Skip to content

RBAC & Identity Reference

For the conceptual identity model (service principal vs. UAMI vs. developer identity), see Architecture → Identity & Security. This page is the operational reference: exact permissions, grant commands, and cleanup steps.

Resource Used by Required role/grant
Azure Storage Blob /api/azure/assembleReporteVolumenes, /api/azure/backfillMatchData Storage Blob Data Contributor on the storage account
Azure Service Bus Same routes — sends messages to trigger Airflow DAGs Azure Service Bus Data Sender on the namespace
PostgreSQL Flexible Server /api/balance (v_conservation_of_mass) Database role named after the identity name, with CONNECT + USAGE + SELECT only (read-only)
Databricks SQL Warehouse Not currently used by any route CAN_USE (only needed if/when inventory features land)
Microsoft Graph (SharePoint/VDR) /api/documents* Sites.Selected application permission on the visory-dashboard app registration — not Sites.Read.All. Each client SharePoint site must be individually granted via the Graph site-permissions API as new client orgs onboard; this is per-site and ongoing, not a one-time setup step.

Airflow uses one custom secrets backend to read from two Azure Key Vaults. The existing pipeline secrets remain in visory-dev-scus-ds-kv without a variable prefix. Regulator credential variables with the cne-regulator- prefix are read from the dedicated CNE credential vault.

The backend class is mx_fuels_reporting.secrets_backend.RoutedAzureKeyVaultBackend. It delegates normal variables, connections, and configuration values to the existing Azure Key Vault backend. It routes variable names by a configured prefix. A new vault needs only a new route entry; the Python backend does not need a code change.

Configure the Astro deployment with:

AIRFLOW__SECRETS__BACKEND=mx_fuels_reporting.secrets_backend.RoutedAzureKeyVaultBackend
AIRFLOW__SECRETS__BACKEND_KWARGS={"ds_vault_url":"https://visory-dev-scus-ds-kv.vault.azure.net/","ds_variables_prefix":"","variable_vaults":[{"prefix":"cne-regulator-","vault_url":"https://visory-dev-scus-cne-kv.vault.azure.net/","variables_prefix":""}]}

Install the Microsoft Azure Airflow provider in the Astro runtime. Set the Azure authentication values through Astro environment settings. Do not place credentials in the repository.

Grant the Airflow identity read access to both vaults. Grant the dashboard identity a custom Key Vault data-plane role containing only Microsoft.KeyVault/vaults/secrets/setSecret/action on visory-dev-scus-cne-kv. The role is named Key Vault Secrets Writer and has role definition ID e52bc23c-0d4e-4b88-92aa-a309aa8e9f72. Do not use Key Vault Secrets Officer, because it also permits reads. Assign the custom role only at the CNE vault scope. Do not grant the dashboard identity read access to the CNE vault. See the Microsoft Key Vault RBAC guide for built-in roles and custom-role guidance.

The DAG continues to use Variable.get() for CNE credentials. No DAG code should create a second Key Vault client or bypass the secrets backend. The dashboard stores the CNE Masivos API key for each company as cne-regulator-{company-id}-api-key-base64 in the CNE vault. The routed backend supplies it only to Airflow.

The dashboard Key Vault client uses WEB_KEY_VAULT_URL for existing application secrets and CNE_CREDENTIALS_KEY_VAULT_URL for regulator credential writes. The CNE URL is required when the credential upload route runs. The dashboard never reads CNE secret values.

The Container Apps deployment workflow sets both URLs with az containerapp update --set-env-vars:

WEB_KEY_VAULT_URL=https://visory-dev-scus-web-kv.vault.azure.net/
CNE_CREDENTIALS_KEY_VAULT_URL=https://visory-dev-scus-cne-kv.vault.azure.net/

These values identify the vaults and are not secrets. Keep credential values in Key Vault. The same workflow sets AZURE_CLIENT_ID to the attached UAMI client ID. The dashboard UAMI needs read access to the web vault and the documented write-only custom role on the CNE vault.

VDR site access model (Graph permission + app-level RBAC)

Section titled “VDR site access model (Graph permission + app-level RBAC)”

Sites.Selected only controls which sites the visory-dashboard app can technically reach at all — it says nothing about which authenticated user is allowed to see which site. That distinction is enforced entirely in application code, not in Azure AD:

  • Client-org users: src/lib/documents/orgSiteMapping.ts maps each WorkOS orgId to exactly one SharePoint siteId (stored in the visory-dashboard-vdr-org-site-map Key Vault secret). Every VDR route resolves siteId server-side from the caller’s own orgId — the client never supplies a siteId, so a client user cannot address another org’s site. Folder-level visibility within that one site is a further RBAC layer (allowedFolders / userFolderGrants, enforced in src/lib/documents/accessPolicy.ts).
  • Visory users (cross-company access): Visory staff (VISORY_ORG_ID) need to work across multiple client sites, but not every Visory user should see every client’s VDR (staff are typically assigned to a subset of clients). Sites.Selected already supports this — the app identity can hold grants to as many client sites as needed simultaneously; scoping which Visory user can select which client is handled entirely in the app’s RBAC layer.

Visory staff holding the global admin role can have memberships in multiple WorkOS organizations. Org selection and switching uses WorkOS’s native authenticateWithRefreshToken({ organizationId }) primitive rather than a custom cookie or query param, and role is always resolved fresh from the currently active, just-authenticated session’s membership — never cached across an org switch. Which orgs a given admin can switch into is governed entirely by their existing WorkOS organization memberships (no separate app-level grant table): granting access to a new client org means creating/updating that org’s own membership via the existing PATCH /api/admin/users/[id] role-assignment flow, the same mechanism used for every other role grant in the app. This matches WorkOS’s own multi-tenant guidance (How to build flexible authorization for multi-tenant B2B SaaS): role is scoped per organization membership, not global to the user.

Assign Storage and Service Bus roles to the production portal UAMI. Grant equivalent roles to a developer’s Azure CLI identity only when local feature testing needs those services. Do not grant the old visory-dashboard service principal access unless a separate CI job explicitly uses it.

Verify each assignment at the narrowest resource scope with Azure Portal or az role assignment list. See Migration and Rollout for the current identity model and rollout sequence.

PostgreSQL: read-only roles by identity name

Section titled “PostgreSQL: read-only roles by identity name”

PostgreSQL Azure AD auth uses the identity name as the username and a short-lived access token as the password. Create one read-only database role for the production UAMI and one for each local developer who needs access:

SET aad_validate_oids_in_tenant = off;
SELECT * FROM pgaadauth_create_principal('visory-dev-scus-portal-uami', false, false);
SELECT * FROM pgaadauth_create_principal('your.email@domain.com', false, false);
GRANT CONNECT ON DATABASE analytics TO "visory-dev-scus-portal-uami", "your.email@domain.com";
GRANT USAGE ON SCHEMA public TO "visory-dev-scus-portal-uami", "your.email@domain.com";
GRANT SELECT ON v_conservation_of_mass TO "visory-dev-scus-portal-uami", "your.email@domain.com";
-- Enforce read-only explicitly
REVOKE INSERT, UPDATE, DELETE, TRUNCATE ON ALL TABLES IN SCHEMA public FROM "visory-dev-scus-portal-uami", "your.email@domain.com";
REVOKE CREATE ON SCHEMA public FROM "visory-dev-scus-portal-uami", "your.email@domain.com";

For local development, replace your.email@domain.com with the developer’s Azure AD UPN. Both the UAMI and developer roles use identity names, not client IDs or object IDs.

SELECT grantee, privilege_type
FROM information_schema.table_privileges
WHERE grantee IN ('visory-dev-scus-portal-uami', 'your.email@domain.com')
AND table_schema = 'public'
ORDER BY grantee, privilege_type;
-- Expected: only SELECT rows, no INSERT/UPDATE/DELETE

Cleanup: removing incorrectly-created roles

Section titled “Cleanup: removing incorrectly-created roles”

If a role was ever created with the wrong client ID (e.g. an orchestration identity accidentally granted dashboard access):

REVOKE ALL PRIVILEGES ON DATABASE analytics FROM "<WRONG_CLIENT_ID>";
REVOKE ALL PRIVILEGES ON SCHEMA public FROM "<WRONG_CLIENT_ID>";
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM "<WRONG_CLIENT_ID>";
DROP ROLE IF EXISTS "<WRONG_CLIENT_ID>";

Audit existing roles first with \du and cross-check any UUID-looking role names against the actual visory-dashboard app registration and UAMI client IDs (az ad sp list --display-name "visory-dashboard" --query "[0].appId" -o tsv, az containerapp identity show ... --query "userAssignedIdentities.*.clientId") before dropping anything.

Terminal window
cd apps/mx_fuels_reporting_app && make down && make js
curl http://localhost:3000/api/balance # PostgreSQL
curl http://localhost:3000/api/documents/smoke # Graph/VDR
curl -X POST http://localhost:3000/api/azure/assembleReporteVolumenes \
-F "fileType=xml" -F "file=@test.xml" # Storage + Service Bus