Golang最强大的访问控制框架casbin全解析

Golang最强大的访问控制框架casbin全解析

Casbin是一个强大的、高效的开源访问控制框架,其权限管理机制支持多种访问控制模型。目前这个框架的生态已经发展的越来越好了。提供了各种语言的类库,自定义的权限模型语言,以及模型编辑器。在各种语言中,golang的支持还是最全的,所以我们就研究casbin的golang实现。

访问控制模型

控制访问模型有哪几种?我们需要先来了解下这个。

UGO(User, Group, Other)

这个是Linux中对于资源进行权限管理的访问模型。Linux中一切资源都是文件,每个文件都可以设置三种角色的访问权限(文件创建者,文件创建者所在组,其他人)。这种访问模型的缺点很明显,只能为一类用户设置权限,如果这类用户中有特殊的人,那么它无能为力了。

ACL(访问控制列表)

它的原理是,每个资源都配置有一个列表,这个列表记录哪些用户可以对这项资源进行CRUD操作。当系统试图访问这项资源的时候,会首先检查这个列表中是否有关于当前用户的访问权限,从而确定这个用户是否有权限访问当前资源。linux在UGO之外,也增加了这个功能。

setfacl -m user:yejianfeng:rw- ./test
[yejianfeng@ipd-itstool ~]$ getfacl test
# file: test
# owner: yejianfeng
# group: yejianfeng
user::rw-
user:yejianfeng:rw-
group::rw-
mask::rw-
other::r--

当我们使用getfacl和setfacl命令的时候我们就能对某个资源设置增加某个人,某个组的权限列表。操作系统会根据这个权限列表进行判断,当前用户是否有权限操作这个资源。

RBAC(基于角色的权限访问控制)

这个是很多业务系统最通用的权限访问控制系统。它的特点是在用户和具体权限之间增加了一个角色。就是先设置一个角色,比如管理员,然后将用户关联某个角色中,再将角色设置某个权限。用户和角色是多对多关系,角色和权限是多对多关系。所以一个用户是否有某个权限,根据用户属于哪些角色,再根据角色是否拥有某个权限来判断这个用户是否有某个权限。

RBAC的逻辑有更多的变种。

变种一:角色引入继承

角色引入了继承概念,那么继承的角色有了上下级或者等级关系。

变种二:角色引入了约束

角色引入了约束概念。约束概念有两种,

一种是静态职责分离:
a、互斥角色:同一个用户在两个互斥角色中只能选择一个
b、基数约束:一个用户拥有的角色是有限的,一个角色拥有的许可也是有限的
c、先决条件约束:用户想要获得高级角色,首先必须拥有低级角色

一种是动态职责分离:
可以动态的约束用户拥有的角色,如一个用户可以拥有两个角色,但是运行时只能激活一个角色。

变种三:既有角色约束,又有角色继承

就是前面两种角色变种的集合。

ABAC(基于属性的权限验证)

Attribute-based access control,这种权限验证模式是用属性来标记资源权限的。比如k8s中就用到这个权限验证方法。比如某个资源有pod属性,有命名空间属性,那么我设置的时候可以这样设置:

Bob 可以在命名空间 projectCaribou 中读取 pod:
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user": "bob", "namespace": "projectCaribou", "resource": "pods", "readonly": true}}

这个权限验证模型的好处就是扩展性好,一旦要增加某种权限,就可以直接增加某种属性。

DAC(自主访问控制)

在ACL的访问控制模式下,有个问题,能给资源增加访问控制的是谁,这里就有几种办法,比如增加一个super user,这个超级管理员来做统一的操作。还有一种办法,有某个权限的用户来负责给其他用户分配权限。这个就叫做自主访问控制。

比如我们常用的windows就是用这么一种方法。
Golang最强大的访问控制框架casbin全解析_第1张图片

很多的wiki权限也是这样的权限管理方式。

MAC(强制访问控制)

强制访问控制和DAC相反,它不将某些权限下放给用户,而是在更高维度(比如操作系统)上将所有的用户设置某些策略,这些策略是需要所有用户强制执行的。这种访问控制也是基于某些安全因素考虑。

casbin的基本使用

casbin使用配置文件来设置访问控制模型。我们可以通过casbin的模型编辑器来查看。

它有两个配置文件,model.conf 和 policy.csv。其中 model.conf 存储的是我们的访问控制模型,policy.csv 存储的是我们具体的用户权限配置。

权限本质上就是最终询问这么一个问题“某个用户,对某个资源,是否可以进行某种操作”。casbin的使用非常精炼。基本上就生成一个结构,Enforcer,构造这个结构的时候加载 model.conf 和 policy.csv。使用示例如下:

import "github.com/casbin/casbin/v2"

e, err := casbin.NewEnforcer("path/to/model.conf", "path/to/policy.csv")

sub := "alice" // the user that wants to access a resource.
obj := "data1" // the resource that is going to be accessed.
act := "read" // the operation that the user performs on the resource.

ok, err := e.Enforce(sub, obj, act)  // 查看alice是否对data1z这个资源有read权限

if err != nil {
    // handle err
}

if ok == true {
    // permit alice to read data1
} else {
    // deny the request, show an error
}

当然,casbin 可以读取具体 policy 的时候不仅仅可以通过 csv 文件进行读取,也可以通过数据库进行读取。这样我们甚至可以写一个用户管理后台来配置不同的用户权限。model.conf 也是可以从配置文件中获取,也可以从代码中获取,从代码中获取就可以扩展为先读取数据库,再代码加载。但是 model.conf 一旦修改,对应的 policy 就需要进行同步修改,所以 model 在一个系统中不要进行频繁修改。

PML

casbin 是一种典型的“配置即一切”的软件思路,那么它的配置语法就显得格外重要。我们可以通过 casbin 的在线配置编辑器 https://casbin.org/en/editor 来进行学习。

casbin 的理论基础是这么一篇论文:PML:一种基于Interpreter的Web服务访问控制策略语言 。这篇论文是北大的三个学生一起发表的。要理解 casbin 的配置文件,就需要先看这篇论文。

论文的作者觉得现在云计算时代,权限管理系统是各种云非常重要的组成部分,但是各种权限管理模型在各个云厂商,或者各种云时代的产品又都不一样。那么是否有一种权限模型来统一描述各种权限访问方式呢?如果有的化,这种权限模型又需要独立于各种语言而存在,才能被各种语言的云产品所通用。

于是论文就创造除了这么一种语言:PML(PERM modeling language)。其中的 PERM 指的是 Policy-Effect-Request-Matcher 。 下面我们需要一一了解每一个概念。

Request

Request 代表的是请求,它的写法是

request ::= r : attributes
attributes ::= {attr1, attr2, attr3, ..}

比如我们写一行:

r = sub, obj, act

代表一个请求有三个标准的元素,请求主体,请求对象,请求操作。其中的sub, obj, act 可以是自己定义的,只要你在一个配置文件中定义的元素标识符一致就行。

Policy

Policy 代表策略,它表示具体的权限定义的规则是什么。

它同样是形如 p = sub, obj, act 的表示方法,比如我们定义了 policy 的规则如此,那么我们在 policy.csv 中每一行定义的 policy_rule 就必须和这个属性一一对应。

Policy_Rule

在 policy.csv 文件中定义的策略就是 policy_rule。它和 Policy 是一一对应的。

比如 policy 为

p = sub, obj, act

我设置的一条 policy_rule 为

p, bob, data2, write

表示bob(p.sub = bob)可以对data2 (p.obj = data2)进行 write (p.act = write) 操作这个规则。

policy 默认的最后一个属性为决策结果,字段名eft,默认值为allow,即通过情况下,p.eft就设置为allow。

Matcher

有请求,有规则,那么请求是否匹配某个规则,则是matcher进行判断的。

matcher ::=< boolean expr > (variables, constants, stub functions)
variables ::= {r.attr1, r.attr2, .., p.attr1, p.attr2, ..}
constants ::= {const1, const2, const3, ..}

比如下面这个matcher :

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

表示当(r.sub == p.sub && r.obj == p.obj && r.act == p.act )的时候,返回true,否则返回false。

Effect

Effect 用来判断如果一个请求满足了规则,是否需要同意请求。它的规则比较复杂一些。

effect ::=< boolean expr > (effect term1, effect term2, ..)
effect term ::= quantifier, condition
quantif ier ::= some|any|max|min
condition ::=< expr > (variables, constants, stub functions)
variables ::= {r.attr1, r.attr2, .., p.attr1, p.attr2, ..}
constants ::= {const1, const2, const3, ..}

这里的 quantifier一般是some(论文中支持max和min),some表示括号中的表达式个数大于等于1就行。max/min表示括号中表达式的结果取最大/小的。(这里我不是很理解,不过好像casbin也没有实现min和max)

下面这个例子:

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

这句话的意思就是将 request 和所有 policy 比对完之后,所有 policy 的策略结果(p.eft)为allow的个数 >=1,整个请求的策略就是为 true。

自定义函数

自定义函数是在 matcher 中使用的。我们可以自己定义一个函数,然后注册进enforcer,在matcher中我们就可以使用了。

比如


func KeyMatch(key1 string, 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]
}

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

casbin中有一些自定义的函数:

关系

上面几个概念关系如下:
Golang最强大的访问控制框架casbin全解析_第2张图片

大概解释一下:

1 我们先定义属性,通用的一些属性如 subject, object, action。
2 定义的属性可以作为 Request 的属性,也可以作为 Policy的属性。
3 Policy_Rule 是 Policy 的具体规则。
4 使用定义的 Matcher 将 Request 和 Policy 进行匹配,这个匹配的过程可能使用到自定义函数。
5 所有的 Policy 匹配完成的结果,通过 Effect 规则得出最终是否可以访问的结果。

例子:ACL

理解上面的知识,我们应该能理解这个ACL的例子:

这个例子中定义了两个 Policy_Rule: (alice 对 data1 有 read 权限) 和 (bob 对 data2 有 write 权限)

当request (alice, data1, read)进来的时候,它匹配了其中一条规则,所以some 之后的最终结果为true。

例子:RESTFUL

RESTFUL接口使用URL和HTTP请求方法表示资源的增删改查,那么我们可以使用自定义函数来判断是否可以进行某个请求

更多

这个论文还有一些其他的定义:

Has_Role

其实这个就是一个自定义函数的概念,只是它的参数是请求的主体和角色。这里引入了一个角色的概念。这个也是RBAC 权限模型所定义的。Has_Role 本质就是定义了一个 g 函数,这个 g 用于判断哪个用户是否属于哪个角色。这个 g 的函数也可以用配置写规则:

g = _, _

然后在 Policy 写规则:

g, alice, data2_admin

表示 alice 属于角色 data2_admin。
matcher 就可以写成这样:

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

例子:RBAC

我们来看下下面这个RBAC的规则:

我们可以看这里的 Policy 中,其实用户和角色是分不出来的,(比如我们单看policy里面的p,是不了解data2_admin是用户,还是角色的)。但是我们有一个 g (has_role)的规则,说明了alice 是有 data2_admin的角色的。

那么最终判断请求, alice, data2, read, 由于alice 有data2_admin的角色,它满足了(p, data2_admin, data2, read) 这条规则,所以最终判定结果为 true。

其实有了这个has_role,我们也可以把一个用户属于另一个用户的关系做出来。这个也就是 RBAC1 的。

Has Tenant Role

g 函数同时也可以有三个参数,两个参数的时候表示“谁 是 什么角色”,三个参数的时候表示“谁 在 什么域 是 什么角色”。

这个还是直接看例子:

在这个例子里面,有个域的概念,它就相当于可以表示“某个用户在某个域(租户)中是什么角色”。

这个是实现了一种基于RBAC的分权分域用户权限系统。

总结

Casbin 支持的权限模型有:

  • ACL (Access Control List, 访问控制列表)
  • 具有 超级用户 的 ACL
  • 没有用户的 ACL: 对于没有身份验证或用户登录的系统尤其有用。
  • 没有资源的 ACL: 某些场景可能只针对资源的类型, 而不是单个资源, 诸如 write-article, read-log等权限。 它不控制对特定文章或日志的访问。
  • RBAC (基于角色的访问控制)
  • 支持资源角色的RBAC: 用户和资源可以同时具有角色 (或组)。
  • 支持域/租户的RBAC: 用户可以为不同的域/租户设置不同的角色集。
  • ABAC (基于属性的访问控制): 支持利用resource.Owner这种语法糖获取元素的属性。
  • RESTful: 支持路径, 如 /res/*, /res/: id 和 HTTP 方法, 如 GET, POST, PUT, DELETE。
  • 拒绝优先: 支持允许和拒绝授权, 拒绝优先于允许。
  • 优先级: 策略规则按照先后次序确定优先级,类似于防火墙规则。

我们可以通过这个页面上的链接看每个权限模型的配置:https://casbin.org/docs/zh-CN/supported-models

源码阅读

我们阅读的是 v2.1.2版本
源码地址:https://github.com/casbin/casbin 。
注释版地址:https://github.com/jianfengye/inside-go/tree/master/casbin-2.1.2。

按照大象装进冰箱的逻辑,我们也很容易想象得到 casbin 应该分为几个步骤:

1 加载 model 的配置
2 加载 policy 的配置
3 具体请求进来之后,和 model 和 policy 进行匹配判断。

确实源码也就是这么写的。

整个 casbin 最核心的结构是

// Enforcer是权限验证的主体
type Enforcer struct {
    modelPath string            // model文件地址
    model     model.Model       // model结构
    fm        model.FunctionMap // 自定义函数
    eft       effect.Effector   // effecter的逻辑

    adapter persist.Adapter // 持久化的Adapter,就是police的Adapter
    watcher persist.Watcher
    rm      rbac.RoleManager // 这个要是这个模型是rbac(根据是否有g判断),就增加这个角色管理器

    enabled            bool // 这个Enforcer的开关
    autoSave           bool // 如果调用了api修改的Policy,是否自动保存到Adapter中
    autoBuildRoleLinks bool
}

所有加载的 model 和 policy 都是丰富这个结构体。

比如:

func (e *Enforcer) LoadModel() error

或者

func (e *Enforcer) LoadPolicy() error

里面的逻辑就不细说了,可以去看 https://github.com/jianfengye/inside-go/tree/master/casbin-2.1.2 看这个的源码注释。我就说几个我觉得这个项目代码值得学习或者比较特别的点。

代码依赖少

这个项目的 config, log 都是自己从标准库从头开始写的。我认为,作者一开始对项目就定位很清晰,这个是一个基础类库,能依赖尽可能少的项目就依赖尽可能少的项目。

文件夹及文件定义很明确

casbin 的文件夹结构并不是扁平的,而是树形结构,基本上是两层,甚至用到了三层。我个人觉得一个小的类库倒是没有必要分割这么多文件夹,很容易让人感觉很复杂。不过在 casbin 这个项目中,文件夹分割的还是比较清晰的。基本上是顺着 enforcer 这个结构体的定义,在涉及到需要扩展的字段的时候就起一个文件夹。每个扩展的文件夹基本都是 interface + implement 的方式。比如 enforcer 中有个 adapter 字段,使用了persist文件夹进行接口的定义和实现。

文件的定义也很清晰。比如 enforcer_interface 定义了80多个接口,在一个文件中全部实现并不是很好的写法,它就分成了同一个文件夹下的几个文件来写(internal_api, rbac_api, management_api,rbac_api_with_domain)。这样不仅减少了代码的长度,还使用文件名称把接口进行了分类。

基于 enforcer 扩展了 CacheEnforcer 和 SyncedEnforcer

我们写 enforcer 可能会想到是否需要缓存,是否配置变化能及时更新,这里使用了一个父类和两个子类的方式来实现。把是否使用缓存,是否使用配置的选择权交给用户。而不是简单在一个结构体里面塞上这些功能。

先定义接口

基本上每个可以扩展的结构都考虑到使用接口进行定义。定义接口扩展性提高了,且可以更加丰富了。

adapter

我觉得 persist/adapter 里面这个写法挺好的。

首先它定义了 adapter 这个接口,让实现接口的类来具体实现我从哪个持久化存储里面读取配置文件,但是希望每一行都有统一的读取规则,所以它就把 adapter 接口和 LoadPolicyLine 方法放同一个文件里面。

其次继承结构:
Golang最强大的访问控制框架casbin全解析_第3张图片
它实现了接口的继承,filterdApapter 接口继承 Adapter,file-filterdAdapter继承了file-Adapter。

如果我们写,有可能就简单定义了一个 filterLine 的函数,它这里更近一步,可以直接设置根据 p 或者 g 的某个字段进行过滤。

if err := e.LoadFilteredPolicy(&fileadapter.Filter{
    P: []string{"", "domain1"},
    G: []string{"", "", "domain1"},
}); err != nil {
    t.Errorf("unexpected error in LoadFilteredPolicy: %v", err)
}

使用起来就大大方便了。

总结

casbin 项目最核心的一定是那篇 PML 的论文。基于理论论文发展出来的这个项目也是非常牛X的。casbin 应该能满足大多数的权限管理系统的要求了。如果 casbin 不能实现你的权限需求的话,我觉得应该先思考下,产品经理提出的需求是不是靠谱的。。。哈哈哈

你可能感兴趣的:(Golang最强大的访问控制框架casbin全解析)