Stage 7 · Master
Authentication & Authorization
Enforcing RBAC in Middleware
Declare permission requirements once at the edge and stop teaching every handler how to say no.
Why Handler-Level Role Checks Rot
Once a permission model exists, the next failure mode is obvious: scattering it everywhere anyway. The fastest way to wire authorization is to let every Gin handler ask some version of does this user have role X? That feels harmless on the third route. By the thirtieth route, it is chaos. Some handlers check for one role name, others call a helper with slightly different semantics, and a few forget to check anything because the author assumed the parent route group already handled it.
Fieldwork's gateway already authenticates the caller and resolves tenant context before forwarding requests. That makes it the natural place to enforce RBAC too. I did not want handler code in the tasks service to keep re-litigating who may delete a task. The handler should care whether the task exists and whether the domain rule allows deletion, not whether a string in a JWT matches some half-remembered admin convention.
| Placement | Upside | Cost paid later |
|---|---|---|
| Role checks inside handlers | Fast initial implementation | Duplication, drift, and forgotten checks |
| Permission checks inside service methods | Can guard non-HTTP call sites too | Services now depend on transport-flavored identity plumbing |
| Gateway middleware with explicit requirements | One consistent HTTP enforcement layer | Requires route metadata discipline |
Making Permission Requirements Declarative
The specific pattern in Fieldwork is to make permission requirements route metadata, not inline conditionals. A route registers the permission or permissions it needs. Middleware reads that requirement, compares it against the actor context already derived from the validated JWT, and either continues or returns the standard forbidden envelope. That puts the access rule where reviewers expect to see it: next to the route declaration, not buried in line sixty of the handler body.
I rejected annotation-style magic or reflection-heavy tags for this. In a Go codebase, explicit function composition is usually clearer than inventing a mini framework. The route setup should look like ordinary Gin, with one extra middleware layer that names the required permission set. You should be able to grep for tasks:task:delete and find the exact routes that depend on it.
Loading Actor Capabilities Once per Request
The middleware only works if actor context is stable and already available. In Fieldwork, the authentication middleware validates the JWT, extracts subject, tenant, roles, and permissions, and stores that actor on the request context. The RBAC middleware then reads from context instead of re-parsing headers or hitting the memberships table again. That one-pass model matters because the gateway sits on every request path; repeating work there scales linearly with traffic.
This is also where permission checks stay intentionally coarse. Middleware decides whether the caller may attempt the operation at all. Resource-specific domain checks still happen deeper in the service. For example, a user might hold tasks:task:update but still be blocked from editing a task in a workspace they are no longer a member of. RBAC answers can they ever do this kind of thing; domain rules answer can they do it to this specific record right now.
If deleting a project is forbidden once it has billed invoices attached, that is not an RBAC check. Keep authorization and business invariants separate so each layer stays understandable.
Keeping Handlers Focused on Business Rules
Once middleware owns the permission gate, handlers get simpler in a useful way. They can assume an authenticated, tenant-scoped actor exists and spend their attention on request binding, DTO validation, and the service call. That is not just cleaner code. It also means security review has a shorter path. You verify the middleware contract once, then you verify that each route attached the right requirement.
The alternative was to let handlers call some shared HasPermission helper and trust discipline. I have seen that drift before. One handler forgets to abort after a failed check. Another checks the wrong permission because the naming is ambiguous. A third checks a role name left over from the previous permission model. Central middleware eliminates a whole class of those mistakes by making the secure path the boring default.
- Declare required permissions at route registration so reviewers can audit the surface area quickly.
- Load actor permissions once from the validated JWT and reuse them for all later middleware.
- Reserve handler logic for domain validation and resource-specific invariants, not generic role parsing.
The Enforcement Pattern Fieldwork Keeps
Fieldwork enforces RBAC in gateway middleware, not in scattered handler conditionals. Routes declare the permissions they require. Authentication middleware provides a tenant-scoped actor from the validated JWT. Authorization middleware checks the actor once and returns the standard forbidden response when the contract is not met.
That decision keeps the public HTTP surface consistent and keeps service code honest. We still do deeper domain checks where needed, but the generic question of who may attempt a task, project, or membership operation no longer lives in ten slightly different copies. We picked one place, one mechanism, and one vocabulary, because authorization errors spread faster than almost any other kind of duplicated logic.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.