# PRD: SSO Authentication on Coordinators ## Problem Statement Today, Memgraph coordinators (the Raft-based HA coordinator role) skip authentication entirely: any Bolt client that connects is accepted with an empty user, and username/password are neither validated nor meaningful. There is no way to require that a client proves its identity against a corporate identity provider (IdP) before talking to a coordinator, even though the same organizations that run SSO against data instances expect their tooling (e.g. Memgraph Lab, admin scripts) to authenticate the same way against coordinators. At the same time, coordinators have no concept of roles, so there is nothing for an SSO identity to map onto. And two pieces of dead surface — the `COORDINATOR` privilege and the `EnableWritingOnMainRpc` — add confusion without providing behavior. ## Solution Enable SSO (OIDC, SAML, Kerberos) authentication on coordinators, backed by a small, Raft-replicated set of roles, each carrying a coordinator privilege (`COORDINATOR_READ` or `COORDINATOR_WRITE`). From the user's perspective: - Connecting to a coordinator with **basic auth (username + password), no credentials, or any scheme not listed in `--auth-module-mappings` succeeds as a passthrough** whenever SSO is not in effect — credentials are ignored, and the session has full `COORDINATOR_WRITE` access. Nothing about existing tooling breaks, including clients that send a data-instance SSO scheme to a coordinator without SSO. Once SSO is configured (`--auth-module-mappings` non-empty), the enterprise license is valid, **and** a `COORDINATOR_WRITE` role exists, the passthrough is denied so the credential-less path cannot bypass the SSO privilege model. If the license is missing, expired, or invalid, SSO itself rejects every login, and without a writable role SSO can admit no administrator, so the passthrough **stays open** (break-glass) — a license transition or an empty role set never locks every Bolt session out of a coordinator, and the break-glass session can re-install the license over Bolt (`SET DATABASE SETTING "enterprise.license" TO ...`) or create the first writable role. - Connecting with an **SSO scheme** (one listed in `--auth-module-mappings`) now runs the corresponding auth module and **actually authenticates**: the connection is accepted only if the IdP token is valid and every role the module returns already exists on the coordinator; otherwise the connection is rejected. SSO on coordinators works the same way for SAML and Kerberos as it does for OIDC. - An authenticated SSO session is **restricted by its role privileges**. Coordinators have exactly two privileges: **`COORDINATOR_READ`** (routing-table read, `SHOW COORDINATOR SETTINGS`, `SHOW INSTANCE`, `SHOW INSTANCES`, `SHOW REPLICATION LAG`, and other read-only introspection) and **`COORDINATOR_WRITE`** (everything runnable on a coordinator; `COORDINATOR_WRITE` is a superset of `COORDINATOR_READ`). A session's effective privilege is the union of its roles' privileges. (These are new privilege names chosen because bare `READ`/`WRITE` already exist as data-instance fine-grained privileges.) - Administrators manage roles with **`CREATE ROLE`**, **`DROP ROLE`**, **`SHOW ROLES`**, assign privileges with **`GRANT COORDINATOR_READ|COORDINATOR_WRITE TO `** / **`REVOKE`**, and inspect them with **`SHOW PRIVILEGES FOR ROLE `**. **`GRANT ALL PRIVILEGES TO `** grants both coordinator privileges and **`REVOKE ALL PRIVILEGES FROM `** removes both. These queries work on any coordinator: if run on a follower they are transparently forwarded to the leader. The role set and its privileges are persisted through the Raft log, so they survive restarts, follower catch-up, and leader failover. - All other auth queries remain **rejected** on coordinators: users/passwords, `DENY` in any form, `GRANT DATABASE` / multi-tenancy database access, fine-grained access control (label/edge-type entity privileges), and property-permission grants. - SSO, role management, privilege grants, and privilege enforcement on coordinators are **enterprise features** (require a valid license); basic-auth passthrough remains free and retains full `COORDINATOR_WRITE` access, so community/unlicensed deployments are unaffected. Because basic auth is license-free, the deny-basic-under-SSO rule above is itself conditioned on a valid license: with SSO configured but no valid license, basic auth still works (break-glass) while SSO logins are rejected with a license error. Separately, this work removes two unused constructs — the `COORDINATOR` privilege and `EnableWritingOnMainRpc` — to reduce confusion. ## User Stories 1. As an operator, I want to connect to a coordinator with a username and password and succeed whenever SSO is not in effect (not configured, or configured without a valid license), so that my existing admin tooling keeps working unchanged. 2. As a security engineer, I want SSO clients to be rejected on a coordinator when they present an invalid IdP token, so that only authenticated identities reach the cluster control plane. 3. As a security engineer, I want an SSO client to be rejected when the IdP returns a role that does not exist on the coordinator, so that misconfigured IdP group mappings fail loudly instead of silently granting access. 4. As a security engineer, I want an SSO client whose IdP response lists multiple roles to be accepted only when all of those roles exist on the coordinator, so that partial misconfiguration is caught. 5. As an operator using OIDC, I want to authenticate to a coordinator with my OIDC token, so that I use the same corporate identity I use for data instances. 6. As an operator using SAML, I want SAML authentication on coordinators to behave exactly like OIDC, so that my SAML deployment is a first-class citizen. 7. As an operator using Kerberos, I want Kerberos authentication on coordinators to behave exactly like OIDC, so that my Kerberos deployment is a first-class citizen. 8. As an administrator, I want to run `CREATE ROLE ` on a coordinator, so that an SSO identity mapping to that role can authenticate. 9. As an administrator, I want to run `DROP ROLE ` on a coordinator, so that I can revoke a role that SSO identities map to. 10. As an administrator, I want to run `SHOW ROLES` on a coordinator, so that I can see the exact set of roles the coordinator will accept from SSO. 11. As an administrator, I want `CREATE ROLE`/`DROP ROLE`/`SHOW ROLES` run on a follower coordinator to be automatically forwarded to the leader, so that I don't have to find and reconnect to the leader myself. 12. As an administrator, I want `CREATE ROLE` on an already-existing role to error (unless I pass `IF NOT EXISTS`), so that role management behaves the same as on data instances. 13. As an administrator, I want `DROP ROLE` on a non-existent role to error, so that role management behaves the same as on data instances. 14. As an administrator, I want the coordinator role list to survive a full-cluster restart, so that authentication configuration is durable. 15. As an administrator, I want a coordinator that was down while roles changed to catch up to the exact role set when it rejoins, so that all coordinators agree on who can authenticate. 16. As an administrator, I want the role list to survive a leader failover, so that a change of leadership does not lose authentication configuration. 17. As an administrator, I want all non-role auth queries (`CREATE USER`, `SET PASSWORD`, `SHOW USERS`, `SET ROLE`, `DENY` in any form, `GRANT DATABASE`, fine-grained access control, property-permission grants, …) to be rejected on coordinators, so that the coordinator's auth surface stays minimal and predictable. 18. As an administrator, I want to grant a coordinator role either `COORDINATOR_READ` or `COORDINATOR_WRITE` with `GRANT COORDINATOR_READ|COORDINATOR_WRITE TO `, so that I can control what an SSO identity mapping to that role may do. 19. As an administrator, I want `GRANT ALL PRIVILEGES TO ` to grant both coordinator privileges and `REVOKE ALL PRIVILEGES FROM ` to remove both, so that I can set a role to full access (or clear it) in one statement. 20. As an administrator, I want to remove a granted privilege with `REVOKE COORDINATOR_READ|COORDINATOR_WRITE FROM `, so that I can tighten access without dropping the role. 21. As an administrator, I want `SHOW PRIVILEGES FOR ROLE ` to report a coordinator role's granted privilege, so that I can audit and debug access. 22. As a security engineer, I want a `COORDINATOR_READ`-only SSO session to read the routing table and run the read/introspection queries but be denied every mutating query, so that least-privilege access to the control plane is enforced. 23. As a security engineer, I want a `COORDINATOR_WRITE` SSO session to run every coordinator query (`COORDINATOR_WRITE` being a superset of `COORDINATOR_READ`), so that operators with write access are not additionally blocked from reads. 24. As a security engineer, I want an SSO session whose role has no privilege granted to be denied everything (including the routing table) until an admin grants a coordinator privilege, so that roles never confer implicit access. 25. As a security engineer, I want a multi-role SSO session to receive the union of its roles' privileges, so that combined roles behave predictably. 26. As an operator, I want basic-auth sessions to keep full `COORDINATOR_WRITE` access, so that existing admin tooling that connects with username/password is never locked out by the new privilege model. 27. As a security engineer, I want fine-grained access control queries (label/edge-type entity privileges, property permissions) and `GRANT DATABASE` to be rejected on coordinators, so that coordinators never appear to accept data-plane access control they cannot honor. 28. As an administrator, I want SSO login for a given role to be rejected before I create that role and to succeed after I create it, so that role provisioning has an observable, verifiable effect. 29. As an operator, I want the coordinator to inherit my `MEMGRAPH_SSO_*` environment variables into the auth module, so that I configure SSO on coordinators exactly as I do on data instances. 30. As an operator, I want to configure the coordinator's SSO scheme-to-module mappings with `--auth-module-mappings`, so that I control which schemes and modules the coordinator uses. 31. As an operator running a rolling upgrade, I want a role or privilege query forwarded to a not-yet-upgraded leader to fail with an error rather than crash the coordinator, so that a mixed-version window degrades safely. 32. As an operator without an enterprise license, I want SSO auth, role queries, privilege grants, and enforcement on coordinators to be gated by a license error while basic-auth still works with full access — **even when SSO is configured** — so that licensing is consistent with the rest of auth and a license expiry never locks every Bolt session out of a coordinator (basic auth is the break-glass through which the license can be re-installed). 33. As a developer, I want the unused `COORDINATOR` privilege removed, so that the privilege model does not advertise a permission that gates nothing. 34. As a developer, I want the unused `EnableWritingOnMainRpc` removed, so that the RPC surface reflects what is actually called. 35. As a developer, I want a dedicated e2e suite for coordinator authentication, so that every scenario above is protected against regressions. 36. As a developer, I want Kerberos exercised through the real coordinator Bolt handshake (not just as an isolated Python unit test), so that the coordinator SSO path has genuine Kerberos coverage. ## Implementation Decisions ### Auth semantics on coordinators - **Everything but a listed SSO scheme is a passthrough**: `basic`/`none`, and any scheme not present in `--auth-module-mappings`, succeed on a coordinator with credentials ignored (current behavior preserved, including for clients that send a data-instance SSO scheme to a coordinator without SSO), **except** when SSO is configured (`--auth-module-mappings` non-empty), the enterprise license is valid, and a `COORDINATOR_WRITE` role exists in the committed role set, in which case they are denied. No license required. The deny condition deliberately mirrors what the SSO path needs to admit an administrator: passthrough is denied only while SSO can actually authenticate someone with write privileges, so there is always at least one working auth path on a coordinator (an unreachable leader leaves the role set unknown and is fail-closed). A denied unlisted scheme gets a message naming the scheme, distinct from the basic/none message. - **SSO authenticates**: for a scheme present in `--auth-module-mappings`, run the auth module for that scheme, then validate the returned roles. Authentication **succeeds only if every returned role exists** in the coordinator's role list. Invalid token or any missing role → **connection rejected**. - **Privilege gating (supersedes the earlier no-gating decision)**: coordinators have exactly two privileges, **`COORDINATOR_READ`** and **`COORDINATOR_WRITE`**, where `COORDINATOR_WRITE` is a superset of `COORDINATOR_READ`. A READ-classified query requires `COORDINATOR_READ` or `COORDINATOR_WRITE`; a WRITE-classified query requires `COORDINATOR_WRITE`. A basic-auth passthrough session has full `COORDINATOR_WRITE` (backward compatible). An SSO session's effective privilege is the **union** of the masks of its matched roles; a role with no grant confers nothing, so such a session is denied everything (including the routing table) until granted. - **Query classification**: READ = routing-table read, `SHOW COORDINATOR SETTINGS`, `SHOW INSTANCE`, `SHOW INSTANCES`, `SHOW REPLICATION LAG`, and all other read-only introspection (`SHOW ROLES`, `SHOW PRIVILEGES FOR ROLE`, `SHOW CONFIG`, `SYSTEM INFO`, the SHOW form of settings). WRITE = every mutating/admin query (`REGISTER`/`UNREGISTER INSTANCE`, `SET INSTANCE TO MAIN`, `DEMOTE INSTANCE`, `ADD`/`REMOVE COORDINATOR`, `YIELD LEADERSHIP`, `SET COORDINATOR SETTING`, `UPDATE CONFIG`, `FORCE RESET CLUSTER STATE`, `RELOAD SSL`, the SET form of settings, `CREATE ROLE`, `DROP ROLE`, `GRANT`, `REVOKE`). - **Enterprise-gated**: SSO authentication, the role queries, privilege grants (`GRANT`/`REVOKE`), `SHOW PRIVILEGES FOR ROLE`, and privilege enforcement require `MG_ENTERPRISE` + a valid license; without it they are rejected with a license error. Basic-auth passthrough works in any build and retains full `COORDINATOR_WRITE` — including when SSO is configured but the license is missing/expired/invalid (break-glass): denying basic there too would leave no successful auth path on any coordinator Bolt session, recoverable only by restarting with `--auth-module-mappings` cleared. - The coordinator branch of the Bolt handshake's `AuthenticateUser` is changed from "ignore auth on coordinators" to: SSO scheme in mappings → the dedicated coordinator SSO path; every other scheme (basic, none, or unlisted) → passthrough, unless SSO is configured, the license is valid, **and** a `COORDINATOR_WRITE` role exists, in which case deny (with a scheme-naming message for an unlisted scheme). ### Coordinator SSO authenticator (deep module) - A **dedicated coordinator SSO path** lives in the session/glue layer and shares as little as possible with the data-instance `Auth` path. It reuses the existing auth-module subprocess machinery (scheme→path mappings, pipe protocol) to run the module, but the role-existence check is performed against `CoordinatorState`'s committed role list rather than the auth kvstore. - The authenticator is shaped as a testable unit: given a scheme and an IdP response, it produces an authenticated/rejected result plus an **effective privilege mask** (the union of the matched roles' masks), using an injected module-runner and an injected role/mask provider over the coordinator role set. - On success, the session carries that effective mask; the coordinator privilege check consults the session mask, **not** the auth kvstore (coordinator roles live only in Raft). - `MEMGRAPH_SSO_*` variables are the module's contract and are inherited by the spawned module subprocess. **No new C++ env parsing** is added on coordinators. ### Coordinator privilege model & enforcement - Two new privileges are introduced (an `AuthQuery::Privilege` value and a `Permission` bit each): **`COORDINATOR_READ`** and **`COORDINATOR_WRITE`**, added to the grammar `privilege` rule. These names are required because bare `READ`/`WRITE` already exist as data-instance fine-grained privileges. The previously-removed `COORDINATOR` privilege stays removed (its reserved bit is not reused). Granting them is a coordinator-only carve-out and they are not meaningful on data instances. - Roles carry a permission bitmask. The coordinator carve-out reuses the existing `grantPrivilege`/`revokePrivilege` grammar but **inspects the content**: it accepts only `ALL PRIVILEGES` or a privilege list containing solely `COORDINATOR_READ`/ `COORDINATOR_WRITE`. `GRANT ALL PRIVILEGES TO ROLE ` grants **both** coordinator privileges; `REVOKE ALL PRIVILEGES FROM ROLE ` removes **both**. `SHOW PRIVILEGES FOR ROLE ` reports the role's privilege (the trailing `ON MAIN|CURRENT|DATABASE` clause is rejected — coordinators have no database). `SHOW ROLES` stays name-only. - **Explicitly rejected** on coordinators, even though they share grammar with the accepted forms: `DENY` in any form; a privilege list containing any non-coordinator privilege; fine-grained access control (the `entityPrivilegeList` label/edge-type forms); the property-permission grants; and `GRANT/DENY/REVOKE DATABASE` (multi-tenancy database access). These raise the coordinator-not-allowed error. - Enforcement of Cypher coordinator queries reuses the existing required-privilege extraction + `CheckAuthorized` path, comparing the query's required privilege against the session's effective mask (`COORDINATOR_WRITE` satisfies a `COORDINATOR_READ` requirement). - The **routing table** is a Bolt `ROUTE` message that bypasses the query privilege path, so an explicit `COORDINATOR_READ` check is added in the ROUTE handler against the session's effective mask. ### Roles as Raft-replicated state - A new field holding a **`vector<{name, privilege-mask}>`** (a small role struct: role name plus its coordinator permission bitmask) is added to `CoordinatorClusterState` and is the **sole source of truth** for coordinator roles and their privileges. There are no user records stored on coordinators; the auth kvstore is not used for coordinator roles. - The field is added through the full established pattern: member + default, all special member functions, `operator==` tie-lists, a new optional in `CoordinatorClusterStateDelta`, getter/setter, `to_json`/`from_json` (full-state snapshot), `DoAction`, `SerializeUpdateClusterState`/`DecodeLog` (per-log delta), and a new key constant. - **No `LogStoreVersion` bump.** Backward/forward compatibility is achieved the same way as the most recent fields: `add_if_set` when serializing the delta, a `contains`-guard in `DecodeLog`, and `j.value(key, {})` (default empty) in `from_json`. An older coordinator ignores the unknown key; a newer coordinator reading an older log/snapshot sees an empty role set. ### Role & privilege query handling and forwarding - The interpreter's coordinator query gate is extended to permit exactly `CREATE ROLE`, `DROP ROLE`, `SHOW ROLES`, `GRANT (COORDINATOR_READ|COORDINATOR_WRITE|ALL PRIVILEGES) TO ROLE `, `REVOKE (COORDINATOR_READ|COORDINATOR_WRITE|ALL PRIVILEGES) FROM ROLE `, and `SHOW PRIVILEGES FOR ROLE ` (carved out of the auth-query rejection by inspecting the auth-query sub-action and privilege content). All other auth queries — including `DENY`, FGAC/entity privileges, property permissions, and `GRANT DATABASE` — remain rejected on coordinators with the existing coordinator-only-queries error. - Semantics **match data instances**: `CREATE ROLE x` errors if `x` already exists, is a no-op with `IF NOT EXISTS`; `DROP ROLE x` errors if `x` does not exist. - New coordinator→coordinator RPCs are introduced for the write operations (create role, drop role, grant, revoke) and read operations (get roles, get role privileges). - **All role/privilege writes forward to the leader** via the existing `ForwardToLeader` mechanism. On the leader, each write builds a `CoordinatorClusterStateDelta` carrying the updated role set and commits it through `AppendLogAndWaitForCommit`. `SHOW ROLES` and `SHOW PRIVILEGES FOR` are also forwarded to the leader (strong read). ### Mixed-version behavior (rolling upgrade) - **No RPC version negotiation.** If a new follower forwards a role/privilege RPC to a not-yet-upgraded leader that has no handler for it, the RPC **fails and surfaces an error** to the client. - This path must **not** trigger `MG_ASSERT` or otherwise crash the coordinator; the receive/dispatch and forwarding code must handle a missing handler / RPC failure by returning an error. - The cluster-state field remains safe across versions via the serialization pattern above. ### Removals - **`COORDINATOR` privilege** is removed from the `Permission` enum (its bit is left reserved and not reused), from `kPermissionsAll`, from the permission stringification, from the grammar **privilege rule** (the `COORDINATOR` lexer keyword is kept — it is used by `ADD COORDINATOR`, `SHOW COORDINATOR SETTINGS`, etc.), from the visitor mapping, from the AST privilege list, from the query→permission mapping, and from the required-privileges attachment for `CoordinatorQuery`. **No migration** is added; a stale persisted bit in an existing deployment is simply never reported and never checked. - **`EnableWritingOnMainRpc`** is removed entirely: type alias, request/response structs and their Save/Load, the server-handler registration and its handler implementation, the `RpcInfoSpecialize` entry, and the Prometheus counters. The underlying `ReplicationState::EnableWritingOnMain()` method is **kept** (still used; writing on a promoted main is enabled via the `writing_enabled` flag inside `PromoteToMainRpc`). ### Out-of-scope confirmations baked into the design - Password masking behavior is unchanged (see Out of Scope). - SSO-driven auto-creation of roles (readonly/readwrite/admin) is not implemented on coordinators — roles must be created explicitly. ## Testing Decisions Good tests here assert **external behavior** — the outcome a client or operator observes (connection accepted/rejected, `SHOW ROLES` output, query error, role set after restart/failover) — not internal call sequences or private data structures. Tests should remain valid if the implementation is refactored, as long as behavior holds. ### Unit tests - **Coordinator role store**: add/drop/list operations on `CoordinatorClusterState`, plus a serialization roundtrip for both the per-log delta and the full-state snapshot (encode → decode → identical role set **including each role's privilege mask**, and the empty / backward-compatible-missing-key cases). This directly protects the Raft-persisted `vector<{name, mask}>` code path in isolation. Prior art: existing `CoordinatorClusterState` / coordinator state-machine unit tests. - **Auth-query gate**: assert that the coordinator gate permits exactly `CREATE ROLE`, `DROP ROLE`, `SHOW ROLES`, `GRANT`/`REVOKE COORDINATOR_READ|COORDINATOR_WRITE|ALL PRIVILEGES`, and `SHOW PRIVILEGES FOR ROLE`, and rejects all other auth queries — including `DENY`, `GRANT DATABASE`, FGAC/entity privileges, and property permissions — at unit level where feasible. Prior art: existing interpreter/required-privileges tests. - **Effective-privilege computation**: union of masks across multiple roles; the authorization check semantics (`COORDINATOR_READ` requirement satisfied by either privilege; `COORDINATOR_WRITE` requirement needs `COORDINATOR_WRITE`; a bare role confers nothing); and that `ALL PRIVILEGES` maps to both coordinator privileges. The role/privilege RPCs and the coordinator SSO authenticator are covered by the e2e suite rather than dedicated unit tests. ### End-to-end tests — `tests/e2e/high_availability/auth.py` Shaped like `distributed_coords.py` (driven by `interactive_mg_runner`, `ADD COORDINATOR` wiring) but with **3 coordinators and no data instances**. A dedicated dummy auth module covering OIDC, SAML, and Kerberos schemes (canned success roles + error cases) is driven through the **real coordinator Bolt handshake**. The new test is registered in the directory's `workloads.yaml` and `CMakeLists.txt`. Functional buckets: - **Auth passthrough**: with no SSO module configured, username/password and an SSO-style scheme not in mappings both succeed and the session can run coordinator queries. With SSO configured, a valid license, and a writable role, basic/none is denied and an unlisted scheme is denied with a message naming it. With SSO configured but the license revoked at runtime, or no `COORDINATOR_WRITE` role in the committed role set, the passthrough stays open (break-glass) for basic/none and unlisted schemes alike: the session retains full `COORDINATOR_WRITE`, can re-install the license over Bolt or create the first writable role, and the cluster returns to SSO-only access — no restart, no total lockout. - **SSO across all three schemes + failures**: OIDC, SAML, Kerberos each succeed when the returned role(s) exist; rejection on invalid token, on a missing role, and multi-role where all-exist succeeds and any-missing rejects. Kerberos is exercised through the Bolt handshake (closing the "no server-path coverage" gap). - **Role CRUD + follower forwarding**: `CREATE`/`DROP`/`SHOW ROLE` on the leader; the same run on a follower and transparently forwarded; duplicate → error, `IF NOT EXISTS` → no-op, `DROP` missing → error; forwarded `SHOW ROLES`. - **Privilege management**: `GRANT COORDINATOR_READ|COORDINATOR_WRITE TO ROLE ` reflected in `SHOW PRIVILEGES FOR ROLE `; `GRANT ALL PRIVILEGES` grants both and `REVOKE ALL PRIVILEGES` removes both; `REVOKE` clears a single privilege; a bare role shows no privilege; grants/revokes forwarded from a follower; `GRANT`/`REVOKE`/`SHOW PRIVILEGES` rejected for non-role auth targets. - **Privilege enforcement (via SSO)**: a `COORDINATOR_READ`-only SSO session can read the routing table and run the read/introspection queries but is denied every mutating query; a `COORDINATOR_WRITE` SSO session can run everything; a bare-role SSO session is denied everything including the routing table; basic-auth retains full `COORDINATOR_WRITE`. - **Disallowed queries + SSO/role tie-in**: `CREATE USER`, `SET PASSWORD`, `SHOW USERS`, `SET ROLE`, `DENY` (any form), `GRANT DATABASE` / `SET MAIN DATABASE`, fine-grained access control (`GRANT ... ON NODES/EDGES ...`), and property-permission grants are all rejected on coordinators; SSO login for a role rejected before `CREATE ROLE` and succeeding after. Persistence / recovery: - **Full-cluster restart**: create roles, restart all coordinators, `SHOW ROLES` still returns them (durable log/snapshot replay). - **Lagging-follower catch-up**: kill a follower, mutate roles on the leader, restart the follower, assert it reconstructs the exact role set (replication + apply on rejoin). - **Leader failover**: create roles, kill the leader, let a new leader be elected, assert the new leader's role set matches (committed state survives leadership change). Prior art: `tests/e2e/high_availability/distributed_coords.py` for cluster shape and lifecycle; `tests/e2e/sso/` (`test_sso.py`, `test_oidc.py`, `test_saml_sso_module.py`, `dummy_sso_module.py`) for the mock-module structure and IdP-response fixtures. ## Out of Scope - **Password masking changes**: current behavior is intentionally left as-is — enterprise + valid license masks the password in the query log; community and unlicensed-enterprise builds are unchanged. No masking is added to failed/slow/session-trace/audit sinks, and the syntax-error/client-facing path is untouched. - **Privileges beyond `COORDINATOR_READ`/`COORDINATOR_WRITE`**: coordinators support only the two coarse privileges; the fine-grained data-instance privilege set is not modeled on coordinators. (This supersedes the earlier "no privilege gating on coordinators" decision.) - **SSO auto-provisioning of roles**: the data-instance-style scenario where SSO implies built-in roles (readonly/readwrite/admin) is explicitly not implemented on coordinators. - **User management on coordinators**: no users or passwords are stored on coordinators; the only persisted auth state is the role set and each role's READ/WRITE mask. - **Real KDC/SPNEGO Kerberos integration**: Kerberos is tested through the coordinator Bolt handshake with a dummy module, not against a live KDC. - **RPC version negotiation / capability detection** for the new role RPCs. ## Further Notes - Two items are deliberately deferred to implementation because they are mechanism-reuse questions, not product decisions: the exact reuse of the existing coordinator RPC registration/dispatch for the new role RPCs (ensuring a missing handler returns an error rather than asserting), and the precise driver/connection mechanism the e2e uses to send SSO tokens over Bolt (mirroring `tests/e2e/sso`). - No new gflags are introduced (`--auth-module-mappings` already exists), so `tests/e2e/configuration/default_config.py` does not need updating. - Lock ordering and the `CoordinatorClusterState` update checklist from the repo guidelines must be respected when adding the `roles_` field.