Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions app/backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@

@bp.route("/")
async def index():
# TODO: use msal loginRedirect on a blankish page
return await bp.send_static_file("index.html")


Expand Down Expand Up @@ -753,6 +754,9 @@ def create_app():
logging.getLogger("scripts").setLevel(app_level)

if allowed_origin := os.getenv("ALLOWED_ORIGIN"):
app.logger.info("ALLOWED_ORIGIN is set, enabling CORS for %s", allowed_origin)
cors(app, allow_origin=allowed_origin, allow_methods=["GET", "POST"])
allowed_origins = allowed_origin.split(";")
if len(allowed_origins) > 0:
app.logger.info("CORS enabled for %s", allowed_origins)
cors(app, allow_origin=allowed_origins, allow_methods=["GET", "POST"])

return app
7 changes: 1 addition & 6 deletions infra/core/host/appservice.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,6 @@ param publicNetworkAccess string = 'Enabled'
param enableUnauthenticatedAccess bool = false
param disableAppServicesAuthentication bool = false

var msftAllowedOrigins = [ 'https://portal.azure.com', 'https://ms.portal.azure.com' ]
var loginEndpoint = environment().authentication.loginEndpoint
var loginEndpointFixed = lastIndexOf(loginEndpoint, '/') == length(loginEndpoint) - 1 ? substring(loginEndpoint, 0, length(loginEndpoint) - 1) : loginEndpoint
var allMsftAllowedOrigins = !(empty(clientAppId)) ? union(msftAllowedOrigins, [ loginEndpointFixed ]) : msftAllowedOrigins

// .default must be the 1st scope for On-Behalf-Of-Flow combined consent to work properly
// Please see https://learn.microsoft.com/entra/identity-platform/v2-oauth2-on-behalf-of-flow#default-and-combined-consent
var requiredScopes = [ 'api://${serverAppId}/.default', 'openid', 'profile', 'email', 'offline_access' ]
Expand All @@ -72,7 +67,7 @@ var coreConfig = {
functionAppScaleLimit: functionAppScaleLimit != -1 ? functionAppScaleLimit : null
healthCheckPath: healthCheckPath
cors: {
allowedOrigins: union(allMsftAllowedOrigins, allowedOrigins)
allowedOrigins: allowedOrigins
}
}

Expand Down
18 changes: 12 additions & 6 deletions infra/core/host/container-app-upsert.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ param keyvaultIdentities object = {}
@description('The environment variables for the container in key value pairs')
param env object = {}

@description('The environment variables with secret references')
param envSecrets array = []

@description('Specifies if the resource ingress is exposed externally')
param external bool = true

Expand All @@ -85,6 +88,13 @@ resource existingApp 'Microsoft.App/containerApps@2023-05-02-preview' existing =
name: name
}

var envAsArray = [
for key in objectKeys(env): {
name: key
value: '${env[key]}'
}
]

module app 'container-app.bicep' = {
name: '${deployment().name}-update'
params: {
Expand All @@ -110,12 +120,7 @@ module app 'container-app.bicep' = {
keyvaultIdentities: keyvaultIdentities
allowedOrigins: allowedOrigins
external: external
env: [
for key in objectKeys(env): {
name: key
value: '${env[key]}'
}
]
env: concat(envAsArray, envSecrets)
imageName: !empty(imageName) ? imageName : exists ? existingApp.properties.template.containers[0].image : ''
targetPort: targetPort
serviceBinds: serviceBinds
Expand All @@ -128,3 +133,4 @@ output name string = app.outputs.name
output uri string = app.outputs.uri
output id string = app.outputs.id
output identityPrincipalId string = app.outputs.identityPrincipalId
output identityResourceId string = app.outputs.identityResourceId
1 change: 1 addition & 0 deletions infra/core/host/container-app.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01'

output defaultDomain string = containerAppsEnvironment.properties.defaultDomain
output identityPrincipalId string = normalizedIdentityType == 'None' ? '' : (empty(identityName) ? app.identity.principalId : userIdentity.properties.principalId)
output identityResourceId string = normalizedIdentityType == 'UserAssigned' ? resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', userIdentity.name) : ''
output imageName string = imageName
output name string = app.name
output serviceBind object = !empty(serviceType) ? { serviceId: app.id, name: name } : {}
Expand Down
69 changes: 69 additions & 0 deletions infra/core/host/container-apps-auth.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
metadata description = 'Creates an Azure Container Apps Auth Config using Microsoft Entra as Identity Provider.'

@description('The name of the container apps resource within the current resource group scope')
param name string

param additionalScopes array = []
param additionalAllowedAudiences array = []
param allowedApplications array = []

param clientAppId string = ''
param serverAppId string = ''
@secure()
param clientSecretSettingName string = ''
param authenticationIssuerUri string = ''
param enableUnauthenticatedAccess bool = false
param blobContainerUri string = ''
param appIdentityResourceId string = ''

// .default must be the 1st scope for On-Behalf-Of-Flow combined consent to work properly
// Please see https://learn.microsoft.com/entra/identity-platform/v2-oauth2-on-behalf-of-flow#default-and-combined-consent
var requiredScopes = [ 'api://${serverAppId}/.default', 'openid', 'profile', 'email', 'offline_access' ]
var requiredAudiences = [ 'api://${serverAppId}' ]

resource app 'Microsoft.App/containerApps@2023-05-01' existing = {
name: name
}

resource auth 'Microsoft.App/containerApps/authConfigs@2024-10-02-preview' = {
parent: app
name: 'current'
properties: {
platform: {
enabled: true
}
globalValidation: {
redirectToProvider: 'azureactivedirectory'
unauthenticatedClientAction: enableUnauthenticatedAccess ? 'AllowAnonymous' : 'RedirectToLoginPage'
}
identityProviders: {
azureActiveDirectory: {
enabled: true
registration: {
clientId: clientAppId
clientSecretSettingName: clientSecretSettingName
openIdIssuer: authenticationIssuerUri
}
login: {
loginParameters: [ 'scope=${join(union(requiredScopes, additionalScopes), ' ')}' ]
}
validation: {
allowedAudiences: union(requiredAudiences, additionalAllowedAudiences)
defaultAuthorizationPolicy: {
allowedApplications: allowedApplications
}
}
}
}
login: {
// https://learn.microsoft.com/en-us/azure/container-apps/token-store
tokenStore: {
enabled: true
azureBlobStorage: {
blobContainerUri: blobContainerUri
managedIdentityResourceId: appIdentityResourceId
}
}
}
}
}
58 changes: 52 additions & 6 deletions infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ param storageSkuName string // Set in main.parameters.json
param userStorageAccountName string = ''
param userStorageContainerName string = 'user-content'

param tokenStorageContainerName string = 'tokens'

param appServiceSkuName string // Set in main.parameters.json

@allowed(['azure', 'openai', 'azure_custom'])
Expand Down Expand Up @@ -248,6 +250,16 @@ param containerRegistryName string = deploymentTarget == 'containerapps'
? '${replace(environmentName, '-', '')}acr'
: ''

// Configure CORS for allowing different web apps to use the backend
// For more information please see https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
var msftAllowedOrigins = [ 'https://portal.azure.com', 'https://ms.portal.azure.com' ]
var loginEndpoint = environment().authentication.loginEndpoint
var loginEndpointFixed = lastIndexOf(loginEndpoint, '/') == length(loginEndpoint) - 1 ? substring(loginEndpoint, 0, length(loginEndpoint) - 1) : loginEndpoint
var allMsftAllowedOrigins = !(empty(clientAppId)) ? union(msftAllowedOrigins, [ loginEndpointFixed ]) : msftAllowedOrigins
var allowedOrigins = union(split(allowedOrigin, ';'), allMsftAllowedOrigins)
// Filter out any empty origin strings and remove any duplicate origins
var allowedOriginsEnv = join(reduce(filter(allowedOrigins, o => length(trim(o)) > 0), [], (cur, next) => union(cur, [next])), ';')

// Organize resources in a resource group
resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = {
name: !empty(resourceGroupName) ? resourceGroupName : '${abbrs.resourcesResourceGroups}${environmentName}'
Expand Down Expand Up @@ -376,14 +388,12 @@ var appEnvVariables = {
AZURE_ENABLE_GLOBAL_DOCUMENT_ACCESS: enableGlobalDocuments
AZURE_ENABLE_UNAUTHENTICATED_ACCESS: enableUnauthenticatedAccess
AZURE_SERVER_APP_ID: serverAppId
AZURE_SERVER_APP_SECRET: serverAppSecret
AZURE_CLIENT_APP_ID: clientAppId
AZURE_CLIENT_APP_SECRET: clientAppSecret
AZURE_TENANT_ID: tenantId
AZURE_AUTH_TENANT_ID: tenantIdForAuth
AZURE_AUTHENTICATION_ISSUER_URI: authenticationIssuerUri
// CORS support, for frontends on other hosts
ALLOWED_ORIGIN: allowedOrigin
ALLOWED_ORIGIN: allowedOriginsEnv
USE_VECTORS: useVectors
USE_GPT4V: useGPT4V
USE_USER_UPLOAD: useUserUpload
Expand Down Expand Up @@ -412,7 +422,7 @@ module backend 'core/host/appservice.bicep' = if (deploymentTarget == 'appservic
managedIdentity: true
virtualNetworkSubnetId: isolation.outputs.appSubnetId
publicNetworkAccess: publicNetworkAccess
allowedOrigins: [allowedOrigin]
allowedOrigins: allowedOrigins
clientAppId: clientAppId
serverAppId: serverAppId
enableUnauthenticatedAccess: enableUnauthenticatedAccess
Expand All @@ -421,7 +431,10 @@ module backend 'core/host/appservice.bicep' = if (deploymentTarget == 'appservic
authenticationIssuerUri: authenticationIssuerUri
use32BitWorkerProcess: appServiceSkuName == 'F1'
alwaysOn: appServiceSkuName != 'F1'
appSettings: appEnvVariables
appSettings: union(appEnvVariables, {
AZURE_SERVER_APP_SECRET: serverAppSecret
AZURE_CLIENT_APP_SECRET: clientAppSecret
})
}
}

Expand Down Expand Up @@ -472,11 +485,40 @@ module acaBackend 'core/host/container-app-upsert.bicep' = if (deploymentTarget
targetPort: 8000
containerCpuCoreCount: '1.0'
containerMemory: '2Gi'
allowedOrigins: [allowedOrigin]
allowedOrigins: allowedOrigins
env: union(appEnvVariables, {
// For using managed identity to access Azure resources. See https://github.com/microsoft/azure-container-apps/issues/442
AZURE_CLIENT_ID: (deploymentTarget == 'containerapps') ? acaIdentity.outputs.clientId : ''
})
secrets: useAuthentication ? {
azureclientappsecret: clientAppSecret
azureserverappsecret: serverAppSecret
} : {}
envSecrets: useAuthentication ? [
{
name: 'AZURE_CLIENT_APP_SECRET'
secretRef: 'azureclientappsecret'
}
{
name: 'AZURE_SERVER_APP_SECRET'
secretRef: 'azureserverappsecret'
}
] : []
}
}

module acaAuth 'core/host/container-apps-auth.bicep' = if (deploymentTarget == 'containerapps' && !empty(clientAppId)) {
name: 'aca-auth'
scope: resourceGroup
params: {
name: acaBackend.outputs.name
clientAppId: clientAppId
serverAppId: serverAppId
clientSecretSettingName: !empty(clientAppSecret) ? 'azureclientappsecret' : ''
authenticationIssuerUri: authenticationIssuerUri
enableUnauthenticatedAccess: enableUnauthenticatedAccess
blobContainerUri: 'https://${storageAccountName}.blob.${environment().suffixes.storage}/${tokenStorageContainerName}'
appIdentityResourceId: (deploymentTarget == 'appservice') ? '' : acaBackend.outputs.identityResourceId
}
}

Expand Down Expand Up @@ -661,6 +703,10 @@ module storage 'core/storage/storage-account.bicep' = {
name: storageContainerName
publicAccess: 'None'
}
{
name: tokenStorageContainerName
publicAccess: 'None'
}
]
}
}
Expand Down
Loading