perm元模型
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, read
p, lizi, data2, write
上面policy.csv文件的两行内容表示dajun对数据data1有read权限,lizi对数据data2有write权限。
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
上面例子中实现的就是
ACL(access-control-list,访问控制列表)。
ACL显示定义了每个主体对每个资源的权限情况,未定义的就没有权限。我们还可以加上超级管理员,超级管理员可以进行任何操作。假设超级管理员为
root,我们只需要修改匹配器:
[matchers]
e = r.sub == p.sub && r.obj == p.obj && r.act == p.act || r.sub == “root”
只要访问主体是root一律放行。
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.sub与p.sub完全相等,只需要使用g(r.sub, p.sub)来判断请求主体r.sub是否属于p.sub这个角色即可。最后我们修改策略文件添加用户——角色定义:
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权限。
多个RBAC
casbin支持同时存在多个RBAC系统,即用户和资源都有角色:
[role_definition]
g=,
g2=,
[matchers]
m = g(r.sub, p.sub) && g2(r.obj, p.obj) && r.act == p.act
策略文件:
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的读权限。
多层角色
casbin还能为角色定义所属角色,从而实现多层角色关系,这种权限关系是可以传递的。例如dajun属于高级开发者senior,seinor属于开发者,那么dajun也属于开发者,拥有开发者的所有权限。我们可以定义开发者共有的权限,然后额外为senior定义一些特殊的权限。
模型文件不用修改,策略文件改动如下:
p, senior, data, write
p, developer, data, read
g, dajun, senior
g, senior, developer
g, lizi, developer
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, read
p, admin, tenant2, data2, read
g, dajun, admin, tenant1
g, dajun, developer, tenant2
在tenant1中,只有admin可以读取数据data1。在tenant2中,只有admin可以读取数据
data2。dajun在tenant1中是admin,但是在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")
}
策略存储
在前面的例子中,我们都是将策略存储在policy.csv文件中。一般在实际应用中,很少使用文件存储。casbin以第三方适配器的方式支持多种存储方式包括MySQL/MongoDB/Redis/Etcd等,还可以实现自己的存储。完整列表看这里https://casbin.org/docs/en/adapters。下面我们介绍使用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", "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")
}
使用函数
我们可以在匹配器中使用函数。casbin内置了一些函数keyMatch/keyMatch2/keyMatch3/keyMatch4都是匹配 URL 路径的,
regexMatch使用正则匹配,ipMatch匹配 IP 地址。参见https://casbin.org/docs/en/function。使用内置函数我们能很容易对路由进行权限划分:
[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
不同用户只能访问其对应路由下的 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")
}
我们当然也可以定义自己的函数。先定义一个函数,返回 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