Casdoor的权限检查

Casdoor 的权限检查机制是基于 Casbin 的,参考文档:

简要介绍

所有在同一个组织的用户可以访问该组织下的所有应用,但是有时候需要做一些限制,这里就要用到 权限(permission) 了。

那么 权限(permission) 的作用就是用于控制 用户 能否对 应用 进行某些 操作

最简单的方式就是在 权限(permission) 配置页面新建一个权限,如下图所示:

image.png

作为三个要素,用户,应用和操作,在这里是需要配置的:

  • 包含用户
  • 资源
  • 动作

意义也比较简单,就是哪些 用户 可以对哪些 应用 进行哪些 操作

包含角色 也容易理解,一个角色对应多个人,所以这里就是用来控制一组人的。

那上面的模型是什么意思呢,Casdoor 是如何根据这个配置进行权限校验的呢?

ACL 权限控制模型

ACL 权限控制模型应该很熟悉了吧,Linux 上就在使用,这里就不再赘述,参考:

Casbin 的权限控制

Casbin 是一个开源的权限控制库,它支持了多种权限控制模型

Casbin 实施权限规则比较简单,管理员需要列出 主体(subject)对象(object) 和期望允许的 操作(action) 即可:

  • subject,主体,比如用户
  • object,对象,需要被权限控制的对象
  • action,动作,read,write,delete或者其他定义的动作(操作)

管理员需要定义 模型(model) 文件来确定校验条件,Casbin 提供了一个 执行器(Enforcer) 来根据规则定义和模型文件来校验请求。

也就是说 Casbin 权限校验需要三个部分:模型(Model), 校验规则(Policy)和执行器(Enforcer)。

执行器是 Casbin 自带的,可以不必特别关注,只需要知道这件事情就行。下来着重研究模型和校验规则。

哦,对了进行权限校验还需要一个 请求(request)

模型定义

Casbin 里的权限控制模型被抽象成了一个 CONF 文件,基于 PERM metamodel (Policy, Effect, Request, Matchers),这个 PERM metamodel 基于 4 个部分 Policy, Effect, Request, Matchers,其描述了 资源 和 用户 之间的关系。

Casbin 里最简单的一个模型,也是默认模型:

1
2
3
4
5
6
7
8
9
10
11
[request_definition]
r = sub, obj, act

[policy_definition]
p = permission, 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
Request

r = sub, obj, act

定义了请求参数,也就是说请求需要三个部分,subject,object 和 action,通俗点讲就是 访问的用户,访问的对象,访问的方法。

这里其实定义了我们应该提供给权限控制匹配函数的参数名和顺序。

Policy

p = permission, sub, obj, act

定义了模型的访问策略,其实是定义了在策略规则文档中字段名和顺序

这里还有一种定义:p={sub, obj, act, eft}

eft是可以被省略的,eft代表的是策略的结果,比如就是 allow 或者是 deny,如果没有定义的话,读取策略文件的时候就会被忽略,默认会返回 allow

Matcher

m = r.sub == p.sub && r.obj == p.obj && r.act == p.act

请求和策略的匹配规则,这里 r 就是请求(request),p 就是策略(policy)

Effect

e = some(where (p.eft == allow))

就是对策略的结果再次进行条件判断,这里的意思就是只要有一条规则返回的是 allow,那么整个就是 allow

还有这样的定义:e = some(where (p.eft == allow)) && !some(where (p.eft == deny))

就是说有一条是 allow 而且 没有 deny 的情况,整个结果是 allow

这里主要是针对多条匹配规则都匹配上的情况做处理的

在 Casdoor 定义权限控制

定义模型

我们已经了解了模型的定义规则,那么就开始在 Casdoor 上面实际操作一番吧。

模型定义用来针对要控制的 object,访问的 subject 能允许怎样的 action

首先在页面顶部导航栏点击 模型(Model),点击新增:

image.png

模型文本这里就填写上面介绍过的模型定义字符串,点击保存&退出

定义权限

在页面顶部导航栏点击 权限(permission),点击新增:

image.png

permission 其实就是具体的控制权限设置,可以针对多个资源进行控制

其实这里的配置界面只是简化了规则的配置,这个页面的权限其实会转换成类似如下的数据:

1
2
p	alice	data1	read
p bob data2 write

代码上

image.png

permission 里定义的规则会先转换成 CasbinRule 对象,每一个对象就包含了 Policy 定义的每个字段,之后就会转成如下的字符串:

p, built-in/permission-built-in, built-in/app-built-in/admin, app-built-in, read

执行校验

模型和权限定义好之后,对应被控制的用户访问被控制的应用时,Casdoor 就会进行加载对应模型和权限进行校验,执行这一工作的叫做 Enforcer

关于 Enforcer 的文档:New a Casbin enforcer

代码分析

流程已经分析差不多了,让我们看看代码上是怎么处理的

Casdoor 是在这个函数做权限校验的。用户登陆时候就会调用,当然在任何你想进行权限校验的地方,都可以进行调用。

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
func CheckAccessPermission(userId string, application *Application) (bool, error) {
permissions := GetPermissions(application.Organization)
allowed := true
var err error
for _, permission := range permissions {
if !permission.IsEnabled || len(permission.Users) == 0 {
continue
}

isHit := false
for _, resource := range permission.Resources {
if application.Name == resource {
isHit = true
break
}
}

if isHit {
enforcer := getEnforcer(permission) // v2.Enforcer
allowed, err = enforcer.Enforce(userId, application.Name, "read")
break
}
}
return allowed, err
}

逻辑就是通过 organization 来获取所有的 permission 配置,接下来遍历 permission 中定义的 resource,如果跟当前的应用匹配,那么用这条权限去检查用户是否有对应的权限。

匹配成功之后,调用 getEnforcer 来获取一个执行权限校验的 enforcer。

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
func getEnforcer(permission *Permission) *casbin.Enforcer {
tableNamePrefix := conf.GetConfigString("tableNamePrefix")
adapter, err := xormadapter.NewAdapterWithTableName(conf.GetConfigString("driverName"), conf.GetBeegoConfDataSourceName()+conf.GetConfigString("dbName"), "permission_rule", tableNamePrefix, true)
if err != nil {
panic(err)
}

modelText := `
[request_definition]
r = sub, obj, act

[policy_definition]
p = permission, 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`

permissionModel := getModel(permission.Owner, permission.Model)
if permissionModel != nil {
modelText = permissionModel.ModelText
}
m, err := model.NewModelFromString(modelText)
if err != nil {
panic(err)
}

enforcer, err := casbin.NewEnforcer(m, adapter)
if err != nil {
panic(err)
}

err = enforcer.LoadFilteredPolicy(xormadapter.Filter{V0: []string{permission.GetId()}})
if err != nil {
panic(err)
}

return enforcer
}

getEnforcer 做的工作就是根据 权限(permission) 数据去看有没有定义好的model,有的话使用定义好的模型文本来加载 enforcer,否则使用默认的模型文本:

1
2
3
4
5
6
7
8
9
10
11
[request_definition]
r = sub, obj, act

[policy_definition]
p = permission, 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

加载模型文本之后,enforcer 就会加载对应 权限(permission) 所对应定义的规则,对应 LoadFilteredPolicy 函数。

这些数据加载完成之后,enforcer 会根据传入的 subject,entity,action,即 用户,应用,操作,来检查权限是否允许

allowed, err = enforcer.Enforce(userId, application.Name, "read")

总结

总结一下,主要有以下核心:

  1. 理解 Casdoor 的权限是做什么的
  2. 理解 模型(Model) 以及 模型文本 是如何定义的
  3. 理解 模型,权限是如何配置的,以及是如何对用户,应用进行控制的
  4. 理解代码上是如何调用的,可以扩展到其他需要进行权限校验的地方

注意

在代码上新建或者编辑权限(permission) 之后,记得要重新生成一下规则(policy),因为不论在界面上是怎样的表现形式,最后都是enforcer通过model textpolicy来检查request

代码如下:

1
2
3
4
5
6
7
8
9
10
// 更新permission之后需要调用
if affected != 0 {
removePolicies(oldPermission)
addPolicies(permission)
}

// 新建permission之后需要调用
if affected != 0 {
addPolicies(permission)
}

Casdoor 部署问题

后端运行时 timeout

运行go run main.go,时候报 get 包 timeout,应该是代理问题,解决办法就是:go env -w GOPROXY=https://goproxy.cn,direct

如下:

1
2
3
4
5
6
7
8
duguangting@c123:~/casdoor$ go get github.com/RobotsAndPencils/go-saml@v0.0.0-20170520135329-fb13cb52a46b
go: github.com/RobotsAndPencils/go-saml@v0.0.0-20170520135329-fb13cb52a46b: Get "https://proxy.golang.org/github.com/%21robot
s%21and%21pencils/go-saml/@v/v0.0.0-20170520135329-fb13cb52a46b.mod": dial tcp 172.217.163.49:443: i/o timeout
duguangting@c123:~/casdoor$ go env -w GOPROXY=https://goproxy.cn,direct
duguangting@c123:~/casdoor$ go get github.com/RobotsAndPencils/go-saml@v0.0.0-20170520135329-fb13cb52a46b
go: downloading github.com/RobotsAndPencils/go-saml v0.0.0-20170520135329-fb13cb52a46b
go: downloading github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d
go: downloading github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0

前端运行报错

后端跑通之后,前端运行yarn install的时候报:

The engine "node" is incompatible with this module

解决办法:

yarn install --ignore-engines

Goland 方法没法跳转

在Goland里所有引入的外部包无法跳转,搜了一下,大概这样:

  • step 1

在 Goland 的 go modules 设置勾选,然后设置 GOPROXY

image.png

  • step2

然后因为项目已经有 go.mod 文件了,那么直接执行 go mod tidy,这个命令的作用就是:add missing and remove unused modules

然后就 OK 了

初识 Casdoor

最近工作需要研究了下Casdoor:

Casdoor is a UI-first Identity Access Management (IAM) / Single-Sign-On (SSO) platform based on OAuth 2.0, OIDC, SAML and CAS.

简介

Casdoor 是一个开源的单点登录系统,单点登录系统的好处就是集中管理用户,使的我们开发的应用只需要关心业务逻辑而不用每个应用都是去实现一套用户系统。

Casdoor 使用了 OAuth2 的方式来完成单点认证,大概逻辑如下图:

image.png

总结一下就是:

  • 第一步 获取code
  • 第二步 用code获取access_token
  • 第三步 用access_token获取所需要的资源

安装

服务安装文档

文档说的很明白,我这里是下载源码使用的,因为我用的PostgreSQL,所以需要改一下app.confadapter.go

然后后端启动:go run main.go

后端启动:

1
2
3
cd web
yarn install
yarn start

非常简单

界面配置

这里我是用最小的改动来完成对接demo的,首先需要添加一个组织,组织就是一堆资源,应用的的集合:

image.png

后面的用户,角色,权限,模型,提供商都可以先不管,需要了解的可以看文档

然后需要添加一个新的应用,主要就是下图这些

image.png

客户端ID和客户端密钥,都是后面对接时候需要的

还有一个就是证书,这个不需要改,只需要点编辑进入将公钥拿到

image.png

上面就是在界面上需要完成的事情和完成一个对接demo所需要的数据了

web 对接

下面是和 Casdoor 对接的 web 接口代码,主要有两部分,第一部分是获取 code,第二部分是通过 code 获取 access_token,而这个 access_token 已经包含了用户信息,用对应的公钥进行解密,即可得到一个用户的 json 数据。

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
44
45
import logging
from urllib.parse import urljoin

from casdoor import CasdoorSDK


@route(r'/casdoor/sso-login')
class CASDoorSSOLoginHandler(BaseHandler):

def save_user(self, user_data):
user = User.make_user(uid=user_data['id'], ext_uname=user_data['name'], username=user_data['name'], _from='casdoor')
return user

def get(self, *args, **kwargs):
code = self.get_argument('code', None)
target_uri = self.get_argument('target_uri')

subpath = config.get_config("webif.redirect_subpath", '')
trident_base = urljoin(self.origin_host, subpath.lstrip('/'))

endpoint = config.get_config('casdoor_auth.endpoint')
client_id = config.get_config('casdoor_auth.client_id')
client_secret = config.get_config('casdoor_auth.secret')
org_name = config.get_config('casdoor_auth.org_name')
certificate = config.get_config('casdoor_auth.cert')

sdk = CasdoorSDK(
endpoint,
client_id,
client_secret,
certificate,
org_name,
)

if code:
access_token = sdk.get_oauth_token(code)
user_data = sdk.parse_jwt_token(access_token)
user = self.save_user(user_data)
self.session['proxy_user_id'] = str(user.id)
redirect_url = urljoin(trident_base, target_uri)
else:
origin_url = '{}/api/v1/casdoor/sso-login?target_uri={}'.format(trident_base, target_uri)
redirect_url = sdk.get_auth_link(origin_url, state='casdoor')
logging.info('redirect to %s', redirect_url)
return self.redirect(redirect_url)

配置也比较简单:

image.png

这里需要特别注意的是,endpoint 配的是 Casdoor 访问的首页地址,也就是这里是前端地址,而不是后端地址。前端地址是:http://localhost:7001/,后端地址是:http://localhost:8000/,这里很有迷惑性,让我折腾了半天。