对于使用教程就不多说了,文档中已经写的很详细了,今天主要关注 casbin 的内部实现。

从使用方式上来看,casbin 最重要的两个函数一个是 NewEnforced() 还有 Enforced() 两个函数,一个是负责对 model.conf 还有 policy 进行读取,从而生成自己的 model,另一个是将 model 与 rvals 进行对比,得出想要的结论

Untitled Diagram.png

Demo

model.conf

  1. # Request definition
  2. [request_definition]
  3. r = sub, obj, act
  4. # Policy definition
  5. [policy_definition]
  6. p = sub, obj, act
  7. # Policy effect
  8. [policy_effect]
  9. e = some(where (p.eft == allow))
  10. # Matchers
  11. [matchers]
  12. m = r.sub == p.sub && r.obj == p.obj && r.act == p.act

policy.csv

  1. p, alice, data1, read
  2. p, bob, data2, write

main.go

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "github.com/casbin/casbin"
  6. )
  7. func main() {
  8. // setup casbin auth rules
  9. e, err := casbin.NewEnforcer("./model.conf", "./policy.csv")
  10. if err != nil {
  11. log.Fatal(err)
  12. }
  13. ok, err := e.Enforce("alice", "data1", "read")
  14. if ok && err == nil {
  15. fmt.Print("Success")
  16. } else {
  17. fmt.Print("Fail")
  18. }
  19. }

Model

在实例中,我们使用 “./model.conf” 和 “policy.csv” 生成 Model,这个结构体是我们进行对比时的核心,在 model.conf 文件中,我们指定请求参数还有策略的形式,还可以声明 g 函数,可以用来绑定实体之间的关系,这一部分只能通过 model.conf 文件来进行指定。对于真正的策略信息,是在 policy 中的,通过 Adapter 接口,我们可以从不同的地方读取到这一内容。,官方提供的工具这里真的是相当之多了,基本上大部分的需求都能够满足。

  1. func (model Model) loadModelFromConfig(cfg config.ConfigInterface) error {
  2. for s := range sectionNameMap {
  3. loadSection(model, cfg, s)
  4. }
  5. // 检验是否全部需要的 section 都存在
  6. // ...
  7. }
  8. func loadSection(model Model, cfg config.ConfigInterface, sec string) {
  9. // 处理 r, r1 的情况
  10. i := 1
  11. for {
  12. if !loadAssertion(model, cfg, sec, sec+getKeySuffix(i)) {
  13. break
  14. } else {
  15. i++
  16. }
  17. }
  18. }
  19. // 这里 sec 是 "r", "p", "e", "m","g",而 key 是 “r”, “r1”
  20. func loadAssertion(model Model, cfg config.ConfigInterface, sec string, key string) bool {
  21. value := cfg.String(sectionNameMap[sec] + "::" + key)
  22. fmt.Println(value)
  23. return model.AddDef(sec, key, value)
  24. }

Model 其实是两个连续的 map,对于两个键值的关系,很多时候我们会看到 model[r][r] 的内容,这是因为我们将整个 model 文件看作是由 section 组成的,而没一个 section 中,能够多个表达式,也就是 r,r1 的样子,所以我们需要两层的 map 支持这一功能。

Untitled Diagram.png

每一个表达式最后会转化为一个 Assertion,每个 Assertion 主要的内容是两个切片,一个是 Tokens 存储我们的 model 内容,比如说如果 r = sub, obj, act 那么最后 Tokens 中会是 ["r_sub","r_obj","r_act"]

而 Policy 中就是策略的内容了,如果是文件的话,其实就是对于每一行都执行这个函数,如果是空或者 # 开头就会被忽略,否则附加到 Policy 后面。

  1. func LoadPolicyLine(line string, model model.Model) {
  2. if line == "" || strings.HasPrefix(line, "#") {
  3. return
  4. }
  5. tokens := strings.Split(line, ",")
  6. for i := 0; i < len(tokens); i++ {
  7. tokens[i] = strings.TrimSpace(tokens[i])
  8. }
  9. key := tokens[0]
  10. sec := key[:1]
  11. model[sec][key].Policy = append(model[sec][key].Policy, tokens[1:])
  12. }

在初始化中还有一个很重要的点是 FunctionMap,这是实现模式匹配的重点,这些函数存储在 ‘util/builtin_opertion.go’ 文件中,在注释中这些函数的作用已经很清楚了,因为对于表达式的处理,casbin 使用的是第三方库 govaluate,如果想要真正弄懂这些函数到底是怎么运作的,还需要去读这个库的源码

enforce()

这个函数应该是这个项目中最长的一个函数了,它的内容也相对来说复杂一点。这个函数内的核心操作其实就是使用 macther 生成表达式,并且将数据传入进行验证,然后验证的结果再进行整理。

function

这里使用的主要函数是 Knetic/govaluate,这里只用到其中一个函数,介绍中是这样的,我们在 matcher 中的函数可以调用我们传递进入的 functionMap 中的函数。

  1. functions := map[string]govaluate.ExpressionFunction {
  2. "strlen": func(args ...interface{}) (interface{}, error) {
  3. length := len(args[0].(string))
  4. return (float64)(length), nil
  5. },
  6. }
  7. expString := "strlen('someReallyLongInputString') <= 16"
  8. expression, _ := govaluate.NewEvaluableExpressionWithFunctions(expString, functions)
  9. result, _ := expression.Evaluate(nil)

parameters

传入到表达式的参数,定义在下面这个结构体中,在一次检测中 rTokens , rVals , pTokens 都是不变的,需要变得是 pVals 中的参数,主要是让 pVals 与 rVals 进行对比,有多少 Policy 就要对比到少次,也就产生多少结果。所以这个时候就需要 effect 来处理结果了。

  1. type enforceParameters struct {
  2. rTokens map[string]int
  3. rVals []interface{}
  4. pTokens map[string]int
  5. pVals []string
  6. }

最后产生的结果,是一个 interface{} 类型,有两种映射的可能 ,bool 和 float64,真的想象不到到底什么情况下会出现 float64 的情况,这个第三方库也提供计算功能吗?

effect

结果是 int 型的

  1. type Effect int
  2. const (
  3. Allow Effect = iota
  4. Indeterminate
  5. Deny
  6. )

其中一共有四种合并策略,这个实现也非常简单,仅仅只是字符串的匹配,本来还以为这是通过解析器做的

some(where (p.eft == allow)) 有一个允许,就允许
!some(where (p.eft == deny)) 全部允许才允许
some(where (p.eft == allow)) && !some(where (p.eft == deny)) 存在允许,切不存在拒绝
priority(p.eft) || deny 选择第一个确定的结果

RoleManager