Skip to main content

Echo Permission Management

Authorization is a critical aspect of web application security that determines what actions users are allowed to perform after they have been authenticated. Echo provides powerful tools to implement permission management systems that control access to resources and operations within your application.

Introduction to Permission Management

Permission management involves defining and enforcing access rules that determine which users have the right to perform specific actions or access certain resources. In an Echo application, permissions are typically implemented using middleware that intercepts requests and validates user permissions before allowing the request to proceed.

Let's explore how to build a robust permission management system in Echo applications.

Basic Permission Concepts

Before diving into implementation, it's important to understand these key concepts:

  1. Permissions - Specific actions that can be performed (e.g., "read_user", "edit_post")
  2. Roles - Collections of permissions assigned to users (e.g., "admin", "editor")
  3. Middleware - Functions that intercept requests to verify permissions
  4. Context - The Echo context which contains user information

Implementing a Permission System

Let's build a simple permission system step by step.

Step 1: Define a User Model with Permissions

First, we need a user structure that includes permission information:

go
// User represents an application user with roles
type User struct {
ID int
Username string
Roles []string
}

// Permission represents an action that can be performed
type Permission struct {
Name string
Description string
}

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

// Store roles and their permissions
var roles = map[string][]string{
"admin": {"create_user", "read_user", "update_user", "delete_user"},
"editor": {"read_user", "update_user"},
"viewer": {"read_user"},
}

Step 2: Create a Permission Checking Function

Next, let's create a function that checks if a user has a specific permission:

go
// HasPermission checks if a user has a specific permission
func HasPermission(user *User, permission string) bool {
// Check each role the user has
for _, role := range user.Roles {
// Get permissions for this role
permissions, exists := roles[role]
if !exists {
continue
}

// Check if the required permission is in this role
for _, p := range permissions {
if p == permission {
return true
}
}
}
return false
}

Step 3: Create Permission Middleware

Now, let's create middleware that uses our permission checker:

go
// RequirePermission creates middleware that requires a specific permission
func RequirePermission(permission string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Get the user from context (assuming it was set by authentication middleware)
user, ok := c.Get("user").(*User)
if !ok {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "User not authenticated",
})
}

// Check permission
if !HasPermission(user, permission) {
return c.JSON(http.StatusForbidden, map[string]string{
"error": "Permission denied",
})
}

// User has permission, continue to the handler
return next(c)
}
}
}

Step 4: Apply the Middleware to Routes

Now we can apply our middleware to specific routes:

go
func main() {
e := echo.New()

// Set up routes with permission middleware
e.GET("/users", listUsers, RequirePermission("read_user"))
e.POST("/users", createUser, RequirePermission("create_user"))
e.PUT("/users/:id", updateUser, RequirePermission("update_user"))
e.DELETE("/users/:id", deleteUser, RequirePermission("delete_user"))

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

// Handler functions
func listUsers(c echo.Context) error {
return c.String(http.StatusOK, "List of users")
}

func createUser(c echo.Context) error {
return c.String(http.StatusCreated, "User created")
}

func updateUser(c echo.Context) error {
return c.String(http.StatusOK, "User updated")
}

func deleteUser(c echo.Context) error {
return c.String(http.StatusOK, "User deleted")
}

Step 5: Authentication Setup

For our permission system to work, we need authentication middleware to set the user in the context:

go
// Sample authentication middleware
func AuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// In a real application, you would verify tokens, sessions, etc.
// For demo purposes, we'll just create a sample user

// Get token from header
token := c.Request().Header.Get("Authorization")

// Validate token and get user (simplified)
if token == "admin-token" {
user := &User{
ID: 1,
Username: "admin",
Roles: []string{"admin"},
}
c.Set("user", user)
} else if token == "editor-token" {
user := &User{
ID: 2,
Username: "editor",
Roles: []string{"editor"},
}
c.Set("user", user)
} else if token == "viewer-token" {
user := &User{
ID: 3,
Username: "viewer",
Roles: []string{"viewer"},
}
c.Set("user", user)
} else {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Invalid or missing token",
})
}

return next(c)
}
}

Now we can apply the authentication middleware globally or to groups of routes:

go
func main() {
e := echo.New()

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

// Routes with permission requirements
e.GET("/users", listUsers, RequirePermission("read_user"))
e.POST("/users", createUser, RequirePermission("create_user"))
// ... other routes

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

Advanced Permission Management

For more complex applications, consider these advanced techniques:

Resource-Based Permissions

Often you need to check permissions on specific resources:

go
// RequireResourcePermission checks permission on a specific resource
func RequireResourcePermission(permissionCheck func(c echo.Context, user *User) bool) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user, ok := c.Get("user").(*User)
if !ok {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "User not authenticated",
})
}

// Use the custom permission check function
if !permissionCheck(c, user) {
return c.JSON(http.StatusForbidden, map[string]string{
"error": "Permission denied for this resource",
})
}

return next(c)
}
}
}

Example usage for editing a specific post:

go
// Check if user can edit a specific post
postEditCheck := func(c echo.Context, user *User) bool {
postID, err := strconv.Atoi(c.Param("id"))
if err != nil {
return false
}

// First check if user has general edit permission
if !HasPermission(user, "edit_post") {
return false
}

// Then check if user owns this post or is an admin
post, err := getPostByID(postID)
if err != nil {
return false
}

return post.AuthorID == user.ID || HasRole(user, "admin")
}

// Apply the middleware
e.PUT("/posts/:id", updatePost, RequireResourcePermission(postEditCheck))

Role-Based Access Control (RBAC)

We can extend our system to fully support RBAC:

go
// HasRole checks if a user has a specific role
func HasRole(user *User, role string) bool {
for _, r := range user.Roles {
if r == role {
return true
}
}
return false
}

// RequireRole middleware checks if a user has a specific role
func RequireRole(role string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user, ok := c.Get("user").(*User)
if !ok {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "User not authenticated",
})
}

if !HasRole(user, role) {
return c.JSON(http.StatusForbidden, map[string]string{
"error": "Role required: " + role,
})
}

return next(c)
}
}
}

Permission Hierarchy

For complex applications, implement a hierarchical permission system:

go
var permissionHierarchy = map[string][]string{
"manage_users": {"create_user", "read_user", "update_user", "delete_user"},
"manage_posts": {"create_post", "read_post", "update_post", "delete_post"},
}

// HasPermissionHierarchical checks permission considering hierarchy
func HasPermissionHierarchical(user *User, permission string) bool {
// First check direct permissions
if HasPermission(user, permission) {
return true
}

// Check if user has any parent permissions
for parent, children := range permissionHierarchy {
if HasPermission(user, parent) {
for _, child := range children {
if child == permission {
return true
}
}
}
}

return false
}

Real-World Example: Blog Platform

Let's implement a complete example for a blog platform:

go
package main

import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"net/http"
"strconv"
)

// Models
type User struct {
ID int
Username string
Roles []string
}

type Post struct {
ID int
Title string
Content string
AuthorID int
}

// Sample data
var posts = []Post{
{ID: 1, Title: "First Post", Content: "Hello world", AuthorID: 2},
{ID: 2, Title: "Second Post", Content: "Echo is awesome", AuthorID: 2},
{ID: 3, Title: "Admin Post", Content: "This is from admin", AuthorID: 1},
}

// Permission system
var roles = map[string][]string{
"admin": {"create_post", "read_post", "update_post", "delete_post", "manage_users"},
"author": {"create_post", "read_post", "update_own_post"},
"reader": {"read_post"},
}

func HasPermission(user *User, permission string) bool {
for _, role := range user.Roles {
if permissions, exists := roles[role]; exists {
for _, p := range permissions {
if p == permission {
return true
}
}
}
}
return false
}

func RequirePermission(permission string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user, ok := c.Get("user").(*User)
if !ok {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Authentication required",
})
}

if !HasPermission(user, permission) {
return c.JSON(http.StatusForbidden, map[string]string{
"error": "Permission denied",
})
}

return next(c)
}
}
}

// Resource-level permission check
func CanEditPost(c echo.Context, user *User) bool {
// Get the post ID from the URL
postID, err := strconv.Atoi(c.Param("id"))
if err != nil {
return false
}

// Find the post
var post Post
found := false
for _, p := range posts {
if p.ID == postID {
post = p
found = true
break
}
}

if !found {
return false
}

// Admin can edit any post
if HasPermission(user, "update_post") {
return true
}

// Authors can edit their own posts
if HasPermission(user, "update_own_post") && post.AuthorID == user.ID {
return true
}

return false
}

// Authentication middleware
func AuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
token := c.Request().Header.Get("Authorization")

var user *User

switch token {
case "admin-token":
user = &User{ID: 1, Username: "admin", Roles: []string{"admin"}}
case "author-token":
user = &User{ID: 2, Username: "author", Roles: []string{"author"}}
case "reader-token":
user = &User{ID: 3, Username: "reader", Roles: []string{"reader"}}
default:
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Invalid or missing token",
})
}

c.Set("user", user)
return next(c)
}
}

// Main function
func main() {
e := echo.New()

// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(AuthMiddleware)

// Routes
e.GET("/posts", getAllPosts, RequirePermission("read_post"))
e.GET("/posts/:id", getPost, RequirePermission("read_post"))
e.POST("/posts", createPost, RequirePermission("create_post"))

// Custom resource permission
e.PUT("/posts/:id", updatePost, func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user, ok := c.Get("user").(*User)
if !ok {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Authentication required",
})
}

if !CanEditPost(c, user) {
return c.JSON(http.StatusForbidden, map[string]string{
"error": "You don't have permission to edit this post",
})
}

return next(c)
}
})

e.DELETE("/posts/:id", deletePost, RequirePermission("delete_post"))

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

// Handlers
func getAllPosts(c echo.Context) error {
return c.JSON(http.StatusOK, posts)
}

func getPost(c echo.Context) error {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid ID"})
}

for _, post := range posts {
if post.ID == id {
return c.JSON(http.StatusOK, post)
}
}

return c.JSON(http.StatusNotFound, map[string]string{"error": "Post not found"})
}

func createPost(c echo.Context) error {
// Implementation omitted for brevity
return c.JSON(http.StatusCreated, map[string]string{"message": "Post created"})
}

func updatePost(c echo.Context) error {
// Implementation omitted for brevity
return c.JSON(http.StatusOK, map[string]string{"message": "Post updated"})
}

func deletePost(c echo.Context) error {
// Implementation omitted for brevity
return c.JSON(http.StatusOK, map[string]string{"message": "Post deleted"})
}

Using the Complete Blog Platform Example

To use the blog example above, run the application and make requests with different tokens:

Request with admin token:

GET /posts HTTP/1.1
Host: localhost:8080
Authorization: admin-token

Response:

HTTP/1.1 200 OK
Content-Type: application/json

[
{"ID":1,"Title":"First Post","Content":"Hello world","AuthorID":2},
{"ID":2,"Title":"Second Post","Content":"Echo is awesome","AuthorID":2},
{"ID":3,"Title":"Admin Post","Content":"This is from admin","AuthorID":1}
]

Update post as author (own post):

PUT /posts/1 HTTP/1.1
Host: localhost:8080
Authorization: author-token
Content-Type: application/json

{
"Title": "Updated First Post",
"Content": "New content here"
}

Response:

HTTP/1.1 200 OK
Content-Type: application/json

{"message":"Post updated"}

Update post as reader (forbidden):

PUT /posts/1 HTTP/1.1
Host: localhost:8080
Authorization: reader-token
Content-Type: application/json

{
"Title": "Updated First Post",
"Content": "New content here"
}

Response:

HTTP/1.1 403 Forbidden
Content-Type: application/json

{"error":"You don't have permission to edit this post"}

Best Practices for Echo Permission Management

  1. Separate concerns: Keep authentication and permission logic in separate middleware
  2. Use context values: Store user data and permissions in the Echo context
  3. Granular permissions: Define specific permissions rather than broad ones
  4. Hierarchical roles: Implement a role hierarchy for easier management
  5. Resource-specific checks: Always validate permissions against specific resources
  6. Logging: Log permission denials for security auditing
  7. Error messages: Provide clear but safe error messages that don't expose system details
  8. Testing: Create unit tests that verify permission logic works correctly

Common Permission Management Patterns

  1. Role-Based Access Control (RBAC): Assign permissions to roles, then roles to users
  2. Attribute-Based Access Control (ABAC): Permissions based on user attributes and resource properties
  3. Permission Inheritance: Hierarchical permissions where higher levels include lower levels
  4. Resource Ownership: Users have special permissions on resources they created

Summary

In this guide, we've covered how to implement a comprehensive permission management system in Echo applications. We've learned:

  • How to define permissions, roles, and users
  • How to build middleware for permission checking
  • How to implement resource-level permissions
  • How to build advanced permission systems with hierarchies
  • How to create a complete real-world example with permission controls

By following these patterns, you can create secure Echo applications that properly restrict access based on user permissions and roles.

Additional Resources and Exercises

Further Reading

Exercises

  1. Basic Role System: Implement a simple role system with three roles: admin, editor, and viewer.
  2. Resource Ownership: Extend the blog example to include a comment system where users can only edit their own comments.
  3. Permission API: Create an API that allows admins to manage permissions and roles at runtime.
  4. Audit Logging: Add middleware that logs all permission checks and denials for security auditing.
  5. UI Integration: Build a simple web interface that shows/hides elements based on user permissions.

By practicing these exercises, you'll gain hands-on experience with Echo permission management and be able to implement secure authorization systems in your own applications.



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