Casbin学习笔记
Casbin是一个权限控制的开发库,它的特性包括:
- 支持多种编程语言,包括Go、Java、Node.js、PHP、Python等
- 支持ACL、RBAC、ABAC等多种访问控制模型
- 支持以典型的 {subject, object, action}形式,或者自定义的形式来定义策略,allow/deny授权均支持
- 支持处理访问控制模型,及其策略的存取(和存储后端交互)
- 支持管理角色-用户映射,以及角色-角色映射(RBAC中的角色层次)
- 支持内置超级用户,例如root/administrator,不需要明确授权就可以作任何事情
- 很多内置的操作符,来支持规则匹配
Casbin不负责:
- 身份验证
- 管理角色、用户的详细信息。但是它维护角色和用户之间的关系
Casbin支持的访问控制模型列表:
模型 | 说明 | ||||
ACL |
为对象添加一个访问许可(Permissions)列表,每个条目指定主体(Subject,例如用户/进程)可以对对象执行何种操作(Action) 模型示例:
策略示例:
|
||||
ACL with superuser |
ACL模型,外加指定特权用户 模型示例:
|
||||
ACL without users |
用于没有身份验证机制/用户登陆的系统 模型示例:
策略示例:
|
||||
ACL without resources |
针对资源类别,而非资源实例进行访问控制。例如write-article, read-log,不去控制某个article、log的访问权限 模型示例:
|
||||
RBAC |
基于角色的访问控制 模型示例:
策略示例:
|
||||
RBAC with resource roles |
用户、资源都可以具有角色(roles,或groups) 模型示例:
策略示例:
|
||||
RBAC with domains/tenants |
对于不同的域(domain)/租户(tenant),用户可以具有不同的角色 模型示例:
策略示例:
|
||||
ABAC |
基于属性的角色控制,也叫(Policy-Based Access Control)或CBAC(Claims-Based Access Control) 不同于常见的将用户通过某种方式关联到权限的方式,ABAC通过动态计算一个或一组属性是否满足某种条件来进行授权判断(可以编写简单的逻辑)。属性通常来说分为四类:
理论上能够实现非常灵活的权限控制,几乎能满足所有类型的需求 举例来说,规则:允许所有班主任在上课时间自由进出校门,班主任是用户的角色熟悉,上课时间是环境属性,进出是操作属性,校门则是对象属性 ABAC的缺点是过于复杂,因此K8S在1.8版本引入RBAC 模型示例:
|
||||
RESTful |
支持以HTTP动词,以及/res/*, /res/:id这样的路径来描述操作和资源 模型示例:
策略示例:
|
||||
Deny-override |
支持allow、deny,deny可以覆盖allow 模型示例:
策略示例:
|
||||
Priority |
策略规则可以支持优先级 模型示例:
策略示例:
|
在线编辑器:
在Casbin中,访问控制模型被抽象到基于PERM元模型的CONF文件中。
PERM元模型包含四大要素: https://casbin.org/editor/ 可以用于编写模型、策略。
- Request:关于访问请求的信息,简单的请求是主体、操作、资源构成的元组: r = {sub, action, resource}
- Policy:构成一个访问规则,例如管理员可以读取用户信息,同样由三元组构成: p = {sub, action, resource}
- Matchers:描述请求和策略如何匹配。最简单的方式是相等判断: m = r.sub == p.sub && r.action == p.action && r.resource == p.resource 。对于一个请求来说,所有使用Matcher的Policy都会产生一个值,记为 p.eft
- Effect:联合/化简(reducing)匹配某个请求的策略,并得到最终的结果(允许或拒绝访问),例如: e = some(p.eft == allow)
下图展示了基于PERM的模型对一个请求进行授权的过程:
1 2 3 4 5 6 7 8 9 10 11 |
[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 |
对于该模型定义,定义授权规则的策略文件(模型是定义了公式,策略则是将具体的主体、操作、资源带入公式计算)可以如下:
1 2 |
p, alice, data1, read p, bob, data2, write |
如果我们想允许admin执行任何操作,需要修改matcher:
1 2 3 |
[matchers] m = r.sub == admin || (r.sub == p.sub && r.obj == p.obj && r.act == p.act) # 如果太长,可以 \ 结尾换行 |
要将上述模型从ACL改为ABAC风格,我们可以在matcher中访问资源的属性:
1 2 3 4 5 6 |
[matchers] m = r.obj.owner == r.sub && (r.sub == p.sub && r.obj == p.obj && r.act == p.act) # 如果对象所有者是请求主体 m = r.obj == p.obj && r.act == p.act || r.obj in ('data2', 'data3') # Golang版本的Casbin还支持 in 操作符 |
可以看到,PERM元模型足够灵活,可以让我们以简单模型开始,并按需切换到复杂的授权模型。
首先,从模型文件、策略文件创建Enforcer:
1 |
e, _ := casbin.NewEnforcer("path/to/model.conf", "path/to/policy.csv") |
判断一个Request是否可以被允许:
1 2 3 4 5 6 7 8 9 10 |
// 请求信息 sub := "alice" obj := "data1" act := "read" if res := e.Enforce(sub, obj, act); res { // 允许访问 } else { // 拒绝访问 } |
Casbin提供了两套管理策略(Permission)的API:
- Management API:底层API,完全支持Casbin策略管理
- RBAC API:更加友好的,编写RBAC模型的API,是Managlement API的子集
1 2 3 4 5 |
// 获取所有主体 allSubjects := e.GetAllSubjects() // 获取指定的命名的策略中的所有主体 allNamedSubjects := e.GetAllNamedSubjects("p") |
1 2 3 4 |
// 获取所有资源 allObjects := e.GetAllObjects() // 获取指定的命名的策略中所有的资源 allNamedObjects := e.GetAllNamedObjects("p") |
1 2 3 |
// 获取所有操作 allActions := e.GetAllActions() allNamedActions := e.GetAllNamedActions("p") |
1 2 3 |
// 获取所有角色 allRoles = e.GetAllRoles() allNamedRoles := e.GetAllNamedRoles("g") |
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
// 获取所有策略(授权规则) policy = e.GetPolicy() // 获取过滤后的策略,可以根据字段进行过滤 字段索引 字段值 filteredPolicy := e.GetFilteredPolicy(0, "alice") // 获取命名的策略中的所有授权规则 namedPolicy := e.GetNamedPolicy("p") filteredNamedPolicy = e.GetFilteredNamedPolicy("p", 0, "bob") // 获取所有角色继承规则 groupingPolicy := e.GetGroupingPolicy() filteredGroupingPolicy := e.GetFilteredGroupingPolicy(0, "alice") namedGroupingPolicy := e.GetNamedGroupingPolicy("g") namedGroupingPolicy := e.GetFilteredNamedGroupingPolicy("g", 0, "alice") // 判断指定的授权规则是否存在 hasPolicy := e.HasPolicy("data2_admin", "data2", "read") hasNamedPolicy := e.HasNamedPolicy("p", "data2_admin", "data2", "read") // 添加一个授权规则 added := e.AddPolicy("eve", "data3", "read") // 添加若干 rules := [][] string { []string {"jack", "data4", "read"}, []string {"katy", "data4", "write"}, []string {"leyo", "data4", "read"}, []string {"ham", "data4", "write"}, } areRulesAdded := e.AddPolicies(rules) // 添加到命名策略中 added := e.AddNamedPolicy("p", "eve", "data3", "read") areRulesAdded := e.AddNamedPolicies("p", rules) // 删除一个授权规则 removed := e.RemovePolicy("alice", "data1", "read") // 删除多个授权规则 areRulesRemoved := e.RemovePolicies(rules) // 字段索引 字段值... removed := e.RemoveFilteredPolicy(0, "alice", "data1", "read") removed := e.RemoveNamedPolicy("p", "alice", "data1", "read") areRulesRemoved := e.RemoveNamedPolicies("p", rules) removed := e.RemoveFilteredNamedPolicy("p", 0, "alice", "data1", "read") // 添加一个角色继承规则,如果规则已经存在,返回false added := e.AddGroupingPolicy("group1", "data2_admin") areRulesAdded := e.AddGroupingPolicies(rules) added := e.AddNamedGroupingPolicy("g", "group1", "data2_admin") areRulesAdded := e.AddNamedGroupingPolicies("g", rules) // 删除角色继承规则 removed := e.RemoveGroupingPolicy("alice", "data2_admin") |
你添加一个Go函数,并在编写规则时,使用该函数进行Match:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
func CustomFunction(key1 string, key2 string) bool { if key1 == "/alice_data2/myid/using/res_id" && key2 == "/alice_data/:resource" { return true } else if key1 == "/alice_data2/myid/using/res_id" && key2 == "/alice_data2/:id/using/:resId" { return true } else { return false } } func CustomFunctionWrapper(args ...interface{}) (interface{}, error) { key1 := args[0].(string) key2 := args[1].(string) return bool(CustomFunction(key1, key2)), nil } e.AddFunction("keyMatchCustom", CustomFunctionWrapper) |
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 |
// 获取具有角色的用户 res := e.GetRolesForUser("alice") // 判断用户是否具有角色 res := e.HasRoleForUser("alice", "data1_admin") // 为用户添加角色 e.AddRoleForUser("alice", "data2_admin") // 为用户删除角色 e.DeleteRoleForUser("alice", "data1_admin") // 删除用户的所有角色 e.DeleteRolesForUser("alice") // 删除一个角色 e.DeleteRole("data2_admin") // 获取用户具有的隐式特权,不同于GetRolesForUser,该函数会同时 // 返回所有间接得到的角色(因为角色之间可以有继承关系) // // 例如: // g, alice, role:admin alice属于admin // g, role:admin, role:user admin属于user // 该方法会返回 ["role:admin", "role:user"] e.GetImplicitRolesForUser("alice") // 获取用户的特权,不同于GetPermissionsForUser,该函数会同时 // 返回来自用户所属角色的特权 e.GetImplicitPermissionsForUser("alice") |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// 删除一个特权(操作) e.DeletePermission("read") // 为用户或角色添加一个特权 e.AddPermissionForUser("bob", "read") // 为用户删除一个特权 e.DeletePermissionForUser("bob", "read") // 删除用户的全部特权 e.DeletePermissionsForUser("bob") // 查询用户的特权 e.GetPermissionsForUser("bob") // 查看用户是否具有指定的特权 e.HasPermissionForUser("alice", []string{"read"}) |
在Casbin中,策略存储以适配器的形式实现。你可以使用适配器的 LoadPolicy()来加载策略规则,使用 SavePolicy()来保存规则。
所有可用的适配器:https://casbin.org/docs/en/adapters#supported-adapters
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 |
type Adapter interface { // 加载所有策略到model中 LoadPolicy(model model.Model) error // 保存所有策略 SavePolicy(model model.Model) error // 添加一个策略到后端存储,作为自动保存特性的一部分 AddPolicy(sec string, ptype string, rule []string) error // 从后端存储删除一个策略,作为自动保存特性的一部分 RemovePolicy(sec string, ptype string, rule []string) error // 从后端存储删除匹配的策略规则,作为自动保存特性的一部分 RemovFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error } // 建模访问控制模型 type Model map[string]AssertionMap type AssertionMap map[string]*Assertion type Assertion struct { Key string Value string Tokens []string Policy [][]string RM rbac.RoleManager } var sectionNameMap = map[string]string{ "r": "request_definition", "p": "policy_definition", "g": "role_definition", "e": "policy_effect", "m": "matchers", } |
1 2 3 4 5 6 7 8 |
import ( "github.com/casbin/casbin" "github.com/casbin/casbin/file-adapter" ) a := fileadapter.NewAdapter("examples/basic_policy.csv") // 从适配器,而非文件加载策略 e := casbin.NewEnforcer("examples/basic_model.conf", a) |
Github地址:https://github.com/sebastianliu/etcd-adapter
1 2 3 4 5 6 7 8 9 |
import ( "github.com/sebastianliu/etcd-adapter" "github.com/casbin/casbin" ) a := etcdadapter.NewAdapter([]string{"http://127.0.0.1:2379"}, "casbin_policy_test") e := casbin.NewEnforcer("rbac_model.conf", a) e.LoadPolicy() |
角色管理器用于管理RBAC角色层次(用户-角色、角色-角色映射)。角色管理器可以从Casbin策略文件,或者第三方数据源提取角色数据。除了默认角色管理器,都是out-of-tree的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package rbac type RoleManager interface { // 清空所有数据,重置到初始状态 Clear() error // 在两个角色之间添加继承关系。domain是角色的前缀 AddLink(name1 string, name2 string, domain ...string) error // 解除两个角色之间的继承关系 DeleteLink(name1 string, name2 string, domain ...string) error // 判断两个角色之间是否有继承关系 HasLink(name1 string, name2 string, domain ...string) (bool, error) // 获取用户继承的角色列表 GetRoles(name string, domain ...string) ([]string, error) // 获取继承某角色的用户列表 GetUsers(name string, domain ...string) ([]string, error) PrintRoles() error } |
腾讯的TKE项目使用Casbin实现了权限控制。相关代码位于auth-api和auth-controller中。
可以先将字符串解析为Model对象,然后再创建Enforcer:
1 2 3 |
m, err := model.NewModelFromString(authapi.DefaultRuleModel) // SyncedEnforcer保持和文件或数据库同步 enforcer, err = casbin.NewSyncedEnforcer(m) |
1 2 3 4 |
import casbinlog "github.com/casbin/casbin/v2/log" casbinlog.SetLogger(&casbinlogger.WrapLogger{}) enforcer.EnableLog(true) |
在创建auth-controller的ControllerContext时,会初始化适配器:
1 2 3 4 5 6 7 8 9 10 |
adpt := util2.NewAdapter(client.AuthV1().Rules(), sharedInformers.Auth().V1().Rules().Lister()) func NewAdapter(ruleClient authv1client.RuleInterface, ruleLister authv1lister.RuleLister) *RestAdapter { adapter := &RestAdapter{ ruleClient: ruleClient, lister: ruleLister, } return adapter } |
可以猜测到,此适配器使用API Server的Lister接口,从Etcd中读取策略信息。
该适配器的实现:
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
type RestAdapter struct { ruleClient authv1client.RuleInterface lister authv1lister.RuleLister } func (a *RestAdapter) LoadPolicy(model model.Model) error { // 加载所有Rule资源 rules, err := a.lister.List(labels.Everything()) if err != nil { return fmt.Errorf("list all rules failed: %v", err) } for _, rule := range rules { a.loadPolicy(rule, model) } return nil } // 将自定义资源转换为文本,然后从文本解析出策略 // V0 - V6是策略规则可能的参数,一般没有这么多参数 func (a *RestAdapter) loadPolicy(rule *authv1.Rule, model model.Model) { casRule := rule.Spec lineText := casRule.PType if casRule.V0 != "" { lineText += ", " + casRule.V0 } if casRule.V1 != "" { lineText += ", " + casRule.V1 } if casRule.V2 != "" { lineText += ", " + casRule.V2 } if casRule.V3 != "" { lineText += ", " + casRule.V3 } if casRule.V4 != "" { lineText += ", " + casRule.V4 } if casRule.V5 != "" { lineText += ", " + casRule.V5 } if casRule.V6 != "" { lineText += ", " + casRule.V6 } persist.LoadPolicyLine(lineText, model) } // 将内存中的策略保存到Etcd,保存为若干Rule资源 func (a *RestAdapter) SavePolicy(model model.Model) error { // 删除老数据 err := a.destroy(context.Background()) if err != nil { return err } var rules []authv1.Rule for ptype, ast := range model["p"] { for _, line := range ast.Policy { rules = append(rules, ConvertRule(ptype, line)) } } for ptype, ast := range model["g"] { for _, line := range ast.Policy { rules = append(rules, ConvertRule(ptype, line)) } } return a.savePolicy(context.Background(), rules) } func (a *RestAdapter) savePolicy(ctx context.Context, rules []authv1.Rule) error { for _, rule := range rules { if _, err := a.ruleClient.Create(ctx, &rule, metav1.CreateOptions{}); err != nil && !apierrors.IsAlreadyExists(err) { return err } } return nil } |
TKE使用的默认模型是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
[request_definition] r = sub, dom, obj, act [policy_definition] p = sub, dom, obj, act, eft [role_definition] # 请求主体, 策略主体, 租户 g = _, _, _ [policy_effect] e = some(where (p.eft == allow)) && !some(where (p.eft == deny)) [matchers] m = g(r.sub, p.sub, r.dom) && keyMatchCustom(r.obj, p.obj) && keyMatchCustom(r.act, p.act) |
其中keyMatchCustom是自定义的函数:
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 |
enforcer.AddFunction("keyMatchCustom", CustomFunctionWrapper) func CustomFunctionWrapper(args ...interface{}) (interface{}, error) { key1 := args[0].(string) key2 := args[1].(string) return keyMatchCustomFunction(key1, key2), nil } // 目标匹配规则 // /project:123/cluster:456 匹配 /project:*/cluster:456 // registry:123/* 匹配 registry:123/456 func keyMatchCustomFunction(key1 string, key2 string) bool { key1 = strings.ToLower(key1) key2 = strings.ToLower(key2) key2 = strings.Replace(key2, "*", ".*", -1) re := regexp.MustCompile(`(.*):[^/]+(.*)`) i := 2 for { if !strings.Contains(key2, "/:") { break } key2 = re.ReplaceAllString(key2, "$1[^/]+$2") i = i + 1 } return casbinutil.RegexMatch(key1, "^"+key2+"$") } |
TKE使用了自定义的角色管理器:
1 2 3 |
rm := domainrolemanager.NewRoleManager(10) enforcer.SetRoleManager(rm) enforcer.StartAutoLoadPolicy(cfg.CasbinReloadInterval) |
该角色管理器来自github.com/dovics/domain-role-manager
Leave a Reply