Model Syntax
每个模型 CONF 文件都必须定义这四个部分:[request_definition]、[policy_definition]、[policy_effect] 和 [matchers]。
- 使用 RBAC 时,加上
[role_definition]部分。 - 使用 带约束的 RBAC(例如职责分离)时,可以加上
[constraint_definition]。 - 以
#开头的行是注释,这一行的其余部分会被忽略。
请求定义
[request_definition] 部分定义传给 e.Enforce(...) 的参数。
[request_definition]
r = sub, obj, act
这里 sub、obj 和 act 是标准的三元组:主体、对象和操作。可以修改格式,例如没有资源时用 sub, act,涉及两个主体时用 sub, sub2, obj, act。
策略定义
[policy_definition] 部分描述策略规则的结构。例如:
[policy_definition]
p = sub, obj, act
p2 = sub, act
对于这样的策略文件:
p, alice, data1, read
p2, bob, write-all-objects
策略文件中的每一行是一条规则。第一个字段是策略类型(p、p2 等),必须与某个策略定义对应。上面的例子为匹配器产生以下绑定:
(alice, data1, read) -> (p.sub, p.obj, p.act)
(bob, write-all-objects) -> (p2.sub, p2.act)
策略规则中的元素始终被当作字符串处理。相关讨论见 casbin/casbin#113。
策略效果
[policy_effect] 部分定义多条策略同时匹配时(例如一条允许、另一条拒绝)如何合并结果。
[policy_effect]
e = some(where (p.eft == allow))
这就是 允许优先:只要有一条匹配的策略效果为 allow,结果就是允许。 p.eft 字段是策略效果(allow 或 deny)。它是可选的,省略时默认为 allow。
另一种策略效果:
[policy_effect]
e = !some(where (p.eft == deny))
这实现了 拒绝优先:只有没有匹配的策略效果为 deny 时,结果才是允许。可以用逻辑运算符组合表达式:
[policy_effect]
e = some(where (p.eft == allow)) && !some(where (p.eft == deny))
这个表达式要求至少有一条 allow,并且没有 deny。 Casbin 同时支持允许和拒绝;两者同时匹配时,拒绝优先。
策略效果必须是下面的内置效果之一。当前实现不支持自定义的效果表达式。
可用的内置策略效果:
| 策略效果 | 含义 | 例子 |
|---|---|---|
| some(where (p.eft == allow)) | allow-override | ACL, RBAC, etc. |
| !some(where (p.eft == deny)) | deny-override | Deny-override |
| some(where (p.eft == allow)) && !some(where (p.eft == deny)) | allow-and-deny | Allow-and-deny |
| priority(p.eft) || deny | priority | Priority |
| subjectPriority(p.eft) | 基于角色的优先级 | 主题-优先级 |
约束定义
可选的 [constraint_definition] 部分为 RBAC 定义不变量(例如职责分离)。约束在角色分配变化时检查。使用约束必须有 [role_definition]。
[constraint_definition]
c = sod("finance_requester", "finance_approver")
c2 = sodMax(["payroll_view", "payroll_edit", "payroll_approve"], 1)
c3 = roleMax("superadmin", 2)
c4 = rolePre("db_admin", "security_trained")
约束类型
职责分离(sod):一个用户不能同时拥有两个角色。如果 Alice 拥有 finance_requester,就不能再给她分配 finance_approver。
c = sod("finance_requester", "finance_approver")
职责分离上限(sodMax):限制用户最多能拥有一组角色中的几个。对薪资相关角色使用 sodMax(..., 1) 时,用户最多只能拥有查看、编辑、审批中的一个。
c2 = sodMax(["payroll_view", "payroll_edit", "payroll_approve"], 1)
角色基数(roleMax):限制一个角色最多能有多少用户。例如最多两个用户可以拥有 superadmin。
c3 = roleMax("superadmin", 2)
前置角色(rolePre):拥有某个角色前必须先拥有另一个角色。例如用户必须先拥有 security_trained,才能被分配 db_admin。
c4 = rolePre("db_admin", "security_trained")
约束如何生效
约束在分组策略变化时执行(例如 AddGroupingPolicy()、RemoveGroupingPolicy())。如果某个变更会违反约束,操作会报错失败,策略不会被更新。
加载模型时,所有约束都会针对当前策略检查一遍。无效的约束(语法错误、缺少 RBAC 配置,或当前数据已经违反约束)会导致加载失败,并给出明确的错误。
匹配器
[matchers] 部分定义请求如何与策略规则比较。
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
这个匹配器要求请求的主体、对象和操作与策略字段完全一致。
匹配器支持算术运算符(+、-、*、/)和逻辑运算符(&&、||、!)。
匹配器中表达式的顺序
匹配器中表达式的顺序可能会显著影响性能。示例:
const rbac_models = `
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
`
func TestManyRoles(t *testing.T) {
m, _ := model.NewModelFromString(rbac_models)
e, _ := NewEnforcer(m, false)
roles := []string{"admin", "manager", "developer", "tester"}
// 2500 projects
for nbPrj := 1; nbPrj < 2500; nbPrj++ {
// 4 objects and 1 role per object (so 4 roles)
for _, role := range roles {
roleDB := fmt.Sprintf("%s_project:%d", role, nbPrj)
objectDB := fmt.Sprintf("/projects/%d", nbPrj)
e.AddPolicy(roleDB, objectDB, "GET")
}
jasmineRole := fmt.Sprintf("%s_project:%d", roles[1], nbPrj)
e.AddGroupingPolicy("jasmine", jasmineRole)
}
e.AddGroupingPolicy("abu", "manager_project:1")
e.AddGroupingPolicy("abu", "manager_project:2499")
// With same number of policies
// User 'abu' has only two roles
// User 'jasmine' has many roles (1 role per policy, here 2500 roles)
request := func(subject, object, action string) {
t0 := time.Now()
resp, _ := e.Enforce(subject, object, action)
tElapse := time.Since(t0)
t.Logf("RESPONSE %-10s %s\t %s : %5v IN: %+v", subject, object, action, resp, tElapse)
if tElapse > time.Millisecond*100 {
t.Errorf("More than 100 milliseconds for %s %s %s : %+v", subject, object, action, tElapse)
}
}
request("abu", "/projects/1", "GET") // really fast because only 2 roles in all policies and at the beginning of the casbin_rule table
request("abu", "/projects/2499", "GET") // fast because only 2 roles in all policies
request("jasmine", "/projects/1", "GET") // really fast at the beginning of the casbin_rule table
request("jasmine", "/projects/2499", "GET") // slow and fails the only 1st time <<<< pb here
request("jasmine", "/projects/2499", "GET") // fast maybe due to internal cache mechanism
// same issue with non-existing roles
// request("jasmine", "/projects/999999", "GET") // slow fails the only 1st time <<<< pb here
// request("jasmine", "/projects/999999", "GET") // fast maybe due to internal cache mechanism
}
判定耗时可能达到 6 秒。
go test -run ^TestManyRoles$ github.com/casbin/casbin/v3 -v
=== RUN TestManyRoles
rbac_api_test.go:598: RESPONSE abu /projects/1 GET : true IN: 438.379µs
rbac_api_test.go:598: RESPONSE abu /projects/2499 GET : true IN: 39.005173ms
rbac_api_test.go:598: RESPONSE jasmine /projects/1 GET : true IN: 1.774319ms
rbac_api_test.go:598: RESPONSE jasmine /projects/2499 GET : true IN: 6.164071648s
rbac_api_test.go:600: More than 100 milliseconds for jasmine /projects/2499 GET : 6.164071648s
rbac_api_test.go:598: RESPONSE jasmine /projects/2499 GET : true IN: 12.164122ms
--- FAIL: TestManyRoles (6.24s)
FAIL
FAIL github.com/casbin/casbin/v3 6.244s
FAIL
把开销小的条件放在前面,开销大的(例如角色查询)放在后面。把上面例子中的匹配器调整为:
[matchers]
m = r.obj == p.obj && g(r.sub, p.sub) && r.act == p.act
go test -run ^TestManyRoles$ github.com/casbin/casbin/v3 -v
=== RUN TestManyRoles
rbac_api_test.go:599: RESPONSE abu /projects/1 GET : true IN: 786.635µs
rbac_api_test.go:599: RESPONSE abu /projects/2499 GET : true IN: 4.933064ms
rbac_api_test.go:599: RESPONSE jasmine /projects/1 GET : true IN: 2.908534ms
rbac_api_test.go:599: RESPONSE jasmine /projects/2499 GET : true IN: 7.292963ms
rbac_api_test.go:599: RESPONSE jasmine /projects/2499 GET : true IN: 6.168307ms
--- PASS: TestManyRoles (0.05s)
PASS
ok github.com/casbin/casbin/v3 0.053s
多组定义
可以用 r2、p2、e2、m2 这样的后缀定义多组请求、策略、效果或匹配器。它们按后缀配对:r2 通过 m2 与 p2 匹配,并用 e2 合并结果。
要使用非默认的一组,把 EnforceContext 作为 enforce 的第一个参数传入。结构:
EnforceContext{"r2","p2","e2","m2"}
type EnforceContext struct {
RType string
PType string
EType string
MType string
}
const enforceContext = new EnforceContext('r2', 'p2', 'e2', 'm2');
class EnforceContext {
constructor(rType, pType, eType, mType) {
this.pType = pType;
this.eType = eType;
this.mType = mType;
this.rType = rType;
}
}
EnforceContext enforceContext = new EnforceContext("2");
public class EnforceContext {
private String pType;
private String eType;
private String mType;
private String rType;
public EnforceContext(String suffix) {
this.pType = "p" + suffix;
this.eType = "e" + suffix;
this.mType = "m" + suffix;
this.rType = "r" + suffix;
}
}
// Pass in a suffix as a parameter to NewEnforceContext, such as 2 or 3, and it will create r2, p2, etc.
enforceContext := NewEnforceContext("2")
// You can also specify a certain type individually
enforceContext.EType = "e"
// Don't pass in EnforceContext; the default is r, p, e, m
e.Enforce("alice", "data2", "read") // true
// Pass in EnforceContext
e.Enforce(enforceContext, struct{ Age int }{Age: 70}, "/data1", "read") //false
e.Enforce(enforceContext, struct{ Age int }{Age: 30}, "/data1", "read") //true
// Pass in a suffix as a parameter to NewEnforceContext, such as 2 or 3, and it will create r2, p2, etc.
const enforceContext = new NewEnforceContext('2');
// You can also specify a certain type individually
enforceContext.eType = "e"
// Don't pass in EnforceContext; the default is r, p, e, m
e.Enforce("alice", "data2", "read") // true
// Pass in EnforceContext
e.Enforce(enforceContext, {Age: 70}, "/data1", "read") //false
e.Enforce(enforceContext, {Age: 30}, "/data1", "read") //true
// Pass in a suffix as a parameter to NewEnforceContext, such as 2 or 3, and it will create r2, p2, etc.
EnforceContext enforceContext = new EnforceContext("2");
// You can also specify a certain type individually
enforceContext.seteType("e");
// Don't pass in EnforceContext; the default is r, p, e, m
e.enforce("alice", "data2", "read"); // true
// Pass in EnforceContext
// TestEvalRule is located in https://github.com/apache/casbin-jcasbin/blob/master/src/test/java/org/casbin/jcasbin/main/AbacAPIUnitTest.java#L56
e.enforce(enforceContext, new AbacAPIUnitTest.TestEvalRule("alice", 70), "/data1", "read"); // false
e.enforce(enforceContext, new AbacAPIUnitTest.TestEvalRule("alice", 30), "/data1", "read"); // true
特殊语法:in 运算符
in 运算符检查右侧数组是否包含左侧的值(用 == 比较,不做类型转换)。左侧可以是任意值;右侧必须是同类型的数组。在 Go 中,数组必须是 []interface{}。
参考示例:rbac_model_matcher_using_in_op、keyget2_model 和 keyget_model。
示例:
[request_definition]
r = sub, obj
...
[matchers]
m = r.sub.Name in (r.obj.Admins)
e.Enforce(Sub{Name: "alice"}, Obj{Name: "a book", Admins: []interface{}{"alice", "bob"}})
表达式求值器
匹配器由各语言对应的表达式引擎求值。 Casbin 借助它们提供统一的 PERM 语法。有些引擎支持文档以外的额外功能,这些功能在其他语言中可能不可用。如果需要跨语言兼容,只使用文档中列出的语法。
各实现使用的表达式求值器:
| 实现 | 语言 | 表达式求值器 |
|---|---|---|
| Casbin | Golang | https://github.com/apache/casbin-govaluate |
| jCasbin | Java | https://github.com/killme2008/aviatorscript |
| Node-Casbin | Node.js | https://github.com/donmccurdy/expression-eval |
| PHP-Casbin | PHP | https://github.com/symfony/expression-language |
| PyCasbin | Python | https://github.com/danthedeckie/simpleeval |
| Casbin.NET | C# | https://github.com/davideicardi/DynamicExpresso |
| Casbin4D | Delphi | https://github.com/casbin4d/Casbin4D/tree/master/SourceCode/Common/Third%20Party/TExpressionParser |
| casbin-rs | Rust | https://github.com/jonathandturner/rhai |
| casbin-cpp | C++ | https://github.com/ArashPartow/exprtk |
如果判定很慢,瓶颈往往在表达式求值器。见 基准测试 页面,也可以考虑向 Casbin 或求值器项目报告问题。