Перейти до основного контенту

ABAC in Go

Role-based access control answers "what may an editor do?". Many real rules need more than a role: authors may edit their own drafts, editors may edit articles from their own department, anyone may read a published article. These depend on attributes of the user and of the resource, which is what attribute-based access control (ABAC) is for.

This tutorial builds a small article API in Go with the standard net/http package and puts all of those rules in a Casbin policy file. The same model and policy work unchanged in the Node.js and Python versions of this tutorial. If you are still choosing between RBAC and ABAC, read Choosing an access control model first.

1. Install​

Go 1.22 or later is needed for the method and wildcard patterns in http.ServeMux.

go mod init example.com/abac
go get github.com/casbin/casbin/v3

2. Write the model​

Save this as model.conf:

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub_rule, obj_type, act

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = r.obj.Type == p.obj_type && r.act == p.act && eval(p.sub_rule)
  • r.sub and r.obj are not strings here but Go structs. The matcher reads their fields, such as r.obj.Type.
  • Each policy rule carries a condition in sub_rule. eval(p.sub_rule) evaluates that condition against the request, so the rules live in the policy instead of in the model.

3. Write the policy​

Save this as policy.csv:

p, r.obj.Status == 'published', article, read
p, r.sub.Name == r.obj.Owner, article, read
p, r.sub.Name == r.obj.Owner && r.obj.Status == 'draft', article, update
p, r.sub.Role == 'editor' && r.sub.Department == r.obj.Department, article, update
p, r.sub.Role == 'editor' && r.sub.Department == r.obj.Department, article, publish

Read each line as "allow this action on articles when the condition is true":

  1. Anyone may read a published article.
  2. Authors may read their own articles, including drafts.
  3. Authors may edit their own articles while they are drafts.
  4. Editors may edit any article from their own department.
  5. Editors may publish articles from their own department.
порада

Write string literals in conditions with single quotes, as in 'published'. A double quote in the middle of a field breaks CSV parsing. If a condition needs a comma, wrap the whole field in double quotes.

4. Write the server​

ABAC needs the resource itself, not just its URL, so the check runs after the article has been loaded. The authorize helper does that and writes the error response when the answer is no.

main.go
package main

import (
"encoding/json"
"log"
"net/http"
"sync"

"github.com/casbin/casbin/v3"
)

type User struct {
Name string
Role string
Department string
}

type Article struct {
ID string `json:"id"`
Type string `json:"-"`
Title string `json:"title"`
Owner string `json:"owner"`
Department string `json:"department"`
Status string `json:"status"`
}

// In a real application these come from your database.
var users = map[string]User{
"alice": {Name: "alice", Role: "author", Department: "engineering"},
"bob": {Name: "bob", Role: "editor", Department: "engineering"},
"carol": {Name: "carol", Role: "editor", Department: "sales"},
"dave": {Name: "dave", Role: "author", Department: "engineering"},
}

var (
mu sync.Mutex
articles = map[string]*Article{
"1": {ID: "1", Type: "article", Title: "Draft by alice", Owner: "alice", Department: "engineering", Status: "draft"},
"2": {ID: "2", Type: "article", Title: "Published by alice", Owner: "alice", Department: "engineering", Status: "published"},
}
)

type server struct {
e *casbin.Enforcer
}

// authorize loads the current user and asks Casbin whether they may perform act on the article.
func (s *server) authorize(w http.ResponseWriter, r *http.Request, a *Article, act string) bool {
// Replace this with the user from your session or JWT.
user, ok := users[r.Header.Get("X-User")]
if !ok {
http.Error(w, "unauthenticated", http.StatusUnauthorized)
return false
}
allowed, err := s.e.Enforce(user, *a, act)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return false
}
if !allowed {
http.Error(w, "forbidden", http.StatusForbidden)
return false
}
return true
}

func (s *server) article(w http.ResponseWriter, r *http.Request) *Article {
mu.Lock()
defer mu.Unlock()
a, ok := articles[r.PathValue("id")]
if !ok {
http.NotFound(w, r)
return nil
}
return a
}

func main() {
e, err := casbin.NewEnforcer("model.conf", "policy.csv")
if err != nil {
log.Fatal(err)
}
s := &server{e: e}

mux := http.NewServeMux()
mux.HandleFunc("GET /articles/{id}", func(w http.ResponseWriter, r *http.Request) {
a := s.article(w, r)
if a == nil || !s.authorize(w, r, a, "read") {
return
}
json.NewEncoder(w).Encode(a)
})
mux.HandleFunc("PUT /articles/{id}", func(w http.ResponseWriter, r *http.Request) {
a := s.article(w, r)
if a == nil || !s.authorize(w, r, a, "update") {
return
}
mu.Lock()
json.NewDecoder(r.Body).Decode(&struct {
Title *string `json:"title"`
}{&a.Title})
mu.Unlock()
json.NewEncoder(w).Encode(a)
})
mux.HandleFunc("POST /articles/{id}/publish", func(w http.ResponseWriter, r *http.Request) {
a := s.article(w, r)
if a == nil || !s.authorize(w, r, a, "publish") {
return
}
mu.Lock()
a.Status = "published"
mu.Unlock()
json.NewEncoder(w).Encode(a)
})

log.Fatal(http.ListenAndServe(":8080", mux))
}

Casbin reads struct fields through reflection, so the fields used in the policy (Name, Owner, Status, and so on) must be exported.

примітка

The X-User header keeps the example short. Casbin handles authorization only; in a real application the user comes from your authentication layer, such as a verified JWT or a session.

5. Try it​

go run .

Article 1 is alice's draft and article 2 is her published article, both in the engineering department.

curl -i -H "X-User: dave"  http://localhost:8080/articles/1                 # 403, someone else's draft
curl -i -H "X-User: dave" http://localhost:8080/articles/2 # 200, published
curl -i -X PUT -H "X-User: alice" -d '{"title":"New title"}' \
http://localhost:8080/articles/1 # 200, her own draft
curl -i -X PUT -H "X-User: alice" -d '{"title":"x"}' \
http://localhost:8080/articles/2 # 403, no longer a draft
curl -i -X PUT -H "X-User: bob" -d '{"title":"x"}' \
http://localhost:8080/articles/2 # 200, editor in engineering
curl -i -X PUT -H "X-User: carol" -d '{"title":"x"}' \
http://localhost:8080/articles/2 # 403, editor in sales
curl -i -X POST -H "X-User: alice" http://localhost:8080/articles/1/publish # 403, authors cannot publish
curl -i -X POST -H "X-User: bob" http://localhost:8080/articles/1/publish # 200
curl -i -H "X-User: dave" http://localhost:8080/articles/1 # 200, now published

6. Change rules at runtime​

Conditions are policy rows, so they can be added and removed while the server is running, and saved to a database through an adapter:

// Admins may edit any article.
e.AddPolicy("r.sub.Role == 'admin'", "article", "update")

// Nobody may publish any more.
e.RemoveFilteredPolicy(1, "article", "publish")

To find out which rule allowed a request, use EnforceEx. It returns the matching rule, which is useful for audit logs:

ok, rule, _ := e.EnforceEx(users["bob"], *articles["2"], "update")
// true [r.sub.Role == 'editor' && r.sub.Department == r.obj.Department article update]

ABAC or RBAC?​

  • Use RBAC when permissions follow job functions and do not depend on which record is being accessed. It is easier to audit: "list everything an editor can do" is a simple query. See the RBAC in Go tutorial.
  • Use ABAC when the answer depends on the record: its owner, department, status, or sensitivity.
  • Most applications combine both: roles decide which features a user can reach, and attributes narrow that down to the records they may touch. The comparison of access control models shows a combined model.

Next steps​

  • Full ABAC reference. ABAC covers JSON request parameters, quoting rules, and eval() in detail.
  • Rules about time and place. Add more request fields, for example an IP address checked with ipMatch, or the current time passed in as an attribute. See Functions.
  • Deny rules. Add an eft column and use a deny-override effect so that one rule can block what others allow. See Effector.
  • Experiment in the browser. The online editor can evaluate ABAC requests with JSON attributes.