Introduction to the

Rights management is a mandatory module in almost every system. If the project development every time to achieve a permission management, will undoubtedly waste the development time, increase the development cost. Hence the casbin library. Casbin is a powerful and efficient access control repository. Supports multiple commonly used access control models, such as ACL, RBAC, and ABAC. Flexible access control can be implemented. At the same time, casbin support a variety of programming languages, Go/Java/Node/PHP/Python/.NET/Rust. We just need to learn it once and use it multiple times.

Quick to use

We still use the Go Module to write the code, first initialize:

$ mkdir casbin && cd casbin
$ go mod init github.com/darjun/go-daily-lib/casbin
Copy the code

Then install Casbin, currently version v2:

$ go get github.com/casbin/casbin/v2
Copy the code

Permissions are really about controlling who can do what on what resources. Casbin abstractions the access control model into a configuration file (model file) based on the PERM (Policy, Effect, Request, Matchers) metamodel. Therefore, switching or updating the authorization mechanism requires a simple modification of the configuration file.

Policy is the definition of a policy or rule. It defines specific rules.

Request is an abstraction of an access request that corresponds one-to-one to the arguments to the e.force() function

The Matcher matcher matches the request to each policy defined, generating multiple matches.

Effect determines whether a request is allowed or denied based on all the results of applying the matcher to the request.

Here’s a good illustration of the process:

We first write the model file:

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act

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

The above model file specifies that the permission is composed of sub, OBj, and ACT. The request can be passed only when there are exactly the same policies in the policy list. The result of the matcher can be obtained by using p.eft, some(where (p.eft == allow)) means as long as one policy is allowed.

Then we have policy files (i.e. who can do what on what resources) :

p, dajun, data1, read
p, lizi, data2, write
Copy the code

The two lines in the policy.csv file indicate that Dajun has read permission on data1 and Lizi has write permission on data2.

Here’s the code to use:

package main

import (
  "fmt"
  "log"

  "github.com/casbin/casbin/v2"
)

func check(e *casbin.Enforcer, sub, obj, act string) {
  ok, _ := e.Enforce(sub, obj, act)
  if ok {
    fmt.Printf("%s CAN %s %s\n", sub, act, obj)
  } else {
    fmt.Printf("%s CANNOT %s %s\n", sub, act, obj)
  }
}

func main(a) {
  e, err := casbin.NewEnforcer("./model.conf"."./policy.csv")
  iferr ! =nil {
    log.Fatalf("NewEnforecer failed:%v\n", err)
  }

  check(e, "dajun"."data1"."read")
  check(e, "lizi"."data2"."write")
  check(e, "dajun"."data1"."write")
  check(e, "dajun"."data2"."read")}Copy the code

The code is not complicated. First create a casbin.enforcer object, load the model file model.conf and the policy file policy.csv, and invoke the Enforce method to check the permissions. Run the program:

$ go run main.go
dajun CAN read data1
lizi CAN write data2
dajun CANNOT write data1
dajun CANNOT read data2
Copy the code

Requests must exactly match a policy to pass. (“dajun”, “data1”, “read”) matches p, dajun, data1, read, (“lizi”, “data2”, “write”) matches p, lizi, data2, write, so the first two checks pass. The third check failed because “dajun” has no write permission on data1, and the fourth because dajun has no read permission on data2. The output is as expected.

Sub /obj/ ACT corresponds to the three parameters passed to Enforce in turn. In fact, sub/obj/act and read/write/data1/data2 are arbitrary names, you can use any other names, as long as they are consistent.

The example above implements an ACL (Access-control-list). The ACL displays the permissions that each principal has on each resource. Those that are not defined have no permissions. We can also add a super administrator, who can do anything. Assuming the super administrator is root, we only need to change the matcher:

[matchers]
e = r.sub == p.sub && r.obj == p.obj && r.act == p.act || r.sub == "root"
Copy the code

As long as the access subject is root, all access is allowed.

Validation:

func main(a) {
  e, err := casbin.NewEnforcer("./model.conf"."./policy.csv")
  iferr ! =nil {
    log.Fatalf("NewEnforecer failed:%v\n", err)
  }

  check(e, "root"."data1"."read")
  check(e, "root"."data2"."write")
  check(e, "root"."data1"."execute")
  check(e, "root"."data3"."rwx")}Copy the code

When sub = “root”, the matcher must pass.

$ go run main.go
root CAN read data1
root CAN write data2
root CAN execute data1
root CAN rwx data3
Copy the code

RBAC model

The ACL model has no problems when the number of users and resources is relatively small. However, when the number of users and resources is large, the ACL becomes very cumbersome. Imagine the pain of having to reset the permissions each time you add a new user. Role-based-access-control (RBAC) model solves this problem by introducing role as the middle layer. Each user belongs to a role, such as developer, administrator, and OPERATION and maintenance (O&M). Each role has specific permissions. Permissions can be added or deleted through roles. When a new user is added, all we need to do is assign it a role and it will have all the privileges of that role. When the rights of a role are modified, the rights of the users in the role are modified accordingly.

To use the RBAC model in Casbin, add the roLE_definition module to the model file:

[role_definition]
g = _, _

[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
Copy the code

G = _,_ defines the mapping between user – role and role – role. The former is a member of the latter and has the rights of the latter. Then in the matcher, we don’t need to determine that R.sub is exactly equal to p.sub, we just need to use g(r.sub, p.sub) to determine whether the request body R.sub belongs to the role of P.sub. Finally, we modify the policy file to add user — role definition:

p, admin, data, read
p, admin, data, write
p, developer, data, read
g, dajun, admin
g, lizi, developer
Copy the code

The policy.csv file above specifies that Dajun belongs to admin and Lizi belongs to developer. G is used to define this relationship. Admin has read and write permissions on data, whereas developer has only read permissions on data.

package main

import (
  "fmt"
  "log"

  "github.com/casbin/casbin/v2"
)

func check(e *casbin.Enforcer, sub, obj, act string) {
  ok, _ := e.Enforce(sub, obj, act)
  if ok {
    fmt.Printf("%s CAN %s %s\n", sub, act, obj)
  } else {
    fmt.Printf("%s CANNOT %s %s\n", sub, act, obj)
  }
}

func main(a) {
  e, err := casbin.NewEnforcer("./model.conf"."./policy.csv")
  iferr ! =nil {
    log.Fatalf("NewEnforecer failed:%v\n", err)
  }

  check(e, "dajun"."data"."read")
  check(e, "dajun"."data"."write")
  check(e, "lizi"."data"."read")
  check(e, "lizi"."data"."write")}Copy the code

Lizi’s role does not have write permission:

dajun CAN read data
dajun CAN write data
lizi CAN read data
lizi CANNOT write data
Copy the code

multipleRBAC

Casbin supports multiple RBAC systems at the same time, that is, users and resources have roles:

[role_definition]
g=_,_
g2=_,_

[matchers]
m = g(r.sub, p.sub) && g2(r.obj, p.obj) && r.act == p.act
Copy the code

The model file above defines two RBAC systems G and G2. We use G (R.sub, p.sub) in the matcher to determine that the request body belongs to a specific group, and G2 (R.bj, p.bj) to determine that the request resource belongs to a specific group, and the operation is consistent.

Policy file:

p, admin, prod, read
p, admin, prod, write
p, admin, dev, read
p, admin, dev, write
p, developer, dev, read
p, developer, dev, write
p, developer, prod, read
g, dajun, admin
g, lizi, developer
g2, prod.data, prod
g2, dev.data, dev
Copy the code

In the last 4 rows, dajun belongs to admin role, Lizi belongs to developer role, prod.data belongs to production resource prod role, and dev.data belongs to development resource dev role. The admin role has read and write permissions on prod and dev resources. The developer role has read and write permissions on dev and PROD resources only.

check(e, "dajun"."prod.data"."read")
check(e, "dajun"."prod.data"."write")
check(e, "lizi"."dev.data"."read")
check(e, "lizi"."dev.data"."write")
check(e, "lizi"."prod.data"."write")
Copy the code

In the first function, the e.force () method obtains the admin role of dajun, and then obtains the prod.data role prod, and allows the request according to the first line of p, admin, prod, read. In the last function, lizi belongs to role developer, and prod.data belongs to role prod, so the request is denied:

dajun CAN read prod.data
dajun CAN write prod.data
lizi CAN read dev.data
lizi CAN write dev.data
lizi CANNOT write prod.data
Copy the code

Multiple roles

Casbin can also define the roles to which the roles belong, thus implementing a multi-layer role relationship that can be passed. For example, if Dajun is a senior developer and Seinor is a developer, then Dajun is also a developer and has all the rights of the developer. We can define common permissions for developers, and then define some special permissions for senior.

The model file is not modified, and the policy file is modified as follows:

p, senior, data, write
p, developer, data, read
g, dajun, senior
g, senior, developer
g, lizi, developer
Copy the code

The above policy.csv file defines that the senior developer has the write permission for data, while the common developer has the read permission for data. And senior is also developer, so senior also inherits the read permission. Dajun belongs to senior, so Dajun has read and write permissions on data, while Lizi belongs only to developer and only has read permissions on data.

check(e, "dajun", "data", "read")
check(e, "dajun", "data", "write")
check(e, "lizi", "data", "read")
check(e, "lizi", "data", "write")
Copy the code

RBAC domain

In Casbin, roles can be global or specific domains or tenants, which can be simply understood as groups. For example, Dajun is an administrator in group Tenant1 and has relatively high permissions, but he may be just a brother in Tenant2.

Using RBAC Domain requires the following changes to the model file:

[request_definition]
r = sub, dom, obj, act

[policy_definition]
p = sub, dom, obj, act

[role_definition]
g = _,_,_

[matchers]
m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.obj
Copy the code

G =_,_,_ means that the former has an intermediate role in the latter, and when g is used in matchers, dom is taken with it.

p, admin, tenant1, data1, read
p, admin, tenant2, data2, read
g, dajun, admin, tenant1
g, dajun, developer, tenant2
Copy the code

In Tenant1, only admin can read data1. In Tenant2, only admin can read data2. Dajun is admin in Tenant1, but not in Tenant2.

func check(e *casbin.Enforcer, sub, domain, obj, act string) {
  ok, _ := e.Enforce(sub, domain, obj, act)
  if ok {
    fmt.Printf("%s CAN %s %s in %s\n", sub, act, obj, domain)
  } else {
    fmt.Printf("%s CANNOT %s %s in %s\n", sub, act, obj, domain)
  }
}

func main(a) {
  e, err := casbin.NewEnforcer("./model.conf"."./policy.csv")
  iferr ! =nil {
    log.Fatalf("NewEnforecer failed:%v\n", err)
  }

  check(e, "dajun"."tenant1"."data1"."read")
  check(e, "dajun"."tenant2"."data2"."read")}Copy the code

The results were not surprising:

dajun CAN read data1 in tenant1
dajun CANNOT read data2 in tenant2
Copy the code

ABAC

The RBAC model is very useful for implementing relatively static rights management that compares rules. But for specific, dynamic needs, RBAC may be a little out of its depth. For example, we implement different permissions on data at different time periods. During normal working hours from 9:00 to 18:00, everyone can read and write data. During other hours, only the data owner can read and write data. This requirement can be easily completed by using the Attribute Base Access List (ABAC) model:

[request_definition] r = sub, obj, act [policy_definition] p = sub, obj, act [matchers] m = r.sub.Hour >= 9 && r.sub.Hour < 18 || r.sub.Name == r.obj.Owner [policy_effect] e = some(where (p.eft  == allow))Copy the code

This rule does not require a policy file:

type Object struct {
  Name  string
  Owner string
}

type Subject struct {
  Name string
  Hour int
}

func check(e *casbin.Enforcer, sub Subject, obj Object, act string) {
  ok, _ := e.Enforce(sub, obj, act)
  if ok {
    fmt.Printf("%s CAN %s %s at %d:00\n", sub.Name, act, obj.Name, sub.Hour)
  } else {
    fmt.Printf("%s CANNOT %s %s at %d:00\n", sub.Name, act, obj.Name, sub.Hour)
  }
}

func main(a) {
  e, err := casbin.NewEnforcer("./model.conf"."./policy.csv")
  iferr ! =nil {
    log.Fatalf("NewEnforecer failed:%v\n", err)
  }

  o := Object{"data"."dajun"}
  s1 := Subject{"dajun".10}
  check(e, s1, o, "read")

  s2 := Subject{"lizi".10}
  check(e, s2, o, "read")

  s3 := Subject{"dajun".20}
  check(e, s3, o, "read")

  s4 := Subject{"lizi".20}
  check(e, s4, o, "read")}Copy the code

Lizi cannot read data at 20:00:

dajun CAN read data at 10:00
lizi CAN read data at 10:00
dajun CAN read data at 20:00
lizi CANNOT read data at 20:00
Copy the code

As we know, the parameters passed to the Enforce method can be accessed in the model.conf file through r.unit and R.o.bj, r.ct. In fact, sub/obj can be structured objects, and thanks to the power of the GoValuate library, we can get the field values of these structures in the model.conf file. R.sub.name, r.obj.owner, etc. The goValuate library can be seen in my previous post, Go Library of GoValuate daily.

The ABAC model allows for very flexible permission control, but RBAC is generally sufficient.

Model storage

In the above code, we have always stored the model in a file. Casbin can also be used to dynamically initialize the model in code. For example, the example of get-started can be rewritten as:

func main(a) {
  m := model.NewModel()
  m.AddDef("r"."r"."sub, obj, act")
  m.AddDef("p"."p"."sub, obj, act")
  m.AddDef("e"."e"."some(where (p.eft == allow))")
  m.AddDef("m"."m"."r.sub == g.sub && r.obj == p.obj && r.act == p.act")

  a := fileadapter.NewAdapter("./policy.csv")
  e, err := casbin.NewEnforcer(m, a)
  iferr ! =nil {
    log.Fatalf("NewEnforecer failed:%v\n", err)
  }

  check(e, "dajun"."data1"."read")
  check(e, "lizi"."data2"."write")
  check(e, "dajun"."data1"."write")
  check(e, "dajun"."data2"."read")}Copy the code

Similarly, we can load a model from a string:

func main(a) {
  text := ` [request_definition] r = sub, obj, act [policy_definition] p = sub, obj, act [policy_effect] e = some(where (p.eft == allow)) [matchers] m = r.sub == p.sub && r.obj == p.obj && r.act == p.act `

  m, _ := model.NewModelFromString(text)
  a := fileadapter.NewAdapter("./policy.csv")
  e, _ := casbin.NewEnforcer(m, a)

  check(e, "dajun"."data1"."read")
  check(e, "lizi"."data2"."write")
  check(e, "dajun"."data1"."write")
  check(e, "dajun"."data2"."read")}Copy the code

But neither of these approaches is recommended.

Strategy storage

In the previous examples, we stored the policy in a policy.csv file. In general, file storage is rarely used in practical applications. Casbin supports a variety of storage methods including MySQL/MongoDB/Redis/Etcd in the way of third-party adapter, and can also implement its own storage. See the full list here casbin.org/docs/en/ada… . Here we introduce the use of Gorm Adapter. Connect to the database and execute the following SQL:

CREATE DATABASE IF NOT EXISTS casbin;

USE casbin;

CREATE TABLE IF NOT EXISTS casbin_rule (
  p_type VARCHAR(100) NOT NULL,
  v0 VARCHAR(100),
  v1 VARCHAR(100),
  v2 VARCHAR(100),
  v3 VARCHAR(100),
  v4 VARCHAR(100),
  v5 VARCHAR(100));INSERT INTO casbin_rule VALUES
('p'.'dajun'.'data1'.'read'.' '.' '.' '),
('p'.'lizi'.'data2'.'write'.' '.' '.' ');
Copy the code

Then use the Gorm Adapter to load the policy. The Gorm Adapter uses the casbin_rule table in the Casbin library by default:

package main

import (
  "fmt"

  "github.com/casbin/casbin/v2"
  gormadapter "github.com/casbin/gorm-adapter/v2"
  _ "github.com/go-sql-driver/mysql"
)

func check(e *casbin.Enforcer, sub, obj, act string) {
  ok, _ := e.Enforce(sub, obj, act)
  if ok {
    fmt.Printf("%s CAN %s %s\n", sub, act, obj)
  } else {
    fmt.Printf("%s CANNOT %s %s\n", sub, act, obj)
  }
}

func main(a) {
  a, _ := gormadapter.NewAdapter("mysql"."Root: 12345 @ TCP (127.0.0.1:3306)/")
  e, _ := casbin.NewEnforcer("./model.conf", a)

  check(e, "dajun"."data1"."read")
  check(e, "lizi"."data2"."write")
  check(e, "dajun"."data1"."write")
  check(e, "dajun"."data2"."read")}Copy the code

Run:

dajun CAN read data1
lizi CAN write data2
dajun CANNOT write data1
dajun CANNOT read data2
Copy the code

Using the function

We can use functions in matchers. Casbin built in some function keyMatch/keyMatch2 / keyMatch3 / keyMatch4 are matching URL path, the regexMatch matching, using regular ipMatch match IP address. See casbin.org/docs/en/fun… . We can easily assign permissions to routes using built-in functions:

[matchers]
m = r.sub == p.sub && keyMatch(r.obj, p.obj) && r.act == p.act
Copy the code
p, dajun, user/dajun/*, read
p, lizi, user/lizi/*, read
Copy the code

Different users can only access the URL of the corresponding route:

func main(a) {
  e, err := casbin.NewEnforcer("./model.conf"."./policy.csv")
  iferr ! =nil {
    log.Fatalf("NewEnforecer failed:%v\n", err)
  }

  check(e, "dajun"."user/dajun/1"."read")
  check(e, "lizi"."user/lizi/2"."read")
  check(e, "dajun"."user/lizi/1"."read")}Copy the code

Output:

dajun CAN read user/dajun/1
lizi CAN read user/lizi/2
dajun CANNOT read user/lizi/1
Copy the code

We can of course define our own functions. First define a function that returns bool:

func KeyMatch(key1, key2 string) bool {
  i := strings.Index(key2, "*")
  if i == - 1 {
    return key1 == key2
  }

  if len(key1) > i {
    return key1[:i] == key2[:i]
  }

  return key1 == key2[:i]
}
Copy the code

Here we implement a simple regular match that only handles *.

Then wrap the function with the interface{} type:

func KeyMatchFunc(args ...interface{}) (interface{}, error) {
  name1 := args[0]. (string)
  name2 := args[1]. (string)

  return (bool)(KeyMatch(name1, name2)), nil
}
Copy the code

Then add it to the permission authenticator:

e.AddFunction("my_func", KeyMatchFunc)
Copy the code

We can then use this function in the matcher to implement regular matching:

[matchers]
m = r.sub == p.sub && my_func(r.obj, p.obj) && r.act == p.act
Copy the code

Next we give Dajun privileges in the policy file:

p, dajun, data/*, read
Copy the code

Dajun has read permission on all files matching the data/* pattern.

Verify:

check(e, "dajun"."data/1"."read")
check(e, "dajun"."data/2"."read")
check(e, "dajun"."data/1"."write")
check(e, "dajun"."mydata"."read")
Copy the code

Mydata does not conform to data/* mode, nor does mydata have read permission:

dajun CAN read data/1
dajun CAN read data/2
dajun CANNOT write data/1
dajun CANNOT read mydata
Copy the code

conclusion

Casbin is powerful, simple and efficient, and versatile in multiple languages. It’s worth learning.

If you find a fun and useful Go library, please Go to GitHub and submit the issue😄

reference

  1. Casbin GitHub:github.com/casbin/casb…
  2. Casbin website: casbin.org/
  3. A metamodel based access control policy description language: www.jos.org.cn/html/2020/2…
  4. Go daily GitHub: github.com/darjun/go-d…

I

My blog: darjun.github. IO

Welcome to follow my wechat public account [GoUpUp], learn together, make progress together ~