PRD: Provision APL users into Dex
Nobody has claimed this yet.
- Dominant language
- Go Template
- Stars
- 2.3k
- Forks
- 186
- Avg merge
- 3d 11h
- Merged PRs (30d)
- 66
Description
## Problem Statement
APL users cannot log in to Dex. Only the platform administrator can, because that one account is written into Dex's configuration as a single known entry.
Users are created through the API, which stores each one as a SealedSecret in the `apl-users` namespace. Under Keycloak they reach the issuer at runtime: the Keycloak operator watches those Secrets, decodes each record, derives group membership from the administrator flags and team list, and pushes the user into the realm. Dex has no equivalent path.
Rendering users into Dex's configuration — the original plan for this PRD — turned out not to be the right shape either. Upstream Dex's gRPC `Password` message carries no `groups` field, so provisioning via the gRPC API looked closed off; but `storage.Password` has always had a `Groups` field, the gap is only in the message the API exposes. We patched that gap ourselves rather than working around it, and that patch is now the whole design of this PRD.
## Solution
`apl-api` provisions users directly into Dex over its gRPC API — `CreatePassword`, `UpdatePassword`, `DeletePassword` — instead of apl-core rendering a config file.
This runs on our fork of Dex, which adds a `groups` field to the `Password` message and threads it through `server/apiserver/passwords.go` into `storage.Password`. The patch is submitted upstream as dexidp/dex#4972 and not yet merged; until it is, APL's Dex image is built from the fork (see ADR-2026-08-06 and #3536).
Whenever a user is created, updated, deleted, or has their team membership change, `apl-api` calls Dex directly with `email`, bcrypt `hash`, `username`, `user_id`, and `groups`. There is no render step in apl-core for this and nothing for the Helmfile pipeline to generate — Dex's state changes immediately, and nothing restarts. Group membership is derived the same way it always was — from the stored administrator flag and team list — but computed by `apl-api` at call time rather than reproduced later during apply.
The password hash is still computed once by `apl-api`, when it holds the plaintext — at user creation or password change — because bcrypt is salted and must not be recomputed. `apl-core` needs a backfill for records created before the hash field existed, same as before.
`user_id` is set explicitly on `CreatePassword` to the APL user's existing UUID, so the token subject is stable and repository-derived rather than Dex-allocated.
## Acceptance Criteria
1. **GIVEN** a user is created through the API, **WHEN** the record is stored, **THEN** it carries a bcrypt password hash in the variant Dex accepts, and the plaintext is not derivable from it.
2. **GIVEN** a user's password is changed through the API, **WHEN** the record is updated, **THEN** the stored hash matches the new password, and `apl-api` calls Dex's `UpdatePassword` with the new hash.
3. **GIVEN** a stored user record without a hash, **WHEN** it is next processed, **THEN** a hash is derived from the stored initial password and persisted, so the record is upgraded once rather than on every pass.
4. **GIVEN** a user is created through the API, **WHEN** the record is saved, **THEN** `apl-api` calls Dex's `CreatePassword` with email, hash, username, `user_id`, and the derived groups, and the call succeeds before the API request returns.
5. **GIVEN** a user who is a platform administrator, a team administrator, or a member of teams, **WHEN** their groups are computed for the Dex call, **THEN** they match what the Keycloak operator produces for the same record today.
6. **GIVEN** a user's team membership changes, **WHEN** the change is saved, **THEN** `apl-api` calls Dex's `UpdatePassword` with the new `groups`, and the change is effective immediately with no Dex restart.
7. **GIVEN** a user, **WHEN** they authenticate against Dex, **THEN** the token carries their group membership and the platform grants the roles that membership implies.
8. **GIVEN** a user is deleted, **WHEN** the deletion is saved, **THEN** `apl-api` calls Dex's `DeletePassword`, and they can no longer authenticate.
9. **GIVEN** the issuer is Keycloak, **WHEN** a user is created, updated or deleted, **THEN** nothing about user provisioning changes — the Dex calls only fire when Dex is the configured issuer.
10. **GIVEN** a call to Dex's gRPC API fails, **WHEN** `apl-api` handles the error, **THEN** the user record is not left silently inconsistent between the values repository and Dex — the failure is surfaced and retryable.
## Testing
- Unit tests for hash creation and update in the API, and for the backfill, including that an existing hash is not recomputed.
- Unit tests for the Dex gRPC client in `apl-api`: `CreatePassword`/`UpdatePassword`/`DeletePassword` payload shape, group derivation for each combination of administrator flags and team membership, and error handling when the call fails.
- Integration test against a running Dex (built from our fork) for the full create → authenticate → update-groups → delete cycle.
- Criterion 7 needs a cluster and a real login, since it concerns token claims rather than a mocked API response.
- Criterion 9 needs coverage on a Keycloak-issuer cluster to confirm the Dex client is never invoked.
## Out of Scope
- Self-service password change. `UpdatePassword` needs no restart, so this becomes possible without further Dex-side work, but no client for it is built here.
- Rendering users into Dex's configuration file. Superseded by the gRPC approach above.
- Reload-on-configuration-change for Dex (#3539) — not needed for user changes any more, only for team/OAuth2-client config.
- Federated clusters, where users come from an external identity provider and are never stored by APL.
- Migrating users from an existing Keycloak cluster.
- Upstreaming or merging our Dex fork — tracked separately against dexidp/dex#4972.
## Further Notes
- This work spans two repositories: the gRPC client and hash handling live in `apl-api`; the fork the client depends on lives in the Dex image apl-core deploys (#3536). Neither half is useful alone.
- The group derivation to match is in the Keycloak operator, which builds `platform-admin`, `team-admin` and `team-` entries from the stored flags. Reproducing it exactly is what keeps authorization identical across a change of issuer.
- The user identifier drives the token subject, so it must be the stable record identifier rather than anything editable such as an email address. Changing it later re-orphans accounts in applications that key on the subject.
- If dexidp/dex#4972 merges upstream, the fork can be dropped and APL moves to stock Dex without any change to `apl-api`'s calls — the gRPC contract is what we're upstreaming, not a private extension we keep long-term.
## Dependencies
Depends on #3536 — there is no Dex to call until the chart, and its fork-built image, exist.
Blocks #3537.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the apl-api Dex gRPC client requirements and the fork entry points named in server/apiserver/passwords.go and storage.Password, then review dependency #3536. Use the specified hash, client, integration, cluster, and Keycloak-issuer tests to validate the acceptance criteria; done means create, update, group changes, authentication, deletion, failures, and issuer selection remain consistent.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- grpc, helm, kubernetes
- Domain
- authentication, authorization, backend-api-design, devops
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100