Documentation menu

"Example: auto-grant opes:access (Entra IDP migration)"

Port the v1 prefilRegisterFromEntraId auto-grant to a v2 user.human.added Event.

The v1 prefilRegisterFromEntraId script does two things:

  1. Maps SAML attributes from Entra IDP into Zitadel user fields
  2. Auto-grants opes:access on opes-hr so the new user can sign into opes

The v2 migration splits these:

  • (1) SAML attribute mapping moves to Zitadel's native IDP attribute mapping in the Console (no code) — search docs for "External IDP claims" under Default Settings → Identity Providers
  • (2) Auto-grant becomes this script:

Save in /admin/actions:

  • Trigger type: EVENT
  • Trigger name: user.human.added
// Auto-grant opes:access on every new user creation.
// Idempotent: safely re-runnable (checks existing grants first).

const OPES_HR_PROJECT_ID = '371501187121396417';
const GATE_ROLE = 'opes:access';

const userId = ctx.payload.aggregateID ?? ctx.payload.userID;
if (!userId) {
  ctx.log.warn('no userId in event_payload', { payload: ctx.payload });
  return null;
}

const grants = await ctx.zitadel.users.getGrants(userId);
const alreadyHas = grants.some(
  (g) => g.projectId === OPES_HR_PROJECT_ID && g.roleKeys.includes(GATE_ROLE),
);

if (alreadyHas) {
  ctx.log.info('user already has opes:access; skipping', { userId });
  return null;
}

await ctx.zitadel.users.appendGrant(userId, OPES_HR_PROJECT_ID, [GATE_ROLE]);
ctx.log.info('granted opes:access', { userId });
return { granted: true };

Difference from v1: scope

The v1 script gates on ctx.v1.externalUser.externalIdpId === "261432977777494558" (only Entra-sourced users). The v2 Event fires for all new users — including console-created and SAML-sourced. Decision point:

  • If all OMMAX users SHOULD have opes:access (current effective policy): no gating needed; this v2 version is correct as-is
  • If you want to keep Entra-only: add a check via ctx.zitadel.users.get(userId) to inspect IDP links — but this requires the user-created event to carry that info, which it may not

OMMAX-effective policy is "everyone gets it" today, so the v2 version above is appropriate.

Cutover plan

  1. Configure SAML attribute mapping in Zitadel UI (Default Settings → Identity Providers → Entra IDP) — this replaces the firstname/lastname/email/department-setting branch of v1
  2. Save the script above as DRAFT, promote to ACTIVE
  3. Wire Execution: condition event: { event: "user.human.added" } → Target atrium-actions-events
  4. Test by adding a fresh user in Zitadel — verify the event fires and the script appends the grant
  5. Disable v1 prefilRegisterFromEntraId in Zitadel Console
  6. Edge case: any users created during the cutover window may end up missing opes:access. Run a backfill query if needed.

Why this lives in Atrium and not in Zitadel-adjacent infra

Unlike flatRoles (which fires on every token issue and is latency-critical), this fires once per new hire (~weekly). Atrium's runtime is the right home — Atrium being slow doesn't break sign-ins, just delays the welcome grant by minutes.