docs / grandstack-auth / Concepts

Role and permission model

Roles, permissions and where each is checked

ConceptDefined inChecked byExample
RoleKeycloak realm role, sent as the roles claimthe server, in schema/auth.graphql ruleseditor, moderator
Permissionderived from roles in the gateway (DEFAULT_ROLE_PERMISSIONS in gateway/rules.ts)the gateway, per mutation fieldcatalog:write, reviews:write

The gateway answers “may this token call this mutation at all?” using permissions. The server answers “may this token change this row?” using roles and the token’s sub.

Why permissions are derived from roles, not read from the OAuth scope

In OAuth, a scope is something a client application is allowed to request, so Keycloak grants scopes per client, not per user: every user logging in through the same client gets the same scopes. What differs between alice and bob is their realm roles. So the gateway maps roles to permissions. It also accepts permissions the token already carries, in a permissions array (Auth0 puts RBAC permissions there) or in the space-separated scope claim, so other providers work without changes.

permissionsFromClaims (in gateway/rules.ts) is the whole rule: a token’s permissions are the union of the permissions of its roles, its permissions array and its scope string.

The default map

RolePermissions at the gatewayServer-level extras
userreviews:writemay edit or delete their own reviews
editor (includes user)reviews:write, catalog:write@authentication on movies, people and genres requires the editor role
moderator (includes editor)same as editormay edit or delete anyone’s review

Keycloak makes the roles composite, so a moderator’s token carries ["editor", "moderator", "user"].

Override the map without changing code: set the gateway’s ROLE_PERMISSIONS environment variable to JSON, for example {"user":["reviews:write"],"critic":["reviews:write"]}. It must be an object of role to array of permission strings; anything else stops the gateway at startup.

Which permission each mutation needs

Defined by MUTATION_PERMISSIONS in gateway/rules.ts:

Mutation fieldsPermission
createMovies, updateMovies, deleteMovies, createPeople, updatePeople, deletePeople, createGenres, updateGenres, deleteGenrescatalog:write
createReview, updateReviews, deleteReviewsreviews:write
anything elserefused (default deny)

Queries are never checked at the gateway: reading is public. If a request contains several mutation fields, all of them must be allowed. If you add a mutation to the schema, add it to this table or the gateway will refuse it.

Row-level rules on the server

All in schema/auth.graphql:

  • Movie, Person, Genre: @authentication on create, update, delete and relationship changes with jwt: { roles: { includes: "editor" } }.
  • Review: @authentication on update and delete, and an @authorization filter: only reviews whose author.sub equals the token’s sub, or any review if the token has the moderator role, can be updated or deleted.
  • Review.author and Review.movie (and Movie.reviews) have nestedOperations: [], so nobody can re-point a review at another author or movie through an update.
  • createReview is a custom mutation that takes the author from $jwt.sub; the generated createReviews is removed with @mutation(operations: [UPDATE, DELETE]) on Review. So a client cannot pass an author, and cannot create a review as someone else.
  • User has no generated mutations; users are created by createReview the first time someone reviews.

Adding a role

See recipes. Note that the Review rule in schema/auth.graphql is a single filter shared by update and delete; the recipe shows how to split it when a role should be able to do only one of them.

Source: grandstack-auth/docs/role-and-scope-model.md (this page is a copy; edit it in the repository)