diff --git a/application/single_app/Dockerfile b/application/single_app/Dockerfile index 8178f898..56c8c145 100644 --- a/application/single_app/Dockerfile +++ b/application/single_app/Dockerfile @@ -1,5 +1,6 @@ # Builder stage: install dependencies in a virtualenv -FROM cgr.dev/chainguard/python:latest-dev AS builder +# FROM cgr.dev/chainguard/python:latest-dev AS builder +FROM python:3.13-slim AS builder USER root @@ -12,14 +13,19 @@ WORKDIR /app 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 +#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" RUN pip install --no-cache-dir -r requirements.txt -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 @@ -40,4 +46,4 @@ EXPOSE 5000 USER nonroot:nonroot -ENTRYPOINT [ "python", "/app/app.py" ] +ENTRYPOINT [ "python", "/app/app.py" ] \ No newline at end of file diff --git a/application/single_app/config.py b/application/single_app/config.py index 16156c37..b04e9cb5 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.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 54914e9e..8ef377dd 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 diff --git a/deployers/Initialize-EntraApplication.ps1 b/deployers/Initialize-EntraApplication.ps1 index 13b5ae8f..5a411be0 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)] diff --git a/deployers/azure.yaml b/deployers/azure.yaml index ae9a6840..90a40cc9 100644 --- a/deployers/azure.yaml +++ b/deployers/azure.yaml @@ -5,6 +5,48 @@ infra: provider: bicep path: bicep hooks: + postprovision: + posix: + 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_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_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 + 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 + 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 + predeploy: posix: shell: sh diff --git a/deployers/bicep/README.md b/deployers/bicep/README.md index 7a76363b..492d569e 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. @@ -113,26 +103,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 +125,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" diff --git a/deployers/bicep/README_orig.md b/deployers/bicep/README_orig.md deleted file mode 100644 index 4ded5491..00000000 --- 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/cosmosDb-postDeployPerms.sh b/deployers/bicep/cosmosDb-postDeployPerms.sh new file mode 100644 index 00000000..c4d3c311 --- /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 a65d0d4c..86e0cfaa 100644 --- a/deployers/bicep/main.bicep +++ b/deployers/bicep/main.bicep @@ -29,29 +29,23 @@ 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 -- Default is true''') -param enableEnterpriseApp bool = true +@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 - 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 @@ -59,19 +53,62 @@ 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('''Configure permissions (based on authenticationType) for the deployed web application to access required resources. +''') +param configureApplicationPermissions bool + +@description('Optional object containing additional tags to apply to all resources.') +param specialTags object = {} -@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('''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('''The name of the Azure Container Registry resource group. -- Required if useExistingAcr is true''') -param existingAcrResourceGroup string +@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''') @@ -85,43 +122,19 @@ param deployRedisCache bool - 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('''Enable deployment of Azure Video Indexer service and related resources. +- Default is false''') +param deployVideoIndexerService 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}' //========================================================= @@ -134,10 +147,10 @@ resource rg 'Microsoft.Resources/resourceGroups@2022-09-01' = { } //========================================================= -// Create managed identity +// Create log analytics workspace //========================================================= -module managedIdentity 'modules/managedIdentity.bicep' = { - name: 'managedIdentity' +module logAnalytics 'modules/logAnalyticsWorkspace.bicep' = { + name: 'logAnalytics' scope: rg params: { location: location @@ -148,16 +161,17 @@ module managedIdentity 'modules/managedIdentity.bicep' = { } //========================================================= -// Create log analytics workspace +// Create application insights //========================================================= -module logAnalytics 'modules/logAnalytics.bicep' = { - name: 'logAnalytics' +module applicationInsights 'modules/applicationInsights.bicep' = { + name: 'applicationInsights' scope: rg params: { location: location appName: appName environment: environment tags: tags + logAnalyticsId: logAnalytics.outputs.logAnalyticsId } } @@ -172,46 +186,21 @@ 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 - enableEnterpriseApp: enableEnterpriseApp - enterpriseAppClientId: enterpriseAppClientId - enterpriseAppClientSecret: enterpriseAppClientSecret - } -} - -//========================================================= -// Create application insights -//========================================================= -module appInsights 'modules/appInsights.bicep' = { - name: 'appInsights' - scope: rg - params: { - location: location - appName: appName - environment: environment - tags: tags - 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 } } @@ -226,55 +215,31 @@ 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 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 } } @@ -289,72 +254,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 } } @@ -385,10 +353,7 @@ 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 enableDiagLogging: enableDiagLogging logAnalyticsId: logAnalytics.outputs.logAnalyticsId appServicePlanId: appServicePlan.outputs.appServicePlanId @@ -396,52 +361,58 @@ 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 - Redis Cache //========================================================= -module appServiceAuth 'modules/appServiceAuthentication.bicep' = if (enableEnterpriseApp && !empty(enterpriseAppClientId)) { - name: 'appServiceAuthentication' +module redisCache 'modules/redisCache.bicep' = if (deployRedisCache) { + name: 'redisCache' 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 } } @@ -456,25 +427,49 @@ module speechService 'modules/speechService.bicep' = if (deploySpeechService) { 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 +// Create Optional Resource - Video Indexer Service //========================================================= -module enterpriseAppPermissions 'modules/enterpriseAppPermissions.bicep' = if (enableEnterpriseApp) { - name: 'enterpriseAppPermissions' +module videoIndexerService 'modules/videoIndexer.bicep' = if (deployVideoIndexerService) { + name: 'videoIndexerService' scope: rg params: { + location: location + appName: appName + environment: environment + tags: tags + enableDiagLogging: enableDiagLogging + logAnalyticsId: logAnalytics.outputs.logAnalyticsId + + storageAccount: storageAccount.outputs.name + openAiServiceName: openAI.outputs.openAIName + } +} + +//========================================================= +// configure permissions for managed identity to access resources +//========================================================= +module setPermissions 'modules/setPermissions.bicep' = if (configureApplicationPermissions) { + name: 'setPermissions' + scope: rg + params: { + webAppName: appService.outputs.name + authenticationType: authenticationType + enterpriseAppServicePrincipalId: enterpriseAppServicePrincipalId keyVaultName: keyVault.outputs.keyVaultName 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 docIntelName: docIntel.outputs.documentIntelligenceServiceName storageAccountName: storageAccount.outputs.name #disable-next-line BCP318 // expect one value to be null @@ -482,18 +477,47 @@ module enterpriseAppPermissions 'modules/enterpriseAppPermissions.bicep' = if (e 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 : '' } } - //========================================================= -// Outputs for deployment of container image +// output values //========================================================= - +// output required for both predeploy and postprovision scripts in azure.yaml output var_rgName string = rgName -output var_acrName string = useExistingAcr ? existingAcrResourceName : toLower('${appName}${environment}acr') -output var_containerRegistry string = containerRegistry -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 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 values required for postprovision script in azure.yaml +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 +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 + ? videoIndexerService.outputs.videoIndexerServiceName + : '' +output var_videoIndexerAccountId string = deployVideoIndexerService +#disable-next-line BCP318 // expect one value to be null + ? 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 61a30105..e819bca9 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}" @@ -26,20 +26,11 @@ "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 3c60ee04..b972ee3a 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 diff --git a/deployers/bicep/modules/appService.bicep b/deployers/bicep/modules/appService.bicep index b3af02ab..4f70f03a 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) { @@ -40,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 } @@ -53,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 @@ -71,57 +65,93 @@ 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' 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: 'managed_identity'} - - //{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: '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_DOCUMENT_INTELLIGENCE_ENDPOINT', value: documentIntelligence.properties.endpoint} - {name: 'AZURE_DOCUMENT_INTELLIGENCE_API_KEY', value: documentIntelligence.listKeys().key1} - {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'} + { 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}' } + + // 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 } + // 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)' + } + ] + : []) + { 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 httpsOnly: true } identity: { - type: 'SystemAssigned, UserAssigned' - userAssignedIdentities: { - '${managedIdentityId}': {} - } + type: 'SystemAssigned' } tags: union(tags, { 'azd-service-name': 'web' }) } @@ -141,8 +171,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 +184,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 0a484a03..00000000 --- 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 a9c76808..00000000 --- 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 2065d6dd..00000000 --- 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 41f3b64b..4023447e 100644 --- a/deployers/bicep/modules/azureContainerRegistry.bicep +++ b/deployers/bicep/modules/azureContainerRegistry.bicep @@ -4,11 +4,13 @@ param location string param acrName 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' @@ -29,17 +31,15 @@ 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' +//========================================================= +// 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 040af5fd..59c40125 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' @@ -25,28 +27,13 @@ resource contentSafety 'Microsoft.CognitiveServices/accounts@2025-06-01' = { } properties: { publicNetworkAccess: 'Enabled' - disableLocalAuth: false customSubDomainName: toLower('${appName}-${environment}-contentsafety') } 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) { +resource contentSafetyDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (enableDiagLogging) { name: toLower('${contentSafety.name}-diagnostics') scope: contentSafety properties: { @@ -58,4 +45,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 67af0d4f..67ca669a 100644 --- a/deployers/bicep/modules/cosmosDb.bicep +++ b/deployers/bicep/modules/cosmosDb.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' @@ -22,7 +24,6 @@ resource cosmosDb 'Microsoft.DocumentDB/databaseAccounts@2023-04-15' = { kind: 'GlobalDocumentDB' properties: { databaseAccountOfferType: 'Standard' - disableLocalAuth: true capabilities: [ { name: 'EnableServerless' @@ -69,32 +70,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,4 +82,17 @@ 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 7b24aedf..00000000 --- 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/diagnosticSettings.bicep b/deployers/bicep/modules/diagnosticSettings.bicep index 08003eac..4951f861 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 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/documentIntelligence.bicep b/deployers/bicep/modules/documentIntelligence.bicep index e11bb707..70b343e3 100644 --- a/deployers/bicep/modules/documentIntelligence.bicep +++ b/deployers/bicep/modules/documentIntelligence.bicep @@ -5,13 +5,15 @@ 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){ +module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging) { name: 'diagnosticConfigs' } @@ -25,26 +27,11 @@ resource docIntel 'Microsoft.CognitiveServices/accounts@2025-06-01' = { } properties: { publicNetworkAccess: 'Enabled' - disableLocalAuth: false customSubDomainName: toLower('${appName}-${environment}-docintel') } 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,5 +45,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/enterpriseApplication.bicep b/deployers/bicep/modules/enterpriseApplication.bicep deleted file mode 100644 index 30077aec..00000000 --- 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 00000000..acd09328 --- /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 194ca583..6b2786de 100644 --- a/deployers/bicep/modules/keyVault.bicep +++ b/deployers/bicep/modules/keyVault.bicep @@ -5,21 +5,9 @@ param appName string param environment string param tags object -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,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') @@ -73,17 +46,6 @@ 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 be883e74..01c6234b 100644 --- a/deployers/bicep/modules/managedIdentity.bicep +++ b/deployers/bicep/modules/managedIdentity.bicep @@ -12,6 +12,6 @@ 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 28cb92a4..00000000 --- a/deployers/bicep/modules/openAI-existing.bicep +++ /dev/null @@ -1,40 +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 - - - diff --git a/deployers/bicep/modules/openAI.bicep b/deployers/bicep/modules/openAI.bicep index d31337ad..9a04b79d 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,23 +29,19 @@ resource newOpenAI 'Microsoft.CognitiveServices/accounts@2024-10-01' = { name: 'S0' } identity: { - type: 'SystemAssigned, UserAssigned' - userAssignedIdentities: { - '${managedIdentityId}': {} - } + type: 'SystemAssigned' } properties: { publicNetworkAccess: 'Enabled' - disableLocalAuth: false customSubDomainName: toLower('${appName}-${environment}-openai') } tags: tags } // 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,35 +51,33 @@ 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 = openAI.properties.endpoint diff --git a/deployers/bicep/modules/redisCache.bicep b/deployers/bicep/modules/redisCache.bicep index 796a2c1a..faab2e23 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 5ec5acec..2786b91e 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,28 +25,19 @@ resource searchService 'Microsoft.Search/searchServices@2025-05-01' = { name: 'basic' } properties: { + #disable-next-line BCP036 // template is incorrect hostingMode: 'default' publicNetworkAccess: 'Enabled' replicaCount: 1 partitionCount: 1 + authOptions: { + aadOrApiKey: {aadAuthFailureMode: 'http403' } + } + disableLocalAuth: false } 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,4 +51,18 @@ 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) { + name: 'storeSearchServiceSecret' + params: { + keyVaultName: keyVault + secretName: 'search-service-key' + secretValue: searchService.listAdminKeys().primaryKey + } +} + output searchServiceName string = searchService.name +output searchServiceEndpoint string = searchService.properties.endpoint +output searchServiceAuthencationType string = authenticationType diff --git a/deployers/bicep/modules/enterpriseAppPermissions.bicep b/deployers/bicep/modules/setPermissions.bicep similarity index 54% rename from deployers/bicep/modules/enterpriseAppPermissions.bicep rename to deployers/bicep/modules/setPermissions.bicep index 07943cc7..33564d8e 100644 --- a/deployers/bicep/modules/enterpriseAppPermissions.bicep +++ b/deployers/bicep/modules/setPermissions.bicep @@ -1,14 +1,18 @@ targetScope = 'resourceGroup' param webAppName string +param authenticationType string param keyVaultName string +param enterpriseAppServicePrincipalId string param cosmosDBName string +param acrName string param openAIName string param docIntelName string 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 @@ -22,7 +26,11 @@ resource cosmosDb 'Microsoft.DocumentDB/databaseAccounts@2023-04-15' existing = name: cosmosDBName } -resource openAiService 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = if (openAIName != '') { +resource acr 'Microsoft.ContainerRegistry/registries@2025-04-01' existing = { + name: acrName +} + +resource openAiService 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = { name: openAIName } @@ -34,11 +42,6 @@ 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 } @@ -51,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') @@ -67,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' = { +resource cosmosContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'managed_identity') { name: guid(cosmosDb.id, webApp.id, 'cosmos-contributor') scope: cosmosDb properties: { @@ -81,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' = { +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: { @@ -92,10 +99,24 @@ resource cosmosDataContributorRole 'Microsoft.DocumentDB/databaseAccounts/sqlRol } } +// 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 (openAIName != '') { - name: guid(openAiService.id, webApp.id, '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', @@ -106,8 +127,22 @@ 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 (docIntelName != '') { +resource docIntelUserRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (authenticationType == 'managed_identity') { name: guid(docIntelService.id, webApp.id, 'doc-intel-user') scope: docIntelService properties: { @@ -121,7 +156,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' = { +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: { @@ -135,7 +170,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 != '') { +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: { @@ -149,8 +184,8 @@ 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' = { - name: guid(searchService.id, webApp.id, '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( @@ -160,10 +195,24 @@ resource searchServiceContributorRole 'Microsoft.Authorization/roleAssignments@2 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', + '7ca78c08-252a-4471-8644-bb5ff32d4ba0' + ) + 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 != '') { +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: { @@ -174,4 +223,49 @@ 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 != '') { + 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/speechService.bicep b/deployers/bicep/modules/speechService.bicep index e5c70389..17391ac7 100644 --- a/deployers/bicep/modules/speechService.bicep +++ b/deployers/bicep/modules/speechService.bicep @@ -5,13 +5,15 @@ 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){ +module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging) { name: 'diagnosticConfigs' } @@ -24,33 +26,15 @@ resource speechService 'Microsoft.CognitiveServices/accounts@2024-10-01' = { name: 'S0' } identity: { - type: 'SystemAssigned, UserAssigned' - userAssignedIdentities: { - '${managedIdentityId}': {} - } + type: 'SystemAssigned' } properties: { publicNetworkAccess: 'Enabled' - disableLocalAuth: false customSubDomainName: toLower('${appName}-${environment}-speech') } 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 +48,18 @@ 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 +output speechServiceAuthenticationType string = authenticationType diff --git a/deployers/bicep/modules/storageAccount.bicep b/deployers/bicep/modules/storageAccount.bicep index 436957f54..7e10ccae 100644 --- a/deployers/bicep/modules/storageAccount.bicep +++ b/deployers/bicep/modules/storageAccount.bicep @@ -5,13 +5,15 @@ 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){ +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,4 +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/modules/videoIndexer.bicep b/deployers/bicep/modules/videoIndexer.bicep new file mode 100644 index 00000000..8f41544a --- /dev/null +++ b/deployers/bicep/modules/videoIndexer.bicep @@ -0,0 +1,63 @@ +targetScope = 'resourceGroup' + +param location string +param appName string +param environment string +param tags object + +param enableDiagLogging bool +param logAnalyticsId string + +param storageAccount string +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 video indexer service if required +resource videoIndexerService 'Microsoft.VideoIndexer/accounts@2025-04-01' = { + name: toLower('${appName}-${environment}-video') + location: location + + identity: { + type: 'SystemAssigned' + } + properties: { + publicNetworkAccess: 'Enabled' + storageServices: { + resourceId: storage.id + } + openAiServices: { + resourceId: openAiService.id + } + } + tags: tags + dependsOn: [ + storage + openAiService + ] +} + +// 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.limitedLogCategories + } +} + +output videoIndexerServiceName string = videoIndexerService.name +output videoIndexerAccountId string = videoIndexerService.properties.accountId diff --git a/deployers/bicep/postconfig.py b/deployers/bicep/postconfig.py new file mode 100644 index 00000000..44406da2 --- /dev/null +++ b/deployers/bicep/postconfig.py @@ -0,0 +1,201 @@ +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") + +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) + +# Read the existing item by ID and partition key +item_id = "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") +except CosmosResourceNotFoundError: + print(f"app_setting document not found.") + item = { + "id": item_id, + "partition_key": partition_key + } + +# Get values from environment variables +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_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 + +# General > Health Check +item["enable_external_healthcheck"] = True + +# AI Models +item["azure_openai_gpt_endpoint"] = var_openAIEndpoint +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": gpt_models_list[0]["modelName"], + "modelName": gpt_models_list[0]["modelName"] + } + ], + "all": [ + { + "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"] = var_authenticationType +item["azure_openai_embedding_subscription_id"] = var_subscriptionId +item["azure_openai_embedding_resource_group"] = var_openAIResourceGroup +item["embedding_model"] = { + "selected": [ + { + "deploymentName": embedding_models_list[0]["modelName"], + "modelName": embedding_models_list[0]["modelName"] + } + ], + "all": [ + { + "deploymentName": model["modelName"], + "modelName": model["modelName"] + } + for model in embedding_models_list + ] +} + +# Agents and Actions > Agents Configuration +item["enable_semantic_kernel"] = False + +# 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"] = gpt_models_list[0]["modelName"] + +# Workspaces > Multimodal Vision Analysis +item["enable_multimodal_vision"] = True +item["multimodal_vision_model"] = gpt_models_list[0]["modelName"] + +# Citations > Enhanced Citations +item["enable_enhanced_citations"] = True +item["office_docs_authentication_type"] = var_authenticationType +item["office_docs_storage_account_blob_endpoint"] = var_blobStorageEndpoint + +# 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"] = 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"] = 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"] = 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) +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 00000000..e6e1fd60 --- /dev/null +++ b/deployers/bicep/requirements.txt @@ -0,0 +1,4 @@ +# requirements.txt +azure-identity>=1.15.0 +azure-cosmos>=4.5.0 +azure-keyvault-secrets>=4.4.0 \ No newline at end of file