Give users roles, give roles permissions โ and let routes ask only for permissions.
5a
Each cell is a real status code from go run ./demos/05. Fill the matrix one role at a time.
type Role string type Perm string const ( RoleUser Role = "user" RoleModerator Role = "moderator" RoleAdmin Role = "admin" ) const ( ReadProfile Perm = "profile:read" DeletePost Perm = "post:delete" ListUsers Perm = "user:list" SetRole Perm = "user:set_role" ) var rolePerms = map[Role][]Perm{ RoleUser: {ReadProfile}, RoleModerator: {ReadProfile, DeletePost}, RoleAdmin: {ReadProfile, DeletePost, ListUsers, SetRole}, } func Can(r Role, p Perm) bool { for _, have := range rolePerms[r] { if have == p { return true } } return false // unknown role or perm: deny }
Role / Perm constants โ a typo is a compile error, not a silent 403.superuser โ gets nothing.5b
It runs after RequireAuth, reads the role from the claims and answers 403 on its own.
func RequirePerm(p Perm, next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { c := auth.ClaimsFrom(r.Context()) if c == nil || !Can(Role(c.Role), p) { http.Error(w, "forbidden", 403) return } next.ServeHTTP(w, r) } return http.HandlerFunc(fn) } // wiring: authenticate first, then authorize protect := func(p Perm) http.Handler { return auth.RequireAuth(RequirePerm(p, h)) } mux.Handle("GET /me", protect(ReadProfile)) mux.Handle("DELETE /posts/{id}", protect(DeletePost)) mux.Handle("GET /admin/users", protect(ListUsers)) mux.Handle("PATCH /admin/users/{id}/role", protect(SetRole))
me del list role (no token) 401 401 401 401 user 200 403 403 403 moderator 200 200 403 403 admin 200 200 200 200 superuser 403 403 403 403 # unknown role โ denied everywhere
RequireAuth outside, RequirePerm inside โ so no token is 401, wrong role is 403.jti (session 04).Letting users send their own role, e.g. {"role":"admin"} in the register body. Roles are only ever set by an admin endpoint guarded by SetRole.
RoleSupport that can ReadProfile and ListUsers.Recap
Can(role, perm) returns false for anything unknown โ default deny.RequireAuth(RequirePerm(p, h)): 401 first, 403 second.