diff --git a/sdk/identity/Azure.Identity/assets.json b/sdk/identity/Azure.Identity/assets.json index ea01c6b3b2d85..a93ca15ca276f 100644 --- a/sdk/identity/Azure.Identity/assets.json +++ b/sdk/identity/Azure.Identity/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "net", "TagPrefix": "net/identity/Azure.Identity", - "Tag": "net/identity/Azure.Identity_7f050cb3f3" + "Tag": "net/identity/Azure.Identity_f0e02fe424" } diff --git a/sdk/identity/Azure.Identity/integration/Integration.Identity.Common/Integration.Identity.Common.csproj b/sdk/identity/Azure.Identity/integration/Integration.Identity.Common/Integration.Identity.Common.csproj new file mode 100644 index 0000000000000..3dc20577717f2 --- /dev/null +++ b/sdk/identity/Azure.Identity/integration/Integration.Identity.Common/Integration.Identity.Common.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + enable + enable + + + + + + + + diff --git a/sdk/identity/Azure.Identity/integration/Integration.Identity.Common/ManagedIdentityTests.cs b/sdk/identity/Azure.Identity/integration/Integration.Identity.Common/ManagedIdentityTests.cs new file mode 100644 index 0000000000000..45ef41859932b --- /dev/null +++ b/sdk/identity/Azure.Identity/integration/Integration.Identity.Common/ManagedIdentityTests.cs @@ -0,0 +1,22 @@ +using Azure.Identity; +using Azure.Storage.Blobs; +using Azure.Core; + +namespace Integration.Identity.Common; + +public static class ManagedIdentityTests +{ + public static void AuthToStorage() + { + string resourceId = Environment.GetEnvironmentVariable("IDENTITY_WEBAPP_USER_DEFINED_IDENTITY")!; + string account1 = Environment.GetEnvironmentVariable("IDENTITY_STORAGE_NAME_1")!; + string account2 = Environment.GetEnvironmentVariable("IDENTITY_STORAGE_NAME_2")!; + + var credential1 = new ManagedIdentityCredential(); + var credential2 = new ManagedIdentityCredential(new ResourceIdentifier(resourceId)); + var client1 = new BlobServiceClient(new Uri($"https://{account1}.blob.core.windows.net/"), credential1); + var client2 = new BlobServiceClient(new Uri($"https://{account2}.blob.core.windows.net/"), credential2); + client1.GetBlobContainers().ToList(); + client2.GetBlobContainers().ToList(); + } +} diff --git a/sdk/identity/Azure.Identity/integration/Integration.Identity.Func/Function1.cs b/sdk/identity/Azure.Identity/integration/Integration.Identity.Func/Function1.cs new file mode 100644 index 0000000000000..a28f57ccbde1e --- /dev/null +++ b/sdk/identity/Azure.Identity/integration/Integration.Identity.Func/Function1.cs @@ -0,0 +1,34 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs; +using Microsoft.Azure.WebJobs.Extensions.Http; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Integration.Identity.Common; + +namespace Integration.Identity.Func +{ + public static class Function1 + { + [FunctionName("Function1")] + public static async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, + ILogger log) + { + log.LogInformation("C# HTTP trigger function processed a request."); + + try + { + ManagedIdentityTests.AuthToStorage(); + return new OkObjectResult("Successfully acquired a token from ManagedIdentityCredential"); + } + catch (Exception ex) + { + return new BadRequestObjectResult(ex.ToString()); + } + } + } +} diff --git a/sdk/identity/Azure.Identity/integration/Integration.Identity.Func/Integration.Identity.Func.csproj b/sdk/identity/Azure.Identity/integration/Integration.Identity.Func/Integration.Identity.Func.csproj new file mode 100644 index 0000000000000..822d40d898bcf --- /dev/null +++ b/sdk/identity/Azure.Identity/integration/Integration.Identity.Func/Integration.Identity.Func.csproj @@ -0,0 +1,21 @@ + + + net6.0 + v4 + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + + + + diff --git a/sdk/identity/Azure.Identity/integration/Integration.Identity.Func/Properties/serviceDependencies.json b/sdk/identity/Azure.Identity/integration/Integration.Identity.Func/Properties/serviceDependencies.json new file mode 100644 index 0000000000000..c264e8ca80b19 --- /dev/null +++ b/sdk/identity/Azure.Identity/integration/Integration.Identity.Func/Properties/serviceDependencies.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "appInsights1": { + "type": "appInsights" + } + } +} \ No newline at end of file diff --git a/sdk/identity/Azure.Identity/integration/Integration.Identity.Func/Properties/serviceDependencies.local.json b/sdk/identity/Azure.Identity/integration/Integration.Identity.Func/Properties/serviceDependencies.local.json new file mode 100644 index 0000000000000..5a956e8592682 --- /dev/null +++ b/sdk/identity/Azure.Identity/integration/Integration.Identity.Func/Properties/serviceDependencies.local.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "appInsights1": { + "type": "appInsights.sdk" + } + } +} \ No newline at end of file diff --git a/sdk/identity/Azure.Identity/integration/Integration.Identity.Func/host.json b/sdk/identity/Azure.Identity/integration/Integration.Identity.Func/host.json new file mode 100644 index 0000000000000..d13723c98ce65 --- /dev/null +++ b/sdk/identity/Azure.Identity/integration/Integration.Identity.Func/host.json @@ -0,0 +1,10 @@ +{ + "version": "2.0", + "logging": { + "fileLoggingMode": "always", + "logLevel": { + "Function.MyFunction": "Information", + "default": "None" + } + } +} diff --git a/sdk/identity/Azure.Identity/integration/Integration.Identity.Func/local.settings.json b/sdk/identity/Azure.Identity/integration/Integration.Identity.Func/local.settings.json new file mode 100644 index 0000000000000..bf70960ee259c --- /dev/null +++ b/sdk/identity/Azure.Identity/integration/Integration.Identity.Func/local.settings.json @@ -0,0 +1,7 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "", + "FUNCTIONS_WORKER_RUNTIME": "dotnet" + } +} \ No newline at end of file diff --git a/sdk/identity/Azure.Identity/integration/WebApp/Controllers/TestController.cs b/sdk/identity/Azure.Identity/integration/WebApp/Controllers/TestController.cs index 0e792196f251a..470bee3938dee 100644 --- a/sdk/identity/Azure.Identity/integration/WebApp/Controllers/TestController.cs +++ b/sdk/identity/Azure.Identity/integration/WebApp/Controllers/TestController.cs @@ -4,6 +4,7 @@ using Azure.Identity; using Azure.Storage.Blobs; using Microsoft.AspNetCore.Mvc; +using Integration.Identity.Common; namespace WebApp.Controllers { @@ -16,18 +17,9 @@ public class TestController : ControllerBase [HttpGet(Name = "GetTest")] public IActionResult Get() { - string resourceId = Environment.GetEnvironmentVariable("IDENTITY_WEBAPP_USER_DEFINED_IDENTITY")!; - string account1 = Environment.GetEnvironmentVariable("IDENTITY_STORAGE_NAME_1")!; - string account2 = Environment.GetEnvironmentVariable("IDENTITY_STORAGE_NAME_2")!; - - var credential1 = new ManagedIdentityCredential(); - var credential2 = new ManagedIdentityCredential(new ResourceIdentifier(resourceId)); - var client1 = new BlobServiceClient(new Uri($"https://{account1}.blob.core.windows.net/"), credential1); - var client2 = new BlobServiceClient(new Uri($"https://{account2}.blob.core.windows.net/"), credential2); try { - var results = client1.GetBlobContainers().ToList(); - results = client2.GetBlobContainers().ToList(); + ManagedIdentityTests.AuthToStorage(); return Ok("Successfully acquired a token from ManagedIdentityCredential"); } catch (Exception ex) diff --git a/sdk/identity/Azure.Identity/integration/WebApp/Integration.Identity.WebApp.csproj b/sdk/identity/Azure.Identity/integration/WebApp/Integration.Identity.WebApp.csproj index 8460903024cfe..9a7894b5d4ba9 100644 --- a/sdk/identity/Azure.Identity/integration/WebApp/Integration.Identity.WebApp.csproj +++ b/sdk/identity/Azure.Identity/integration/WebApp/Integration.Identity.WebApp.csproj @@ -10,7 +10,7 @@ - + diff --git a/sdk/identity/Azure.Identity/integration/nuget.config b/sdk/identity/Azure.Identity/integration/nuget.config deleted file mode 100644 index 9ac17c067abd9..0000000000000 --- a/sdk/identity/Azure.Identity/integration/nuget.config +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - diff --git a/sdk/identity/Azure.Identity/tests/IdentityTestEnvironment.cs b/sdk/identity/Azure.Identity/tests/IdentityTestEnvironment.cs index a760018dc9a4d..472b8d9e529d0 100644 --- a/sdk/identity/Azure.Identity/tests/IdentityTestEnvironment.cs +++ b/sdk/identity/Azure.Identity/tests/IdentityTestEnvironment.cs @@ -39,5 +39,6 @@ public class IdentityTestEnvironment : TestEnvironment public string ServicePrincipalCertificatePemPath => GetOptionalVariable("IDENTITY_SP_CERT_PEM") ?? Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "cert.pem"); public string ServicePrincipalSniCertificatePath => GetOptionalVariable("IDENTITY_SP_CERT_SNI") ?? Path.Combine(TestContext.CurrentContext.TestDirectory, "Data", "cert.pfx"); public string IdentityTestWebName => GetRecordedVariable("IDENTITY_WEBAPP_NAME"); + public string IdentityTestAzFuncName => GetRecordedVariable("IDENTITY_FUNCTION_NAME"); } } diff --git a/sdk/identity/Azure.Identity/tests/ManagedIdentityCredentialWebAppTests.cs b/sdk/identity/Azure.Identity/tests/ManagedIdentityCredentialIntegrationTests.cs similarity index 50% rename from sdk/identity/Azure.Identity/tests/ManagedIdentityCredentialWebAppTests.cs rename to sdk/identity/Azure.Identity/tests/ManagedIdentityCredentialIntegrationTests.cs index 8a7d7ed4e3ed3..4fb5562adc23a 100644 --- a/sdk/identity/Azure.Identity/tests/ManagedIdentityCredentialWebAppTests.cs +++ b/sdk/identity/Azure.Identity/tests/ManagedIdentityCredentialIntegrationTests.cs @@ -11,19 +11,17 @@ namespace Azure.Identity.Tests { - public class ManagedIdentityCredentialWebAppTests : IdentityRecordedTestBase + public class ManagedIdentityCredentialIntegrationTests : IdentityRecordedTestBase { private HttpPipeline _pipeline; - private Uri _testEndpoint; - public ManagedIdentityCredentialWebAppTests(bool isAsync) : base(isAsync) + public ManagedIdentityCredentialIntegrationTests(bool isAsync) : base(isAsync) { } [SetUp] public void Setup() { var options = new TokenCredentialOptions(); - _testEndpoint = new Uri($"https://{TestEnvironment.IdentityTestWebName}.azurewebsites.net/test"); _pipeline = HttpPipelineBuilder.Build(InstrumentClientOptions(options), Array.Empty(), Array.Empty(), new ResponseClassifier()); } @@ -31,10 +29,26 @@ public void Setup() { [SyncOnly] // This test leverages the test app found in Azure.Identity\integration\WebApp // It validates that ManagedIdentityCredential can acquire a token in an actual Azure Web App environment - public async Task CallTestWebApp() + public async Task CallIntegrationTestWebApp() { + var testEndpoint = new Uri($"https://{TestEnvironment.IdentityTestWebName}.azurewebsites.net/test"); Request request = _pipeline.CreateRequest(); - request.Uri.Reset(_testEndpoint); + request.Uri.Reset(testEndpoint); + Response response = await _pipeline.SendRequestAsync(request, default); + + Assert.AreEqual((int)HttpStatusCode.OK, response.Status); + Assert.AreEqual("Successfully acquired a token from ManagedIdentityCredential", response.Content.ToString(), response.Content.ToString()); + } + + [RecordedTest] + [SyncOnly] + // This test leverages the test app found in Azure.Identity\integration\Integration.Identity.Func + // It validates that ManagedIdentityCredential can acquire a token in an actual Azure Web App environment + public async Task CallIntegrationTestAzFunction() + { + var testEndpoint = new Uri($"https://{TestEnvironment.IdentityTestAzFuncName}.azurewebsites.net/api/function1"); + Request request = _pipeline.CreateRequest(); + request.Uri.Reset(testEndpoint); Response response = await _pipeline.SendRequestAsync(request, default); Assert.AreEqual((int)HttpStatusCode.OK, response.Status); diff --git a/sdk/identity/test-resources-post.ps1 b/sdk/identity/test-resources-post.ps1 index cc8637d9a4e3e..d9fc316cffbb5 100644 --- a/sdk/identity/test-resources-post.ps1 +++ b/sdk/identity/test-resources-post.ps1 @@ -9,11 +9,21 @@ if ($null -ne $Env:AGENT_WORKFOLDER) { } az login --service-principal -u $DeploymentOutputs['IDENTITY_CLIENT_ID'] -p $DeploymentOutputs['IDENTITY_CLIENT_SECRET'] --tenant $DeploymentOutputs['IDENTITY_TENANT_ID'] az account set --subscription $DeploymentOutputs['IDENTITY_SUBSCRIPTION_ID'] + +# Deploy the webapp dotnet publish "$webappRoot/WebApp/Integration.Identity.WebApp.csproj" -o "$workingFolder/Pub" /p:EnableSourceLink=false Compress-Archive -Path "$workingFolder/Pub/*" -DestinationPath "$workingFolder/Pub/package.zip" -Force az webapp deploy --resource-group $DeploymentOutputs['IDENTITY_RESOURCE_GROUP'] --name $DeploymentOutputs['IDENTITY_WEBAPP_NAME'] --src-path "$workingFolder/Pub/package.zip" + +# clean up Remove-Item -Force -Recurse "$workingFolder/Pub" -if ($null -eq $Env:AGENT_WORKFOLDER) { - Remove-Item -Force -Recurse "$webappRoot/%AGENT_WORKFOLDER%" -} + +# Deploy the function app +dotnet publish "$webappRoot/Integration.Identity.Func/Integration.Identity.Func.csproj" -o "$workingFolder/Pub" /p:EnableSourceLink=false +Compress-Archive -Path "$workingFolder/Pub/*" -DestinationPath "$workingFolder/Pub/package.zip" -Force +az functionapp deployment source config-zip -g $DeploymentOutputs['IDENTITY_RESOURCE_GROUP'] -n $DeploymentOutputs['IDENTITY_FUNCTION_NAME'] --src "$workingFolder/Pub/package.zip" + +# clean up +Remove-Item -Force -Recurse "$workingFolder/Pub" + az logout \ No newline at end of file diff --git a/sdk/identity/test-resources.bicep b/sdk/identity/test-resources.bicep index b175d52a5e5ee..7162aa11b24c8 100644 --- a/sdk/identity/test-resources.bicep +++ b/sdk/identity/test-resources.bicep @@ -9,6 +9,9 @@ param baseName string = resourceGroup().name @description('The location of the resource. By default, this is the same as the resource group.') param location string = resourceGroup().location +param runtime string = 'node' +var functionWorkerRuntime = runtime + //See https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles var blobContributor = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') //Storage Blob Data Contributor var websiteContributor = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'de139f84-1756-47ae-9be6-808fbbe84772') //Website Contributor @@ -28,6 +31,16 @@ resource blobRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { } } +resource blobRoleFunc 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: sa + name: guid(resourceGroup().id, blobContributor, 'azfunc') + properties: { + principalId: azfunc.identity.principalId + roleDefinitionId: blobContributor + principalType: 'ServicePrincipal' + } +} + resource blobRole2 'Microsoft.Authorization/roleAssignments@2022-04-01' = { scope: sa2 name: guid(resourceGroup().id, blobContributor, usermgdid.id) @@ -76,18 +89,76 @@ resource farm 'Microsoft.Web/serverfarms@2021-03-01' = { name: '${baseName}_asp' location: location sku: { - name: 'F1' - tier: 'Free' - size: 'F1' - family: 'F' - capacity: 0 + name: 'B1' + tier: 'Bassic' + size: 'B1' + family: 'B' + capacity: 1 } properties: { } kind: 'app' } +resource azfunc 'Microsoft.Web/sites@2021-03-01' = { + name: '${baseName}func' + location: location + kind: 'functionapp' + identity: { + type: 'SystemAssigned, UserAssigned' + userAssignedIdentities: { + '${usermgdid.id}' : { } + } + } + properties: { + enabled: true + serverFarmId: farm.id + httpsOnly: true + keyVaultReferenceIdentity: 'SystemAssigned' + siteConfig: { + alwaysOn: true + netFrameworkVersion: 'v6.0' + http20Enabled: true + minTlsVersion: '1.2' + appSettings: [ + { + name: 'IDENTITY_STORAGE_NAME_1' + value: sa.name + } + { + name: 'IDENTITY_STORAGE_NAME_2' + value: sa2.name + } + { + name: 'IDENTITY_WEBAPP_USER_DEFINED_IDENTITY' + value: usermgdid.id + } + { + name: 'AzureWebJobsStorage' + value: 'DefaultEndpointsProtocol=https;AccountName=${sa.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${sa.listKeys().keys[0].value}' + } + { + name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING' + value: 'DefaultEndpointsProtocol=https;AccountName=${sa.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${sa.listKeys().keys[0].value}' + } + { + name: 'WEBSITE_CONTENTSHARE' + value: toLower('${baseName}-func') + } + { + name: 'FUNCTIONS_EXTENSION_VERSION' + value: '~4' + } + { + name: 'FUNCTIONS_WORKER_RUNTIME' + value: 'dotnet' + } + ] + } + } +} + resource web 'Microsoft.Web/sites@2021-03-01' = { - name: '${baseName}-webapp' + name: '${baseName}webapp' location: location kind: 'app' identity: { @@ -127,7 +198,26 @@ resource web 'Microsoft.Web/sites@2021-03-01' = { } } +resource scmweb 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@2022-09-01' = { + kind: 'app' + parent: web + name: 'scm' + properties: { + allow: true + } +} + +resource scmfunc 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@2022-09-01' = { + kind: 'functionapp' + parent: azfunc + name: 'scm' + properties: { + allow: true + } +} + output IDENTITY_WEBAPP_NAME string = web.name output IDENTITY_WEBAPP_USER_DEFINED_IDENTITY string = usermgdid.id output IDENTITY_STORAGE_NAME_1 string = sa.name output IDENTITY_STORAGE_NAME_2 string = sa2.name +output IDENTITY_FUNCTION_NAME string = azfunc.name