diff --git a/docs/ConfigureAppAuthentication.md b/docs/ConfigureAppAuthentication.md index 8de1c105..440b839b 100644 --- a/docs/ConfigureAppAuthentication.md +++ b/docs/ConfigureAppAuthentication.md @@ -15,6 +15,55 @@ This document provides step-by-step instructions to configure Azure App Registra - Access to **Microsoft Entra ID** - Necessary permissions to create and manage **App Registrations** +- Azure CLI (`az`) signed in to the deployment subscription (for the automated script) + +## Automated configuration (recommended) + +Authentication is configured by a standalone script, +[`infra/scripts/configure_app_authentication`](../infra/scripts/configure_app_authentication.ps1), +which you run as a **manual post-deployment step** (it is intentionally not run +during `azd up` provisioning, to avoid deployment failures). Run it immediately +after the post-deployment schema-registration step. The script performs every +step in this document without the manual portal clicks: it creates (or reuses) +the API and Web app registrations, exposes the `user_impersonation` scope, +enables Container Apps authentication (the **API is set to return HTTP 401** for +unauthenticated callers — fail closed), allows the Web client on the API, and +updates the Web container environment variables. + +Run it from the project root: + +```powershell +# PowerShell (Windows) +.\infra\scripts\configure_app_authentication.ps1 +``` + +```bash +# Bash (Linux/macOS/WSL) +bash infra/scripts/configure_app_authentication.sh +``` + +To reuse existing app registrations instead of creating new ones, pass their +client ids: + +```powershell +.\infra\scripts\configure_app_authentication.ps1 -ApiClientId -WebClientId +``` + +> **Permissions:** Creating app registrations and granting admin consent +> requires the **Application Administrator** role (or equivalent). If the script +> cannot grant admin consent automatically it prints a warning; a tenant +> administrator must then consent to the API permission for the Web app. See the +> admin-consent note in [Step 2](#step-2-configure-application-registration---web-application). + +> **Web interactive login:** The script configures the Web app's identity +> provider. If your tenant requires a client secret for the browser sign-in +> (authorization-code) flow, add one with +> `az containerapp auth microsoft update --name --resource-group --client-secret `, +> or complete Step 1 for the Web app through the portal. The **API** protection +> (HTTP 401 for unauthenticated callers) does not require a secret. + +The remaining sections describe the equivalent **manual portal steps**, kept as +a fallback for environments where the script cannot be run. ## Step 1: Add Authentication Provider diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md index 695cbfe1..163a1553 100644 --- a/docs/DeploymentGuide.md +++ b/docs/DeploymentGuide.md @@ -331,6 +331,12 @@ azd up .\infra\scripts\post_deployment.ps1 ``` + > **Note:** Run this schema-registration step **before** configuring + > authentication (next step). At this point the API is still open, so no token + > is required. If you re-run it **after** enabling authentication, the script + > authenticates automatically using the deploying user's token — just ensure + > you are signed in with `az login`. + ### 5.2 Schema Registration (Automatic) > Want to customize the schemas for your own documents? [Learn more about adding your own schemas here.](./CustomizeSchemaData.md) @@ -391,20 +397,46 @@ Schema registration process completed. ✅ Schema registration complete. ``` -### 5.2 Configure Authentication (Required) +### 5.3 Configure Authentication (Required Manual Step) + +**This step is mandatory.** Until it is completed the API has external ingress +and is reachable without authentication, so run it immediately after the +post-deployment script. Authentication is configured by a standalone script. + +Run the configuration script from the project root: + +- For Bash (Linux/macOS/WSL): + + ```bash + bash infra/scripts/configure_app_authentication.sh + ``` + +- For PowerShell (Windows): + + ```powershell + .\infra\scripts\configure_app_authentication.ps1 + ``` -**This step is mandatory for application access:** +This enables Microsoft Entra ID (Easy Auth) on the API and Web container apps and +sets the **API to return HTTP 401** for unauthenticated callers (fail closed). +To reuse existing app registrations, pass their client ids +(`-ApiClientId` / `-WebClientId` in PowerShell). For details, options, the +admin-consent requirement, and the manual portal fallback, see +[App Authentication Configuration](./ConfigureAppAuthentication.md). -1. Follow [App Authentication Configuration](./ConfigureAppAuthentication.md). -2. Wait up to 10 minutes for authentication changes to take effect. +> **Note:** Allow up to 10 minutes for authentication changes to take effect. +> Creating app registrations and granting admin consent requires the +> **Application Administrator** role; if admin consent cannot be granted +> automatically, a tenant administrator must consent to the API permission for +> the Web app. -### 5.3 Verify Deployment +### 5.4 Verify Deployment 1. Access your application using the **Web App Endpoint** from the deployment output. 2. Confirm the application loads successfully. 3. Verify you can sign in with your authenticated account. -### 5.4 Test the Application +### 5.5 Test the Application **Quick Test Steps:** 1. **Download Samples**: Get sample files from the [samples directory](../src/ContentProcessorAPI/samples) — use the `claim_date_of_loss/` or `claim_hail/` folders for auto claim documents. diff --git a/infra/scripts/configure_app_authentication.ps1 b/infra/scripts/configure_app_authentication.ps1 new file mode 100644 index 00000000..805539da --- /dev/null +++ b/infra/scripts/configure_app_authentication.ps1 @@ -0,0 +1,380 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +<# +.SYNOPSIS + Configures Microsoft Entra ID (Easy Auth) authentication on the deployed + API and Web Container Apps, automating the manual portal steps described in + docs/ConfigureAppAuthentication.md. + +.DESCRIPTION + This script is intended to run AFTER `azd up` (or `azd provision`). It: + + 1. Reads Container App / resource-group values from the azd environment. + 2. Creates (or reuses) two Entra app registrations: one for the API + (resource server) and one for the Web SPA (client). + 3. Exposes a `user_impersonation` scope on the API app registration. + 4. Grants the Web app permission to call the API and attempts admin + consent (best effort). + 5. Enables Container Apps authentication: + - API -> unauthenticated requests receive HTTP 401 (fail closed). + - Web -> unauthenticated requests are redirected to login. + 6. Adds the Web client id to the API's allowed client applications. + 7. Updates the Web container app environment variables + (APP_WEB_CLIENT_ID, APP_WEB_SCOPE, APP_API_SCOPE). + + The script is idempotent: existing registrations and settings are reused. + + NOTE ON TIMING: This is a manual post-deployment step. Run it immediately + after the post-deployment schema-registration script. Until it completes, the + API has external ingress and is reachable without authentication, so do not + defer it. It is intentionally not wired into the azd provisioning hooks, to + avoid deployment-time failures. + +.PARAMETER ApiClientId + Optional. Reuse an existing API app registration client id instead of + creating a new one. + +.PARAMETER WebClientId + Optional. Reuse an existing Web app registration client id instead of + creating a new one. + +.PARAMETER TenantId + Optional. Entra tenant id. Defaults to the current `az account` tenant. + +.EXAMPLE + ./configure_app_authentication.ps1 + +.EXAMPLE + ./configure_app_authentication.ps1 -ApiClientId -WebClientId +#> + +[CmdletBinding()] +param( + [string]$ApiClientId, + [string]$WebClientId, + [string]$TenantId +) + +$ErrorActionPreference = "Stop" + +function Write-Step { + param([string]$Message) + Write-Host "" + Write-Host ("=" * 70) + Write-Host $Message + Write-Host ("=" * 70) +} + +function Get-AzdValue { + param([string]$Key) + try { + $value = azd env get-value $Key 2>$null + if ($LASTEXITCODE -eq 0 -and $value -and $value -notmatch "not found") { + return $value.Trim() + } + } catch { + # fall through + } + return $null +} + +# --------------------------------------------------------------------------- +# Step 0: Resolve context from the azd environment +# --------------------------------------------------------------------------- +Write-Step "Step 0: Resolving deployment context from azd environment" + +$ResourceGroup = Get-AzdValue "AZURE_RESOURCE_GROUP" +$SubscriptionId = Get-AzdValue "AZURE_SUBSCRIPTION_ID" +$ApiAppName = Get-AzdValue "CONTAINER_API_APP_NAME" +$ApiAppFqdn = Get-AzdValue "CONTAINER_API_APP_FQDN" +$WebAppName = Get-AzdValue "CONTAINER_WEB_APP_NAME" +$WebAppFqdn = Get-AzdValue "CONTAINER_WEB_APP_FQDN" + +if (-not $TenantId) { + $TenantId = Get-AzdValue "AZURE_TENANT_ID" +} +if (-not $TenantId) { + $TenantId = az account show --query tenantId --output tsv +} + +foreach ($pair in @( + @{ Name = "AZURE_RESOURCE_GROUP"; Value = $ResourceGroup }, + @{ Name = "CONTAINER_API_APP_NAME"; Value = $ApiAppName }, + @{ Name = "CONTAINER_API_APP_FQDN"; Value = $ApiAppFqdn }, + @{ Name = "CONTAINER_WEB_APP_NAME"; Value = $WebAppName }, + @{ Name = "CONTAINER_WEB_APP_FQDN"; Value = $WebAppFqdn } +)) { + if (-not $pair.Value) { + throw "Required value '$($pair.Name)' could not be resolved from the azd environment. Run 'azd provision' first." + } +} + +if ($SubscriptionId) { + az account set --subscription $SubscriptionId | Out-Null +} + +$ApiUri = "https://$ApiAppFqdn" +$WebUri = "https://$WebAppFqdn" + +Write-Host " Resource group : $ResourceGroup" +Write-Host " Tenant : $TenantId" +Write-Host " API app : $ApiAppName ($ApiUri)" +Write-Host " Web app : $WebAppName ($WebUri)" + +# --------------------------------------------------------------------------- +# Step 1: API app registration (resource server) + exposed scope +# --------------------------------------------------------------------------- +Write-Step "Step 1: Configuring API app registration" + +if (-not $ApiClientId) { + $ApiClientId = Get-AzdValue "API_CLIENT_ID" +} + +if (-not $ApiClientId) { + Write-Host " Creating API app registration '$ApiAppName'..." + $ApiClientId = az ad app create ` + --display-name $ApiAppName ` + --sign-in-audience AzureADMyOrg ` + --query appId --output tsv +} else { + Write-Host " Reusing API app registration: $ApiClientId" +} + +# Ensure the Application ID URI is api:// so tokens carry a stable audience. +$ApiIdentifierUri = "api://$ApiClientId" +az ad app update --id $ApiClientId --identifier-uris $ApiIdentifierUri | Out-Null + +# Expose a user_impersonation scope (idempotent: skip if already present). +# Merge the new scope into the existing api object so that reusing an existing +# registration preserves any other scopes and api settings already configured. +$apiObjectId = az ad app show --id $ApiClientId --query id --output tsv +$apiObj = az ad app show --id $ApiClientId --query api --output json | ConvertFrom-Json +if (-not $apiObj) { $apiObj = [pscustomobject]@{ oauth2PermissionScopes = @() } } +$existingScopeList = @() +if ($apiObj.oauth2PermissionScopes) { $existingScopeList = @($apiObj.oauth2PermissionScopes) } +$existingScopeValues = @($existingScopeList | ForEach-Object { $_.value }) +if ($existingScopeValues -notcontains "user_impersonation") { + Write-Host " Exposing 'user_impersonation' scope on the API app..." + $newScope = [pscustomobject]@{ + id = [guid]::NewGuid().ToString() + adminConsentDescription = "Allow the application to access the Content Processing API on behalf of the signed-in user." + adminConsentDisplayName = "Access Content Processing API" + userConsentDescription = "Allow the application to access the Content Processing API on your behalf." + userConsentDisplayName = "Access Content Processing API" + value = "user_impersonation" + type = "User" + isEnabled = $true + } + $apiObj | Add-Member -NotePropertyName oauth2PermissionScopes -NotePropertyValue (@($existingScopeList) + $newScope) -Force + $apiPatchBody = @{ api = $apiObj } | ConvertTo-Json -Depth 20 -Compress + $apiPatchFile = New-TemporaryFile + Set-Content -Path $apiPatchFile -Value $apiPatchBody -Encoding utf8 -NoNewline + try { + az rest ` + --method PATCH ` + --uri "https://graph.microsoft.com/v1.0/applications/$apiObjectId" ` + --headers "Content-Type=application/json" ` + --body "@$apiPatchFile" | Out-Null + } finally { + Remove-Item -Path $apiPatchFile -ErrorAction SilentlyContinue + } +} else { + Write-Host " 'user_impersonation' scope already exposed." +} + +# Ensure a service principal exists for the API app. +$apiSp = az ad sp show --id $ApiClientId --query id --output tsv 2>$null +if (-not $apiSp) { + az ad sp create --id $ApiClientId | Out-Null +} + +$ApiScope = "$ApiIdentifierUri/user_impersonation" +Write-Host " API scope: $ApiScope" + +# --------------------------------------------------------------------------- +# Step 2: Web app registration (SPA client) +# --------------------------------------------------------------------------- +Write-Step "Step 2: Configuring Web app registration" + +if (-not $WebClientId) { + $WebClientId = Get-AzdValue "WEB_CLIENT_ID" +} + +if (-not $WebClientId) { + Write-Host " Creating Web app registration '$WebAppName'..." + $WebClientId = az ad app create ` + --display-name $WebAppName ` + --sign-in-audience AzureADMyOrg ` + --query appId --output tsv +} else { + Write-Host " Reusing Web app registration: $WebClientId" +} + +# Register the SPA redirect URI so MSAL can complete the login flow. +# Use a Microsoft Graph PATCH via `az rest`; `az ad app update --set spa=...` +# is unreliable and fails with "Property spa in payload does not match schema". +# The JSON body is written to a temp file (--body @file) because passing inline +# JSON to az on Windows gets mangled by shell quoting. +Write-Host " Setting SPA redirect URI: $WebUri" +$webObjectId = az ad app show --id $WebClientId --query id --output tsv +$existingSpaUris = az ad app show --id $WebClientId --query "spa.redirectUris" --output json | ConvertFrom-Json +$spaUris = @() +if ($existingSpaUris) { $spaUris = @($existingSpaUris) } +if ($spaUris -notcontains $WebUri) { $spaUris += $WebUri } +$spaBody = @{ spa = @{ redirectUris = $spaUris } } | ConvertTo-Json -Compress -Depth 5 +$spaBodyFile = New-TemporaryFile +Set-Content -Path $spaBodyFile -Value $spaBody -Encoding utf8 -NoNewline +try { + az rest ` + --method PATCH ` + --uri "https://graph.microsoft.com/v1.0/applications/$webObjectId" ` + --headers "Content-Type=application/json" ` + --body "@$spaBodyFile" | Out-Null +} finally { + Remove-Item -Path $spaBodyFile -ErrorAction SilentlyContinue +} + +# Enable ID token issuance for the implicit grant. Container Apps Easy Auth +# requests an id_token during the login redirect; without this Entra returns +# AADSTS700054 "response_type 'id_token' is not enabled for the application". +# The Easy Auth callback (/.auth/login/aad/callback) must also be registered as +# a Web-platform redirect URI, otherwise login fails with AADSTS50011. +Write-Host " Enabling ID token issuance and Web redirect URI on the Web app..." +$easyAuthRedirect = "$WebUri/.auth/login/aad/callback" +$existingWebUris = az ad app show --id $WebClientId --query "web.redirectUris" --output json | ConvertFrom-Json +$webUris = @() +if ($existingWebUris) { $webUris = @($existingWebUris) } +if ($webUris -notcontains $easyAuthRedirect) { $webUris += $easyAuthRedirect } +$implicitBody = @{ web = @{ redirectUris = $webUris; implicitGrantSettings = @{ enableIdTokenIssuance = $true } } } | ConvertTo-Json -Compress -Depth 5 +$implicitBodyFile = New-TemporaryFile +Set-Content -Path $implicitBodyFile -Value $implicitBody -Encoding utf8 -NoNewline +try { + az rest ` + --method PATCH ` + --uri "https://graph.microsoft.com/v1.0/applications/$webObjectId" ` + --headers "Content-Type=application/json" ` + --body "@$implicitBodyFile" | Out-Null +} finally { + Remove-Item -Path $implicitBodyFile -ErrorAction SilentlyContinue +} + +# Grant the Web app permission to call the API's user_impersonation scope. +$scopeGuid = az ad app show --id $ApiClientId --query "api.oauth2PermissionScopes[?value=='user_impersonation'].id | [0]" --output tsv +Write-Host " Adding API permission to the Web app..." +az ad app permission add ` + --id $WebClientId ` + --api $ApiClientId ` + --api-permissions "$scopeGuid=Scope" | Out-Null + +$webSp = az ad sp show --id $WebClientId --query id --output tsv 2>$null +if (-not $webSp) { + az ad sp create --id $WebClientId | Out-Null +} + +Write-Host " Attempting admin consent (best effort)..." +az ad app permission admin-consent --id $WebClientId 2>$null | Out-Null +if ($LASTEXITCODE -eq 0) { + Write-Host " Admin consent granted." +} else { + Write-Warning " Could not grant admin consent automatically. A tenant administrator must consent to the API permission for '$WebAppName'. See docs/ConfigureAppAuthentication.md." +} + +# --------------------------------------------------------------------------- +# Step 3: Enable Container Apps authentication +# --------------------------------------------------------------------------- +Write-Step "Step 3: Enabling Container Apps authentication (Easy Auth)" + +$Issuer = "https://sts.windows.net/$TenantId/" + +# API: fail closed. Unauthenticated callers get HTTP 401 (no login redirect). +Write-Host " Configuring API authentication (Return401)..." +az containerapp auth microsoft update ` + --name $ApiAppName ` + --resource-group $ResourceGroup ` + --client-id $ApiClientId ` + --issuer $Issuer ` + --allowed-audiences $ApiIdentifierUri ` + --yes | Out-Null + +az containerapp auth update ` + --name $ApiAppName ` + --resource-group $ResourceGroup ` + --unauthenticated-client-action Return401 ` + --redirect-provider AzureActiveDirectory ` + --yes | Out-Null + +# Web: redirect unauthenticated browser users to the login page. +Write-Host " Configuring Web authentication (RedirectToLoginPage)..." +az containerapp auth microsoft update ` + --name $WebAppName ` + --resource-group $ResourceGroup ` + --client-id $WebClientId ` + --issuer $Issuer ` + --yes | Out-Null + +az containerapp auth update ` + --name $WebAppName ` + --resource-group $ResourceGroup ` + --unauthenticated-client-action RedirectToLoginPage ` + --redirect-provider AzureActiveDirectory ` + --yes | Out-Null + +# --------------------------------------------------------------------------- +# Step 4: Allow the Web client to call the API +# --------------------------------------------------------------------------- +Write-Step "Step 4: Allowing the Web client on the API" + +# The `--allowed-client-applications` flag is not available in older containerapp +# CLI extensions. The authConfigs resource does not support PATCH, so GET the +# current config, merge in the allowed application, and PUT it back. +Write-Host " Adding Web client id to the API allowed client applications..." +$authConfigUri = "https://management.azure.com/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroup/providers/Microsoft.App/containerApps/$ApiAppName/authConfigs/current?api-version=2024-03-01" +$authConfig = az rest --method GET --uri $authConfigUri | ConvertFrom-Json +$aad = $authConfig.properties.identityProviders.azureActiveDirectory +if (-not $aad.validation) { + $aad | Add-Member -NotePropertyName validation -NotePropertyValue ([pscustomobject]@{}) -Force +} +if (-not $aad.validation.defaultAuthorizationPolicy) { + $aad.validation | Add-Member -NotePropertyName defaultAuthorizationPolicy -NotePropertyValue ([pscustomobject]@{}) -Force +} +$aad.validation.defaultAuthorizationPolicy | Add-Member -NotePropertyName allowedApplications -NotePropertyValue @($WebClientId) -Force +$allowedAppsBody = @{ properties = $authConfig.properties } | ConvertTo-Json -Compress -Depth 20 +$allowedAppsFile = New-TemporaryFile +Set-Content -Path $allowedAppsFile -Value $allowedAppsBody -Encoding utf8 -NoNewline +try { + az rest ` + --method PUT ` + --uri $authConfigUri ` + --headers "Content-Type=application/json" ` + --body "@$allowedAppsFile" | Out-Null +} finally { + Remove-Item -Path $allowedAppsFile -ErrorAction SilentlyContinue +} + +# --------------------------------------------------------------------------- +# Step 5: Update Web container environment variables +# --------------------------------------------------------------------------- +Write-Step "Step 5: Updating Web container environment variables" + +az containerapp update ` + --name $WebAppName ` + --resource-group $ResourceGroup ` + --set-env-vars ` + "APP_WEB_CLIENT_ID=$WebClientId" ` + "APP_WEB_SCOPE=$ApiScope" ` + "APP_API_SCOPE=$ApiScope" | Out-Null + +# Persist the resulting client ids back into the azd environment for reuse. +if (Get-Command azd -ErrorAction SilentlyContinue) { + azd env set API_CLIENT_ID $ApiClientId 2>$null | Out-Null + azd env set WEB_CLIENT_ID $WebClientId 2>$null | Out-Null +} + +Write-Step "Authentication configuration complete" +Write-Host " API client id : $ApiClientId" +Write-Host " Web client id : $WebClientId" +Write-Host " API scope : $ApiScope" +Write-Host "" +Write-Host " The API now returns HTTP 401 to unauthenticated callers." +Write-Host " Note: post-deployment data ingestion scripts must send a bearer token." diff --git a/infra/scripts/configure_app_authentication.sh b/infra/scripts/configure_app_authentication.sh new file mode 100644 index 00000000..e2dbaefb --- /dev/null +++ b/infra/scripts/configure_app_authentication.sh @@ -0,0 +1,299 @@ +#!/bin/bash +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# +# Configures Microsoft Entra ID (Easy Auth) authentication on the deployed API +# and Web Container Apps, automating the manual portal steps described in +# docs/ConfigureAppAuthentication.md. +# +# Run AFTER `azd up` (or `azd provision`). Idempotent: existing registrations +# and settings are reused. +# +# TIMING: This is a manual post-deployment step. Run it immediately after the +# post-deployment schema-registration script. Until it completes, the API has +# external ingress and is reachable without authentication, so do not defer it. +# It is intentionally not wired into the azd provisioning hooks, to avoid +# deployment-time failures. +# +# Usage: +# ./configure_app_authentication.sh +# API_CLIENT_ID= WEB_CLIENT_ID= ./configure_app_authentication.sh + +set -euo pipefail + +step() { + echo "" + echo "======================================================================" + echo "$1" + echo "======================================================================" +} + +get_azd_value() { + local key="$1" + local value + value=$(azd env get-value "$key" 2>/dev/null || echo "") + if [ -n "$value" ] && [[ "$value" != *"not found"* ]]; then + echo "$value" + fi +} + +# --------------------------------------------------------------------------- +# Step 0: Resolve context from the azd environment +# --------------------------------------------------------------------------- +step "Step 0: Resolving deployment context from azd environment" + +RESOURCE_GROUP=$(get_azd_value "AZURE_RESOURCE_GROUP") +SUBSCRIPTION_ID=$(get_azd_value "AZURE_SUBSCRIPTION_ID") +API_APP_NAME=$(get_azd_value "CONTAINER_API_APP_NAME") +API_APP_FQDN=$(get_azd_value "CONTAINER_API_APP_FQDN") +WEB_APP_NAME=$(get_azd_value "CONTAINER_WEB_APP_NAME") +WEB_APP_FQDN=$(get_azd_value "CONTAINER_WEB_APP_FQDN") + +TENANT_ID="${TENANT_ID:-$(get_azd_value "AZURE_TENANT_ID")}" +if [ -z "$TENANT_ID" ]; then + TENANT_ID=$(az account show --query tenantId --output tsv) +fi + +for pair in \ + "AZURE_RESOURCE_GROUP=$RESOURCE_GROUP" \ + "CONTAINER_API_APP_NAME=$API_APP_NAME" \ + "CONTAINER_API_APP_FQDN=$API_APP_FQDN" \ + "CONTAINER_WEB_APP_NAME=$WEB_APP_NAME" \ + "CONTAINER_WEB_APP_FQDN=$WEB_APP_FQDN"; do + name="${pair%%=*}" + value="${pair#*=}" + if [ -z "$value" ]; then + echo "Error: required value '$name' could not be resolved from the azd environment. Run 'azd provision' first." >&2 + exit 1 + fi +done + +if [ -n "$SUBSCRIPTION_ID" ]; then + az account set --subscription "$SUBSCRIPTION_ID" +fi + +API_URI="https://$API_APP_FQDN" +WEB_URI="https://$WEB_APP_FQDN" + +echo " Resource group : $RESOURCE_GROUP" +echo " Tenant : $TENANT_ID" +echo " API app : $API_APP_NAME ($API_URI)" +echo " Web app : $WEB_APP_NAME ($WEB_URI)" + +# --------------------------------------------------------------------------- +# Step 1: API app registration (resource server) + exposed scope +# --------------------------------------------------------------------------- +step "Step 1: Configuring API app registration" + +API_CLIENT_ID="${API_CLIENT_ID:-$(get_azd_value "API_CLIENT_ID")}" + +if [ -z "$API_CLIENT_ID" ]; then + echo " Creating API app registration '$API_APP_NAME'..." + API_CLIENT_ID=$(az ad app create \ + --display-name "$API_APP_NAME" \ + --sign-in-audience AzureADMyOrg \ + --query appId --output tsv) +else + echo " Reusing API app registration: $API_CLIENT_ID" +fi + +API_IDENTIFIER_URI="api://$API_CLIENT_ID" +az ad app update --id "$API_CLIENT_ID" --identifier-uris "$API_IDENTIFIER_URI" + +EXISTING_SCOPES=$(az ad app show --id "$API_CLIENT_ID" --query "api.oauth2PermissionScopes[].value" --output tsv || echo "") +if ! echo "$EXISTING_SCOPES" | grep -q "user_impersonation"; then + echo " Exposing 'user_impersonation' scope on the API app..." + SCOPE_ID=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || python -c "import uuid;print(uuid.uuid4())") + # Merge the new scope into the existing api object so that reusing an existing + # registration preserves any other scopes and api settings already configured. + API_OBJECT_ID=$(az ad app show --id "$API_CLIENT_ID" --query id --output tsv) + CURRENT_API_FILE=$(mktemp) + MERGED_API_FILE=$(mktemp) + az ad app show --id "$API_CLIENT_ID" --query api --output json > "$CURRENT_API_FILE" + jq --arg id "$SCOPE_ID" \ + '{api: (. + {oauth2PermissionScopes: ((.oauth2PermissionScopes // []) + [{ + id: $id, + adminConsentDescription: "Allow the application to access the Content Processing API on behalf of the signed-in user.", + adminConsentDisplayName: "Access Content Processing API", + userConsentDescription: "Allow the application to access the Content Processing API on your behalf.", + userConsentDisplayName: "Access Content Processing API", + value: "user_impersonation", + type: "User", + isEnabled: true + }])})}' \ + "$CURRENT_API_FILE" > "$MERGED_API_FILE" + az rest \ + --method PATCH \ + --uri "https://graph.microsoft.com/v1.0/applications/$API_OBJECT_ID" \ + --headers "Content-Type=application/json" \ + --body "@$MERGED_API_FILE" + rm -f "$CURRENT_API_FILE" "$MERGED_API_FILE" +else + echo " 'user_impersonation' scope already exposed." +fi + +if ! az ad sp show --id "$API_CLIENT_ID" --query id --output tsv >/dev/null 2>&1; then + az ad sp create --id "$API_CLIENT_ID" +fi + +API_SCOPE="$API_IDENTIFIER_URI/user_impersonation" +echo " API scope: $API_SCOPE" + +# --------------------------------------------------------------------------- +# Step 2: Web app registration (SPA client) +# --------------------------------------------------------------------------- +step "Step 2: Configuring Web app registration" + +WEB_CLIENT_ID="${WEB_CLIENT_ID:-$(get_azd_value "WEB_CLIENT_ID")}" + +if [ -z "$WEB_CLIENT_ID" ]; then + echo " Creating Web app registration '$WEB_APP_NAME'..." + WEB_CLIENT_ID=$(az ad app create \ + --display-name "$WEB_APP_NAME" \ + --sign-in-audience AzureADMyOrg \ + --query appId --output tsv) +else + echo " Reusing Web app registration: $WEB_CLIENT_ID" +fi + +# Register the SPA redirect URI via a Microsoft Graph PATCH; `az ad app update +# --set spa=...` is unreliable and fails with "Property spa in payload does not +# match schema". The JSON body is written to a temp file (--body @file) to avoid +# shell-quoting issues. +echo " Setting SPA redirect URI: $WEB_URI" +WEB_OBJECT_ID=$(az ad app show --id "$WEB_CLIENT_ID" --query id --output tsv) +SPA_BODY_FILE=$(mktemp) +# Merge with existing SPA redirect URIs (deduped) so reusing a registration does +# not remove previously configured URIs. +az ad app show --id "$WEB_CLIENT_ID" --query "spa.redirectUris" --output json \ + | jq --arg u "$WEB_URI" '{spa: {redirectUris: ((. // []) + [$u] | unique)}}' > "$SPA_BODY_FILE" +az rest \ + --method PATCH \ + --uri "https://graph.microsoft.com/v1.0/applications/$WEB_OBJECT_ID" \ + --headers "Content-Type=application/json" \ + --body "@$SPA_BODY_FILE" +rm -f "$SPA_BODY_FILE" + +# Enable ID token issuance for the implicit grant. Container Apps Easy Auth +# requests an id_token during the login redirect; without this Entra returns +# AADSTS700054 "response_type 'id_token' is not enabled for the application". +# The Easy Auth callback (/.auth/login/aad/callback) must also be registered as +# a Web-platform redirect URI, otherwise login fails with AADSTS50011. +echo " Enabling ID token issuance and Web redirect URI on the Web app..." +IMPLICIT_BODY_FILE=$(mktemp) +# Merge with existing Web redirect URIs (deduped) so reusing a registration does +# not remove previously configured URIs. +az ad app show --id "$WEB_CLIENT_ID" --query "web.redirectUris" --output json \ + | jq --arg u "$WEB_URI/.auth/login/aad/callback" \ + '{web: {redirectUris: ((. // []) + [$u] | unique), implicitGrantSettings: {enableIdTokenIssuance: true}}}' > "$IMPLICIT_BODY_FILE" +az rest \ + --method PATCH \ + --uri "https://graph.microsoft.com/v1.0/applications/$WEB_OBJECT_ID" \ + --headers "Content-Type=application/json" \ + --body "@$IMPLICIT_BODY_FILE" +rm -f "$IMPLICIT_BODY_FILE" + +SCOPE_GUID=$(az ad app show --id "$API_CLIENT_ID" --query "api.oauth2PermissionScopes[?value=='user_impersonation'].id | [0]" --output tsv) +echo " Adding API permission to the Web app..." +az ad app permission add \ + --id "$WEB_CLIENT_ID" \ + --api "$API_CLIENT_ID" \ + --api-permissions "$SCOPE_GUID=Scope" + +if ! az ad sp show --id "$WEB_CLIENT_ID" --query id --output tsv >/dev/null 2>&1; then + az ad sp create --id "$WEB_CLIENT_ID" +fi + +echo " Attempting admin consent (best effort)..." +if az ad app permission admin-consent --id "$WEB_CLIENT_ID" 2>/dev/null; then + echo " Admin consent granted." +else + echo " WARNING: Could not grant admin consent automatically. A tenant administrator must consent to the API permission for '$WEB_APP_NAME'. See docs/ConfigureAppAuthentication.md." >&2 +fi + +# --------------------------------------------------------------------------- +# Step 3: Enable Container Apps authentication +# --------------------------------------------------------------------------- +step "Step 3: Enabling Container Apps authentication (Easy Auth)" + +ISSUER="https://sts.windows.net/$TENANT_ID/" + +echo " Configuring API authentication (Return401)..." +az containerapp auth microsoft update \ + --name "$API_APP_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --client-id "$API_CLIENT_ID" \ + --issuer "$ISSUER" \ + --allowed-audiences "$API_IDENTIFIER_URI" \ + --yes + +az containerapp auth update \ + --name "$API_APP_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --unauthenticated-client-action Return401 \ + --redirect-provider AzureActiveDirectory \ + --yes + +echo " Configuring Web authentication (RedirectToLoginPage)..." +az containerapp auth microsoft update \ + --name "$WEB_APP_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --client-id "$WEB_CLIENT_ID" \ + --issuer "$ISSUER" \ + --yes + +az containerapp auth update \ + --name "$WEB_APP_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --unauthenticated-client-action RedirectToLoginPage \ + --redirect-provider AzureActiveDirectory \ + --yes + +# --------------------------------------------------------------------------- +# Step 4: Allow the Web client to call the API +# --------------------------------------------------------------------------- +step "Step 4: Allowing the Web client on the API" + +# The --allowed-client-applications flag is not available in older containerapp +# CLI extensions. The authConfigs resource does not support PATCH, so GET the +# current config, merge in the allowed application with jq, and PUT it back. +echo " Adding Web client id to the API allowed client applications..." +AUTH_CONFIG_URI="https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.App/containerApps/$API_APP_NAME/authConfigs/current?api-version=2024-03-01" +CURRENT_AUTH_FILE=$(mktemp) +ALLOWED_APPS_FILE=$(mktemp) +az rest --method GET --uri "$AUTH_CONFIG_URI" > "$CURRENT_AUTH_FILE" +jq --arg app "$WEB_CLIENT_ID" \ + '{properties: (.properties | .identityProviders.azureActiveDirectory.validation.defaultAuthorizationPolicy.allowedApplications = [$app])}' \ + "$CURRENT_AUTH_FILE" > "$ALLOWED_APPS_FILE" +az rest \ + --method PUT \ + --uri "$AUTH_CONFIG_URI" \ + --headers "Content-Type=application/json" \ + --body "@$ALLOWED_APPS_FILE" +rm -f "$CURRENT_AUTH_FILE" "$ALLOWED_APPS_FILE" + +# --------------------------------------------------------------------------- +# Step 5: Update Web container environment variables +# --------------------------------------------------------------------------- +step "Step 5: Updating Web container environment variables" + +az containerapp update \ + --name "$WEB_APP_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --set-env-vars \ + "APP_WEB_CLIENT_ID=$WEB_CLIENT_ID" \ + "APP_WEB_SCOPE=$API_SCOPE" \ + "APP_API_SCOPE=$API_SCOPE" + +if command -v azd >/dev/null 2>&1; then + azd env set API_CLIENT_ID "$API_CLIENT_ID" 2>/dev/null || true + azd env set WEB_CLIENT_ID "$WEB_CLIENT_ID" 2>/dev/null || true +fi + +step "Authentication configuration complete" +echo " API client id : $API_CLIENT_ID" +echo " Web client id : $WEB_CLIENT_ID" +echo " API scope : $API_SCOPE" +echo "" +echo " The API now returns HTTP 401 to unauthenticated callers." +echo " Note: post-deployment data ingestion scripts must send a bearer token." diff --git a/infra/scripts/post_deployment.ps1 b/infra/scripts/post_deployment.ps1 index fb4c3a5f..9c7d6361 100644 --- a/infra/scripts/post_deployment.ps1 +++ b/infra/scripts/post_deployment.ps1 @@ -57,9 +57,35 @@ $RetryInterval = 15 $ApiBaseUrl = "https://$CONTAINER_API_APP_FQDN" $ApiReady = $false +# Acquire a bearer token for the API when authentication has been configured. +# API_CLIENT_ID is written to the azd environment by configure_app_authentication. +# - First deployment (before auth is configured): API_CLIENT_ID is absent, so +# this script proceeds unauthenticated against the still-open API. +# - After auth is configured: API_CLIENT_ID is present and the API returns 401 to +# unauthenticated callers. A token is then REQUIRED; if it cannot be acquired we +# fail fast rather than silently 401 through every schema operation. +$AuthHeaders = @{} +$AuthRequired = $false +$ApiClientId = azd env get-value API_CLIENT_ID 2>$null +if ($LASTEXITCODE -eq 0 -and $ApiClientId -and $ApiClientId -notmatch "not found") { + $AuthRequired = $true + $ApiClientId = $ApiClientId.Trim() + Write-Host " [Auth] Acquiring access token for API (api://$ApiClientId)..." + $AccessToken = az account get-access-token --resource "api://$ApiClientId" --query accessToken --output tsv 2>$null + if ($LASTEXITCODE -eq 0 -and $AccessToken) { + $AuthHeaders = @{ Authorization = "Bearer $($AccessToken.Trim())" } + Write-Host " [Auth] Access token acquired." + } else { + throw "Authentication is configured (API_CLIENT_ID='$ApiClientId') but an access token for 'api://$ApiClientId' could not be acquired. Sign in with 'az login' as a principal permitted to call the API and re-run. Refusing to continue unauthenticated because every schema operation would fail with HTTP 401." + } +} + for ($i = 1; $i -le $MaxRetries; $i++) { try { - $response = Invoke-WebRequest -Uri "$ApiBaseUrl/schemavault/" -Method GET -UseBasicParsing -TimeoutSec 10 -ErrorAction Stop + # Probe the startup endpoint to confirm readiness. When authentication is + # enabled the API returns 401 to anonymous callers (Return401 does not + # exclude /startup), so send the acquired auth headers on the probe. + $response = Invoke-WebRequest -Uri "$ApiBaseUrl/startup" -Method GET -Headers $AuthHeaders -UseBasicParsing -TimeoutSec 10 -ErrorAction Stop if ($response.StatusCode -eq 200) { Write-Host " [OK] API is ready." $ApiReady = $true @@ -73,6 +99,9 @@ for ($i = 1; $i -le $MaxRetries; $i++) { } if (-not $ApiReady) { + if ($AuthRequired) { + throw "API did not become ready after $MaxRetries authenticated attempts. Aborting schema registration (the API is authentication-protected; verify the deploying principal is permitted to call it)." + } Write-Host " API did not become ready after $MaxRetries attempts. Skipping schema registration." Write-Host " Run manually after the API is ready." } else { @@ -92,7 +121,7 @@ if (-not $ApiReady) { # Fetch existing schemas $ExistingSchemas = @() try { - $ExistingSchemas = Invoke-RestMethod -Uri $SchemaVaultUrl -Method GET -TimeoutSec 30 -ErrorAction Stop + $ExistingSchemas = Invoke-RestMethod -Uri $SchemaVaultUrl -Method GET -Headers $AuthHeaders -TimeoutSec 30 -ErrorAction Stop Write-Host "Fetched $($ExistingSchemas.Count) existing schema(s)." } catch { Write-Host "Warning: Could not fetch existing schemas. Proceeding..." @@ -154,6 +183,7 @@ if (-not $ApiReady) { try { $resp = Invoke-RestMethod -Uri $SchemaVaultUrl -Method POST ` -ContentType "multipart/form-data; boundary=$boundary" ` + -Headers $AuthHeaders ` -Body $bodyLines -TimeoutSec 60 -ErrorAction Stop $schemaId = $resp.Id Write-Host " Successfully registered: $Description's Schema Id - $schemaId" @@ -174,7 +204,7 @@ if (-not $ApiReady) { $ExistingSets = @() try { - $ExistingSets = Invoke-RestMethod -Uri $SchemaSetVaultUrl -Method GET -TimeoutSec 30 -ErrorAction Stop + $ExistingSets = Invoke-RestMethod -Uri $SchemaSetVaultUrl -Method GET -Headers $AuthHeaders -TimeoutSec 30 -ErrorAction Stop Write-Host "Fetched $($ExistingSets.Count) existing schema set(s)." } catch { Write-Host "Warning: Could not fetch existing schema sets. Proceeding..." @@ -190,6 +220,7 @@ if (-not $ApiReady) { try { $setResp = Invoke-RestMethod -Uri $SchemaSetVaultUrl -Method POST ` -ContentType "application/json" ` + -Headers $AuthHeaders ` -Body (@{ Name = $SetName; Description = $SetDesc } | ConvertTo-Json) ` -TimeoutSec 30 -ErrorAction Stop $SchemaSetId = $setResp.Id @@ -210,7 +241,7 @@ if (-not $ApiReady) { $AlreadyInSet = @() try { - $AlreadyInSet = Invoke-RestMethod -Uri "$SchemaSetVaultUrl$SchemaSetId/schemas" -Method GET -TimeoutSec 30 -ErrorAction Stop + $AlreadyInSet = Invoke-RestMethod -Uri "$SchemaSetVaultUrl$SchemaSetId/schemas" -Method GET -Headers $AuthHeaders -TimeoutSec 30 -ErrorAction Stop } catch { } $AlreadyInSetIds = $AlreadyInSet | ForEach-Object { $_.Id } @@ -224,6 +255,7 @@ if (-not $ApiReady) { try { Invoke-RestMethod -Uri "$SchemaSetVaultUrl$SchemaSetId/schemas" -Method POST ` -ContentType "application/json" ` + -Headers $AuthHeaders ` -Body (@{ SchemaId = $schemaId } | ConvertTo-Json) ` -TimeoutSec 30 -ErrorAction Stop | Out-Null Write-Host " Added '$className' ($schemaId) to schema set" diff --git a/infra/scripts/post_deployment.sh b/infra/scripts/post_deployment.sh index 43f23f14..28ab5945 100755 --- a/infra/scripts/post_deployment.sh +++ b/infra/scripts/post_deployment.sh @@ -66,8 +66,35 @@ MAX_RETRIES=10 RETRY_INTERVAL=15 API_BASE_URL="https://$CONTAINER_API_APP_FQDN" +# Acquire a bearer token for the API when authentication has been configured. +# API_CLIENT_ID is written to the azd environment by configure_app_authentication. +# - First deployment (before auth is configured): API_CLIENT_ID is absent, so +# this script proceeds unauthenticated against the still-open API. +# - After auth is configured: API_CLIENT_ID is present and the API returns 401 to +# unauthenticated callers. A token is then REQUIRED; if it cannot be acquired we +# fail fast rather than silently 401 through every schema operation. +AUTH_ARGS=() +AUTH_REQUIRED=false +API_CLIENT_ID=$(azd env get-value API_CLIENT_ID 2>/dev/null || echo "") +if [ -n "$API_CLIENT_ID" ] && [[ "$API_CLIENT_ID" != *"not found"* ]]; then + AUTH_REQUIRED=true + echo " [Auth] Acquiring access token for API (api://$API_CLIENT_ID)..." + ACCESS_TOKEN=$(az account get-access-token --resource "api://$API_CLIENT_ID" --query accessToken --output tsv 2>/dev/null || echo "") + if [ -n "$ACCESS_TOKEN" ]; then + AUTH_ARGS=(-H "Authorization: Bearer $ACCESS_TOKEN") + echo " [Auth] Access token acquired." + else + echo " [Auth] ERROR: authentication is configured (API_CLIENT_ID='$API_CLIENT_ID') but an access token for 'api://$API_CLIENT_ID' could not be acquired." >&2 + echo " Sign in with 'az login' as a principal permitted to call the API and re-run. Refusing to continue unauthenticated because every schema operation would fail with HTTP 401." >&2 + exit 1 + fi +fi + for i in $(seq 1 $MAX_RETRIES); do - STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$API_BASE_URL/schemavault/" 2>/dev/null || echo "000") + # Probe the startup endpoint to confirm readiness. When authentication is + # enabled the API returns 401 to anonymous callers (Return401 does not exclude + # /startup), so send the acquired auth headers on the probe. + STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${AUTH_ARGS[@]+"${AUTH_ARGS[@]}"}" "$API_BASE_URL/startup" 2>/dev/null || echo "000") if [ "$STATUS" = "200" ]; then echo " ✅ API is ready." break @@ -77,6 +104,10 @@ for i in $(seq 1 $MAX_RETRIES); do done if [ "$STATUS" != "200" ]; then + if [ "$AUTH_REQUIRED" = true ]; then + echo " ERROR: API did not become ready after $MAX_RETRIES authenticated attempts. Aborting schema registration (the API is authentication-protected; verify the deploying principal is permitted to call it)." >&2 + exit 1 + fi echo " API did not become ready after $MAX_RETRIES attempts. Skipping schema registration." echo " Run manually after the API is ready." else @@ -92,7 +123,7 @@ else echo "============================================================" # Fetch existing schemas - EXISTING_SCHEMAS=$(curl -s "$SCHEMAVAULT_URL" 2>/dev/null || echo "[]") + EXISTING_SCHEMAS=$(curl -s "${AUTH_ARGS[@]+"${AUTH_ARGS[@]}"}" "$SCHEMAVAULT_URL" 2>/dev/null || echo "[]") EXISTING_COUNT=$(echo "$EXISTING_SCHEMAS" | grep -o '"Id"' | wc -l) echo "Fetched $EXISTING_COUNT existing schema(s)." @@ -146,6 +177,7 @@ else CONTENT_TYPE="application/json" RESPONSE=$(curl -s -w "\n%{http_code}" \ + "${AUTH_ARGS[@]+"${AUTH_ARGS[@]}"}" \ -X POST "$SCHEMAVAULT_URL" \ -F "data=$DATA_PAYLOAD" \ -F "file=@$SCHEMA_FILE;type=$CONTENT_TYPE" \ @@ -176,7 +208,7 @@ else SET_DESC=$(cat "$SCHEMA_INFO_FILE" | grep -A3 '"schemaset"' | grep '"Description"' | sed 's/.*"Description"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') # Fetch existing schema sets - EXISTING_SETS=$(curl -s "$SCHEMASETVAULT_URL" 2>/dev/null || echo "[]") + EXISTING_SETS=$(curl -s "${AUTH_ARGS[@]+"${AUTH_ARGS[@]}"}" "$SCHEMASETVAULT_URL" 2>/dev/null || echo "[]") SCHEMASET_ID="" if echo "$EXISTING_SETS" | grep -q "\"Name\"[[:space:]]*:[[:space:]]*\"$SET_NAME\""; then @@ -185,6 +217,7 @@ else else echo " Creating schema set '$SET_NAME'..." RESPONSE=$(curl -s -w "\n%{http_code}" \ + "${AUTH_ARGS[@]+"${AUTH_ARGS[@]}"}" \ -X POST "$SCHEMASETVAULT_URL" \ -H "Content-Type: application/json" \ -d "{\"Name\": \"$SET_NAME\", \"Description\": \"$SET_DESC\"}" \ @@ -211,7 +244,7 @@ else echo "Step 3: Add schemas to schema set" echo "============================================================" - ALREADY_IN_SET=$(curl -s "${SCHEMASETVAULT_URL}${SCHEMASET_ID}/schemas" 2>/dev/null || echo "[]") + ALREADY_IN_SET=$(curl -s "${AUTH_ARGS[@]+"${AUTH_ARGS[@]}"}" "${SCHEMASETVAULT_URL}${SCHEMASET_ID}/schemas" 2>/dev/null || echo "[]") # Iterate over registered schemas for i in "${!REGISTERED_IDS[@]}"; do @@ -224,6 +257,7 @@ else fi RESPONSE=$(curl -s -w "\n%{http_code}" \ + "${AUTH_ARGS[@]+"${AUTH_ARGS[@]}"}" \ -X POST "${SCHEMASETVAULT_URL}${SCHEMASET_ID}/schemas" \ -H "Content-Type: application/json" \ -d "{\"SchemaId\": \"$SCHEMA_ID\"}" \ diff --git a/src/ContentProcessorAPI/samples/upload_files.ps1 b/src/ContentProcessorAPI/samples/upload_files.ps1 index e5feda97..d78ed3e6 100644 --- a/src/ContentProcessorAPI/samples/upload_files.ps1 +++ b/src/ContentProcessorAPI/samples/upload_files.ps1 @@ -6,7 +6,12 @@ param ( [string]$FolderPath, [Parameter(Mandatory = $true)] - [string]$SchemaId + [string]$SchemaId, + + # Optional bearer token used when the API has authentication enabled. + # Acquire with: az account get-access-token --resource api:// --query accessToken -o tsv + [Parameter(Mandatory = $false)] + [string]$AccessToken ) # Validate if the folder exists @@ -21,6 +26,13 @@ Add-Type -AssemblyName System.Net.Http # Create an HttpClient instance $httpClient = New-Object System.Net.Http.HttpClient +# Attach the bearer token when provided so uploads succeed against an +# authentication-protected API. +if ($AccessToken) { + $httpClient.DefaultRequestHeaders.Authorization = ` + New-Object System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", $AccessToken) +} + # Function to determine the MIME type based on file extension function Get-MimeType { param ([string]$FileName) diff --git a/src/ContentProcessorAPI/samples/upload_files.sh b/src/ContentProcessorAPI/samples/upload_files.sh index 480ff1f2..fff9e832 100755 --- a/src/ContentProcessorAPI/samples/upload_files.sh +++ b/src/ContentProcessorAPI/samples/upload_files.sh @@ -1,8 +1,8 @@ #!/bin/bash # Check if the correct number of arguments is provided -if [ "$#" -ne 3 ]; then - echo "Usage: $0 " +if [ "$#" -lt 3 ] || [ "$#" -gt 4 ]; then + echo "Usage: $0 [ACCESS_TOKEN]" exit 1 fi @@ -10,6 +10,15 @@ fi API_ENDPOINT_URL=$1 FOLDER_PATH=$2 SCHEMA_ID=$3 +# Optional bearer token, from the 4th positional arg or the ACCESS_TOKEN env var, +# used when the API has authentication enabled. Acquire with: +# az account get-access-token --resource api:// --query accessToken -o tsv +ACCESS_TOKEN="${4:-${ACCESS_TOKEN:-}}" + +AUTH_ARGS=() +if [ -n "$ACCESS_TOKEN" ]; then + AUTH_ARGS=(-H "Authorization: Bearer $ACCESS_TOKEN") +fi # Validate if the folder exists if [ ! -d "$FOLDER_PATH" ]; then @@ -34,6 +43,7 @@ for FILE in "$FOLDER_PATH"/*; do # Invoke the API with multipart/form-data RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" -X POST "$API_ENDPOINT_URL" \ + "${AUTH_ARGS[@]+"${AUTH_ARGS[@]}"}" \ -H "Content-Type: multipart/form-data" \ -F "file=@$FILE;filename=$FILENAME" \ -F "data=$DATA_JSON")