diff --git a/eng/Packages.Data.props b/eng/Packages.Data.props index d4c80facc4c71..ac1cdfd2abb87 100644 --- a/eng/Packages.Data.props +++ b/eng/Packages.Data.props @@ -128,7 +128,7 @@ - + @@ -158,6 +158,7 @@ + diff --git a/sdk/cloudmachine/Azure.CloudMachine/src/CloudMachineWorkspace.cs b/sdk/cloudmachine/Azure.CloudMachine/src/CloudMachineWorkspace.cs index ee7d352493d6c..a0f8ecf1acb61 100644 --- a/sdk/cloudmachine/Azure.CloudMachine/src/CloudMachineWorkspace.cs +++ b/sdk/cloudmachine/Azure.CloudMachine/src/CloudMachineWorkspace.cs @@ -18,10 +18,7 @@ namespace Azure.CloudMachine; /// public class CloudMachineWorkspace : ClientWorkspace { - private TokenCredential Credential { get; } = new ChainedTokenCredential( - new AzureCliCredential(), - new AzureDeveloperCliCredential() - ); + private TokenCredential Credential { get; } /// /// The cloud machine ID. @@ -42,20 +39,21 @@ public CloudMachineWorkspace(TokenCredential credential = default, IConfiguratio { Credential = credential; } - - string cmid; - if (configuration == default) - { - cmid = ReadOrCreateCmid(); - } else { - cmid = configuration["CloudMachine:ID"]; - if (cmid == null) - throw new Exception("CloudMachine:ID configuration value missing"); + // This environment variable is set by the CloudMachine App Service feature during provisioning. + Credential = Environment.GetEnvironmentVariable("CLOUDMACHINE_MANAGED_IDENTITY_CLIENT_ID") switch + { + string clientId when !string.IsNullOrEmpty(clientId) => new ManagedIdentityCredential(clientId), + _ => new ChainedTokenCredential(new AzureCliCredential(), new AzureDeveloperCliCredential()) + }; } - Id = cmid!; + Id = configuration switch + { + null => ReadOrCreateCmid(), + _ => configuration["CloudMachine:ID"] ?? throw new Exception("CloudMachine:ID configuration value missing") + }; } /// @@ -69,7 +67,8 @@ public CloudMachineWorkspace(TokenCredential credential = default, IConfiguratio public override ClientConnectionOptions GetConnectionOptions(Type clientType, string instanceId) { string clientId = clientType.FullName; - if (instanceId != null && instanceId.StartsWith("$")) clientId = $"{clientType.FullName}{instanceId}"; + if (instanceId != null && instanceId.StartsWith("$")) + clientId = $"{clientType.FullName}{instanceId}"; switch (clientId) { @@ -78,13 +77,13 @@ public override ClientConnectionOptions GetConnectionOptions(Type clientType, st case "Azure.Messaging.ServiceBus.ServiceBusClient": return new ClientConnectionOptions(new($"https://{Id}.servicebus.windows.net"), Credential); case "Azure.Messaging.ServiceBus.ServiceBusSender": - return new ClientConnectionOptions(instanceId?? "cm_servicebus_default_topic"); + return new ClientConnectionOptions(instanceId ?? "cm_servicebus_default_topic"); case "Azure.Messaging.ServiceBus.ServiceBusProcessor": return new ClientConnectionOptions("cm_servicebus_default_topic/cm_servicebus_subscription_default"); case "Azure.Messaging.ServiceBus.ServiceBusProcessor$private": return new ClientConnectionOptions("cm_servicebus_topic_private/cm_servicebus_subscription_private"); case "Azure.Storage.Blobs.BlobContainerClient": - return new ClientConnectionOptions(new($"https://{Id}.blob.core.windows.net/{instanceId??"default"}"), Credential); + return new ClientConnectionOptions(new($"https://{Id}.blob.core.windows.net/{instanceId ?? "default"}"), Credential); case "Azure.AI.OpenAI.AzureOpenAIClient": return new ClientConnectionOptions(new($"https://{Id}.openai.azure.com"), Credential); case "OpenAI.Chat.ChatClient": diff --git a/sdk/provisioning/Azure.Provisioning.CloudMachine/api/Azure.Provisioning.CloudMachine.net8.0.cs b/sdk/provisioning/Azure.Provisioning.CloudMachine/api/Azure.Provisioning.CloudMachine.net8.0.cs index 02b7cef4a0ba6..0bd82024d3767 100644 --- a/sdk/provisioning/Azure.Provisioning.CloudMachine/api/Azure.Provisioning.CloudMachine.net8.0.cs +++ b/sdk/provisioning/Azure.Provisioning.CloudMachine/api/Azure.Provisioning.CloudMachine.net8.0.cs @@ -46,6 +46,15 @@ public FeatureCollection() { } public System.Collections.Generic.IEnumerable FindAll() where T : Azure.Provisioning.CloudMachine.CloudMachineFeature { throw null; } } } +namespace Azure.CloudMachine.AppService +{ + public partial class AppServiceFeature : Azure.Provisioning.CloudMachine.CloudMachineFeature + { + public AppServiceFeature(Azure.Provisioning.AppService.AppServiceSkuDescription? sku = null) { } + public Azure.Provisioning.AppService.AppServiceSkuDescription Sku { get { throw null; } set { } } + protected override Azure.Provisioning.Primitives.ProvisionableResource EmitCore(Azure.CloudMachine.CloudMachineInfrastructure infrastructure) { throw null; } + } +} namespace Azure.CloudMachine.KeyVault { public partial class KeyVaultFeature : Azure.Provisioning.CloudMachine.CloudMachineFeature diff --git a/sdk/provisioning/Azure.Provisioning.CloudMachine/api/Azure.Provisioning.CloudMachine.netstandard2.0.cs b/sdk/provisioning/Azure.Provisioning.CloudMachine/api/Azure.Provisioning.CloudMachine.netstandard2.0.cs index 02b7cef4a0ba6..0bd82024d3767 100644 --- a/sdk/provisioning/Azure.Provisioning.CloudMachine/api/Azure.Provisioning.CloudMachine.netstandard2.0.cs +++ b/sdk/provisioning/Azure.Provisioning.CloudMachine/api/Azure.Provisioning.CloudMachine.netstandard2.0.cs @@ -46,6 +46,15 @@ public FeatureCollection() { } public System.Collections.Generic.IEnumerable FindAll() where T : Azure.Provisioning.CloudMachine.CloudMachineFeature { throw null; } } } +namespace Azure.CloudMachine.AppService +{ + public partial class AppServiceFeature : Azure.Provisioning.CloudMachine.CloudMachineFeature + { + public AppServiceFeature(Azure.Provisioning.AppService.AppServiceSkuDescription? sku = null) { } + public Azure.Provisioning.AppService.AppServiceSkuDescription Sku { get { throw null; } set { } } + protected override Azure.Provisioning.Primitives.ProvisionableResource EmitCore(Azure.CloudMachine.CloudMachineInfrastructure infrastructure) { throw null; } + } +} namespace Azure.CloudMachine.KeyVault { public partial class KeyVaultFeature : Azure.Provisioning.CloudMachine.CloudMachineFeature diff --git a/sdk/provisioning/Azure.Provisioning.CloudMachine/src/Azure.Provisioning.CloudMachine.csproj b/sdk/provisioning/Azure.Provisioning.CloudMachine/src/Azure.Provisioning.CloudMachine.csproj index 791ca221494cb..ff39f04075f17 100644 --- a/sdk/provisioning/Azure.Provisioning.CloudMachine/src/Azure.Provisioning.CloudMachine.csproj +++ b/sdk/provisioning/Azure.Provisioning.CloudMachine/src/Azure.Provisioning.CloudMachine.csproj @@ -18,6 +18,7 @@ + diff --git a/sdk/provisioning/Azure.Provisioning.CloudMachine/src/AzureSdkExtensions/AppServiceFeature.cs b/sdk/provisioning/Azure.Provisioning.CloudMachine/src/AzureSdkExtensions/AppServiceFeature.cs new file mode 100644 index 0000000000000..4b72fc998eeaa --- /dev/null +++ b/sdk/provisioning/Azure.Provisioning.CloudMachine/src/AzureSdkExtensions/AppServiceFeature.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Provisioning.CloudMachine; +using Azure.Provisioning.Expressions; +using Azure.Provisioning.AppService; +using Azure.Provisioning.Primitives; +using Azure.Provisioning.Resources; + +namespace Azure.CloudMachine.AppService; + +public class AppServiceFeature : CloudMachineFeature +{ + public AppServiceSkuDescription Sku { get; set; } + + public AppServiceFeature(AppServiceSkuDescription? sku = default) + { + if (sku == default) + { + sku = new AppServiceSkuDescription { Tier = "Free", Name = "F1" }; + } + Sku = sku; + } + + protected override ProvisionableResource EmitCore(CloudMachineInfrastructure infrastructure) + { + //Add a App Service to the CloudMachine infrastructure. + AppServicePlan hostingPlan = new("cm_hosting_plan") + { + Name = infrastructure.Id, + Sku = Sku, + Kind = "app" + }; + infrastructure.AddResource(hostingPlan); + + WebSite appService = new("cm_website") + { + Name = infrastructure.Id, + Kind = "app", + Tags = { { "azd-service-name", infrastructure.Id } }, + Identity = new() + { + ManagedServiceIdentityType = ManagedServiceIdentityType.UserAssigned, + UserAssignedIdentities = { { BicepFunction.Interpolate($"{infrastructure.Identity.Id}").Compile().ToString(), new UserAssignedIdentityDetails() } } + }, + AppServicePlanId = hostingPlan.Id, + IsHttpsOnly = true, + IsEnabled = true, + SiteConfig = new() + { + IsHttp20Enabled = true, + MinTlsVersion = AppServiceSupportedTlsVersion.Tls1_2, + IsWebSocketsEnabled = true, + AppSettings = new() + { + // This is used by the CloudMachineWorkspace to detect that it is running in a deployed App Service. + // The ClientId is used to create a ManagedIdentityCredential so that it wires up to our CloudMachine user-assigned identity. + new AppServiceNameValuePair + { + Name = "CLOUDMACHINE_MANAGED_IDENTITY_CLIENT_ID", + Value = infrastructure.Identity.ClientId + }, + } + } + }; + infrastructure.AddResource(appService); + + return appService; + } +} diff --git a/sdk/provisioning/Azure.Provisioning.CloudMachine/src/AzureSdkExtensions/OpenAIFeature.cs b/sdk/provisioning/Azure.Provisioning.CloudMachine/src/AzureSdkExtensions/OpenAIFeature.cs index e5ca6413d0418..2b925548b9562 100644 --- a/sdk/provisioning/Azure.Provisioning.CloudMachine/src/AzureSdkExtensions/OpenAIFeature.cs +++ b/sdk/provisioning/Azure.Provisioning.CloudMachine/src/AzureSdkExtensions/OpenAIFeature.cs @@ -27,6 +27,13 @@ protected override ProvisionableResource EmitCore(CloudMachineInfrastructure clo cloudMachine.PrincipalIdParameter) ); + cloudMachine.AddResource(cloudMachine.CreateRoleAssignment( + cognitiveServices, + cognitiveServices.Id, + CognitiveServicesBuiltInRole.CognitiveServicesOpenAIContributor, + cloudMachine.Identity) + ); + Emitted = cognitiveServices; OpenAIModel? previous = null; diff --git a/sdk/provisioning/Azure.Provisioning.CloudMachine/src/CDKLevel3/CloudMachineInfrastructure.cs b/sdk/provisioning/Azure.Provisioning.CloudMachine/src/CDKLevel3/CloudMachineInfrastructure.cs index a442a905b2e5b..e883d9defd022 100644 --- a/sdk/provisioning/Azure.Provisioning.CloudMachine/src/CDKLevel3/CloudMachineInfrastructure.cs +++ b/sdk/provisioning/Azure.Provisioning.CloudMachine/src/CDKLevel3/CloudMachineInfrastructure.cs @@ -13,6 +13,8 @@ using System.Collections.Generic; using Azure.Provisioning; using Azure.Provisioning.CloudMachine; +using Azure.Core; +using System.Runtime.CompilerServices; namespace Azure.CloudMachine; @@ -221,7 +223,8 @@ public void AddFeature(CloudMachineFeature feature) public void AddEndpoints() { Type endpointsType = typeof(T); - if (!endpointsType.IsInterface) throw new InvalidOperationException("Endpoints type must be an interface."); + if (!endpointsType.IsInterface) + throw new InvalidOperationException("Endpoints type must be an interface."); Endpoints.Add(endpointsType); } @@ -242,33 +245,35 @@ public ProvisioningPlan Build(ProvisioningBuildOptions? context = null) //Add(PrincipalTypeParameter); //Add(PrincipalNameParameter); + var storageBlobDataContributor = StorageBuiltInRole.StorageBlobDataContributor; + var storageTableDataContributor = StorageBuiltInRole.StorageTableDataContributor; + var azureServiceBusDataSender = ServiceBusBuiltInRole.AzureServiceBusDataSender; + var azureServiceBusDataOwner = ServiceBusBuiltInRole.AzureServiceBusDataOwner; + _infrastructure.Add(Identity); _infrastructure.Add(_storage); - _infrastructure.Add(_storage.CreateRoleAssignment(StorageBuiltInRole.StorageBlobDataContributor, RoleManagementPrincipalType.User, PrincipalIdParameter)); - _infrastructure.Add(_storage.CreateRoleAssignment(StorageBuiltInRole.StorageTableDataContributor, RoleManagementPrincipalType.User, PrincipalIdParameter)); + _infrastructure.Add(_storage.CreateRoleAssignment(storageBlobDataContributor, RoleManagementPrincipalType.User, PrincipalIdParameter)); + _infrastructure.Add(CreateRoleAssignment(_storage, _storage.Id, storageBlobDataContributor, Identity)); + _infrastructure.Add(_storage.CreateRoleAssignment(storageTableDataContributor, RoleManagementPrincipalType.User, PrincipalIdParameter)); + _infrastructure.Add(CreateRoleAssignment(_storage, _storage.Id, storageTableDataContributor, Identity)); _infrastructure.Add(_container); _infrastructure.Add(_blobs); _infrastructure.Add(_serviceBusNamespace); - _infrastructure.Add(_serviceBusNamespace.CreateRoleAssignment(ServiceBusBuiltInRole.AzureServiceBusDataOwner, RoleManagementPrincipalType.User, PrincipalIdParameter)); + _infrastructure.Add(_serviceBusNamespace.CreateRoleAssignment(azureServiceBusDataOwner, RoleManagementPrincipalType.User, PrincipalIdParameter)); + _infrastructure.Add(CreateRoleAssignment(_serviceBusNamespace,_serviceBusNamespace.Id, azureServiceBusDataOwner, Identity)); _infrastructure.Add(_serviceBusNamespaceAuthorizationRule); _infrastructure.Add(_serviceBusTopic_private); _infrastructure.Add(_serviceBusTopic_default); _infrastructure.Add(_serviceBusSubscription_private); _infrastructure.Add(_serviceBusSubscription_default); - // This is necessary until SystemTopic adds an AssignRole method. - var role = ServiceBusBuiltInRole.AzureServiceBusDataSender; - RoleAssignment roleAssignment = new RoleAssignment("cm_servicebus_role"); - roleAssignment.Name = BicepFunction.CreateGuid(_serviceBusNamespace.Id, Identity.Id, BicepFunction.GetSubscriptionResourceId("Microsoft.Authorization/roleDefinitions", role.ToString())); - roleAssignment.Scope = new IdentifierExpression(_serviceBusNamespace.BicepIdentifier); - roleAssignment.PrincipalType = RoleManagementPrincipalType.ServicePrincipal; - roleAssignment.RoleDefinitionId = BicepFunction.GetSubscriptionResourceId("Microsoft.Authorization/roleDefinitions", role.ToString()); - roleAssignment.PrincipalId = Identity.PrincipalId; + RoleAssignment roleAssignment = CreateRoleAssignment(_serviceBusNamespace, _serviceBusNamespace.Id, azureServiceBusDataSender, Identity); _infrastructure.Add(roleAssignment); + + CreateRoleAssignment(_serviceBusNamespace, _serviceBusNamespace.Id, azureServiceBusDataSender, Identity); // the role assignment must exist before the system topic event subscription is created. _eventGridSubscription_blobs.DependsOn.Add(roleAssignment); _infrastructure.Add(_eventGridSubscription_blobs); - _infrastructure.Add(_eventGridTopic_blobs); // Placeholders for now. @@ -283,4 +288,21 @@ public ProvisioningPlan Build(ProvisioningBuildOptions? context = null) return _infrastructure.Build(context); } + + // Temporary until the bug is fixed in the CDK generator which uses the PrincipalId instead of the Id in BicepFunction.CreateGuid. + internal RoleAssignment CreateRoleAssignment(ProvisionableResource resource, BicepValue Id, object role, UserAssignedIdentity identity) + { + if (role is null) throw new ArgumentException("Role must not be null.", nameof(role)); + var method = role.GetType().GetMethod("GetBuiltInRoleName", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public); + string roleName = (string)method!.Invoke(null, [role])!; + + return new($"{resource.BicepIdentifier}_{identity.BicepIdentifier}_{roleName}") + { + Name = BicepFunction.CreateGuid(Id, identity.Id, BicepFunction.GetSubscriptionResourceId("Microsoft.Authorization/roleDefinitions", role!.ToString()!)), + Scope = new IdentifierExpression(resource.BicepIdentifier), + PrincipalType = RoleManagementPrincipalType.ServicePrincipal, + RoleDefinitionId = BicepFunction.GetSubscriptionResourceId("Microsoft.Authorization/roleDefinitions", role.ToString()!), + PrincipalId = identity.PrincipalId + }; + } } diff --git a/sdk/provisioning/Azure.Provisioning.CloudMachine/tests/CloudMachineTests.cs b/sdk/provisioning/Azure.Provisioning.CloudMachine/tests/CloudMachineTests.cs index ee4fc0edfe1d8..cd87f20d7fe99 100644 --- a/sdk/provisioning/Azure.Provisioning.CloudMachine/tests/CloudMachineTests.cs +++ b/sdk/provisioning/Azure.Provisioning.CloudMachine/tests/CloudMachineTests.cs @@ -3,6 +3,7 @@ #nullable enable +using Azure.CloudMachine.AppService; using Azure.CloudMachine.KeyVault; using Azure.CloudMachine.OpenAI; using NUnit.Framework; @@ -19,6 +20,7 @@ public void GenerateBicep() infrastructure.AddFeature(new KeyVaultFeature()); infrastructure.AddFeature(new OpenAIModel("gpt-35-turbo", "0125")); infrastructure.AddFeature(new OpenAIModel("text-embedding-ada-002", "2", AIModelKind.Embedding)); + infrastructure.AddFeature(new AppServiceFeature()); }, exitProcessIfHandled:false); }