Skip to content

feat(permissions): map roles to permissions and carry them in the access token - #576

Merged
Bccorb merged 2 commits into
mainfrom
feat/permissions-mapped-to-roles
Nov 6, 2026
Merged

Bccorb merged 2 commits into
mainfrom
feat/permissions-mapped-to-roles

Conversation

@Bccorb

@Bccorb Bccorb commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

Closes #415. Design in docs/adr/0004-permissions-and-organization-roles.md, agreed before building. #416 is stacked on this branch. Uses @seamless-auth/types 0.38.0 (fells-code/seamless-auth-types#154).

What

  • Catalogue and mappings. New permissions and role_permissions tables. Instance roles stay in available_roles, and removing one also deletes its mappings, so adding the name back grants nothing.
  • Admin API. GET and POST /admin/permissions, PATCH and DELETE /admin/permissions/:permissionName, GET /admin/roles, PUT /admin/roles/:kind/:roleName/permissions. Writes need admin:write and step-up, and are audited. Deleting a permission a role still grants is a 409 naming the roles, and the foreign key holds the same line against a race.
  • Token claim. permissions on every access token, matched exactly with no wildcards or implied grants. Empty on a recovery session.
  • One mint. Sign-in, refresh, organization switch and session exchange all go through mintAccessToken.
  • Overflow. Over 1 KiB, the token carries permissions_omitted: true instead of a partial list, and a guard reads GET /users/me/permissions, which answers from the database.
  • GET /users/me gains user.permissions.

For review

  • Permissions reach a token when it is next issued, within access_token_ttl, the same as roles today.
  • A role grants through its literal name: holding billing does not pick up a mapping on billing:read.
  • On this branch /admin/roles lists instance roles only, so PUT /admin/roles/organization/... answers 404 until feat: organization-scoped roles and permissions in the access token #416.
  • The 1 KiB budget is checked by a test that builds the adapter session cookie and holds it under 4 KiB. The test reproduces the cookie shape from issueSessionCookies in seamless-auth-server rather than calling that code.

Testing

Run locally on this branch with the published types:

  • Unit and Postgres projects on postgres:18-alpine: 3,217 passed, 1 todo
  • Pre-commit: coverage, lint, typecheck and build

Not run: the conformance harness. This PR adds no adapter behavior, and the guards arrive with step 3 of Wave 3.

Release

Part of the held API v0.23.0 release. #566 stays open. Minor changeset included. New migration: 20261105120000-add-permissions.

…ess token

Adds a permission catalogue and a role mapping, an audited admin API with
step-up on writes, and a permissions claim on every access token, minted in
one place. A set that would not fit is omitted whole, and a guard reads the
live set from GET /users/me/permissions. Permissions match exactly.

Records the design in ADR 0004. Closes #415.
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.

feat: permissions mapped to roles

1 participant