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 @@ -137,7 +137,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 @@ -172,12 +172,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'
])
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 @@ -126,13 +124,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')
param customCognitiveServicesScope string = 'https://cognitiveservices.azure.com/.default'
Comment on lines +136 to +137
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.
@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.

//=========================================================
// 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 @@ -369,6 +381,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
}
}

Expand Down Expand Up @@ -462,7 +481,6 @@ module setPermissions 'modules/setPermissions.bicep' = if (configureApplicationP
name: 'setPermissions'
scope: rg
params: {

webAppName: appService.outputs.name
authenticationType: authenticationType
enterpriseAppServicePrincipalId: enterpriseAppServicePrincipalId
Expand All @@ -487,11 +505,9 @@ module setPermissions 'modules/setPermissions.bicep' = if (configureApplicationP
//=========================================================
// output required for both predeploy and postprovision scripts in azure.yaml
output var_rgName string = rgName

// output values required for predeploy script in azure.yaml
output var_webService string = appService.outputs.name
output var_imageName string = contains(imageName, ':') ? split(imageName, ':')[0] : imageName
output var_imageTag string = split(imageName, ':')[1]
output var_imageTag string = contains(imageName, ':') ? split(imageName, ':')[1] : 'latest'
output var_containerRegistry string = containerRegistry
output var_acrName string = toLower('${appName}${environment}acr')

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 @@ -25,6 +25,25 @@ param authenticationType string
param enterpriseAppClientSecret string = ''
param keyVaultUri 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')
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 @@ -45,15 +64,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 @@ -70,16 +88,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}
{ 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 @@ -145,6 +161,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}
{name: 'KEY_VAULT_DOMAIN', value: az.environment().suffixes.keyvaultDns}
{name: 'CUSTOM_OIDC_METADATA_URL_VALUE', value: openIdMetadataUrl}]
: [])
]
}
clientAffinityEnabled: false
Expand Down