> For the complete documentation index, see [llms.txt](https://docs.opendatadiscovery.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.opendatadiscovery.org/configuration-and-deployment/enable-security/admin-promotion.md).

# Admin promotion across providers

ODD Platform has **four top-level authentication modes** (`DISABLED`, `LOGIN_FORM`, `OAUTH2`, `LDAP`), and the `OAUTH2` mode supports **six provider sub-shapes** (AWS Cognito, GitHub, Google, Azure AD, ODD\_IAM, plus a Custom OIDC fallback that Okta and Keycloak deployments use today). Whether a given user becomes `ADMIN` on login depends on the mode, the provider, and a handful of configuration knobs whose names look the same across providers but **behave differently per provider runtime**.

This page is the single reference for that divergence. Use it to (1) understand what a configured value will actually do on your provider, (2) plan migrations between auth modes without losing or accidentally widening ADMIN grants, and (3) pick a provider for a new deployment based on the precision of the admin grant.

Per-provider operational details live on the provider's own page; this page focuses on the comparison.

## Admin-promotion matrix

| #   | Mode / provider                         | Admin-detection mechanism                                                                                                                              | Match semantic                                            | Notable caveat                                                                                                                                                                                                                                                                                    |
| --- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1   | `LDAP`                                  | `auth.ldap.groups.admin-groups` against the user's LDAP group memberships                                                                              | **case-insensitive, full group-name match**               | A token must equal a group name in full (case ignored): `admin-groups: [ops]` promotes members of `ops` / `Ops` / `OPS` but **not** `devops` or `team-ops`. If `admin-groups` is empty, every user is `USER` with no path to ADMIN                                                                |
| 2   | `OAUTH2` → AWS Cognito                  | `admin-groups` (against the `cognito:groups` claim) and `admin-principals` (email / configured username attribute)                                     | case-insensitive, full match                              | The match ignores case (`Admins` and `admins` both match) but requires the whole value — it is not a substring match                                                                                                                                                                              |
| 3   | `OAUTH2` → GitHub                       | `admin-principals` (login) **before** the `organization-name` check; `admin-groups` (team name) **after**, against `/user/teams`                       | case-insensitive, full match (both principals and groups) | `admin-principals` bypasses `organization-name`; a team token must equal a team name in full (e.g. `admins` does **not** match `team-admins`); GitHub Enterprise Server unsupported (`api.github.com` hard-coded)                                                                                 |
| 4   | `OAUTH2` → Google                       | `admin-principals` (email by default; overridable via `admin-attribute`)                                                                               | case-insensitive, full match                              | The match ignores case but requires the whole value. **`admin-groups` is a silent no-op** — the field binds without error but is never read by the Google handler                                                                                                                                 |
| 5   | `OAUTH2` → Azure AD                     | `admin-principals` (against `admin-attribute`); `admin-groups` against the `roles` claim by default (override to `groups` via `groups-claim`)          | case-insensitive, full match                              | The match ignores case but requires the whole value — it is not a substring match                                                                                                                                                                                                                 |
| 6   | `OAUTH2` → ODD\_IAM                     | userinfo flag from the IAM provider                                                                                                                    | flag-based                                                | —                                                                                                                                                                                                                                                                                                 |
| (7) | `LOGIN_FORM`                            | None — every authenticated user is granted `ADMIN` authority unconditionally                                                                           | —                                                         | The `SECURITY_RULES` table is **not consulted** in this mode; authored Policies do not gate the live auth chain. See [Login form → Authorization posture](/configuration-and-deployment/enable-security/authentication/login-form.md#authorization-posture-under-login_form).                     |
| —   | Okta, Keycloak, any other OIDC provider | Custom OIDC handler — evaluates `admin-principals` always, and `admin-groups` **only if** you set `groups-claim`                                       | case-insensitive, full match                              | `admin-principals` promotes at login with no extra configuration. `admin-groups` is inert unless `groups-claim` is set, because this handler has no default groups claim. If neither is configured, every user is `USER` and ADMIN promotion is a manual Owner-Role binding via the Management UI |
| —   | `DISABLED`                              | Synthetic admin — `GET /api/identity/whoami` returns `{ username: "admin", permissions: [ … every Permission enum value … ] }` to any anonymous caller | —                                                         | The permission set is `Permission.values()` so a new platform capability is automatically granted. See [Disabled authentication → What's anonymously reachable](/configuration-and-deployment/enable-security/authentication/disabled-authentication.md#whats-anonymously-reachable).             |

## Provider-specific gotchas

Each item below points at the page that owns the operational detail; this list is the cross-cutting summary so an operator scanning for "what bites me on each provider" has one reading surface.

* **LDAP group-name match is full-string + groups-unset silent USER-only.** `admin-groups: [ops]` promotes members of a group named `ops` (case ignored), but **not** `devops` or `team-ops` — the token must equal the group name in full. An empty `admin-groups` silently leaves the deployment with no LDAP ADMIN path. See [LDAP → Admin promotion](/configuration-and-deployment/enable-security/authentication/ldap.md#admin-promotion-group-name-matching).
* **GitHub `admin-principals` bypasses `organization-name`.** A login in `admin-principals` is granted `ADMIN` regardless of organization membership — a typo or stale entry that matches an attacker-registerable GitHub login is a platform-ADMIN backdoor. See [OAuth — GitHub](/configuration-and-deployment/enable-security/authentication/oauth2-oidc.md#github).
* **GitHub `admin-groups` is a full team-name match.** Same shape as LDAP — `admin-groups: [admins]` promotes members of a team named `admins` (case ignored), but **not** `team-admins`, `admins-readonly`, or `data-admins`. Enter each admin team's name exactly as it appears in GitHub. See [OAuth — GitHub](/configuration-and-deployment/enable-security/authentication/oauth2-oidc.md#github).
* **GitHub Enterprise Server not supported.** `api.github.com` is hard-coded in the GitHub handler with no override knob; GHES deployments cannot use the GitHub provider today. See [OAuth — GitHub](/configuration-and-deployment/enable-security/authentication/oauth2-oidc.md#github).
* **Google `admin-groups` is a silent no-op.** The field binds without error but the Google handler never reads it; promote with `admin-principals` or `admin-attribute`. See [OAuth — Google](/configuration-and-deployment/enable-security/authentication/oauth2-oidc.md#google).
* **Okta / Keycloak / Custom OIDC do detect `admin-principals`, but `admin-groups` needs `groups-claim`.** The generic Custom OIDC handler promotes a login whose principal is listed in `admin-principals` (case-insensitive, full match). It reads `admin-groups` only when you also set `groups-claim`, because this handler has no default groups claim. With neither set, every login is `USER` and promotion is manual via the Management UI. See [OAuth — Other OIDC providers](/configuration-and-deployment/enable-security/authentication/oauth2-oidc.md#other-oidc-providers).
* **`LOGIN_FORM` SECURITY\_RULES are inert.** Every authenticated user is `ADMIN`; the Policy/Role table you author via Management UI does not gate the live auth chain in this mode. See [Login form → Authorization posture](/configuration-and-deployment/enable-security/authentication/login-form.md#authorization-posture-under-login_form).
* **`DISABLED` synthetic admin is anonymous.** No credentials needed; `Permission.values()` dynamically expands so new capabilities auto-enter the grant. See [Disabled authentication → What's anonymously reachable](/configuration-and-deployment/enable-security/authentication/disabled-authentication.md#whats-anonymously-reachable).

## Migration-mode notes

When you move between auth modes, the admin-detection rules change underneath you. The notes below name the most common transitions:

* **`DISABLED` → any production mode (`LOGIN_FORM`, `OAUTH2`, `LDAP`).** The synthetic-admin response from `/api/identity/whoami` vanishes — anonymous callers stop seeing ADMIN. Before flipping, read [Disabled authentication → Migrating away from DISABLED](/configuration-and-deployment/enable-security/authentication/disabled-authentication.md#migrating-away-from-disabled) and audit any `Owner` rows whose `OIDC_USERNAME` matches the literal `admin` or `ADMIN` (collision with the synthetic identities). Any RBAC `Policy` or `Role` you authored while DISABLED was active was never consulted — they take effect for the first time after the migration.
* **`LOGIN_FORM` → `OAUTH2` or `LDAP`.** Policies become consulted for the first time (the LOGIN\_FORM chain omits the `AuthorizationCustomizer`). Test the production-mode Policy table BEFORE migrating; under LOGIN\_FORM every user was ADMIN so Policy bugs were invisible. Cross-mode username collisions surface — see the [cross-mode user-name collision](/configuration-and-deployment/enable-security/authentication/login-form.md#cross-mode-user-name-collision-activity-feed-read-paths) note.
* **`LDAP` → `OAUTH2`.** `admin-groups` matching is full group-name equality (case ignored) under both LDAP and every OAuth2 provider, so the matching semantic does not change. What changes is the source of the names: LDAP matches your directory's group names, GitHub matches GitHub team names, Cognito matches `cognito:groups`, Azure matches the `roles` (or `groups`) claim. Re-enter `admin-groups` using the new provider's group names; the LDAP group names will not match unless they happen to be identical.
* **Adding S2S to any production mode.** The S2S filter's synthetic principal is literal uppercase `ADMIN`. If you have an existing user named `ADMIN`, that user's owner mapping becomes the S2S caller's owner — see [S2S → Operator caveats](/configuration-and-deployment/enable-security/authentication/s2s.md#operator-caveats).

## Choosing a provider (decision guide)

For a new deployment, the table below summarises the trade-offs operators most often weigh. Across every mode and provider, `admin-groups` / `admin-principals` matching is **case-insensitive, full-value equality** — a configured token grants ADMIN only when it equals a group name (or principal) in full, with case ignored. There is no substring or prefix matching, so a short token like `ops` cannot accidentally promote `devops`. The columns instead compare what is most worth weighing per provider — whether `admin-groups` works at all on that provider, and whether the platform revokes the token at the IdP on sign-out. **Logout revocation** is whether the platform asks the IdP to revoke the access token on sign-out — see the [logout token-revocation matrix](/configuration-and-deployment/enable-security/authentication/oauth2-oidc.md#logout-token-revocation-matrix) for details.

All providers match `admin-groups` / `admin-principals` the same way — case-insensitive, full-value equality. The column below therefore compares which knobs each provider actually honours, not match precision.

| Mode / provider                          | Admin grant by                                              | Logout revocation                                          | Best fit for                                         |
| ---------------------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------- |
| `LOGIN_FORM`                             | n/a — every user is ADMIN                                   | n/a — local session only                                   | Dev / demo over HTTPS terminating at a trusted proxy |
| `LDAP`                                   | `admin-groups` (group names)                                | n/a — bind-only auth                                       | Existing LDAP / Active Directory shop                |
| `OAUTH2` → AWS Cognito                   | `admin-principals` + `admin-groups`                         | **no** (the platform does not call `/oauth2/revoke` today) | AWS-first deployments; pair with short token TTL     |
| `OAUTH2` → GitHub                        | `admin-principals` + `admin-groups` (team names)            | **yes**                                                    | github.com-hosted (not GHES) teams                   |
| `OAUTH2` → Google                        | `admin-principals` only (`admin-groups` is a no-op)         | **yes**                                                    | Google Workspace organisations                       |
| `OAUTH2` → Azure AD                      | `admin-principals` + `admin-groups`                         | **no** (Azure v2.0 lacks RFC 7009 — protocol-level)        | Microsoft 365 / Entra ID shops                       |
| `OAUTH2` → ODD\_IAM                      | userinfo flag from the IAM provider                         | **no**                                                     | Vendor-managed ODD deployments                       |
| `OAUTH2` → Okta / Keycloak / Custom OIDC | `admin-principals` (+ `admin-groups` if `groups-claim` set) | **no**                                                     | Existing OIDC IdPs                                   |
| `DISABLED`                               | n/a — anonymous synthetic admin                             | n/a                                                        | Local-only dev; **never** for any reachable network  |

If your security posture requires IdP-level token revocation on logout, **Google** and **GitHub** are today's best matches. If you grant ADMIN by group membership, every mode except Google supports `admin-groups` (Custom OIDC requires `groups-claim` to be set). For any production deployment, audit the trade-offs above against your incident-response model before choosing.
