ADR-0005 — sync-users files participants in the group the declaration administers
Date: 2026-09-10 Status: superseded by ADR-0007.
sync-users still reads admin_permissions and still files participants into every group
it names — the behaviour is untouched. Nothing declares one any more, so it files them
into none, and /participants stops being refilled. The group is left in the realm with
its existing members; it authorises nothing, and removing it is an operator's deliberate
act with no deadline.
Context
ADR-0003 scopes a service account's realm administration to the members of one group. A participant outside that group is invisible to the service: it can neither find the account nor act on it.
sync_users creates users with the REC organization's membership, the org viewers group,
and whatever --group paths were passed. None of that is the administered group, so every
participant this repository provisions from a REC registry lands outside the grant.
../onboarding then attempts to create the same person, Keycloak answers
409 User exists with same username, the scan of the group does not find them, and the
enablement step fails naming an account that plainly exists. That failure is the agreed
behaviour on the consumer's side — it tells a human the account exists and is not in the
group — but nothing on this side could put it there.
The group is not the only per-participant container in play. This repository already
maintains a Keycloak organization per REC, and files each participant in it. That is
the natural per-community grouping, and it is unusable for this purpose: the Organizations
API is outside fine-grained admin permissions entirely, and answers 403 even to a service
account holding realm-wide Users: view + manage granted that way.
Decision
sync-users reads the admin_permissions block and adds every participant it processes
to the groups it declares. The paths come from the same declaration sync grants from,
so the command that fills a group and the command that grants over it cannot name different
groups. --no-admin-groups opts out.
It applies to accounts that already exist, not only to new ones, which makes a re-run the backfill for participants provisioned before the group was declared. No separate backfill command is owed.
The group must already exist. sync owns realm structure and creates the group when it
grants the permission; a missing group means the realm has not been synced from the
declaration sync-users just read, and creating it here would paper over that. It is
resolved before any user is touched, the way --group already is, so a run does not stop
half way leaving some participants filed and some not.
Consequences
The administered group is flat, and it will hold every participant on the deployment.
This is the cost, it is known, and it was chosen deliberately over the alternative below.
Its sharp edge is the consumer's lookup: GET /groups/{id}/members ignores search
(ADR-0004), so finding one participant is a paged scan of the whole group.
Per-organization groups were measured before deciding, and cost one Keycloak permission
per organization. A grant on /participants does not descend to
/participants/<org> for anything group-addressed — measured on 26.6.0 holding all seven
Groups scopes, and even on a child group the service account had just created itself:
POST /users into the child, group-by-path on it, GET /groups/{id} and
GET /groups/{id}/members are all 403. A permission on the child alone works completely.
Subgroup members are also not members of the parent — the parent's member list excludes
them, and ?subGroups=true changes nothing.
So the per-org shape is a different change rather than a larger version of this one:
clients.yaml does not know organizations and the REC registry does, so granting would move
from clients.yaml-driven sync to registry-driven sync-users, and the consumer would
have to resolve an organization's group rather than one fixed path. It needs its own ADR,
and the measurements are recorded in
celine-policies#6.
One thing does descend, and is worth not re-deriving: member-addressed calls. Reading,
updating and password-resetting a user who is only in /participants/<org> all succeed
under a grant on /participants, while the same calls on a member of an unrelated group
are 403. A parent grant can administer everyone beneath it; it cannot create into a child
or find anyone there.
A deployment that declares no admin_permissions sees sync-users behave exactly as
before — no group is resolved and no membership is added.