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.
Permissions required, by integration
Section titled “Permissions required, by integration”| 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. |
Routed Airflow Key Vault access
Section titled “Routed Airflow Key Vault access”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.RoutedAzureKeyVaultBackendAIRFLOW__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.
Dashboard deployment configuration
Section titled “Dashboard deployment configuration”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.tsmaps each WorkOSorgIdto exactly one SharePointsiteId(stored in thevisory-dashboard-vdr-org-site-mapKey Vault secret). Every VDR route resolvessiteIdserver-side from the caller’s ownorgId— the client never supplies asiteId, 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 insrc/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.Selectedalready 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.
Global admin (multi-org) access model
Section titled “Global admin (multi-org) access model”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.
Resource role assignments
Section titled “Resource role assignments”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 explicitlyREVOKE 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.
Verify read-only status
Section titled “Verify read-only status”SELECT grantee, privilege_typeFROM information_schema.table_privilegesWHERE 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/DELETECleanup: 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.
Testing after granting RBAC
Section titled “Testing after granting RBAC”cd apps/mx_fuels_reporting_app && make down && make js
curl http://localhost:3000/api/balance # PostgreSQLcurl http://localhost:3000/api/documents/smoke # Graph/VDRcurl -X POST http://localhost:3000/api/azure/assembleReporteVolumenes \ -F "fileType=xml" -F "file=@test.xml" # Storage + Service Bus