RBAC 和 ABAC 的理解
mingzaily / 2025-04-28
定义
RBAC
Role-Based Access Control - 基于角色的访问控制
- 核心思想: 权限不是直接授予用户,而是授予角色。用户通过被分配一个或多个角色来间接获得这些权限。
- 构成:
- 人 (User/Subject): 操作的主体。
- 角色 (Role): 一组权限的集合,通常对应用户的职责或工作职能(如“管理员”、“编辑”、“访客”)。
- 权限 (Permission/Operation): 对某个资源执行某种操作的许可(如“读取文件”、“创建用户”、“发布文章”)。
- 逻辑: 查看 -> 这个人有没有分配某个角色? -> 这个角色有没有被授予某个权限? 如果都有,则允许操作。
- 特点: 相对静态,易于管理,适用于组织结构和职责分明的场景。管理权限变成管理角色,管理用户变成给用户分配角色。
ABAC
Attribute-Based Access Control - 基于属性的访问控制
- 核心思想: 访问决策是基于与主体(用户)、客体(资源)、操作以及环境条件相关的属性,通过策略来动态计算的。
- 构成:
- 属性 (Attributes): 这是 ABAC 的关键。属性是描述实体的特征或特性。
- 主体属性: 如用户的部门、职位、安全许可级别、年龄、所属项目组等。
- 客体/资源属性: 如文件的敏感度标签、创建者、所属部门、数据类型等。
- 操作属性: 如“读取”、“写入”、“删除”、“审批”等。
- 环境属性: 如访问时间、访问地点(IP 地址)、设备类型、当前系统风险级别等。
- 策略 (Policies): 定义了基于属性组合的访问规则。例如:“允许‘财务部’的‘经理’(主体属性)在‘工作时间’(环境属性)内‘读取’(操作)‘本部门’(资源属性)的‘财务报告’(资源属性)”。
- 属性 (Attributes): 这是 ABAC 的关键。属性是描述实体的特征或特性。
- 逻辑: 收集 -> 主体、客体、操作、环境的属性 -> 查找适用的策略 -> 评估策略是否满足,如果满足,则允许操作。
- 特点: 非常灵活、动态、细粒度。可以实现非常复杂的访问控制逻辑,适应不断变化的需求和环境。但策略管理可能更复杂。
ABAC 是不是指权限的属性?
- 不完全是。ABAC 的核心是基于属性做决策,而不是给“权限”本身附加属性。它是用策略来描述“在什么属性条件下,主体可以对客体执行什么操作”。属性是策略评估的输入,而权限(允许/拒绝访问)是策略评估的输出。
RBAC 和 ABAC 结合
RBAC 和 ABAC 可以很好地结合,形成更强大、更灵活的访问控制模型。通常结合方式如下:
RBAC 作为基础/粗粒度控制: 首先,通过 RBAC 确定用户是否具备执行某类操作的基本资格。用户的角色决定了他“可能”拥有的权限集。
- 检查:这个人有没有这个角色? -> 这个角色有没有(基础的)这个权限?
ABAC 作为补充/细粒度/动态控制: 在 RBAC 检查通过后(即用户具备基础权限),再通过 ABAC 策略进行进一步的、基于上下文的判断。
- 检查:在当前的主体属性、资源属性、操作属性和环境属性下,是否有 ABAC 策略允许这个具体的操作?
结合后的检查逻辑:
查看这个人有没有这个角色,并且这个角色有没有这个权限 (RBAC),同时 (AND) 再看下这个人/资源/环境等属性是否符合允许该操作的 ABAC 策略?
例子:
- 需求: 只有文档的作者或同部门的经理才能编辑“草稿”状态的文档。
- RBAC:
- 角色:“编辑者”
- 权限:“编辑文档”
- 用户 A 被分配了“编辑者”角色。RBAC 检查通过(他有编辑文档的基础权限)。
- ABAC 策略:
- 规则 1:
IF (操作 == '编辑' AND 资源.状态 == '草稿' AND 主体.ID == 资源.作者ID) THEN 允许
- 规则 2:
IF (操作 == '编辑' AND 资源.状态 == '草稿' AND 主体.职位 == '经理' AND 主体.部门 == 资源.所属部门) THEN 允许
- 规则 1:
- 组合检查:
- 用户 A (编辑者) 尝试编辑文档 X (状态: 草稿, 作者: 用户 B, 部门: 销售部)。
- RBAC 通过。
- ABAC 检查:
- 规则 1 不满足 (A 不是作者 B)。
- 假设 A 不是经理,或者 A 是经理但不在销售部,规则 2 也不满足。
- 最终结果:拒绝。
这种结合方式利用了 RBAC 的管理简便性和 ABAC 的灵活性、动态性。
伪代码
我们使用 golang 和 casbin 来演示
1. Model (model.conf
)
这个模型定义了请求的结构、策略的结构、角色定义、策略效果以及最重要的——匹配器(matcher),匹配器中包含了 RBAC 和 ABAC 的组合逻辑。
[request_definition]
r = sub, obj, act # 请求:主体(Subject), 客体(Object), 操作(Action)
[policy_definition]
p = sub, obj_type, act # 策略:主体(这里用作角色Role), 对象类型, 操作
[role_definition]
g = _, _ # 角色继承/用户角色分配:用户, 角色
[policy_effect]
e = some(where (p.eft == allow)) # 效果:只要有一条策略允许,就允许
[matchers]
m = g(r.sub.ID, p.sub) && \ # RBAC检查:请求的用户(r.sub.ID) 是否拥有策略中定义的角色(p.sub)?
r.obj.Type == p.obj && \ # 基础检查:请求的对象类型(r.obj.Type) 是否匹配策略中的对象类型(p.obj)?
r.act == p.act && \ # 基础检查:请求的操作(r.act) 是否匹配策略中的操作(p.act)?
r.act == "edit" && \ # ABAC条件:操作必须是 "edit"?
r.obj.Status == "draft" && \ # ABAC条件:对象的状态必须是 "draft"?
( \ # ABAC条件:满足以下任一条件
r.sub.ID == r.obj.AuthorID || \ # 条件1:请求的用户(r.sub.ID) 是对象的作者(r.obj.AuthorID)?
(r.sub.Title == "manager" && r.sub.Dept == r.obj.Dept) \ # 条件2:请求的用户头衔是 "manager" 并且 用户的部门(r.sub.Dept) 与对象的部门(r.obj.Dept) 相同?
)
关键点解释:
[request_definition]
: 我们定义请求的主体sub
和客体obj
。在 Go 代码中,我们会传入包含属性的结构体。[policy_definition]
:p
定义了基础的 RBAC 规则,即哪个角色(p.sub
) 能对哪种类型的对象(p.obj_type
) 执行什么操作(p.act
)。[role_definition]
:g
定义了用户和角色的关系。[matchers]
:g(r.sub.ID, p.sub)
: 这是 RBAC 检查,看请求的用户(通过其 IDr.sub.ID
)是否被赋予了策略p
中指定的角色 (p.sub
)。r.obj.Type == p.obj && r.act == p.act
: 确保请求的对象类型和操作与基础策略匹配。r.act == "edit" && r.obj.Status == "draft"
: 这是 ABAC 的第一层过滤,只关心“编辑草稿”的操作。(r.sub.ID == r.obj.AuthorID || (r.sub.Title == "manager" && r.sub.Dept == r.obj.Dept))
: 这是核心的 ABAC 属性判断逻辑,实现了“作者或同部门经理”的规则。注意这里我们直接访问了传入的r.sub
和r.obj
结构体的属性。
==关于 ABAC 的表示,可以通过自定义函数完成==
2. Policy (policy.csv
)
这个文件定义了具体的角色分配和基础权限。
# RBAC 基础权限: "editor" 角色可以 "edit" "document" 类型的对象
p, editor, document, edit
# 用户角色分配
g, alice, editor
g, bob, editor
g, charlie, editor # 假设经理也需要 editor 角色才有基础编辑权
g, charlie, manager # 查理同时也是经理
3. Go 伪代码 (main.go
)
package main
import (
"fmt"
"log"
"github.com/casbin/casbin/v2"
)
// 主体(用户)结构体,包含属性
type Subject struct {
ID string
Title string
Dept string
}
// 客体(文档)结构体,包含属性
type Object struct {
ID string
Type string // 对象类型,匹配 model 中的 obj_type
Status string
AuthorID string
Dept string
}
func main() {
// 从文件加载模型和策略
enforcer, err := casbin.NewEnforcer("model.conf", "policy.csv")
if err != nil {
log.Fatalf("Failed to create enforcer: %v", err)
}
// --- 定义测试数据 ---
// 用户
alice := Subject{ID: "alice", Title: "developer", Dept: "Tech"}
bob := Subject{ID: "bob", Title: "developer", Dept: "Tech"}
charlie := Subject{ID: "charlie", Title: "manager", Dept: "Sales"} // Sales 部门经理
dave := Subject{ID: "dave", Title: "manager", Dept: "Tech"} // Tech 部门经理
// 文档
doc1 := Object{ID: "doc1", Type: "document", Status: "draft", AuthorID: "alice", Dept: "Tech"}
doc2 := Object{ID: "doc2", Type: "document", Status: "draft", AuthorID: "bob", Dept: "Sales"}
doc3 := Object{ID: "doc3", Type: "document", Status: "published", AuthorID: "alice", Dept: "Tech"} // 已发布
// --- 执行权限检查 ---
action := "edit"
// 场景 1: Alice (作者) 编辑自己的草稿 doc1 (Tech 部门) -> 应该允许 (作者规则)
check(enforcer, alice, doc1, action)
// 场景 2: Bob (非作者, Tech 部门普通员工) 编辑 Alice 的草稿 doc1 -> 应该拒绝
check(enforcer, bob, doc1, action)
// 场景 3: Charlie (Sales 经理) 编辑 Bob 的草稿 doc2 (Sales 部门) -> 应该允许 (同部门经理规则)
check(enforcer, charlie, doc2, action)
// 场景 4: Charlie (Sales 经理) 编辑 Alice 的草稿 doc1 (Tech 部门) -> 应该拒绝 (不同部门)
check(enforcer, charlie, doc1, action)
// 场景 5: Dave (Tech 经理) 编辑 Alice 的草稿 doc1 (Tech 部门) -> 应该允许 (同部门经理规则)
check(enforcer, dave, doc1, action)
// 场景 6: Alice (作者) 编辑自己已发布的 doc3 -> 应该拒绝 (状态不是 draft)
check(enforcer, alice, doc3, action)
// 场景 7: Alice (作者) 尝试 "view" 自己的草稿 doc1 -> 应该拒绝 (因为基础策略 p 只有 edit, 且 matcher 强制 act=="edit")
// 注意:如果需要 view 权限,需要单独添加策略和可能的 matcher 逻辑
check(enforcer, alice, doc1, "view")
}
// 辅助函数,执行检查并打印结果
func check(e *casbin.Enforcer, sub Subject, obj Object, act string) {
allowed, err := e.Enforce(sub, obj, act)
if err != nil {
fmt.Printf("Error checking permission for %s to %s %s (%s): %v\n", sub.ID, act, obj.ID, obj.Status, err)
return
}
if allowed {
fmt.Printf("Allow: %s (%s, %s) can %s %s (%s, Author:%s, Dept:%s)\n", sub.ID, sub.Title, sub.Dept, act, obj.ID, obj.Status, obj.AuthorID, obj.Dept)
} else {
fmt.Printf("Deny: %s (%s, %s) cannot %s %s (%s, Author:%s, Dept:%s)\n", sub.ID, sub.Title, sub.Dept, act, obj.ID, obj.Status, obj.AuthorID, obj.Dept)
}
}
运行结果预期:
Allow: alice (developer, Tech) can edit doc1 (draft, Author:alice, Dept:Tech)
Deny: bob (developer, Tech) cannot edit doc1 (draft, Author:alice, Dept:Tech)
Allow: charlie (manager, Sales) can edit doc2 (draft, Author:bob, Dept:Sales)
Deny: charlie (manager, Sales) cannot edit doc1 (draft, Author:alice, Dept:Tech)
Allow: dave (manager, Tech) can edit doc1 (draft, Author:alice, Dept:Tech)
Deny: alice (developer, Tech) cannot edit doc3 (published, Author:alice, Dept:Tech)
Deny: alice (developer, Tech) cannot view doc1 (draft, Author:alice, Dept:Tech)
这个例子展示了如何在 Casbin 中:
- 使用
g
定义用户角色 (RBAC)。 - 使用
p
定义角色对应的基础权限 (RBAC)。 - 在
matcher
中结合g(...)
的 RBAC 检查和直接访问请求对象属性 (r.sub.*
,r.obj.*
) 的 ABAC 检查,来实现复杂的、基于属性的访问控制逻辑。
总结
RBAC 的抽象与动态性
- RBAC 的核心概念(用户、角色、权限、分配关系)是高度结构化和抽象化的。
- Casbin 的
model.conf
定义了这种结构(p
,g
)。 - Casbin 的 Policy (存储在文件或数据库中) 存储了具体的实例数据(哪个用户分配了哪个角色,哪个角色拥有哪个基础权限)。
- 因此,RBAC 部分非常适合用 Casbin 的 Policy/Adapter 机制来实现动态管理。你可以通过增删改查 Policy 数据来动态调整用户角色和基础权限,而不需要修改代码或模型。RBAC 在 Casbin 中是高度抽象和可配置的。
ABAC 的定制化与实现
- ABAC 的核心是基于属性,而“属性”本身以及基于属性的“判断逻辑”是高度依赖具体业务场景的。
- 哪些属性重要?(用户部门?资源状态?访问时间?IP 地址?风险等级?)
- 如何组合这些属性进行判断?(是 AND 还是 OR?是否有例外条件?是否需要查外部数据?)
- 这些问题的答案在不同系统、不同场景下千差万别,很难用一套完全通用的、预定义的模型或规则来抽象。
- 因此,在 Casbin 中(尤其是采用自定义函数方法时),ABAC 部分往往需要根据具体的业务需求进行定制化开发。
- 自定义函数就成了实现这种定制化逻辑的主要方式。开发者需要在函数内部编写代码,根据传入的属性(来自请求上下文)和可能的外部数据(如数据库查询结果)来完成具体的判断逻辑。
- 所以,ABAC 部分不像 RBAC 那样容易完全抽象出来并仅通过配置来驱动(除非你的 ABAC 规则非常简单,或者你构建了一个非常复杂的配置驱动的自定义函数/引擎)。
因此在典型的 Casbin 应用中:
- RBAC 更能体现出 Casbin 模型驱动、策略数据动态管理的优势,是高度抽象和可配置的。
- ABAC 则更多地依赖开发者根据具体业务场景编写自定义函数(或其他集成方式),将特定的属性判断逻辑嵌入到代码中