Skip to content
4 changes: 3 additions & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@
CUSTOM_COGNITIVE_SERVICES_URL_VALUE = os.getenv("CUSTOM_COGNITIVE_SERVICES_URL_VALUE", "")
CUSTOM_SEARCH_RESOURCE_MANAGER_URL_VALUE = os.getenv("CUSTOM_SEARCH_RESOURCE_MANAGER_URL_VALUE", "")
CUSTOM_REDIS_CACHE_INFRASTRUCTURE_URL_VALUE = os.getenv("CUSTOM_REDIS_CACHE_INFRASTRUCTURE_URL_VALUE", "")

CUSTOM_OIDC_METADATA_URL_VALUE = os.getenv("CUSTOM_OIDC_METADATA_URL_VALUE", "")

# Azure AD Configuration
CLIENT_ID = os.getenv("CLIENT_ID")
Expand Down Expand Up @@ -174,12 +174,14 @@
KEY_VAULT_DOMAIN = ".vault.usgovcloudapi.net"

elif AZURE_ENVIRONMENT == "custom":
OIDC_METADATA_URL = CUSTOM_OIDC_METADATA_URL_VALUE
resource_manager = CUSTOM_RESOURCE_MANAGER_URL_VALUE
authority = CUSTOM_IDENTITY_URL_VALUE
credential_scopes=[resource_manager + "/.default"]
cognitive_services_scope = CUSTOM_COGNITIVE_SERVICES_URL_VALUE
search_resource_manager = CUSTOM_SEARCH_RESOURCE_MANAGER_URL_VALUE
KEY_VAULT_DOMAIN = os.getenv("KEY_VAULT_DOMAIN", ".vault.azure.net")
video_indexer_endpoint = os.getenv("VIDEO_INDEXER_ENDPOINT", "https://api.videoindexer.ai")
else:
OIDC_METADATA_URL = f"https://login.microsoftonline.com/{TENANT_ID}/v2.0/.well-known/openid-configuration"
resource_manager = "https://management.azure.com"
Expand Down
2 changes: 1 addition & 1 deletion application/single_app/route_backend_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def api_user_search():
if AZURE_ENVIRONMENT == "usgovernment":
user_endpoint = "https://graph.microsoft.us/v1.0/users"
elif AZURE_ENVIRONMENT == "custom":
user_endpoint = CUSTOM_GRAPH_URL_VALUE
user_endpoint = f"{CUSTOM_GRAPH_URL_VALUE}/v1.0/users"
else:
user_endpoint = "https://graph.microsoft.com/v1.0/users"

Expand Down
38 changes: 27 additions & 11 deletions deployers/bicep/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@ targetScope = 'subscription'
- Region must align to the target cloud environment''')
param location string

@description('''The target Azure Cloud environment.
- Accepted values are: AzureCloud, AzureUSGovernment
- Default is AzureCloud''')
@allowed([
'AzureCloud'
'AzureUSGovernment'
'public'
'usgovernment'
'custom'
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is trailing whitespace at the end of this line after 'custom'. While this doesn't affect functionality, it violates best practices for clean code formatting.

Suggested change
'custom'
'custom'

Copilot uses AI. Check for mistakes.
])
param cloudEnvironment string
param cloudEnvironment string = az.environment().name == 'AzureCloud' ? 'public' : (az.environment().name == 'AzureUSGovernment' ? 'usgovernment' : 'custom')
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default value logic for cloudEnvironment uses nested ternary expressions that check az.environment().name for 'AzureCloud' and 'AzureUSGovernment', mapping them to 'public' and 'usgovernment' respectively, with 'custom' as the fallback. However, if az.environment().name returns other known Azure cloud names like 'AzureChinaCloud' or 'AzureGermanCloud', they would incorrectly be mapped to 'custom' instead of having their own handling. Consider explicitly handling all known Azure cloud types or documenting that only these three cloud types are supported.

Copilot uses AI. Check for mistakes.

@description('''The name of the application to be deployed.
- Name may only contain letters and numbers
Expand Down Expand Up @@ -143,13 +141,27 @@ param deploySpeechService bool
- Default is false''')
param deployVideoIndexerService bool

// --- Custom Azure Environment Parameters (for 'custom' azureEnvironment) ---
@description('Custom blob storage URL suffix, e.g. blob.core.usgovcloudapi.net')
param customBlobStorageSuffix string = 'blob.${az.environment().suffixes.storage}'
@description('Custom Graph API URL, e.g. https://graph.microsoft.us')
param customGraphUrl string = az.environment().graph
@description('Custom Identity URL, e.g. https://login.microsoftonline.us')
param customIdentityUrl string = az.environment().authentication.loginEndpoint
@description('Custom Resource Manager URL, e.g. https://management.usgovcloudapi.net')
param customResourceManagerUrl string = az.environment().resourceManager
@description('Custom Cognitive Services scope ex: https://cognitiveservices.azure.com/.default')
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description for this parameter uses "ex:" instead of the more standard "e.g." or "for example". For consistency with other parameter descriptions in the file (which use "e.g."), consider updating this to match.

Suggested change
@description('Custom Cognitive Services scope ex: https://cognitiveservices.azure.com/.default')
@description('Custom Cognitive Services scope e.g. https://cognitiveservices.azure.com/.default')

Copilot uses AI. Check for mistakes.
param customCognitiveServicesScope string = 'https://cognitiveservices.azure.com/.default'
Comment on lines +153 to +154
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default value for customCognitiveServicesScope is hardcoded to 'https://cognitiveservices.azure.com/.default', which is the public Azure cloud endpoint. This default will be incorrect for users deploying to AzureUSGovernment or other custom clouds, as they use different cognitive services endpoints (e.g., 'https://cognitiveservices.azure.us/.default' for US Government). Consider making this value conditional based on the cloud environment or documenting that users must override this parameter when deploying to non-public clouds.

Suggested change
@description('Custom Cognitive Services scope ex: https://cognitiveservices.azure.com/.default')
param customCognitiveServicesScope string = 'https://cognitiveservices.azure.com/.default'
@description('Custom Cognitive Services scope, e.g. https://cognitiveservices.azure.com/.default (public), https://cognitiveservices.azure.us/.default (US Gov)')
param customCognitiveServicesScope string = az.environment().name == 'AzureUSGovernment' ? 'https://cognitiveservices.azure.us/.default' : 'https://cognitiveservices.azure.com/.default'

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default value for customCognitiveServicesScope uses the public Azure endpoint ('https://cognitiveservices.azure.com/.default'), but this may be incorrect for government or other custom clouds. When az.environment() detects a non-public cloud but cloudEnvironment is set to 'custom', this will use the wrong cognitive services scope. Consider providing environment-specific defaults based on az.environment().

Suggested change
param customCognitiveServicesScope string = 'https://cognitiveservices.azure.com/.default'
param customCognitiveServicesScope string = az.environment().name == 'AzureUSGovernment' ? 'https://cognitiveservices.azure.us/.default' : 'https://cognitiveservices.azure.com/.default'

Copilot uses AI. Check for mistakes.
@description('Custom search resource URL for token audience, e.g. https://search.azure.us')
param customSearchResourceUrl string = 'https://search.azure.com'
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default value for customSearchResourceUrl is hardcoded to 'https://search.azure.com', which is the public Azure cloud endpoint. This default will be incorrect for users deploying to AzureUSGovernment or other custom clouds, as they use different search endpoints (e.g., 'https://search.azure.us' for US Government). Consider making this value conditional based on the cloud environment or documenting that users must override this parameter when deploying to non-public clouds.

Suggested change
param customSearchResourceUrl string = 'https://search.azure.com'
param customSearchResourceUrl string = cloudEnvironment == 'usgovernment'
? 'https://search.azure.us'
: (cloudEnvironment == 'public' ? 'https://search.azure.com' : '')

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default value for customSearchResourceUrl uses the public Azure endpoint ('https://search.azure.com'), but this may be incorrect for government or other custom clouds. When az.environment() detects a non-public cloud but cloudEnvironment is set to 'custom', this will use the wrong search endpoint. Consider using az.environment().suffixes or providing environment-specific defaults.

Suggested change
param customSearchResourceUrl string = 'https://search.azure.com'
param customSearchResourceUrl string = 'https://search.${az.environment().suffixes.search}'

Copilot uses AI. Check for mistakes.

//=========================================================
// 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 acrCloudSuffix = az.environment().suffixes.acrLoginServer
var acrName = toLower('${appName}${environment}acr')
var containerRegistry = '${acrName}${acrCloudSuffix}'
var containerImageName = '${containerRegistry}/${imageName}'
Expand Down Expand Up @@ -435,6 +447,13 @@ module appService 'modules/appService.bicep' = {
enterpriseAppClientSecret: enterpriseAppClientSecret
authenticationType: authenticationType
keyVaultUri: keyVault.outputs.keyVaultUri
// --- Custom Azure Environment Parameters (for 'custom' azureEnvironment) ---
customBlobStorageSuffix: customBlobStorageSuffix
customGraphUrl: customGraphUrl
customIdentityUrl: customIdentityUrl
customResourceManagerUrl: customResourceManagerUrl
customCognitiveServicesScope: customCognitiveServicesScope
customSearchResourceUrl: customSearchResourceUrl

enablePrivateNetworking: enablePrivateNetworking
#disable-next-line BCP318 // expect one value to be null if private networking is disabled
Expand Down Expand Up @@ -540,7 +559,6 @@ module setPermissions 'modules/setPermissions.bicep' = if (configureApplicationP
name: 'setPermissions'
scope: rg
params: {

webAppName: appService.outputs.name
authenticationType: authenticationType
enterpriseAppServicePrincipalId: enterpriseAppServicePrincipalId
Expand Down Expand Up @@ -603,8 +621,6 @@ module privateNetworking 'modules/privateNetworking.bicep' = if (enablePrivateNe
//=========================================================
// output values
//=========================================================


// output values required for postprovision script in azure.yaml
output var_acrName string = toLower('${appName}${environment}acr')
output var_authenticationType string = toLower(authenticationType)
Expand Down Expand Up @@ -637,7 +653,7 @@ output var_videoIndexerName string = deployVideoIndexerService ? videoIndexerSer
// output values required for predeploy script in azure.yaml
output var_containerRegistry string = containerRegistry
output var_imageName string = contains(imageName, ':') ? split(imageName, ':')[0] : imageName
output var_imageTag string = split(imageName, ':')[1]
output var_imageTag string = contains(imageName, ':') ? split(imageName, ':')[1] : 'latest'
output var_webService string = appService.outputs.name

// output values required for postup script in azure.yaml
Expand Down
40 changes: 33 additions & 7 deletions deployers/bicep/modules/appService.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,25 @@ param keyVaultUri string
param enablePrivateNetworking bool
param appServiceSubnetId string = ''

// --- Custom Azure Environment Parameters (for 'custom' azureEnvironment) ---
@description('Custom blob storage URL suffix, e.g. blob.core.usgovcloudapi.net')
param customBlobStorageSuffix string?
@description('Custom Graph API URL, e.g. https://graph.microsoft.us')
param customGraphUrl string?
@description('Custom Identity URL, e.g. https://login.microsoftonline.us')
param customIdentityUrl string?
@description('Custom Resource Manager URL, e.g. https://management.usgovcloudapi.net')
param customResourceManagerUrl string?

@description('Custom Cognitive Services scope ex: https://cognitiveservices.azure.com/.default')
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description for this parameter uses "ex:" instead of the more standard "e.g." or "for example". For consistency with other parameter descriptions in the file (which use "e.g."), consider updating this to match.

Suggested change
@description('Custom Cognitive Services scope ex: https://cognitiveservices.azure.com/.default')
@description('Custom Cognitive Services scope, e.g. https://cognitiveservices.azure.com/.default')

Copilot uses AI. Check for mistakes.
param customCognitiveServicesScope string?

@description('Custom search resource URL for token audience, e.g. https://search.azure.us')
param customSearchResourceUrl string?

var tenantId = tenant().tenantId
var openIdMetadataUrl = '${az.environment().authentication.loginEndpoint}/${tenantId}/v2.0/.well-known/openid-configuration'

// Import diagnostic settings configurations
module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging) {
name: 'diagnosticConfigs'
Expand All @@ -47,15 +66,14 @@ resource searchService 'Microsoft.Search/searchServices@2025-05-01' existing = {
resource openAiService 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = {
name: openAiServiceName
}

resource documentIntelligence 'Microsoft.CognitiveServices/accounts@2025-06-01' existing = {
name: documentIntelligenceServiceName
}
resource appInsights 'Microsoft.Insights/components@2020-02-02' existing = {
name: appInsightsName
}

var acrDomain = azurePlatform == 'AzureUSGovernment' ? '.azurecr.us' : '.azurecr.io'
var acrDomain = az.environment().suffixes.acrLoginServer

// add web app
resource webApp 'Microsoft.Web/sites@2022-03-01' = {
Expand All @@ -77,16 +95,14 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' = {
ftpsState: 'Disabled'
healthCheckPath: '/external/healthcheck'
appSettings: [
{ name: 'AZURE_ENDPOINT', value: azurePlatform == 'AzureUSGovernment' ? 'usgovernment' : 'public' }
{ name: 'SCM_DO_BUILD_DURING_DEPLOYMENT', value: 'false' }
{ name: 'AZURE_COSMOS_ENDPOINT', value: cosmosDb.properties.documentEndpoint }
{name: 'AZURE_ENVIRONMENT', value: azurePlatform }
{name: 'SCM_DO_BUILD_DURING_DEPLOYMENT', value: 'false'}
{name: 'AZURE_COSMOS_ENDPOINT', value: cosmosDb.properties.documentEndpoint}
Comment on lines +98 to +100
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The formatting style for these app settings is inconsistent with the rest of the appSettings array. Lines 98-100 use compact formatting without spaces after braces (e.g., {name:), while most other entries in the array use spaced formatting (e.g., { name:). For consistency, these should match the predominant style used in the file with spaces.

Suggested change
{name: 'AZURE_ENVIRONMENT', value: azurePlatform }
{name: 'SCM_DO_BUILD_DURING_DEPLOYMENT', value: 'false'}
{name: 'AZURE_COSMOS_ENDPOINT', value: cosmosDb.properties.documentEndpoint}
{ name: 'AZURE_ENVIRONMENT', value: azurePlatform }
{ name: 'SCM_DO_BUILD_DURING_DEPLOYMENT', value: 'false' }
{ name: 'AZURE_COSMOS_ENDPOINT', value: cosmosDb.properties.documentEndpoint }

Copilot uses AI. Check for mistakes.
{ 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 }
{
Expand Down Expand Up @@ -152,6 +168,16 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' = {
{ name: 'XDT_MicrosoftApplicationInsights_BaseExtensions', value: 'disabled' }
{ name: 'XDT_MicrosoftApplicationInsights_Mode', value: 'recommended' }
{ name: 'XDT_MicrosoftApplicationInsights_PreemptSdk', value: 'disabled' }
...(azurePlatform == 'custom' ? [
{name: 'CUSTOM_GRAPH_URL_VALUE', value: customGraphUrl}
{name: 'CUSTOM_IDENTITY_URL_VALUE', value: customIdentityUrl}
{name: 'CUSTOM_RESOURCE_MANAGER_URL_VALUE', value: customResourceManagerUrl}
{name: 'CUSTOM_BLOB_STORAGE_URL_VALUE', value: customBlobStorageSuffix}
{name: 'CUSTOM_COGNITIVE_SERVICES_URL_VALUE', value: customCognitiveServicesScope}
{name: 'CUSTOM_SEARCH_RESOURCE_MANAGER_URL_VALUE', value: customSearchResourceUrl}
Comment on lines +172 to +177
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nullable parameters are passed directly to environment variables without null checking. If these parameters are not provided (which is valid for nullable parameters), the environment variables will be set to empty or undefined values. For the 'custom' environment, consider either making these parameters required when azurePlatform is 'custom', or adding validation to ensure they are provided when needed.

Copilot uses AI. Check for mistakes.
{name: 'KEY_VAULT_DOMAIN', value: az.environment().suffixes.keyvaultDns}
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The environment variable KEY_VAULT_DOMAIN is being set conditionally only for the 'custom' environment, but it's also used in the 'usgovernment' and default environments in config.py (lines 174, 183, 192). Consider setting this environment variable for all platforms to maintain consistency, or ensure the application code can handle its absence.

Copilot uses AI. Check for mistakes.
{name: 'CUSTOM_OIDC_METADATA_URL_VALUE', value: openIdMetadataUrl}]
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is trailing whitespace at the end of this line after the closing bracket. While this doesn't affect functionality, it violates best practices for clean code formatting.

Suggested change
{name: 'CUSTOM_OIDC_METADATA_URL_VALUE', value: openIdMetadataUrl}]
{name: 'CUSTOM_OIDC_METADATA_URL_VALUE', value: openIdMetadataUrl}]

Copilot uses AI. Check for mistakes.
Comment on lines +172 to +179
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The formatting style for these environment variable entries is inconsistent. They use compact formatting without spaces after braces (e.g., {name:), while most other entries in the appSettings array use spaced formatting (e.g., { name:). For consistency, these should match the predominant style used in the file with spaces.

Suggested change
{name: 'CUSTOM_GRAPH_URL_VALUE', value: customGraphUrl}
{name: 'CUSTOM_IDENTITY_URL_VALUE', value: customIdentityUrl}
{name: 'CUSTOM_RESOURCE_MANAGER_URL_VALUE', value: customResourceManagerUrl}
{name: 'CUSTOM_BLOB_STORAGE_URL_VALUE', value: customBlobStorageSuffix}
{name: 'CUSTOM_COGNITIVE_SERVICES_URL_VALUE', value: customCognitiveServicesScope}
{name: 'CUSTOM_SEARCH_RESOURCE_MANAGER_URL_VALUE', value: customSearchResourceUrl}
{name: 'KEY_VAULT_DOMAIN', value: az.environment().suffixes.keyvaultDns}
{name: 'CUSTOM_OIDC_METADATA_URL_VALUE', value: openIdMetadataUrl}]
{ name: 'CUSTOM_GRAPH_URL_VALUE', value: customGraphUrl }
{ name: 'CUSTOM_IDENTITY_URL_VALUE', value: customIdentityUrl }
{ name: 'CUSTOM_RESOURCE_MANAGER_URL_VALUE', value: customResourceManagerUrl }
{ name: 'CUSTOM_BLOB_STORAGE_URL_VALUE', value: customBlobStorageSuffix }
{ name: 'CUSTOM_COGNITIVE_SERVICES_URL_VALUE', value: customCognitiveServicesScope }
{ name: 'CUSTOM_SEARCH_RESOURCE_MANAGER_URL_VALUE', value: customSearchResourceUrl }
{ name: 'KEY_VAULT_DOMAIN', value: az.environment().suffixes.keyvaultDns }
{ name: 'CUSTOM_OIDC_METADATA_URL_VALUE', value: openIdMetadataUrl }]

Copilot uses AI. Check for mistakes.
: [])
]
}
clientAffinityEnabled: false
Expand Down
Loading