Go之Casbin简介,安装,模型,存储,函数

简介

Casbin是一个强大的,高效的开源访问控制框架,其权限管理机制支持多种访问控制模型mysql

支持编程语言

不一样语言中支持的特性

咱们一直致力于让 Casbin 在不一样的编程语言中拥有相同的特性。 可是现实老是不完美的。 上方的表格展现了当前的进度。 Watcher 和 Role Manager 的 ✅ 仅表明 Casbin 对该编程语言有接口, 是否实现了 watcher 或 role manager 接口则是另外一回事了。git

Casbin是什么?

Casbin 能够:github

  1. 支持自定义请求的格式,默认的请求格式为{subject, object, action}。
  2. 具备访问控制模型model和策略policy两个核心概念。
  3. 支持RBAC中的多层角色继承,不止主体能够有角色,资源也能够具备角色。
  4. 支持内置的超级用户 例如:root或administrator。超级用户能够执行任何操做而无需显式的权限声明。
  5. 支持多种内置的操做符,如 keyMatch,方便对路径式的资源进行管理,如 /foo/bar 能够映射到 /foo*

Casbin 不能:sql

  1. 身份认证 authentication(即验证用户的用户名、密码),casbin只负责访问控制。应该有其余专门的组件负责身份认证,而后由casbin进行访问控制,两者是相互配合的关系。
  2. 管理用户列表或角色列表。 Casbin 认为由项目自身来管理用户、角色列表更为合适, 用户一般有他们的密码,可是 Casbin 的设计思想并非把它做为一个存储密码的容器。 而是存储RBAC方案中用户和角色之间的映射关系。

常见访问控制模型

ABAC: 基于属性的访问控制。
DAC: 自主访问控制模型(DAC,Discretionary Access Control)是根据自主访问控制策略创建的一种模型,容许合法用户以用户或用户组的身份访问策略规定的客体,同时阻止非受权用户访问客体。拥有客体权限的用户,能够将该客体的权限分配给其余用户。
ACL:  ACL是最先也是最基本的一种访问控制机制,它的原理很是简单:每一项资源,都配有一个列表,这个列表记录的就是哪些用户能够对这项资源执行CRUD中的那些操做。当系统试图访问这项资源时,会首先检查这个列表中是否有关于当前用户的访问权限,从而肯定当前用户能否执行相应的操做。总得来讲,ACL是一种面向资源的访问控制模型,它的机制是围绕“资源”展开的。
RBAC: 基于角色的访问控制(RBAC, Role Based Access Control)在用户和权限之间引入了“角色(Role)”的概念,角色解耦了用户和权限之间的关系数据库

安装

go get github.com/casbin/casbin/v2

快速使用(ACL)

编写模型文件

// model.conf

[request_definition]
r = sub, obj, act

// definition  (defer 勒行 "定义")
[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))

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

policy (泼喔c "政策") 是策略或者说是规则的定义。它定义了具体的规则, effect 用来判断若是一个请求知足了规则,是否须要赞成请求.
request_definition 是对访问请求的抽象,它与e.Enforce()函数的参数是一一对应的, r=sub,obj,act 表明一个请求有三个标准元素: 请求主体,请求对象,请求操做.
matcher (麦觉 "匹配器" ) 匹配器会将请求与定义的每一个policy一一匹配,生成多个匹配结果, 有请求,有规则,那么请求是否匹配某个规则,则是matcher进行判断的.
effect (呃 fai "影响") 根据对请求运用匹配器得出的全部结果进行汇总,来决定该请求是容许仍是拒绝框架

上面模型文件规定了权限由sub,obj,act三要素组成,只有在策略列表中有和它彻底相同的策略时,该请求才能经过。匹配器的结果能够经过p.eft获取,some(where (p.eft == allow))表示只要有一条策略容许便可。
而后咱们策略文件(即谁能对什么资源进行什么操做):运维

// policy.csv

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

上面policy.csv文件的两行内容表示dajun对数据data1有read权限,lizi对数据data2有write权限。dom

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

使用代码

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

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

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

请求必须彻底匹配某条策略才能经过。("dajun", "data1", "read")匹配p, dajun, data1, read,("lizi", "data2", "write")匹配p, lizi, data2, write,因此前两个检查经过。第 3 个由于"dajun"没有对data1的write权限,第 4 个由于dajun对data2没有read权限,因此检查都不能经过。输出结果符合预期。
sub/obj/act依次对应传给Enforce方法的三个参数。实际上这里的sub/obj/act和read/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一概放行

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, "root", "data1", "read")
	check(e, "root", "data2", "write")
	check(e, "root", "data1", "execute")
	check(e, "root", "data3", "rwx")
}

// 访问
youmen@youmendeMacBook-Pro casbin_demo1 % go run main.go
root CAN read data1
root CAN write data2
root CAN execute data1
root CAN rwx data3

RBAC模型

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

单个RBAC

添加role_definition模块

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

// model.conf

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _

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

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

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

编写policy

// policy.csv

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

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

编写main.go

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

// 验证
// 很显然lizi所属角色没有write权限:
youmen@youmendeMacBook-Pro casbin_demo1 % go run main.go 
dajun CAN read data
dajun CAN write data
lizi CAN read data
lizi CANNOT write data

多个RBAC

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

编写modl.conf

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

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

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

编写policy.csv

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

先看角色关系,即最后 4 行,dajun属于admin角色,lizi属于developer角色,prod.data属于生产资源prod角色,dev.data属于开发资源dev角色。admin角色拥有对prod和dev类资源的读写权限,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.data
dajun CAN write prod.data
lizi CAN read dev.data
lizi CAN write dev.data
lizi CANNOT write prod.data

多层角色

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

编写policy.csv

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

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

上面policy.csv文件定义了高级开发者senior对数据data有write权限,普通开发者developer对数据只有read权限。同时senior也是developer,因此senior也继承其read权限。dajun属于senior,因此dajun对data有read和write权限,而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可能只是个弟弟.

编写model.conf

[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, read
p, admin, tenant2, data2, read
g, dajun, admin, tenant1
g, dajun, developer, tenant2

在tenant1中,只有admin能够读取数据data1。在tenant2中,只有admin能够读取数据data2。dajun在tenant1中是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 tenant1
dajun 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))

该规则不须要策略文件

package main

import (
	"fmt"
	"log"

	"github.com/casbin/casbin/v2"
)
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")
}

// 显然lizi在20:00不能read数据data:
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

咱们知道,在model.conf文件中能够经过r.sub和r.obj,r.act来访问传给Enforce方法的参数。实际上sub/obj能够是结构体对象,得益于govaluate库的强大功能,咱们能够在model.conf文件中获取这些结构体的字段值。如上面的r.sub.Name、r.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加载policy,Gorm Adapter默认使用casbin库中的casbin_rule表:

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", "test:xxxxx@tcp(36.5.139.203: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")
}


// 运行
youmen@youmendeMacBook-Pro casbin_demo1 % go run main.go
dajun CAN read data1
lizi CAN write data2
dajun CANNOT write data1
dajun CANNOT read data2

使用函数

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

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

policy.csv

p, dajun, user/dajun/*, read
p, lizi, user/lizi/*, read

不一样用户只能访问对应路由下的URL

package main

import (
	"fmt"

	"github.com/casbin/casbin/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() {
	e, err := casbin.NewEnforcer("./model.conf", "./policy.csv")
	if err != nil {
		fmt.Printf("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/1
lizi CAN read user/lizi/2
dajun 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")

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

dajun CAN read data/1
dajun CAN read data/2
dajun CANNOT write data/1
dajun CANNOT read mydata
相关文章
相关标签/搜索