Authorization Patterns: RBAC and ABAC in Go

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]

    style RBAC fill:#e3f2fd
    style ABAC fill:#e8f5e9
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
// models/rbac.go
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"` // e.g., "users:read"
Resource string `json:"resource"` // e.g., "users"
Action string `json:"action"` // e.g., "read"
}

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
// services/rbac.go
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
// middleware/rbac.go
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())

// User management - requires specific permissions
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-only routes
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) {
// Create permissions
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})
}

// Create roles with permissions
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] style Policy fill:#e3f2fd

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
// models/abac.go
type Policy struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name"`
Description string `json:"description"`
Resource string `json:"resource"` // e.g., "documents"
Action string `json:"action"` // e.g., "read", "write", "*"
Conditions string `json:"conditions" gorm:"type:jsonb"` // JSON conditions
Effect string `json:"effect"` // "allow" or "deny"
Priority int `json:"priority"` // Higher = evaluated first
}

type PolicyCondition struct {
Attribute string `json:"attribute"` // e.g., "user.department"
Operator string `json:"operator"` // e.g., "equals", "in", "contains"
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
// services/abac.go
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 {
// Check if policy applies to this resource and action
if policy.Resource != req.Resource && policy.Resource != "*" {
continue
}
if policy.Action != req.Action && policy.Action != "*" {
continue
}

// Evaluate conditions
if s.evaluateConditions(policy.Conditions, userAttrs, resourceAttrs, req.Context) {
return policy.Effect == "allow"
}
}

// Default deny
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")

// Get user attributes
userAttrs := map[string]interface{}{
"id": userID,
"role": c.GetString("role"),
"department": c.GetString("department"),
}

// Get resource attributes (if accessing specific resource)
resourceAttrs := map[string]interface{}{}
if resourceID := c.Param("id"); resourceID != "" {
resourceAttrs = getResourceAttributes(c, resource, resourceID)
}

// Build context
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
// Policy: Users can only read documents from their department
policy1 := Policy{
Name: "department-documents",
Resource: "documents",
Action: "read",
Effect: "allow",
Conditions: `[
{"attribute": "user.department", "operator": "equals", "value": "resource.department"}
]`,
Priority: 10,
}

// Policy: Managers can edit any document in their department
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,
}

// Policy: Deny access outside business hours
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")

// First check: RBAC (does user's role have this permission?)
if !rbac.HasPermission(userID, resource, action) {
c.JSON(http.StatusForbidden, gin.H{"error": "Permission denied"})
c.Abort()
return
}

// Second check: ABAC (do attributes satisfy policies?)
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")

// Admins bypass ownership check
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()
}
}

// Usage
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]

    style RBAC fill:#e3f2fd
    style ABAC fill:#e8f5e9

Next post: Securing Go APIs: OWASP Best Practices - Protecting your API against common security vulnerabilities.

JWT Authentication in Go Securing Go APIs: OWASP Best Practices

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×