Golang RBAC Tutorial: Role-Based Access Control in Gin
This tutorial adds role-based access control (RBAC) to a REST API written in Go with Gin. At the end, three roles (admin, editor, viewer) are allowed different HTTP methods on different routes, and the rules live in a policy file that you can change without touching the handlers.
The same model and policy work in every Casbin implementation, so you can reuse them in the Node.js, Python, Java, and Rust versions of this tutorial. For gRPC services, see gRPC authorization in Go.
1. Install
go mod init example.com/rbac
go get github.com/casbin/casbin/v3 github.com/gin-gonic/gin
2. Write the model
The model says what a request looks like and how it is matched against the policy. Save this as 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) && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act)
r = sub, obj, act: each request is a user, a URL path, and an HTTP method.g = _, _: users can be assigned to roles.g(r.sub, p.sub): the request's user must have the role named in the policy rule.keyMatch2matches paths such as/api/articles/:id, andregexMatchlets one rule list several methods. See Functions for the other built-in matchers.
ทดลองใช้ K8s-gatekeeper Write the policy
Save this as policy.csv:
p, admin, /api/*, (GET)|(POST)|(PUT)|(DELETE)
p, editor, /api/articles, (GET)|(POST)
p, editor, /api/articles/:id, (GET)|(PUT)
p, viewer, /api/articles, GET
p, viewer, /api/articles/:id, GET
g, alice, admin
g, bob, editor
g, carol, viewer
Lines starting with p grant a role access to a path and a set of methods. Lines starting with g assign users to roles.
4. Add the middleware
The middleware calls Enforce with the user, the path, and the method, and rejects the request with 403 Forbidden when Casbin says no.
package main
import (
"log"
"net/http"
"github.com/casbin/casbin/v3"
"github.com/gin-gonic/gin"
)
// Authorize asks Casbin whether the current user may call this route.
func Authorize(e *casbin.Enforcer) gin.HandlerFunc {
return func(c *gin.Context) {
// Replace this with the user from your session or JWT.
user := c.GetHeader("X-User")
allowed, err := e.Enforce(user, c.Request.URL.Path, c.Request.Method)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if !allowed {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "forbidden"})
return
}
c.Next()
}
}
func main() {
e, err := casbin.NewEnforcer("model.conf", "policy.csv")
if err != nil {
log.Fatal(err)
}
r := gin.Default()
api := r.Group("/api", Authorize(e))
api.GET("/articles", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"articles": []string{}}) })
api.POST("/articles", func(c *gin.Context) { c.JSON(http.StatusCreated, gin.H{"created": true}) })
api.PUT("/articles/:id", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"updated": c.Param("id")}) })
api.DELETE("/articles/:id", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"deleted": c.Param("id")}) })
log.Fatal(r.Run(":8080"))
}
The X-User header keeps the example short. Casbin handles authorization only; in a real application the user name comes from your authentication layer, such as a verified JWT or a session.
5. Try it
go run .
curl -i -X POST -H "X-User: bob" http://localhost:8080/api/articles # 201, editors can create
curl -i -X DELETE -H "X-User: bob" http://localhost:8080/api/articles/1 # 403, editors cannot delete
curl -i -X DELETE -H "X-User: alice" http://localhost:8080/api/articles/1 # 200, admins can
curl -i -X POST -H "X-User: carol" http://localhost:8080/api/articles # 403, viewers are read-only