Gin Role-Based Access Control
Introduction
Role-Based Access Control (RBAC) is an approach to restricting system access to authorized users based on their roles within an organization. After implementing basic authentication in your Gin application, the next logical step is to control what authenticated users can actually do based on their assigned roles.
In this tutorial, you'll learn how to implement role-based access control in a Gin application, allowing certain routes to be accessible only by users with specific roles such as "admin", "editor", or "user".
Prerequisites
Before diving into role-based access control, make sure you:
- Have a basic understanding of Go and the Gin framework
- Have implemented user authentication in your Gin application
- Understand the basics of middleware in Gin
Understanding Role-Based Access Control
RBAC consists of three primary components:
- Roles: Categories that define the level of access (e.g., admin, user, editor)
- Permissions: Actions that can be performed (e.g., create, read, update, delete)
- Users: Individuals who are assigned specific roles
The main benefit of RBAC is that it simplifies managing access rights. Instead of assigning permissions directly to users, permissions are assigned to roles, and users are assigned to roles.
Implementing User Roles in Your Database
First, let's modify our user model to include roles:
type User struct {
ID uint `json:"id" gorm:"primary_key"`
Username string `json:"username" gorm:"unique"`
Password string `json:"-"` // Password won't be sent in JSON responses
Role string `json:"role"`
CreatedAt time.Time `json:"created_at"`
}
When registering users, you'll need to assign a default role:
func RegisterHandler(c *gin.Context) {
var user User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// Hash the password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
if err != nil {
c.JSON(500, gin.H{"error": "Failed to hash password"})
return
}
// Assign a default role (usually "user")
user.Role = "user"
user.Password = string(hashedPassword)
// Save the user to the database
if err := db.Create(&user).Error; err != nil {
c.JSON(500, gin.H{"error": "Failed to create user"})
return
}
c.JSON(201, gin.H{"message": "User registered successfully"})
}
Creating Role-Based Middleware
Next, let's create middleware to check if a user has the required role to access a route:
func RoleAuth(roles ...string) gin.HandlerFunc {
return func(c *gin.Context) {
// Get the user from the context (set during authentication)
userInterface, exists := c.Get("user")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized: user not found in context"})
c.Abort()
return
}
user, ok := userInterface.(User)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error: user type assertion failed"})
c.Abort()
return
}
// Check if the user's role is in the allowed roles
roleAllowed := false
for _, role := range roles {
if user.Role == role {
roleAllowed = true
break
}
}
if !roleAllowed {
c.JSON(http.StatusForbidden, gin.H{"error": "Forbidden: insufficient permissions"})
c.Abort()
return
}
c.Next()
}
}
Protecting Routes Based on Roles
Now let's use our middleware to protect different routes based on user roles:
func SetupRouter() *gin.Engine {
router := gin.Default()
// Public routes
public := router.Group("/api")
{
public.POST("/register", RegisterHandler)
public.POST("/login", LoginHandler)
}
// Protected routes that require authentication
protected := router.Group("/api")
protected.Use(AuthMiddleware()) // Your authentication middleware
{
// Routes accessible by all authenticated users
protected.GET("/profile", GetProfileHandler)
// Routes only for users with "editor" or "admin" role
editorRoutes := protected.Group("/content")
editorRoutes.Use(RoleAuth("editor", "admin"))
{
editorRoutes.POST("/articles", CreateArticleHandler)
editorRoutes.PUT("/articles/:id", UpdateArticleHandler)
}
// Routes only for "admin" role
adminRoutes := protected.Group("/admin")
adminRoutes.Use(RoleAuth("admin"))
{
adminRoutes.GET("/users", ListUsersHandler)
adminRoutes.DELETE("/users/:id", DeleteUserHandler)
adminRoutes.PUT("/users/:id/role", UpdateUserRoleHandler)
}
}
return router
}
Implementing a Role Update Handler
Let's create a handler that allows administrators to update user roles:
func UpdateUserRoleHandler(c *gin.Context) {
userID := c.Param("id")
var roleUpdate struct {
Role string `json:"role" binding:"required"`
}
if err := c.ShouldBindJSON(&roleUpdate); err != nil {
c.JSON(400, gin.H{"error": "Invalid request"})
return
}
// Validate the role
allowedRoles := []string{"admin", "editor", "user"}
validRole := false
for _, role := range allowedRoles {
if roleUpdate.Role == role {
validRole = true
break
}
}
if !validRole {
c.JSON(400, gin.H{"error": "Invalid role"})
return
}
// Find the user
var user User
if err := db.First(&user, userID).Error; err != nil {
c.JSON(404, gin.H{"error": "User not found"})
return
}
// Update the user's role
user.Role = roleUpdate.Role
if err := db.Save(&user).Error; err != nil {
c.JSON(500, gin.H{"error": "Failed to update role"})
return
}
c.JSON(200, gin.H{"message": "Role updated successfully"})
}
Real-World Example: Blog Application
Let's create a practical example of a blog application with different user roles:
- User - Can read articles and post comments
- Editor - Can create and edit articles
- Admin - Can manage users and all content
Here's how we might set up the routes:
func SetupBlogRouter() *gin.Engine {
router := gin.Default()
// Public routes for viewing content
router.GET("/articles", GetArticlesHandler)
router.GET("/articles/:id", GetArticleByIDHandler)
// Authentication routes
auth := router.Group("/auth")
{
auth.POST("/register", RegisterHandler)
auth.POST("/login", LoginHandler)
}
// User routes (requires basic authentication)
user := router.Group("/user")
user.Use(AuthMiddleware())
{
user.GET("/profile", GetProfileHandler)
user.POST("/articles/:id/comments", PostCommentHandler)
user.PUT("/profile", UpdateProfileHandler)
}
// Editor routes
editor := router.Group("/editor")
editor.Use(AuthMiddleware(), RoleAuth("editor", "admin"))
{
editor.POST("/articles", CreateArticleHandler)
editor.PUT("/articles/:id", UpdateArticleHandler)
editor.DELETE("/articles/:id", DeleteArticleHandler)
editor.GET("/dashboard", EditorDashboardHandler)
}
// Admin routes
admin := router.Group("/admin")
admin.Use(AuthMiddleware(), RoleAuth("admin"))
{
admin.GET("/users", ListUsersHandler)
admin.PUT("/users/:id/role", UpdateUserRoleHandler)
admin.DELETE("/users/:id", DeleteUserHandler)
admin.GET("/dashboard", AdminDashboardHandler)
admin.GET("/stats", SiteStatsHandler)
}
return router
}
Adding Permission-Based Controls
For more granular control, you can also implement permission-based access:
type Permission string
const (
PermissionReadArticle Permission = "read:article"
PermissionCreateArticle Permission = "create:article"
PermissionEditArticle Permission = "edit:article"
PermissionDeleteArticle Permission = "delete:article"
PermissionManageUsers Permission = "manage:users"
)
var RolePermissions = map[string][]Permission{
"user": {
PermissionReadArticle,
},
"editor": {
PermissionReadArticle,
PermissionCreateArticle,
PermissionEditArticle,
},
"admin": {
PermissionReadArticle,
PermissionCreateArticle,
PermissionEditArticle,
PermissionDeleteArticle,
PermissionManageUsers,
},
}
func PermissionAuth(requiredPermission Permission) gin.HandlerFunc {
return func(c *gin.Context) {
userInterface, exists := c.Get("user")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
c.Abort()
return
}
user, ok := userInterface.(User)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "User type assertion failed"})
c.Abort()
return
}
// Get permissions for the role
permissions, exists := RolePermissions[user.Role]
if !exists {
c.JSON(http.StatusForbidden, gin.H{"error": "Role has no permissions"})
c.Abort()
return
}
// Check if the required permission is in the role's permissions
hasPermission := false
for _, permission := range permissions {
if permission == requiredPermission {
hasPermission = true
break
}
}
if !hasPermission {
c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
c.Abort()
return
}
c.Next()
}
}
Then you can use this permission-based middleware:
// Protect a route with specific permission
router.DELETE("/articles/:id", AuthMiddleware(),
PermissionAuth(PermissionDeleteArticle), DeleteArticleHandler)
Testing Role-Based Access
Here's how you can test your RBAC implementation:
- First, register users with different roles:
# Register a regular user
curl -X POST http://localhost:8080/auth/register \
-H "Content-Type: application/json" \
-d '{"username":"user1","password":"password123"}'
# Later, use admin account to update roles
curl -X PUT http://localhost:8080/admin/users/2/role \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-d '{"role":"editor"}'
- Then try accessing protected routes with different user roles:
# Login as a regular user
curl -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"user1","password":"password123"}'
# Output:
# {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
# Try accessing admin route with regular user token
curl -X GET http://localhost:8080/admin/users \
-H "Authorization: Bearer USER_TOKEN"
# Output:
# {"error":"Forbidden: insufficient permissions"}
# Try accessing editor route with editor token
curl -X POST http://localhost:8080/editor/articles \
-H "Content-Type: application/json" \
-H "Authorization: Bearer EDITOR_TOKEN" \
-d '{"title":"New Article","content":"This is a test article"}'
# Output:
# {"message":"Article created successfully","id":1}
Best Practices for RBAC
- Least Privilege Principle: Assign users the minimum level of access required for their job.
- Role Hierarchies: Consider implementing role hierarchies where higher roles inherit permissions from lower roles.
- Auditing: Log role changes and access control decisions for security audits.
- Dynamic Role Checking: For critical operations, re-check roles from the database rather than relying solely on tokens.
- Role Expiry: Consider implementing role expiry for temporary role elevations.
Summary
In this tutorial, you've learned how to implement role-based access control in a Gin application:
- We added roles to our user model
- Created middleware for role-based authorization
- Protected routes based on roles
- Implemented more granular permission-based controls
- Created practical examples in a blog application context
- Tested RBAC implementation
Role-based access control is a crucial component of application security, allowing you to restrict access based on user roles and protect sensitive operations from unauthorized users.
Additional Resources and Exercises
Resources
- Gin Framework Documentation
- OWASP Authorization Cheat Sheet
- JWT.io for inspecting JWT tokens
Exercises
-
Implement Role Hierarchy: Modify the RBAC system to implement a hierarchy where "admin" inherits all permissions from "editor" and "editor" inherits from "user".
-
Add Role-Based UI Elements: Create a frontend that shows/hides UI elements based on the user's role.
-
Create a Role Management Dashboard: Build an admin dashboard that allows managing roles for all users.
-
Resource-Based Authorization: Extend the RBAC system to include resource-based authorization, such as allowing editors to only edit articles they created.
-
Implement Role Timeouts: Add the ability to assign temporary roles that expire after a set time.
By implementing proper role-based access control, you'll significantly enhance the security of your Gin web applications and ensure users can only perform actions appropriate to their roles.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)