Role-Based Access Control in Go with Chi

Go Web Development

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

  1. Request hits AuthMiddleware → validates token, adds role to context
  2. Request hits RequirePermission middleware → checks role has permission
  3. 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.

Imprint

This website is created and run by

Daniel Benner

Zur Deutschen Einheit 2

81929 München

Germany

hello(a)danielbenner.de