Casbin: access control in go. Part 1
Here we go again... The idea for this article has been brewing in my mind for a long time. The problem of access control isn't new, nor are the tools that solve it. Speaking of Go - I've seen quite a variety...
In modern management systems, access to resources is a critical security component. As a system grows, the number of users increases, and the organizational structure becomes more complex, there is a need for a flexible and reliable access control mechanism. Traditional approaches, such as hardcoded permission checks or simple role systems, often prove insufficient for complex business requirements.
This is where Casbin comes to help - an open-source library that allows implementing complex access policies without complicating business logic code. The series will consist of two articles, and in the first part, as usual, we'll look at everything from a theoretical perspective. We'll discuss types of access control, models and their capabilities, functions, and much more.
I want to note that the result of this series will be an open-source abstraction over Casbin in Go (RBAC). Why? The answer is quite simple - Casbin's capabilities are quite extensive, but everything needs to be written manually. Moreover, the out-of-the-box functionality covers (as far as my experience allows me to claim) about 90+% of what's needed for any SaaS system.
How did the idea come about?
As I mentioned - the idea came up quite a while ago, but the opportunity to try writing a wrapper over Casbin came about six months ago when the startup I work for needed a rather flexible access control system. At the same time, there was a requirement for the structure - three levels were conceived, which depend on each other:
- Organization level - the base level where team management, organization information, etc. is handled.
- Project level - allows separating individual projects and granting access only to necessary people or groups, and only in required projects.
- Environment level - the highest level where access to specific software applications can be restricted.
And my colleague, Herman, (huge thanks to him for the work done!) implemented all the necessary requirements, and it worked amazingly well! For obvious reasons, I can't share that code here, but I decided that we could create a certain wrapper based on what Herman did. Moreover, give developers the ability to implement the model they need with their specific requirements! We'll only look at RBAC though.
Types of Access Control
Each type of access control has its advantages and limitations that should be considered when designing a security system. Let's examine the main approaches to access control and their features in modern systems to better understand which approach best suits specific cases.
Discretionary Access Control (DAC)
A system where the resource owner determines access rights. For example, Google Docs, where the document author decides who has access.
Pros: intuitive, flexible settings, quick rights management, delegation capability, minimal administrator overhead
Cons: risks from incorrect permission granting, difficult propagation control, possibility of overly broad permissions, lack of centralization
Mandatory Access Control (MAC)
A system where access rights are determined centrally based on security levels. Used in government and military systems, where documents have security classifications (top secret, secret, confidential), and users have clearance levels.
Pros: high security, clear hierarchy, minimal data leakage, centralized management, easy implementation in government structures
Cons: low flexibility, complex administration, high costs, possible delays, limited scalability
Role-Based Access Control (RBAC)
A system where users are assigned roles, and roles are assigned sets of access rights. The most common approach in corporate systems and web applications, where access rights are determined through user roles.
Pros: easy management, good scalability, low administrative costs, adherence to least privilege principle
Cons: possible role proliferation, complex fine-tuning, potential conflicts
Access Control Lists (ACL)
A mechanism where a list of users and their rights is defined for each resource. A classic example is the Unix file system with read, write, and execute permissions.
Pros: flexible settings, easy system integration, transparent access rights
Cons: complex administration at scale, performance issues, lack of hierarchy
Attribute-Based Access Control (ABAC)
Access is determined based on user attributes, resource, and environment. A modern flexible approach that considers context when making access decisions.
Pros: high flexibility, context awareness, complex logic capability
Cons: complex implementation, high cost, performance impact
Organization-Based Access Control (OrBAC)
An extended version of RBAC that considers organizational context and hierarchy. Suitable for large organizations with complex structures and various access policies.
Pros: support for complex structures, policy flexibility, delegation capability
Cons: complex implementation, requires special expertise, maintenance complexity
In practice, hybrid approaches are often used. For example, a system might be based on RBAC but include ABAC elements for more precise access control in special cases.
Comparison Table
| Characteristic / Type | DAC | MAC | RBAC | ACL | ABAC | OrBAC |
|---|---|---|---|---|---|---|
| Flexibility | High | Low | High | Medium | Very High | High |
| Security | Medium | Very High | High | Medium | High | High |
| Administration Complexity | Low | High | Medium | High | High | High |
| Scalability | Medium | Medium | High | Low | High | Very High |
| Typical Use Cases | Cloud storage, shared documents | Military systems, government institutions | Corporate systems, web applications | File systems, network equipment | Complex business systems, banking applications | Large organizations with complex structure |
Introduction to Casbin
Casbin is a robust open-source access control library that supports various control models for complete authorization, such as ACL, RBAC, and ABAC.
An important advantage worth noting is that Casbin is implemented in many languages: Go, Java, C/C++, NodeJS, PHP, Python, C#, Ruby and others.
How It Works - An Example
Casbin works like a security guard standing at the archive door checking access passes. When a user wants to do something (like edit a document), Casbin checks its rule book and decides: "yes, access granted" or "no, sorry, access denied".
A key advantage of the library is that these "rules" can be written with great flexibility. Need a simple system with just users and admins? No problem. Need a complex scheme where access depends on time of day, weather outside, and moon phase? That's possible too.
An important element is the model file (usually model.conf), which can be thought of as an instruction manual for the guard: "if you see a red badge - allow access to the server room, if blue - office only". All specific rules (who has which badge) are stored separately - in a database or file.
The developer essentially communicates with the guard through a simple interface: "Can user Vladyslav edit document #123?" (e.Enforce("vladyslav", "doc123", "edit")). And receives a simple answer: yes or no.
What is a Casbin Model?
Let's look at a typical Casbin model for RBAC:
[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
To describe the required logic, we need to configure the model.conf file. This file is based on the PERM metamodel, which consists of four key components:
- Policy - defines access rules and permissions in the system
- Effect - specifies how rules are applied (for example, whether one permission is sufficient)
- Request - describes the structure of access check requests
- Matchers - defines the logic for comparing requests with rules
Let's look at these components in detail.
Policy
The [policy_definition] section defines the format of access rules. In our case:
p = sub, obj, act means that each rule consists of:
- sub (subject) - subject (user or role)
- obj (object) - object (resource)
- act (action) - action (operation)
Effect
The [policy_effect] section with the rule e = some(where (p.eft == allow)) specifies that access is granted if at least one rule allows the action.
Let's break down these functions in detail:
- some() - an aggregator function that checks if at least one element meets the condition
- where() - a filtering function that defines the condition for checking
- p.eft - policy effect, which can have values "allow" or "deny"
Built-in policy effects supported:
| Policy Effect | Value | Example | | ------------------------------------------------------------ | ---------------------- | --------------------------------------------------------------------- | -------- | ------------------------------------------------------------- | | some(where (p.eft == allow)) | allow-override | ACL, RBAC, etc. | | !some(where (p.eft == deny)) | deny-override | Deny-override | | some(where (p.eft == allow)) && !some(where (p.eft == deny)) | allow-and-deny | Allow-and-deny | | priority(p.eft) | | deny | priority | Priority | | subjectPriority(p.eft) | priority based on role | Subject-Priority |
Request
The [request_definition] section defines the format of access check requests. The structure r = sub, obj, act corresponds to the policy structure.
Matchers
The [matchers] section with the rule m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act defines the access check logic:
g(r.sub, p.sub)- checks if the user belongs to the roler.obj == p.obj- matches the request object with the policy objectr.act == p.act- verifies the requested action matches
Additionally, the model includes [role_definition] with g = _, _, which allows creating a role hierarchy where the first parameter inherits the rights of the second.
Functions in Matchers
Casbin provides several built-in functions that can be used in matchers:
- keyMatch() - checks key matching, supports _ wildcard. Example:
keyMatch("/foo/bar", "/foo/_") - keyMatch2() - extended version of keyMatch, supports
:paramin URLs. Example:keyMatch2("/alice/data/123", "/alice/data/:id") - keyMatch3() - supports in URLs. Example:
keyMatch3("/alice/data/123", "/alice/data/{id}") - regexMatch() - regex-based checking. Example:
regexMatch("user.123", "user.[0-9]+") - ipMatch() - IP address and CIDR checking. Example:
ipMatch("192.168.2.123", "192.168.2.0/24") - globMatch() - glob pattern checking. Example:
globMatch("/foo/*", "/foo/bar")
Additionally, you can add custom functions, making the processing very flexible.
Let's create a custom function that checks string suffixes:
// direct function implementation
func hasSuffix(str string, suffix string) bool {
return strings.HasSuffix(str, suffix)
}
// wrapper for adding to enforcer (casbin)
func hasSuffixFunc(args ...any) (any, error) {
str := args[0].(string)
suffix := args[1].(string)
return hasSuffix(str, suffix), nil
}
// function registration
enforcer.AddFunction("hasSuffix", hasSuffixFunc)Now this function can be used in the matcher. For example:
[matchers]
m = r.sub == p.sub && keyMatch(r.obj, p.obj) && (r.act == p.act || hasSuffix(r.act, "-admin"))
This will allow checking suffixes in objects. For example, if you have a rule for .pdf files, you can use it like this:
// add rule for PDF files
enforcer.AddPolicy("admin", "*.pdf", "read")
enforcer.AddPolicy("admin", "*.pdf", "create")
enforcer.AddPolicy("user", "*.pdf", "read")
// check access
allowed, _ := enforcer.Enforce("admin", "app.pdf", "read") // true
allowed, _ = enforcer.Enforce("admin", "app.pdf", "create-admin") // true
allowed, _ = enforcer.Enforce("user", "app.pdf", "create") // falseYou can verify this yourself using the Online Editor: https://editor.casbin.org/#E95ET7TJT
Super admin
You can add a rule to ignore all checks if sub is a super admin:
[matchers]
m = (r.sub == p.sub && keyMatch(r.obj, p.obj) && (r.act == p.act || hasSuffix(r.act, "-admin"))) || r.sub == "superadmin"Model Storage
There are 3 ways to import the model:
-
File
.conf -
Directly from code:
goimport ( "github.com/casbin/casbin/v2" "github.com/casbin/casbin/v2/model" "github.com/casbin/casbin/v2/persist/file-adapter" ) // initialize model in Go m := model.NewModel() m.AddDef("r", "r", "sub, obj, act") m.AddDef("p", "p", "sub, obj, act") m.AddDef("g", "g", "_, _") m.AddDef("e", "e", "some(where (p.eft == allow))") m.AddDef("m", "m", "g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act") // load policy rules from .CSV // you can use other adapters (e.g., for PostgreSQL etc.) a := fileadapter.NewAdapter("policies.csv") // create our enforcer e := casbin.NewEnforcer(m, a) -
Pass the model as a string:
goimport ( "github.com/casbin/casbin/v2" "github.com/casbin/casbin/v2/model" ) // write model as a string strModel := ` [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 ` // create model m, err := model.NewModelFromString(strModel) if err != nil { return log.Fatal(err) }
Adapter
Adapter in Casbin is an interface that defines how policy rules are stored and loaded. The main adapter interface looks like this:
// Adapter is the interface for Casbin adapters.
type Adapter interface {
// LoadPolicy loads all policy rules from the storage.
LoadPolicy(model model.Model) error
// SavePolicy saves all policy rules to the storage.
SavePolicy(model model.Model) error
// AddPolicy adds a policy rule to the storage.
// This is part of the Auto-Save feature.
AddPolicy(sec string, ptype string, rule []string) error
// RemovePolicy removes a policy rule from the storage.
// This is part of the Auto-Save feature.
RemovePolicy(sec string, ptype string, rule []string) error
// RemoveFilteredPolicy removes policy rules that match the filter from the storage.
// This is part of the Auto-Save feature.
RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error
}
// BatchAdapter is the interface for Casbin adapters with multiple add and remove policy functions.
type BatchAdapter interface {
Adapter
// AddPolicies adds policy rules to the storage.
// This is part of the Auto-Save feature.
AddPolicies(sec string, ptype string, rules [][]string) error
// RemovePolicies removes policy rules from the storage.
// This is part of the Auto-Save feature.
RemovePolicies(sec string, ptype string, rules [][]string) error
}
// FilteredAdapter is the interface for Casbin adapters supporting filtered policies.
type FilteredAdapter interface {
Adapter
// LoadFilteredPolicy loads only policy rules that match the filter.
LoadFilteredPolicy(model model.Model, filter interface{}) error
// IsFiltered returns true if the loaded policy has been filtered.
IsFiltered() bool
}
// UpdatableAdapter is the interface for Casbin adapters with add update policy function.
type UpdatableAdapter interface {
Adapter
// UpdatePolicy updates a policy rule from storage.
// This is part of the Auto-Save feature.
UpdatePolicy(sec string, ptype string, oldRule, newRule []string) error
// UpdatePolicies updates some policy rules to storage, like db, redis.
UpdatePolicies(sec string, ptype string, oldRules, newRules [][]string) error
// UpdateFilteredPolicies deletes old rules and adds new rules.
UpdateFilteredPolicies(sec string, ptype string, newRules [][]string, fieldIndex int, fieldValues ...string) ([][]string, error)
}Popular ready-made adapters include:
- Databases:
- PostgreSQL
- MySQL
- MongoDB
- Redis
- Cassandra
- DynamoDB
- and others
- ORM:
- Bun ORM
- Gorm ORM
- Beego ORM
- Ent ORM
- sqlx
- and others
- File systems:
- File adapter (built-in, for .csv files)
- JSON adapter (github.com/casbin/json-adapter)
- YAML adapter (github.com/casbin/yaml-adapter)
- Cloud/Distributed:
- Etcd
- GCP Cloud Storage
- AWS S3
- Apache ZooKeeper
- and others
Each adapter has its own implementation specifics, but they all follow the basic interface. For example, here's what the basic structure of a PostgreSQL adapter looks like:
type Adapter struct {
db *sql.DB
tableName string
}
func NewAdapter(db *sql.DB) *Adapter {
return &Adapter{
db: db,
tableName: "casbin_rule",
}
}
func (a *Adapter) LoadPolicy(model model.Model) error {
// loading rules from DB
rows, err := a.db.Query("SELECT * FROM " + a.tableName)
if err != nil {
return err
}
defer rows.Close()
// parsing results
for rows.Next() {
// filling model with data
}
return nil
}
func (a *Adapter) SavePolicy(model model.Model) error {
// saving rules to DB
return nil
}package main
import (
"context"
sqlxadapter "github.com/Blank-Xu/sqlx-adapter"
"github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v2/model"
"github.com/jmoiron/sqlx"
)
func initBun() *bun.DB {
dsn := "postgres://user:password@localhost:5432/db?sslmode=disable"
sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn)))
return bun.NewDB(sqldb, pgdialect.New())
}
func initCasbin(db *bun.DB) (*casbin.Enforcer, error) {
dsn := "postgres://user:password@localhost:5432/db?sslmode=disable"
sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn)))
bunDB := bun.NewDB(sqldb, pgdialect.New())
// initialize adapter for Bun ORM
sqlxDB := sqlx.NewDb(bunDB.DB.DB, "pgx")
a, err := sqlxadapter.NewAdapter(sqlxDB, "casbin_rules")
if err != nil {
return nil, err
}
// Create enforcer with our model and adapter
enforcer, err := casbin.NewEnforcer("model.conf", a)
if err != nil {
return nil, err
}
// Load policies from database
err = enforcer.LoadPolicy()
if err != nil {
return nil, err
}
return enforcer, nil
}Logging
Casbin provides a flexible logging mechanism through the Logger interface. By default, a basic logger is used, but you can easily connect your own by implementing this interface:
type Logger interface {
EnableLog(bool)
IsEnabled() bool
LogModel(model.Model)
LogEnforce(matcher string, request []interface{}, result bool, explains [][]string)
LogRole(roles []string)
LogPolicy(policy []string)
}I've seen that there are adapters for:
Example usage with zap:
// import example
import (
zaplogger "github.com/casbin/zap-logger/v2"
"github.com/casbin/casbin/v2"
)
// enforcer with new zap logger instance
e, _ := casbin.NewEnforcer("examples/rbac_model.conf", a)
e.EnableLog(true)
e.SetLogger(zaplogger.NewLogger(true, true))
// enforcer with existing zap logger instance
logger := zaplogger.NewLoggerByZap(existsZapLogger, true)
e, _ := casbin.NewEnforcer("examples/rbac_model.conf", a)
e.EnableLog(true)
e.SetLogger(logger)Logging helps track all access checks, changes in policies and roles, which is especially useful for debugging and security system auditing.
Conclusions
In this article, we've covered the main components of the Casbin library for access control in Go applications. We've learned about ways to import and configure access models, different types of adapters for storing policy rules, and the possibility of integration with databases and various ORMs.
In the next article, we'll move on to the practical part and look at specific examples of implementing access control using Casbin in a real project that I plan to open source. We'll create a universal wrapper for Casbin to be able to quickly deploy access control in projects.