After authentication confirms user identity, authorization determines what they can access. This post covers two powerful authorization models: Role-Based Access Control (RBAC) and Attribute-Based Access Control (ABAC), with practical Go implementations.
Authorization Models Overview flowchart TD
subgraph Models["Authorization Models"]
ACL[ACL - Access Control Lists]
RBAC[RBAC - Role-Based]
ABAC[ABAC - Attribute-Based]
end
ACL --> |User -> Resource| Simple[Simple permissions]
RBAC --> |User -> Role -> Permission| Medium[Scalable roles]
ABAC --> |Policies + Attributes| Complex[Dynamic rules]
classDef blueClass fill:#4A90E2,stroke:#333,stroke-width:2px,color:#fff
classDef greenClass fill:#27AE60,stroke:#333,stroke-width:2px,color:#fff
class RBAC blueClass
class ABAC greenClass
Model
Complexity
Use Case
ACL
Low
Simple apps, file systems
RBAC
Medium
Most applications
ABAC
High
Complex, dynamic requirements
Role-Based Access Control (RBAC) Core Concepts flowchart LR
U[User] --> R[Role]
R --> P[Permissions]
subgraph Roles
R1[Admin]
R2[Manager]
R3[User]
end
subgraph Permissions
P1[Create]
P2[Read]
P3[Update]
P4[Delete]
end
R1 --> P1
R1 --> P2
R1 --> P3
R1 --> P4
R2 --> P2
R2 --> P3
R3 --> P2
RBAC Implementation Database Models 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 type Role struct { ID uint `json:"id" gorm:"primaryKey"` Name string `json:"name" gorm:"uniqueIndex;not null"` Description string `json:"description"` Permissions []Permission `json:"permissions" gorm:"many2many:role_permissions"` CreatedAt time.Time `json:"created_at"` } type Permission struct { ID uint `json:"id" gorm:"primaryKey"` Name string `json:"name" gorm:"uniqueIndex;not null"` Resource string `json:"resource"` Action string `json:"action"` } type User struct { ID uint `json:"id" gorm:"primaryKey"` Username string `json:"username"` Email string `json:"email"` RoleID uint `json:"role_id"` Role Role `json:"role" gorm:"foreignKey:RoleID"` }
Permission Service 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 type RBACService struct { db *gorm.DB } func NewRBACService (db *gorm.DB) *RBACService { return &RBACService{db: db} } func (s *RBACService) HasPermission(userID uint , resource, action string ) bool { var count int64 permissionName := fmt.Sprintf("%s:%s" , resource, action) s.db.Model(&User{}). Joins("JOIN roles ON roles.id = users.role_id" ). Joins("JOIN role_permissions ON role_permissions.role_id = roles.id" ). Joins("JOIN permissions ON permissions.id = role_permissions.permission_id" ). Where("users.id = ? AND permissions.name = ?" , userID, permissionName). Count(&count) return count > 0 } func (s *RBACService) GetUserPermissions(userID uint ) ([]string , error ) { var permissions []string err := s.db.Model(&Permission{}). Joins("JOIN role_permissions ON role_permissions.permission_id = permissions.id" ). Joins("JOIN roles ON roles.id = role_permissions.role_id" ). Joins("JOIN users ON users.role_id = roles.id" ). Where("users.id = ?" , userID). Pluck("permissions.name" , &permissions).Error return permissions, err } func (s *RBACService) AssignRole(userID, roleID uint ) error { return s.db.Model(&User{}).Where("id = ?" , userID).Update("role_id" , roleID).Error }
RBAC Middleware 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 func RequirePermission (rbacService *RBACService, resource, action string ) gin.HandlerFunc { return func (c *gin.Context) { userID := c.GetUint("user_id" ) if !rbacService.HasPermission(userID, resource, action) { c.JSON(http.StatusForbidden, gin.H{ "error" : "Permission denied" , "required" : fmt.Sprintf("%s:%s" , resource, action), }) c.Abort() return } c.Next() } } func RequireRole (allowedRoles ...string ) gin.HandlerFunc { return func (c *gin.Context) { userRole := c.GetString("role" ) for _, role := range allowedRoles { if userRole == role { c.Next() return } } c.JSON(http.StatusForbidden, gin.H{ "error" : "Insufficient role" , }) c.Abort() } }
Using RBAC in Routes 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func SetupRoutes (r *gin.Engine, rbacService *RBACService) { api := r.Group("/api/v1" ) api.Use(AuthMiddleware()) users := api.Group("/users" ) { users.GET("" , RequirePermission(rbacService, "users" , "read" ), listUsers) users.POST("" , RequirePermission(rbacService, "users" , "create" ), createUser) users.PUT("/:id" , RequirePermission(rbacService, "users" , "update" ), updateUser) users.DELETE("/:id" , RequirePermission(rbacService, "users" , "delete" ), deleteUser) } admin := api.Group("/admin" ) admin.Use(RequireRole("admin" )) { admin.GET("/stats" , getStats) admin.POST("/roles" , createRole) } }
Seeding Roles and Permissions 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 func SeedRBAC (db *gorm.DB) { permissions := []Permission{ {Name: "users:read" , Resource: "users" , Action: "read" }, {Name: "users:create" , Resource: "users" , Action: "create" }, {Name: "users:update" , Resource: "users" , Action: "update" }, {Name: "users:delete" , Resource: "users" , Action: "delete" }, {Name: "orders:read" , Resource: "orders" , Action: "read" }, {Name: "orders:create" , Resource: "orders" , Action: "create" }, {Name: "orders:update" , Resource: "orders" , Action: "update" }, {Name: "orders:delete" , Resource: "orders" , Action: "delete" }, } for _, p := range permissions { db.FirstOrCreate(&p, Permission{Name: p.Name}) } var allPerms, userPerms, managerPerms []Permission db.Find(&allPerms) db.Where("name LIKE ?" , "%:read%" ).Or("action = ?" , "create" ).Find(&userPerms) db.Where("action IN ?" , []string {"read" , "create" , "update" }).Find(&managerPerms) roles := []Role{ {Name: "admin" , Description: "Full access" , Permissions: allPerms}, {Name: "manager" , Description: "Manage resources" , Permissions: managerPerms}, {Name: "user" , Description: "Basic access" , Permissions: userPerms}, } for _, r := range roles { var existing Role if db.Where("name = ?" , r.Name).First(&existing).Error != nil { db.Create(&r) } } }
Attribute-Based Access Control (ABAC) ABAC makes decisions based on attributes of the user, resource, action, and environment.
flowchart TD
subgraph Attributes
U[User Attributes Department, Level]
R[Resource Attributes Owner, Sensitivity]
A[Action Attributes Read, Write]
E[Environment Time, Location]
end
subgraph Policy["Policy Engine"]
P[Policy Rules]
end
U --> P
R --> P
A --> P
E --> P
P --> D{Decision}
D -->|Allow| Allow[Access Granted]
D -->|Deny| Deny[Access Denied]
classDef blueClass fill:#4A90E2,stroke:#333,stroke-width:2px,color:#fff
class Policy blueClass
ABAC Implementation Policy Definition 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 type Policy struct { ID uint `json:"id" gorm:"primaryKey"` Name string `json:"name"` Description string `json:"description"` Resource string `json:"resource"` Action string `json:"action"` Conditions string `json:"conditions" gorm:"type:jsonb"` Effect string `json:"effect"` Priority int `json:"priority"` } type PolicyCondition struct { Attribute string `json:"attribute"` Operator string `json:"operator"` Value interface {} `json:"value"` } type AccessRequest struct { UserID uint `json:"user_id"` Resource string `json:"resource"` ResourceID string `json:"resource_id"` Action string `json:"action"` Context map [string ]interface {} `json:"context"` }
ABAC Service 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 type ABACService struct { db *gorm.DB policies []Policy } func NewABACService (db *gorm.DB) *ABACService { svc := &ABACService{db: db} svc.LoadPolicies() return svc } func (s *ABACService) LoadPolicies() { s.db.Order("priority DESC" ).Find(&s.policies) } func (s *ABACService) Evaluate(req AccessRequest, userAttrs, resourceAttrs map [string ]interface {}) bool { for _, policy := range s.policies { if policy.Resource != req.Resource && policy.Resource != "*" { continue } if policy.Action != req.Action && policy.Action != "*" { continue } if s.evaluateConditions(policy.Conditions, userAttrs, resourceAttrs, req.Context) { return policy.Effect == "allow" } } return false } func (s *ABACService) evaluateConditions(conditionsJSON string , user, resource, context map [string ]interface {}) bool { var conditions []PolicyCondition json.Unmarshal([]byte (conditionsJSON), &conditions) for _, cond := range conditions { value := s.resolveAttribute(cond.Attribute, user, resource, context) if !s.evaluateCondition(cond, value) { return false } } return true } func (s *ABACService) resolveAttribute(attr string , user, resource, context map [string ]interface {}) interface {} { parts := strings.SplitN(attr, "." , 2 ) if len (parts) != 2 { return nil } var source map [string ]interface {} switch parts[0 ] { case "user" : source = user case "resource" : source = resource case "context" : source = context default : return nil } return source[parts[1 ]] } func (s *ABACService) evaluateCondition(cond PolicyCondition, value interface {}) bool { switch cond.Operator { case "equals" : return value == cond.Value case "not_equals" : return value != cond.Value case "in" : if arr, ok := cond.Value.([]interface {}); ok { for _, v := range arr { if value == v { return true } } } return false case "contains" : if str, ok := value.(string ); ok { return strings.Contains(str, cond.Value.(string )) } return false case "greater_than" : return toFloat(value) > toFloat(cond.Value) case "less_than" : return toFloat(value) < toFloat(cond.Value) default : return false } }
ABAC Middleware 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 func ABACMiddleware (abacService *ABACService, resource, action string ) gin.HandlerFunc { return func (c *gin.Context) { userID := c.GetUint("user_id" ) userAttrs := map [string ]interface {}{ "id" : userID, "role" : c.GetString("role" ), "department" : c.GetString("department" ), } resourceAttrs := map [string ]interface {}{} if resourceID := c.Param("id" ); resourceID != "" { resourceAttrs = getResourceAttributes(c, resource, resourceID) } context := map [string ]interface {}{ "time" : time.Now(), "ip" : c.ClientIP(), "method" : c.Request.Method, } req := AccessRequest{ UserID: userID, Resource: resource, Action: action, Context: context, } if !abacService.Evaluate(req, userAttrs, resourceAttrs) { c.JSON(http.StatusForbidden, gin.H{"error" : "Access denied by policy" }) c.Abort() return } c.Next() } }
Example ABAC Policies 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 policy1 := Policy{ Name: "department-documents" , Resource: "documents" , Action: "read" , Effect: "allow" , Conditions: `[ {"attribute": "user.department", "operator": "equals", "value": "resource.department"} ]` , Priority: 10 , } policy2 := Policy{ Name: "manager-edit" , Resource: "documents" , Action: "write" , Effect: "allow" , Conditions: `[ {"attribute": "user.role", "operator": "equals", "value": "manager"}, {"attribute": "user.department", "operator": "equals", "value": "resource.department"} ]` , Priority: 20 , } policy3 := Policy{ Name: "business-hours" , Resource: "*" , Action: "*" , Effect: "deny" , Conditions: `[ {"attribute": "context.hour", "operator": "less_than", "value": 9} ]` , Priority: 100 , }
Combining RBAC and ABAC 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 func CombinedAuthMiddleware (rbac *RBACService, abac *ABACService, resource, action string ) gin.HandlerFunc { return func (c *gin.Context) { userID := c.GetUint("user_id" ) if !rbac.HasPermission(userID, resource, action) { c.JSON(http.StatusForbidden, gin.H{"error" : "Permission denied" }) c.Abort() return } userAttrs := getUserAttributes(c) resourceAttrs := getResourceAttributes(c, resource, c.Param("id" )) req := AccessRequest{ UserID: userID, Resource: resource, Action: action, } if !abac.Evaluate(req, userAttrs, resourceAttrs) { c.JSON(http.StatusForbidden, gin.H{"error" : "Policy denied access" }) c.Abort() return } c.Next() } }
Resource Ownership Check 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 func ResourceOwnerMiddleware (resource string , getOwnerID func (c *gin.Context) uint ) gin.HandlerFunc { return func (c *gin.Context) { userID := c.GetUint("user_id" ) userRole := c.GetString("role" ) if userRole == "admin" { c.Next() return } ownerID := getOwnerID(c) if ownerID != userID { c.JSON(http.StatusForbidden, gin.H{"error" : "Not owner of this resource" }) c.Abort() return } c.Next() } } router.PUT("/orders/:id" , AuthMiddleware(), ResourceOwnerMiddleware("orders" , func (c *gin.Context) uint { var order Order db.First(&order, c.Param("id" )) return order.UserID }), updateOrder, )
Summary
Model
Best For
Implementation
RBAC
Most applications
Roles + Permissions tables
ABAC
Complex, dynamic rules
Policy engine + attributes
Combined
Enterprise apps
RBAC base + ABAC overrides
flowchart TD
R[Request] --> Auth[Authentication]
Auth --> RBAC{RBAC Check}
RBAC -->|Has Role| ABAC{ABAC Check}
RBAC -->|No Role| D1[Deny]
ABAC -->|Policy Allows| Allow[Allow]
ABAC -->|Policy Denies| D2[Deny]
classDef blueClass fill:#4A90E2,stroke:#333,stroke-width:2px,color:#fff
classDef greenClass fill:#27AE60,stroke:#333,stroke-width:2px,color:#fff
class RBAC blueClass
class ABAC greenClass
Next post: Securing Go APIs: OWASP Best Practices - Protecting your API against common security vulnerabilities.
Comments