最近在研究 iam 相关的内容,iam 即身份访问管理(identify access management) 身份: 用户在整个系统体系中扮演什么样的角色 访问管理:对身份的权限控制。 那既然谈到权限控制,不得不谈谈 casbin, casbin 目前是 github 上在权限控制处理方面 star 数是最多的,虽然之前没接触过 casbin,但也调研了一段时间,从易用性,扩展性,实现上来谈谈 casbin。

核心概念

权限模型目前主流的主要包括acl, rbac, abac。 简单先从这几个模型说起。而了解这几个模型之前,我们先约定一下一些术语。

  • subject 主体,在一般的权限体系中,主体一般是指操作者,比如某个用户,某个角色(其实角色最下面也是个用户),当然更广义的讲,可能主体也可以代表其他需要约束的对象,比如在物联网中,他也可以代表一个iot设备。
  • resouce 资源,一切可操作的实体都可以当做是一种资源,比如aliyun上的我们购买的ecs实例,后台系统的一个user
  • action 操作,主体对资源的操作,比如我们可以对ecs实例进行开机,关机,重启等操作,可以新增,删除,编辑一个user
  • condition 操作的条件,一般基于主体与资源的约事条件,比如 userip必须在172.168.0.1/24的段内
  • effect 影响效力,一般只有allow(允许)/deny(不允许) 有了这些术语,我们去解释对应的权限模型,可能更知道不同模型的区别

ACL

acl(访问控制列表),他代表的是,哪些用户(subject)或系统进程可以访问(action)资源(resource) 如果某个文件 apple.jpg(resource) 的 ACL 包含(Alice(subject): read(action),write(action);Bob(subject): read(action)),这将赋予 Alice 读写文件的权限,而只赋予 Bob 读取文件的权限。

RBAC

ABAC

casbin 中的概念

理解casbin,最主要理解其核心思路,casbin中的核心思路是使用policy(策略)中的规则去应用到model(模型)中,model 是一个规则引擎,帮我们处理一个主体(subject)是否有操作(action)某个资源(subject)的权限。

policy策略:一组规则的集合, 规则 model模型:对每行规则进行解析

因此,对于我们使用者来说,大多时候仅仅只需要关注policymodel就行。

model 初览

model层面,casbin又基于 PERM (Policy, Effect, Request, Matcher). 一个完整的model(perm)文件格式如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Request definition
[request_definition]
r = sub, obj, act

# Policy definition
[policy_definition]
p = sub, obj, act

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

# Matchers
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
request_definition
1
2
[request_definition]
r = sub, obj, act

定义了在 Enforcer.Enforce 方法中请求的参数和这些传入参数的顺序;一个基本的 Request 由一个三元组组成:[sub,obj,act],subject 是指访问的实体,也就是用户;obj 是指请求的资源,act 是指对这个资源的操作,比如alice请求了一个接口/user/add, 我们就可以传入sub(alice), obj(user/add), act(post)casbin的规则引擎会将alice,user/add,post这三个参数赋值给 r。

当然,如果我们需要更多的请求判断,可以将三元组往后扩展,变成四元组,甚至五元组,这取决于我们的业务需求。

policy_definition
1
2
[policy_definition]
p = sub, obj, act

定义了访问策略的模型,其实就是定义了在 Policy Document 中的策略规则的字段名称以及顺序, 同样,在一个 policy 定义中,由一个三元组组成:[sub,obj,act],但在不同的模型中,可能会有不一样,如 abac 模型中,sub 可以是一个表达式,用于对象属性的处理。

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

策略影响效力,一般 effect 的值为 allow,deny, 如果我们并未定义,默认的 p.eft 为 allow, 代表只要有任意一条规则匹配通过(allow),则代表最终的效力是通过(allow)的。

当然我们也可以显式的定义在策略规则(policy document)中。就像如下再通过一个属性去接收 eft

1
2
[policy_definition]
p = sub, obj, act, eft

对于效力的处理,默认的使用了 allow_overwrite. 同时,casbin 也支持 deny overwrite, 意思是没有匹配到任意一条 deny 的规则,那么最终的效力是通过(deny)的

1
2
[policy_effect]
e = !some(where (p.eft == deny))

而在大多数情况下,如果 allow 与 deny 同时出现,一般都是 deny 优先,就是未匹配到任意一条 deny 的规则,同时也得匹配到任意一条 allow 的规则,最终的效力才是通过(allow)的。 具体的定义如下:

1
2
[policy_effect]
e = some(where (p.eft == allow)) && !some(where (p.eft == deny))
matchers

通过上面的各种变量定义及效力判断,matchers 中就可以引入上面定义的内容,进行匹配规则的定义

1
2
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act

上面的含义就是 r.sub(请求中传递的主体)等于策略中的定义的主体 并且 请求中传递的操作资源 等于策略中的操作资源 并且 请求中传递的操作等于策略中定义的操作。

matchers 给了我们很大的自由度去定义匹配规则。同时 casbin 也内置了一些方法帮我们去快速的处理一些判断。

role_definition

如果使用 rbac 模型, 我们还需要定义用户的角色, 角色定义的值依旧从 policy document 中读取

1
2
[role_definition]
g = _, _
model 的存储

虽然 casbin 提供了很多方式存储 model,但一般对于我们使用者来说,可按以下两种情况去存储。

  1. 需要很多不同的 model 去匹配规则,那我们就存储在 mysql 中
  2. 只需要少量几个 model 去匹配规则,那我们可以以 string 的方式写在配置中

policy 文件

在上面了解了 model 的基本语法后,我们从 model 中可以看出,policy_definition 实际上就是读取 policy 文件的内容。

那 policy 文件长啥样? 大概的格式如下:

1
2
p,alice,data1,read  // 表示:alice 可以 read data1
p,bob,data2,write   // 表示:bob 可以 write data2

p 代表这是一条策略规则,alice, data1, read 分别对应 policy_definition 中的 sub, obj, act.

如果是使用 RBAC model,那么还会在这个文件定义用户与角色的关系,[role_definition] 根据定义生成用户和角色的实例,比如:

1
2
3
4
5
6
p,alice,data1,read
p,bob,data2,read
p,data2_admin,data2,read   //表示 data2_admin 可以 read data2
p,data2_admin,data2,write  //表示 data2_admin 可以 write data2
g,alice,data2_admin  //表示 alice 是 data2_admin

g 代表这是一条角色定义策略,表示用户 alice 属于角色 data2_admin.

policy document 存储

官方给出的示例基本上是以文件的形式存储成以下格式

1
2
p,alice,data1,read  // 表示:alice 可以 read data1
p,bob,data2,write   // 表示:bob 可以 write data2

而对于我们使用者来说,显然,不会以文件文本的方式存储,一般都会存储至数据库,或存储至 redis 做缓存。 官方有一些 adapter 的封装,如果存储至 mysql, policy 的定义字段大概如下:

id ptype v0 v1 v2 v3 v4 v5
1 p data2_admin data2 write
2 p data2_admin data2 read
3 g alice admin

可以看出,对应的字段的含义其实是与我们上面的文件形式一一对应的。ptype 代表策略规则类型,v0-v5 代表具体规则的内容。一般可能我们只能用到其中几列,但如果我们规则的复杂度够高,可能需要更多的列,那就可以在剩下的列中处理。一般的 adapter 也只帮我们处理到 v0-v5,比如 gorm-adapter, 所以,如果有超过 v5 的列,只能只行去实现 adapter.

实现 adapter 也很简单,adapter 是一个接口类型,只需要实现以下接口即可

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Adapter is the interface for Casbin adapters.
type Adapter interface {
	// LoadPolicy loads all policy rules from the storage.
	LoadPolicy(model model.Model) error
	// SavePolicy saves all policy rules to the storage.
	SavePolicy(model model.Model) error

	// AddPolicy adds a policy rule to the storage.
	// This is part of the Auto-Save feature.
	AddPolicy(sec string, ptype string, rule []string) error
	// RemovePolicy removes a policy rule from the storage.
	// This is part of the Auto-Save feature.
	RemovePolicy(sec string, ptype string, rule []string) error
	// RemoveFilteredPolicy removes policy rules that match the filter from the storage.
	// This is part of the Auto-Save feature.
	RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error
}

实战

如何实现一个模型,带有主体的属性判断,主体又可能是角色/用户组/用户

在我们的系统中,不纯粹只有 rbac, 还有部分带有 abac 的特性,比如需要主体(用户)的 ip 在某个段内(比如 192.168.1.0/24)访问, 或需要主体的访问时间在每天的 10 点-22 点之间,那如何处理呢?

  • 定义 request_definition
1
2
3
# Request definition
[request_definition]
r = sub, obj, act, cond

cond 代表接收一个条件,我们可以用 json 去传递,例如,{“ip”:“192.168.1.2”}

  • 定义 policy_definition
1
2
[policy_definition]
p = sub, obj, act, eft, cond

同时定义 eft, cond, eft 用于定义一组规则的影响效力,cond 代表这组规则比如 ip 的域需要在 192.168.1.0/24 内,例如 {“cidr”:{“type”:“equal”, “value”:“192.168.1.0/24”}}

  • 定义 policy_effect, 我们使用的是 deny 优先,同时规则必须包含要命中 allow。
1
2
[policy_effect]
e = some(where (p.eft == allow)) && !some(where (p.eft == deny))
  • 定义 matcher, 因为增加了一个 cond 的匹配,而 cond 的匹配很灵活,我们可以借助自定义的函数帮助检验
1
2
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act && condChecker(r.cond, p.cond)
  • 定义 role_definition, 因为有使用到角色相关的
1
2
[role_definition]
g = _, _
  • 自定义函数 condChecker
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func condCheckerFunc(args ...interface{})(interface{}, error) {
    key1, key2 := args[0].(string), args[1].(string)

    return bool(condChecker(key1, key2)), nil
}

func condChecker(key1 string, key2 string) bool {
    if key2 == "" {
        return true
    }

    if key1 == "" {
        return false
    }

    // 解析key2 json string
    // 解析key1 json string

    // todo 这边可以根据key2的具体类型去判断key1传递的条件值 是否符合key2的设定

}
  • policy document 的记录
1
2
3
4
5
6
p,alice,/user/add,post,allow,{"ip":{"type":"equal", "value": "192.168.1.0/24"}}
p,bob,/user/list,get,deny,
p,group:developer,/user/policy/add,post,allow,{"ip":{"type":"equal", "value": "192.168.1.0/24"}}

g alice role:data_admin
g alice group:developer
  • 鉴权处理
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
var modelTxt = `
# Request definition
[request_definition]
r = sub, obj, act, cond

[policy_definition]
p = sub, obj, act, eft, cond

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

[role_definition]
g = _, _

[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act && condChecker(r.cond, p.cond)
`

a := `
p,alice,/user/add,post,allow,{"ip":{"type":"equal", "value": "192.168.1.0/24"}}
p,bob,/user/list,get,deny,
p,group:developer,/user/policy/add,post,allow,{"ip":{"type":"equal", "value": "192.168.1.0/24"}}

g alice role:data_admin
g alice group:developer
`

m, err := model.NewModelFromString(modelTxt)

if err != nil {
    return err
}

e, err := casbin.NewEnforcer(m, a)
if err != nil {
    return err
}
sub, obj, act ,cond := "alice", "/user/add", "post", `{"ip":"192.168.1.10"}`
res, err := e.Enforce(sub, obj, act, cond)
if err != nil {
    return err
}
return res

可能存在的问题

  • 如果使用 casbin 的 adapter,目前看,所有的用户共享一张表,casbin 很有可能将这些所有的策略规则都放在一个文件里,很显然,当规则很多的时候,每个用户都会走一次全部的规则,是不是会很影响效率?

  • 如果我们建立的规则类似 aws iam 这种组织方式,可能一个策略中 resource, condition, effect 都共用,而 casbin 的 policy 更像是结构化的存储,多个 action 就冗余 resource, conition, effect, 实际上还是挺占用空间的。在设计层面上可能不太优雅,但对于量不是特别大的话,可能影响也并不算大