Skip to content

Bind persisted roles to JWT in profile-sync handler#458

Merged
colinmxs merged 1 commit into
developfrom
harden-profile-sync-roles
Jun 5, 2026
Merged

Bind persisted roles to JWT in profile-sync handler#458
colinmxs merged 1 commit into
developfrom
harden-profile-sync-roles

Conversation

@colinmxs
Copy link
Copy Markdown
Contributor

@colinmxs colinmxs commented Jun 5, 2026

POST /users/me/sync now derives the persisted roles list exclusively from current_user.roles (the validated JWT claims). Roles are already populated server-side by the BFF callback's _sync_user_from_id_token off the trusted ID-token decode, and per-request RBAC reads them from the access token's verified claims plus the Users table that the callback writes — the request body has no role to play in this path.

UserProfileSyncRequest no longer declares a 'roles' field. Pydantic's extra='allow' setting keeps the endpoint backwards-compatible with clients that still send extra keys (no 4xx; the extras surface via model_extra and are dropped when building the persisted profile). The handler logs a WARNING when it sees a legacy 'roles' key so stale clients can be tracked down.

Tests:

  • test_sync_persists_jwt_roles_not_body_roles: body roles do not influence the persisted record.
  • test_sync_with_no_jwt_roles_persists_empty_list: empty JWT roles cannot be filled in by the body.
  • test_sync_warns_when_legacy_roles_field_present: legacy field emits the WARN log.
  • Plus seven baseline tests covering email normalization, name and picture pass-through, validation errors, auth requirement, and the graceful no-op when the Users repository is disabled.

POST /users/me/sync now derives the persisted roles list exclusively
from current_user.roles (the validated JWT claims). Roles are already
populated server-side by the BFF callback's _sync_user_from_id_token
off the trusted ID-token decode, and per-request RBAC reads them from
the access token's verified claims plus the Users table that the
callback writes — the request body has no role to play in this path.

UserProfileSyncRequest no longer declares a 'roles' field. Pydantic's
extra='allow' setting keeps the endpoint backwards-compatible with
clients that still send extra keys (no 4xx; the extras surface via
model_extra and are dropped when building the persisted profile). The
handler logs a WARNING when it sees a legacy 'roles' key so stale
clients can be tracked down.

Tests:
- test_sync_persists_jwt_roles_not_body_roles: body roles do not
  influence the persisted record.
- test_sync_with_no_jwt_roles_persists_empty_list: empty JWT roles
  cannot be filled in by the body.
- test_sync_warns_when_legacy_roles_field_present: legacy field emits
  the WARN log.
- Plus seven baseline tests covering email normalization, name and
  picture pass-through, validation errors, auth requirement, and the
  graceful no-op when the Users repository is disabled.
@colinmxs colinmxs merged commit 25add9e into develop Jun 5, 2026
1 check passed
@colinmxs colinmxs deleted the harden-profile-sync-roles branch June 5, 2026 23:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant