Skip to main content

Echo RBAC Implementation

Introduction

Role-Based Access Control (RBAC) is an approach to restricting system access based on the roles of individual users within an organization. In this tutorial, we'll learn how to implement a comprehensive RBAC system in Echo, the high-performance web framework for Go.

RBAC allows you to:

  • Define roles with specific permissions
  • Assign roles to users
  • Control access to resources based on user roles
  • Maintain a clean separation between authorization logic and business logic

This guide takes you through creating a complete RBAC system in Echo from scratch, suitable for both small applications and larger systems that require fine-grained access control.

Prerequisites

Before starting, ensure you have:

  • Basic knowledge of Go programming
  • Familiarity with Echo framework basics
  • Go installed on your system
  • A working Go development environment

Understanding RBAC Concepts

Before diving into code, let's understand the key components of our RBAC system:

  1. Permissions: Individual actions that can be performed (e.g., create:user, delete:post)
  2. Roles: Collections of permissions (e.g., Admin, Editor, User)
  3. Users: Associated with one or more roles
  4. Middleware: Intercepts requests to check if the user has the required permissions

Basic RBAC Implementation

Let's start by creating a simple RBAC implementation for Echo.

Step 1: Define Our Models

First, let's define our core structures:

go
package rbac

// Permission represents a single action that can be performed
type Permission string

// Role represents a collection of permissions
type Role struct {
Name string
Permissions map[Permission]bool
}

// User represents an authenticated user with roles
type User struct {
ID string
Roles map[string]*Role
}

// RBAC is our role-based access control manager
type RBAC struct {
Roles map[string]*Role
}

// NewRBAC creates a new RBAC instance
func NewRBAC() *RBAC {
return &RBAC{
Roles: make(map[string]*Role),
}
}

Step 2: Implement Core RBAC Functions

Now, let's add the core functions for our RBAC system:

go
// AddRole adds a new role to the RBAC system
func (r *RBAC) AddRole(name string) *Role {
role := &Role{
Name: name,
Permissions: make(map[Permission]bool),
}
r.Roles[name] = role
return role
}

// AddPermission adds a permission to a role
func (role *Role) AddPermission(permission Permission) {
role.Permissions[permission] = true
}

// HasPermission checks if a role has a specific permission
func (role *Role) HasPermission(permission Permission) bool {
return role.Permissions[permission]
}

// HasPermission checks if a user has a specific permission through any of their roles
func (user *User) HasPermission(permission Permission) bool {
for _, role := range user.Roles {
if role.HasPermission(permission) {
return true
}
}
return false
}

Step 3: Create RBAC Middleware for Echo

Now, let's create Echo middleware to enforce our RBAC rules:

go
package middleware

import (
"net/http"

"github.com/labstack/echo/v4"
"your-project/rbac"
)

// RBACMiddleware creates middleware that checks if a user has the required permission
func RBACMiddleware(permission rbac.Permission) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Get user from context (after authentication)
user, ok := c.Get("user").(*rbac.User)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}

// Check if user has the required permission
if !user.HasPermission(permission) {
return echo.NewHTTPError(http.StatusForbidden, "Forbidden")
}

return next(c)
}
}
}

Step 4: Setting Up Authentication Context

For the RBAC middleware to work, we need to ensure authenticated users are added to the Echo context:

go
// AuthMiddleware is a simplified authentication middleware example
// In real applications, use proper authentication like JWT
func AuthMiddleware(rbacManager *rbac.RBAC) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// In a real application, you would validate tokens, etc.
userID := c.Request().Header.Get("X-User-ID")
if userID == "" {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user ID")
}

// Create a user with roles (in real apps, fetch from database)
user := &rbac.User{
ID: userID,
Roles: make(map[string]*rbac.Role),
}

// Add roles to user (in real apps, fetch from database)
if roleNames := c.Request().Header.Get("X-User-Roles"); roleNames != "" {
for _, roleName := range strings.Split(roleNames, ",") {
if role, exists := rbacManager.Roles[roleName]; exists {
user.Roles[roleName] = role
}
}
}

// Set user in context for later middleware
c.Set("user", user)

return next(c)
}
}
}

Complete Example: Blog API with RBAC

Now, let's put everything together in a practical example of a blog API with RBAC protection:

go
package main

import (
"net/http"

"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"

"your-project/rbac"
customMiddleware "your-project/middleware"
)

func main() {
// Create Echo instance
e := echo.New()

// Create RBAC manager and define roles
rbacManager := rbac.NewRBAC()

// Create roles with permissions
adminRole := rbacManager.AddRole("admin")
adminRole.AddPermission("create:post")
adminRole.AddPermission("update:post")
adminRole.AddPermission("delete:post")
adminRole.AddPermission("read:post")

editorRole := rbacManager.AddRole("editor")
editorRole.AddPermission("create:post")
editorRole.AddPermission("update:post")
editorRole.AddPermission("read:post")

userRole := rbacManager.AddRole("user")
userRole.AddPermission("read:post")

// Add middlewares
e.Use(middleware.Logger())
e.Use(middleware.Recover())

// Apply authentication middleware to all routes
e.Use(customMiddleware.AuthMiddleware(rbacManager))

// Define routes with appropriate permissions
e.GET("/posts", getAllPosts, customMiddleware.RBACMiddleware("read:post"))
e.POST("/posts", createPost, customMiddleware.RBACMiddleware("create:post"))
e.PUT("/posts/:id", updatePost, customMiddleware.RBACMiddleware("update:post"))
e.DELETE("/posts/:id", deletePost, customMiddleware.RBACMiddleware("delete:post"))

// Start server
e.Logger.Fatal(e.Start(":8080"))
}

// Route handlers
func getAllPosts(c echo.Context) error {
// In real app, fetch posts from database
posts := []map[string]interface{}{
{"id": 1, "title": "First Post", "content": "Content 1"},
{"id": 2, "title": "Second Post", "content": "Content 2"},
}
return c.JSON(http.StatusOK, posts)
}

func createPost(c echo.Context) error {
// In real app, save post to database
return c.JSON(http.StatusCreated, map[string]interface{}{
"message": "Post created successfully",
})
}

func updatePost(c echo.Context) error {
id := c.Param("id")
// In real app, update post in database
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "Post " + id + " updated successfully",
})
}

func deletePost(c echo.Context) error {
id := c.Param("id")
// In real app, delete post from database
return c.JSON(http.StatusOK, map[string]interface{}{
"message": "Post " + id + " deleted successfully",
})
}

Testing the RBAC Implementation

You can test this implementation using curl commands:

bash
# User with read-only access
curl -X GET http://localhost:8080/posts \
-H "X-User-ID: user123" \
-H "X-User-Roles: user"

# This will return 200 OK with posts

# User with read-only access trying to create a post
curl -X POST http://localhost:8080/posts \
-H "X-User-ID: user123" \
-H "X-User-Roles: user" \
-H "Content-Type: application/json" \
-d '{"title":"New Post","content":"Content"}'

# This will return 403 Forbidden

# Editor creating a post
curl -X POST http://localhost:8080/posts \
-H "X-User-ID: editor123" \
-H "X-User-Roles: editor" \
-H "Content-Type: application/json" \
-d '{"title":"New Post","content":"Content"}'

# This will return 201 Created

Advanced RBAC Features

1. Resource-Based Permissions

Let's extend our RBAC system to handle resource-based permissions:

go
// PermissionChecker validates if a user has permission for a specific resource
type PermissionChecker func(c echo.Context, user *User) bool

// ResourceRBACMiddleware creates middleware that checks resource permissions
func ResourceRBACMiddleware(checker PermissionChecker) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Get user from context
user, ok := c.Get("user").(*rbac.User)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}

// Check permission using the provided checker
if !checker(c, user) {
return echo.NewHTTPError(http.StatusForbidden, "Forbidden")
}

return next(c)
}
}
}

// Example usage
e.PUT("/posts/:id", updatePost, ResourceRBACMiddleware(func(c echo.Context, user *User) bool {
postID := c.Param("id")

// Check if user has general permission
if !user.HasPermission("update:post") {
return false
}

// Check if user is the post owner (would need to query database in real app)
// Return true if user is admin or post owner
return user.HasRole("admin") || isPostOwner(postID, user.ID)
}))

2. Role Hierarchy

We can also implement role hierarchy:

go
// Role with parent role support
type Role struct {
Name string
Permissions map[Permission]bool
Parent *Role
}

// HasPermission checks if a role or any of its parents has a specific permission
func (role *Role) HasPermission(permission Permission) bool {
if role.Permissions[permission] {
return true
}

if role.Parent != nil {
return role.Parent.HasPermission(permission)
}

return false
}

// Usage example:
userRole := rbacManager.AddRole("user")
userRole.AddPermission("read:post")

// Editor inherits from user
editorRole := rbacManager.AddRole("editor")
editorRole.Parent = userRole
editorRole.AddPermission("create:post")
editorRole.AddPermission("update:post")

// Admin inherits from editor
adminRole := rbacManager.AddRole("admin")
adminRole.Parent = editorRole
adminRole.AddPermission("delete:post")

Database Integration

In a production environment, you'll want to store roles and permissions in a database:

go
// Example using a simple database interface (implementation details omitted)
type RBACRepository interface {
GetRoles() (map[string]*Role, error)
GetUserRoles(userID string) (map[string]*Role, error)
AddRole(role *Role) error
AddPermissionToRole(roleName string, permission Permission) error
}

// Load RBAC data from database
func LoadRBACFromDatabase(repo RBACRepository) (*RBAC, error) {
rbacManager := NewRBAC()

roles, err := repo.GetRoles()
if err != nil {
return nil, err
}

rbacManager.Roles = roles
return rbacManager, nil
}

// Middleware to load user roles from database
func AuthMiddlewareWithDB(rbacManager *RBAC, repo RBACRepository) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Authenticate user (simplified)
userID := c.Request().Header.Get("X-User-ID")
if userID == "" {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user ID")
}

// Create user and load roles from database
user := &rbac.User{
ID: userID,
}

roles, err := repo.GetUserRoles(userID)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Error loading user roles")
}

user.Roles = roles
c.Set("user", user)

return next(c)
}
}
}

Summary

In this tutorial, we've built a comprehensive RBAC system for Echo web applications:

  1. We created core RBAC models for permissions, roles, and users
  2. We implemented Echo middleware to enforce RBAC policies
  3. We built a complete example of a blog API with different user roles
  4. We extended the system with advanced features like resource-based permissions and role hierarchies
  5. We outlined how to integrate the RBAC system with a database

This RBAC implementation provides a robust foundation for securing your Echo applications with fine-grained access control. You can adapt and extend it based on your specific application requirements.

Further Exercises

  1. Implement caching for role and permission lookups to improve performance
  2. Create a web interface for managing roles and permissions
  3. Add support for time-based permissions that expire after a certain period
  4. Implement attribute-based access control (ABAC) to extend the permission system
  5. Add comprehensive logging for security audits

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)