您的位置 首页 golang

Go 每日一库之 casbin


简介

权限管理在几乎每个系统中都是必备的模块。如果项目开发每次都要实现一次权限管理,无疑会浪费开发时间,增加开发成本。因此,casbin库出现了。casbin是一个强大、高效的访问控制库。支持常用的多种访问控制模型,如ACL/RBAC/ABAC等。可以实现灵活的访问权限控制。同时,casbin支持多种编程语言,Go/Java/Node/PHP/Python/.NET/Rust。我们只需要一次学习,多处运用

快速使用

我们依然使用 Go Module 编写代码,先初始化:

$ mkdir casbin && cd casbin$ go mod init github.com/darjun/go-daily-lib/casbin复制代码

然后安装casbin,目前是v2版本:

$ go get github.com/casbin/casbin/v2复制代码

权限实际上就是控制能对什么资源进行什么操作。casbin将访问控制模型抽象到一个基于 PERM(Policy,Effect,Request,Matchers) 元模型的配置文件(模型文件)中。因此切换或更新授权机制只需要简单地修改配置文件。

policy是策略或者说是规则的定义。它定义了具体的规则。

request是对访问请求的抽象,它与e.Enforce()函数的参数是一一对应的

matcher匹配器会将请求与定义的每个policy一一匹配,生成多个匹配结果。

effect根据对请求运用匹配器得出的所有结果进行汇总,来决定该请求是允许还是拒绝

下面这张图很好地描绘了这个过程:

我们首先编写模型文件:

[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))复制代码

上面模型文件规定了权限由sub,obj,act三要素组成,只有在策略列表中有和它完全相同的策略时,该请求才能通过。匹配器的结果可以通过p.eft获取,some(where (p.eft == allow))表示只要有一条策略允许即可。

然后我们策略文件(即谁能对什么资源进行什么操作):

p, dajun, data1, readp, lizi, data2, write复制代码

上面policy.csv文件的两行内容表示dajun对数据data1read权限,lizi对数据data2write权限。

接下来就是使用的代码:

package mainimport (  "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")}复制代码

代码其实不复杂。首先创建一个casbin.Enforcer对象,加载模型文件model.conf和策略文件policy.csv,调用其Enforce方法来检查权限。运行程序:

$ go run main.godajun CAN read data1lizi CAN write data2dajun CANNOT write data1dajun CANNOT read data2复制代码

请求必须完全匹配某条策略才能通过。("dajun", "data1", "read")匹配p, dajun, data1, read("lizi", "data2", "write")匹配p, lizi, data2, write,所以前两个检查通过。第 3 个因为"dajun"没有对data1write权限,第 4 个因为dajundata2没有read权限,所以检查都不能通过。输出结果符合预期。

sub/obj/act依次对应传给Enforce方法的三个参数。实际上这里的sub/obj/actread/write/data1/data2是我自己随便取的,你完全可以使用其它的名字,只要能前后一致即可。

上面例子中实现的就是ACL(access-control-list,访问控制列表)。ACL显示定义了每个主体对每个资源的权限情况,未定义的就没有权限。我们还可以加上超级管理员,超级管理员可以进行任何操作。假设超级管理员为root,我们只需要修改匹配器:

[matchers]e = r.sub == p.sub && r.obj == p.obj && r.act == p.act || r.sub == "root"复制代码

只要访问主体是root一律放行。

验证:

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")}复制代码

因为sub = "root"时,匹配器一定能通过,运行结果:

$ go run main.goroot CAN read data1root CAN write data2root CAN execute data1root CAN rwx data3复制代码

RBAC 模型

ACL模型在用户和资源都比较少的情况下没什么问题,但是用户和资源量一大,ACL就会变得异常繁琐。想象一下,每次新增一个用户,都要把他需要的权限重新设置一遍是多么地痛苦。RBAC(role-based-access-control)模型通过引入角色(role)这个中间层来解决这个问题。每个用户都属于一个角色,例如开发者、管理员、运维等,每个角色都有其特定的权限,权限的增加和删除都通过角色来进行。这样新增一个用户时,我们只需要给他指派一个角色,他就能拥有该角色的所有权限。修改角色的权限时,属于这个角色的用户权限就会相应的修改。

casbin中使用RBAC模型需要在模型文件中添加role_definition模块:

[role_definition]g = _, _[matchers]m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act复制代码

g = _,_定义了用户——角色,角色——角色的映射关系,前者是后者的成员,拥有后者的权限。然后在匹配器中,我们不需要判断r.subp.sub完全相等,只需要使用g(r.sub, p.sub)来判断请求主体r.sub是否属于p.sub这个角色即可。最后我们修改策略文件添加用户——角色定义:

p, admin, data, readp, admin, data, writep, developer, data, readg, dajun, adming, lizi, developer复制代码

上面的policy.csv文件规定了,dajun属于admin管理员,lizi属于developer开发者,使用g来定义这层关系。另外admin对数据datareadwrite权限,而developer对数据data只有read权限。

package mainimport (  "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")}复制代码

很显然lizi所属角色没有write权限:

dajun CAN read datadajun CAN write datalizi CAN read datalizi CANNOT write data复制代码

多个RBAC

casbin支持同时存在多个RBAC系统,即用户和资源都有角色:

[role_definition]g=_,_g2=_,_[matchers]m = g(r.sub, p.sub) && g2(r.obj, p.obj) && r.act == p.act复制代码

上面的模型文件定义了两个RBAC系统gg2,我们在匹配器中使用g(r.sub, p.sub)判断请求主体属于特定组,g2(r.obj, p.obj)判断请求资源属于特定组,且操作一致即可放行。

策略文件:

p, admin, prod, readp, admin, prod, writep, admin, dev, readp, admin, dev, writep, developer, dev, readp, developer, dev, writep, developer, prod, readg, dajun, adming, lizi, developerg2, prod.data, prodg2, dev.data, dev复制代码

先看角色关系,即最后 4 行,dajun属于admin角色,lizi属于developer角色,prod.data属于生产资源prod角色,dev.data属于开发资源dev角色。admin角色拥有对proddev类资源的读写权限,developer只能拥有对dev的读写权限和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")复制代码

第一个函数中e.Enforce()方法在实际执行的时候先获取dajun所属角色admin,再获取prod.data所属角色prod,根据文件中第一行p, admin, prod, read允许请求。最后一个函数中lizi属于角色developer,而prod.data属于角色prod,所有策略都不允许,故该请求被拒绝:

dajun CAN read prod.datadajun CAN write prod.datalizi CAN read dev.datalizi CAN write dev.datalizi CANNOT write prod.data复制代码

多层角色

casbin还能为角色定义所属角色,从而实现多层角色关系,这种权限关系是可以传递的。例如dajun属于高级开发者seniorseinor属于开发者,那么dajun也属于开发者,拥有开发者的所有权限。我们可以定义开发者共有的权限,然后额外为senior定义一些特殊的权限。

模型文件不用修改,策略文件改动如下:

p, senior, data, writep, developer, data, readg, dajun, seniorg, senior, developerg, lizi, developer复制代码

上面policy.csv文件定义了高级开发者senior对数据datawrite权限,普通开发者developer对数据只有read权限。同时senior也是developer,所以senior也继承其read权限。dajun属于senior,所以dajundatareadwrite权限,而lizi只属于developer,对数据data只有read权限。

check(e, "dajun", "data", "read")check(e, "dajun", "data", "write")check(e, "lizi", "data", "read")check(e, "lizi", "data", "write")复制代码

RBAC domain

casbin中,角色可以是全局的,也可以是特定domain(领域)或tenant(租户),可以简单理解为。例如dajun在组tenant1中是管理员,拥有比较高的权限,在tenant2可能只是个弟弟。

使用RBAC domain需要对模型文件做以下修改:

[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=_,_,_表示前者在后者中拥有中间定义的角色,在匹配器中使用g要带上dom

p, admin, tenant1, data1, readp, admin, tenant2, data2, readg, dajun, admin, tenant1g, dajun, developer, tenant2复制代码

tenant1中,只有admin可以读取数据data1。在tenant2中,只有admin可以读取数据data2dajuntenant1中是admin,但是在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")}复制代码

结果不出意料:

dajun CAN read data1 in tenant1dajun CANNOT read data2 in tenant2复制代码

ABAC

RBAC模型对于实现比较规则的、相对静态的权限管理非常有用。但是对于特殊的、动态的需求,RBAC就显得有点力不从心了。例如,我们在不同的时间段对数据data实现不同的权限控制。正常工作时间9:00-18:00所有人都可以读写data,其他时间只有数据所有者能读写。这种需求我们可以很方便地使用ABAC(attribute base access list)模型完成:

[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))复制代码

该规则不需要策略文件:

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")}复制代码

显然lizi20:00不能read数据data

dajun CAN read data at 10:00lizi CAN read data at 10:00dajun CAN read data at 20:00lizi CANNOT read data at 20:00复制代码

我们知道,在model.conf文件中可以通过r.subr.objr.act来访问传给Enforce方法的参数。实际上sub/obj可以是结构体对象,得益于govaluate库的强大功能,我们可以在model.conf文件中获取这些结构体的字段值。如上面的r.sub.Namer.Obj.Owner等。govaluate库的内容可以参见我之前的一篇文章《Go 每日一库之 govaluate》

使用ABAC模型可以非常灵活的权限控制,但是一般情况下RBAC就已经够用了。

模型存储

上面代码中,我们一直将模型存储在文件中。casbin也可以实现在代码中动态初始化模型,例如get-started的例子可以改写为:

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")}复制代码

同样地,我们也可以从字符串中加载模型:

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")}复制代码

但是这两种方式并不推荐。

策略存储

在前面的例子中,我们都是将策略存储在policy.csv文件中。一般在实际应用中,很少使用文件存储。casbin以第三方适配器的方式支持多种存储方式包括MySQL/MongoDB/Redis/Etcd等,还可以实现自己的存储。完整列表看这里casbin.org/docs/en/ada…。下面我们介绍使用Gorm Adapter。先连接到数据库,执行下面的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', '', '', '');复制代码

然后使用Gorm Adapter加载policyGorm Adapter默认使用casbin库中的casbin_rule表:

package mainimport (  "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")}复制代码

运行:

dajun CAN read data1lizi CAN write data2dajun CANNOT write data1dajun CANNOT read data2复制代码

使用函数

我们可以在匹配器中使用函数。casbin内置了一些函数keyMatch/keyMatch2/keyMatch3/keyMatch4都是匹配 URL 路径的,regexMatch使用正则匹配,ipMatch匹配 IP 地址。参见casbin.org/docs/en/fun…。使用内置函数我们能很容易对路由进行权限划分:

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

不同用户只能访问其对应路由下的 URL:

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")}复制代码

输出:

dajun CAN read user/dajun/1lizi CAN read user/lizi/2dajun CANNOT read user/lizi/1复制代码

我们当然也可以定义自己的函数。先定义一个函数,返回 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]}复制代码

这里实现了一个简单的正则匹配,只处理*

然后将这个函数用interface{}类型包装一层:

func KeyMatchFunc(args ...interface{}) (interface{}, error) {  name1 := args[0].(string)  name2 := args[1].(string)  return (bool)(KeyMatch(name1, name2)), nil}复制代码

然后添加到权限认证器中:

e.AddFunction("my_func", KeyMatchFunc)复制代码

这样我们就可以在匹配器中使用该函数实现正则匹配了:

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

接下来我们在策略文件中为dajun赋予权限:

p, dajun, data/*, read复制代码

dajun对匹配模式data/*的文件都有read权限。

验证一下:

check(e, "dajun", "data/1", "read")check(e, "dajun", "data/2", "read")check(e, "dajun", "data/1", "write")check(e, "dajun", "mydata", "read")复制代码

dajundata/1没有write权限,mydata不符合data/*模式,也没有read权限:

dajun CAN read data/1dajun CAN read data/2dajun CANNOT write data/1dajun CANNOT read mydata复制代码

总结

casbin功能强大,简单高效,且多语言通用。值得学习。

大家如果发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue?

参考

  1. casbin GitHub:github.com/casbin/casb…
  2. casbin 官网:casbin.org/
  3. 一种基于元模型的访问控制策略描述语言:www.jos.org.cn/html/2020/2…
  4. Go 每日一库 GitHub:github.com/darjun/go-d…

我的博客:darjun.github.io

欢迎关注我的微信公众号【GoUpUp】,共同学习,一起进步~


文章来源:智云一二三科技

文章标题:Go 每日一库之 casbin

文章地址:https://www.zhihuclub.com/6972.shtml

关于作者: 智云科技

热门文章

网站地图