Go RBAC Authorization using Gin, GORM, and Casbin
Casbin Model Configuration
The RBAC model is defined in access_control_model.conf:
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
r = sub, obj, act: Specifies the request parameters: subject (user or role), object (target resource or URL), and action (HTTP method or operation type).p = sub, obj, act: Defines the policy structure. For instance,editor, /documents, POSTpermits the editor role to perform POST requests on the/documentsresource.e = some(where (p.eft == allow)): The policy effect dictates that access is granted if any matched policy results in an allow decision.g = _, _: Establishes the role hierarchy structure. For example,david, editordesignates david as an editor.m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act: The matcher algorithm validates the request by resolving the subject's role, confirming the target object, and verifying the requested action.
Policy Storage
Authorization rules can be persisted in a CSV file or a relational database. A sample CSV format:
p, viewer, /documents, GET
p, editor, /documents, GET
p, editor, /documents, POST
g, emma, viewer
g, david, editor
When using a database, the schema and initial data resemble the following:
CREATE TABLE authorization_rule (
ptype VARCHAR(100),
v0 VARCHAR(100),
v1 VARCHAR(100),
v2 VARCHAR(100)
);
INSERT INTO authorization_rule VALUES('p', 'viewer', '/documents', 'GET');
INSERT INTO authorization_rule VALUES('p', 'editor', '/documents', 'GET');
INSERT INTO authorization_rule VALUES('p', 'editor', '/documents', 'POST');
INSERT INTO authorization_rule(ptype, v0, v1) VALUES('g', 'emma', 'viewer');
INSERT INTO authorization_rule(ptype, v0, v1) VALUES('g', 'david', 'editor');
Gin Middleware Implementation
Required dependencies:
github.com/casbin/gorm-adapter/v3
gorm.io/gorm
github.com/casbin/casbin/v2
Approach 1: Manually iterating through the user's assigned roles:
func ValidateRoleAccess(dbAdapter *gormadapter.Adapter) gin.HandlerFunc {
return func(ctx *gin.Context) {
targetPath := ctx.Request.URL.Path
httpVerb := ctx.Request.Method
// Simulated roles assigned to the authenticated user
userRoles := []string{"viewer"}
authEnforcer := casbin.NewEnforcer("config/access_control_model.conf", dbAdapter)
var permitted bool
var checkErr error
for _, role := range userRoles {
permitted, checkErr = authEnforcer.Enforce(role, targetPath, httpVerb)
if permitted {
break
}
}
if checkErr != nil {
ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": checkErr.Error()})
return
}
if !permitted {
ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "access denied"})
return
}
ctx.Next()
}
}
Approach 2: Direct user validation where Casbin automatically resolves role inheritance from the policy:
func ValidateUserAccess(dbAdapter *gormadapter.Adapter) gin.HandlerFunc {
return func(ctx *gin.Context) {
targetPath := ctx.Request.URL.Path
httpVerb := ctx.Request.Method
// Retrieve authenticated user identifier from context
currentUser := "david"
authEnforcer := casbin.NewEnforcer("config/access_control_model.conf", dbAdapter)
// Casbin automatically resolves roles assigned to currentUser via 'g' rules
permitted, checkErr := authEnforcer.Enforce(currentUser, targetPath, httpVerb)
if checkErr != nil {
ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": checkErr.Error()})
return
}
if !permitted {
ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "access denied"})
return
}
ctx.Next()
}
}