Documentation menu

Directory sync (Entra)

How Atrium reads group membership from Microsoft Entra as a connector, why it stays read-only today, and the least-privilege design for ever writing back.

Atrium keeps its own groups — but it doesn't have to be the source of who's in them. The directory connector lets a super-admin point Atrium at an external identity directory (Microsoft Entra today, Google Workspace and others later) and mirror group membership from there. Entra stays the system of record; Atrium reflects it and turns it into access.

This is a different path from the one on the architecture page. There, Entra sits behind Zitadel as a federated login IdP and Atrium reads a few claims (name, email, department) when someone signs in. The directory connector is a direct, app-only link to the Graph API — it reads whole groups and their rosters on a schedule, independent of anyone logging in.

Where it sits among the connectors

Everything Atrium talks to on the outside is a connector: a (capability, provider) pair with encrypted credentials, a test button, and a setup guide, all managed by a super-admin under /admin/integrations.

CapabilityWhat it fillsProviders today
identitythe grant/login store (source of truth for identity)Zitadel
directorythe group source of record (who is in which group)Microsoft Entra
emailtransactional mailPlunk, SMTP
llm-gatewaymodel access + budgetsLiteLLM

identity and directory are deliberately separate. Zitadel owns who you are; the directory connector owns which teams you're in. Splitting them is what lets an org move group storage off the Atrium database and onto their own Entra tenant without touching login.

How the Entra connector works today

  1. A super-admin registers an Entra application and configures the connector with the tenant ID, client ID and a client secret (encrypted at rest, AES-256-GCM). Test connection proves Atrium can read Graph before anything trusts it.
  2. An admin links an Atrium group to an Entra group. From then on, that Atrium group's membership is a mirror of the Entra one.
  3. On every Sync now (and, later, on a schedule) the reconciler fetches the Entra roster, resolves each member to a user by email, and updates the Atrium group. It only ever touches rows it created — anyone added by hand stays put. Departments are the exception: they materialise automatically from the sign-in department claim, so they're never mirrored a second time.
  4. Membership changes flow downstream exactly like any other Atrium membership change — through the same provenance-gated reconciler described in Groups → Reconciliation. Atrium only ever revokes a grant it can prove it created; everything else is surfaced as drift, never silently removed.

Read-only, on purpose. The connector holds exactly three Microsoft Graph application permissions — Group.Read.All, GroupMember.Read.All, User.Read.All — and nothing that can write. Entra is the source of truth; Atrium reads it. That single fact removes a whole class of "the tool changed my directory" incidents from the audit conversation. The full click-path (register app → add the three permissions → grant admin consent → create a secret → paste the three identifiers) lives inline in the connector's setup guide at /admin/integrations.

Two shapes of Entra group

Entra groups come in two flavours, and the difference decides how Atrium can interact with them.

  • Dynamic groups — membership is computed from a rule over user attributes (e.g. user.department -eq "AI"). Nobody adds or removes members by hand; you change the attribute and Entra recomputes. These are the cleanest thing to mirror: Atrium reads them and never needs to write, because the rule already governs membership. Department groups are the canonical case, and they already arrive via the login claim.
  • Assigned (static) groups — membership is an explicit list. Someone (or something) puts people in and takes them out.

Reading either is identical. The distinction only matters the moment Atrium wants to grant access by changing Entra — covered next.

Extending safely: writing back to Entra

Status: design, not yet shipped. Everything above is live and read-only. This section is the agreed target design for the day Atrium needs to push access into Entra, and the guardrails it will ship with.

The temptation is "let Atrium put people into a dynamic group." Entra doesn't allow it, and that's the right instinct wearing the wrong hat. Two facts shape the design:

  • You cannot write a dynamic group's membership — it's computed. The only lever is the attribute the rule reads. Rules can key on extensionAttribute1–15 or directory-extension properties, but writing those needs User.ReadWrite.All — permission to write every field on every user. That is the opposite of least privilege.
  • Custom security attributes — the one Entra thing with per-attribute-set RBAC — are explicitly not usable in dynamic-membership rules. So the elegant "scope my writes to one attribute" model can't drive group membership. You get scoped writes or dynamic membership, not both on the same object.

So the design splits by who owns the driving attribute, which happens to match how Atrium already works:

  • Attribute owned elsewhere → read-only dynamic mirror. Department is the live example: it flows in from the sign-in claim, membership is 100% attribute-driven, and Atrium needs zero write permission. Keep it that way.
  • Atrium is the grantor → assigned group, penned inside an Administrative Unit. For access Atrium actively hands out, use an assigned Entra group whose membership Atrium owns. To make "cannot write everything" enforceable by Entra rather than by hope: place those groups in a dedicated Administrative Unit and give the app an AU-scoped Groups Administrator role plus tenant-wide Directory.Read.Allnot the tenant-wide GroupMember.ReadWrite.All. A service principal with an AU-scoped role can manage members of only that AU's groups; every other group in the tenant is untouchable. That is the least-privilege guarantee, written into Entra's own authorization model.

Audit posture

The whole shape is built to be explainable to an auditor in one sitting:

  • Least privilege, enforced not promised. Read today is three read-only Graph scopes. Write, when it comes, is bounded to one Administrative Unit — the app cannot reach anything outside it.
  • Separation of duties. Identity (Zitadel) and group source (Entra) are distinct connectors with distinct credentials. Registering the Entra app is a Global-Administrator action; running the connector is a super-admin action; they need not be the same person.
  • Provenance-gated writes. Downstream, Atrium only ever revokes grants it can prove it created. Anything it can't prove is preserved and shown as drift, so an over-eager sync can't quietly strip access.
  • Full trail. Every membership resolution and grant mutation is written to the run log / grant audit table; a Sync now is reconstructable after the fact.
  • Reversible. Disable the connector and the mirror stops; Entra, the source of truth, is unchanged.

Operating notes

  • Secret rotation. The client secret has a 12–24 month expiry set at registration. Diarise a rotation before it lapses — an expired secret makes every Graph call return 401, the connector's last-run status flips to ERROR, and the next sync surfaces the same error before it changes anything.
  • Email is the join key. Members are matched to Atrium users by mail / userPrincipalName. A member with no matching Atrium user is skipped and logged, not invented.
  • Nested groups aren't expanded. The roster read filters to direct user members; a group nested inside an Entra group is not recursed. Model nesting on the Atrium side (Groups) if you need it.
  • Consent is required. App-only permissions do nothing until an admin clicks Grant admin consent — until then every permission row reads "Not granted" and Test connection fails fast.