From 4a83ec05f3aeec5045f395571f15fce8bff2c62b Mon Sep 17 00:00:00 2001 From: SteveCInVA Date: Mon, 24 Nov 2025 16:19:50 +0000 Subject: [PATCH 01/22] Add Cosmos DB post-configuration script and update requirements - Initial POC --- deployers/azure.yaml | 15 ++++++++++++++ deployers/bicep/main.bicep | 1 + deployers/bicep/modules/cosmosDb.bicep | 1 + deployers/bicep/postconfig.py | 28 ++++++++++++++++++++++++++ deployers/bicep/requirements.txt | 3 +++ 5 files changed, 48 insertions(+) create mode 100644 deployers/bicep/postconfig.py create mode 100644 deployers/bicep/requirements.txt diff --git a/deployers/azure.yaml b/deployers/azure.yaml index ae9a6840f..f560dc9d2 100644 --- a/deployers/azure.yaml +++ b/deployers/azure.yaml @@ -5,6 +5,21 @@ infra: provider: bicep path: bicep hooks: + postprovision: + posix: + shell: sh + run: | + # Set up variables + export var_cosmosDb_uri=${var_cosmosDb_uri} + export var_subscriptionId=${AZURE_SUBSCRIPTION_ID} + export var_rgName=${var_rgName} + + # Execute post-configuration script + python3 -m pip install --user -r ./bicep/requirements.txt + #chmod +x ./bicep/postconfig.py + python3 ./bicep/postconfig.py + + predeploy: posix: shell: sh diff --git a/deployers/bicep/main.bicep b/deployers/bicep/main.bicep index a65d0d4c7..36c7371eb 100644 --- a/deployers/bicep/main.bicep +++ b/deployers/bicep/main.bicep @@ -497,3 +497,4 @@ output var_imageName string = contains(imageName, ':') ? split(imageName, ':')[0 output var_imageTag string = split(imageName, ':')[1] output var_specialImage bool = contains(imageName, ':') ? split(imageName, ':')[1] != 'latest' : false output var_webService string = appService.outputs.name +output var_cosmosDb_uri string = cosmosDB.outputs.cosmosDbUri diff --git a/deployers/bicep/modules/cosmosDb.bicep b/deployers/bicep/modules/cosmosDb.bicep index 67af0d4f2..527d93698 100644 --- a/deployers/bicep/modules/cosmosDb.bicep +++ b/deployers/bicep/modules/cosmosDb.bicep @@ -108,3 +108,4 @@ resource cosmosDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-pre } output cosmosDbName string = cosmosDb.name +output cosmosDbUri string = cosmosDb.properties.documentEndpoint diff --git a/deployers/bicep/postconfig.py b/deployers/bicep/postconfig.py new file mode 100644 index 000000000..f7aa7889e --- /dev/null +++ b/deployers/bicep/postconfig.py @@ -0,0 +1,28 @@ +from azure.cosmos import CosmosClient +from azure.identity import DefaultAzureCredential +import os + +credential = DefaultAzureCredential() +token = credential.get_token("https://cosmos.azure.com/.default") + +#endpoint = "https://bicepchat-demo-cosmos.documents.azure.com:443/" +cosmosEndpoint = os.getenv("var_cosmosDb_uri") +client = CosmosClient(cosmosEndpoint, credential=credential) + +database_name = "SimpleChat" +container_name = "settings" + +database = client.get_database_client(database_name) +container = database.get_container_client(container_name) + +# 3. Read the existing item by ID and partition key +item_id = "app_settings" +partition_key = "app_settings" # Use the actual partition key value for this item +item = container.read_item(item=item_id, partition_key=partition_key) + +# 4. Update the property +item["enable_external_healthcheck"] = True + +# 5. Upsert the updated item back into Cosmos DB +response = container.upsert_item(item) +print(f"Updated item: {response['id']} with enable_external_healthcheck = {response['enable_external_healthcheck']}") diff --git a/deployers/bicep/requirements.txt b/deployers/bicep/requirements.txt new file mode 100644 index 000000000..2e8c64249 --- /dev/null +++ b/deployers/bicep/requirements.txt @@ -0,0 +1,3 @@ +# requirements.txt +azure-identity>=1.15.0 +azure-cosmos>=4.5.0 \ No newline at end of file From a2c90edbc4c00c0169ffcdc92fd3d90086efe018 Mon Sep 17 00:00:00 2001 From: SteveCInVA Date: Mon, 24 Nov 2025 19:51:08 +0000 Subject: [PATCH 02/22] post deploy configure services in cosmosdb --- deployers/azure.yaml | 8 ++ deployers/bicep/main.bicep | 11 ++- deployers/bicep/modules/aiModel.bicep | 2 + deployers/bicep/modules/contentSafety.bicep | 1 + .../bicep/modules/documentIntelligence.bicep | 1 + deployers/bicep/modules/openAI-existing.bicep | 2 + deployers/bicep/modules/openAI.bicep | 3 + deployers/bicep/modules/search.bicep | 1 + deployers/bicep/modules/storageAccount.bicep | 1 + deployers/bicep/postconfig.py | 78 ++++++++++++++++++- 10 files changed, 106 insertions(+), 2 deletions(-) diff --git a/deployers/azure.yaml b/deployers/azure.yaml index f560dc9d2..768b46067 100644 --- a/deployers/azure.yaml +++ b/deployers/azure.yaml @@ -13,6 +13,14 @@ hooks: export var_cosmosDb_uri=${var_cosmosDb_uri} export var_subscriptionId=${AZURE_SUBSCRIPTION_ID} export var_rgName=${var_rgName} + export var_openAIEndpoint=${var_openAIEndpoint} + export var_openAIResourceGroup=${var_openAIResourceGroup} + export var_openAIGPTModel=${var_openAIGPTModel} + export var_openAITextEmbeddingModel=${var_openAITextEmbeddingModel} + export var_blobStorageEndpoint=${var_blobStorageEndpoint} + export var_contentSafetyName=${var_contentSafetyName} + export var_searchServiceEndpoint=${var_searchServiceEndpoint} + export var_documentIntelligenceServiceEndpoint=${var_documentIntelligenceServiceEndpoint} # Execute post-configuration script python3 -m pip install --user -r ./bicep/requirements.txt diff --git a/deployers/bicep/main.bicep b/deployers/bicep/main.bicep index 36c7371eb..190d86ee4 100644 --- a/deployers/bicep/main.bicep +++ b/deployers/bicep/main.bicep @@ -485,7 +485,6 @@ module enterpriseAppPermissions 'modules/enterpriseAppPermissions.bicep' = if (e } } - //========================================================= // Outputs for deployment of container image //========================================================= @@ -497,4 +496,14 @@ output var_imageName string = contains(imageName, ':') ? split(imageName, ':')[0 output var_imageTag string = split(imageName, ':')[1] output var_specialImage bool = contains(imageName, ':') ? split(imageName, ':')[1] != 'latest' : false output var_webService string = appService.outputs.name + output var_cosmosDb_uri string = cosmosDB.outputs.cosmosDbUri +output var_openAIEndpoint string = useExistingOpenAISvc ? openAI_existing.outputs.openAIEndpoint : openAI_create.outputs.openAIEndpoint +output var_openAIResourceGroup string = useExistingOpenAISvc ? existingOpenAIResourceGroupName : openAI_create.outputs.openAIResourceGroup +output var_openAIGPTModel string = useExistingOpenAISvc ? '' : openAI_create.outputs.openAIGptModel +output var_openAITextEmbeddingModel string = useExistingOpenAISvc ? '' : openAI_create.outputs.openAITextEmbeddingModel +output var_blobStorageEndpoint string = storageAccount.outputs.endpoint + +output var_contentSafetyEndpoint string = deployContentSafety ? contentSafety.outputs.contentSafetyEndpoint : '' +output var_searchServiceEndpoint string = searchService.outputs.searchServiceEndpoint +output var_documentIntelligenceServiceEndpoint string = docIntel.outputs.documentIntelligenceServiceEndpoint diff --git a/deployers/bicep/modules/aiModel.bicep b/deployers/bicep/modules/aiModel.bicep index 3c60ee040..5d295c84a 100644 --- a/deployers/bicep/modules/aiModel.bicep +++ b/deployers/bicep/modules/aiModel.bicep @@ -24,3 +24,5 @@ resource aiModel 'Microsoft.CognitiveServices/accounts/deployments@2025-06-01' = capacity: skuCapacity } } + +output modelName string = aiModel.name diff --git a/deployers/bicep/modules/contentSafety.bicep b/deployers/bicep/modules/contentSafety.bicep index 040af5fdc..3c58dcc6c 100644 --- a/deployers/bicep/modules/contentSafety.bicep +++ b/deployers/bicep/modules/contentSafety.bicep @@ -59,3 +59,4 @@ resource contentSafetyDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05 } output contentSafetyName string = contentSafety.name +output contentSafetyEndpoint string = contentSafety.properties.endpoint diff --git a/deployers/bicep/modules/documentIntelligence.bicep b/deployers/bicep/modules/documentIntelligence.bicep index e11bb707a..db27943cd 100644 --- a/deployers/bicep/modules/documentIntelligence.bicep +++ b/deployers/bicep/modules/documentIntelligence.bicep @@ -60,3 +60,4 @@ resource docIntelDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-p output documentIntelligenceServiceName string = docIntel.name output diagnosticLoggingEnabled bool = enableDiagLogging +output documentIntelligenceServiceEndpoint string = docIntel.properties.endpoint diff --git a/deployers/bicep/modules/openAI-existing.bicep b/deployers/bicep/modules/openAI-existing.bicep index 28cb92a4f..4e09c2743 100644 --- a/deployers/bicep/modules/openAI-existing.bicep +++ b/deployers/bicep/modules/openAI-existing.bicep @@ -36,5 +36,7 @@ dependsOn: [ output openAIName string = existingOpenAI.name output openAIResourceGroup string = resourceGroup().name +output openAIEndpoint string = existingOpenAI.properties.endpoint + diff --git a/deployers/bicep/modules/openAI.bicep b/deployers/bicep/modules/openAI.bicep index d31337ad1..78c951306 100644 --- a/deployers/bicep/modules/openAI.bicep +++ b/deployers/bicep/modules/openAI.bicep @@ -79,6 +79,9 @@ dependsOn: [ output openAIName string = newOpenAI.name output openAIResourceGroup string = resourceGroup().name +output openAIEndpoint string = newOpenAI.properties.endpoint +output openAIGptModel string = aiModel_gpt4o.outputs.modelName +output openAITextEmbeddingModel string = aiModel_textEmbedding.outputs.modelName diff --git a/deployers/bicep/modules/search.bicep b/deployers/bicep/modules/search.bicep index 5ec5acec9..c8821e44a 100644 --- a/deployers/bicep/modules/search.bicep +++ b/deployers/bicep/modules/search.bicep @@ -59,3 +59,4 @@ resource searchDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-pre } output searchServiceName string = searchService.name +output searchServiceEndpoint string = searchService.properties.endpoint diff --git a/deployers/bicep/modules/storageAccount.bicep b/deployers/bicep/modules/storageAccount.bicep index 436957f54..446904614 100644 --- a/deployers/bicep/modules/storageAccount.bicep +++ b/deployers/bicep/modules/storageAccount.bicep @@ -95,3 +95,4 @@ resource storageDiagnosticsBlob 'Microsoft.Insights/diagnosticSettings@2021-05-0 } output name string = storageAccount.name +output endpoint string = storageAccount.properties.primaryEndpoints.blob diff --git a/deployers/bicep/postconfig.py b/deployers/bicep/postconfig.py index f7aa7889e..3211ab920 100644 --- a/deployers/bicep/postconfig.py +++ b/deployers/bicep/postconfig.py @@ -20,9 +20,85 @@ partition_key = "app_settings" # Use the actual partition key value for this item item = container.read_item(item=item_id, partition_key=partition_key) +var_openAIEndpoint=os.getenv("var_openAIEndpoint") +var_openAIResourceGroup=os.getenv("var_openAIResourceGroup") +var_subscriptionId = os.getenv("var_subscriptionId") +var_rgName = os.getenv("var_rgName") +var_openAIGPTModel = os.getenv("var_openAIGPTModel") +var_openAITextEmbeddingModel = os.getenv("var_openAITextEmbeddingModel") +var_blobStorageEndpoint = os.getenv("var_blobStorageEndpoint") +var_contentSafetyEndpoint = os.getenv("var_contentSafetyEndpoint") +var_searchServiceEndpoint = os.getenv("var_searchServiceEndpoint") +var_documentIntelligenceServiceEndpoint = os.getenv("var_documentIntelligenceServiceEndpoint") + # 4. Update the property item["enable_external_healthcheck"] = True -# 5. Upsert the updated item back into Cosmos DB +item["azure_openai_gpt_endpoint"] = var_openAIEndpoint +item["azure_openai_gpt_authentication_type"] = "managed_identity" +item["azure_openai_gpt_subscription_id"] = var_subscriptionId +item["azure_openai_gpt_resource_group"] = var_openAIResourceGroup +item["gpt_model"] = { + "selected": [ + { + "deploymentName": var_openAIGPTModel, + "modelName": var_openAIGPTModel + } + ], + "all": [ + { + "deploymentName": var_openAIGPTModel, + "modelName": var_openAIGPTModel + } + ] +} + +item["azure_openai_embedding_endpoint"] = var_openAIEndpoint +item["azure_openai_embedding_authentication_type"] = "managed_identity" +item["azure_openai_embedding_subscription_id"] = var_subscriptionId +item["azure_openai_embedding_resource_group"] = var_openAIResourceGroup +item["embedding_model"] = { + "selected": [ + { + "deploymentName": var_openAITextEmbeddingModel, + "modelName": var_openAITextEmbeddingModel + } + ], + "all": [ + { + "deploymentName": var_openAITextEmbeddingModel, + "modelName": var_openAITextEmbeddingModel + } + ] +} + +item["enable_semantic_kernel"] = True + +item["enable_appinsights_global_logging"] = True + +item["enable_extract_meta_data"] = True +item["metadata_extraction_model"] = var_openAIGPTModel + +item["enable_multimodal_vision"] = True +item["multimodal_vision_model"] = var_openAIGPTModel + +item["enable_enhanced_citations"] = True +item["office_docs_storage_account_blob_endpoint"] = var_blobStorageEndpoint + +# if contentSafetyEndpoint is not blank then set enable_content_safety to true +if var_contentSafetyEndpoint and var_contentSafetyEndpoint.strip(): + item["enable_content_safety"] = True + item["content_safety_endpoint"] = var_contentSafetyEndpoint + item["content_safety_authentication_type"] = "managed_identity" + +item["enable_conversation_archiving"] = True + +item["azure_ai_search_endpoint"] = var_searchServiceEndpoint +item["azure_ai_search_authentication_type"] = "managed_identity" + +item["azure_document_intelligence_endpoint"] = var_documentIntelligenceServiceEndpoint +item["azure_document_intelligence_authentication_type"] = "managed_identity" + +# 5. Upsert the updated items back into Cosmos DB response = container.upsert_item(item) print(f"Updated item: {response['id']} with enable_external_healthcheck = {response['enable_external_healthcheck']}") From c89c377e43c41ed194e46f741ecb41380c6e7361 Mon Sep 17 00:00:00 2001 From: SteveCInVA Date: Tue, 25 Nov 2025 13:29:57 +0000 Subject: [PATCH 03/22] refactor to prevent post deploy configuration + begin support of key based auth. --- deployers/azure.yaml | 16 ++++++++++++---- deployers/bicep/main.bicep | 10 ++++++++-- deployers/bicep/postconfig.py | 23 +++++++++++++++++++---- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/deployers/azure.yaml b/deployers/azure.yaml index 768b46067..bcd47fc29 100644 --- a/deployers/azure.yaml +++ b/deployers/azure.yaml @@ -10,9 +10,13 @@ hooks: shell: sh run: | # Set up variables + + export var_configureApplication=${var_configureApplication} export var_cosmosDb_uri=${var_cosmosDb_uri} export var_subscriptionId=${AZURE_SUBSCRIPTION_ID} export var_rgName=${var_rgName} + export var_enableEnterpriseApp=${var_enableEnterpriseApp} + export var_openAIEndpoint=${var_openAIEndpoint} export var_openAIResourceGroup=${var_openAIResourceGroup} export var_openAIGPTModel=${var_openAIGPTModel} @@ -22,10 +26,14 @@ hooks: export var_searchServiceEndpoint=${var_searchServiceEndpoint} export var_documentIntelligenceServiceEndpoint=${var_documentIntelligenceServiceEndpoint} - # Execute post-configuration script - python3 -m pip install --user -r ./bicep/requirements.txt - #chmod +x ./bicep/postconfig.py - python3 ./bicep/postconfig.py + # Execute post-configuration script if enabled + if [ "${var_configureApplication}" = "true" ]; then + echo "Running post-deployment configuration..." + python3 -m pip install --user -r ./bicep/requirements.txt + python3 ./bicep/postconfig.py + else + echo "Skipping post-deployment configuration (var_configureApplication is not true)" + fi predeploy: diff --git a/deployers/bicep/main.bicep b/deployers/bicep/main.bicep index 190d86ee4..ac8c95adb 100644 --- a/deployers/bicep/main.bicep +++ b/deployers/bicep/main.bicep @@ -44,8 +44,8 @@ param enableDiagLogging bool @description('''Enable enterprise application (Azure AD App Registration) configuration. - Enables SSO, conditional access, and centralized identity management -- Default is true''') -param enableEnterpriseApp bool = true +''') +param enableEnterpriseApp bool @description('''Azure AD Application Client ID for enterprise authentication. - Required if enableEnterpriseApp is true @@ -112,6 +112,10 @@ param imageName string ]) param unauthenticatedClientAction string = 'RedirectToLoginPage' +@description('''Excute post deployment configuration script to pre-populate application with required settings and secrets. +''') +param configureApplication bool + //========================================================= // variable declarations for the main deployment //========================================================= @@ -507,3 +511,5 @@ output var_blobStorageEndpoint string = storageAccount.outputs.endpoint output var_contentSafetyEndpoint string = deployContentSafety ? contentSafety.outputs.contentSafetyEndpoint : '' output var_searchServiceEndpoint string = searchService.outputs.searchServiceEndpoint output var_documentIntelligenceServiceEndpoint string = docIntel.outputs.documentIntelligenceServiceEndpoint +output var_configureApplication bool = configureApplication +output var_enableEnterpriseApp bool = enableEnterpriseApp diff --git a/deployers/bicep/postconfig.py b/deployers/bicep/postconfig.py index 3211ab920..599e43517 100644 --- a/deployers/bicep/postconfig.py +++ b/deployers/bicep/postconfig.py @@ -1,11 +1,11 @@ from azure.cosmos import CosmosClient +from azure.cosmos.exceptions import CosmosResourceNotFoundError from azure.identity import DefaultAzureCredential import os credential = DefaultAzureCredential() token = credential.get_token("https://cosmos.azure.com/.default") -#endpoint = "https://bicepchat-demo-cosmos.documents.azure.com:443/" cosmosEndpoint = os.getenv("var_cosmosDb_uri") client = CosmosClient(cosmosEndpoint, credential=credential) @@ -15,10 +15,21 @@ database = client.get_database_client(database_name) container = database.get_container_client(container_name) -# 3. Read the existing item by ID and partition key +# Read the existing item by ID and partition key item_id = "app_settings" partition_key = "app_settings" # Use the actual partition key value for this item -item = container.read_item(item=item_id, partition_key=partition_key) +try: + item = container.read_item(item=item_id, partition_key=partition_key) + print(f"Found existing app_setting document") +except CosmosResourceNotFoundError: + print(f"app_setting document not found.") + item = { + "id": item_id, + "partition_key": partition_key + } + +# Get values from environment variables +var_enableEnterpriseApp = os.getenv("var_enableEnterpriseApp") # expected to be true or false... will determine if key based or managed identity is used var_openAIEndpoint=os.getenv("var_openAIEndpoint") var_openAIResourceGroup=os.getenv("var_openAIResourceGroup") @@ -35,7 +46,11 @@ item["enable_external_healthcheck"] = True item["azure_openai_gpt_endpoint"] = var_openAIEndpoint -item["azure_openai_gpt_authentication_type"] = "managed_identity" + +if var_contentSafetyEndpoint and var_contentSafetyEndpoint.strip(): + item["azure_openai_gpt_authentication_type"] = "managed_identity" +else: + item["azure_openai_gpt_authentication_type"] = "key" item["azure_openai_gpt_subscription_id"] = var_subscriptionId item["azure_openai_gpt_resource_group"] = var_openAIResourceGroup item["gpt_model"] = { From a1513296762dcb1d7ba2bd64165e07876e5e4556 Mon Sep 17 00:00:00 2001 From: SteveCInVA Date: Tue, 25 Nov 2025 14:14:08 +0000 Subject: [PATCH 04/22] Add additional parameter validation for creating entra app --- deployers/Initialize-EntraApplication.ps1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/deployers/Initialize-EntraApplication.ps1 b/deployers/Initialize-EntraApplication.ps1 index 13b5ae8f6..5a411be00 100644 --- a/deployers/Initialize-EntraApplication.ps1 +++ b/deployers/Initialize-EntraApplication.ps1 @@ -41,9 +41,13 @@ [CmdletBinding()] param( [Parameter(Mandatory = $true)] + [ValidateLength(3, 12)] # Length between 3 and 12 + [ValidatePattern('^[a-zA-Z0-9]+$')] # Only letters and numbers [string]$AppName, [Parameter(Mandatory = $true)] + [ValidateLength(2, 10)] # Length between 2 and 10 + [ValidatePattern('^[a-zA-Z0-9]+$')] # Only letters and numbers [string]$Environment, [Parameter(Mandatory = $false)] From 1c9454aebffd0ae954cd60a2748f09ef9458d65f Mon Sep 17 00:00:00 2001 From: SteveCInVA Date: Sat, 29 Nov 2025 13:32:58 +0000 Subject: [PATCH 05/22] Refactor Bicep modules for improved authentication and key management - Added keyVault-Secrets.bicep module for storing secrets in Key Vault. - Modified keyVault.bicep to remove enterprise app client secret handling and commented out managed identity role assignments. - Removed openAI-existing.bicep and refactored openAI.bicep to handle model deployments dynamically. - Added setPermissions.bicep for managing role assignments for various resources. - Updated postconfig.py to reflect changes in environment variable handling for authentication type. --- application/single_app/config.py | 1 + deployers/azure.yaml | 1 - deployers/bicep/main.bicep | 406 ++++++++---------- deployers/bicep/main.parameters.json | 16 +- deployers/bicep/modules/aiModel.bicep | 1 - deployers/bicep/modules/appService.bicep | 99 ++++- .../modules/appServiceAuthentication.bicep | 103 ----- ...sights.bicep => applicationInsights.bicep} | 0 .../azureContainerRegistry-existing.bicep | 18 - ...zureContainerRegistry-roleAssignment.bicep | 27 -- .../modules/azureContainerRegistry.bicep | 42 +- deployers/bicep/modules/contentSafety.bicep | 32 +- deployers/bicep/modules/cosmosDb.bicep | 46 +- deployers/bicep/modules/createAppSecret.bicep | 113 ----- .../bicep/modules/documentIntelligence.bicep | 32 +- .../modules/enterpriseAppPermissions.bicep | 177 -------- .../bicep/modules/enterpriseApplication.bicep | 91 ---- .../bicep/modules/keyVault-Secrets.bicep | 23 + deployers/bicep/modules/keyVault.bicep | 54 +-- ...tics.bicep => logAnalyticsWorkspace.bicep} | 0 deployers/bicep/modules/managedIdentity.bicep | 3 +- deployers/bicep/modules/openAI-existing.bicep | 42 -- deployers/bicep/modules/openAI.bicep | 79 ++-- deployers/bicep/modules/redisCache.bicep | 36 +- deployers/bicep/modules/search.bicep | 33 +- deployers/bicep/modules/setPermissions.bicep | 193 +++++++++ deployers/bicep/modules/speechService.bicep | 38 +- deployers/bicep/modules/storageAccount.bicep | 32 +- deployers/bicep/postconfig.py | 2 +- 29 files changed, 698 insertions(+), 1042 deletions(-) delete mode 100644 deployers/bicep/modules/appServiceAuthentication.bicep rename deployers/bicep/modules/{appInsights.bicep => applicationInsights.bicep} (100%) delete mode 100644 deployers/bicep/modules/azureContainerRegistry-existing.bicep delete mode 100644 deployers/bicep/modules/azureContainerRegistry-roleAssignment.bicep delete mode 100644 deployers/bicep/modules/createAppSecret.bicep delete mode 100644 deployers/bicep/modules/enterpriseAppPermissions.bicep delete mode 100644 deployers/bicep/modules/enterpriseApplication.bicep create mode 100644 deployers/bicep/modules/keyVault-Secrets.bicep rename deployers/bicep/modules/{logAnalytics.bicep => logAnalyticsWorkspace.bicep} (100%) delete mode 100644 deployers/bicep/modules/openAI-existing.bicep create mode 100644 deployers/bicep/modules/setPermissions.bicep diff --git a/application/single_app/config.py b/application/single_app/config.py index 938621556..cf0d0329b 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -184,6 +184,7 @@ credential_scopes=[resource_manager + "/.default"] cognitive_services_scope = "https://cognitiveservices.azure.com/.default" video_indexer_endpoint = "https://api.videoindexer.ai" + search_resource_manager = "https://search.windows.net" KEY_VAULT_DOMAIN = ".vault.azure.net" def get_redis_cache_infrastructure_endpoint(redis_hostname: str) -> str: diff --git a/deployers/azure.yaml b/deployers/azure.yaml index bcd47fc29..6085ee91c 100644 --- a/deployers/azure.yaml +++ b/deployers/azure.yaml @@ -35,7 +35,6 @@ hooks: echo "Skipping post-deployment configuration (var_configureApplication is not true)" fi - predeploy: posix: shell: sh diff --git a/deployers/bicep/main.bicep b/deployers/bicep/main.bicep index ac8c95adb..232da6182 100644 --- a/deployers/bicep/main.bicep +++ b/deployers/bicep/main.bicep @@ -29,23 +29,14 @@ param appName string @maxLength(10) param environment string -@description('Optional object containing additional tags to apply to all resources.') -param specialTags object = {} - @minLength(1) @maxLength(64) @description('Name of the AZD environment') param azdEnvironmentName string -@description('''Enable diagnostic logging for resources deployed in the resource group. -- All content will be sent to the deployed Log Analytics workspace -- Default is false''') -param enableDiagLogging bool - -@description('''Enable enterprise application (Azure AD App Registration) configuration. -- Enables SSO, conditional access, and centralized identity management -''') -param enableEnterpriseApp bool +@description('''The name of the container image to deploy to the web app. +- should be in the format :''') +param imageName string @description('''Azure AD Application Client ID for enterprise authentication. - Required if enableEnterpriseApp is true @@ -59,73 +50,84 @@ param enterpriseAppClientId string @secure() param enterpriseAppClientSecret string -@description('''Use existing Azure Container Registry -- Default is false''') -param useExistingAcr bool +//---------------- +// configurations +@description('''Authentication type for resources that support Managed Identity or Key authentication. +- Key: Use access keys for authentication (application keys will be stored in Key Vault) +- Managed_Identity: Use Managed Identity for authentication''') +@allowed([ + 'Key' + 'Managed_Identity' +]) +param authenticationType string -@description('''The name of the existing Azure Container Registry containing the container image to deploy to the web app. -- Required if useExistingAcr is true -- should be in the format -- Do not include any domain suffix such as .azurecr.io''') -param existingAcrResourceName string +@description('''Configure permissions (based on authenticationType) for the deployed web application to access required resources. +''') +param configureApplicationPermissions bool -@description('''The name of the Azure Container Registry resource group. -- Required if useExistingAcr is true''') -param existingAcrResourceGroup string +@description('Optional object containing additional tags to apply to all resources.') +param specialTags object = {} -@description('''Enable deployment of Content Safety service and related resources. +@description('''Enable diagnostic logging for resources deployed in the resource group. +- All content will be sent to the deployed Log Analytics workspace - Default is false''') -param deployContentSafety bool +param enableDiagLogging bool -@description('''Enable deployment of Azure Cache for Redis and related resources. +@description('''Array of GPT model names to deploy to the OpenAI resource.''') +param gptModels array = [ + { + modelName: 'gpt-4.1' + modelVersion: '2025-04-14' + skuName: 'GlobalStandard' + skuCapacity: 150 + } + { + modelName: 'gpt-4o' + modelVersion: '2024-11-20' + skuName: 'GlobalStandard' + skuCapacity: 100 + } +] + +@description('''Array of embedding model names to deploy to the OpenAI resource.''') +param embeddingModels array = [ + { + modelName: 'text-embedding-3-small' + modelVersion: '1' + skuName: 'GlobalStandard' + skuCapacity: 150 + } + { + modelName: 'text-embedding-3-large' + modelVersion: '1' + skuName: 'GlobalStandard' + skuCapacity: 150 + } +] +//---------------- +// optional services + +@description('''Enable deployment of Content Safety service and related resources. - Default is false''') -param deployRedisCache bool +param deployContentSafety bool @description('''Enable deployment of Azure Speech service and related resources. - Default is false''') param deploySpeechService bool -@description('''Use existing Azure OpenAI resource''') -param useExistingOpenAISvc bool - -@description('''Existing Azure OpenAI Resource Group Name -- Required if useExistingOpenAISvc is true''') -param existingOpenAIResourceGroupName string - -@description('''Existing Azure OpenAI Resource Name -- Required if useExistingOpenAISvc is true''') -param existingOpenAIResourceName string - -@description('''The name of the container image to deploy to the web app. -- should be in the format :''') -param imageName string - -@description('''Unauthenticated client action for enterprise application. -- RedirectToLoginPage: Redirect unauthenticated users to login -- Return401: Return 401 Unauthorized for unauthenticated requests -- AllowAnonymous: Allow anonymous access''') -@allowed([ - 'AllowAnonymous' - 'RedirectToLoginPage' - 'Return401' - 'Return403' -]) -param unauthenticatedClientAction string = 'RedirectToLoginPage' - -@description('''Excute post deployment configuration script to pre-populate application with required settings and secrets. -''') -param configureApplication bool +@description('''Enable deployment of Azure Cache for Redis and related resources. +- Default is false''') +param deployRedisCache bool //========================================================= // variable declarations for the main deployment //========================================================= - var rgName = '${appName}-${environment}-rg' var requiredTags = { application: appName, environment: environment, 'azd-env-name': azdEnvironmentName } var tags = union(requiredTags, specialTags) var acrCloudSuffix = cloudEnvironment == 'AzureCloud' ? '.azurecr.io' : '.azurecr.us' +var acrName = toLower('${appName}${environment}acr') var containerRegistry = '${acrName}${acrCloudSuffix}' -var acrName = useExistingAcr ? existingAcrResourceName : toLower('${appName}${environment}acr') var containerImageName = '${containerRegistry}/${imageName}' //========================================================= @@ -140,21 +142,21 @@ resource rg 'Microsoft.Resources/resourceGroups@2022-09-01' = { //========================================================= // Create managed identity //========================================================= -module managedIdentity 'modules/managedIdentity.bicep' = { - name: 'managedIdentity' - scope: rg - params: { - location: location - appName: appName - environment: environment - tags: tags - } -} +// module managedIdentity 'modules/managedIdentity.bicep' = { +// name: 'managedIdentity' +// scope: rg +// params: { +// location: location +// appName: appName +// environment: environment +// tags: tags +// } +// } //========================================================= // Create log analytics workspace //========================================================= -module logAnalytics 'modules/logAnalytics.bicep' = { +module logAnalytics 'modules/logAnalyticsWorkspace.bicep' = { name: 'logAnalytics' scope: rg params: { @@ -166,56 +168,48 @@ module logAnalytics 'modules/logAnalytics.bicep' = { } //========================================================= -// Create key vault +// Create application insights //========================================================= -module keyVault 'modules/keyVault.bicep' = { - name: 'keyVault' +module applicationInsights 'modules/applicationInsights.bicep' = { + name: 'applicationInsights' scope: rg params: { location: location appName: appName environment: environment tags: tags - managedIdentityPrincipalId: managedIdentity.outputs.principalId - managedIdentityId: managedIdentity.outputs.resourceId - enableDiagLogging: enableDiagLogging logAnalyticsId: logAnalytics.outputs.logAnalyticsId - enableEnterpriseApp: enableEnterpriseApp - enterpriseAppClientId: enterpriseAppClientId - enterpriseAppClientSecret: enterpriseAppClientSecret } } //========================================================= -// Create application insights +// Create key vault //========================================================= -module appInsights 'modules/appInsights.bicep' = { - name: 'appInsights' +module keyVault 'modules/keyVault.bicep' = { + name: 'keyVault' scope: rg params: { location: location appName: appName environment: environment tags: tags + //managedIdentityPrincipalId: managedIdentity.outputs.principalId + //managedIdentityId: managedIdentity.outputs.resourceId + enableDiagLogging: enableDiagLogging logAnalyticsId: logAnalytics.outputs.logAnalyticsId } } //========================================================= -// Create storage account +// Store enterprise app client secret in key vault //========================================================= -module storageAccount 'modules/storageAccount.bicep' = { - name: 'storageAccount' +module storeEnterpriseAppSecret 'modules/keyVault-Secrets.bicep' = if (!empty(enterpriseAppClientSecret)) { + name: 'storeEnterpriseAppSecret' scope: rg params: { - location: location - appName: appName - environment: environment - tags: tags - managedIdentityPrincipalId: managedIdentity.outputs.principalId - managedIdentityId: managedIdentity.outputs.resourceId - enableDiagLogging: enableDiagLogging - logAnalyticsId: logAnalytics.outputs.logAnalyticsId + keyVaultName: keyVault.outputs.keyVaultName + secretName: 'enterprise-app-client-secret' + secretValue: enterpriseAppClientSecret } } @@ -230,55 +224,33 @@ module cosmosDB 'modules/cosmosDb.bicep' = { appName: appName environment: environment tags: tags - managedIdentityPrincipalId: managedIdentity.outputs.principalId - managedIdentityId: managedIdentity.outputs.resourceId enableDiagLogging: enableDiagLogging logAnalyticsId: logAnalytics.outputs.logAnalyticsId - } -} -//========================================================= -// Create Document Intelligence resource -//========================================================= -module docIntel 'modules/documentIntelligence.bicep' = { - name: 'docIntel' - scope: rg - params: { - location: location - appName: appName - environment: environment - tags: tags - managedIdentityPrincipalId: managedIdentity.outputs.principalId - managedIdentityId: managedIdentity.outputs.resourceId - enableDiagLogging: enableDiagLogging - logAnalyticsId: logAnalytics.outputs.logAnalyticsId + keyVault: keyVault.outputs.keyVaultName + authenticationType: authenticationType + configureApplicationPermissions: configureApplicationPermissions } } //========================================================= -// Create or get Azure Container Registry +// Create Azure Container Registry //========================================================= -module acr_create 'modules/azureContainerRegistry.bicep' = if (!useExistingAcr) { - name: 'azureContainerRegistry_create' +module acr 'modules/azureContainerRegistry.bicep' = { + name: 'azureContainerRegistry' scope: rg params: { location: location acrName: acrName tags: tags - managedIdentityPrincipalId: managedIdentity.outputs.principalId - managedIdentityId: managedIdentity.outputs.resourceId + //managedIdentityPrincipalId: managedIdentity.outputs.principalId + //managedIdentityId: managedIdentity.outputs.resourceId enableDiagLogging: enableDiagLogging logAnalyticsId: logAnalytics.outputs.logAnalyticsId - } -} -module acr_existing 'modules/azureContainerRegistry-existing.bicep' = if (useExistingAcr) { - name: 'acr-existing' - scope: rg - params: { - acrName: existingAcrResourceName - acrResourceGroup: existingAcrResourceGroup - managedIdentityPrincipalId: managedIdentity.outputs.principalId + keyVault: keyVault.outputs.keyVaultName + authenticationType: authenticationType + configureApplicationPermissions: configureApplicationPermissions } } @@ -293,72 +265,75 @@ module searchService 'modules/search.bicep' = { appName: appName environment: environment tags: tags - managedIdentityPrincipalId: managedIdentity.outputs.principalId - managedIdentityId: managedIdentity.outputs.resourceId enableDiagLogging: enableDiagLogging logAnalyticsId: logAnalytics.outputs.logAnalyticsId + + keyVault: keyVault.outputs.keyVaultName + authenticationType: authenticationType + configureApplicationPermissions: configureApplicationPermissions } } //========================================================= -// Create or get Optional Resource - OpenAI Service +// Create Document Intelligence resource //========================================================= -module openAI_create 'modules/openAI.bicep' = if (!useExistingOpenAISvc) { - name: 'openAICreate' +module docIntel 'modules/documentIntelligence.bicep' = { + name: 'docIntel' scope: rg params: { location: location appName: appName environment: environment tags: tags - //managedIdentityPrincipalId: managedIdentity.outputs.principalId - managedIdentityId: managedIdentity.outputs.resourceId enableDiagLogging: enableDiagLogging logAnalyticsId: logAnalytics.outputs.logAnalyticsId - } -} -module openAI_existing 'modules/openAI-existing.bicep' = if (useExistingOpenAISvc) { - name: 'openAIExisting' - scope: resourceGroup(useExistingOpenAISvc ? existingOpenAIResourceGroupName : rgName) - params: { - openAIName: existingOpenAIResourceName + keyVault: keyVault.outputs.keyVaultName + authenticationType: authenticationType + configureApplicationPermissions: configureApplicationPermissions } } //========================================================= -// Create Optional Resource - Content Safety +// Create storage account //========================================================= -module contentSafety 'modules/contentSafety.bicep' = if (deployContentSafety) { - name: 'contentSafety' +module storageAccount 'modules/storageAccount.bicep' = { + name: 'storageAccount' scope: rg params: { location: location appName: appName environment: environment tags: tags - managedIdentityPrincipalId: managedIdentity.outputs.principalId - managedIdentityId: managedIdentity.outputs.resourceId enableDiagLogging: enableDiagLogging logAnalyticsId: logAnalytics.outputs.logAnalyticsId + + keyVault: keyVault.outputs.keyVaultName + authenticationType: authenticationType + configureApplicationPermissions: configureApplicationPermissions } } //========================================================= -// Create Optional Resource - Redis Cache +// Create - OpenAI Service //========================================================= -module redisCache 'modules/redisCache.bicep' = if (deployRedisCache) { - name: 'redisCache' +module openAI 'modules/openAI.bicep' = { + name: 'openAI' scope: rg params: { location: location appName: appName environment: environment tags: tags - //managedIdentityPrincipalId: managedIdentity.outputs.principalId - //managedIdentityId: managedIdentity.outputs.resourceId enableDiagLogging: enableDiagLogging logAnalyticsId: logAnalytics.outputs.logAnalyticsId + + keyVault: keyVault.outputs.keyVaultName + authenticationType: authenticationType + configureApplicationPermissions: configureApplicationPermissions + + gptModels: gptModels + embeddingModels: embeddingModels } } @@ -389,10 +364,9 @@ module appService 'modules/appService.bicep' = { appName: appName environment: environment tags: tags - #disable-next-line BCP318 // expect one value to be null - acrName: useExistingAcr ? acr_existing.outputs.acrName : acr_create.outputs.acrName - managedIdentityId: managedIdentity.outputs.resourceId - managedIdentityClientId: managedIdentity.outputs.clientId + acrName: acr.outputs.acrName + // managedIdentityId: managedIdentity.outputs.resourceId + // managedIdentityClientId: managedIdentity.outputs.clientId enableDiagLogging: enableDiagLogging logAnalyticsId: logAnalytics.outputs.logAnalyticsId appServicePlanId: appServicePlan.outputs.appServicePlanId @@ -400,116 +374,118 @@ module appService 'modules/appService.bicep' = { azurePlatform: cloudEnvironment cosmosDbName: cosmosDB.outputs.cosmosDbName searchServiceName: searchService.outputs.searchServiceName - #disable-next-line BCP318 // expect one value to be null - openAiServiceName: useExistingOpenAISvc ? openAI_existing.outputs.openAIName : openAI_create.outputs.openAIName - #disable-next-line BCP318 // expect one value to be null - openAiResourceGroupName: useExistingOpenAISvc - ? existingOpenAIResourceGroupName - #disable-next-line BCP318 // expect one value to be null - : openAI_create.outputs.openAIResourceGroup + openAiServiceName: openAI.outputs.openAIName + openAiResourceGroupName: openAI.outputs.openAIResourceGroup documentIntelligenceServiceName: docIntel.outputs.documentIntelligenceServiceName - appInsightsName: appInsights.outputs.appInsightsName + appInsightsName: applicationInsights.outputs.appInsightsName enterpriseAppClientId: enterpriseAppClientId - enterpriseAppClientSecret: '' + enterpriseAppClientSecret: enterpriseAppClientSecret + authenticationType: authenticationType keyVaultUri: keyVault.outputs.keyVaultUri } } //========================================================= -// Create Enterprise Application Configuration +// configure optional services +//========================================================= + +//========================================================= +// Create Optional Resource - Content Safety //========================================================= -module enterpriseApp 'modules/enterpriseApplication.bicep' = if (enableEnterpriseApp) { - name: 'enterpriseApplication' +module contentSafety 'modules/contentSafety.bicep' = if (deployContentSafety) { + name: 'contentSafety' scope: rg params: { + location: location appName: appName environment: environment - redirectUri: 'https://${appService.outputs.defaultHostName}' + tags: tags + enableDiagLogging: enableDiagLogging + logAnalyticsId: logAnalytics.outputs.logAnalyticsId + + keyVault: keyVault.outputs.keyVaultName + authenticationType: authenticationType + configureApplicationPermissions: configureApplicationPermissions } } //========================================================= -// Configure App Service Authentication with Enterprise App +// Create Optional Resource - Speech Service //========================================================= -module appServiceAuth 'modules/appServiceAuthentication.bicep' = if (enableEnterpriseApp && !empty(enterpriseAppClientId)) { - name: 'appServiceAuthentication' +module speechService 'modules/speechService.bicep' = if (deploySpeechService) { + name: 'speechService' scope: rg - dependsOn: [ - enterpriseApp - ] params: { - webAppName: appService.outputs.name - clientId: enterpriseAppClientId - // Use the auto-generated secret URI if no manual secret was provided, otherwise use the manual one - clientSecretKeyVaultUri: !empty(enterpriseAppClientSecret) ? keyVault.outputs.enterpriseAppClientSecretUri : '' - tenantId: tenant().tenantId - enableAuthentication: enableEnterpriseApp - unauthenticatedClientAction: unauthenticatedClientAction - tokenStoreEnabled: true + location: location + appName: appName + environment: environment + tags: tags + enableDiagLogging: enableDiagLogging + logAnalyticsId: logAnalytics.outputs.logAnalyticsId + + keyVault: keyVault.outputs.keyVaultName + authenticationType: authenticationType + configureApplicationPermissions: configureApplicationPermissions } } //========================================================= -// Create Optional Resource - Speech Service +// Create Optional Resource - Redis Cache //========================================================= -module speechService 'modules/speechService.bicep' = if (deploySpeechService) { - name: 'speechService' +module redisCache 'modules/redisCache.bicep' = if (deployRedisCache) { + name: 'redisCache' scope: rg params: { location: location appName: appName environment: environment tags: tags - managedIdentityPrincipalId: managedIdentity.outputs.principalId - managedIdentityId: managedIdentity.outputs.resourceId enableDiagLogging: enableDiagLogging logAnalyticsId: logAnalytics.outputs.logAnalyticsId + + keyVault: keyVault.outputs.keyVaultName + authenticationType: authenticationType + configureApplicationPermissions: configureApplicationPermissions } } //========================================================= -// Resource to Configure Enterprise App Permissions +// configure permissions for managed identity to access resources //========================================================= -module enterpriseAppPermissions 'modules/enterpriseAppPermissions.bicep' = if (enableEnterpriseApp) { - name: 'enterpriseAppPermissions' +module setPermissions 'modules/setPermissions.bicep' = if (configureApplicationPermissions) { + name: 'setPermissions' scope: rg params: { webAppName: appService.outputs.name keyVaultName: keyVault.outputs.keyVaultName + authenticationType: authenticationType cosmosDBName: cosmosDB.outputs.cosmosDbName - #disable-next-line BCP318 // expect one value to be null - openAIName: useExistingOpenAISvc ? '' : openAI_create.outputs.openAIName + acrName: acr.outputs.acrName + openAIName: openAI.outputs.openAIName + // openAIResourceGroupName: useExistingOpenAISvc ? existingOpenAIResourceGroupName : openAI_create.outputs.openAIResourceGroup docIntelName: docIntel.outputs.documentIntelligenceServiceName - storageAccountName: storageAccount.outputs.name - #disable-next-line BCP318 // expect one value to be null - speechServiceName: deploySpeechService ? speechService.outputs.speechServiceName : '' - searchServiceName: searchService.outputs.searchServiceName - #disable-next-line BCP318 // expect one value to be null - contentSafetyName: deployContentSafety ? contentSafety.outputs.contentSafetyName : '' + // storageAccountName: storageAccount.outputs.name + // #disable-next-line BCP318 // expect one value to be null + // speechServiceName: deploySpeechService ? speechService.outputs.speechServiceName : '' + // searchServiceName: searchService.outputs.searchServiceName + // #disable-next-line BCP318 // expect one value to be null + // contentSafetyName: deployContentSafety ? contentSafety.outputs.contentSafetyName : '' } } //========================================================= -// Outputs for deployment of container image +// output values //========================================================= - output var_rgName string = rgName -output var_acrName string = useExistingAcr ? existingAcrResourceName : toLower('${appName}${environment}acr') -output var_containerRegistry string = containerRegistry +output var_webService string = appService.outputs.name output var_imageName string = contains(imageName, ':') ? split(imageName, ':')[0] : imageName output var_imageTag string = split(imageName, ':')[1] -output var_specialImage bool = contains(imageName, ':') ? split(imageName, ':')[1] != 'latest' : false -output var_webService string = appService.outputs.name +output var_containerRegistry string = containerRegistry +output var_acrName string = toLower('${appName}${environment}acr') + +output gptModel string = gptModels[0].modelName +output embeddingModel string = embeddingModels[0].modelName + + + -output var_cosmosDb_uri string = cosmosDB.outputs.cosmosDbUri -output var_openAIEndpoint string = useExistingOpenAISvc ? openAI_existing.outputs.openAIEndpoint : openAI_create.outputs.openAIEndpoint -output var_openAIResourceGroup string = useExistingOpenAISvc ? existingOpenAIResourceGroupName : openAI_create.outputs.openAIResourceGroup -output var_openAIGPTModel string = useExistingOpenAISvc ? '' : openAI_create.outputs.openAIGptModel -output var_openAITextEmbeddingModel string = useExistingOpenAISvc ? '' : openAI_create.outputs.openAITextEmbeddingModel -output var_blobStorageEndpoint string = storageAccount.outputs.endpoint - -output var_contentSafetyEndpoint string = deployContentSafety ? contentSafety.outputs.contentSafetyEndpoint : '' -output var_searchServiceEndpoint string = searchService.outputs.searchServiceEndpoint -output var_documentIntelligenceServiceEndpoint string = docIntel.outputs.documentIntelligenceServiceEndpoint -output var_configureApplication bool = configureApplication -output var_enableEnterpriseApp bool = enableEnterpriseApp diff --git a/deployers/bicep/main.parameters.json b/deployers/bicep/main.parameters.json index 61a301052..20a70fb1b 100644 --- a/deployers/bicep/main.parameters.json +++ b/deployers/bicep/main.parameters.json @@ -26,20 +26,12 @@ "enterpriseAppClientSecret": { "value": "${ENTERPRISE_APP_CLIENT_SECRET}" }, - "existingAcrResourceName": { - "value": "${EXISTING_ACR_RESOURCE_NAME}" - }, - "existingAcrResourceGroup": { - "value": "${EXISTING_ACR_RESOURCE_GROUP}" - }, - "existingOpenAIResourceName": { - "value": "${EXISTING_OPENAI_RESOURCE_NAME}" - }, - "existingOpenAIResourceGroupName": { - "value": "${EXISTING_OPENAI_RESOURCE_GROUP}" - }, "imageName": { "value": "${CONTAINER_IMAGE_NAME}" + }, + "authenticationType": { + "value": "${AUTHENTICATION_TYPE}" } + } } \ No newline at end of file diff --git a/deployers/bicep/modules/aiModel.bicep b/deployers/bicep/modules/aiModel.bicep index 5d295c84a..2e1d89afc 100644 --- a/deployers/bicep/modules/aiModel.bicep +++ b/deployers/bicep/modules/aiModel.bicep @@ -25,4 +25,3 @@ resource aiModel 'Microsoft.CognitiveServices/accounts/deployments@2025-06-01' = } } -output modelName string = aiModel.name diff --git a/deployers/bicep/modules/appService.bicep b/deployers/bicep/modules/appService.bicep index b3af02aba..01eeae15a 100644 --- a/deployers/bicep/modules/appService.bicep +++ b/deployers/bicep/modules/appService.bicep @@ -5,8 +5,6 @@ param appName string param environment string param tags object -param managedIdentityId string -param managedIdentityClientId string param enableDiagLogging bool param logAnalyticsId string @@ -20,16 +18,12 @@ param openAiServiceName string param openAiResourceGroupName string param documentIntelligenceServiceName string param appInsightsName string - -@description('Enterprise application client ID for Azure AD authentication') param enterpriseAppClientId string = '' +param authenticationType string -@description('Enterprise application client secret for Azure AD authentication') @secure() param enterpriseAppClientSecret string = '' - -@description('Key Vault URI for secret references') -param keyVaultUri string = '' +param keyVaultUri string // Import diagnostic settings configurations module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging) { @@ -71,7 +65,7 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' = { siteConfig: { linuxFxVersion: 'DOCKER|${containerImageName}' acrUseManagedIdentityCreds: true - acrUserManagedIdentityID: managedIdentityClientId + acrUserManagedIdentityID: '' // managedIdentityId alwaysOn: true ftpsState: 'Disabled' healthCheckPath: '/external/healthcheck' @@ -80,26 +74,26 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' = { {name: 'AZURE_ENDPOINT', value: azurePlatform == 'AzureUSGovernment' ? 'usgovernment' : 'public'} {name: 'SCM_DO_BUILD_DURING_DEPLOYMENT', value: 'false'} {name: 'AZURE_COSMOS_ENDPOINT', value: cosmosDb.properties.documentEndpoint} - {name: 'AZURE_COSMOS_AUTHENTICATION_TYPE', value: 'managed_identity'} + {name: 'AZURE_COSMOS_AUTHENTICATION_TYPE', value: authenticationType} + + {name: 'AZURE_COSMOS_KEY', value: authenticationType == 'Key' ? '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/cosmos-db-key)' : ''} - //{name: 'AZURE_COSMOS_AUTHENTICATION_TYPE', value: 'key'} - //{name: 'AZURE_COSMOS_KEY', value: cosmosDb.listKeys().primaryMasterKey} {name: 'TENANT_ID', value: tenant().tenantId } {name: 'CLIENT_ID', value: enterpriseAppClientId } {name: 'SECRET_KEY', value: !empty(enterpriseAppClientSecret) ? enterpriseAppClientSecret : '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/enterprise-app-client-secret)' } {name: 'MICROSOFT_PROVIDER_AUTHENTICATION_SECRET', value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/enterprise-app-client-secret)'} {name: 'DOCKER_REGISTRY_SERVER_URL', value: 'https://${acrService.name}${acrDomain}' } - //{name: 'DOCKER_REGISTRY_SERVER_USERNAME', value: acrService.listCredentials().username } - //{name: 'DOCKER_REGISTRY_SERVER_PASSWORD', value: acrService.listCredentials().passwords[0].value } + {name: 'DOCKER_REGISTRY_SERVER_USERNAME', value: acrService.listCredentials().username } + {name: 'DOCKER_REGISTRY_SERVER_PASSWORD', value: authenticationType == 'Key' ? '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/container-registry-key)' : '' } {name: 'WEBSITE_AUTH_AAD_ALLOWED_TENANTS', value: tenant().tenantId } {name: 'AZURE_OPENAI_RESOURCE_NAME', value: openAiService.name} {name: 'AZURE_OPENAI_RESOURCE_GROUP_NAME', value: openAiResourceGroupName} {name: 'AZURE_OPENAI_URL', value: openAiService.properties.endpoint} {name: 'AZURE_SEARCH_SERVICE_NAME', value: searchService.name} - {name: 'AZURE_SEARCH_API_KEY', value: searchService.listAdminKeys().primaryKey} + {name: 'AZURE_SEARCH_API_KEY', value: authenticationType == 'Key' ? '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/search-service-key)' : ''} {name: 'AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT', value: documentIntelligence.properties.endpoint} - {name: 'AZURE_DOCUMENT_INTELLIGENCE_API_KEY', value: documentIntelligence.listKeys().key1} + {name: 'AZURE_DOCUMENT_INTELLIGENCE_API_KEY', value: authenticationType == 'Key' ? '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/document-intelligence-key)' : ''} {name: 'APPINSIGHTS_INSTRUMENTATIONKEY', value: appInsights.properties.InstrumentationKey} {name: 'APPLICATIONINSIGHTS_CONNECTION_STRING', value: appInsights.properties.ConnectionString} {name: 'APPINSIGHTS_PROFILERFEATURE_VERSION', value: '1.0.0'} @@ -118,10 +112,11 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' = { httpsOnly: true } identity: { - type: 'SystemAssigned, UserAssigned' - userAssignedIdentities: { - '${managedIdentityId}': {} - } + type: 'SystemAssigned' + // type: 'SystemAssigned, UserAssigned' + // userAssignedIdentities: { + // '${managedIdentityId}': {} + // } } tags: union(tags, { 'azd-service-name': 'web' }) } @@ -141,8 +136,6 @@ resource webAppLogging 'Microsoft.Web/sites/config@2022-03-01' = { } } -// prepare to add in app servce to have key vault secrets users rbac role. - // configure diagnostic settings for web app resource webAppDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (enableDiagLogging) { name: toLower('${webApp.name}-diagnostics') @@ -156,6 +149,68 @@ resource webAppDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-pre } } +// Configure authentication settings for the web app +resource authSettings 'Microsoft.Web/sites/config@2022-03-01' = { + name: 'authsettingsV2' + parent: webApp + properties: { + globalValidation: { + requireAuthentication: true + unauthenticatedClientAction: 'RedirectToLoginPage' + redirectToProvider: 'azureActiveDirectory' + } + identityProviders: { + azureActiveDirectory: { + enabled: true + registration: { + openIdIssuer: 'https://sts.windows.net/${tenant().tenantId}/' + clientId: enterpriseAppClientId + clientSecretSettingName: 'MICROSOFT_PROVIDER_AUTHENTICATION_SECRET' + } + validation: { + jwtClaimChecks: {} + allowedAudiences: [ + 'api://${enterpriseAppClientId}' + enterpriseAppClientId + ] + } + isAutoProvisioned: false + } + } + login: { + routes: { + logoutEndpoint: '/.auth/logout' + } + tokenStore: { + enabled: true + tokenRefreshExtensionHours: 72 + fileSystem: { + directory: '/home/data/.auth' + } + } + preserveUrlFragmentsForLogins: false + allowedExternalRedirectUrls: [] + cookieExpiration: { + convention: 'FixedTime' + timeToExpiration: '08:00:00' + } + nonce: { + validateNonce: true + nonceExpirationInterval: '00:05:00' + } + } + httpSettings: { + requireHttps: true + routes: { + apiPrefix: '/.auth' + } + forwardProxy: { + convention: 'NoProxy' + } + } + } +} + // Outputs output name string = webApp.name output defaultHostName string = webApp.properties.defaultHostName diff --git a/deployers/bicep/modules/appServiceAuthentication.bicep b/deployers/bicep/modules/appServiceAuthentication.bicep deleted file mode 100644 index 0a484a03a..000000000 --- a/deployers/bicep/modules/appServiceAuthentication.bicep +++ /dev/null @@ -1,103 +0,0 @@ -targetScope = 'resourceGroup' - -@description('The name of the web app to configure authentication for') -param webAppName string - -@description('Client ID of the Azure AD application') -param clientId string - -@description('Key Vault secret URI for client secret (recommended approach)') -#disable-next-line secure-secrets-in-params // Doesn't contain a secret -param clientSecretKeyVaultUri string = '' - -@description('Azure AD tenant ID') -param tenantId string - -@description('Allowed token audiences') -param allowedAudiences array = [] - -@description('Enable Azure AD authentication') -param enableAuthentication bool = true - -@description('Authentication action when request is not authenticated') -@allowed([ - 'AllowAnonymous' - 'RedirectToLoginPage' - 'Return401' - 'Return403' -]) -param unauthenticatedClientAction string = 'RedirectToLoginPage' - -@description('Token store enabled') -param tokenStoreEnabled bool = true - -resource webApp 'Microsoft.Web/sites@2022-03-01' existing = { - name: webAppName -} - -// Configure authentication settings for the web app -resource authSettings 'Microsoft.Web/sites/config@2022-03-01' = if (enableAuthentication && !empty(clientId)) { - name: 'authsettingsV2' - parent: webApp - properties: { - globalValidation: { - requireAuthentication: unauthenticatedClientAction != 'AllowAnonymous' - unauthenticatedClientAction: unauthenticatedClientAction - redirectToProvider: 'azureActiveDirectory' - } - identityProviders: { - azureActiveDirectory: { - enabled: true - registration: { - openIdIssuer: 'https://sts.windows.net/${tenantId}/' - clientId: clientId - clientSecretSettingName: !empty(clientSecretKeyVaultUri) ? 'MICROSOFT_PROVIDER_AUTHENTICATION_SECRET' : null - } - validation: { - jwtClaimChecks: {} - allowedAudiences: !empty(allowedAudiences) ? allowedAudiences : [ - 'api://${clientId}' - clientId - ] - } - isAutoProvisioned: false - } - } - login: { - routes: { - logoutEndpoint: '/.auth/logout' - } - tokenStore: { - enabled: tokenStoreEnabled - tokenRefreshExtensionHours: 72 - fileSystem: { - directory: '/home/data/.auth' - } - } - preserveUrlFragmentsForLogins: false - allowedExternalRedirectUrls: [] - cookieExpiration: { - convention: 'FixedTime' - timeToExpiration: '08:00:00' - } - nonce: { - validateNonce: true - nonceExpirationInterval: '00:05:00' - } - } - httpSettings: { - requireHttps: true - routes: { - apiPrefix: '/.auth' - } - forwardProxy: { - convention: 'NoProxy' - } - } - } -} - -// Output authentication configuration details -output authenticationEnabled bool = enableAuthentication && !empty(clientId) -output loginUrl string = enableAuthentication && !empty(clientId) ? 'https://${webApp.properties.defaultHostName}/.auth/login/aad' : '' -output logoutUrl string = enableAuthentication && !empty(clientId) ? 'https://${webApp.properties.defaultHostName}/.auth/logout' : '' diff --git a/deployers/bicep/modules/appInsights.bicep b/deployers/bicep/modules/applicationInsights.bicep similarity index 100% rename from deployers/bicep/modules/appInsights.bicep rename to deployers/bicep/modules/applicationInsights.bicep diff --git a/deployers/bicep/modules/azureContainerRegistry-existing.bicep b/deployers/bicep/modules/azureContainerRegistry-existing.bicep deleted file mode 100644 index a9c76808a..000000000 --- a/deployers/bicep/modules/azureContainerRegistry-existing.bicep +++ /dev/null @@ -1,18 +0,0 @@ -targetScope = 'resourceGroup' - -param acrName string -param acrResourceGroup string -param managedIdentityPrincipalId string - -// Deploy role assignment to the ACR's resource group -module roleAssignment 'azureContainerRegistry-roleAssignment.bicep' = { - name: 'acr-role-assignment' - scope: resourceGroup(acrResourceGroup) - params: { - acrName: acrName - managedIdentityPrincipalId: managedIdentityPrincipalId - } -} - -output acrName string = roleAssignment.outputs.acrName -output acrResourceGroup string = acrResourceGroup diff --git a/deployers/bicep/modules/azureContainerRegistry-roleAssignment.bicep b/deployers/bicep/modules/azureContainerRegistry-roleAssignment.bicep deleted file mode 100644 index 2065d6dd0..000000000 --- a/deployers/bicep/modules/azureContainerRegistry-roleAssignment.bicep +++ /dev/null @@ -1,27 +0,0 @@ -targetScope = 'resourceGroup' - -param acrName string -param managedIdentityPrincipalId string - -resource existingACR 'Microsoft.ContainerRegistry/registries@2025-04-01' existing = { - name: acrName -} - -// Built-in role definition ID for AcrPull -var acrPullRoleDefinitionId = subscriptionResourceId( - 'Microsoft.Authorization/roleDefinitions', - '7f951dda-4ed3-4680-a7ca-43fe172d538d' -) - -// grant the managed identity access to azure container registry as a pull contributor -resource acrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(existingACR.id, managedIdentityPrincipalId, acrPullRoleDefinitionId) - scope: existingACR - properties: { - roleDefinitionId: acrPullRoleDefinitionId - principalId: managedIdentityPrincipalId - principalType: 'ServicePrincipal' - } -} - -output acrName string = existingACR.name diff --git a/deployers/bicep/modules/azureContainerRegistry.bicep b/deployers/bicep/modules/azureContainerRegistry.bicep index 41f3b64b8..3da9c205c 100644 --- a/deployers/bicep/modules/azureContainerRegistry.bicep +++ b/deployers/bicep/modules/azureContainerRegistry.bicep @@ -4,11 +4,15 @@ param location string param acrName string param tags object -param managedIdentityPrincipalId string -param managedIdentityId string +// param managedIdentityPrincipalId string +// param managedIdentityId string param enableDiagLogging bool param logAnalyticsId string +param keyVault string +param authenticationType string +param configureApplicationPermissions bool + // Import diagnostic settings configurations module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging) { name: 'diagnosticConfigs' @@ -29,17 +33,29 @@ resource acr 'Microsoft.ContainerRegistry/registries@2025-04-01' = { tags: tags } -// grant the managed identity access to azure container registry as a pull contributor -resource acrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(acr.id, managedIdentityId, 'acr-acrpull') - scope: acr - properties: { - roleDefinitionId: subscriptionResourceId( - 'Microsoft.Authorization/roleDefinitions', - '7f951dda-4ed3-4680-a7ca-43fe172d538d' - ) - principalId: managedIdentityPrincipalId - principalType: 'ServicePrincipal' +// // grant the managed identity access to azure container registry as a pull contributor +// resource acrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (configureApplicationPermissions) { +// name: guid(acr.id, managedIdentityId, 'acr-acrpull') +// scope: acr +// properties: { +// roleDefinitionId: subscriptionResourceId( +// 'Microsoft.Authorization/roleDefinitions', +// '7f951dda-4ed3-4680-a7ca-43fe172d538d' +// ) +// principalId: managedIdentityPrincipalId +// principalType: 'ServicePrincipal' +// } +// } + +//========================================================= +// store container registry keys in key vault if using key authentication and configure app permissions = true +//========================================================= +module containerRegistrySecret 'keyVault-Secrets.bicep' = if (authenticationType == 'Key' && configureApplicationPermissions) { + name: 'storeContainerRegistrySecret' + params: { + keyVaultName: keyVault + secretName: 'container-registry-key' + secretValue: acr.listCredentials().passwords[0].value } } diff --git a/deployers/bicep/modules/contentSafety.bicep b/deployers/bicep/modules/contentSafety.bicep index 3c58dcc6c..6531c09c9 100644 --- a/deployers/bicep/modules/contentSafety.bicep +++ b/deployers/bicep/modules/contentSafety.bicep @@ -5,11 +5,13 @@ param appName string param environment string param tags object -param managedIdentityPrincipalId string -param managedIdentityId string param enableDiagLogging bool param logAnalyticsId string +param keyVault string +param authenticationType string +param configureApplicationPermissions bool + // Import diagnostic settings configurations module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging) { name: 'diagnosticConfigs' @@ -31,20 +33,6 @@ resource contentSafety 'Microsoft.CognitiveServices/accounts@2025-06-01' = { tags: tags } -// grant the managed identity access to content safety as a Cognitive Services User -resource contentSafetyUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(contentSafety.id, managedIdentityId, 'content-safety-user') - scope: contentSafety - properties: { - roleDefinitionId: subscriptionResourceId( - 'Microsoft.Authorization/roleDefinitions', - 'a97b65f3-24c7-4388-baec-2e87135dc908' - ) - principalId: managedIdentityPrincipalId - principalType: 'ServicePrincipal' - } -} - // configure diagnostic settings for content safety resource contentSafetyDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (enableDiagLogging) { name: toLower('${contentSafety.name}-diagnostics') @@ -58,5 +46,17 @@ resource contentSafetyDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05 } } +//========================================================= +// store contentSafety keys in key vault if using key authentication and configure app permissions = true +//========================================================= +module contentSafetySecret 'keyVault-Secrets.bicep' = if (authenticationType == 'Key' && configureApplicationPermissions) { + name: 'storeContentSafetySecret' + params: { + keyVaultName: keyVault + secretName: 'content-safety-key' + secretValue: contentSafety.listKeys().key1 + } +} + output contentSafetyName string = contentSafety.name output contentSafetyEndpoint string = contentSafety.properties.endpoint diff --git a/deployers/bicep/modules/cosmosDb.bicep b/deployers/bicep/modules/cosmosDb.bicep index 527d93698..fb9e2cb32 100644 --- a/deployers/bicep/modules/cosmosDb.bicep +++ b/deployers/bicep/modules/cosmosDb.bicep @@ -5,11 +5,14 @@ param appName string param environment string param tags object -param managedIdentityPrincipalId string -param managedIdentityId string param enableDiagLogging bool param logAnalyticsId string +param keyVault string +param authenticationType string +param configureApplicationPermissions bool + + // Import diagnostic settings configurations module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging) { name: 'diagnosticConfigs' @@ -69,32 +72,6 @@ resource cosmosContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/con } } -// grant the managed identity access to cosmos db as a contributor -resource cosmosContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(cosmosDb.id, managedIdentityId, 'cosmos-contributor') - scope: cosmosDb - properties: { - roleDefinitionId: subscriptionResourceId( - 'Microsoft.Authorization/roleDefinitions', - 'b24988ac-6180-42a0-ab88-20f7382dd24c' - ) - principalId: managedIdentityPrincipalId - principalType: 'ServicePrincipal' - } -} - -// Grant the managed identity Cosmos DB Built-in Data Contributor role -resource cosmosDataContributorRole 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2023-04-15' = { - name: guid(cosmosDb.id, managedIdentityPrincipalId, 'cosmos-data-contributor') - parent: cosmosDb - properties: { - // Cosmos DB Built-in Data Contributor role definition ID - roleDefinitionId: '${cosmosDb.id}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002' - principalId: managedIdentityPrincipalId - scope: cosmosDb.id - } -} - // configure diagnostic settings for cosmos db resource cosmosDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (enableDiagLogging) { name: toLower('${cosmosDb.name}-diagnostics') @@ -107,5 +84,18 @@ resource cosmosDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-pre } } +//========================================================= +// store cosmos db keys in key vault if using key authentication and configure app permissions = true +//========================================================= +module storeEnterpriseAppSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'Key' && configureApplicationPermissions) { + name: 'storeEnterpriseAppSecret' + params: { + keyVaultName: keyVault + secretName: 'cosmos-db-key' + secretValue: cosmosDb.listKeys().primaryMasterKey + } +} + + output cosmosDbName string = cosmosDb.name output cosmosDbUri string = cosmosDb.properties.documentEndpoint diff --git a/deployers/bicep/modules/createAppSecret.bicep b/deployers/bicep/modules/createAppSecret.bicep deleted file mode 100644 index 7b24aedf8..000000000 --- a/deployers/bicep/modules/createAppSecret.bicep +++ /dev/null @@ -1,113 +0,0 @@ -targetScope = 'resourceGroup' - -@description('Location for the deployment script') -param location string - -@description('Azure AD Application (Client) ID') -param applicationId string - -@description('Key Vault name where the secret will be stored') -param keyVaultName string - -@description('Name of the secret to create in Key Vault') -param secretName string = 'enterprise-app-client-secret' - -@description('Managed identity ID for the deployment script') -param managedIdentityId string - -@description('Display name for the client secret in Azure AD') -param secretDisplayName string = 'Deployment-Generated-Secret' - -@description('Number of months until the secret expires (max 24)') -@minValue(1) -@maxValue(24) -param secretExpirationMonths int = 12 - -resource createSecretScript 'Microsoft.Resources/deploymentScripts@2023-08-01' = { - name: 'create-app-secret-${uniqueString(applicationId, secretName)}' - location: location - kind: 'AzureCLI' - identity: { - type: 'UserAssigned' - userAssignedIdentities: { - '${managedIdentityId}': {} - } - } - properties: { - azCliVersion: '2.59.0' - retentionInterval: 'PT1H' - timeout: 'PT10M' - cleanupPreference: 'OnSuccess' - environmentVariables: [ - { - name: 'APPLICATION_ID' - value: applicationId - } - { - name: 'KEY_VAULT_NAME' - value: keyVaultName - } - { - name: 'SECRET_NAME' - value: secretName - } - { - name: 'SECRET_DISPLAY_NAME' - value: secretDisplayName - } - { - name: 'EXPIRATION_MONTHS' - value: string(secretExpirationMonths) - } - ] - scriptContent: ''' - #!/bin/bash - set -e - - echo "Creating client secret for Azure AD application: $APPLICATION_ID" - - # Calculate expiration date - EXPIRATION_DATE=$(date -u -d "+${EXPIRATION_MONTHS} months" +"%Y-%m-%dT%H:%M:%SZ") - - # Create the client secret using Microsoft Graph API - # Note: This requires the managed identity to have appropriate Microsoft Graph permissions - echo "Creating client secret with expiration: $EXPIRATION_DATE" - - SECRET_RESPONSE=$(az rest \ - --method POST \ - --uri "https://graph.microsoft.com/v1.0/applications(appId='$APPLICATION_ID')/addPassword" \ - --body "{\"passwordCredential\": {\"displayName\": \"$SECRET_DISPLAY_NAME\", \"endDateTime\": \"$EXPIRATION_DATE\"}}" \ - --headers "Content-Type=application/json") - - # Extract the secret value from the response - SECRET_VALUE=$(echo "$SECRET_RESPONSE" | jq -r '.secretText') - - if [ -z "$SECRET_VALUE" ] || [ "$SECRET_VALUE" = "null" ]; then - echo "Failed to create client secret" - exit 1 - fi - - echo "Client secret created successfully" - - # Store the secret in Key Vault - echo "Storing secret in Key Vault: $KEY_VAULT_NAME" - az keyvault secret set \ - --vault-name "$KEY_VAULT_NAME" \ - --name "$SECRET_NAME" \ - --value "$SECRET_VALUE" \ - --description "Client secret for Azure AD application $APPLICATION_ID (expires: $EXPIRATION_DATE)" - - echo "Secret stored successfully in Key Vault" - - # Output the secret URI (not the value) - SECRET_URI=$(az keyvault secret show \ - --vault-name "$KEY_VAULT_NAME" \ - --name "$SECRET_NAME" \ - --query id -o tsv) - - echo "{\"secretUri\": \"$SECRET_URI\"}" > $AZ_SCRIPTS_OUTPUT_PATH - ''' - } -} - -output secretUri string = createSecretScript.properties.outputs.secretUri diff --git a/deployers/bicep/modules/documentIntelligence.bicep b/deployers/bicep/modules/documentIntelligence.bicep index db27943cd..2534557a0 100644 --- a/deployers/bicep/modules/documentIntelligence.bicep +++ b/deployers/bicep/modules/documentIntelligence.bicep @@ -5,11 +5,13 @@ param appName string param environment string param tags object -param managedIdentityPrincipalId string -param managedIdentityId string param enableDiagLogging bool param logAnalyticsId string +param keyVault string +param authenticationType string +param configureApplicationPermissions bool + // Import diagnostic settings configurations module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging){ name: 'diagnosticConfigs' @@ -31,20 +33,6 @@ resource docIntel 'Microsoft.CognitiveServices/accounts@2025-06-01' = { tags: tags } -// grant the managed identity access to document intelligence as a Cognitive Services User -resource docIntelUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(docIntel.id, managedIdentityId, 'doc-intel-user') - scope: docIntel - properties: { - roleDefinitionId: subscriptionResourceId( - 'Microsoft.Authorization/roleDefinitions', - 'a97b65f3-24c7-4388-baec-2e87135dc908' - ) - principalId: managedIdentityPrincipalId - principalType: 'ServicePrincipal' - } -} - // configure diagnostic settings for document intelligence resource docIntelDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (enableDiagLogging) { name: toLower('${docIntel.name}-diagnostics') @@ -58,6 +46,18 @@ resource docIntelDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-p } } +//========================================================= +// store document intelligence keys in key vault if using key authentication and configure app permissions = true +//========================================================= +module documentIntelligenceSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'Key' && configureApplicationPermissions) { + name: 'storeDocumentIntelligenceSecret' + params: { + keyVaultName: keyVault + secretName: 'document-intelligence-key' + secretValue: docIntel.listKeys().key1 + } +} + output documentIntelligenceServiceName string = docIntel.name output diagnosticLoggingEnabled bool = enableDiagLogging output documentIntelligenceServiceEndpoint string = docIntel.properties.endpoint diff --git a/deployers/bicep/modules/enterpriseAppPermissions.bicep b/deployers/bicep/modules/enterpriseAppPermissions.bicep deleted file mode 100644 index 07943cc7b..000000000 --- a/deployers/bicep/modules/enterpriseAppPermissions.bicep +++ /dev/null @@ -1,177 +0,0 @@ -targetScope = 'resourceGroup' - -param webAppName string -param keyVaultName string -param cosmosDBName string -param openAIName string -param docIntelName string -param storageAccountName string -param speechServiceName string -param searchServiceName string -param contentSafetyName string - -resource webApp 'Microsoft.Web/sites@2022-03-01' existing = { - name: webAppName -} - -resource kv 'Microsoft.KeyVault/vaults@2025-05-01' existing = { - name: keyVaultName -} - -resource cosmosDb 'Microsoft.DocumentDB/databaseAccounts@2023-04-15' existing = { - name: cosmosDBName -} - -resource openAiService 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = if (openAIName != '') { - name: openAIName -} - -resource docIntelService 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = if (docIntelName != '') { - name: docIntelName -} - -resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' existing = { - name: storageAccountName -} - -resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = { - name: 'default' - parent: storageAccount -} - -resource speechService 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = if (speechServiceName != '') { - name: speechServiceName -} - -resource searchService 'Microsoft.Search/searchServices@2025-05-01' existing = { - name: searchServiceName -} - -resource contentSafety 'Microsoft.CognitiveServices/accounts@2025-06-01' existing = if (contentSafetyName != '') { - name: contentSafetyName -} - -// grant the webApp access to the key vault -resource kvSecretsUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(kv.id, webApp.id, 'kv-secrets-user') - scope: kv - properties: { - // Built-in role definition id for "Key Vault Secrets User" - roleDefinitionId: subscriptionResourceId( - 'Microsoft.Authorization/roleDefinitions', - '4633458b-17de-408a-b874-0445c86b69e6' - ) - principalId: webApp.identity.principalId - principalType: 'ServicePrincipal' - } -} - -// grant the webApp access to cosmos db as a contributor -resource cosmosContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(cosmosDb.id, webApp.id, 'cosmos-contributor') - scope: cosmosDb - properties: { - roleDefinitionId: subscriptionResourceId( - 'Microsoft.Authorization/roleDefinitions', - 'b24988ac-6180-42a0-ab88-20f7382dd24c' - ) - principalId: webApp.identity.principalId - principalType: 'ServicePrincipal' - } -} - -// Grant the managed identity Cosmos DB Built-in Data Contributor role -resource cosmosDataContributorRole 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2023-04-15' = { - name: guid(cosmosDb.id, webApp.id, 'cosmos-data-contributor') - parent: cosmosDb - properties: { - // Cosmos DB Built-in Data Contributor role definition ID - roleDefinitionId: '${cosmosDb.id}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002' - principalId: webApp.identity.principalId - scope: cosmosDb.id - } -} - -// Grant the openai service access cognitive services openai user -resource openAIUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (openAIName != '') { - name: guid(openAiService.id, webApp.id, 'openai-user') - scope: openAiService - properties: { - roleDefinitionId: subscriptionResourceId( - 'Microsoft.Authorization/roleDefinitions', - '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' - ) - principalId: webApp.identity.principalId - principalType: 'ServicePrincipal' - } -} - -// grant the managed identity access to document intelligence as a Cognitive Services User -resource docIntelUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (docIntelName != '') { - name: guid(docIntelService.id, webApp.id, 'doc-intel-user') - scope: docIntelService - properties: { - roleDefinitionId: subscriptionResourceId( - 'Microsoft.Authorization/roleDefinitions', - 'a97b65f3-24c7-4388-baec-2e87135dc908' - ) - principalId: webApp.identity.principalId - principalType: 'ServicePrincipal' - } -} - -// grant the managed identity access to the storage account as a blob data contributor -resource storageBlobDataContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(storageAccount.id, webApp.id, 'storage-blob-data-contributor') - scope: storageAccount - properties: { - roleDefinitionId: subscriptionResourceId( - 'Microsoft.Authorization/roleDefinitions', - 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' - ) - principalId: webApp.identity.principalId - principalType: 'ServicePrincipal' - } -} - -// grant the managed identity access to speech service as a Cognitive Services User -resource speechServiceUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (speechServiceName != '') { - name: guid(speechService.id, webApp.id, 'speech-service-user') - scope: speechService - properties: { - roleDefinitionId: subscriptionResourceId( - 'Microsoft.Authorization/roleDefinitions', - 'a97b65f3-24c7-4388-baec-2e87135dc908' - ) - principalId: webApp.identity.principalId - principalType: 'ServicePrincipal' - } -} - -// grant the managed identity access to search service as a Search Service Contributor -resource searchServiceContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(searchService.id, webApp.id, 'search-service-contributor') - scope: searchService - properties: { - roleDefinitionId: subscriptionResourceId( - 'Microsoft.Authorization/roleDefinitions', - '8ebe5a00-799e-43f5-93ac-243d3dce84a7' - ) - principalId: webApp.identity.principalId - principalType: 'ServicePrincipal' - } -} - -// grant the managed identity access to content safety as a Cognitive Services User -resource contentSafetyUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (contentSafetyName != '') { - name: guid(contentSafety.id, webApp.id, 'content-safety-user') - scope: contentSafety - properties: { - roleDefinitionId: subscriptionResourceId( - 'Microsoft.Authorization/roleDefinitions', - 'a97b65f3-24c7-4388-baec-2e87135dc908' - ) - principalId: webApp.identity.principalId - principalType: 'ServicePrincipal' - } -} diff --git a/deployers/bicep/modules/enterpriseApplication.bicep b/deployers/bicep/modules/enterpriseApplication.bicep deleted file mode 100644 index 30077aecf..000000000 --- a/deployers/bicep/modules/enterpriseApplication.bicep +++ /dev/null @@ -1,91 +0,0 @@ -targetScope = 'resourceGroup' - -@description('The name of the application to be deployed') -param appName string - -@description('The environment name (dev/test/prod)') -param environment string - -@description('The redirect URI for the application') -param redirectUri string - -@description('Application description') -param appDescription string = 'Enterprise application for ${appName} ${environment} environment' - -@description('Application display name') -param displayName string = '${appName}-${environment}-app' - -@description('Required application permissions/scopes') -param requiredResourceAccess array = [ - { - resourceAppId: '00000003-0000-0000-c000-000000000000' // Microsoft Graph - resourceAccess: [ - { - id: 'e1fe6dd8-ba31-4d61-89e7-88639da4683d' // User.Read - type: 'Scope' - } - { - id: '14dad69e-099b-42c9-810b-d002981feec1' // Profile.Read - type: 'Scope' - } - { - id: '37f7f235-527c-4136-accd-4a02d197296e' // openid - type: 'Scope' - } - { - id: '7427e0e9-2fba-42fe-b0c0-848c9e6a8182' // offline_access - type: 'Scope' - } - { - id: '64a6cdd6-aab1-4aaf-94b8-3cc8405e90d0' // email - type: 'Scope' - } - { - id: '5f8c59db-677d-491f-a6b8-5f174b11ec1d' // Group.Read.All - type: 'Scope' - } - ] - } -] - -@description('Supported account types for the application') -@allowed([ - 'AzureADMyOrg' - 'AzureADMultipleOrgs' - 'AzureADandPersonalMicrosoftAccount' -]) -param signInAudience string = 'AzureADMyOrg' - -// Note: Azure AD App Registration requires Microsoft Graph API which is not directly supported in Bicep -// This module creates the configuration that can be used by Azure CLI or PowerShell scripts -// The actual app registration will need to be created using az ad app create or equivalent - -var appRegistrationConfig = { - displayName: displayName - description: appDescription - signInAudience: signInAudience - web: { - redirectUris: [ - redirectUri - '${redirectUri}/.auth/login/aad/callback' - ] - implicitGrantSettings: { - enableAccessTokenIssuance: false - enableIdTokenIssuance: true - } - } - requiredResourceAccess: requiredResourceAccess - api: { - requestedAccessTokenVersion: 2 - } -} - -// Output the configuration that can be used for app registration -output appRegistrationConfig object = appRegistrationConfig -output displayName string = displayName -output redirectUri string = redirectUri -output callbackUri string = '${redirectUri}/.auth/login/aad/callback' - -// Output placeholder values that would be populated after app registration -output clientId string = '' // Will be populated after app registration -output tenantId string = tenant().tenantId diff --git a/deployers/bicep/modules/keyVault-Secrets.bicep b/deployers/bicep/modules/keyVault-Secrets.bicep new file mode 100644 index 000000000..acd09328b --- /dev/null +++ b/deployers/bicep/modules/keyVault-Secrets.bicep @@ -0,0 +1,23 @@ +targetScope = 'resourceGroup' + +param keyVaultName string +param secretName string +@secure() +param secretValue string + +resource kv 'Microsoft.KeyVault/vaults@2025-05-01' existing = { + name: keyVaultName +} + +resource secret 'Microsoft.KeyVault/vaults/secrets@2025-05-01' = { + name: secretName + parent: kv + properties: { + value: secretValue + } +} + +//------------------------------------------------ +// output values +//------------------------------------------------ +output SecretUri string = '${kv.properties.vaultUri}secrets/${secretName}' diff --git a/deployers/bicep/modules/keyVault.bicep b/deployers/bicep/modules/keyVault.bicep index 194ca5839..e3c6729f2 100644 --- a/deployers/bicep/modules/keyVault.bicep +++ b/deployers/bicep/modules/keyVault.bicep @@ -5,21 +5,11 @@ param appName string param environment string param tags object -param managedIdentityPrincipalId string -param managedIdentityId string +// param managedIdentityPrincipalId string +// param managedIdentityId string param enableDiagLogging bool param logAnalyticsId string -@description('Enable enterprise app authentication') -param enableEnterpriseApp bool - -@description('Enterprise app client ID - used for documentation') -param enterpriseAppClientId string - -@description('Enterprise app client secret to store in Key Vault') -@secure() -param enterpriseAppClientSecret string - // Import diagnostic settings configurations module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging) { name: 'diagnosticConfigs' @@ -45,20 +35,20 @@ resource kv 'Microsoft.KeyVault/vaults@2024-11-01' = { tags: tags } -// grant the managed identity access to the key vault -resource kvSecretsUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(kv.id, managedIdentityId, 'kv-secrets-user') - scope: kv - properties: { - // Built-in role definition id for "Key Vault Secrets User" - roleDefinitionId: subscriptionResourceId( - 'Microsoft.Authorization/roleDefinitions', - '4633458b-17de-408a-b874-0445c86b69e6' - ) - principalId: managedIdentityPrincipalId - principalType: 'ServicePrincipal' - } -} +// // grant the managed identity access to the key vault +// resource kvSecretsUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { +// name: guid(kv.id, managedIdentityId, 'kv-secrets-user') +// scope: kv +// properties: { +// // Built-in role definition id for "Key Vault Secrets User" +// roleDefinitionId: subscriptionResourceId( +// 'Microsoft.Authorization/roleDefinitions', +// '4633458b-17de-408a-b874-0445c86b69e6' +// ) +// principalId: managedIdentityPrincipalId +// principalType: 'ServicePrincipal' +// } +// } // configure diagnostic settings for key vault resource kvDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (enableDiagLogging) { @@ -73,17 +63,7 @@ resource kvDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview } } -// Store the enterprise app client secret in Key Vault if provided -resource enterpriseAppSecret 'Microsoft.KeyVault/vaults/secrets@2024-11-01' = if (enableEnterpriseApp && !empty(enterpriseAppClientSecret)) { - name: 'enterprise-app-client-secret' - parent: kv - properties: { - value: enterpriseAppClientSecret - contentType: 'Client secret for Azure AD enterprise app ${enterpriseAppClientId}' - } -} - output keyVaultId string = kv.id output keyVaultName string = kv.name output keyVaultUri string = kv.properties.vaultUri -output enterpriseAppClientSecretUri string = enableEnterpriseApp ? '${kv.properties.vaultUri}secrets/enterprise-app-client-secret' : '' + diff --git a/deployers/bicep/modules/logAnalytics.bicep b/deployers/bicep/modules/logAnalyticsWorkspace.bicep similarity index 100% rename from deployers/bicep/modules/logAnalytics.bicep rename to deployers/bicep/modules/logAnalyticsWorkspace.bicep diff --git a/deployers/bicep/modules/managedIdentity.bicep b/deployers/bicep/modules/managedIdentity.bicep index be883e74a..8389cbb2c 100644 --- a/deployers/bicep/modules/managedIdentity.bicep +++ b/deployers/bicep/modules/managedIdentity.bicep @@ -12,6 +12,7 @@ resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023- tags: tags } +output clientId string = managedIdentity.properties.clientId output principalId string = managedIdentity.properties.principalId output resourceId string = managedIdentity.id -output clientId string = managedIdentity.properties.clientId + diff --git a/deployers/bicep/modules/openAI-existing.bicep b/deployers/bicep/modules/openAI-existing.bicep deleted file mode 100644 index 4e09c2743..000000000 --- a/deployers/bicep/modules/openAI-existing.bicep +++ /dev/null @@ -1,42 +0,0 @@ -targetScope = 'resourceGroup' - -param openAIName string - -resource existingOpenAI 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = { - name: openAIName -} - -// deploy GPT-4o model to the new OpenAI resource -module aiModel_gpt4o 'aiModel.bicep' = { - name: 'gpt-4o' - params: { - parent: existingOpenAI.name - modelName: 'gpt-4o' - modelVersion: '2024-11-20' - skuName: 'GlobalStandard' - skuCapacity: 100 - } -} - -// deploy Text Embedding model to the new OpenAI resource -module aiModel_textEmbedding 'aiModel.bicep' = { - name: 'text-embedding' - params: { - parent: existingOpenAI.name - modelName: 'text-embedding-3-small' - modelVersion: '1' - skuName: 'GlobalStandard' - skuCapacity: 150 - } -dependsOn: [ - aiModel_gpt4o - ] -} - -output openAIName string = existingOpenAI.name -output openAIResourceGroup string = resourceGroup().name - -output openAIEndpoint string = existingOpenAI.properties.endpoint - - - diff --git a/deployers/bicep/modules/openAI.bicep b/deployers/bicep/modules/openAI.bicep index 78c951306..e7a8df11e 100644 --- a/deployers/bicep/modules/openAI.bicep +++ b/deployers/bicep/modules/openAI.bicep @@ -5,18 +5,23 @@ param appName string param environment string param tags object -//param managedIdentityPrincipalId string -param managedIdentityId string param enableDiagLogging bool param logAnalyticsId string +param keyVault string +param authenticationType string +param configureApplicationPermissions bool + +param gptModels array +param embeddingModels array + // Import diagnostic settings configurations module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging) { name: 'diagnosticConfigs' } // deploy new Azure OpenAI Resource -resource newOpenAI 'Microsoft.CognitiveServices/accounts@2024-10-01' = { +resource openAI 'Microsoft.CognitiveServices/accounts@2024-10-01' = { name: toLower('${appName}-${environment}-openai') location: location kind: 'OpenAI' @@ -24,10 +29,11 @@ resource newOpenAI 'Microsoft.CognitiveServices/accounts@2024-10-01' = { name: 'S0' } identity: { - type: 'SystemAssigned, UserAssigned' - userAssignedIdentities: { - '${managedIdentityId}': {} - } + type: 'SystemAssigned' + // type: 'SystemAssigned, UserAssigned' + // userAssignedIdentities: { + // '${managedIdentityId}': {} + // } } properties: { publicNetworkAccess: 'Enabled' @@ -38,9 +44,9 @@ resource newOpenAI 'Microsoft.CognitiveServices/accounts@2024-10-01' = { } // configure diagnostic settings for OpenAI Resource if required -resource openAIDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (enableDiagLogging) { - name: toLower('${newOpenAI.name}-diagnostics') - scope: newOpenAI +resource openAIDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (enableDiagLogging) { + name: toLower('${openAI.name}-diagnostics') + scope: openAI properties: { workspaceId: logAnalyticsId #disable-next-line BCP318 // expect one value to be null @@ -50,38 +56,35 @@ resource openAIDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-pre } } -// deploy GPT-4o model to the new OpenAI resource -module aiModel_gpt4o 'aiModel.bicep' = { - name: 'gpt-4o' - params: { - parent: newOpenAI.name - modelName: 'gpt-4o' - modelVersion: '2024-11-20' - skuName: 'GlobalStandard' - skuCapacity: 100 +// deploy AI models defined in the input arrays +@batchSize(1) +module aiModel 'aiModel.bicep' = [ + for (model, i) in concat(gptModels, embeddingModels): { + name: 'model-${replace(model.modelName, '.', '-')}-${i}' + params: { + parent: openAI.name + modelName: model.modelName + modelVersion: model.modelVersion + skuName: model.skuName + skuCapacity: model.skuCapacity + } } -} +] -// deploy Text Embedding model to the new OpenAI resource -module aiModel_textEmbedding 'aiModel.bicep' = { - name: 'text-embedding' +//========================================================= +// store openAI keys in key vault if using key authentication and configure app permissions = true +//========================================================= +module openAISecret 'keyVault-Secrets.bicep' = if (authenticationType == 'Key' && configureApplicationPermissions) { + name: 'storeOpenAISecret' params: { - parent: newOpenAI.name - modelName: 'text-embedding-3-small' - modelVersion: '1' - skuName: 'GlobalStandard' - skuCapacity: 150 + keyVaultName: keyVault + secretName: 'openAi-key' + secretValue: openAI.listKeys().key1 } -dependsOn: [ - aiModel_gpt4o - ] } -output openAIName string = newOpenAI.name +output openAIName string = openAI.name output openAIResourceGroup string = resourceGroup().name -output openAIEndpoint string = newOpenAI.properties.endpoint -output openAIGptModel string = aiModel_gpt4o.outputs.modelName -output openAITextEmbeddingModel string = aiModel_textEmbedding.outputs.modelName - - - +output openAIEndpoint string = openAI.properties.endpoint +// output openAIGptModel string = aiModel_gpt4o.outputs.modelName +// output openAITextEmbeddingModel string = aiModel_textEmbedding.outputs.modelName diff --git a/deployers/bicep/modules/redisCache.bicep b/deployers/bicep/modules/redisCache.bicep index 796a2c1a9..2b2b38fae 100644 --- a/deployers/bicep/modules/redisCache.bicep +++ b/deployers/bicep/modules/redisCache.bicep @@ -5,11 +5,13 @@ param appName string param environment string param tags object -//param managedIdentityPrincipalId string -//param managedIdentityId string param enableDiagLogging bool param logAnalyticsId string +param keyVault string +param authenticationType string +param configureApplicationPermissions bool + // Import diagnostic settings configurations module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging) { name: 'diagnosticConfigs' @@ -34,22 +36,6 @@ resource redisCache 'Microsoft.Cache/Redis@2024-11-01' = { tags: tags } -// todo: grant the managed identity access to content safety as a Cognitive Services User -/* -resource contentSafetyUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (deployContentSafety) { - name: guid(contentSafety.id, managedIdentity.id, 'content-safety-user') - scope: contentSafety - properties: { - roleDefinitionId: subscriptionResourceId( - 'Microsoft.Authorization/roleDefinitions', - 'a97b65f3-24c7-4388-baec-2e87135dc908' - ) - principalId: managedIdentity.properties.principalId - principalType: 'ServicePrincipal' - } -} -*/ - // configure diagnostic settings for redis cache resource redisCacheDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (enableDiagLogging) { name: toLower('${redisCache.name}-diagnostics') @@ -62,3 +48,17 @@ resource redisCacheDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01 metrics: diagnosticConfigs.outputs.standardMetricsCategories } } + +//========================================================= +// store redis cache keys in key vault if using key authentication and configure app permissions = true +//========================================================= +module redisCacheSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'Key' && configureApplicationPermissions) { + name: 'storeRedisCacheSecret' + params: { + keyVaultName: keyVault + secretName: 'redis-cache-key' + secretValue: redisCache.listKeys().primaryKey + } +} + +output redisCacheName string = redisCache.name diff --git a/deployers/bicep/modules/search.bicep b/deployers/bicep/modules/search.bicep index c8821e44a..6fb2791a0 100644 --- a/deployers/bicep/modules/search.bicep +++ b/deployers/bicep/modules/search.bicep @@ -5,11 +5,13 @@ param appName string param environment string param tags object -param managedIdentityPrincipalId string -param managedIdentityId string param enableDiagLogging bool param logAnalyticsId string +param keyVault string +param authenticationType string +param configureApplicationPermissions bool + // Import diagnostic settings configurations module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging) { name: 'diagnosticConfigs' @@ -23,6 +25,7 @@ resource searchService 'Microsoft.Search/searchServices@2025-05-01' = { name: 'basic' } properties: { + #disable-next-line BCP036 // template is incorrect hostingMode: 'default' publicNetworkAccess: 'Enabled' replicaCount: 1 @@ -31,20 +34,6 @@ resource searchService 'Microsoft.Search/searchServices@2025-05-01' = { tags: tags } -// grant the managed identity access to search service as a search index data contributor -resource searchContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(searchService.id, managedIdentityId, 'search-contributor') - scope: searchService - properties: { - roleDefinitionId: subscriptionResourceId( - 'Microsoft.Authorization/roleDefinitions', - '8ebe5a00-799e-43f5-93ac-243d3dce84a7' - ) - principalId: managedIdentityPrincipalId - principalType: 'ServicePrincipal' - } -} - // configure diagnostic settings for search service resource searchDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (enableDiagLogging) { name: toLower('${searchService.name}-diagnostics') @@ -58,5 +47,17 @@ resource searchDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-pre } } +//========================================================= +// store search Service keys in key vault if using key authentication and configure app permissions = true +//========================================================= +module searchServiceSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'Key' && configureApplicationPermissions) { + name: 'storeSearchServiceSecret' + params: { + keyVaultName: keyVault + secretName: 'search-service-key' + secretValue: searchService.listAdminKeys().primaryKey + } +} + output searchServiceName string = searchService.name output searchServiceEndpoint string = searchService.properties.endpoint diff --git a/deployers/bicep/modules/setPermissions.bicep b/deployers/bicep/modules/setPermissions.bicep new file mode 100644 index 000000000..2033982d8 --- /dev/null +++ b/deployers/bicep/modules/setPermissions.bicep @@ -0,0 +1,193 @@ +targetScope = 'resourceGroup' + +param webAppName string +param authenticationType string +param keyVaultName string +param cosmosDBName string +param acrName string +param openAIName string +// param openAIResourceGroupName string +param docIntelName string +// param storageAccountName string +// param speechServiceName string +// param searchServiceName string +// param contentSafetyName string + +resource webApp 'Microsoft.Web/sites@2022-03-01' existing = { + name: webAppName +} + +resource kv 'Microsoft.KeyVault/vaults@2025-05-01' existing = { + name: keyVaultName +} + +resource cosmosDb 'Microsoft.DocumentDB/databaseAccounts@2023-04-15' existing = { + name: cosmosDBName +} + +resource acr 'Microsoft.ContainerRegistry/registries@2025-04-01' existing = { + name: acrName +} + +resource openAiService 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = { + name: openAIName +} + +resource docIntelService 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = if (docIntelName != '') { + name: docIntelName +} + +// resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' existing = { +// name: storageAccountName +// } + +// resource speechService 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = if (speechServiceName != '') { +// name: speechServiceName +// } + +// resource searchService 'Microsoft.Search/searchServices@2025-05-01' existing = { +// name: searchServiceName +// } + +// resource contentSafety 'Microsoft.CognitiveServices/accounts@2025-06-01' existing = if (contentSafetyName != '') { +// name: contentSafetyName +// } + +// grant the webApp access to the key vault +resource kvSecretsUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(kv.id, webApp.id, 'kv-secrets-user') + scope: kv + properties: { + // Built-in role definition id for "Key Vault Secrets User" + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '4633458b-17de-408a-b874-0445c86b69e6' + ) + principalId: webApp.identity.principalId + principalType: 'ServicePrincipal' + } +} + +// grant the webApp access to cosmos db as a contributor +resource cosmosContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'Managed_Identity') { + name: guid(cosmosDb.id, webApp.id, 'cosmos-contributor') + scope: cosmosDb + properties: { + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'b24988ac-6180-42a0-ab88-20f7382dd24c' + ) + principalId: webApp.identity.principalId + principalType: 'ServicePrincipal' + } +} + +// Grant the managed identity Cosmos DB Built-in Data Contributor role +resource cosmosDataContributorRole 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2023-04-15' = if (authenticationType == 'Managed_Identity') { + name: guid(cosmosDb.id, webApp.id, 'cosmos-data-contributor') + parent: cosmosDb + properties: { + // Cosmos DB Built-in Data Contributor role definition ID + roleDefinitionId: '${cosmosDb.id}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002' + principalId: webApp.identity.principalId + scope: cosmosDb.id + } +} + +// grant the webApp access to the ACR with acrpull role +resource acrPullRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(acr.id, webApp.id, 'acr-pull-role') + scope: acr + properties: { + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '7f951dda-4ed3-4680-a7ca-43fe172d538d' // ACR Pull role + ) + principalId: webApp.identity.principalId + principalType: 'ServicePrincipal' + } +} + +// Grant the openai service access cognitive services openai user +resource openAIUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'Managed_Identity') { + scope: openAiService + name: guid(openAiService.id, webApp.id, 'openai-user') + properties: { + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' + ) + principalId: webApp.identity.principalId + principalType: 'ServicePrincipal' + } +} + +// grant the managed identity access to document intelligence as a Cognitive Services User +resource docIntelUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (docIntelName != '' && authenticationType == 'Managed_Identity') { + name: guid(docIntelService.id, webApp.id, 'doc-intel-user') + scope: docIntelService + properties: { + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'a97b65f3-24c7-4388-baec-2e87135dc908' + ) + principalId: webApp.identity.principalId + principalType: 'ServicePrincipal' + } +} + +// // grant the managed identity access to the storage account as a blob data contributor +// resource storageBlobDataContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'Managed_Identity') { +// name: guid(storageAccount.id, webApp.id, 'storage-blob-data-contributor') +// scope: storageAccount +// properties: { +// roleDefinitionId: subscriptionResourceId( +// 'Microsoft.Authorization/roleDefinitions', +// 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' +// ) +// principalId: webApp.identity.principalId +// principalType: 'ServicePrincipal' +// } +// } + +// // grant the managed identity access to speech service as a Cognitive Services User +// resource speechServiceUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (speechServiceName != '' && authenticationType == 'Managed_Identity') { +// name: guid(speechService.id, webApp.id, 'speech-service-user') +// scope: speechService +// properties: { +// roleDefinitionId: subscriptionResourceId( +// 'Microsoft.Authorization/roleDefinitions', +// 'a97b65f3-24c7-4388-baec-2e87135dc908' +// ) +// principalId: webApp.identity.principalId +// principalType: 'ServicePrincipal' +// } +// } + +// // grant the managed identity access to search service as a Search Service Contributor +// resource searchServiceContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'Managed_Identity') { +// name: guid(searchService.id, webApp.id, 'search-service-contributor') +// scope: searchService +// properties: { +// roleDefinitionId: subscriptionResourceId( +// 'Microsoft.Authorization/roleDefinitions', +// '8ebe5a00-799e-43f5-93ac-243d3dce84a7' +// ) +// principalId: webApp.identity.principalId +// principalType: 'ServicePrincipal' +// } +// } + +// // grant the managed identity access to content safety as a Cognitive Services User +// resource contentSafetyUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (contentSafetyName != '' && authenticationType == 'Managed_Identity') { +// name: guid(contentSafety.id, webApp.id, 'content-safety-user') +// scope: contentSafety +// properties: { +// roleDefinitionId: subscriptionResourceId( +// 'Microsoft.Authorization/roleDefinitions', +// 'a97b65f3-24c7-4388-baec-2e87135dc908' +// ) +// principalId: webApp.identity.principalId +// principalType: 'ServicePrincipal' +// } +// } diff --git a/deployers/bicep/modules/speechService.bicep b/deployers/bicep/modules/speechService.bicep index e5c703899..26f3cc4d7 100644 --- a/deployers/bicep/modules/speechService.bicep +++ b/deployers/bicep/modules/speechService.bicep @@ -5,11 +5,13 @@ param appName string param environment string param tags object -param managedIdentityPrincipalId string -param managedIdentityId string param enableDiagLogging bool param logAnalyticsId string +param keyVault string +param authenticationType string +param configureApplicationPermissions bool + // Import diagnostic settings configurations module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging){ name: 'diagnosticConfigs' @@ -24,10 +26,7 @@ resource speechService 'Microsoft.CognitiveServices/accounts@2024-10-01' = { name: 'S0' } identity: { - type: 'SystemAssigned, UserAssigned' - userAssignedIdentities: { - '${managedIdentityId}': {} - } + type: 'SystemAssigned' } properties: { publicNetworkAccess: 'Enabled' @@ -37,20 +36,6 @@ resource speechService 'Microsoft.CognitiveServices/accounts@2024-10-01' = { tags: tags } -// grant the managed identity access to speech service as a Cognitive Services User -resource speechServiceUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(speechService.id, managedIdentityId, 'speech-service-user') - scope: speechService - properties: { - roleDefinitionId: subscriptionResourceId( - 'Microsoft.Authorization/roleDefinitions', - 'a97b65f3-24c7-4388-baec-2e87135dc908' - ) - principalId: managedIdentityPrincipalId - principalType: 'ServicePrincipal' - } -} - // configure diagnostic settings for speech service resource speechServiceDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (enableDiagLogging) { name: toLower('${speechService.name}-diagnostics') @@ -64,4 +49,17 @@ resource speechServiceDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05 } } +//========================================================= +// store speech Service keys in key vault if using key authentication and configure app permissions = true +//========================================================= +module speechServiceSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'Key' && configureApplicationPermissions) { + name: 'storeSpeechServiceSecret' + params: { + keyVaultName: keyVault + secretName: 'speech-service-key' + secretValue: speechService.listKeys().key1 + } +} + output speechServiceName string = speechService.name +output speechServiceEndpoint string = speechService.properties.endpoint diff --git a/deployers/bicep/modules/storageAccount.bicep b/deployers/bicep/modules/storageAccount.bicep index 446904614..0ae61b3a2 100644 --- a/deployers/bicep/modules/storageAccount.bicep +++ b/deployers/bicep/modules/storageAccount.bicep @@ -5,11 +5,13 @@ param appName string param environment string param tags object -param managedIdentityPrincipalId string -param managedIdentityId string param enableDiagLogging bool param logAnalyticsId string +param keyVault string +param authenticationType string +param configureApplicationPermissions bool + // Import diagnostic settings configurations module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging){ name: 'diagnosticConfigs' @@ -56,20 +58,6 @@ resource groupDocumentsContainer 'Microsoft.Storage/storageAccounts/blobServices } } -// grant the managed identity access to the storage account as a blob data contributor -resource storageBlobDataContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(storageAccount.id, managedIdentityId, 'storage-blob-data-contributor') - scope: storageAccount - properties: { - roleDefinitionId: subscriptionResourceId( - 'Microsoft.Authorization/roleDefinitions', - 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' - ) - principalId: managedIdentityPrincipalId - principalType: 'ServicePrincipal' - } -} - // configure diagnostic settings for storage account resource storageDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (enableDiagLogging) { name: toLower('${storageAccount.name}-diagnostics') @@ -94,5 +82,17 @@ resource storageDiagnosticsBlob 'Microsoft.Insights/diagnosticSettings@2021-05-0 } } +//========================================================= +// store storage keys in key vault if using key authentication and configure app permissions = true +//========================================================= +module storageAccountSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'Key' && configureApplicationPermissions) { + name: 'storeStorageAccountSecret' + params: { + keyVaultName: keyVault + secretName: 'storage-account-key' + secretValue: storageAccount.listKeys().keys[0].value + } +} + output name string = storageAccount.name output endpoint string = storageAccount.properties.primaryEndpoints.blob diff --git a/deployers/bicep/postconfig.py b/deployers/bicep/postconfig.py index 599e43517..516fa2f1f 100644 --- a/deployers/bicep/postconfig.py +++ b/deployers/bicep/postconfig.py @@ -29,7 +29,7 @@ } # Get values from environment variables -var_enableEnterpriseApp = os.getenv("var_enableEnterpriseApp") # expected to be true or false... will determine if key based or managed identity is used +var_authenticationType = os.getenv("var_authenticationType") # expected to be true or false... will determine if key based or managed identity is used var_openAIEndpoint=os.getenv("var_openAIEndpoint") var_openAIResourceGroup=os.getenv("var_openAIResourceGroup") From eaf846be530d454ed0a2625a190459005001e0fc Mon Sep 17 00:00:00 2001 From: SteveCInVA Date: Mon, 1 Dec 2025 14:15:18 +0000 Subject: [PATCH 06/22] Refactor Bicep modules to conditionally add settings based on authentication type and enable resource declarations for services --- deployers/bicep/modules/appService.bicep | 21 ++- deployers/bicep/modules/setPermissions.bicep | 146 +++++++++---------- 2 files changed, 87 insertions(+), 80 deletions(-) diff --git a/deployers/bicep/modules/appService.bicep b/deployers/bicep/modules/appService.bicep index 01eeae15a..27f835e19 100644 --- a/deployers/bicep/modules/appService.bicep +++ b/deployers/bicep/modules/appService.bicep @@ -75,25 +75,32 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' = { {name: 'SCM_DO_BUILD_DURING_DEPLOYMENT', value: 'false'} {name: 'AZURE_COSMOS_ENDPOINT', value: cosmosDb.properties.documentEndpoint} {name: 'AZURE_COSMOS_AUTHENTICATION_TYPE', value: authenticationType} - - {name: 'AZURE_COSMOS_KEY', value: authenticationType == 'Key' ? '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/cosmos-db-key)' : ''} - + + // Only add this setting if authenticationType is 'Key' + ...(authenticationType == 'Key' ? [{name: 'AZURE_COSMOS_KEY', value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/cosmos-db-key)'}] : []) {name: 'TENANT_ID', value: tenant().tenantId } {name: 'CLIENT_ID', value: enterpriseAppClientId } {name: 'SECRET_KEY', value: !empty(enterpriseAppClientSecret) ? enterpriseAppClientSecret : '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/enterprise-app-client-secret)' } {name: 'MICROSOFT_PROVIDER_AUTHENTICATION_SECRET', value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/enterprise-app-client-secret)'} {name: 'DOCKER_REGISTRY_SERVER_URL', value: 'https://${acrService.name}${acrDomain}' } - {name: 'DOCKER_REGISTRY_SERVER_USERNAME', value: acrService.listCredentials().username } - {name: 'DOCKER_REGISTRY_SERVER_PASSWORD', value: authenticationType == 'Key' ? '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/container-registry-key)' : '' } + + // Only add this setting if authenticationType is 'Key' + ...(authenticationType == 'Key' ? [{name: 'DOCKER_REGISTRY_SERVER_USERNAME', value: acrService.listCredentials().username}] : []) + + // Only add this setting if authenticationType is 'Key' + ...(authenticationType == 'Key' ? [{name: 'DOCKER_REGISTRY_SERVER_PASSWORD', value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/container-registry-key)'}] : []) + {name: 'WEBSITE_AUTH_AAD_ALLOWED_TENANTS', value: tenant().tenantId } {name: 'AZURE_OPENAI_RESOURCE_NAME', value: openAiService.name} {name: 'AZURE_OPENAI_RESOURCE_GROUP_NAME', value: openAiResourceGroupName} {name: 'AZURE_OPENAI_URL', value: openAiService.properties.endpoint} {name: 'AZURE_SEARCH_SERVICE_NAME', value: searchService.name} - {name: 'AZURE_SEARCH_API_KEY', value: authenticationType == 'Key' ? '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/search-service-key)' : ''} + // Only add this setting if authenticationType is 'Key' + ...(authenticationType == 'Key' ? [{name: 'AZURE_SEARCH_API_KEY', value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/search-service-key)'}] : []) {name: 'AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT', value: documentIntelligence.properties.endpoint} - {name: 'AZURE_DOCUMENT_INTELLIGENCE_API_KEY', value: authenticationType == 'Key' ? '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/document-intelligence-key)' : ''} + // Only add this setting if authenticationType is 'Key' + ...(authenticationType == 'Key' ? [{name: 'AZURE_DOCUMENT_INTELLIGENCE_API_KEY', value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/document-intelligence-key)'}] : []) {name: 'APPINSIGHTS_INSTRUMENTATIONKEY', value: appInsights.properties.InstrumentationKey} {name: 'APPLICATIONINSIGHTS_CONNECTION_STRING', value: appInsights.properties.ConnectionString} {name: 'APPINSIGHTS_PROFILERFEATURE_VERSION', value: '1.0.0'} diff --git a/deployers/bicep/modules/setPermissions.bicep b/deployers/bicep/modules/setPermissions.bicep index 2033982d8..78f6ba5a4 100644 --- a/deployers/bicep/modules/setPermissions.bicep +++ b/deployers/bicep/modules/setPermissions.bicep @@ -6,12 +6,12 @@ param keyVaultName string param cosmosDBName string param acrName string param openAIName string -// param openAIResourceGroupName string +//param openAIResourceGroupName string param docIntelName string -// param storageAccountName string -// param speechServiceName string -// param searchServiceName string -// param contentSafetyName string +param storageAccountName string +param speechServiceName string +param searchServiceName string +param contentSafetyName string resource webApp 'Microsoft.Web/sites@2022-03-01' existing = { name: webAppName @@ -37,21 +37,21 @@ resource docIntelService 'Microsoft.CognitiveServices/accounts@2024-10-01' exist name: docIntelName } -// resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' existing = { -// name: storageAccountName -// } +resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' existing = { + name: storageAccountName +} -// resource speechService 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = if (speechServiceName != '') { -// name: speechServiceName -// } +resource speechService 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = if (speechServiceName != '') { + name: speechServiceName +} -// resource searchService 'Microsoft.Search/searchServices@2025-05-01' existing = { -// name: searchServiceName -// } +resource searchService 'Microsoft.Search/searchServices@2025-05-01' existing = { + name: searchServiceName +} -// resource contentSafety 'Microsoft.CognitiveServices/accounts@2025-06-01' existing = if (contentSafetyName != '') { -// name: contentSafetyName -// } +resource contentSafety 'Microsoft.CognitiveServices/accounts@2025-06-01' existing = if (contentSafetyName != '') { + name: contentSafetyName +} // grant the webApp access to the key vault resource kvSecretsUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { @@ -123,7 +123,7 @@ resource openAIUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = i } // grant the managed identity access to document intelligence as a Cognitive Services User -resource docIntelUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (docIntelName != '' && authenticationType == 'Managed_Identity') { +resource docIntelUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'Managed_Identity') { name: guid(docIntelService.id, webApp.id, 'doc-intel-user') scope: docIntelService properties: { @@ -136,58 +136,58 @@ resource docIntelUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = } } -// // grant the managed identity access to the storage account as a blob data contributor -// resource storageBlobDataContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'Managed_Identity') { -// name: guid(storageAccount.id, webApp.id, 'storage-blob-data-contributor') -// scope: storageAccount -// properties: { -// roleDefinitionId: subscriptionResourceId( -// 'Microsoft.Authorization/roleDefinitions', -// 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' -// ) -// principalId: webApp.identity.principalId -// principalType: 'ServicePrincipal' -// } -// } - -// // grant the managed identity access to speech service as a Cognitive Services User -// resource speechServiceUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (speechServiceName != '' && authenticationType == 'Managed_Identity') { -// name: guid(speechService.id, webApp.id, 'speech-service-user') -// scope: speechService -// properties: { -// roleDefinitionId: subscriptionResourceId( -// 'Microsoft.Authorization/roleDefinitions', -// 'a97b65f3-24c7-4388-baec-2e87135dc908' -// ) -// principalId: webApp.identity.principalId -// principalType: 'ServicePrincipal' -// } -// } - -// // grant the managed identity access to search service as a Search Service Contributor -// resource searchServiceContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'Managed_Identity') { -// name: guid(searchService.id, webApp.id, 'search-service-contributor') -// scope: searchService -// properties: { -// roleDefinitionId: subscriptionResourceId( -// 'Microsoft.Authorization/roleDefinitions', -// '8ebe5a00-799e-43f5-93ac-243d3dce84a7' -// ) -// principalId: webApp.identity.principalId -// principalType: 'ServicePrincipal' -// } -// } - -// // grant the managed identity access to content safety as a Cognitive Services User -// resource contentSafetyUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (contentSafetyName != '' && authenticationType == 'Managed_Identity') { -// name: guid(contentSafety.id, webApp.id, 'content-safety-user') -// scope: contentSafety -// properties: { -// roleDefinitionId: subscriptionResourceId( -// 'Microsoft.Authorization/roleDefinitions', -// 'a97b65f3-24c7-4388-baec-2e87135dc908' -// ) -// principalId: webApp.identity.principalId -// principalType: 'ServicePrincipal' -// } -// } +// grant the managed identity access to the storage account as a blob data contributor +resource storageBlobDataContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'Managed_Identity') { + name: guid(storageAccount.id, webApp.id, 'storage-blob-data-contributor') + scope: storageAccount + properties: { + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' + ) + principalId: webApp.identity.principalId + principalType: 'ServicePrincipal' + } +} + +// grant the managed identity access to speech service as a Cognitive Services User +resource speechServiceUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (speechServiceName != '' && authenticationType == 'Managed_Identity') { + name: guid(speechService.id, webApp.id, 'speech-service-user') + scope: speechService + properties: { + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'a97b65f3-24c7-4388-baec-2e87135dc908' + ) + principalId: webApp.identity.principalId + principalType: 'ServicePrincipal' + } +} + +// grant the managed identity access to search service as a Search Service Contributor +resource searchServiceContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'Managed_Identity') { + name: guid(searchService.id, webApp.id, 'search-service-contributor') + scope: searchService + properties: { + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '8ebe5a00-799e-43f5-93ac-243d3dce84a7' + ) + principalId: webApp.identity.principalId + principalType: 'ServicePrincipal' + } +} + +// grant the managed identity access to content safety as a Cognitive Services User +resource contentSafetyUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (contentSafetyName != '' && authenticationType == 'Managed_Identity') { + name: guid(contentSafety.id, webApp.id, 'content-safety-user') + scope: contentSafety + properties: { + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'a97b65f3-24c7-4388-baec-2e87135dc908' + ) + principalId: webApp.identity.principalId + principalType: 'ServicePrincipal' + } +} From cd969db4c6e2baa078fdbe74275b4fbc3e05fca9 Mon Sep 17 00:00:00 2001 From: SteveCInVA Date: Mon, 1 Dec 2025 14:15:52 +0000 Subject: [PATCH 07/22] initial support for VideoIndexer service --- deployers/bicep/main.bicep | 49 +++++++++++---- deployers/bicep/modules/videoIndexer.bicep | 69 ++++++++++++++++++++++ 2 files changed, 107 insertions(+), 11 deletions(-) create mode 100644 deployers/bicep/modules/videoIndexer.bicep diff --git a/deployers/bicep/main.bicep b/deployers/bicep/main.bicep index 232da6182..8c2eb4381 100644 --- a/deployers/bicep/main.bicep +++ b/deployers/bicep/main.bicep @@ -111,13 +111,17 @@ param embeddingModels array = [ - Default is false''') param deployContentSafety bool +@description('''Enable deployment of Azure Cache for Redis and related resources. +- Default is false''') +param deployRedisCache bool + @description('''Enable deployment of Azure Speech service and related resources. - Default is false''') param deploySpeechService bool -@description('''Enable deployment of Azure Cache for Redis and related resources. +@description('''Enable deployment of Azure Video Indexer service and related resources. - Default is false''') -param deployRedisCache bool +param deployVideoIndexerService bool //========================================================= // variable declarations for the main deployment @@ -409,6 +413,27 @@ module contentSafety 'modules/contentSafety.bicep' = if (deployContentSafety) { } } +//========================================================= +// Create Optional Resource - Redis Cache +//========================================================= +module redisCache 'modules/redisCache.bicep' = if (deployRedisCache) { + name: 'redisCache' + scope: rg + params: { + location: location + appName: appName + environment: environment + tags: tags + enableDiagLogging: enableDiagLogging + logAnalyticsId: logAnalytics.outputs.logAnalyticsId + + keyVault: keyVault.outputs.keyVaultName + authenticationType: authenticationType + configureApplicationPermissions: configureApplicationPermissions + } +} + + //========================================================= // Create Optional Resource - Speech Service //========================================================= @@ -429,11 +454,12 @@ module speechService 'modules/speechService.bicep' = if (deploySpeechService) { } } + //========================================================= -// Create Optional Resource - Redis Cache +// Create Optional Resource - Video Indexer Service //========================================================= -module redisCache 'modules/redisCache.bicep' = if (deployRedisCache) { - name: 'redisCache' +module videoIndexerService 'modules/videoIndexer.bicep' = if (deployVideoIndexerService) { + name: 'videoIndexerService' scope: rg params: { location: location @@ -443,6 +469,7 @@ module redisCache 'modules/redisCache.bicep' = if (deployRedisCache) { enableDiagLogging: enableDiagLogging logAnalyticsId: logAnalytics.outputs.logAnalyticsId + storageAccount: storageAccount.outputs.name keyVault: keyVault.outputs.keyVaultName authenticationType: authenticationType configureApplicationPermissions: configureApplicationPermissions @@ -464,12 +491,12 @@ module setPermissions 'modules/setPermissions.bicep' = if (configureApplicationP openAIName: openAI.outputs.openAIName // openAIResourceGroupName: useExistingOpenAISvc ? existingOpenAIResourceGroupName : openAI_create.outputs.openAIResourceGroup docIntelName: docIntel.outputs.documentIntelligenceServiceName - // storageAccountName: storageAccount.outputs.name - // #disable-next-line BCP318 // expect one value to be null - // speechServiceName: deploySpeechService ? speechService.outputs.speechServiceName : '' - // searchServiceName: searchService.outputs.searchServiceName - // #disable-next-line BCP318 // expect one value to be null - // contentSafetyName: deployContentSafety ? contentSafety.outputs.contentSafetyName : '' + storageAccountName: storageAccount.outputs.name + #disable-next-line BCP318 // expect one value to be null + speechServiceName: deploySpeechService ? speechService.outputs.speechServiceName : '' + searchServiceName: searchService.outputs.searchServiceName + #disable-next-line BCP318 // expect one value to be null + contentSafetyName: deployContentSafety ? contentSafety.outputs.contentSafetyName : '' } } diff --git a/deployers/bicep/modules/videoIndexer.bicep b/deployers/bicep/modules/videoIndexer.bicep new file mode 100644 index 000000000..ed0a4a3cb --- /dev/null +++ b/deployers/bicep/modules/videoIndexer.bicep @@ -0,0 +1,69 @@ +targetScope = 'resourceGroup' + +param location string +param appName string +param environment string +param tags object + +param enableDiagLogging bool +param logAnalyticsId string + +param storageAccount string + +param keyVault string +param authenticationType string +param configureApplicationPermissions bool + +// Import diagnostic settings configurations +module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging){ + name: 'diagnosticConfigs' +} + + + +// deploy speech service if required +resource speechService 'Microsoft.CognitiveServices/accounts@2024-10-01' = { + name: toLower('${appName}-${environment}-video') + location: location + kind: 'VideoIndexer' + sku: { + name: 'S0' + } + identity: { + type: 'SystemAssigned' + } + properties: { + publicNetworkAccess: 'Enabled' + disableLocalAuth: false + customSubDomainName: toLower('${appName}-${environment}-speech') + } + tags: tags +} + +// configure diagnostic settings for speech service +resource speechServiceDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (enableDiagLogging) { + name: toLower('${speechService.name}-diagnostics') + scope: speechService + properties: { + workspaceId: logAnalyticsId + #disable-next-line BCP318 // expect one value to be null + logs: diagnosticConfigs.outputs.standardLogCategories + #disable-next-line BCP318 // expect one value to be null + metrics: diagnosticConfigs.outputs.standardMetricsCategories + } +} + +//========================================================= +// store speech Service keys in key vault if using key authentication and configure app permissions = true +//========================================================= +module speechServiceSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'Key' && configureApplicationPermissions) { + name: 'storeSpeechServiceSecret' + params: { + keyVaultName: keyVault + secretName: 'speech-service-key' + secretValue: speechService.listKeys().key1 + } +} + +output speechServiceName string = speechService.name +output speechServiceEndpoint string = speechService.properties.endpoint From 6fc2a917e10332645ce8add976dde8531a20461e Mon Sep 17 00:00:00 2001 From: SteveCInVA Date: Mon, 1 Dec 2025 19:59:29 +0000 Subject: [PATCH 08/22] Refactor Bicep modules to enhance VideoIndexer service integration and update diagnostic settings configurations --- deployers/azure.yaml | 2 +- deployers/bicep/main.bicep | 28 ++++++--- .../bicep/modules/diagnosticSettings.bicep | 12 +++- deployers/bicep/modules/setPermissions.bicep | 50 ++++++++++++++++ deployers/bicep/modules/videoIndexer.bicep | 59 ++++++++----------- 5 files changed, 107 insertions(+), 44 deletions(-) diff --git a/deployers/azure.yaml b/deployers/azure.yaml index 6085ee91c..262dcb294 100644 --- a/deployers/azure.yaml +++ b/deployers/azure.yaml @@ -15,7 +15,7 @@ hooks: export var_cosmosDb_uri=${var_cosmosDb_uri} export var_subscriptionId=${AZURE_SUBSCRIPTION_ID} export var_rgName=${var_rgName} - export var_enableEnterpriseApp=${var_enableEnterpriseApp} + #export var_enableEnterpriseApp=${var_enableEnterpriseApp} export var_openAIEndpoint=${var_openAIEndpoint} export var_openAIResourceGroup=${var_openAIResourceGroup} diff --git a/deployers/bicep/main.bicep b/deployers/bicep/main.bicep index 8c2eb4381..971416cff 100644 --- a/deployers/bicep/main.bicep +++ b/deployers/bicep/main.bicep @@ -470,9 +470,8 @@ module videoIndexerService 'modules/videoIndexer.bicep' = if (deployVideoIndexer logAnalyticsId: logAnalytics.outputs.logAnalyticsId storageAccount: storageAccount.outputs.name - keyVault: keyVault.outputs.keyVaultName - authenticationType: authenticationType - configureApplicationPermissions: configureApplicationPermissions + openAiServiceName: openAI.outputs.openAIName + } } @@ -497,22 +496,33 @@ module setPermissions 'modules/setPermissions.bicep' = if (configureApplicationP searchServiceName: searchService.outputs.searchServiceName #disable-next-line BCP318 // expect one value to be null contentSafetyName: deployContentSafety ? contentSafety.outputs.contentSafetyName : '' + #disable-next-line BCP318 // expect one value to be null + videoIndexerName: deployVideoIndexerService ? videoIndexerService.outputs.videoIndexerServiceName : '' } } //========================================================= // output values //========================================================= +// output required for both predeploy and postprovision scripts in azure.yaml output var_rgName string = rgName + +// output values required for predeploy script in azure.yaml output var_webService string = appService.outputs.name output var_imageName string = contains(imageName, ':') ? split(imageName, ':')[0] : imageName output var_imageTag string = split(imageName, ':')[1] output var_containerRegistry string = containerRegistry output var_acrName string = toLower('${appName}${environment}acr') -output gptModel string = gptModels[0].modelName -output embeddingModel string = embeddingModels[0].modelName - - - - +// output values required for postprovision script in azure.yaml +//output var_configureApplication bool = configureApplicationPermissions +output var_cosmosDb_uri string = cosmosDB.outputs.cosmosDbUri +output var_subscriptionId string = subscription().subscriptionId +output var_openAIEndpoint string = openAI.outputs.openAIEndpoint +output var_openAIResourceGroup string = openAI.outputs.openAIResourceGroup //may be able to remove +output var_openAIGPTModel string = gptModels[0].modelName +output var_openAITextEmbeddingModel string = embeddingModels[0].modelName +output var_blobStorageEndpoint string = storageAccount.outputs.endpoint +#disable-next-line BCP318 // expect one value to be null +output var_contentSafetyName string = deployContentSafety ? contentSafety.outputs.contentSafetyName : '' +output var_documentIntelligenceServiceEndpoint string = docIntel.outputs.documentIntelligenceServiceEndpoint diff --git a/deployers/bicep/modules/diagnosticSettings.bicep b/deployers/bicep/modules/diagnosticSettings.bicep index 08003eac0..668f42ffd 100644 --- a/deployers/bicep/modules/diagnosticSettings.bicep +++ b/deployers/bicep/modules/diagnosticSettings.bicep @@ -23,6 +23,15 @@ var standardLogCategories = [ } ] +// Standard log categories using category groups (recommended for most resources) +var limitedLogCategories = [ + { + categoryGroup: 'allLogs' + enabled: true + retentionPolicy: standardRetentionPolicy + } +] + // Standard metrics configuration var standardMetricsCategories = [ { @@ -91,8 +100,9 @@ var webAppLogCategories = [ ] // Export configurations as outputs so they can be used by other templates +output limitedLogCategories array = limitedLogCategories output standardRetentionPolicy object = standardRetentionPolicy output standardLogCategories array = standardLogCategories output standardMetricsCategories array = standardMetricsCategories output transactionMetricsCategories array = transactionMetricsCategories -output webAppLogCategories array = webAppLogCategories \ No newline at end of file +output webAppLogCategories array = webAppLogCategories diff --git a/deployers/bicep/modules/setPermissions.bicep b/deployers/bicep/modules/setPermissions.bicep index 78f6ba5a4..c20279d20 100644 --- a/deployers/bicep/modules/setPermissions.bicep +++ b/deployers/bicep/modules/setPermissions.bicep @@ -12,6 +12,7 @@ param storageAccountName string param speechServiceName string param searchServiceName string param contentSafetyName string +param videoIndexerName string resource webApp 'Microsoft.Web/sites@2022-03-01' existing = { name: webAppName @@ -53,6 +54,10 @@ resource contentSafety 'Microsoft.CognitiveServices/accounts@2025-06-01' existin name: contentSafetyName } +resource videoIndexerService 'Microsoft.VideoIndexer/accounts@2025-04-01' existing = if (videoIndexerName != '') { + name: videoIndexerName +} + // grant the webApp access to the key vault resource kvSecretsUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(kv.id, webApp.id, 'kv-secrets-user') @@ -191,3 +196,48 @@ resource contentSafetyUserRole 'Microsoft.Authorization/roleAssignments@2022-04- principalType: 'ServicePrincipal' } } + +// grant the video indexer service access to storage account as a Storage Blob Data Contributor +resource videoIndexerStorageBlobDataContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (videoIndexerName != '' ) { + name: guid(storageAccount.id, videoIndexerService.id, 'video-indexer-storage-blob-data-contributor') + scope: storageAccount + properties: { + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' + ) + #disable-next-line BCP318 // may be null if video indexer not deployed + principalId: videoIndexerService.identity.principalId + principalType: 'ServicePrincipal' + } +} + +// grant the video indexer service access to OpenAI service as cognitive services Contributor +resource videoIndexerStorageCogServicesContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (videoIndexerName != '' ) { + name: guid(openAiService.id, videoIndexerService.id, 'video-indexer-cog-services-contributor') + scope: openAiService + properties: { + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68' + ) + #disable-next-line BCP318 // may be null if video indexer not deployed + principalId: videoIndexerService.identity.principalId + principalType: 'ServicePrincipal' + } +} + +// grant the video indexer service access to OpenAI service as cognitive services user +resource videoIndexerStorageCogServicesUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (videoIndexerName != '' ) { + name: guid(openAiService.id, videoIndexerService.id, 'video-indexer-cog-services-user') + scope: openAiService + properties: { + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'a97b65f3-24c7-4388-baec-2e87135dc908' + ) + #disable-next-line BCP318 // may be null if video indexer not deployed + principalId: videoIndexerService.identity.principalId + principalType: 'ServicePrincipal' + } +} diff --git a/deployers/bicep/modules/videoIndexer.bicep b/deployers/bicep/modules/videoIndexer.bicep index ed0a4a3cb..1fe617b04 100644 --- a/deployers/bicep/modules/videoIndexer.bicep +++ b/deployers/bicep/modules/videoIndexer.bicep @@ -9,61 +9,54 @@ param enableDiagLogging bool param logAnalyticsId string param storageAccount string - -param keyVault string -param authenticationType string -param configureApplicationPermissions bool +param openAiServiceName string // Import diagnostic settings configurations module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging){ name: 'diagnosticConfigs' } +resource storage 'Microsoft.Storage/storageAccounts@2021-09-01' existing = { + name: storageAccount +} +resource openAiService 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = { + name: openAiServiceName +} -// deploy speech service if required -resource speechService 'Microsoft.CognitiveServices/accounts@2024-10-01' = { +// deploy video indexer service if required +resource videoIndexerService 'Microsoft.VideoIndexer/accounts@2025-04-01' = { name: toLower('${appName}-${environment}-video') location: location - kind: 'VideoIndexer' - sku: { - name: 'S0' - } + identity: { type: 'SystemAssigned' } properties: { publicNetworkAccess: 'Enabled' - disableLocalAuth: false - customSubDomainName: toLower('${appName}-${environment}-speech') + storageServices: { + resourceId: storage.id + } + openAiServices: { + resourceId: openAiService.id + } } tags: tags + dependsOn: [ + storage + openAiService + ] } -// configure diagnostic settings for speech service -resource speechServiceDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (enableDiagLogging) { - name: toLower('${speechService.name}-diagnostics') - scope: speechService +// configure diagnostic settings for video indexer service +resource videoIndexerServiceDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (enableDiagLogging) { + name: toLower('${videoIndexerService.name}-diagnostics') + scope: videoIndexerService properties: { workspaceId: logAnalyticsId #disable-next-line BCP318 // expect one value to be null - logs: diagnosticConfigs.outputs.standardLogCategories - #disable-next-line BCP318 // expect one value to be null - metrics: diagnosticConfigs.outputs.standardMetricsCategories - } -} - -//========================================================= -// store speech Service keys in key vault if using key authentication and configure app permissions = true -//========================================================= -module speechServiceSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'Key' && configureApplicationPermissions) { - name: 'storeSpeechServiceSecret' - params: { - keyVaultName: keyVault - secretName: 'speech-service-key' - secretValue: speechService.listKeys().key1 + logs: diagnosticConfigs.outputs.limitedLogCategories } } -output speechServiceName string = speechService.name -output speechServiceEndpoint string = speechService.properties.endpoint +output videoIndexerServiceName string = videoIndexerService.name From 7dd32ae2474d9a4a24f4d40681b8f3da67ccc7f4 Mon Sep 17 00:00:00 2001 From: SteveCInVA Date: Mon, 1 Dec 2025 22:43:59 +0000 Subject: [PATCH 09/22] move from using chainguard-dev builder image to python slim image. --- application/single_app/Dockerfile | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/application/single_app/Dockerfile b/application/single_app/Dockerfile index 8178f898d..308e157b4 100644 --- a/application/single_app/Dockerfile +++ b/application/single_app/Dockerfile @@ -1,8 +1,23 @@ -# Builder stage: install dependencies in a virtualenv -FROM cgr.dev/chainguard/python:latest-dev AS builder +# Builder stage: Use standard Python image with all build tools +FROM python:3.12-slim AS builder USER root +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + g++ \ + make \ + libpq-dev \ + libmariadb-dev \ + unixodbc-dev \ + libffi-dev \ + libssl-dev \ + libfreetype6-dev \ + libjpeg-dev \ + libpng-dev \ + zlib1g-dev \ + && rm -rf /var/lib/apt/lists/* + # Ensure /app directory exists and has proper permissions RUN mkdir -p /app && chown root:root /app @@ -11,14 +26,20 @@ WORKDIR /app # Create a Python virtual environment RUN python -m venv /app/venv -# Create and permission the flask_session directory -RUN mkdir -p /app/flask_session && chown -R nonroot:nonroot /app/flask_session +# Create and permission the flask_session directory in builder stage +RUN mkdir -p /app/flask_session && chmod 777 /app/flask_session # Copy requirements and install them into the virtualenv COPY application/single_app/requirements.txt . ENV PATH="/app/venv/bin:$PATH" + +# Upgrade pip and install wheel +RUN pip install --no-cache-dir --upgrade pip setuptools wheel + +# Install requirements RUN pip install --no-cache-dir -r requirements.txt +# Runtime stage: Use secure Chainguard image FROM cgr.dev/chainguard/python:latest WORKDIR /app From 383138b049a7027b38959e9706a40dd7102e05bb Mon Sep 17 00:00:00 2001 From: SteveCInVA Date: Tue, 2 Dec 2025 17:00:52 +0000 Subject: [PATCH 10/22] Updates to support post deployment app config --- deployers/azure.yaml | 10 +- deployers/bicep/main.bicep | 21 +++- deployers/bicep/modules/appService.bicep | 2 +- deployers/bicep/modules/search.bicep | 3 +- deployers/bicep/modules/speechService.bicep | 3 +- deployers/bicep/modules/videoIndexer.bicep | 1 + deployers/bicep/postconfig.py | 127 +++++++++++++++----- deployers/bicep/requirements.txt | 3 +- 8 files changed, 133 insertions(+), 37 deletions(-) diff --git a/deployers/azure.yaml b/deployers/azure.yaml index 262dcb294..bc4a810bf 100644 --- a/deployers/azure.yaml +++ b/deployers/azure.yaml @@ -15,16 +15,22 @@ hooks: export var_cosmosDb_uri=${var_cosmosDb_uri} export var_subscriptionId=${AZURE_SUBSCRIPTION_ID} export var_rgName=${var_rgName} - #export var_enableEnterpriseApp=${var_enableEnterpriseApp} + export var_keyVaultUri=${var_keyVaultUri} + + export var_authenticationType=${var_authenticationType} export var_openAIEndpoint=${var_openAIEndpoint} export var_openAIResourceGroup=${var_openAIResourceGroup} export var_openAIGPTModel=${var_openAIGPTModel} export var_openAITextEmbeddingModel=${var_openAITextEmbeddingModel} export var_blobStorageEndpoint=${var_blobStorageEndpoint} - export var_contentSafetyName=${var_contentSafetyName} + export var_contentSafetyEndpoint=${var_contentSafetyEndpoint} export var_searchServiceEndpoint=${var_searchServiceEndpoint} export var_documentIntelligenceServiceEndpoint=${var_documentIntelligenceServiceEndpoint} + export var_videoIndexerName=${var_videoIndexerName} + export var_deploymentLocation=${var_deploymentLocation} + export var_videoIndexerAccountId=${var_videoIndexerAccountId} + export var_speechServiceEndpoint=${var_speechServiceEndpoint} # Execute post-configuration script if enabled if [ "${var_configureApplication}" = "true" ]; then diff --git a/deployers/bicep/main.bicep b/deployers/bicep/main.bicep index 971416cff..5c4b9cd19 100644 --- a/deployers/bicep/main.bicep +++ b/deployers/bicep/main.bicep @@ -515,14 +515,27 @@ output var_containerRegistry string = containerRegistry output var_acrName string = toLower('${appName}${environment}acr') // output values required for postprovision script in azure.yaml -//output var_configureApplication bool = configureApplicationPermissions +output var_configureApplication bool = configureApplicationPermissions +output var_keyVaultUri string = keyVault.outputs.keyVaultUri output var_cosmosDb_uri string = cosmosDB.outputs.cosmosDbUri output var_subscriptionId string = subscription().subscriptionId + +output var_authenticationType string = toLower(authenticationType) + output var_openAIEndpoint string = openAI.outputs.openAIEndpoint output var_openAIResourceGroup string = openAI.outputs.openAIResourceGroup //may be able to remove -output var_openAIGPTModel string = gptModels[0].modelName -output var_openAITextEmbeddingModel string = embeddingModels[0].modelName +output var_openAIGPTModels array = gptModels +output var_openAIEmbeddingModels array = embeddingModels output var_blobStorageEndpoint string = storageAccount.outputs.endpoint #disable-next-line BCP318 // expect one value to be null -output var_contentSafetyName string = deployContentSafety ? contentSafety.outputs.contentSafetyName : '' +output var_contentSafetyEndpoint string = deployContentSafety ? contentSafety.outputs.contentSafetyEndpoint : '' + +output var_deploymentLocation string = rg.location +output var_searchServiceEndpoint string = searchService.outputs.searchServiceEndpoint output var_documentIntelligenceServiceEndpoint string = docIntel.outputs.documentIntelligenceServiceEndpoint +#disable-next-line BCP318 // expect one value to be null +output var_videoIndexerName string = deployVideoIndexerService ? videoIndexerService.outputs.videoIndexerServiceName : '' +#disable-next-line BCP318 // expect one value to be null +output var_videoIndexerAccountId string = deployVideoIndexerService ? videoIndexerService.outputs.videoIndexerAccountId : '' +#disable-next-line BCP318 // expect one value to be null +output var_speechServiceEndpoint string = deploySpeechService ? speechService.outputs.speechServiceEndpoint : '' diff --git a/deployers/bicep/modules/appService.bicep b/deployers/bicep/modules/appService.bicep index 27f835e19..2242f8fbd 100644 --- a/deployers/bicep/modules/appService.bicep +++ b/deployers/bicep/modules/appService.bicep @@ -74,7 +74,7 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' = { {name: 'AZURE_ENDPOINT', value: azurePlatform == 'AzureUSGovernment' ? 'usgovernment' : 'public'} {name: 'SCM_DO_BUILD_DURING_DEPLOYMENT', value: 'false'} {name: 'AZURE_COSMOS_ENDPOINT', value: cosmosDb.properties.documentEndpoint} - {name: 'AZURE_COSMOS_AUTHENTICATION_TYPE', value: authenticationType} + {name: 'AZURE_COSMOS_AUTHENTICATION_TYPE', value: toLower(authenticationType)} // Only add this setting if authenticationType is 'Key' ...(authenticationType == 'Key' ? [{name: 'AZURE_COSMOS_KEY', value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/cosmos-db-key)'}] : []) diff --git a/deployers/bicep/modules/search.bicep b/deployers/bicep/modules/search.bicep index 6fb2791a0..586debb27 100644 --- a/deployers/bicep/modules/search.bicep +++ b/deployers/bicep/modules/search.bicep @@ -50,7 +50,7 @@ resource searchDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-pre //========================================================= // store search Service keys in key vault if using key authentication and configure app permissions = true //========================================================= -module searchServiceSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'Key' && configureApplicationPermissions) { +module searchServiceSecret 'keyVault-Secrets.bicep' = if (configureApplicationPermissions) { name: 'storeSearchServiceSecret' params: { keyVaultName: keyVault @@ -61,3 +61,4 @@ module searchServiceSecret 'keyVault-Secrets.bicep' = if (authenticationType == output searchServiceName string = searchService.name output searchServiceEndpoint string = searchService.properties.endpoint +output searchServiceAuthencationType string = authenticationType diff --git a/deployers/bicep/modules/speechService.bicep b/deployers/bicep/modules/speechService.bicep index 26f3cc4d7..d1af6e0d9 100644 --- a/deployers/bicep/modules/speechService.bicep +++ b/deployers/bicep/modules/speechService.bicep @@ -52,7 +52,7 @@ resource speechServiceDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05 //========================================================= // store speech Service keys in key vault if using key authentication and configure app permissions = true //========================================================= -module speechServiceSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'Key' && configureApplicationPermissions) { +module speechServiceSecret 'keyVault-Secrets.bicep' = if (configureApplicationPermissions) { name: 'storeSpeechServiceSecret' params: { keyVaultName: keyVault @@ -63,3 +63,4 @@ module speechServiceSecret 'keyVault-Secrets.bicep' = if (authenticationType == output speechServiceName string = speechService.name output speechServiceEndpoint string = speechService.properties.endpoint +output speechServiceAuthenticationType string = authenticationType diff --git a/deployers/bicep/modules/videoIndexer.bicep b/deployers/bicep/modules/videoIndexer.bicep index 1fe617b04..ecdeac628 100644 --- a/deployers/bicep/modules/videoIndexer.bicep +++ b/deployers/bicep/modules/videoIndexer.bicep @@ -60,3 +60,4 @@ resource videoIndexerServiceDiagnostics 'Microsoft.Insights/diagnosticSettings@2 } output videoIndexerServiceName string = videoIndexerService.name +output videoIndexerAccountId string = videoIndexerService.properties.accountId diff --git a/deployers/bicep/postconfig.py b/deployers/bicep/postconfig.py index 516fa2f1f..d88574185 100644 --- a/deployers/bicep/postconfig.py +++ b/deployers/bicep/postconfig.py @@ -1,7 +1,9 @@ from azure.cosmos import CosmosClient from azure.cosmos.exceptions import CosmosResourceNotFoundError from azure.identity import DefaultAzureCredential +from azure.keyvault.secrets import SecretClient import os +import json credential = DefaultAzureCredential() token = credential.get_token("https://cosmos.azure.com/.default") @@ -29,90 +31,161 @@ } # Get values from environment variables -var_authenticationType = os.getenv("var_authenticationType") # expected to be true or false... will determine if key based or managed identity is used +var_authenticationType = os.getenv("var_authenticationType") +var_keyVaultUri = os.getenv("var_keyVaultUri") var_openAIEndpoint=os.getenv("var_openAIEndpoint") var_openAIResourceGroup=os.getenv("var_openAIResourceGroup") var_subscriptionId = os.getenv("var_subscriptionId") var_rgName = os.getenv("var_rgName") -var_openAIGPTModel = os.getenv("var_openAIGPTModel") -var_openAITextEmbeddingModel = os.getenv("var_openAITextEmbeddingModel") +var_openAIGPTModels = os.getenv("var_openAIGPTModels") +gpt_models_list = json.loads(var_openAIGPTModels) +var_openAIEmbeddingModels = os.getenv("var_openAIEmbeddingModels") +embedding_models_list = json.loads(var_openAIEmbeddingModels) var_blobStorageEndpoint = os.getenv("var_blobStorageEndpoint") var_contentSafetyEndpoint = os.getenv("var_contentSafetyEndpoint") var_searchServiceEndpoint = os.getenv("var_searchServiceEndpoint") var_documentIntelligenceServiceEndpoint = os.getenv("var_documentIntelligenceServiceEndpoint") +var_videoIndexerName = os.getenv("var_videoIndexerName") +var_videoIndexerLocation = os.getenv("var_deploymentLocation") +var_videoIndexerAccountId = os.getenv("var_videoIndexerAccountId") +var_speechServiceEndpoint = os.getenv("var_speechServiceEndpoint") +var_speechServiceLocation = os.getenv("var_deploymentLocation") + +# Initialize Key Vault client if Key Vault URI is provided +if var_keyVaultUri: + keyvault_client = SecretClient(vault_url=var_keyVaultUri, credential=credential) +else: + keyvault_client = None + +# 4. Update the Configurations -# 4. Update the property +# General > Health Check item["enable_external_healthcheck"] = True +# AI Models item["azure_openai_gpt_endpoint"] = var_openAIEndpoint - -if var_contentSafetyEndpoint and var_contentSafetyEndpoint.strip(): - item["azure_openai_gpt_authentication_type"] = "managed_identity" -else: - item["azure_openai_gpt_authentication_type"] = "key" +item["azure_openai_gpt_authentication_type"] = var_authenticationType item["azure_openai_gpt_subscription_id"] = var_subscriptionId item["azure_openai_gpt_resource_group"] = var_openAIResourceGroup item["gpt_model"] = { "selected": [ { - "deploymentName": var_openAIGPTModel, - "modelName": var_openAIGPTModel + "deploymentName": gpt_models_list[0]["modelName"], + "modelName": gpt_models_list[0]["modelName"] } ], "all": [ { - "deploymentName": var_openAIGPTModel, - "modelName": var_openAIGPTModel + "deploymentName": model["modelName"], + "modelName": model["modelName"] } + for model in gpt_models_list ] } item["azure_openai_embedding_endpoint"] = var_openAIEndpoint -item["azure_openai_embedding_authentication_type"] = "managed_identity" +item["azure_openai_embedding_authentication_type"] = var_authenticationType item["azure_openai_embedding_subscription_id"] = var_subscriptionId item["azure_openai_embedding_resource_group"] = var_openAIResourceGroup item["embedding_model"] = { "selected": [ { - "deploymentName": var_openAITextEmbeddingModel, - "modelName": var_openAITextEmbeddingModel + "deploymentName": embedding_models_list[0]["modelName"], + "modelName": embedding_models_list[0]["modelName"] } ], "all": [ { - "deploymentName": var_openAITextEmbeddingModel, - "modelName": var_openAITextEmbeddingModel + "deploymentName": model["modelName"], + "modelName": model["modelName"] } + for model in embedding_models_list ] } +# Agents and Actions > Agents Configuration item["enable_semantic_kernel"] = True +# Logging > Application Insights Logging item["enable_appinsights_global_logging"] = True +# Scale > Redis Cache +# todo support redis cache configuration + +# Workspaces > Metadata Extraction item["enable_extract_meta_data"] = True -item["metadata_extraction_model"] = var_openAIGPTModel +item["metadata_extraction_model"] = gpt_models_list[0]["modelName"] +# Workspaces > Multimodal Vision Analysis item["enable_multimodal_vision"] = True -item["multimodal_vision_model"] = var_openAIGPTModel +item["multimodal_vision_model"] = gpt_models_list[0]["modelName"] +# Citations > Enhanced Citations item["enable_enhanced_citations"] = True +item["aoffice_docs_authentication_type"] = var_authenticationType item["office_docs_storage_account_blob_endpoint"] = var_blobStorageEndpoint -# if contentSafetyEndpoint is not blank then set enable_content_safety to true +# Safety > Content Safety if var_contentSafetyEndpoint and var_contentSafetyEndpoint.strip(): item["enable_content_safety"] = True - item["content_safety_endpoint"] = var_contentSafetyEndpoint - item["content_safety_authentication_type"] = "managed_identity" - +item["content_safety_endpoint"] = var_contentSafetyEndpoint +item["content_safety_authentication_type"] = var_authenticationType +if keyvault_client: + try: + contentSafety_key_secret = keyvault_client.get_secret("content-safety-key") + item["content_safety_key"] = contentSafety_key_secret.value + print("Retrieved contentSafety service key from Key Vault") + except Exception as e: + print(f"Warning: Could not retrieve content-safety-key from Key Vault: {e}") + +# Safety > Conversation Archiving item["enable_conversation_archiving"] = True +# Search and Extract > Azure AI Search item["azure_ai_search_endpoint"] = var_searchServiceEndpoint -item["azure_ai_search_authentication_type"] = "managed_identity" - +item["azure_ai_search_authentication_type"] = var_authenticationType +if keyvault_client: + try: + search_key_secret = keyvault_client.get_secret("search-service-key") + item["azure_ai_search_key"] = search_key_secret.value + print("Retrieved search service key from Key Vault") + except Exception as e: + print(f"Warning: Could not retrieve search-service-key from Key Vault: {e}") + +# Search and Extract > Azure Document Intelligence item["azure_document_intelligence_endpoint"] = var_documentIntelligenceServiceEndpoint -item["azure_document_intelligence_authentication_type"] = "managed_identity" +item["azure_document_intelligence_authentication_type"] = var_authenticationType +if keyvault_client: + try: + documentIntelligence_key_secret = keyvault_client.get_secret("document-intelligence-key") + item["azure_document_intelligence_key"] = documentIntelligence_key_secret.value + print("Retrieved document intelligence service key from Key Vault") + except Exception as e: + print(f"Warning: Could not retrieve document-intelligence-key from Key Vault: {e}") + +# Search and Extract > Multimedia Support +# Video Indexer Configuration +if var_videoIndexerName and var_videoIndexerName.strip(): + item["enable_video_file_support"] = True +item["video_indexer_resource_group"] = var_rgName +item["video_indexer_subscription_id"] = var_subscriptionId +item["video_indexer_account_name"] = var_videoIndexerName +item["video_indexer_location"] = var_videoIndexerLocation +item["video_indexer_account_id"] = var_videoIndexerAccountId + +# Speech Service Configuration +if var_speechServiceEndpoint and var_speechServiceEndpoint.strip(): + item["enable_audio_file_support"] = True +item["speech_service_endpoint"] = var_speechServiceEndpoint +item["speech_service_location"] = var_speechServiceLocation +if keyvault_client: + try: + speech_key_secret = keyvault_client.get_secret("speech-service-key") + item["speech_service_key"] = speech_key_secret.value + print("Retrieved speech service key from Key Vault") + except Exception as e: + print(f"Warning: Could not retrieve speech-service-key from Key Vault: {e}") # 5. Upsert the updated items back into Cosmos DB response = container.upsert_item(item) diff --git a/deployers/bicep/requirements.txt b/deployers/bicep/requirements.txt index 2e8c64249..e6e1fd602 100644 --- a/deployers/bicep/requirements.txt +++ b/deployers/bicep/requirements.txt @@ -1,3 +1,4 @@ # requirements.txt azure-identity>=1.15.0 -azure-cosmos>=4.5.0 \ No newline at end of file +azure-cosmos>=4.5.0 +azure-keyvault-secrets>=4.4.0 \ No newline at end of file From d1b1ea15ebc6edf6cddbfc5e07d4375f93e73515 Mon Sep 17 00:00:00 2001 From: SteveCInVA Date: Tue, 2 Dec 2025 20:47:51 +0000 Subject: [PATCH 11/22] Add post-deployment permissions script for CosmosDB and update authentication type handling --- deployers/azure.yaml | 2 + deployers/bicep/cosmosDb-postDeployPerms.sh | 48 +++++++++++++++++++ deployers/bicep/main.bicep | 6 +-- deployers/bicep/modules/appService.bicep | 20 ++++---- .../modules/azureContainerRegistry.bicep | 2 +- deployers/bicep/modules/contentSafety.bicep | 2 +- deployers/bicep/modules/cosmosDb.bicep | 2 +- .../bicep/modules/documentIntelligence.bicep | 2 +- deployers/bicep/modules/openAI.bicep | 2 +- deployers/bicep/modules/redisCache.bicep | 2 +- deployers/bicep/modules/setPermissions.bicep | 16 +++---- deployers/bicep/modules/storageAccount.bicep | 2 +- 12 files changed, 78 insertions(+), 28 deletions(-) create mode 100644 deployers/bicep/cosmosDb-postDeployPerms.sh diff --git a/deployers/azure.yaml b/deployers/azure.yaml index bc4a810bf..f4d32871f 100644 --- a/deployers/azure.yaml +++ b/deployers/azure.yaml @@ -34,6 +34,8 @@ hooks: # Execute post-configuration script if enabled if [ "${var_configureApplication}" = "true" ]; then + echo "Grant permissions to CosmosDB for post deployment steps..." + bash ./bicep/cosmosDb-postDeployPerms.sh echo "Running post-deployment configuration..." python3 -m pip install --user -r ./bicep/requirements.txt python3 ./bicep/postconfig.py diff --git a/deployers/bicep/cosmosDb-postDeployPerms.sh b/deployers/bicep/cosmosDb-postDeployPerms.sh new file mode 100644 index 000000000..c4d3c311f --- /dev/null +++ b/deployers/bicep/cosmosDb-postDeployPerms.sh @@ -0,0 +1,48 @@ + +#!/usr/bin/env bash +set -euo pipefail + +RG_NAME="${var_rgName}" + +COSMOS_URI="${var_cosmosDb_uri}" +ACCOUNT_NAME=$(echo "$COSMOS_URI" | sed -E 's#https://([^.]*)\.documents\.azure\.com.*#\1#') + +echo "===============================" +echo "Cosmos DB Account Name: $ACCOUNT_NAME" + +UPN=$(az account show --query user.name -o tsv) +OBJECT_ID=$(az ad signed-in-user show --query id -o tsv) +SUBSCRIPTION_ID=$(az account show --query id -o tsv) + +# Control-plane assignment +SCOPE="/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RG_NAME/providers/Microsoft.DocumentDB/databaseAccounts/$ACCOUNT_NAME" + +ROLE_NAME="Contributor" +ROLE_ID=$(az role definition list --name "$ROLE_NAME" --query "[0].id" -o tsv) + +echo "Assigning role '$ROLE_NAME' to user '$UPN' on scope '$SCOPE'..." +az role assignment create \ + --assignee-object-id "$OBJECT_ID" \ + --assignee-principal-type "User" \ + --role "$ROLE_ID" \ + --scope "$SCOPE" || echo "Control-plane role may already exist." + + +# Data-plane assignment +DP_ROLE_NAME="Cosmos DB Built-in Data Contributor" +DP_ROLE_ID=$(az cosmosdb sql role definition list \ + --account-name "$ACCOUNT_NAME" \ + --resource-group "$RG_NAME" \ + --query "[?roleName=='$DP_ROLE_NAME'].id | [0]" -o tsv) + +echo "Assigning data-plane role '$DP_ROLE_NAME' to user '$UPN' on Cosmos DB account '$ACCOUNT_NAME'..." + +az cosmosdb sql role assignment create \ + --account-name "$ACCOUNT_NAME" \ + --resource-group "$RG_NAME" \ + --scope "/" \ + --principal-id "$OBJECT_ID" \ + --role-definition-id "$DP_ROLE_ID" || echo "Data-plane role may already exist." + +echo "Assigned Cosmos roles to $UPN ($OBJECT_ID)." +echo "===============================" \ No newline at end of file diff --git a/deployers/bicep/main.bicep b/deployers/bicep/main.bicep index 5c4b9cd19..f05bc92ca 100644 --- a/deployers/bicep/main.bicep +++ b/deployers/bicep/main.bicep @@ -54,10 +54,10 @@ param enterpriseAppClientSecret string // configurations @description('''Authentication type for resources that support Managed Identity or Key authentication. - Key: Use access keys for authentication (application keys will be stored in Key Vault) -- Managed_Identity: Use Managed Identity for authentication''') +- managed_identity: Use Managed Identity for authentication''') @allowed([ - 'Key' - 'Managed_Identity' + 'key' + 'managed_identity' ]) param authenticationType string diff --git a/deployers/bicep/modules/appService.bicep b/deployers/bicep/modules/appService.bicep index 2242f8fbd..71a1c6a51 100644 --- a/deployers/bicep/modules/appService.bicep +++ b/deployers/bicep/modules/appService.bicep @@ -76,8 +76,8 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' = { {name: 'AZURE_COSMOS_ENDPOINT', value: cosmosDb.properties.documentEndpoint} {name: 'AZURE_COSMOS_AUTHENTICATION_TYPE', value: toLower(authenticationType)} - // Only add this setting if authenticationType is 'Key' - ...(authenticationType == 'Key' ? [{name: 'AZURE_COSMOS_KEY', value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/cosmos-db-key)'}] : []) + // Only add this setting if authenticationType is 'key' + ...(authenticationType == 'key' ? [{name: 'AZURE_COSMOS_KEY', value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/cosmos-db-key)'}] : []) {name: 'TENANT_ID', value: tenant().tenantId } {name: 'CLIENT_ID', value: enterpriseAppClientId } @@ -85,22 +85,22 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' = { {name: 'MICROSOFT_PROVIDER_AUTHENTICATION_SECRET', value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/enterprise-app-client-secret)'} {name: 'DOCKER_REGISTRY_SERVER_URL', value: 'https://${acrService.name}${acrDomain}' } - // Only add this setting if authenticationType is 'Key' - ...(authenticationType == 'Key' ? [{name: 'DOCKER_REGISTRY_SERVER_USERNAME', value: acrService.listCredentials().username}] : []) + // Only add this setting if authenticationType is 'key' + ...(authenticationType == 'key' ? [{name: 'DOCKER_REGISTRY_SERVER_USERNAME', value: acrService.listCredentials().username}] : []) - // Only add this setting if authenticationType is 'Key' - ...(authenticationType == 'Key' ? [{name: 'DOCKER_REGISTRY_SERVER_PASSWORD', value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/container-registry-key)'}] : []) + // Only add this setting if authenticationType is 'key' + ...(authenticationType == 'key' ? [{name: 'DOCKER_REGISTRY_SERVER_PASSWORD', value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/container-registry-key)'}] : []) {name: 'WEBSITE_AUTH_AAD_ALLOWED_TENANTS', value: tenant().tenantId } {name: 'AZURE_OPENAI_RESOURCE_NAME', value: openAiService.name} {name: 'AZURE_OPENAI_RESOURCE_GROUP_NAME', value: openAiResourceGroupName} {name: 'AZURE_OPENAI_URL', value: openAiService.properties.endpoint} {name: 'AZURE_SEARCH_SERVICE_NAME', value: searchService.name} - // Only add this setting if authenticationType is 'Key' - ...(authenticationType == 'Key' ? [{name: 'AZURE_SEARCH_API_KEY', value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/search-service-key)'}] : []) + // Only add this setting if authenticationType is 'key' + ...(authenticationType == 'key' ? [{name: 'AZURE_SEARCH_API_KEY', value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/search-service-key)'}] : []) {name: 'AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT', value: documentIntelligence.properties.endpoint} - // Only add this setting if authenticationType is 'Key' - ...(authenticationType == 'Key' ? [{name: 'AZURE_DOCUMENT_INTELLIGENCE_API_KEY', value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/document-intelligence-key)'}] : []) + // Only add this setting if authenticationType is 'key' + ...(authenticationType == 'key' ? [{name: 'AZURE_DOCUMENT_INTELLIGENCE_API_KEY', value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/document-intelligence-key)'}] : []) {name: 'APPINSIGHTS_INSTRUMENTATIONKEY', value: appInsights.properties.InstrumentationKey} {name: 'APPLICATIONINSIGHTS_CONNECTION_STRING', value: appInsights.properties.ConnectionString} {name: 'APPINSIGHTS_PROFILERFEATURE_VERSION', value: '1.0.0'} diff --git a/deployers/bicep/modules/azureContainerRegistry.bicep b/deployers/bicep/modules/azureContainerRegistry.bicep index 3da9c205c..c6f8ddfb4 100644 --- a/deployers/bicep/modules/azureContainerRegistry.bicep +++ b/deployers/bicep/modules/azureContainerRegistry.bicep @@ -50,7 +50,7 @@ resource acr 'Microsoft.ContainerRegistry/registries@2025-04-01' = { //========================================================= // store container registry keys in key vault if using key authentication and configure app permissions = true //========================================================= -module containerRegistrySecret 'keyVault-Secrets.bicep' = if (authenticationType == 'Key' && configureApplicationPermissions) { +module containerRegistrySecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions) { name: 'storeContainerRegistrySecret' params: { keyVaultName: keyVault diff --git a/deployers/bicep/modules/contentSafety.bicep b/deployers/bicep/modules/contentSafety.bicep index 6531c09c9..79e56e7b8 100644 --- a/deployers/bicep/modules/contentSafety.bicep +++ b/deployers/bicep/modules/contentSafety.bicep @@ -49,7 +49,7 @@ resource contentSafetyDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05 //========================================================= // store contentSafety keys in key vault if using key authentication and configure app permissions = true //========================================================= -module contentSafetySecret 'keyVault-Secrets.bicep' = if (authenticationType == 'Key' && configureApplicationPermissions) { +module contentSafetySecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions) { name: 'storeContentSafetySecret' params: { keyVaultName: keyVault diff --git a/deployers/bicep/modules/cosmosDb.bicep b/deployers/bicep/modules/cosmosDb.bicep index fb9e2cb32..205310c7e 100644 --- a/deployers/bicep/modules/cosmosDb.bicep +++ b/deployers/bicep/modules/cosmosDb.bicep @@ -87,7 +87,7 @@ resource cosmosDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-pre //========================================================= // store cosmos db keys in key vault if using key authentication and configure app permissions = true //========================================================= -module storeEnterpriseAppSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'Key' && configureApplicationPermissions) { +module storeEnterpriseAppSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions) { name: 'storeEnterpriseAppSecret' params: { keyVaultName: keyVault diff --git a/deployers/bicep/modules/documentIntelligence.bicep b/deployers/bicep/modules/documentIntelligence.bicep index 2534557a0..19e38638f 100644 --- a/deployers/bicep/modules/documentIntelligence.bicep +++ b/deployers/bicep/modules/documentIntelligence.bicep @@ -49,7 +49,7 @@ resource docIntelDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-p //========================================================= // store document intelligence keys in key vault if using key authentication and configure app permissions = true //========================================================= -module documentIntelligenceSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'Key' && configureApplicationPermissions) { +module documentIntelligenceSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions) { name: 'storeDocumentIntelligenceSecret' params: { keyVaultName: keyVault diff --git a/deployers/bicep/modules/openAI.bicep b/deployers/bicep/modules/openAI.bicep index e7a8df11e..b19cf9191 100644 --- a/deployers/bicep/modules/openAI.bicep +++ b/deployers/bicep/modules/openAI.bicep @@ -74,7 +74,7 @@ module aiModel 'aiModel.bicep' = [ //========================================================= // store openAI keys in key vault if using key authentication and configure app permissions = true //========================================================= -module openAISecret 'keyVault-Secrets.bicep' = if (authenticationType == 'Key' && configureApplicationPermissions) { +module openAISecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions) { name: 'storeOpenAISecret' params: { keyVaultName: keyVault diff --git a/deployers/bicep/modules/redisCache.bicep b/deployers/bicep/modules/redisCache.bicep index 2b2b38fae..3a65a395c 100644 --- a/deployers/bicep/modules/redisCache.bicep +++ b/deployers/bicep/modules/redisCache.bicep @@ -52,7 +52,7 @@ resource redisCacheDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01 //========================================================= // store redis cache keys in key vault if using key authentication and configure app permissions = true //========================================================= -module redisCacheSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'Key' && configureApplicationPermissions) { +module redisCacheSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions) { name: 'storeRedisCacheSecret' params: { keyVaultName: keyVault diff --git a/deployers/bicep/modules/setPermissions.bicep b/deployers/bicep/modules/setPermissions.bicep index c20279d20..56f83d1b1 100644 --- a/deployers/bicep/modules/setPermissions.bicep +++ b/deployers/bicep/modules/setPermissions.bicep @@ -74,7 +74,7 @@ resource kvSecretsUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' } // grant the webApp access to cosmos db as a contributor -resource cosmosContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'Managed_Identity') { +resource cosmosContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'managed_identity') { name: guid(cosmosDb.id, webApp.id, 'cosmos-contributor') scope: cosmosDb properties: { @@ -88,7 +88,7 @@ resource cosmosContributorRole 'Microsoft.Authorization/roleAssignments@2022-04- } // Grant the managed identity Cosmos DB Built-in Data Contributor role -resource cosmosDataContributorRole 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2023-04-15' = if (authenticationType == 'Managed_Identity') { +resource cosmosDataContributorRole 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2023-04-15' = if (authenticationType == 'managed_identity') { name: guid(cosmosDb.id, webApp.id, 'cosmos-data-contributor') parent: cosmosDb properties: { @@ -114,7 +114,7 @@ resource acrPullRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { } // Grant the openai service access cognitive services openai user -resource openAIUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'Managed_Identity') { +resource openAIUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'managed_identity') { scope: openAiService name: guid(openAiService.id, webApp.id, 'openai-user') properties: { @@ -128,7 +128,7 @@ resource openAIUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = i } // grant the managed identity access to document intelligence as a Cognitive Services User -resource docIntelUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'Managed_Identity') { +resource docIntelUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'managed_identity') { name: guid(docIntelService.id, webApp.id, 'doc-intel-user') scope: docIntelService properties: { @@ -142,7 +142,7 @@ resource docIntelUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = } // grant the managed identity access to the storage account as a blob data contributor -resource storageBlobDataContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'Managed_Identity') { +resource storageBlobDataContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'managed_identity') { name: guid(storageAccount.id, webApp.id, 'storage-blob-data-contributor') scope: storageAccount properties: { @@ -156,7 +156,7 @@ resource storageBlobDataContributorRole 'Microsoft.Authorization/roleAssignments } // grant the managed identity access to speech service as a Cognitive Services User -resource speechServiceUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (speechServiceName != '' && authenticationType == 'Managed_Identity') { +resource speechServiceUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (speechServiceName != '' && authenticationType == 'managed_identity') { name: guid(speechService.id, webApp.id, 'speech-service-user') scope: speechService properties: { @@ -170,7 +170,7 @@ resource speechServiceUserRole 'Microsoft.Authorization/roleAssignments@2022-04- } // grant the managed identity access to search service as a Search Service Contributor -resource searchServiceContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'Managed_Identity') { +resource searchServiceContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'managed_identity') { name: guid(searchService.id, webApp.id, 'search-service-contributor') scope: searchService properties: { @@ -184,7 +184,7 @@ resource searchServiceContributorRole 'Microsoft.Authorization/roleAssignments@2 } // grant the managed identity access to content safety as a Cognitive Services User -resource contentSafetyUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (contentSafetyName != '' && authenticationType == 'Managed_Identity') { +resource contentSafetyUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (contentSafetyName != '' && authenticationType == 'managed_identity') { name: guid(contentSafety.id, webApp.id, 'content-safety-user') scope: contentSafety properties: { diff --git a/deployers/bicep/modules/storageAccount.bicep b/deployers/bicep/modules/storageAccount.bicep index 0ae61b3a2..e40401a04 100644 --- a/deployers/bicep/modules/storageAccount.bicep +++ b/deployers/bicep/modules/storageAccount.bicep @@ -85,7 +85,7 @@ resource storageDiagnosticsBlob 'Microsoft.Insights/diagnosticSettings@2021-05-0 //========================================================= // store storage keys in key vault if using key authentication and configure app permissions = true //========================================================= -module storageAccountSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'Key' && configureApplicationPermissions) { +module storageAccountSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions) { name: 'storeStorageAccountSecret' params: { keyVaultName: keyVault From 9e18ecae3619f37e2ed9908f634c2bd914c8765c Mon Sep 17 00:00:00 2001 From: SteveCInVA Date: Tue, 2 Dec 2025 21:31:41 +0000 Subject: [PATCH 12/22] fix typo in enhanced citation deployment config --- deployers/bicep/postconfig.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployers/bicep/postconfig.py b/deployers/bicep/postconfig.py index d88574185..e3ee04976 100644 --- a/deployers/bicep/postconfig.py +++ b/deployers/bicep/postconfig.py @@ -123,7 +123,7 @@ # Citations > Enhanced Citations item["enable_enhanced_citations"] = True -item["aoffice_docs_authentication_type"] = var_authenticationType +item["office_docs_authentication_type"] = var_authenticationType item["office_docs_storage_account_blob_endpoint"] = var_blobStorageEndpoint # Safety > Content Safety From 3fa9cf9689ace02e1079246cdbc7ca81bc7581d3 Mon Sep 17 00:00:00 2001 From: SteveCInVA Date: Tue, 2 Dec 2025 21:43:06 +0000 Subject: [PATCH 13/22] Refactor Dockerfile to use Python 3.13-slim and streamline build process --- application/single_app/Dockerfile | 39 ++++++++++--------------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/application/single_app/Dockerfile b/application/single_app/Dockerfile index 308e157b4..56c8c1452 100644 --- a/application/single_app/Dockerfile +++ b/application/single_app/Dockerfile @@ -1,23 +1,9 @@ -# Builder stage: Use standard Python image with all build tools -FROM python:3.12-slim AS builder +# Builder stage: install dependencies in a virtualenv +# FROM cgr.dev/chainguard/python:latest-dev AS builder +FROM python:3.13-slim AS builder USER root -RUN apt-get update && apt-get install -y --no-install-recommends \ - gcc \ - g++ \ - make \ - libpq-dev \ - libmariadb-dev \ - unixodbc-dev \ - libffi-dev \ - libssl-dev \ - libfreetype6-dev \ - libjpeg-dev \ - libpng-dev \ - zlib1g-dev \ - && rm -rf /var/lib/apt/lists/* - # Ensure /app directory exists and has proper permissions RUN mkdir -p /app && chown root:root /app @@ -26,21 +12,20 @@ WORKDIR /app # Create a Python virtual environment RUN python -m venv /app/venv -# Create and permission the flask_session directory in builder stage -RUN mkdir -p /app/flask_session && chmod 777 /app/flask_session +# Create and permission the flask_session directory +#RUN mkdir -p /app/flask_session && chown -R nonroot:nonroot /app/flask_session +RUN mkdir -p /app/flask_session # Copy requirements and install them into the virtualenv COPY application/single_app/requirements.txt . ENV PATH="/app/venv/bin:$PATH" - -# Upgrade pip and install wheel -RUN pip install --no-cache-dir --upgrade pip setuptools wheel - -# Install requirements RUN pip install --no-cache-dir -r requirements.txt -# Runtime stage: Use secure Chainguard image -FROM cgr.dev/chainguard/python:latest +#FROM cgr.dev/chainguard/python:latest +FROM python:3.13-slim + +# Create nonroot user +RUN useradd -m -u 1000 nonroot WORKDIR /app @@ -61,4 +46,4 @@ EXPOSE 5000 USER nonroot:nonroot -ENTRYPOINT [ "python", "/app/app.py" ] +ENTRYPOINT [ "python", "/app/app.py" ] \ No newline at end of file From 1fa9d59383135350affa5ad227e213a037a11382 Mon Sep 17 00:00:00 2001 From: SteveCInVA Date: Wed, 10 Dec 2025 22:34:59 +0000 Subject: [PATCH 14/22] restart web application after deployment settings applied --- deployers/azure.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/deployers/azure.yaml b/deployers/azure.yaml index f4d32871f..90a40cc9a 100644 --- a/deployers/azure.yaml +++ b/deployers/azure.yaml @@ -39,6 +39,10 @@ hooks: echo "Running post-deployment configuration..." python3 -m pip install --user -r ./bicep/requirements.txt python3 ./bicep/postconfig.py + echo "Post-deployment configuration completed." + echo "Restarting web service to apply new settings..." + az webapp restart --name ${var_webService} --resource-group ${var_rgName} + echo "Web service restarted." else echo "Skipping post-deployment configuration (var_configureApplication is not true)" fi From f2039a854a1f7e69d89181de9a5804c81d13a7a3 Mon Sep 17 00:00:00 2001 From: SteveCInVA Date: Wed, 10 Dec 2025 22:43:40 +0000 Subject: [PATCH 15/22] remove setting for disableLocalAuth --- deployers/bicep/modules/contentSafety.bicep | 1 - deployers/bicep/modules/cosmosDb.bicep | 1 - deployers/bicep/modules/documentIntelligence.bicep | 1 - deployers/bicep/modules/openAI.bicep | 1 - deployers/bicep/modules/speechService.bicep | 3 +-- 5 files changed, 1 insertion(+), 6 deletions(-) diff --git a/deployers/bicep/modules/contentSafety.bicep b/deployers/bicep/modules/contentSafety.bicep index 79e56e7b8..819200f58 100644 --- a/deployers/bicep/modules/contentSafety.bicep +++ b/deployers/bicep/modules/contentSafety.bicep @@ -27,7 +27,6 @@ resource contentSafety 'Microsoft.CognitiveServices/accounts@2025-06-01' = { } properties: { publicNetworkAccess: 'Enabled' - disableLocalAuth: false customSubDomainName: toLower('${appName}-${environment}-contentsafety') } tags: tags diff --git a/deployers/bicep/modules/cosmosDb.bicep b/deployers/bicep/modules/cosmosDb.bicep index 205310c7e..3c80e314d 100644 --- a/deployers/bicep/modules/cosmosDb.bicep +++ b/deployers/bicep/modules/cosmosDb.bicep @@ -25,7 +25,6 @@ resource cosmosDb 'Microsoft.DocumentDB/databaseAccounts@2023-04-15' = { kind: 'GlobalDocumentDB' properties: { databaseAccountOfferType: 'Standard' - disableLocalAuth: true capabilities: [ { name: 'EnableServerless' diff --git a/deployers/bicep/modules/documentIntelligence.bicep b/deployers/bicep/modules/documentIntelligence.bicep index 19e38638f..28f591c7e 100644 --- a/deployers/bicep/modules/documentIntelligence.bicep +++ b/deployers/bicep/modules/documentIntelligence.bicep @@ -27,7 +27,6 @@ resource docIntel 'Microsoft.CognitiveServices/accounts@2025-06-01' = { } properties: { publicNetworkAccess: 'Enabled' - disableLocalAuth: false customSubDomainName: toLower('${appName}-${environment}-docintel') } tags: tags diff --git a/deployers/bicep/modules/openAI.bicep b/deployers/bicep/modules/openAI.bicep index b19cf9191..0875b27f1 100644 --- a/deployers/bicep/modules/openAI.bicep +++ b/deployers/bicep/modules/openAI.bicep @@ -37,7 +37,6 @@ resource openAI 'Microsoft.CognitiveServices/accounts@2024-10-01' = { } properties: { publicNetworkAccess: 'Enabled' - disableLocalAuth: false customSubDomainName: toLower('${appName}-${environment}-openai') } tags: tags diff --git a/deployers/bicep/modules/speechService.bicep b/deployers/bicep/modules/speechService.bicep index d1af6e0d9..69116ef37 100644 --- a/deployers/bicep/modules/speechService.bicep +++ b/deployers/bicep/modules/speechService.bicep @@ -30,7 +30,6 @@ resource speechService 'Microsoft.CognitiveServices/accounts@2024-10-01' = { } properties: { publicNetworkAccess: 'Enabled' - disableLocalAuth: false customSubDomainName: toLower('${appName}-${environment}-speech') } tags: tags @@ -52,7 +51,7 @@ resource speechServiceDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05 //========================================================= // store speech Service keys in key vault if using key authentication and configure app permissions = true //========================================================= -module speechServiceSecret 'keyVault-Secrets.bicep' = if (configureApplicationPermissions) { +module speechServiceSecret 'keyVault-Secrets.bicep' = if ((authenticationType == 'key') && (configureApplicationPermissions) ){ name: 'storeSpeechServiceSecret' params: { keyVaultName: keyVault From 5e08e2c0fc0d54b25a308e23d830f3af470a9f89 Mon Sep 17 00:00:00 2001 From: SteveCInVA Date: Thu, 11 Dec 2025 13:15:00 +0000 Subject: [PATCH 16/22] update to latest version of bicep deployment --- deployers/bicep/README.md | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/deployers/bicep/README.md b/deployers/bicep/README.md index 7a76363b0..ea3984754 100644 --- a/deployers/bicep/README.md +++ b/deployers/bicep/README.md @@ -113,26 +113,19 @@ Using the bash terminal in Visual Studio Code - Select an Azure Subscription to use: *\* -- Enter a value for the 'useExistingAcr' infrastructure parameter: *\* -- Enter a value for the 'useExistingOpenAISvc' infrastructure parameter: *\* Provisioning may take between 10-40 minutes depending on the options selected. @@ -142,31 +135,24 @@ On the completion of the deployment, a URL will be presented, the user may use t ### Post Deployment Tasks: -Once logged in to the newly deployed application with admin credentials, the application will need to be configured with several configurations: +Once logged in to the newly deployed application with admin credentials, the application will need to be set up with several configurations: -1. Admin Settings > Health Check > "Enable External Health Check Endpoint" - Set to "ON" -1. AI Models > GPT Configuration & Embeddings Configuration. Use managed Identity. Configure the subscription and resource group. Click Save +1. AI Models > GPT Configuration & Embeddings Configuration. Application is pre-configured with the chosen security model (key / managed identity). Select "Test GPT Connection" and "Test Embedding Connection" to verify connection. > Known Bug: User will be unable to Fetch GPT or Embedding models.
Workaround: Set configurations in CosmosDB. For details see [Workarounds](##Workarounds) below. -1. Agents and Actions > Agents Configuration > "Enable Agents" - Set to "ON" 1. Logging > Application Insights Logging > "Enable Application Insights Global Logging - Set to "ON" 1. Citations > Ehnahced Citations > "Enable Enhanced Citations" - Set to "ON" - Configure "All Filetypes" - "Storage Account Authentication Type" = Managed Identity - "Storage Account Blob Endpoint" = "https://\\sa.blob.core.windows.net" (or appropiate domain if in Azure Gov.) -1. Workflow > Workflow Settings > "Enable Workflow" - Set to "ON" - > Note if the deployment option for "deployContentSafety" was set to true follow the next step. -1. Safety > Content Safety > "Enable Content Safety" - Set to "ON" - - "Content Safety Endpoint" - "https://\-\-contentsafety.cognitiveservices.azure.com/" (or appropiate domain if in Azure Gov.) 1. Safety > Conversation Archiving > "Enable Conversation Archiving" - Set to "ON" -1. PII Analysis > PII Analysis > "Enable PII Analysis" - Set to "ON" 1. Search & Extract > Azure AI Search - "Search Endpoint" = "https://\-\-search.search.windows.net" (or appropiate domain if in Azure Gov.) > Known Bug: Unable to configure "Managed Identity" authentication type. Must use "Key" - "Authentication Type" - Key - - "Search Key" - Retreive from the deployed search service. + - "Search Key" - *Pre-populated from key vault value*. - At the top of the Admin Page you'll see warning boxes indicating Index Schema Mismatch. - Click "Create user Index" - Click "Create group Index" From c67822d4eb04c923cbc264768e0725ceae6bf90c Mon Sep 17 00:00:00 2001 From: SteveCInVA Date: Thu, 11 Dec 2025 13:22:40 +0000 Subject: [PATCH 17/22] remove dead code --- deployers/bicep/modules/appService.bicep | 4 ---- .../bicep/modules/azureContainerRegistry.bicep | 16 ---------------- deployers/bicep/modules/keyVault.bicep | 17 ----------------- deployers/bicep/modules/openAI.bicep | 7 +------ deployers/bicep/modules/setPermissions.bicep | 1 - deployers/bicep/postconfig.py | 4 ++-- 6 files changed, 3 insertions(+), 46 deletions(-) diff --git a/deployers/bicep/modules/appService.bicep b/deployers/bicep/modules/appService.bicep index 71a1c6a51..1870a4c93 100644 --- a/deployers/bicep/modules/appService.bicep +++ b/deployers/bicep/modules/appService.bicep @@ -120,10 +120,6 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' = { } identity: { type: 'SystemAssigned' - // type: 'SystemAssigned, UserAssigned' - // userAssignedIdentities: { - // '${managedIdentityId}': {} - // } } tags: union(tags, { 'azd-service-name': 'web' }) } diff --git a/deployers/bicep/modules/azureContainerRegistry.bicep b/deployers/bicep/modules/azureContainerRegistry.bicep index c6f8ddfb4..fe99db104 100644 --- a/deployers/bicep/modules/azureContainerRegistry.bicep +++ b/deployers/bicep/modules/azureContainerRegistry.bicep @@ -4,8 +4,6 @@ param location string param acrName string param tags object -// param managedIdentityPrincipalId string -// param managedIdentityId string param enableDiagLogging bool param logAnalyticsId string @@ -33,20 +31,6 @@ resource acr 'Microsoft.ContainerRegistry/registries@2025-04-01' = { tags: tags } -// // grant the managed identity access to azure container registry as a pull contributor -// resource acrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (configureApplicationPermissions) { -// name: guid(acr.id, managedIdentityId, 'acr-acrpull') -// scope: acr -// properties: { -// roleDefinitionId: subscriptionResourceId( -// 'Microsoft.Authorization/roleDefinitions', -// '7f951dda-4ed3-4680-a7ca-43fe172d538d' -// ) -// principalId: managedIdentityPrincipalId -// principalType: 'ServicePrincipal' -// } -// } - //========================================================= // store container registry keys in key vault if using key authentication and configure app permissions = true //========================================================= diff --git a/deployers/bicep/modules/keyVault.bicep b/deployers/bicep/modules/keyVault.bicep index e3c6729f2..508d4a967 100644 --- a/deployers/bicep/modules/keyVault.bicep +++ b/deployers/bicep/modules/keyVault.bicep @@ -5,8 +5,6 @@ param appName string param environment string param tags object -// param managedIdentityPrincipalId string -// param managedIdentityId string param enableDiagLogging bool param logAnalyticsId string @@ -35,21 +33,6 @@ resource kv 'Microsoft.KeyVault/vaults@2024-11-01' = { tags: tags } -// // grant the managed identity access to the key vault -// resource kvSecretsUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { -// name: guid(kv.id, managedIdentityId, 'kv-secrets-user') -// scope: kv -// properties: { -// // Built-in role definition id for "Key Vault Secrets User" -// roleDefinitionId: subscriptionResourceId( -// 'Microsoft.Authorization/roleDefinitions', -// '4633458b-17de-408a-b874-0445c86b69e6' -// ) -// principalId: managedIdentityPrincipalId -// principalType: 'ServicePrincipal' -// } -// } - // configure diagnostic settings for key vault resource kvDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (enableDiagLogging) { name: toLower('${kv.name}-diagnostics') diff --git a/deployers/bicep/modules/openAI.bicep b/deployers/bicep/modules/openAI.bicep index 0875b27f1..e56db352c 100644 --- a/deployers/bicep/modules/openAI.bicep +++ b/deployers/bicep/modules/openAI.bicep @@ -30,10 +30,6 @@ resource openAI 'Microsoft.CognitiveServices/accounts@2024-10-01' = { } identity: { type: 'SystemAssigned' - // type: 'SystemAssigned, UserAssigned' - // userAssignedIdentities: { - // '${managedIdentityId}': {} - // } } properties: { publicNetworkAccess: 'Enabled' @@ -85,5 +81,4 @@ module openAISecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' & output openAIName string = openAI.name output openAIResourceGroup string = resourceGroup().name output openAIEndpoint string = openAI.properties.endpoint -// output openAIGptModel string = aiModel_gpt4o.outputs.modelName -// output openAITextEmbeddingModel string = aiModel_textEmbedding.outputs.modelName + diff --git a/deployers/bicep/modules/setPermissions.bicep b/deployers/bicep/modules/setPermissions.bicep index 56f83d1b1..eab70281a 100644 --- a/deployers/bicep/modules/setPermissions.bicep +++ b/deployers/bicep/modules/setPermissions.bicep @@ -6,7 +6,6 @@ param keyVaultName string param cosmosDBName string param acrName string param openAIName string -//param openAIResourceGroupName string param docIntelName string param storageAccountName string param speechServiceName string diff --git a/deployers/bicep/postconfig.py b/deployers/bicep/postconfig.py index e3ee04976..40b12dafa 100644 --- a/deployers/bicep/postconfig.py +++ b/deployers/bicep/postconfig.py @@ -19,7 +19,7 @@ # Read the existing item by ID and partition key item_id = "app_settings" -partition_key = "app_settings" # Use the actual partition key value for this item +partition_key = "app_settings" try: item = container.read_item(item=item_id, partition_key=partition_key) print(f"Found existing app_setting document") @@ -105,7 +105,7 @@ } # Agents and Actions > Agents Configuration -item["enable_semantic_kernel"] = True +item["enable_semantic_kernel"] = False # Logging > Application Insights Logging item["enable_appinsights_global_logging"] = True From b23aafea53d2b07cfdf696e7eb8fce49eae1527d Mon Sep 17 00:00:00 2001 From: SteveCInVA Date: Thu, 11 Dec 2025 13:29:21 +0000 Subject: [PATCH 18/22] code cleanup / formatting --- deployers/bicep/README_orig.md | 156 ------------------ deployers/bicep/main.bicep | 67 +++----- deployers/bicep/main.parameters.json | 11 +- deployers/bicep/modules/aiModel.bicep | 2 - deployers/bicep/modules/appService.bicep | 108 +++++++----- .../modules/azureContainerRegistry.bicep | 2 +- deployers/bicep/modules/contentSafety.bicep | 4 +- deployers/bicep/modules/cosmosDb.bicep | 4 +- .../bicep/modules/diagnosticSettings.bicep | 2 +- .../bicep/modules/documentIntelligence.bicep | 4 +- deployers/bicep/modules/keyVault.bicep | 1 - deployers/bicep/modules/managedIdentity.bicep | 1 - deployers/bicep/modules/openAI.bicep | 1 - deployers/bicep/modules/redisCache.bicep | 2 +- deployers/bicep/modules/search.bicep | 2 +- deployers/bicep/modules/setPermissions.bicep | 18 +- deployers/bicep/modules/speechService.bicep | 4 +- deployers/bicep/modules/storageAccount.bicep | 4 +- deployers/bicep/modules/videoIndexer.bicep | 6 +- deployers/bicep/postconfig.py | 37 +++-- 20 files changed, 145 insertions(+), 291 deletions(-) delete mode 100644 deployers/bicep/README_orig.md diff --git a/deployers/bicep/README_orig.md b/deployers/bicep/README_orig.md deleted file mode 100644 index 4ded54915..000000000 --- a/deployers/bicep/README_orig.md +++ /dev/null @@ -1,156 +0,0 @@ -# Simple Chat - Deployment using BICEP - -[Return to Main](../README.md) - -## Manual Pre-Requisites (Critically Important) - -Create Entra ID App Registration: - -Go to Azure portal > Microsoft Entra ID > App registrations > New registration. - -- Provide a name (e.g., $appRegistrationName from the script's logic). -- Supported account types: Usually "Accounts in this organizational directory only." -- Do not configure Redirect URI yet. You will get these from the Bicep output. -- Once created, note down the Application (client) ID (this is appRegistrationClientId parameter). -- Go to "Certificates & secrets" > "New client secret" > Create a secret and copy its Value immediately (this is appRegistrationClientSecret parameter). -- **** NO **** Go to "Token configuration" and enable "ID tokens" and "Access tokens" for implicit grant and hybrid flows if needed by your app (the script attempts az ad app update --enable-id-token-issuance true --enable-access-token-issuance true). -- The script also adds API permissions for Microsoft Graph and attempts to add owners. These should be configured manually on the App Registration. - - User.Read, Delegated - - profile, Delegated - - email, Delegated - - Group.Read.All, Delegated - - offline_access, Delegated - - openid, Delegated - - People.Read.All, Delegated - - User.ReadBasic.All, Delegated -- The script also references appRegistrationRoles.json. If your application defines app roles, configure these in the App Registration manifest. -- Obtain the Object ID of the Service Principal associated with this App Registration: az ad sp show --id --query id -o tsv. This will be the appRegistrationSpObjectId parameter. - -Create Entra ID Security Groups: If your application relies on the security groups ($global_EntraSecurityGroupNames), create them manually in Entra ID. - -Azure Container Registry (ACR): Ensure the ACR specified by acrName exists and the image imageName is pushed to it. - -Azure OpenAI Access: If useExistingOpenAiInstance is true, ensure the specified existing OpenAI resource exists and you have its name and resource group. If false, ensure your subscription is approved for Azure OpenAI and the chosen SKU and region support it. - -## Deploy - -(Optional) Create a resource group if you don't have one: az group create --name MySimpleChatRG --location usgovvirginia - -Deploy the Bicep file. - -### azure cli - -#### validate before deploy - -az bicep build --file main.bicep - -az deployment group validate ` ---resource-group MySimpleChatRG ` ---template-file main.bicep ` ---parameters main.json - -az deployment group create ` ---resource-group MySimpleChatRG ` ---template-file main.bicep ` ---parameters main.bicepparam ` ---parameters appRegistrationClientSecret="YOUR_APP_REG_SECRET_VALUE" - -## Post-Deployment Manual Steps (from Bicep outputs and script) - -### App Registration - -- Manage > Authentication - - Web Redirect Url example: - - - - - - Front-channel logout URL: - - Implicit grant and hyrbid flows: - - Access tokens: Check this - - ID tokens: Check this - - Supported account types: Accounts in this organization directly only - - Advanced Settings > Allow public client flows > Enable the following mobile and desktop flows: No - -- Manage > Certificates & secrets - - You will see 2 secrets here in the end. One created by you pre-deployment and one created when you add Authentication to the App Service. - -- Manage > Token configuration: Nothing to do here. Leave empty. - -- Manage > API Permissions: Click "Grant Admin Consent for tenant" to all deletgated permissions - -- Manage > Expose an API: Nothing to do here. Leave empty. - -- Manage > App Roles: You should see the following app roles: [FeedbackAdmin, Safety Violation Admin, Create Group, Users, Admins] - -### Entra Security Groups - -- Assignments: If you created security groups, assign them to the corresponding Enterprise Application application roles and add members to the security groups. - -### App Service - -- Authentication - - Identity Provider: Microsoft - - Choose a tenant for your application and its users: Workforce configuration (current tenant) - - Pick an existing app registration in this directory: Select the app registration you created pre-deployment - - Client secret expiration: Recommended 180 days - - *** Leave all other values default - - Note: Check App Setting "MICROSOFT_PROVIDER_AUTHENTICATION_SECRET" for a secret value created by configuring the Authentication. This secret will be added to your App Registration as well. - -- Deployment Center > Registry Settings (These sometimes get screwed up during a deploy. Make sure these values are correct.) - - The deployer can get messed up here. Make sure the correct values are being displayed for your registry settings. - - Container Type: Single Container - - Registry source: Azure Container Registry - - Subscription Id: [Your subscription] - - Authentication: Managed Identity - - Identity: Managed identity deployer deployed - - Registry: [Name of the ACR: e.g. SomeRegistry] - - Image: simplechat - - Tag: 2025-05-29_1 - - Startup file or command: [Blank] - -- Restart & Test: Restart the App Service and test the Web UI. - -- Open Monitoring > Log stream and make sure the container has loaded and is ready. - -### Azure AI Search - -- Manually create 2 Indexes: Deploy your search index schemas (ai_search-index-group.json, ai_search-index-user.json) using Index as Json in the Azure portal. - - Note: These files can be found in GitHub repository folder /deployers/bicep/artifacts - -### Existing Open AI (Option) - -- Make sure the Managed Idenity and the Entra App Registration have been added to the Open AI Instance IAM with RBAC Roles [Cognitive Services Contributor, Cognitive Services OpenAI User, Cognitive Services User] - -### Admin center in Web UI application - -- Open a browser and navigate to the url of the Azure App Service default domain. - -- Once you have logged into the application, navigate to "Admin" and configure the settings. - - Note: If you cannot login or see the Admin link, make sure you have added yourself to the Enterprise Application (Assigned users and groups) users for the App Registration you created. Make sure you have assigned your user account to the "Admin" app role. diff --git a/deployers/bicep/main.bicep b/deployers/bicep/main.bicep index f05bc92ca..9e4e54e42 100644 --- a/deployers/bicep/main.bicep +++ b/deployers/bicep/main.bicep @@ -76,26 +76,26 @@ param enableDiagLogging bool @description('''Array of GPT model names to deploy to the OpenAI resource.''') param gptModels array = [ { - modelName: 'gpt-4.1' - modelVersion: '2025-04-14' - skuName: 'GlobalStandard' - skuCapacity: 150 + modelName: 'gpt-4.1' + modelVersion: '2025-04-14' + skuName: 'GlobalStandard' + skuCapacity: 150 } { - modelName: 'gpt-4o' - modelVersion: '2024-11-20' - skuName: 'GlobalStandard' - skuCapacity: 100 + modelName: 'gpt-4o' + modelVersion: '2024-11-20' + skuName: 'GlobalStandard' + skuCapacity: 100 } ] @description('''Array of embedding model names to deploy to the OpenAI resource.''') param embeddingModels array = [ { - modelName: 'text-embedding-3-small' - modelVersion: '1' - skuName: 'GlobalStandard' - skuCapacity: 150 + modelName: 'text-embedding-3-small' + modelVersion: '1' + skuName: 'GlobalStandard' + skuCapacity: 150 } { modelName: 'text-embedding-3-large' @@ -103,7 +103,7 @@ param embeddingModels array = [ skuName: 'GlobalStandard' skuCapacity: 150 } -] +] //---------------- // optional services @@ -143,20 +143,6 @@ resource rg 'Microsoft.Resources/resourceGroups@2022-09-01' = { tags: tags } -//========================================================= -// Create managed identity -//========================================================= -// module managedIdentity 'modules/managedIdentity.bicep' = { -// name: 'managedIdentity' -// scope: rg -// params: { -// location: location -// appName: appName -// environment: environment -// tags: tags -// } -// } - //========================================================= // Create log analytics workspace //========================================================= @@ -197,8 +183,6 @@ module keyVault 'modules/keyVault.bicep' = { appName: appName environment: environment tags: tags - //managedIdentityPrincipalId: managedIdentity.outputs.principalId - //managedIdentityId: managedIdentity.outputs.resourceId enableDiagLogging: enableDiagLogging logAnalyticsId: logAnalytics.outputs.logAnalyticsId } @@ -207,7 +191,7 @@ module keyVault 'modules/keyVault.bicep' = { //========================================================= // Store enterprise app client secret in key vault //========================================================= -module storeEnterpriseAppSecret 'modules/keyVault-Secrets.bicep' = if (!empty(enterpriseAppClientSecret)) { +module storeEnterpriseAppSecret 'modules/keyVault-Secrets.bicep' = if (!empty(enterpriseAppClientSecret)) { name: 'storeEnterpriseAppSecret' scope: rg params: { @@ -247,8 +231,6 @@ module acr 'modules/azureContainerRegistry.bicep' = { location: location acrName: acrName tags: tags - //managedIdentityPrincipalId: managedIdentity.outputs.principalId - //managedIdentityId: managedIdentity.outputs.resourceId enableDiagLogging: enableDiagLogging logAnalyticsId: logAnalytics.outputs.logAnalyticsId @@ -321,7 +303,7 @@ module storageAccount 'modules/storageAccount.bicep' = { //========================================================= // Create - OpenAI Service //========================================================= -module openAI 'modules/openAI.bicep' = { +module openAI 'modules/openAI.bicep' = { name: 'openAI' scope: rg params: { @@ -369,8 +351,6 @@ module appService 'modules/appService.bicep' = { environment: environment tags: tags acrName: acr.outputs.acrName - // managedIdentityId: managedIdentity.outputs.resourceId - // managedIdentityClientId: managedIdentity.outputs.clientId enableDiagLogging: enableDiagLogging logAnalyticsId: logAnalytics.outputs.logAnalyticsId appServicePlanId: appServicePlan.outputs.appServicePlanId @@ -433,7 +413,6 @@ module redisCache 'modules/redisCache.bicep' = if (deployRedisCache) { } } - //========================================================= // Create Optional Resource - Speech Service //========================================================= @@ -454,7 +433,6 @@ module speechService 'modules/speechService.bicep' = if (deploySpeechService) { } } - //========================================================= // Create Optional Resource - Video Indexer Service //========================================================= @@ -471,7 +449,6 @@ module videoIndexerService 'modules/videoIndexer.bicep' = if (deployVideoIndexer storageAccount: storageAccount.outputs.name openAiServiceName: openAI.outputs.openAIName - } } @@ -488,7 +465,6 @@ module setPermissions 'modules/setPermissions.bicep' = if (configureApplicationP cosmosDBName: cosmosDB.outputs.cosmosDbName acrName: acr.outputs.acrName openAIName: openAI.outputs.openAIName - // openAIResourceGroupName: useExistingOpenAISvc ? existingOpenAIResourceGroupName : openAI_create.outputs.openAIResourceGroup docIntelName: docIntel.outputs.documentIntelligenceServiceName storageAccountName: storageAccount.outputs.name #disable-next-line BCP318 // expect one value to be null @@ -510,7 +486,7 @@ output var_rgName string = rgName // output values required for predeploy script in azure.yaml output var_webService string = appService.outputs.name output var_imageName string = contains(imageName, ':') ? split(imageName, ':')[0] : imageName -output var_imageTag string = split(imageName, ':')[1] +output var_imageTag string = split(imageName, ':')[1] output var_containerRegistry string = containerRegistry output var_acrName string = toLower('${appName}${environment}acr') @@ -519,9 +495,7 @@ output var_configureApplication bool = configureApplicationPermissions output var_keyVaultUri string = keyVault.outputs.keyVaultUri output var_cosmosDb_uri string = cosmosDB.outputs.cosmosDbUri output var_subscriptionId string = subscription().subscriptionId - output var_authenticationType string = toLower(authenticationType) - output var_openAIEndpoint string = openAI.outputs.openAIEndpoint output var_openAIResourceGroup string = openAI.outputs.openAIResourceGroup //may be able to remove output var_openAIGPTModels array = gptModels @@ -529,13 +503,16 @@ output var_openAIEmbeddingModels array = embeddingModels output var_blobStorageEndpoint string = storageAccount.outputs.endpoint #disable-next-line BCP318 // expect one value to be null output var_contentSafetyEndpoint string = deployContentSafety ? contentSafety.outputs.contentSafetyEndpoint : '' - output var_deploymentLocation string = rg.location output var_searchServiceEndpoint string = searchService.outputs.searchServiceEndpoint output var_documentIntelligenceServiceEndpoint string = docIntel.outputs.documentIntelligenceServiceEndpoint +output var_videoIndexerName string = deployVideoIndexerService #disable-next-line BCP318 // expect one value to be null -output var_videoIndexerName string = deployVideoIndexerService ? videoIndexerService.outputs.videoIndexerServiceName : '' + ? videoIndexerService.outputs.videoIndexerServiceName + : '' +output var_videoIndexerAccountId string = deployVideoIndexerService #disable-next-line BCP318 // expect one value to be null -output var_videoIndexerAccountId string = deployVideoIndexerService ? videoIndexerService.outputs.videoIndexerAccountId : '' + ? videoIndexerService.outputs.videoIndexerAccountId + : '' #disable-next-line BCP318 // expect one value to be null output var_speechServiceEndpoint string = deploySpeechService ? speechService.outputs.speechServiceEndpoint : '' diff --git a/deployers/bicep/main.parameters.json b/deployers/bicep/main.parameters.json index 20a70fb1b..e819bca90 100644 --- a/deployers/bicep/main.parameters.json +++ b/deployers/bicep/main.parameters.json @@ -15,10 +15,10 @@ "value": "${AZURE_ENV_NAME}" }, "specialTags": { - "value": { - "Project": "SimpleChat", - "SystemOwner": "Steve Carroll" - } + "value": { + "Project": "SimpleChat", + "SystemOwner": "Steve Carroll" + } }, "enterpriseAppClientId": { "value": "${ENTERPRISE_APP_CLIENT_ID}" @@ -30,8 +30,7 @@ "value": "${CONTAINER_IMAGE_NAME}" }, "authenticationType": { - "value": "${AUTHENTICATION_TYPE}" + "value": "${AUTHENTICATION_TYPE}" } - } } \ No newline at end of file diff --git a/deployers/bicep/modules/aiModel.bicep b/deployers/bicep/modules/aiModel.bicep index 2e1d89afc..b972ee3ab 100644 --- a/deployers/bicep/modules/aiModel.bicep +++ b/deployers/bicep/modules/aiModel.bicep @@ -1,4 +1,3 @@ - param parent string param modelName string param modelVersion string @@ -24,4 +23,3 @@ resource aiModel 'Microsoft.CognitiveServices/accounts/deployments@2025-06-01' = capacity: skuCapacity } } - diff --git a/deployers/bicep/modules/appService.bicep b/deployers/bicep/modules/appService.bicep index 1870a4c93..4f70f03a4 100644 --- a/deployers/bicep/modules/appService.bicep +++ b/deployers/bicep/modules/appService.bicep @@ -23,7 +23,7 @@ param authenticationType string @secure() param enterpriseAppClientSecret string = '' -param keyVaultUri string +param keyVaultUri string // Import diagnostic settings configurations module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging) { @@ -34,7 +34,7 @@ resource acrService 'Microsoft.ContainerRegistry/registries@2025-04-01' existing name: acrName } -resource cosmosDb 'Microsoft.DocumentDB/databaseAccounts@2023-04-15' existing = { +resource cosmosDb 'Microsoft.DocumentDB/databaseAccounts@2023-04-15' existing = { name: cosmosDbName } @@ -47,7 +47,7 @@ resource openAiService 'Microsoft.CognitiveServices/accounts@2024-10-01' existin } resource documentIntelligence 'Microsoft.CognitiveServices/accounts@2025-06-01' existing = { - name: documentIntelligenceServiceName + name: documentIntelligenceServiceName } resource appInsights 'Microsoft.Insights/components@2020-02-02' existing = { name: appInsightsName @@ -70,49 +70,81 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' = { ftpsState: 'Disabled' healthCheckPath: '/external/healthcheck' appSettings: [ + { name: 'AZURE_ENDPOINT', value: azurePlatform == 'AzureUSGovernment' ? 'usgovernment' : 'public' } + { name: 'SCM_DO_BUILD_DURING_DEPLOYMENT', value: 'false' } + { name: 'AZURE_COSMOS_ENDPOINT', value: cosmosDb.properties.documentEndpoint } + { name: 'AZURE_COSMOS_AUTHENTICATION_TYPE', value: toLower(authenticationType) } - {name: 'AZURE_ENDPOINT', value: azurePlatform == 'AzureUSGovernment' ? 'usgovernment' : 'public'} - {name: 'SCM_DO_BUILD_DURING_DEPLOYMENT', value: 'false'} - {name: 'AZURE_COSMOS_ENDPOINT', value: cosmosDb.properties.documentEndpoint} - {name: 'AZURE_COSMOS_AUTHENTICATION_TYPE', value: toLower(authenticationType)} - // Only add this setting if authenticationType is 'key' - ...(authenticationType == 'key' ? [{name: 'AZURE_COSMOS_KEY', value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/cosmos-db-key)'}] : []) - - {name: 'TENANT_ID', value: tenant().tenantId } - {name: 'CLIENT_ID', value: enterpriseAppClientId } - {name: 'SECRET_KEY', value: !empty(enterpriseAppClientSecret) ? enterpriseAppClientSecret : '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/enterprise-app-client-secret)' } - {name: 'MICROSOFT_PROVIDER_AUTHENTICATION_SECRET', value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/enterprise-app-client-secret)'} - {name: 'DOCKER_REGISTRY_SERVER_URL', value: 'https://${acrService.name}${acrDomain}' } + ...(authenticationType == 'key' + ? [{ name: 'AZURE_COSMOS_KEY', value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/cosmos-db-key)' }] + : []) + + { name: 'TENANT_ID', value: tenant().tenantId } + { name: 'CLIENT_ID', value: enterpriseAppClientId } + { + name: 'SECRET_KEY' + value: !empty(enterpriseAppClientSecret) + ? enterpriseAppClientSecret + : '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/enterprise-app-client-secret)' + } + { + name: 'MICROSOFT_PROVIDER_AUTHENTICATION_SECRET' + value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/enterprise-app-client-secret)' + } + { name: 'DOCKER_REGISTRY_SERVER_URL', value: 'https://${acrService.name}${acrDomain}' } // Only add this setting if authenticationType is 'key' - ...(authenticationType == 'key' ? [{name: 'DOCKER_REGISTRY_SERVER_USERNAME', value: acrService.listCredentials().username}] : []) + ...(authenticationType == 'key' + ? [{ name: 'DOCKER_REGISTRY_SERVER_USERNAME', value: acrService.listCredentials().username }] + : []) // Only add this setting if authenticationType is 'key' - ...(authenticationType == 'key' ? [{name: 'DOCKER_REGISTRY_SERVER_PASSWORD', value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/container-registry-key)'}] : []) - - {name: 'WEBSITE_AUTH_AAD_ALLOWED_TENANTS', value: tenant().tenantId } - {name: 'AZURE_OPENAI_RESOURCE_NAME', value: openAiService.name} - {name: 'AZURE_OPENAI_RESOURCE_GROUP_NAME', value: openAiResourceGroupName} - {name: 'AZURE_OPENAI_URL', value: openAiService.properties.endpoint} - {name: 'AZURE_SEARCH_SERVICE_NAME', value: searchService.name} + ...(authenticationType == 'key' + ? [ + { + name: 'DOCKER_REGISTRY_SERVER_PASSWORD' + value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/container-registry-key)' + } + ] + : []) + + { name: 'WEBSITE_AUTH_AAD_ALLOWED_TENANTS', value: tenant().tenantId } + { name: 'AZURE_OPENAI_RESOURCE_NAME', value: openAiService.name } + { name: 'AZURE_OPENAI_RESOURCE_GROUP_NAME', value: openAiResourceGroupName } + { name: 'AZURE_OPENAI_URL', value: openAiService.properties.endpoint } + { name: 'AZURE_SEARCH_SERVICE_NAME', value: searchService.name } // Only add this setting if authenticationType is 'key' - ...(authenticationType == 'key' ? [{name: 'AZURE_SEARCH_API_KEY', value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/search-service-key)'}] : []) - {name: 'AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT', value: documentIntelligence.properties.endpoint} + ...(authenticationType == 'key' + ? [ + { + name: 'AZURE_SEARCH_API_KEY' + value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/search-service-key)' + } + ] + : []) + { name: 'AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT', value: documentIntelligence.properties.endpoint } // Only add this setting if authenticationType is 'key' - ...(authenticationType == 'key' ? [{name: 'AZURE_DOCUMENT_INTELLIGENCE_API_KEY', value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/document-intelligence-key)'}] : []) - {name: 'APPINSIGHTS_INSTRUMENTATIONKEY', value: appInsights.properties.InstrumentationKey} - {name: 'APPLICATIONINSIGHTS_CONNECTION_STRING', value: appInsights.properties.ConnectionString} - {name: 'APPINSIGHTS_PROFILERFEATURE_VERSION', value: '1.0.0'} - {name: 'APPINSIGHTS_SNAPSHOTFEATURE_VERSION', value: '1.0.0'} - {name: 'APPLICATIONINSIGHTS_CONFIGURATION_CONTENT', value: ''} - {name: 'ApplicationInsightsAgent_EXTENSION_VERSION', value: '~3'} - {name: 'DiagnosticServices_EXTENSION_VERSION', value: '~3'} - {name: 'InstrumentationEngine_EXTENSION_VERSION', value: 'disabled'} - {name: 'SnapshotDebugger_EXTENSION_VERSION', value: 'disabled'} - {name: 'XDT_MicrosoftApplicationInsights_BaseExtensions', value: 'disabled'} - {name: 'XDT_MicrosoftApplicationInsights_Mode', value: 'recommended'} - {name: 'XDT_MicrosoftApplicationInsights_PreemptSdk', value: 'disabled'} + ...(authenticationType == 'key' + ? [ + { + name: 'AZURE_DOCUMENT_INTELLIGENCE_API_KEY' + value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/document-intelligence-key)' + } + ] + : []) + { name: 'APPINSIGHTS_INSTRUMENTATIONKEY', value: appInsights.properties.InstrumentationKey } + { name: 'APPLICATIONINSIGHTS_CONNECTION_STRING', value: appInsights.properties.ConnectionString } + { name: 'APPINSIGHTS_PROFILERFEATURE_VERSION', value: '1.0.0' } + { name: 'APPINSIGHTS_SNAPSHOTFEATURE_VERSION', value: '1.0.0' } + { name: 'APPLICATIONINSIGHTS_CONFIGURATION_CONTENT', value: '' } + { name: 'ApplicationInsightsAgent_EXTENSION_VERSION', value: '~3' } + { name: 'DiagnosticServices_EXTENSION_VERSION', value: '~3' } + { name: 'InstrumentationEngine_EXTENSION_VERSION', value: 'disabled' } + { name: 'SnapshotDebugger_EXTENSION_VERSION', value: 'disabled' } + { name: 'XDT_MicrosoftApplicationInsights_BaseExtensions', value: 'disabled' } + { name: 'XDT_MicrosoftApplicationInsights_Mode', value: 'recommended' } + { name: 'XDT_MicrosoftApplicationInsights_PreemptSdk', value: 'disabled' } ] } clientAffinityEnabled: false diff --git a/deployers/bicep/modules/azureContainerRegistry.bicep b/deployers/bicep/modules/azureContainerRegistry.bicep index fe99db104..4023447e0 100644 --- a/deployers/bicep/modules/azureContainerRegistry.bicep +++ b/deployers/bicep/modules/azureContainerRegistry.bicep @@ -34,7 +34,7 @@ resource acr 'Microsoft.ContainerRegistry/registries@2025-04-01' = { //========================================================= // store container registry keys in key vault if using key authentication and configure app permissions = true //========================================================= -module containerRegistrySecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions) { +module containerRegistrySecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions) { name: 'storeContainerRegistrySecret' params: { keyVaultName: keyVault diff --git a/deployers/bicep/modules/contentSafety.bicep b/deployers/bicep/modules/contentSafety.bicep index 819200f58..59c40125f 100644 --- a/deployers/bicep/modules/contentSafety.bicep +++ b/deployers/bicep/modules/contentSafety.bicep @@ -33,7 +33,7 @@ resource contentSafety 'Microsoft.CognitiveServices/accounts@2025-06-01' = { } // configure diagnostic settings for content safety -resource contentSafetyDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (enableDiagLogging) { +resource contentSafetyDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (enableDiagLogging) { name: toLower('${contentSafety.name}-diagnostics') scope: contentSafety properties: { @@ -48,7 +48,7 @@ resource contentSafetyDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05 //========================================================= // store contentSafety keys in key vault if using key authentication and configure app permissions = true //========================================================= -module contentSafetySecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions) { +module contentSafetySecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions) { name: 'storeContentSafetySecret' params: { keyVaultName: keyVault diff --git a/deployers/bicep/modules/cosmosDb.bicep b/deployers/bicep/modules/cosmosDb.bicep index 3c80e314d..67ca669a0 100644 --- a/deployers/bicep/modules/cosmosDb.bicep +++ b/deployers/bicep/modules/cosmosDb.bicep @@ -12,7 +12,6 @@ param keyVault string param authenticationType string param configureApplicationPermissions bool - // Import diagnostic settings configurations module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging) { name: 'diagnosticConfigs' @@ -86,7 +85,7 @@ resource cosmosDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-pre //========================================================= // store cosmos db keys in key vault if using key authentication and configure app permissions = true //========================================================= -module storeEnterpriseAppSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions) { +module storeEnterpriseAppSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions) { name: 'storeEnterpriseAppSecret' params: { keyVaultName: keyVault @@ -95,6 +94,5 @@ module storeEnterpriseAppSecret 'keyVault-Secrets.bicep' = if (authenticationTy } } - output cosmosDbName string = cosmosDb.name output cosmosDbUri string = cosmosDb.properties.documentEndpoint diff --git a/deployers/bicep/modules/diagnosticSettings.bicep b/deployers/bicep/modules/diagnosticSettings.bicep index 668f42ffd..4951f861f 100644 --- a/deployers/bicep/modules/diagnosticSettings.bicep +++ b/deployers/bicep/modules/diagnosticSettings.bicep @@ -102,7 +102,7 @@ var webAppLogCategories = [ // Export configurations as outputs so they can be used by other templates output limitedLogCategories array = limitedLogCategories output standardRetentionPolicy object = standardRetentionPolicy -output standardLogCategories array = standardLogCategories +output standardLogCategories array = standardLogCategories output standardMetricsCategories array = standardMetricsCategories output transactionMetricsCategories array = transactionMetricsCategories output webAppLogCategories array = webAppLogCategories diff --git a/deployers/bicep/modules/documentIntelligence.bicep b/deployers/bicep/modules/documentIntelligence.bicep index 28f591c7e..70b343e34 100644 --- a/deployers/bicep/modules/documentIntelligence.bicep +++ b/deployers/bicep/modules/documentIntelligence.bicep @@ -13,7 +13,7 @@ param authenticationType string param configureApplicationPermissions bool // Import diagnostic settings configurations -module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging){ +module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging) { name: 'diagnosticConfigs' } @@ -48,7 +48,7 @@ resource docIntelDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-p //========================================================= // store document intelligence keys in key vault if using key authentication and configure app permissions = true //========================================================= -module documentIntelligenceSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions) { +module documentIntelligenceSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions) { name: 'storeDocumentIntelligenceSecret' params: { keyVaultName: keyVault diff --git a/deployers/bicep/modules/keyVault.bicep b/deployers/bicep/modules/keyVault.bicep index 508d4a967..6b2786def 100644 --- a/deployers/bicep/modules/keyVault.bicep +++ b/deployers/bicep/modules/keyVault.bicep @@ -49,4 +49,3 @@ resource kvDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview output keyVaultId string = kv.id output keyVaultName string = kv.name output keyVaultUri string = kv.properties.vaultUri - diff --git a/deployers/bicep/modules/managedIdentity.bicep b/deployers/bicep/modules/managedIdentity.bicep index 8389cbb2c..01c6234b7 100644 --- a/deployers/bicep/modules/managedIdentity.bicep +++ b/deployers/bicep/modules/managedIdentity.bicep @@ -15,4 +15,3 @@ resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023- output clientId string = managedIdentity.properties.clientId output principalId string = managedIdentity.properties.principalId output resourceId string = managedIdentity.id - diff --git a/deployers/bicep/modules/openAI.bicep b/deployers/bicep/modules/openAI.bicep index e56db352c..9a04b79d6 100644 --- a/deployers/bicep/modules/openAI.bicep +++ b/deployers/bicep/modules/openAI.bicep @@ -81,4 +81,3 @@ module openAISecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' & output openAIName string = openAI.name output openAIResourceGroup string = resourceGroup().name output openAIEndpoint string = openAI.properties.endpoint - diff --git a/deployers/bicep/modules/redisCache.bicep b/deployers/bicep/modules/redisCache.bicep index 3a65a395c..faab2e236 100644 --- a/deployers/bicep/modules/redisCache.bicep +++ b/deployers/bicep/modules/redisCache.bicep @@ -52,7 +52,7 @@ resource redisCacheDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01 //========================================================= // store redis cache keys in key vault if using key authentication and configure app permissions = true //========================================================= -module redisCacheSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions) { +module redisCacheSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions) { name: 'storeRedisCacheSecret' params: { keyVaultName: keyVault diff --git a/deployers/bicep/modules/search.bicep b/deployers/bicep/modules/search.bicep index 586debb27..da59d74cc 100644 --- a/deployers/bicep/modules/search.bicep +++ b/deployers/bicep/modules/search.bicep @@ -50,7 +50,7 @@ resource searchDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-pre //========================================================= // store search Service keys in key vault if using key authentication and configure app permissions = true //========================================================= -module searchServiceSecret 'keyVault-Secrets.bicep' = if (configureApplicationPermissions) { +module searchServiceSecret 'keyVault-Secrets.bicep' = if (configureApplicationPermissions) { name: 'storeSearchServiceSecret' params: { keyVaultName: keyVault diff --git a/deployers/bicep/modules/setPermissions.bicep b/deployers/bicep/modules/setPermissions.bicep index eab70281a..2eb7f7c2b 100644 --- a/deployers/bicep/modules/setPermissions.bicep +++ b/deployers/bicep/modules/setPermissions.bicep @@ -61,7 +61,7 @@ resource videoIndexerService 'Microsoft.VideoIndexer/accounts@2025-04-01' existi resource kvSecretsUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(kv.id, webApp.id, 'kv-secrets-user') scope: kv - properties: { + properties: { // Built-in role definition id for "Key Vault Secrets User" roleDefinitionId: subscriptionResourceId( 'Microsoft.Authorization/roleDefinitions', @@ -180,7 +180,7 @@ resource searchServiceContributorRole 'Microsoft.Authorization/roleAssignments@2 principalId: webApp.identity.principalId principalType: 'ServicePrincipal' } -} +} // grant the managed identity access to content safety as a Cognitive Services User resource contentSafetyUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (contentSafetyName != '' && authenticationType == 'managed_identity') { @@ -194,10 +194,10 @@ resource contentSafetyUserRole 'Microsoft.Authorization/roleAssignments@2022-04- principalId: webApp.identity.principalId principalType: 'ServicePrincipal' } -} +} // grant the video indexer service access to storage account as a Storage Blob Data Contributor -resource videoIndexerStorageBlobDataContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (videoIndexerName != '' ) { +resource videoIndexerStorageBlobDataContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (videoIndexerName != '') { name: guid(storageAccount.id, videoIndexerService.id, 'video-indexer-storage-blob-data-contributor') scope: storageAccount properties: { @@ -209,10 +209,10 @@ resource videoIndexerStorageBlobDataContributorRole 'Microsoft.Authorization/rol principalId: videoIndexerService.identity.principalId principalType: 'ServicePrincipal' } -} +} // grant the video indexer service access to OpenAI service as cognitive services Contributor -resource videoIndexerStorageCogServicesContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (videoIndexerName != '' ) { +resource videoIndexerStorageCogServicesContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (videoIndexerName != '') { name: guid(openAiService.id, videoIndexerService.id, 'video-indexer-cog-services-contributor') scope: openAiService properties: { @@ -224,10 +224,10 @@ resource videoIndexerStorageCogServicesContributorRole 'Microsoft.Authorization/ principalId: videoIndexerService.identity.principalId principalType: 'ServicePrincipal' } -} +} // grant the video indexer service access to OpenAI service as cognitive services user -resource videoIndexerStorageCogServicesUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (videoIndexerName != '' ) { +resource videoIndexerStorageCogServicesUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (videoIndexerName != '') { name: guid(openAiService.id, videoIndexerService.id, 'video-indexer-cog-services-user') scope: openAiService properties: { @@ -239,4 +239,4 @@ resource videoIndexerStorageCogServicesUserRole 'Microsoft.Authorization/roleAss principalId: videoIndexerService.identity.principalId principalType: 'ServicePrincipal' } -} +} diff --git a/deployers/bicep/modules/speechService.bicep b/deployers/bicep/modules/speechService.bicep index 69116ef37..17391ac76 100644 --- a/deployers/bicep/modules/speechService.bicep +++ b/deployers/bicep/modules/speechService.bicep @@ -13,7 +13,7 @@ param authenticationType string param configureApplicationPermissions bool // Import diagnostic settings configurations -module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging){ +module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging) { name: 'diagnosticConfigs' } @@ -51,7 +51,7 @@ resource speechServiceDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05 //========================================================= // store speech Service keys in key vault if using key authentication and configure app permissions = true //========================================================= -module speechServiceSecret 'keyVault-Secrets.bicep' = if ((authenticationType == 'key') && (configureApplicationPermissions) ){ +module speechServiceSecret 'keyVault-Secrets.bicep' = if ((authenticationType == 'key') && (configureApplicationPermissions)) { name: 'storeSpeechServiceSecret' params: { keyVaultName: keyVault diff --git a/deployers/bicep/modules/storageAccount.bicep b/deployers/bicep/modules/storageAccount.bicep index e40401a04..7e10ccae3 100644 --- a/deployers/bicep/modules/storageAccount.bicep +++ b/deployers/bicep/modules/storageAccount.bicep @@ -13,7 +13,7 @@ param authenticationType string param configureApplicationPermissions bool // Import diagnostic settings configurations -module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging){ +module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging) { name: 'diagnosticConfigs' } @@ -85,7 +85,7 @@ resource storageDiagnosticsBlob 'Microsoft.Insights/diagnosticSettings@2021-05-0 //========================================================= // store storage keys in key vault if using key authentication and configure app permissions = true //========================================================= -module storageAccountSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions) { +module storageAccountSecret 'keyVault-Secrets.bicep' = if (authenticationType == 'key' && configureApplicationPermissions) { name: 'storeStorageAccountSecret' params: { keyVaultName: keyVault diff --git a/deployers/bicep/modules/videoIndexer.bicep b/deployers/bicep/modules/videoIndexer.bicep index ecdeac628..8f41544ac 100644 --- a/deployers/bicep/modules/videoIndexer.bicep +++ b/deployers/bicep/modules/videoIndexer.bicep @@ -12,7 +12,7 @@ param storageAccount string param openAiServiceName string // Import diagnostic settings configurations -module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging){ +module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging) { name: 'diagnosticConfigs' } @@ -35,8 +35,8 @@ resource videoIndexerService 'Microsoft.VideoIndexer/accounts@2025-04-01' = { properties: { publicNetworkAccess: 'Enabled' storageServices: { - resourceId: storage.id - } + resourceId: storage.id + } openAiServices: { resourceId: openAiService.id } diff --git a/deployers/bicep/postconfig.py b/deployers/bicep/postconfig.py index 40b12dafa..44406da2e 100644 --- a/deployers/bicep/postconfig.py +++ b/deployers/bicep/postconfig.py @@ -19,7 +19,7 @@ # Read the existing item by ID and partition key item_id = "app_settings" -partition_key = "app_settings" +partition_key = "app_settings" try: item = container.read_item(item=item_id, partition_key=partition_key) print(f"Found existing app_setting document") @@ -31,11 +31,11 @@ } # Get values from environment variables -var_authenticationType = os.getenv("var_authenticationType") +var_authenticationType = os.getenv("var_authenticationType") var_keyVaultUri = os.getenv("var_keyVaultUri") -var_openAIEndpoint=os.getenv("var_openAIEndpoint") -var_openAIResourceGroup=os.getenv("var_openAIResourceGroup") +var_openAIEndpoint = os.getenv("var_openAIEndpoint") +var_openAIResourceGroup = os.getenv("var_openAIResourceGroup") var_subscriptionId = os.getenv("var_subscriptionId") var_rgName = os.getenv("var_rgName") var_openAIGPTModels = os.getenv("var_openAIGPTModels") @@ -43,9 +43,10 @@ var_openAIEmbeddingModels = os.getenv("var_openAIEmbeddingModels") embedding_models_list = json.loads(var_openAIEmbeddingModels) var_blobStorageEndpoint = os.getenv("var_blobStorageEndpoint") -var_contentSafetyEndpoint = os.getenv("var_contentSafetyEndpoint") +var_contentSafetyEndpoint = os.getenv("var_contentSafetyEndpoint") var_searchServiceEndpoint = os.getenv("var_searchServiceEndpoint") -var_documentIntelligenceServiceEndpoint = os.getenv("var_documentIntelligenceServiceEndpoint") +var_documentIntelligenceServiceEndpoint = os.getenv( + "var_documentIntelligenceServiceEndpoint") var_videoIndexerName = os.getenv("var_videoIndexerName") var_videoIndexerLocation = os.getenv("var_deploymentLocation") var_videoIndexerAccountId = os.getenv("var_videoIndexerAccountId") @@ -54,7 +55,8 @@ # Initialize Key Vault client if Key Vault URI is provided if var_keyVaultUri: - keyvault_client = SecretClient(vault_url=var_keyVaultUri, credential=credential) + keyvault_client = SecretClient( + vault_url=var_keyVaultUri, credential=credential) else: keyvault_client = None @@ -133,11 +135,13 @@ item["content_safety_authentication_type"] = var_authenticationType if keyvault_client: try: - contentSafety_key_secret = keyvault_client.get_secret("content-safety-key") + contentSafety_key_secret = keyvault_client.get_secret( + "content-safety-key") item["content_safety_key"] = contentSafety_key_secret.value print("Retrieved contentSafety service key from Key Vault") except Exception as e: - print(f"Warning: Could not retrieve content-safety-key from Key Vault: {e}") + print( + f"Warning: Could not retrieve content-safety-key from Key Vault: {e}") # Safety > Conversation Archiving item["enable_conversation_archiving"] = True @@ -151,18 +155,21 @@ item["azure_ai_search_key"] = search_key_secret.value print("Retrieved search service key from Key Vault") except Exception as e: - print(f"Warning: Could not retrieve search-service-key from Key Vault: {e}") + print( + f"Warning: Could not retrieve search-service-key from Key Vault: {e}") # Search and Extract > Azure Document Intelligence item["azure_document_intelligence_endpoint"] = var_documentIntelligenceServiceEndpoint item["azure_document_intelligence_authentication_type"] = var_authenticationType if keyvault_client: try: - documentIntelligence_key_secret = keyvault_client.get_secret("document-intelligence-key") + documentIntelligence_key_secret = keyvault_client.get_secret( + "document-intelligence-key") item["azure_document_intelligence_key"] = documentIntelligence_key_secret.value print("Retrieved document intelligence service key from Key Vault") except Exception as e: - print(f"Warning: Could not retrieve document-intelligence-key from Key Vault: {e}") + print( + f"Warning: Could not retrieve document-intelligence-key from Key Vault: {e}") # Search and Extract > Multimedia Support # Video Indexer Configuration @@ -185,8 +192,10 @@ item["speech_service_key"] = speech_key_secret.value print("Retrieved speech service key from Key Vault") except Exception as e: - print(f"Warning: Could not retrieve speech-service-key from Key Vault: {e}") + print( + f"Warning: Could not retrieve speech-service-key from Key Vault: {e}") # 5. Upsert the updated items back into Cosmos DB response = container.upsert_item(item) -print(f"Updated item: {response['id']} with enable_external_healthcheck = {response['enable_external_healthcheck']}") +print( + f"Updated item: {response['id']} with enable_external_healthcheck = {response['enable_external_healthcheck']}") From 4b71a125cf745b0cae9ceac2677bca742e1d2b6c Mon Sep 17 00:00:00 2001 From: SteveCInVA Date: Thu, 11 Dec 2025 19:42:45 +0000 Subject: [PATCH 19/22] removed unnecessary content from readme.md --- deployers/bicep/README.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/deployers/bicep/README.md b/deployers/bicep/README.md index ea3984754..492d569ec 100644 --- a/deployers/bicep/README.md +++ b/deployers/bicep/README.md @@ -25,16 +25,6 @@ The folloiwng variables will be used within this document: - *\* - Should be presented in the form *imageName:label* **Example:** *simple-chat:latest* -The following variables may be entered with a blank depending on the response to other parameters: - -If *\* = *true* then the following variables need to be set with applicable values, if *false* a blank is permitted -- *\* - Resource group name for the existing Azure Container Registry. -- *\* - Azure Container Registry name - -if *\* = *true* then the following variables need to be set with applicable values, if *false* a blank is permitted. -- *\* - Resource group name for the existing Azure OpenAI service. -- *\* - Azure OpenAI service name. - ## Deployment Process The below steps cover the process to deploy the Simple Chat application to an Azure Subscription. It is assumed the user has administrative rights to the subscription for deployment. If the user does not also have permissions to create an Application Registration in Entra, a stand-alone script can be provided to an administrator with the correct permissions. From 3c50fa0db88123898aca2a4d7d86cac368dbc7ac Mon Sep 17 00:00:00 2001 From: SteveCInVA Date: Mon, 15 Dec 2025 17:18:35 +0000 Subject: [PATCH 20/22] fix token scope for commericial search service --- application/single_app/config.py | 2 +- application/single_app/route_backend_settings.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/application/single_app/config.py b/application/single_app/config.py index 89ccc7f9e..b04e9cb5c 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -184,7 +184,7 @@ credential_scopes=[resource_manager + "/.default"] cognitive_services_scope = "https://cognitiveservices.azure.com/.default" video_indexer_endpoint = "https://api.videoindexer.ai" - search_resource_manager = "https://search.windows.net" + search_resource_manager = "https://search.azure.com" KEY_VAULT_DOMAIN = ".vault.azure.net" def get_redis_cache_infrastructure_endpoint(redis_hostname: str) -> str: diff --git a/application/single_app/route_backend_settings.py b/application/single_app/route_backend_settings.py index 54914e9e7..8ef377dd4 100644 --- a/application/single_app/route_backend_settings.py +++ b/application/single_app/route_backend_settings.py @@ -684,8 +684,7 @@ def _test_azure_ai_search_connection(payload): url = f"{endpoint.rstrip('/')}/indexes?api-version=2023-11-01" if direct_data.get('auth_type') == 'managed_identity': - if AZURE_ENVIRONMENT in ("usgovernment", "custom"): # change credential scopes for US Gov or custom environments - credential_scopes=search_resource_manager + "/.default" + credential_scopes=search_resource_manager + "/.default" arm_scope = credential_scopes credential = DefaultAzureCredential() arm_token = credential.get_token(arm_scope).token From a54c2b5239b90d63e78205e7fd19477d4e116c0a Mon Sep 17 00:00:00 2001 From: SteveCInVA Date: Mon, 15 Dec 2025 20:15:23 +0000 Subject: [PATCH 21/22] set permission correctly for lookup of openAI models --- deployers/bicep/main.bicep | 9 +++++++-- deployers/bicep/modules/setPermissions.bicep | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/deployers/bicep/main.bicep b/deployers/bicep/main.bicep index 9e4e54e42..86e0cfaa4 100644 --- a/deployers/bicep/main.bicep +++ b/deployers/bicep/main.bicep @@ -39,10 +39,13 @@ param azdEnvironmentName string param imageName string @description('''Azure AD Application Client ID for enterprise authentication. -- Required if enableEnterpriseApp is true - Should be the client ID of the registered Azure AD application''') param enterpriseAppClientId string +@description('''Azure AD Application Service Principal Id for the enterprise application. +- Should be the Service Principal ID of the registered Azure AD application''') +param enterpriseAppServicePrincipalId string + @description('''Azure AD Application Client Secret for enterprise authentication. - Required if enableEnterpriseApp is true - Should be created in Azure AD App Registration and passed via environment variable @@ -459,9 +462,11 @@ module setPermissions 'modules/setPermissions.bicep' = if (configureApplicationP name: 'setPermissions' scope: rg params: { + webAppName: appService.outputs.name - keyVaultName: keyVault.outputs.keyVaultName authenticationType: authenticationType + enterpriseAppServicePrincipalId: enterpriseAppServicePrincipalId + keyVaultName: keyVault.outputs.keyVaultName cosmosDBName: cosmosDB.outputs.cosmosDbName acrName: acr.outputs.acrName openAIName: openAI.outputs.openAIName diff --git a/deployers/bicep/modules/setPermissions.bicep b/deployers/bicep/modules/setPermissions.bicep index 2eb7f7c2b..ca7a59d44 100644 --- a/deployers/bicep/modules/setPermissions.bicep +++ b/deployers/bicep/modules/setPermissions.bicep @@ -3,6 +3,7 @@ targetScope = 'resourceGroup' param webAppName string param authenticationType string param keyVaultName string +param enterpriseAppServicePrincipalId string param cosmosDBName string param acrName string param openAIName string @@ -126,6 +127,20 @@ resource openAIUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = i } } +// Grant the enterprise application access to the cognitive services openai user +resource openAIenterpriseAppUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'managed_identity') { + scope: openAiService + name: guid(openAiService.id, webApp.id, 'enterpriseApp-CognitiveServicesOpenAIUserRole') + properties: { + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' + ) + principalId: enterpriseAppServicePrincipalId + principalType: 'ServicePrincipal' + } +} + // grant the managed identity access to document intelligence as a Cognitive Services User resource docIntelUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'managed_identity') { name: guid(docIntelService.id, webApp.id, 'doc-intel-user') From bab4f22286e30c3822c2af4ba015b4a90c495206 Mon Sep 17 00:00:00 2001 From: SteveCInVA Date: Mon, 15 Dec 2025 22:11:55 +0000 Subject: [PATCH 22/22] fixes required to configure search with managed identity --- deployers/bicep/modules/search.bicep | 4 ++++ deployers/bicep/modules/setPermissions.bicep | 16 +++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/deployers/bicep/modules/search.bicep b/deployers/bicep/modules/search.bicep index da59d74cc..2786b91e2 100644 --- a/deployers/bicep/modules/search.bicep +++ b/deployers/bicep/modules/search.bicep @@ -30,6 +30,10 @@ resource searchService 'Microsoft.Search/searchServices@2025-05-01' = { publicNetworkAccess: 'Enabled' replicaCount: 1 partitionCount: 1 + authOptions: { + aadOrApiKey: {aadAuthFailureMode: 'http403' } + } + disableLocalAuth: false } tags: tags } diff --git a/deployers/bicep/modules/setPermissions.bicep b/deployers/bicep/modules/setPermissions.bicep index ca7a59d44..33564d8ed 100644 --- a/deployers/bicep/modules/setPermissions.bicep +++ b/deployers/bicep/modules/setPermissions.bicep @@ -183,6 +183,20 @@ resource speechServiceUserRole 'Microsoft.Authorization/roleAssignments@2022-04- } } +// grant the managed identity access to search service as a Search Service Contributor +resource searchIndexDataContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'managed_identity') { + name: guid(searchService.id, webApp.id, 'search-index-data-contributor') + scope: searchService + properties: { + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '8ebe5a00-799e-43f5-93ac-243d3dce84a7' + ) + principalId: webApp.identity.principalId + principalType: 'ServicePrincipal' + } +} + // grant the managed identity access to search service as a Search Service Contributor resource searchServiceContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'managed_identity') { name: guid(searchService.id, webApp.id, 'search-service-contributor') @@ -190,7 +204,7 @@ resource searchServiceContributorRole 'Microsoft.Authorization/roleAssignments@2 properties: { roleDefinitionId: subscriptionResourceId( 'Microsoft.Authorization/roleDefinitions', - '8ebe5a00-799e-43f5-93ac-243d3dce84a7' + '7ca78c08-252a-4471-8644-bb5ff32d4ba0' ) principalId: webApp.identity.principalId principalType: 'ServicePrincipal'