feat(proxy): groups.<name> synthetic claims + auto-managed admin group; deprecate admin claim #44

Merged
lee merged 1 commit from development_lee_admin_group into development 2026-05-05 11:19:49 +00:00
Member

The proxy now emits groups.<name> for every group an authenticated
user belongs to (transitively, via the same BFS that resolves
role-derived claims). Symmetric with users.<x> — both are facts
about the caller, both flow unconditionally, no per-route opt-in.
Backends that don't care about group membership simply don't reference
groups.* in their rules.

To make groups.admin immediately useful, the proxy seeds a reserved
group named admin at startup and keeps membership in sync with
users.is_admin:

  • users.add(is_admin=true) and users.update(is_admin=true) add the
    user to the admin group
  • users.update(is_admin=false) removes them
  • a startup backfill covers any pre-existing is_admin users
  • groups.remove_member rejects manual removal of an is_admin=true
    user from the admin group, so the invariant cannot be desynced
    by hand (demote via users.update instead)

The legacy admin synthetic claim is now marked DEPRECATED 2026-05.
It's still emitted whenever users.is_admin is true so existing rules
continue to match during the transition; new rules should use
groups.admin instead.

docs/claims.md updated to drop the per-route opt-in language and
document the admin group's semantics.

Tests:

  • admin group exists on a fresh server
  • admin user gets both admin and groups.admin
  • non-admin user in a custom group gets groups.<name> and nothing
    else admin-related
  • users.update(is_admin=false) drops both admin claims
  • groups.remove_member rejects desyncing the admin group
  • test_groups_crud updated to filter the admin group out of
    user-group counts
The proxy now emits `groups.<name>` for every group an authenticated user belongs to (transitively, via the same BFS that resolves role-derived claims). Symmetric with `users.<x>` — both are facts about the caller, both flow unconditionally, no per-route opt-in. Backends that don't care about group membership simply don't reference `groups.*` in their rules. To make `groups.admin` immediately useful, the proxy seeds a reserved group named `admin` at startup and keeps membership in sync with `users.is_admin`: - users.add(is_admin=true) and users.update(is_admin=true) add the user to the admin group - users.update(is_admin=false) removes them - a startup backfill covers any pre-existing is_admin users - groups.remove_member rejects manual removal of an is_admin=true user from the admin group, so the invariant cannot be desynced by hand (demote via users.update instead) The legacy `admin` synthetic claim is now marked DEPRECATED 2026-05. It's still emitted whenever users.is_admin is true so existing rules continue to match during the transition; new rules should use `groups.admin` instead. docs/claims.md updated to drop the per-route opt-in language and document the admin group's semantics. Tests: - admin group exists on a fresh server - admin user gets both `admin` and `groups.admin` - non-admin user in a custom group gets `groups.<name>` and nothing else admin-related - users.update(is_admin=false) drops both admin claims - groups.remove_member rejects desyncing the admin group - test_groups_crud updated to filter the admin group out of user-group counts
feat(proxy): groups.<name> synthetic claims + auto-managed admin group; deprecate admin claim
All checks were successful
Build & Test / check (push) Successful in 3m8s
Build & Test / check (pull_request) Successful in 2m33s
a9ba15ad1d
The proxy now emits `groups.<name>` for every group an authenticated
user belongs to (transitively, via the same BFS that resolves
role-derived claims). Symmetric with `users.<x>` — both are facts
about the caller, both flow unconditionally, no per-route opt-in.
Backends that don't care about group membership simply don't reference
`groups.*` in their rules.

To make `groups.admin` immediately useful, the proxy seeds a reserved
group named `admin` at startup and keeps membership in sync with
`users.is_admin`:

  - users.add(is_admin=true) and users.update(is_admin=true) add the
    user to the admin group
  - users.update(is_admin=false) removes them
  - a startup backfill covers any pre-existing is_admin users
  - groups.remove_member rejects manual removal of an is_admin=true
    user from the admin group, so the invariant cannot be desynced
    by hand (demote via users.update instead)

The legacy `admin` synthetic claim is now marked DEPRECATED 2026-05.
It's still emitted whenever users.is_admin is true so existing rules
continue to match during the transition; new rules should use
`groups.admin` instead.

docs/claims.md updated to drop the per-route opt-in language and
document the admin group's semantics.

Tests:
  - admin group exists on a fresh server
  - admin user gets both `admin` and `groups.admin`
  - non-admin user in a custom group gets `groups.<name>` and nothing
    else admin-related
  - users.update(is_admin=false) drops both admin claims
  - groups.remove_member rejects desyncing the admin group
  - test_groups_crud updated to filter the admin group out of
    user-group counts
lee merged commit 8b08e0105e into development 2026-05-05 11:19:49 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_proxy!44
No description provided.