Building a Role and Scope Based Access Control(RBAC) System in Go
Access control is cornerstone of modern software systems. Wheather you’re building an API, a web app, or a microservice, managing who can do what within your system is critical. Role and Scope Based Access Control (RBAC) offers a simple yet powerfull way to handle this.
In this article, we’ll explore “go-rbac”, a minimalistic example of RBAC implementation in Go. The project serves as a learning tool to help developers understand and implement RBAC concepts in their Go applications.
What is RBAC ?
RBAC enables acces control through these core concepts:
- Users: The individuals or systems interaction with your application
- Roles: A set of permissions grouped under a logical name (e.g., admin, customer, employee)
- Permissions: Define spesific actions that can be performed on resources (e.g., create, read, update, delete)
- Resources: The objects being accessed, such as APIs or database entries.
Example
- User A is assigned the role admin, which allows them to create, read, update and delete users.
- User B is assigned the role customer, which only permits them to update or read the profiles.
About “go-rbac”
The “go-rbac” project is a demonstration of how to build an RBAC system in GO. it provides :
- Role and Permission management.
- Middleware examples for enforcing permissions in APIs.
- A starting point for building custom access control systems.
Repository Overview
Key Features
- Assign roles to users.
- Define permissions for roles.
- Protect API endpoints with role and permission based middleware.
Directory Structure
├── cmd
│ └── main.go # Application entry point
├── config
│ ├── config.go # Configuration loader and management logic
│ └── config.yaml # Configuration file for environment variables and application settings
├── internal
│ ├── {sub_domain} # Grouped by subdomains or modules
│ │ ├── usecase # Application-specific business logic
│ │ │ └── usecase.go # Implementation of use cases for the subdomain
│ │ ├── entities # Core domain entities
│ │ │ └── entity.go # Definitions of core domain entities
│ │ ├── dtos # Data Transfer Objects for request/response payloads
│ │ │ └── dtos.go # DTO definitions for input/output operations
│ │ ├── repository # Persistence and database layer
│ │ │ └── repository.go # Implementation of repository interfaces
│ │ ├── delivery # Delivery layer (e.g., HTTP handlers, routes)
│ │ │ ├── handlers.go # Request/response handlers for the subdomain
│ │ │ └── routes.go # Route definitions for the subdomain
│ │ ├── usecase.go # Interface for the use case layer
│ │ ├── repository.go # Interface for the repository layer
│ │ ├── delivery.go # Interface for the delivery layer
├── middleware # Custom middleware (e.g., RBAC, logging, authentication)
├── pkg # Shared libraries or utility functions
│ ├── redis # Utilities for Redis interactions
│ ├── constants # Application-wide constants and enumerations
│ ├── utils # General utility functions and helpers
│ ├── datasources # Data source configuration and initialization (e.g., MySQL, Redis)
│ └── rbac # Role-based access control utilities and logic
├── migrations # Database migration files
├── infrastructure # Infrastructure setup and configurations
│ └── docker-compose.yml # Docker Compose configuration for service orchestration
├── docs # Documentation (e.g., API specifications, design documents)
├── tests # Testing suite for various layers
│ ├── e2e # End-to-end tests
│ ├── unit # Unit tests
│ └── integration # Integration tests
├── README.md # Project documentation
└── Makefile # Build and automation instructions for the project
Step By Step Example
- Clone the Repository
start by cloning the repository :
git clone https://github.com/DoWithLogic/go-rbac.git
cd go-rbac
2. Understand the RBAC Model
the system defines:
- Roles (e.g., admin, customer, employee)
- Permission (e.g., create, read, update, delete)
for example :
- the admin role has create, read, update and delete permissions for user resources.
- the customer role only has read and update permissions.
3. Define Roles and Permissions
You can hard-code roles and permissions or load them dynamically from a database. For simplicity, the example use dynamic definitions.
4. Middleware for Role Validation
Middleware plays a critical role in validating whether a user has the required role to access a specific resource. In this example, we use middleware to check the role of the user before allowing them to proceed to the handler.
Implementation
Define middleware for role validation in your project:
func (m *Middleware) RolesMiddleware(roles ...constants.UserRole) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
jwtData, ok := c.Get(constants.AuthCredentialContextKey.String()).(*security.JWTClaims)
if !ok {
return response.ErrorBuilder(app_errors.Forbidden(app_errors.ErrAccessDenied)).Send(c)
}
if !m.hasRequiredRole(jwtData.Role, roles) {
return response.ErrorBuilder(app_errors.Forbidden(app_errors.ErrAccessDenied)).Send(c)
}
// Store the token claims in the request context for later use
c.Set(constants.AuthCredentialContextKey.String(), jwtData)
return next(c)
}
}
}
func (m *Middleware) hasRequiredRole(userRole constants.UserRole, roles []constants.UserRole) bool {
for _, r := range roles {
if r == userRole {
return true
}
}
return false
}
5. Middleware for Scope Validation
Scope validation ensures that users not only have the correct role but also the required permission (or “scope”) to perform specific actions on resources.
Implementation
Here’s an example of scope validation middleware:
func (m *Middleware) PermissionsMiddleware(permissions ...constants.Permission) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
jwtData, ok := c.Get(constants.AuthCredentialContextKey.String()).(*security.JWTClaims)
if !ok {
return response.ErrorBuilder(app_errors.Forbidden(app_errors.ErrAccessDenied)).Send(c)
}
if !m.hasRequiredPermission(jwtData.Permissions, permissions) {
return response.ErrorBuilder(app_errors.Forbidden(app_errors.ErrAccessDenied)).Send(c)
}
c.Set(constants.AuthCredentialContextKey.String(), jwtData)
return next(c)
}
}
}
func (m *Middleware) hasRequiredPermission(userPermissions, requiredPermissions []constants.Permission) bool {
requiredPermissionsMap := make(map[constants.Permission]bool)
for _, permission := range requiredPermissions {
requiredPermissionsMap[permission] = true
}
for _, permission := range userPermissions {
if requiredPermissionsMap[permission] {
return true
}
}
return false
}
6. Use Middleware to Enforce RBAC
With the middleware in place, you can now use it to enforce role-based access control in your routes.
Example
Here’s how you would secure routes with the RequireRole
and RequireScope
middleware:
func MapUserRoutes(g echo.Group, h users.Handlers, mw *middlewares.Middleware) {
users := g.Group("/users", mw.JWTMiddleware())
users.POST("", h.CreateUserHandler, mw.RolesMiddleware(constants.AdminUserRole), mw.PermissionsMiddleware(constants.UsersCreatePermission))
}
How it Works
- Role Validation: Ensures that only users with a specific role (e.g.,
Admin
) can access certain endpoints. - Scope Validation: Adds a finer layer of control by validating whether a user has permission for a specific action (e.g.,
users:create
).
Conclusion
Combining role and scope validation in middleware creates a powerful yet flexible RBAC system. By layering these middlewares on your routes, you ensure a scalable approach to access control.
This approach makes it easy to enforce access restrictions at the API level while maintaining a clean and readable codebase.