当前位置:网站首页>Authorization in Golang ProjectUseing Casbin

Authorization in Golang ProjectUseing Casbin

2022-06-12 17:38:00 Ang Jun Li

One 、Casbin Introduce

Privilege management is a necessary module in almost every system . If the project development needs to realize the rights management once every time , It's a waste of development time , Increase development costs .

therefore ,casbin The library appears .casbin Is a powerful 、 Efficient access control library . Support a variety of commonly used access control models , Such as ACL/RBAC/ABAC etc. . It can realize flexible access control . meanwhile ,casbin Support for multiple programming languages ,Go/Java/Node/PHP/Python/.NET/Rust. We only need to learn once , Use it in many ways .

Casbin Sure

  1. Support custom request format , The default request format is {subject, object, action}.
  2. With access control model model And strategy policy Two core concepts .
  3. Support RBAC Multi level role inheritance in , It's not just the subject that can have a role , Resources can also have roles .
  4. Support for built-in super users for example :root  or  administrator. Superuser can perform any operation without explicit permission declaration .
  5. Supports a variety of built-in operators , Such as  keyMatch, It is convenient to manage the path type resources , Such as  /foo/bar  Can be mapped to  /foo*

Casbin You can't

  1. Identity Authentication authentication( That is, verify the user's user name and password ),Casbin Only responsible for access control . There should be other specialized components responsible for authentication , Then from Casbin Access control , The two are complementary .
  2. Manage user list or role list . Casbin The project itself is supposed to manage users 、 The role list is more appropriate , Users usually have their passwords , however Casbin It's not designed to be a container for storing passwords . It's storage RBAC Mapping relationship between users and roles in the scheme .

Two 、 Quick to use

We still use Go Module Write code , Initialize first :

$ mkdir learning-node && cd learning-node
$ go mod init learning-node 

Then install casbin, At present, it is v2 edition :

go get -u github.com/casbin/casbin/v2

Authority is actually control who Be able to What resources What to do .casbin Abstract the access control model to a model based on PERM(Policy,Effect,Request,Matchers) Metamodel configuration file ( Model file ) in . Therefore, switching or updating the authorization mechanism only needs to modify the configuration file .

  • policy, It's the definition of strategy or rule . It defines specific rules , For example, the following example :
p = {sub, obj, act, eft}

Policy rule description :subject(sub Access entity ),object(obj Resources accessed ) and action(act Access method )eft( The policy result is generally blank, and is specified by default allow), Generally, policies are stored in relational data tables , Because there will be a lot of .

  • request, Is the abstraction of access requests , It is associated with e.Enforce() The parameters of a function are one-to-one .
  • matcher, The matcher will match the request with each of the defined policy One by one , Generate multiple matching results .
  • effect, Summarize all results from applying matchers to requests , To decide that the request is allow (allow) still Refuse (deny) visit .

Here's a good picture of the process :

ACL Model (Access Control List, Access control list )

First, in the learning-node A model is defined under the folder model.conf 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 permissions are set by sub,obj,act Three elements make up , Only if there is an identical policy in the policy list , The request can only be passed . The result of the matcher can be passed through p.eft obtain ,some(where (p.eft == allow)) As long as a policy allows , See... For other strategies :( Point me to jump ).

And then we were in learning-node Define a policy under the folder policy.csv file ( That is, who can operate what resources ):

p, zhangsan, data1, read
p, lisi, data2, write

above policy.csv The two lines of the document indicate zhangsan Data pair data1 Yes read jurisdiction ,lisi Data pair data2 Yes write jurisdiction ;

And then in learning-node New under folder main.go file

package main

import (
  "fmt"
  "log"

  "github.com/casbin/casbin/v2"
)
// ACL  Permission access check 
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("path/to/model.conf", "path/to/policy.csv")
  if err != nil {
    log.Fatalf("NewEnforecer failed:%v\n", err)
  }

  check(e, "zhangsan", "data1", "read")
  check(e, "lisi", "data2", "write")
  check(e, "zhangsan", "data1", "write")
  check(e, "zhangsan", "data2", "read")
}

The code is not complicated . First create a casbin.Enforcer object , Load model file model.conf And policy documents policy.csv, Call its Enforce Method to check permissions . Run the program :

$ go run main.go
// echo result
zhangsan CAN read data1
lisi CAN write data2
zhangsan CANNOT write data1
zhangsan CANNOT read data2

The request must match exactly policy.csv A certain strategy can be passed .

The first 1 Because ("zhangsan", "data1", "read") matching p, zhangsan, data1, read.

The first 2 Because ("lisi", "data2", "write") matching p, lisi, data2, write.

The first 3 Because "zhangsan" No, right data1 Of write jurisdiction .

The first 4 Because zhangsan Yes data2 No, read jurisdiction , So the tests can't pass .

All the above output results are in line with expectations .

sub/obj/act Pass it to Enforce Method . Actually... Here sub/obj/act and read/write/data1/data2 I picked it up myself , You can use any other name , As long as it is consistent .

The implementation in the above example is ACL(access-control-list, Access control list ).ACL Shows the defined permissions of each principal to each resource , What is undefined has no authority . We can also add super Administrators , Super administrators can do anything . Suppose the super administrator is root, We just need to modify it model.conf File matcher :

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

explain : As long as the access subject is root Let's go .

verification :

func main() {
  e, err := casbin.NewEnforcer("path/to/model.conf", "path/to/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 sub = "root" when , The matcher must be able to pass , Running results :

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

RBAC Model (Role Based Access Control)

ACL The model has no problem with fewer users and resources , But there are a lot of users and resources ,ACL It's going to be incredibly cumbersome . Imagine , Add one user at a time , How painful it is to have to reset the permissions he needs .RBAC(role-based-access-control) The model introduces roles (role) This middle layer is used to solve this problem . Each user belongs to a role , For example, developers 、 Administrators 、 Operations etc. , Each role has its own specific permissions , Permissions are added and deleted through roles . When a user is added in this way , We just need to assign him a role , He can have all the rights of the role . When modifying the permissions of a role , User permissions belonging to this role will be modified accordingly .

stay casbin Use in RBAC The model needs to be in the model model.conf Add... To the file role_definition modular :

[role_definition]
g = _, _

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

g = _,_ Defines the user —— role , role —— The mapping of roles , The former is a member of the latter , Have the authority of the latter . And then in the matcher , We don't need to judge r.sub And p.sub Completely equal , Just use g(r.sub, p.sub) To determine the subject of the request r.sub Whether to belong to p.sub This role can . Finally, we modify the policy file and add users —— Role definition :

p, admin, data, read
p, admin, data, write
p, developer, data, read
g, zhangsan, admin
g, lisi, developer

above policy.csv The document states that ,zhangsan Belong to admin Administrators ,lisi Belong to developer developer , Use g To define this relationship . in addition admin Data pair data Have read and write jurisdiction , and developer Data pair data Only read jurisdiction .

package main

import (
  "fmt"
  "log"

  "github.com/casbin/casbin/v2"
)
// RBAC  Permission access check 
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("path/to/model.conf", "path/to/policy.csv")
  if err != nil {
    log.Fatalf("NewEnforecer failed:%v\n", err)
  }

  check(e, "zhangsan", "data", "read")
  check(e, "zhangsan", "data", "write")
  check(e, "lisi", "data", "read")
  check(e, "lisi", "data", "write")
}

Obviously lisi The character doesn't have write jurisdiction :

zhangsan CAN read data
zhangsan CAN write data
lisi CAN read data
lisi CANNOT write data

Multiple RBAC Model (Multiple Rbac)

casbin There are more than one support at the same time RBAC System , That is, both 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 model file above defines two RBAC System g and g2, We use... In matchers g(r.sub, p.sub) Determine that the subject of the request belongs to a specific group ,g2(r.obj, p.obj) Determine that the request resource belongs to a specific group , And the operation is consistent to 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, zhangsan, admin
g, lisi, developer
g2, prod.data, prod
g2, dev.data, dev

Let's look at the role relationship first , In the end 4 That's ok ,zhangsan Belong to admin role ,lisi Belong to developer role ,prod.data It belongs to production resources prod role ,dev.data It belongs to development resources dev role .admin The role has the right to prod and dev Read and write permissions for class resources ,developer Can only have the right to dev Read and write permissions and prod Read permission of .

check(e, "zhangsan", "prod.data", "read")
check(e, "zhangsan", "prod.data", "write")
check(e, "lisi", "dev.data", "read")
check(e, "lisi", "dev.data", "write")
check(e, "lisi", "prod.data", "write")

In the first function e.Enforce() Method gets first when it is actually executed zhangsan The role admin, Get more prod.data The role prod, According to the first line of the file p, admin, prod, read Allow requests . In the last function lisi Belongs to the character developer, and prod.data Belongs to the character prod, All strategies don't allow , So the request was rejected :

zhangsan CAN read prod.data
zhangsan CAN write prod.data
lisi CAN read dev.data
lisi CAN write dev.data
lisi CANNOT write prod.data

Multiple roles (Multi Layered Roles)

casbin You can also define the role you belong to , So as to realize multi-layer role relationship , This permission relationship can be passed on . for example zhangsan Belongs to senior developers senior,seinor It belongs to the developer , that zhangsan It's also for developers , Have all the rights of the developer . We can define the common rights of developers , And then extra for senior Define some special permissions .

Model files need not be modified , The policy document has been changed as follows :

p, senior, data, write
p, developer, data, read
g, zhangsan, senior
g, senior, developer
g, lisi, developer

above policy.csv The file defines the advanced Developer senior Data pair data Yes write jurisdiction , Ordinary developers developer For data only read jurisdiction . meanwhile senior It's also developer, therefore senior And inherit it read jurisdiction .zhangsan Belong to senior, therefore zhangsan Yes data Yes read and write jurisdiction , and lisi Only belong to developer, Data pair data Only read jurisdiction .

check(e, "zhangsan", "data", "read")
check(e, "zhangsan", "data", "write")
check(e, "lisi", "data", "read")
check(e, "lisi", "data", "write")
 It turned out not to be expected :
zhangsan CAN read data
zhangsan CAN write data
lisi CAN read data
lisi CANNOT write data

RBAC domain Model

stay casbin in , The role can be global , It can also be specific domain( field ) or tenant( Tenant ), It can be simply understood as Group . for example zhangsan In group tenant1 It's the administrator , Have higher authority , stay tenant2 Maybe it's just a little brother .

Use RBAC domain The following changes need to be made 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 , Use in matchers g Take it with you dom.

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

stay tenant1 in , Only admin Can read data data1. stay tenant2 in , Only admin Can read data data2.zhangsan stay tenant1 Medium is admin, But in tenant2 Middle is not .

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

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

It turned out not to be expected :

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

ABAC Model (Attribute Base Access List)

RBAC The model can be used to implement the regular 、 Relatively static permission management is very useful . But for the special 、 Dynamic demand ,RBAC It's a little bit out of my ability . for example , We're looking at data at different times data Realize different authority control . Normal working hours 9:00-18:00 Everyone can read and write data, At other times, only the data owner can read and write . We can use this kind of demand conveniently ABAC(attribute base access list) The model is completed .

Define a model model.conf file :

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

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

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

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

  s4 := Subject{"lisi", 20}
  check(e, s4, o, "read")
}

It turned out not to be expected :

zhangsan CAN read data at 10:00
lisi CAN read data at 10:00
zhangsan CAN read data at 20:00
lisi CANNOT read data at 20:00

We know , stay model.conf In the document, you can use r.sub and r.obj,r.act Come to visit and pass on to Enforce Method parameters . actually sub/obj It can be a structure object , Thanks to the govaluate Library's powerful function , We can do it in model.conf Get the field values of these structures in the file . As above r.sub.Namer.Obj.Owner etc. .govaluate The contents of the library can be found in my previous article 《Go One storehouse a day govaluate》.

Use ABAC The model can be very flexible in authority control , But in general RBAC That's enough .

Model storage

In the above scenario Introduction , We always store models in files .casbin You can also initialize the model dynamically in the code , for example get-started The example can be rewritten as :

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, "zhangsan", "data1", "read")
  check(e, "lisi", "data2", "write")
  check(e, "zhangsan", "data1", "write")
  check(e, "zhangsan", "data2", "read")
}

similarly , We can also 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, "zhangsan", "data1", "read")
  check(e, "lisi", "data2", "write")
  check(e, "zhangsan", "data1", "write")
  check(e, "zhangsan", "data2", "read")
}

But these two methods are not recommended ( Unable to persist management ).

Policy storage

In the previous example , We all store policies in policy.csv In file . Generally in practical application , File storage is rarely used .casbin Supports a variety of storage methods in the form of a third-party adapter, including MySQL/MongoDB/Redis/Etcd etc. , You can also implement your own storage . Here's a complete list https://casbin.org/docs/en/adapters. Let's introduce the use of Gorm Adapter. First connect to the database , Execute the following mysql Code :

docker compose Rapid deployment mysql ( Point me to jump

CREATE DATABASE IF NOT EXISTS casbin;

USE casbin;

create table casbin_rule
(
    id    bigint unsigned auto_increment primary key,
    ptype varchar(100) null,
    v0    varchar(100) null,
    v1    varchar(100) null,
    v2    varchar(100) null,
    v3    varchar(100) null,
    v4    varchar(100) null,
    v5    varchar(100) null,
    v6    varchar(25)  null,
    v7    varchar(25)  null,
    constraint idx_casbin_rule unique (ptype, v0, v1, v2, v3, v4, v5, v6, v7)
);


INSERT INTO casbin_rule (ptype, v0, v1, v2)
VALUES ('p', 'zhangsan', 'data1', 'read'),
       ('p', 'lisi', 'data2', 'write');

And then use Gorm Adapter load policy,Gorm Adapter By default casbin In the library casbin_rule surface :

package main

import (
  "fmt"

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

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() {
  //  adopt  Xorm Realization , Use the adapter to initialize a   MySQL (true: It will automatically manage and create tables / to update )
  a, _ := gormadapter.NewAdapter("mysql", "root:[email protected](127.0.0.1:13306)/casbin", true)
  // echo sql log
  a.AddLogger(logger.Default.LogMode(logger.Info))
  e, _ := casbin.NewEnforcer("path/to/model.conf", a)

  check(e, "zhangsan", "data1", "read")
  check(e, "lisi", "data2", "write")
  check(e, "zhangsan", "data1", "write")
  check(e, "zhangsan", "data2", "read")
}

It turned out not to be expected :

zhangsan CAN read data1
lisi CAN write data2
zhangsan CANNOT write data1
zhangsan CANNOT read data2

Using functions

We can use functions in matchers .casbin Built in functions keyMatch/keyMatch2/keyMatch3/keyMatch4 It's all matching URL The path of ,regexMatch Use regular matching ,ipMatch matching IP Address . See https://casbin.org/docs/en/function. With built-in functions, we can easily divide the permissions of routes :

Defining models model.conf file :

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

Define strategy policy.csv file ( That is, who can operate what resources ):

p, zhangsan, user/zhangsan/*, read
p, lisi, user/lisi/*, read

Different users can only access under their corresponding routes URL:

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

  check(e, "zhangsan", "user/zhangsan/1", "read")
  check(e, "lisi", "user/lisi/2", "read")
  check(e, "zhangsan", "user/lisi/1", "read")
}

Output :

zhangsan CAN read user/zhangsan/1
lisi CAN read user/lisi/2
zhangsan CANNOT read user/lisi/1

We can also define our own functions . Define a function first , 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]
}

Here we implement a simple regular match , Only deal with *.

And then use this function with interface{} Type packing one layer :

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

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

Then add it to the authority authenticator :

e.AddFunction("my_func", KeyMatchFunc)

In this way, we can 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

Next, in the strategy file, let's say zhangsan To give permission :

p, zhangsan, data/*, read

zhangsan For matching patterns data/* We have all the documents read jurisdiction .

verification :

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

zhangsan Yes zhangsan/1 No, write jurisdiction ,mydata Do not conform to the data/* Pattern , either read jurisdiction :

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

3、 ... and 、 summary

casbin It greatly reduces the amount of business code for permission verification , Just learn once , It can be used in many places , Its multi applicability is worth learning and applying to practical projects .

Four 、 Note address

gitlab Address :http://gitlab.lilogs.com/liang/learning-note/tree/master/casbin

5、 ... and 、 Reference resources

  1. Go One storehouse a day casbin:https://darjun.github.io/2020/06/12/godailylib/casbin/
  2. Casbin Official documents :https://casbin.org/docs/zh-CN/overview
  3. Casbin GitHub:https://github.com/casbin
  4. A meta model based access control policy description language :http://www.jos.org.cn/html/2020/2/5624.htm
原网站

版权声明
本文为[Ang Jun Li]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/163/202206121731439050.html