Legacy employee sync deployment notes

Earlier deployment approach for the Azure employee sync Function App. Follow the current setup guide for deployment.

0. Confirm the runtime version

PowerShell 7.6 is currently preview-only and Windows-only (Premium/Dedicated/Consumption). Flex Consumption on Linux currently runs PowerShell 7.4. Check the Function App's FUNCTIONS_WORKER_RUNTIME_VERSION app setting — if it's not ~7.4, this code will still run fine (nothing here is 7.6-specific) but confirm before you deploy so there are no surprises at runtime resolution.

1. Vendor the Az.Accounts module

Flex Consumption does not support PowerShell managed dependencies, so requirements.psd1 alone will not fetch the module.

Run this on a machine with PowerShell 7.4+ and internet access, from the function app root:

Save-Module -Name Az.Accounts -Path ./Modules -Repository PSGallery

This creates ./Modules/Az.Accounts/<version>/.... That folder must be included in the deployment package (zip deploy) alongside host.json, profile.ps1, requirements.psd1, and HttpTrigger/. Final layout:

/
  host.json
  profile.ps1
  requirements.psd1
  HttpTrigger/
    function.json
    run.ps1
  Modules/
    Az.Accounts/
      <version>/...

2. Enable a system-assigned managed identity on the Function App

Portal: Function App → Identity → System assigned → On.

3. Grant that identity two RBAC roles

Resource Role Why
kv-cia-employee-sync Key Vault Secrets User Lets the Key Vault reference app setting resolve ciaETLsecret
Storage Account Storage Table Data Contributor Lets the function read/write the Employees table and read the OU mapping table, via Entra ID auth (no account keys)

4. Create and seed the organisationUnit mapping table

Create a second table in the same storage account — OrganisationUnitMappings by default (override with the OuMappingTableName app setting if you want a different name). Every row uses a fixed PartitionKey so the function's lookup is always a single-partition query:

PartitionKey RowKey MappedGuid
Mapping GRO <guid>
Mapping ABC <guid>

RowKey is the 3-character prefix (uppercase — the function uppercases the incoming value before comparing, so casing in the source data doesn't matter). Add a row per prefix; any organisationunit value that doesn't match one gets logged as a warning and left unmapped rather than written with a blank/null id, so unmapped records are easy to find in the logs without failing the whole batch.

Seed it however's convenient — Storage Explorer's table editor is the quickest for a handful of rows, or Add-AzTableRow-style REST calls if you want it scripted.

5. App settings to configure on the Function App

Setting Value
CiaEtlSecret @Microsoft.KeyVault(SecretUri=https://kv-cia-employee-sync.vault.azure.net/secrets/ciaETLsecret/)
TableStorageAccountName Your storage account name, e.g. mystorageacct
EmployeeTableName Employees (or your preferred table name — must already exist)
OuMappingTableName OrganisationUnitMappings (optional — this is the default if unset)

Neither table is auto-created — create both once via the portal, Storage Explorer, or New-AzStorageTable.

6. Testing the bearer token

Auth is now a straightforward comparison — the ETL sends Authorization: Bearer <secret>, the Function compares it (constant-time) against ciaETLsecret from Key Vault. To sanity check before wiring up the real ETL step:

$uri    = 'https://<functionapp>.azurewebsites.net/api/employees/sync'
$secret = 'the-actual-secret-value'   # from Key Vault, for local testing only
$body   = Get-Content ./sample-payload.xml -Raw

# Should succeed (200) with a summary of upserted/skipped/failed counts
Invoke-RestMethod -Method Post -Uri $uri -Headers @{ Authorization = "Bearer $secret" } -ContentType 'application/xml' -Body $body

# Should fail with 401 — confirms the check actually rejects bad tokens, not just accepts everything
try {
    Invoke-RestMethod -Method Post -Uri $uri -Headers @{ Authorization = 'Bearer wrong-secret' } -ContentType 'application/xml' -Body $body
} catch {
    $_.Exception.Response.StatusCode
}

If the first call doesn't come back 200, $_.ErrorDetails.Message in a catch block will show the JSON error body (Missing or malformed Authorization header, Invalid token, Could not load organisationUnit mapping table, etc.) rather than a generic HTTP error.

7. Endpoint

POST https://<functionapp>.azurewebsites.net/api/employees/sync (no function key required — the bearer token check is the actual gate; consider still restricting via authLevel + a function key as defense-in-depth if you want belt-and-suspenders).