casbin of Go daily

Keywords: Go github MySQL Database SQL

brief introduction

Privilege management is a necessary module in almost every system. If the project development needs to realize the right management every time, it will undoubtedly waste the development time and increase the development cost. As a result, the caspin library appears. Caspin is a powerful and efficient access control library. It supports a variety of commonly used access control models, such as ACL/RBAC/ABAC, etc. Flexible access control can be realized. Meanwhile, caspin supports multiple programming languages, Go/Java/Node/PHP/Python/.NET/Rust. We only need to learn once and use many things.

Quick 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

Then install caspin, currently v2 version:

$ go get github.com/casbin/casbin/v2

Permission is actually to control who can operate on what resources. Caspin abstracts the access control model into a configuration file (model file) based on the PERM (Policy, Effect, Request, Matchers) metamodel. So switching or updating the authorization mechanism only requires a simple modification of the configuration file.

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

Request is an abstraction of access request, which corresponds to the parameters of e.Enforce() function one by one

The matcher matcher will match the request one by one with each defined policy, and generate multiple matching results.

effect determines whether the request is allowed or denied based on the summary of all the results obtained by applying the matcher to the request.

The following picture depicts the process very well:

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))

The above model file specifies that the permission consists of sub, obj and act. The request can only pass if there is the same policy as it in the policy list. The results of the matcher can be obtained through p.eft. some(where (p.eft == allow)) means that only one policy allows it.

Then our policy file (that is, who can operate on what resources):

p, dajun, data1, read
p, lizi, data2, write

above policy.csv The two lines of the file indicate that dajun has read permission on data1 and lizi has write permission on data2.

Next is the code used:

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() {
  e, err := casbin.NewEnforcer("./model.conf", "./policy.csv")
  if err != 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")
}

The code is not complicated. First create a casbin.Enforcer Objects, loading model files model.conf And policy files policy.csv , call its Enforce method to check permissions. Run program:

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

The request 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 is because "dajun" does not have write permission to data1, and the fourth is because dajun does not have read permission to data2, so the check cannot pass. The output is as expected.

sub/obj/act corresponds to three parameters passed to the Enforce method in turn. In fact, sub/obj/act and read/write/data1/data2 here are freely chosen by myself. You can use other names as long as they are consistent.

In the above example, ACL (access control list) is implemented. ACL display defines the permissions of each principal to each resource, and undefined ones have no permissions. We can also add super administrator, who can do anything. Assuming that the super administrator is root, we only need to modify the matcher:

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

As long as the access principal is root.

verification:

func main() {
  e, err := casbin.NewEnforcer("./model.conf", "./policy.csv")
  if err != 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")
}

Because when sub = root, the matcher must be able to pass, running result:

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

RBAC model

ACL model has no problem when users and resources are relatively small, but when users and resources are large, ACL will become extremely cumbersome. Imagine how painful it is to reset the required permissions every time a new user is added. RBAC (role-based access control) model solves this problem by introducing role as the middle layer. Each user belongs to a role, such as developer, administrator, operation and maintenance, and each role has its own specific permissions. The addition and deletion of permissions are carried out through roles. In this way, when a user is added, we only need to assign him a role, and he can have all the permissions of that role. When you modify the permissions of a role, the user permissions belonging to the role will be modified accordingly.

Using RBAC model in caspin requires adding role in model file_ Definition module:

[role_definition]
g = _, _

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

g = _ , The mapping relationship between user role and role role is defined. The former is a member of the latter and has the latter's permission. Then in the matcher, we don't need to judge that r.sub and p.sub are exactly equal. We just need to use g(r.sub, p.sub) to judge whether the request subject r.sub belongs to the role of p.sub. Finally, we modify the policy file to add the user -- role definition:

p, admin, data, read
p, admin, data, write
p, developer, data, read
g, dajun, admin
g, lizi, developer

above policy.csv According to the document, dajun belongs to admin administrator, lizi belongs to developer developer, and g is used to define this relationship. In addition, admin uses read and write permissions for data data, while developer only has read permissions for data 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() {
  e, err := casbin.NewEnforcer("./model.conf", "./policy.csv")
  if err != 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")
}

Obviously, lizi's role does not have write permission:

dajun CAN read data
dajun CAN write data
lizi CAN read data
lizi CANNOT write data

Multiple rbacs

Caspin 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

The above model file defines two RBAC systems g and g2. In the matcher, we use g(r.sub, p.sub) to determine that the request subject belongs to a specific group, and g2(r.obj, p.obj) to determine that the request resource belongs to a specific group, and the operation is consistent before release.

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

In the last four lines, dajun belongs to the admin role and lizi belongs to the developer role, prod.data It belongs to prod role, dev.data Belongs to the dev role of development resources. The admin role has read and write permissions to prod and dev resources, while developer can only have read and write permissions to dev and prod.

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")

In the first function, the e.Enforce() method gets the role admin of dajun first, and then gets prod.data The role prod allows the request according to the first line P, admin, prod and read in the file. In the last function, lizi belongs to the role developer, and prod.data It belongs to role prod and all policies are not allowed, so the request is rejected:

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

Multi level role

Caspin can also define the role to which the role belongs, so as to realize multi-level role relationship, which can be transferred. For example, if dajun belongs to Senior Developer Senior and seinor belongs to developer, then dajun also belongs to developer and has all rights of developer. We can define common permissions for developers, and then define some special permissions for senior.

The model file does not need to be modified. The policy file changes as follows:

p, senior, data, write
p, developer, data, read
g, dajun, senior
g, senior, developer
g, lizi, developer

above policy.csv The file defines that senior developer senior has write permission for data, while ordinary developer developer only has read permission for data. At the same time, senior is also developer, so senior also inherits its read permission. dajun belongs to senior, so dajun has read and write permissions on data, while lizi only belongs to developer and read permissions on data.

check(e, "dajun", "data", "read")
check(e, "dajun", "data", "write")
check(e, "lizi", "data", "read")
check(e, "lizi", "data", "write")

RBAC domain

In caspin, roles can be global, specific domain or tenant, which can be simply understood as a group. For example, dajun is an administrator in the group tenant1 and has relatively high permissions. In tenant2, he may be just a younger brother.

Using RBAC domain requires the following modifications 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

g=_ , , Indicates that the former has an intermediate defined role in the latter, and the use of g in the matcher requires dom.

p, admin, tenant1, data1, read
p, admin, tenant2, data2, read
g, dajun, admin, tenant1
g, dajun, developer, tenant2

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() {
  e, err := casbin.NewEnforcer("./model.conf", "./policy.csv")
  if err != nil {
    log.Fatalf("NewEnforecer failed:%v\n", err)
  }

  check(e, "dajun", "tenant1", "data1", "read")
  check(e, "dajun", "tenant2", "data2", "read")
}

Not surprisingly:

dajun CAN read data1 in tenant1
dajun CANNOT read data2 in tenant2

ABAC

RBAC model is very useful for implementing the relatively static rights management of comparison rules. But for the special and dynamic requirements, RBAC is a little out of its power. For example, we implement different permission control on data in different time periods. Normal working hours: 9:00-18:00 everyone can read and write data, and other times only the data owner can read and write data. We can easily use ABAC (attribute base access list) model to fulfill this requirement:

[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))

The 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() {
  e, err := casbin.NewEnforcer("./model.conf", "./policy.csv")
  if err != 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")
}

Obviously, 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

We know, at model.conf The parameters passed to the enforcer method can be accessed through r.sub and r.obj, r.act in the file. In fact, sub/obj can be a structure object. Thanks to the powerful function of the govalue library, we can model.conf Get the field values of these structures in the file. As R sub.Name ,r.Obj.Owner Wait. The contents of the govaluate library can be found in my previous article govaluate of Go daily.

The ABAC model can be used for very flexible permission control, but generally RBAC is enough.

Model storage

In the above code, we always store the model in a file. Caspin can also dynamically initialize the model in the code. For example, the example of get started can be rewritten as follows:

func main() {
  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)
  if err != 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")
}

Similarly, we can load models from strings:

func main() {
  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")
}

But these two methods are not recommended.

Policy storage

In the previous example, we all stored the policy in the policy.csv File. Generally, file storage is rarely used in practical applications. Caspin supports a variety of storage methods in the form of a third-party adapter, including MySQL/MongoDB/Redis/Etcd, and can also realize its own storage. Here's a complete list https://casbin.org/docs/en/adapters . Let's talk about using Gorm Adapter. First 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', '', '', '');

Then use Gorm Adapter to load the policy. By default, Gorm Adapter uses the casbin in the casbin library_ Rule table:

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, _ := 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")
}

function:

dajun CAN read data1
lizi CAN write data2
dajun CANNOT write data1
dajun CANNOT read data2

Using functions

We can use functions in matchers. Caspin has built-in functions keyMatch/keyMatch2/keyMatch3/keyMatch4 that match URL paths. regexMatch uses regular matching and ipMatch matches IP addresses. See https://casbin.org/docs/en/function . Using built-in functions, we can easily partition the permissions of routes:

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

Different users can only access the URL under their corresponding route:

func main() {
  e, err := casbin.NewEnforcer("./model.conf", "./policy.csv")
  if err != 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")
}

Output:

dajun CAN read user/dajun/1
lizi CAN read user/lizi/2
dajun CANNOT read user/lizi/1

Of course, we can also define our own functions. First define a function and return 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]
}

A simple regular matching is implemented here, which only deals with *.

Then wrap this function with interface {} type:

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

  return (bool)(KeyMatch(name1, name2)), nil
}

Then add to the rights authenticator:

e.AddFunction("my_func", KeyMatchFunc)

In this way, we can use this function in the matcher to achieve regular matching:

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

Next, we give permissions to dajun in the policy file:

p, dajun, data/*, read

dajun has read permission on all files matching data / *.

To verify:

check(e, "dajun", "data/1", "read")
check(e, "dajun", "data/2", "read")
check(e, "dajun", "data/1", "write")
check(e, "dajun", "mydata", "read")

dajun does not have write permission on data/1, mydata does not conform to data / * mode, and does not have read permission:

dajun CAN read data/1
dajun CAN read data/2
dajun CANNOT write data/1
dajun CANNOT read mydata

summary

Caspin is powerful, simple and efficient, and multilingual. It's worth learning.

If you find a fun and easy-to-use Go language library, you are welcome to submit the issue to GitHub, the daily Go library 😄

reference resources

  1. casbin GitHub: https://github.com/casbin/casbin
  2. Official website of caspin: https://casbin.org/
  3. A meta model based access control policy description language: http://www.jos.org.cn/html/2020/2/5624.htm
  4. Go daily GitHub: https://github.com/darjun/go-daily-lib

I

My blog: https://darjun.github.io

Welcome to my WeChat official account, GoUpUp, learn together and make progress together.

Posted by topsub on Sun, 14 Jun 2020 18:44:09 -0700