RBAC
Import the rbac subpackage to check roles and permissions over the authenticated identity:
import "github.com/gp-system/auth/rbac"
rbac is the auth module's role-and-permission-checks subpackage: role and permission checks over an authenticated Identity carried in the request context. It takes no position on where roles come from: auth.Identify (or anything else) populates the Identity, and rbac only checks: it assumes no database schema and no user table, and it never writes a response.
The package covers three levels: id.HasRole("admin") / id.HasPermission("orders.view") for the simplest boolean checks, rbac.CheckRole(id, "admin") / rbac.CheckPermission(id, "orders.view") which return ErrUnauthenticated/ErrForbidden instead of a bool (useful when you need to distinguish "no identity" from "wrong role" without an extra nil check), and the gpsystem kit's server.RequireRole("admin") / server.RequirePermission("orders.view") for route-level net/http middleware protection that renders those two sentinels as Problems. See Using auth standalone for a hand-written middleware built on CheckRole alone, with no kit involved.
The Identity
The Identity is the caller as authorization decisions and business logic see it: the contract between authentication and authorization.
type Identity struct {
Subject string // the user's identifier (the JWT sub claim)
Username string
Roles []string
Permissions []string
Extra map[string]any // claim data beyond the standard fields (e.g. tenant)
}
It has two query methods, both with any-of semantics: one match among several arguments is enough.
id.HasRole("admin", "editor") // true if any of the roles is present
id.HasPermission("orders.view") // true if the permission is present
Both are safe to call on a nil identity (they report false); this matches the fact that FromContext returns nil when the request did not pass auth middleware. Your guard code therefore never needs a nil check:
if rbac.FromContext(ctx).HasRole("admin") { // works on nil too, reports false
// ...
}
WithIdentity and FromContext
The Identity travels in a plain context.Context:
ctx = rbac.WithIdentity(ctx, id) // called by the auth middleware
id := rbac.FromContext(ctx) // *rbac.Identity, or nil without auth
Identity, WithIdentity and FromContext are framework-agnostic: they don't import net/http, or any router. The same contract therefore works outside the HTTP layer too: in services, listeners and jobs, whenever an identity was put into the context.
In the shop, for example, order listing narrows to the caller at the service layer: an admin sees everything, a customer only their own.
// internal/modules/shop/surfaces/api/service/orders.go
func (s *Service) ListOrders(ctx context.Context, cursor string) ([]Order, error) {
id := rbac.FromContext(ctx)
if id.HasRole("admin") {
return s.orders.ListAll(ctx, cursor)
}
return s.orders.ListByUser(ctx, id.Subject, cursor)
}
This is data scoping (what to show), not an access decision (whether it is allowed at all). Per-request authorization, meaning "may they view this particular order", is the job of policies.
Route guards
The kit's server.RequireRole / server.RequirePermission middlewares protect a whole route group. They assume server.AuthMiddleware already ran before them; they only inspect the Identity in the context, via rbac.CheckRole/CheckPermission, and render the result as a Problem.
router.Route("/admin/shop", func(r chi.Router) {
r.Use(
server.AuthMiddleware(deps.Auth), // Bearer → parse → Identity into context
server.RequireRole("admin"), // 401 without identity, 403 without the role
)
// ...
})
// permission-based variant, with any-of semantics:
router.Route("/reports", func(r chi.Router) {
r.Use(server.AuthMiddleware(deps.Auth),
server.RequirePermission("reports.view", "reports.export"))
// ...
})
The responses are RFC 9457 problem documents:
| Situation | Result |
|---|---|
no Identity in the context (auth did not run) | 401 authentication required |
| identity present, but none of the listed roles/permissions held | 403 insufficient privileges |
| any of the listed roles/permissions held (any-of) | pass |
Underneath, server.RequireRole/RequirePermission call rbac.CheckRole/CheckPermission directly and map rbac.ErrUnauthenticated/ErrForbidden to those two statuses; a caller with no kit does the same mapping by hand, as shown in Using auth standalone.
With the kit
In the shop, the entire admin surface (product CRUD, image upload) sits behind the pair above: the middleware goes into the RegisterAdmin function generated by add surface, so the protection is surface-wide and the sibling api surface is untouched. The exact wiring (and adding the deps.Auth field) is shown on the authentication page; the surface-per-function structure is covered by add surface.
The same Identity also shows up outside the HTTP request/response cycle: the realtime gateway resolves each WebSocket connection's JWT into an Identity and checks it against the Allow/AllowPrefix callbacks you register for a topic, so a connection only ever subscribes to channels the caller is authorized for.
For finer granularity the same guards can go on a smaller group instead of the whole surface, but once the decision is no longer "do they have this role" but "do they own this resource", it is not RBAC anymore: that is what the policy layer is for.
Where roles and permissions come from
In the kit's default setup, from the JWT claims: the login endpoint writes the user's roles and permissions into DefaultClaims, and the auth middleware fills the Identity fields from them. The full chain is on the authentication page. Since rbac only ever sees the Identity, the source is swappable: a custom claims type (Identify/server.AuthMiddlewareFor), an identity built from an API key, or a test fixture all work the same way.
rbac.WithIdentity you can put the desired identity directly into the context, and the service layer behaves as if the middleware had populated it.ctx := rbac.WithIdentity(t.Context(), &rbac.Identity{
Subject: "u-42", Roles: []string{"admin"},
})
A role says what the caller may do in general. When the decision also needs the concrete request (are they the owner, are they in the same tenant, is the record in the right state), move on to policies.
Patterns used
- Context-injected identity (
WithIdentity/FromContext, unexported key, nil-safe methods): Design patterns. - Higher-order authorization guard (
requireFn, shared by bothRequireRoleandRequirePermission): Design patterns.