> 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.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.opendatadiscovery.org/configuration-and-deployment/enable-security/admin-promotion.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
