Echo Policy-based Authorization
Introduction
Policy-based authorization is an advanced approach to securing your web applications that allows you to define fine-grained access control rules. Unlike simple role-based authorization that checks if a user belongs to a specific role, policy-based authorization evaluates complex conditions before granting access to resources.
In this tutorial, we'll explore how to implement policy-based authorization in the Echo framework, a high-performance, minimalist Go web framework. By the end of this guide, you'll understand how to create, apply, and manage authorization policies to secure your Echo applications.
Prerequisites
Before diving into policy-based authorization, you should have:
- Basic understanding of Go programming language
- Familiarity with the Echo framework
- Knowledge of basic authentication concepts
- A working Echo application setup
Understanding Policy-based Authorization
Policy-based authorization centers around the concept of a "policy," which is essentially a rule or set of rules that determines whether a user has permission to perform a specific action on a resource.
A policy typically evaluates:
- Who is the user? (identity)
- What action are they trying to perform? (operation)
- What resource are they trying to access? (target)
- Under what conditions? (context)
Key Benefits of Policy-based Authorization
- Fine-grained control: Define permissions at a granular level
- Flexibility: Create complex rules based on multiple factors
- Separation of concerns: Keep authorization logic separate from business logic
- Scalability: Easily add or modify policies without changing application code
Implementing Policy-based Authorization in Echo
Let's implement a simple yet powerful policy-based authorization system in Echo.
Step 1: Define Policy Types and Interfaces
First, let's create the basic structures for our policy system:
// Policy represents an authorization policy
type Policy interface {
// Evaluate checks if the request meets the policy requirements
Evaluate(c echo.Context) bool
}
// PolicyFunc is a function type that implements the Policy interface
type PolicyFunc func(c echo.Context) bool
// Evaluate implements the Policy interface
func (pf PolicyFunc) Evaluate(c echo.Context) bool {
return pf(c)
}
Step 2: Create a Policy Manager
Next, let's implement a manager to register and check policies:
// PolicyManager handles policy registration and evaluation
type PolicyManager struct {
policies map[string]Policy
}
// NewPolicyManager creates a new policy manager
func NewPolicyManager() *PolicyManager {
return &PolicyManager{
policies: make(map[string]Policy),
}
}
// Register adds a new policy with the specified name
func (pm *PolicyManager) Register(name string, policy Policy) {
pm.policies[name] = policy
}
// Evaluate checks if the request satisfies the named policy
func (pm *PolicyManager) Evaluate(name string, c echo.Context) bool {
policy, exists := pm.policies[name]
if !exists {
return false
}
return policy.Evaluate(c)
}
Step 3: Create Authorization Middleware
Now, let's implement middleware that will check policies before allowing access to routes:
// PolicyManager is our global policy manager
var policyManager = NewPolicyManager()
// RequirePolicy creates middleware that enforces the named policy
func RequirePolicy(policyName string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if !policyManager.Evaluate(policyName, c) {
return echo.NewHTTPError(http.StatusForbidden, "Access denied")
}
return next(c)
}
}
}
Step 4: Define Common Policies
Let's create some common policies that you might use in a real application:
// Setup common policies
func SetupCommonPolicies() {
// IsAuthenticated checks if the user is logged in
policyManager.Register("IsAuthenticated", PolicyFunc(func(c echo.Context) bool {
// Get user from context (set by your authentication middleware)
user := c.Get("user")
return user != nil
}))
// IsAdmin checks if the user has admin role
policyManager.Register("IsAdmin", PolicyFunc(func(c echo.Context) bool {
user := c.Get("user")
if user == nil {
return false
}
// This will depend on your user structure
u, ok := user.(User)
if !ok {
return false
}
return u.Role == "admin"
}))
// OwnsResource checks if the user owns the requested resource
policyManager.Register("OwnsResource", PolicyFunc(func(c echo.Context) bool {
user := c.Get("user")
if user == nil {
return false
}
u, ok := user.(User)
if !ok {
return false
}
resourceID := c.Param("id")
// In real application, you'd check database
// This is simplified for demonstration
return IsResourceOwner(u.ID, resourceID)
}))
}
Practical Example: Blog Application
Let's see how policy-based authorization works in a simple blog application:
package main
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
// User represents a simple user model
type User struct {
ID string
Username string
Role string
}
// IsResourceOwner checks if the user owns the resource (simplified)
func IsResourceOwner(userID, resourceID string) bool {
// In a real app, you would query your database
// This is just a placeholder implementation
return userID == "owner-of-"+resourceID
}
func main() {
// Create a new Echo instance
e := echo.New()
// Add basic middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Setup authentication middleware (simplified)
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// In a real app, you'd verify tokens, etc.
// Here we're just simulating for the example
authHeader := c.Request().Header.Get("Authorization")
if authHeader == "Bearer admin-token" {
c.Set("user", User{ID: "admin123", Username: "admin", Role: "admin"})
} else if authHeader == "Bearer user-token" {
c.Set("user", User{ID: "user123", Username: "regular_user", Role: "user"})
}
return next(c)
}
})
// Initialize and setup policies
SetupCommonPolicies()
// Create custom policies for our blog application
policyManager.Register("CanCreatePosts", PolicyFunc(func(c echo.Context) bool {
user := c.Get("user")
if user == nil {
return false
}
u, ok := user.(User)
if !ok {
return false
}
// Allow any authenticated user to create posts
return true
}))
policyManager.Register("CanModifyPost", PolicyFunc(func(c echo.Context) bool {
user := c.Get("user")
if user == nil {
return false
}
u, ok := user.(User)
if !ok {
return false
}
postID := c.Param("id")
// Admin can modify any post
if u.Role == "admin" {
return true
}
// Regular users can only modify their own posts
return IsResourceOwner(u.ID, postID)
}))
// Define routes with policy-based authorization
// Public routes
e.GET("/posts", getAllPosts)
e.GET("/posts/:id", getPost)
// Protected routes
postsGroup := e.Group("/posts")
postsGroup.POST("", createPost, RequirePolicy("CanCreatePosts"))
postsGroup.PUT("/:id", updatePost, RequirePolicy("CanModifyPost"))
postsGroup.DELETE("/:id", deletePost, RequirePolicy("CanModifyPost"))
// Admin routes
adminGroup := e.Group("/admin")
adminGroup.Use(RequirePolicy("IsAdmin"))
adminGroup.GET("/dashboard", adminDashboard)
adminGroup.GET("/users", listAllUsers)
// Start server
e.Logger.Fatal(e.Start(":8080"))
}
// Route handlers
func getAllPosts(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"message": "List of all posts"})
}
func getPost(c echo.Context) error {
id := c.Param("id")
return c.JSON(http.StatusOK, map[string]string{"message": "Post details for " + id})
}
func createPost(c echo.Context) error {
return c.JSON(http.StatusCreated, map[string]string{"message": "Post created"})
}
func updatePost(c echo.Context) error {
id := c.Param("id")
return c.JSON(http.StatusOK, map[string]string{"message": "Updated post " + id})
}
func deletePost(c echo.Context) error {
id := c.Param("id")
return c.JSON(http.StatusOK, map[string]string{"message": "Deleted post " + id})
}
func adminDashboard(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"message": "Admin dashboard"})
}
func listAllUsers(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"message": "List of all users"})
}
Testing the Application
You can test this application using cURL or any API testing tool:
Public Access (No Authorization)
# Get all posts - Public endpoint
curl http://localhost:8080/posts
# Expected Output:
# {"message":"List of all posts"}
# Get specific post - Public endpoint
curl http://localhost:8080/posts/123
# Expected Output:
# {"message":"Post details for 123"}
Regular User Actions
# Create a post as a regular user
curl -X POST http://localhost:8080/posts \
-H "Authorization: Bearer user-token" \
-H "Content-Type: application/json" \
-d '{"title":"My First Post","content":"This is the content"}'
# Expected Output:
# {"message":"Post created"}
# Try to access admin dashboard (should be forbidden)
curl http://localhost:8080/admin/dashboard \
-H "Authorization: Bearer user-token"
# Expected Output:
# {"message":"Access denied"}
Admin User Actions
# Access admin dashboard
curl http://localhost:8080/admin/dashboard \
-H "Authorization: Bearer admin-token"
# Expected Output:
# {"message":"Admin dashboard"}
# Modify any post (Admin can modify any post)
curl -X PUT http://localhost:8080/posts/123 \
-H "Authorization: Bearer admin-token" \
-H "Content-Type: application/json" \
-d '{"title":"Updated Post","content":"Updated content"}'
# Expected Output:
# {"message":"Updated post 123"}
Advanced Policy-based Authorization
For more complex applications, you might want to extend your policy system:
Combining Policies with Logical Operators
// And combines multiple policies with logical AND
func And(policies ...Policy) Policy {
return PolicyFunc(func(c echo.Context) bool {
for _, policy := range policies {
if !policy.Evaluate(c) {
return false
}
}
return true
})
}
// Or combines multiple policies with logical OR
func Or(policies ...Policy) Policy {
return PolicyFunc(func(c echo.Context) bool {
for _, policy := range policies {
if policy.Evaluate(c) {
return true
}
}
return false
})
}
// Not negates a policy
func Not(policy Policy) Policy {
return PolicyFunc(func(c echo.Context) bool {
return !policy.Evaluate(c)
})
}
Example Usage of Combined Policies
// Example: Register a complex policy
policyManager.Register(
"CanManageUsers",
Or(
policyManager.policies["IsAdmin"],
And(
policyManager.policies["IsAuthenticated"],
PolicyFunc(func(c echo.Context) bool {
user := c.Get("user").(User)
return user.Role == "manager"
}),
),
),
)
// Use combined policy in a route
e.DELETE("/users/:id", deleteUser, RequirePolicy("CanManageUsers"))
Dynamic Policy Evaluation
For even more flexibility, you can create policies that take parameters:
// CreateRolePolicy creates a policy that checks if a user has the specified role
func CreateRolePolicy(role string) Policy {
return PolicyFunc(func(c echo.Context) bool {
user := c.Get("user")
if user == nil {
return false
}
u, ok := user.(User)
if !ok {
return false
}
return u.Role == role
})
}
// Usage example
policyManager.Register("IsModerator", CreateRolePolicy("moderator"))
policyManager.Register("IsEditor", CreateRolePolicy("editor"))
Best Practices for Policy-based Authorization
- Keep policies focused: Each policy should evaluate a single authorization concern.
- Cache policy results: For complex policies that might involve database queries, consider caching results.
- Log policy failures: Include detailed logs when policies deny access to help with debugging and security auditing.
- Test your policies: Write unit tests to ensure policies behave as expected under various conditions.
- Document your policies: Maintain clear documentation about what each policy does and when it should be used.
Summary
In this tutorial, we've explored how to implement policy-based authorization in Echo applications:
- We defined a flexible policy interface and supporting structures
- We created a policy manager to register and evaluate policies
- We implemented middleware to enforce policies on routes
- We built practical examples with a blog application
- We explored advanced concepts like combining policies and dynamic policy creation
Policy-based authorization provides granular control over your application's security, allowing you to create complex access rules that match your exact business needs. By separating authorization logic into policies, you make your code more maintainable and adaptable to changing requirements.
Additional Resources
Exercises
- Implement a time-based policy that only allows access during business hours
- Create a rate-limiting policy that restricts the number of requests per user
- Build a policy that checks if a user has verified their email address
- Implement a policy for a multi-tenant application that checks if a user belongs to the correct organization
- Create a comprehensive test suite for your policies using Echo's testing utilities
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)