Most RBAC implementations are overcomplicated. Hierarchical roles, conditional permissions, attribute-based controls—useful in some contexts, but overkill for many applications.
This implementation is minimal: roles and permissions as enums, a map between them, and middleware that checks permissions. No external dependencies. The entire system fits in one file.
The RBAC Package
Roles and Permissions
Define roles and permissions as typed string constants:
package rbac
type Role string
func (r Role) String() string {
return string(r)
}
const (
RoleAdmin Role = "admin"
RoleManager Role = "manager"
)
type Permission string
func (p Permission) String() string {
return string(p)
}
const (
PermissionManageAccess Permission = "manage_access"
PermissionManageReleaseNote Permission = "manage_release_note"
)Role-Permission Mapping
// rolePermissions maps roles to their granted permissions
var rolePermissions = map[Role][]Permission{
RoleAdmin: {
PermissionManageAccess,
PermissionManageReleaseNote,
},
RoleManager: {
PermissionManageReleaseNote,
},
}
// HasPermission checks if a role has a specific permission
func (r Role) HasPermission(p Permission) bool {
permissions, exists := rolePermissions[r]
if !exists {
return false
}
for _, permission := range permissions {
if permission == p {
return true
}
}
return false
}The Middleware
// RequirePermission creates middleware that checks if the user has the required permission
func RequirePermission(permission Permission) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get user role from context (set by auth middleware)
userRole, ok := r.Context().Value(„userRole“).(Role)
if !ok {
http.Error(w, „Unauthorized“, http.StatusUnauthorized)
return
}
// Check if the role has the required permission
if !userRole.HasPermission(permission) {
http.Error(w, „Forbidden“, http.StatusForbidden)
return
}
// User has permission, proceed to the next handler
next.ServeHTTP(w, r)
})
}
}The middleware extracts the user’s role from the request context (set by auth middleware) and checks if that role has the required permission.
Using It with Chi
Auth Middleware Integration
Your auth middleware needs to add the user’s role to the request context:
// AuthMiddleware authenticates the user and adds their info to the context
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get token from request (e.g., from cookie or Authorization header)
token := getTokenFromRequest(r)
// Validate token and get user info
user, err := validateToken(token)
if err != nil {
http.Error(w, „Unauthorized“, http.StatusUnauthorized)
return
}
// Create a new context with the user role
ctx := context.WithValue(r.Context(), „userRole“, user.Role)
// Call the next handler with the updated context
next.ServeHTTP(w, r.WithContext(ctx))
})
}Protecting Routes
func SetupRoutes(r *chi.Mux) {
// Apply authentication middleware to all routes
r.Use(AuthMiddleware)
// Public routes accessible to all authenticated users
r.Get(„/release-notes“, handlers.ListReleaseNotes)
r.Get(„/profile“, handlers.ViewProfile)
// Routes that require specific permissions
r.Group(func(r chi.Router) {
// Only users with user management permission can access these routes
r.Use(rbac.RequirePermission(rbac.PermissionUserManagement))
r.Get(„/users“, handlers.ListUsers)
r.Post(„/users“, handlers.CreateUser)
r.Put(„/users/{id}“, handlers.UpdateUser)
r.Delete(„/users/{id}“, handlers.DeleteUser)
})
}Chi’s router groups let you apply RBAC middleware to entire groups of routes. The permission check happens in middleware, so handlers focus solely on business logic.
Request Flow
- Request hits
AuthMiddleware→ validates token, adds role to context - Request hits
RequirePermissionmiddleware → checks role has permission - If permitted, request reaches handler
Clean separation: authentication (who are you?), authorization (what can you do?), business logic (how do we do it?).
Why This Works
- Simple. A few dozen lines, no dependencies.
- Explicit. Route definitions show required permissions at a glance.
- Extensible. New roles/permissions = update constants and map. Middleware unchanged.
The pattern works with any Go router. Roles, permissions, and middleware-based checks are the core concepts—the specific router doesn’t matter.