diff --git a/infra/main.bicep b/infra/main.bicep index 1fb0f6a5b..30ec8b466 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -388,6 +388,19 @@ module userAssignedIdentity 'br/public:avm/res/managed-identity/user-assigned-id } } +// ========== SQL Operations User Assigned Identity ========== // +// Dedicated identity for backend SQL operations with limited permissions (db_datareader, db_datawriter) +var sqlUserAssignedIdentityResourceName = 'id-sql-${solutionSuffix}' +module sqlUserAssignedIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.4.1' = { + name: take('avm.res.managed-identity.user-assigned-identity.${sqlUserAssignedIdentityResourceName}', 64) + params: { + name: sqlUserAssignedIdentityResourceName + location: solutionLocation + tags: tags + enableTelemetry: enableTelemetry + } +} + // ========== Network Module ========== // module network 'modules/network.bicep' = if (enablePrivateNetworking) { name: take('network-${solutionSuffix}-deployment', 64) @@ -450,7 +463,7 @@ var aiRelatedDnsZoneIndices = [ @batchSize(5) module avmPrivateDnsZones 'br/public:avm/res/network/private-dns-zone:0.7.1' = [ for (zone, i) in privateDnsZones: if (enablePrivateNetworking && (empty(existingFoundryProjectResourceId) || !contains(aiRelatedDnsZoneIndices, i))) { - name: 'dns-zone-${i}' + name: 'avm.res.network.private-dns-zone.${split(zone, '.')[1]}' params: { name: zone tags: tags @@ -509,6 +522,11 @@ module keyvault 'br/public:avm/res/key-vault/vault:0.12.1' = { principalType: 'ServicePrincipal' roleDefinitionIdOrName: 'Key Vault Administrator' } + { + principalId: sqlUserAssignedIdentity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: 'Key Vault Secrets User' + } ] secrets: [ { @@ -896,7 +914,11 @@ module sqlDBModule 'br/public:avm/res/sql/server:0.20.1' = { connectionPolicy: 'Redirect' databases: [ { - availabilityZone: enableRedundancy ? 1 : -1 + zoneRedundant: enableRedundancy ? true : false + // When enableRedundancy is true (zoneRedundant=true), set availabilityZone to -1 + // to let Azure automatically manage zone placement across multiple zones. + // When enableRedundancy is false, also use -1 (no specific zone assignment). + availabilityZone: -1 collation: 'SQL_Latin1_General_CP1_CI_AS' diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] @@ -988,7 +1010,7 @@ module webSite 'modules/web-sites.bicep' = { name: webSiteResourceName tags: tags location: solutionLocation - managedIdentities: { userAssignedResourceIds: [userAssignedIdentity!.outputs.resourceId] } + managedIdentities: { userAssignedResourceIds: [userAssignedIdentity!.outputs.resourceId, sqlUserAssignedIdentity!.outputs.resourceId] } kind: 'app,linux,container' serverFarmResourceId: webServerFarm.?outputs.resourceId siteConfig: { @@ -1035,7 +1057,7 @@ module webSite 'modules/web-sites.bicep' = { AZURE_COSMOSDB_CONVERSATIONS_CONTAINER: collectionName AZURE_COSMOSDB_DATABASE: cosmosDbDatabaseName AZURE_COSMOSDB_ENABLE_FEEDBACK: azureCosmosDbEnableFeedback - SQLDB_USER_MID: userAssignedIdentity.outputs.clientId + SQLDB_USER_MID: sqlUserAssignedIdentity.outputs.clientId AZURE_AI_SEARCH_ENDPOINT: 'https://${aiSearchName}.search.windows.net' AZURE_SQL_SYSTEM_PROMPT: functionAppSqlPrompt AZURE_CALL_TRANSCRIPT_SYSTEM_PROMPT: functionAppCallTranscriptSystemPrompt @@ -1226,6 +1248,12 @@ output MANAGEDIDENTITY_WEBAPP_NAME string = userAssignedIdentity.outputs.name @description('Client ID of the managed identity used by the web app.') output MANAGEDIDENTITY_WEBAPP_CLIENTID string = userAssignedIdentity.outputs.clientId + +@description('Name of the managed identity used for SQL database operations.') +output MANAGEDIDENTITY_SQL_NAME string = sqlUserAssignedIdentity.outputs.name + +@description('Client ID of the managed identity used for SQL database operations.') +output MANAGEDIDENTITY_SQL_CLIENTID string = sqlUserAssignedIdentity.outputs.clientId @description('Name of the AI Search service.') output AI_SEARCH_SERVICE_NAME string = aiSearchName @@ -1367,3 +1395,6 @@ output USE_AI_PROJECT_CLIENT string = useAIProjectClientFlag @description('Indicates whether the internal stream should be used.') output USE_INTERNAL_STREAM string = useInternalStream + +@description('The client ID of the managed identity.') +output AZURE_CLIENT_ID string = userAssignedIdentity.outputs.clientId diff --git a/infra/scripts/process_sample_data.sh b/infra/scripts/process_sample_data.sh index c0b9f8a34..d59a24662 100644 --- a/infra/scripts/process_sample_data.sh +++ b/infra/scripts/process_sample_data.sh @@ -8,8 +8,8 @@ keyvaultName="$5" sqlServerName="$6" SqlDatabaseName="$7" - webAppManagedIdentityClientId="$8" - webAppManagedIdentityDisplayName="$9" + sqlManagedIdentityClientId="$8" + sqlManagedIdentityDisplayName="$9" aiSearchName="${10}" aif_resource_id="${11}" @@ -316,12 +316,14 @@ SqlDatabaseName=$(azd env get-value SQLDB_DATABASE) fi - if [ -z "$webAppManagedIdentityClientId" ]; then - webAppManagedIdentityClientId=$(azd env get-value MANAGEDIDENTITY_WEBAPP_CLIENTID) + if [ -z "$sqlManagedIdentityClientId" ]; then + # Use the SQL-specific managed identity for database operations with limited permissions + sqlManagedIdentityClientId=$(azd env get-value MANAGEDIDENTITY_SQL_CLIENTID) fi - if [ -z "$webAppManagedIdentityDisplayName" ]; then - webAppManagedIdentityDisplayName=$(azd env get-value MANAGEDIDENTITY_WEBAPP_NAME) + if [ -z "$sqlManagedIdentityDisplayName" ]; then + # Use the SQL-specific managed identity for database operations with limited permissions + sqlManagedIdentityDisplayName=$(azd env get-value MANAGEDIDENTITY_SQL_NAME) fi if [ -z "$aiSearchName" ]; then @@ -335,8 +337,8 @@ azSubscriptionId=$(azd env get-value AZURE_SUBSCRIPTION_ID) # Check if all required arguments are provided - if [ -z "$resourceGroupName" ] || [ -z "$cosmosDbAccountName" ] || [ -z "$storageAccount" ] || [ -z "$fileSystem" ] || [ -z "$keyvaultName" ] || [ -z "$sqlServerName" ] || [ -z "$SqlDatabaseName" ] || [ -z "$webAppManagedIdentityClientId" ] || [ -z "$webAppManagedIdentityDisplayName" ] || [ -z "$aiSearchName" ] || [ -z "$aif_resource_id" ]; then - echo "Usage: $0 " + if [ -z "$resourceGroupName" ] || [ -z "$cosmosDbAccountName" ] || [ -z "$storageAccount" ] || [ -z "$fileSystem" ] || [ -z "$keyvaultName" ] || [ -z "$sqlServerName" ] || [ -z "$SqlDatabaseName" ] || [ -z "$sqlManagedIdentityClientId" ] || [ -z "$sqlManagedIdentityDisplayName" ] || [ -z "$aiSearchName" ] || [ -z "$aif_resource_id" ]; then + echo "Usage: $0 " exit 1 fi @@ -437,8 +439,8 @@ # Call create_sql_user_and_role.sh echo "Running create_sql_user_and_role.sh" bash infra/scripts/add_user_scripts/create_sql_user_and_role.sh "$sqlServerName.database.windows.net" "$SqlDatabaseName" '[ - {"clientId":"'"$webAppManagedIdentityClientId"'", "displayName":"'"$webAppManagedIdentityDisplayName"'", "role":"db_datareader"}, - {"clientId":"'"$webAppManagedIdentityClientId"'", "displayName":"'"$webAppManagedIdentityDisplayName"'", "role":"db_datawriter"} + {"clientId":"'"$sqlManagedIdentityClientId"'", "displayName":"'"$sqlManagedIdentityDisplayName"'", "role":"db_datareader"}, + {"clientId":"'"$sqlManagedIdentityClientId"'", "displayName":"'"$sqlManagedIdentityDisplayName"'", "role":"db_datawriter"} ]' if [ $? -ne 0 ]; then echo "Error: create_sql_user_and_role.sh failed." diff --git a/src/App/backend/common/config.py b/src/App/backend/common/config.py index 06073a427..f841fb43d 100644 --- a/src/App/backend/common/config.py +++ b/src/App/backend/common/config.py @@ -142,7 +142,8 @@ def __init__(self): self.SQL_USERNAME = os.getenv("SQLDB_USERNAME") self.SQL_PASSWORD = os.getenv("SQLDB_PASSWORD") self.ODBC_DRIVER = "{ODBC Driver 18 for SQL Server}" - self.MID_ID = os.getenv("SQLDB_USER_MID") + self.MID_ID = os.getenv("AZURE_CLIENT_ID") + self.SQL_MID_ID = os.getenv("SQLDB_USER_MID") # System Prompts self.SQL_SYSTEM_PROMPT = os.environ.get("AZURE_SQL_SYSTEM_PROMPT") diff --git a/src/App/backend/services/sqldb_service.py b/src/App/backend/services/sqldb_service.py index 063c08bd1..1e003616a 100644 --- a/src/App/backend/services/sqldb_service.py +++ b/src/App/backend/services/sqldb_service.py @@ -17,7 +17,7 @@ database = config.SQL_DATABASE username = config.SQL_USERNAME password = config.SQL_PASSWORD -mid_id = config.MID_ID +mid_id = config.SQL_MID_ID def dict_cursor(cursor):