Casbin 简介

Casbin 是一个开源的访问控制框架,支持包括 ACL、RBAC、ABAC 在内的多种访问控制模型

Casbin 中使用了称为 PERM(Policy, Effect, Request, Matcher)的访问控制元模型,相应的策略描述语言 PML(PERM modeling language)能够表达一些列常见的访问控制模型。

策略实施机制 PML-EM(PML enforcement mechanism)包括策略规则层、策略规则适配器层(可以将某一种策略语言描述的规则转换为等价的 PML 规则)、模型层、解释器层(PML 解释器采用 Lua 实现)。
在 PML-EM 中,策略规则适配器层实现了策略语言无关性,模型层实现了访问控制模 型无关性,解释器层实现了程序设计语言无关性(只需提供 Lua 语言的解释器)。

反映策略实施逻辑的抽象访问控制模型称为“模型”(model),被实施的策略规则称为“策略”或“策略规则”。模型代表了逻辑,策略代表数据(类似于一个程序中代码与数据之间的关系)。

使用 Casbin 后切换或升级项目的授权机制只需修改修改模型配置,可以通过组合可用的模型来定制访问控制模型(例如可以在一个模型中获得 RBAC 角色和 ABAC 属性,并共享一组 policy 规则)。

PERM 元模型原语:

  1. Request 原语:对访问请求的抽象,定义了访问请求的语义

    • 一个访问请求通常由经典的三元组描述:进行访问的主体(subject)、被访问的客体(object)、访问动作(action)。PML 中三元组 Request 原语的语法可以表示为 r = sub, obj, act
    • PML 支持定制原语。
  2. Policy 原语:定义权限规则的原语,决定了 PML 策略的解释器会如何解释策略规则。

    • 经典的 Policy 原语也由主体、客体、动作三元组构成,表示为 p = sub, obj, act,相应的策略规则可以是 p, alice, data1, read,两者的绑定关系为 (alice, data1, read)->(p.sub, p.obj, p.act),类似于变量的赋值,可以在 Matcher 原语中使用。
    • 策略规则的第一个元素是策略类型,与相应的 Policy 原语对应(p, 中的 pp= 中的 p 对应)。支持多个 Policy 原语,如 p, p2。
  3. Matcher 原语:定义了匹配器,它决定了策略规则与访问请求之间的匹配关系。

    • 本质上是布尔表达式,1 表示策略规则满足该匹配器,0 表示不满足。
    • 可以理解为 Request、Policy 原语定义了关于请求和策略的变量,然后将这些变量代入 Matcher 原语中求值,从而进行策略决策。经典三元组的 Matcher 原语可以表示为 m = r.sub == p.sub && r.obj == p.obj && r.act == p.act,表示访问请求中的主体、客体、动作三元组应与策略规则中的主体、客体、动作三元组分别对应相同。
    • Matcher 原语支持算数运算符、关系运算符、逻辑运算符。
  4. Effect 原语:定义了当多个策略规则同时匹配访问请求时,该如何对多个决策结果进行汇总以实现统一决策。

    • p.eft 表示策略规则的决策结果,决策结果包括 allowdeny。
    • Effect 原语支持 someany 等量词,条件关键字 where,与、或、非等逻辑运算符。允许优先(allow-override)可以表示为 e = some(where(p.eft == allow)),拒绝优先(deny-override)可以表示为 e = !some(where(p.eft == deny))
    • 原语可以使用逻辑运算符进行连接,e = some(where(p.eft == allow)) && !some(where(p.eft == deny)) 表示至少存在一个决策结果为 allow 的策略规则且不存在决策结果为 deny 的策略规则时,最终决策结果为 allow
    # requet: alice, data1, read
    p, alice, data1, read
    p, alice, data*, read
    # matcher: m = r.sub == p.sub && ((regexMatch(r.obj, p.obj) && r.obj != 'data1') || r.obj == p.obj) && r.act == p.act
    ## regexMatch(any_string, regular_expression_pattern) # https://casbin.org/docs/zh-CN/function#matchers%E4%B8%AD%E7%9A%84%E5%87%BD%E6%95%B0
    
  5. Role 原语:定义 RBAC 模型中的角色继承关系

    • 支持多重 RBAC 体系,如主体和客体同时具有角色(或组)的概念时,主体角色和客体角色两套 RBAC 体系互相独立、互不干扰。
    g=_,_
    g2=_,_
    
    • gg2 是两个独立的 RBAC 体系。
    • _,_ 表示角色继承关系的前项和后项,前项继承后项角色的权限PML 把角色继承关系也表达为策略规则,如 g, alice, data2_admin,此规则表示 alice 具有角色 data2_adminalice 可以是具体的某个主体或是另一个角色,其中第一个元素 g 表示策略类型,与相应的 Role 原语对应(g, 中的 g 与 g= 中的 g 对应)。
    • Matcher 原语中请求主体与策略规则主体之间是否具有角色继承关系可以用布尔函数 g(r.sub, p.sub)p.sub 来自 Policy 原语的策略规则,而非角色继承关系的策略规则),值为 1 表示两者具有角色继承关系,0 表示不具备继承关系。角色继承关系支持间接继承。
      • 猜:request 主体 => 根据 g, alice, data2_admin 策略找到 data2_admin,加上 request 主体本身(即 alice)一起 => 找到 p, * 中的对应策略,进行实际比对鉴权
    • Matcher 原语中的布尔函数 g 与 Role 原语中的 g 和策略规则中相应的策略类型 g 互相对应,共同完成 RBAC 角色的指派。
    • RBAC 中角色是权限的容器。

PML 通过域内 Role 原语实现对租户的支持,一个域代表一个租户,域内角色只在本域内生效,也只能分配本域内的资源的权限。

# 域内 Role 原语,第三个 `_` 表示域
g=_,_,_
# 策略规则
p,admin,tenant1,data1,read
p,admin,tenant2,data2,read
g,alice,admin,tenant1
g,alice,user,tenant2
# Matcher 原语
m=g(r.sub,p.sub,r.dom) && r.dom==p.dom && r.obj==p.obj && r.act==p.act

PML 采用 . 语法来表示元素(包括主体、客体、动作等)属性,以支持 ABAC。属性本身也是元素,可以继续获取属性的属性。

Casbin 有六个内置 stub 函数,也支持自定义函数。

Model 存储

支持从 .conf 文件和从代码加载 Model。Model 配置至少要包含 [request_definition], [policy_definition], [policy_effect], [matchers] 四个部分,若使用 RBAC 则还需要添加 [role_definition]

model 语法
model 示例

Policy 描述策略规则,Policy 的存储使用 adapter 模式以 Casbin 中间件的形式实现,支持的适配器包括 CSV 文件、常用关系型数据库及其 ORM。

Adapter list
编写 Adapter

配置示例

RBAC

# rbac-model.conf
## 请求格式
[request_definition]
r = sub, obj, act

## 策略格式
[policy_definition]
p = sub, obj, act

## 形成最终决策的规则
[policy_effect]
e = some(where (p.eft == allow))

## 匹配器
## r.sub 可以是用户或角色,是用户时会去检查用户 r.sub 是否有 p.sub 的角色,
## 是角色时会去检查角色 r.sub 是否继承了角色 p.sub
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && regexMatch(r.act, p.act)

## 定义 RBAC 角色继承关系
[role_definition]
g = _, _
# rbac-policy.csv
p, alice, data1, read
p, bob, data2, write
p, data2_admin, data2, read
p, data2_admin, data2, write

# alice 是角色 data2_admin 的一个成员,alice 可以是用户、资源或角色,
# Cabin 只是将其识别为一个字符串
g, alice, data2_admin 

ABAC

# abac-model.conf
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub_rule, obj, act

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

[matchers]
# 使用 eval() 后可将匹配规则写在 policy 里,避免匹配规则随着校验逻辑的增加而变得越来越复杂;
# p.sub_rule 是包含 policy 中需要的属性的结构体
m = eval(p.sub_rule) && r.obj == p.obj && r.act == p.act
# m = r.sub == r.obj.Owner
# abac-policy.csv
p, r.sub.Age > 18, /data1, read
p, r.sub.Age < 60, /data2, write

目前只有请求元素(如 r.subr.obj 等)支持 ABAC,策略元素(如 p.obj 等)不支持。

ACL

# model
[request_definition]
r = sub, obj, act

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


# policy
p, alice, data1, read
p, bob, data2, write

示例程序

RBAC

docker run --name sbmysql -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=yes -e MYSQL_DATABASE=interchain -d mysql:5.7
package main
import (
	"fmt"
	"log"
	"github.com/casbin/casbin"
	gormadapter "github.com/casbin/gorm-adapter"
	_ "github.com/go-sql-driver/mysql"
)

func main() {
	/* 初始化 Gorm 适配器
	 **
	 ** 不指定数据库时会自动创建名为 casbin 的数据库;
	 ** Gorm 硬编码使用名为 casbin_rule 的表,不存在时会自动创建。
	 */
	a, err := gormadapter.NewAdapter("mysql", "root@tcp(127.0.0.1:3306)/interchain?parseTime=true", true)
	/* // 不指定数据库的写法
	   a, _ := gormadapter.NewAdapter("mysql", "root@tcp(127.0.0.1:3306)/") */
	if err != nil {
		log.Fatal("new adapter error:", err)
	}
	// 基于 DB Adapter 创建访问控制执行器
	e, err := casbin.NewEnforcer("rbac-model.conf", a)
	if err != nil {
		log.Fatal("new enforcer error:", err)
	}

	// 添加 Policy
	_, err = e.AddPolicies([][]string{
		[]string{"admin", "data4", "read"},
		[]string{"admin", "data4", "write"},
		[]string{"user", "data4", "read"},
	})
	if err != nil {
		log.Fatal("add named policy error", err)
	}

	// 为用户添加角色
	if _, err := e.AddRoleForUser("alice", "user"); err != nil {
		log.Fatal("add role error", err)
	}
	if _, err := e.AddRoleForUser("bob", "admin"); err != nil {
		log.Fatal("add role error", err)
	}

	// roles4Alice, _ := e.GetRolesForUser("alice")
	// users4Admin, _ := e.GetUsersForRole("admin")
	// fmt.Println(e.GetPolicy(), roles4Alice, users4Admin)

	// 执行鉴权
	ok, err := e.Enforce("alice", "data4", "read")
	if err != nil {
		log.Fatal("permission denied", err)
	}
	fmt.Println("alice can read data4:", ok)
	ok, err = e.Enforce("alice", "data4", "write")
	if err != nil {
		log.Fatal("permission denied", err)
	}
	fmt.Println("alice can write data4:", ok)
	ok, err = e.Enforce("bob", "data4", "write")
	if err != nil {
		log.Fatal("permission denied", err)
	}
	fmt.Println("bob can write data4:", ok)
}

Gorm adapter 中 casbin_rule 表结构:

type CasbinRule struct {
	TablePrefix string `gorm:"-"`           
	PType       string `gorm:"size:100"`    // 值域:r, p, g, e, m
	V0          string `gorm:"size:100"`    
	V1          string `gorm:"size:100"`    
	V2          string `gorm:"size:100"`    
	V3          string `gorm:"size:100"`    // 预留
	V4          string `gorm:"size:100"`    // 预留    
	V5          string `gorm:"size:100"`    // 预留
}
var sectionNameMap = map[string]string{
	"r": "request_definition",
	"p": "policy_definition",
	"g": "role_definition",
	"e": "policy_effect",
	"m": "matchers",
}

ABAC

基本步骤:

  1. 在模型的 Matcher 中指定属性;
  2. 将结构体实例作为参数传入 Enforce() 方法。
/* model definition
*  [request_definition]
*  r = sub, obj, act
*  [policy_definition]
*  p = sub, obj, act
*  [policy_effect]
*  e = some(where (p.eft == allow))
*  [matchers]
*  m = r.sub == r.obj.Owner && r.obj.Name == p.obj
*/
type Resource struct {
	Name  string
	Owner string
}
func main() {
	a, err := gormadapter.NewAdapter("mysql", "root@tcp(127.0.0.1:3306)/interchain?parseTime=true", true)
	if err != nil {
		log.Fatal("new adapter error:", err)
	}
	e, err := casbin.NewEnforcer("abac-model.conf", a)
	if err != nil {
		log.Fatal("new enforcer error:", err)
	}

	_, err = e.AddPolicies([][]string{
		{"sb", "/path1", "GET"},
	})
	if err != nil {
		log.Fatal("add policy err:", err)
	}

	r1 := Resource{
		Name:  "/path1",
		Owner: "sb",
	}
	r2 := Resource{
		Name:  "/path2",
		Owner: "sb",
	}

	ok1, err := e.Enforce("sb", r1, "GET")
	if err != nil {
		log.Fatal("permission denied:", err)
	}
	ok2, err := e.Enforce("sb", r2, "GET")
	if err != nil {
		log.Fatal("permission denied:", err)
	}
	fmt.Println(ok1, ok2)
}

使用 eval()

type Subject struct {
	Age int
}
func main() {
	a, err := gormadapter.NewAdapter("mysql", "root@tcp(127.0.0.1:3306)/interchain?parseTime=true", true)
	if err != nil {
		log.Fatal("new adapter error:", err)
	}
	e, err := casbin.NewEnforcer("abac-model.conf", a)
	if err != nil {
		log.Fatal("new enforcer error:", err)
	}

	_, err = e.AddPolicies([][]string{
		{"r.sub.Age > 18", "/data1", "read"},
		{"r.sub.Age < 60", "/data2", "write"},
	})
	if err != nil {
		log.Fatal("add policy err:", err)
	}

	r1 := Subject{19}
	r2 := Subject{18}

	ok00, err := e.Enforce(r1, "/data1", "read")
	if err != nil {
		log.Fatal("permission denied:", err)
	}
	ok01, err := e.Enforce(r1, "/data2", "write")
	if err != nil {
		log.Fatal("permission denied:", err)
	}
	fmt.Println(ok00, ok01)
	ok10, err := e.Enforce(r2, "/data1", "read")
	if err != nil {
		log.Fatal("permission denied:", err)
	}
	ok11, err := e.Enforce(r2, "/data2", "write")
	if err != nil {
		log.Fatal("permission denied:", err)
	}
	fmt.Println(ok10, ok11)
}

References