Azure / static-web-apps

Azure Static Web Apps. For bugs and feature requests, please create an issue in this repo. For community discussions, latest updates, kindly refer to the Discussions Tab. To know what's new in Static Web Apps, visit https://aka.ms/swa/ThisMonth
https://aka.ms/swa
MIT License
318 stars 53 forks source link

Azure Functions that ship with Static Web Apps cannot be configured to access Key Vault #1474

Open johnnyreilly opened 1 month ago

johnnyreilly commented 1 month ago

Describe the bug

Opening for visibility as #1091 was closed without explanation and the feature is still not available.

Having failed to use Key Vault references with the Azure Function that ships with Azure Static Web Apps, I tried to access the Key Vault directly and could not.

See #1090

To Reproduce

Use the same config as described in #1090, but this time pass through what's necessary to access a Key Vault; ClientId / TenantId / Client Secret:

module staticWebApp 'static-sites.bicep' = {
  name: '${deploymentPrefix}-how-you-doin-${branchHash}'
  params: {
    appSettings: {
      // THESE WILL WORK FINE
      AAD_CLIENT_ID: aadClientIdRef
      AAD_CLIENT_SECRET: aadClientSecretRef
    }
    functionAppAppSettings: {
      APPINSIGHTS_INSTRUMENTATIONKEY: appInsights.properties.InstrumentationKey
      APPLICATIONINSIGHTS_CONNECTION_STRING: appInsights.properties.ConnectionString

      // These are the settings that are used to access the key vault
      AZURE_TENANT_ID: aadTenantId
      AZURE_CLIENT_ID: aadClientId
      AZURE_CLIENT_SECRET: aadClientSecret

      keyVaultName: keyVaultName
    }
    location: location
    repositoryBranch: repositoryBranch
    repositoryUrl: repositoryUrl
    staticSiteName: staticSiteName
    tags: tagsWithHiddenLinks
    customDomainName: (repositoryBranch == 'main') ? customDomainName : ''
  }
}

Create an Azure Function which does something like this:

export async function getFromKeyVault(
    secretNames: string[],
    log: Logger
): Promise<Map<string, string>> {
    const credential = new DefaultAzureCredential();
    const secrets = new Map<string, string>();

    const url = `https://${getConfig().keyVaultName}.vault.azure.net`;

    const client = new SecretClient(url, credential);

    for (const secretName of secretNames) {
        // Read the secret we created
        const secret = await client.getSecret(secretName);
        log("secret: ", secret);
        secrets.set(secretName, secret.value);
    }

    return secrets;
}

const httpTrigger: AzureFunction = async function (
    context: Context,
    req: HttpRequest
): Promise<void> {
    try {
            const secrets = await getFromKeyVault(
                ["telemetryDbKey"],
                context.log
            );
            context.log("secrets", secrets);
    } catch (error) {
        context.log("unexpected error", error, req);
        context.res = {
            status: 500,
            body: "There was an error",
        };
    }
};

It will error out like this at runtime:

problem reading from keyvault RestError: Caller is not authorized to perform action on resource.
If role assignments, deny assignments or role definitions were changed recently, please observe propagation time.
Caller: appid=586bc697-2c6b-40f3-b91d-2ee65e5d4c7a;oid=92521cf5-b763-4b0e-ac50-1060dca0095d;iss=https://sts.windows.net/6d6a11bc-469a-48df-a548-d3f353ac1be8/
Action: 'Microsoft.KeyVault/vaults/secrets/getSecret/action'
Resource: '/subscriptions/5b380244-4d89-4218-a9ad-e6295602f103/resourcegroups/rg-azure-engineering-metrics-dev-001/providers/microsoft.keyvault/vaults/kv-kv-2mb9k5bwz8-dev/secrets/telemetrydbkey'
Assignment: (not found)
Vault: kv-kv-2mb9k5bwz8-dev;location=westeurope

There appears to be no way to provide this RBAC access to the built in function. The same RBAC access has been supplied to the SWA:

// Allow the static web app to read from the Key Vault
// Allow the service connection and admin group to create secrets
module keyVaultRoles 'br:icebox.azurecr.io/bicep/ice/composites/key-vault/role-assignments:v1.0' = {
  name: '${deploymentPrefix}-roles-${branchHash}'
  params: {
    keyVaultName: keyVault.outputs.keyVaultName
    roleAssignments: [
      {
        roleDefinitionId: keyVaultSecretsOfficerRole
        principalId: serviceConnectionPrincipalId
        principalType: 'ServicePrincipal'
      }
      {
        roleDefinitionId: keyVaultSecretsUserRole
        principalId: staticWebApp.outputs.staticWebAppSystemAssignedIdentityId // ACCESS PROVIDED HERE
        principalType: 'ServicePrincipal'
      }
    ]
  }
}

Expected behavior

The values are returned from the vault.

Screenshots

image

Device info (if applicable): N/A

Additional context Happy to demo this issue to you.