Permify 属性访问控制(ABAC)实战指南:用 attributes 与 rules 构建基于属性的细粒度授权
【免费下载链接】permifyAn open-source authorization as a service inspired by Google Zanzibar, designed to build and manage fine-grained and scalable authorization systems for any application. — Permify is now part of FusionAuth 🎉项目地址: https://gitcode.com/GitHub_Trending/pe/permify
导读
本文系统讲解 Permify 中基于属性访问控制(Attribute-Based Access Control,ABAC)的设计思路与实战方法。你将学会如何在 Permify Schema 中通过attribute与rule两大核心构件描述用户、资源与环境的属性条件(如地理位置、访问时段、账户余额、设备信息),如何结合关系型授权(ReBAC/RBAC)组合出灵活且可追溯的访问策略,以及如何使用validate命令与场景化 YAML 对 ABAC 模型进行自动化验证。读完本文,你可以直接在自己的授权模型中落地基于布尔、字符串、整数、浮点数的条件判权。
什么是 ABAC:从游乐园到访问控制
ABAC(Attribute-Based Access Control)是一种基于"属性"做出访问决策的访问控制模型。它像一名保安,根据请求者、目标资源乃至当前环境所具备的**特定特征(attributes)**来决定是否放行——这些属性可以归属于用户(subject)、资源(object)或环境(environment),其取值会直接影响一次访问请求的最终结果。
用一个直观的类比来理解:想象一个游乐园里有三个不同的游乐设施,每个设施对游客有不同要求:
- 设施一:身高必须超过 6 英尺;
- 设施二:体重必须低于 200 磅;
- 设施三:年龄必须在 12~18 岁之间。
ABAC 的工作方式与此完全相同——它检查你在用户、资源或环境上定义的某些"品质"是否满足预设条件,满足则授予访问,否则拒绝。
为什么需要 ABAC
答案很朴素:有些场景用 ReBAC 或 RBAC 并不合适。就好比在炎热的沙漠公路上装雪地胎、或在暴风雪中换夏季胎——工具与场景不匹配。ABAC 适合以下典型场景:
- 地理限制(Geographically Restricted):像夜店门口只放行来自特定城镇的客人一样,例如流媒体服务可以根据国家/地区决定哪些影片可见,以符合各地的播放许可规则。
- 时间限制(Time-Based):像家长为孩子设定电脑使用时间一样,例如系统只允许在办公时段内执行某些操作。
- 隐私法规合规(Compliance with Privacy Regulations):例如医院系统需要根据患者授权、访问目的以及访问者身份三重条件,限制谁能查看患者数据。
- 数值限额(Limit Range):用规则定义数量上限或区间,例如银行系统对转账或取款金额设置限额。
- 设备信息(Device Information):根据设备类型、操作系统版本、是否已安装最新安全补丁等设备属性控制访问。
可见 ABAC 是一种更"情境化(contextual)"的授权方式:你可以在应用的请求上下文中携带任意数据,围绕主体(subject)与客体(object)所处的情境来定义访问权限。Permify 正是通过向 DSL 中引入attributes(属性)与rules(规则)两个构件来支撑这种能力。
在 Permify DSL 中建模 ABAC
定义属性(Attributes)
属性用于以特定数据类型描述实体(entity)的属性特征。例如,可以为组织实体定义一个ip_range属性,其类型为字符串数组:
attribute ip_range string[]定义attribute时可选的全部数据类型如下:
// A boolean attribute type boolean // A boolean array attribute type. boolean[] // A string attribute type. string // A string array attribute type. string[] // An integer attribute type. integer // An integer array attribute type. integer[] // A double attribute type. double // A double array attribute type. double[]从仓库实现看,这 8 种类型与pkg/attribute/attribute.go中TypeUrlToString所支持的 proto 值类型一一对应(BooleanValue、BooleanArrayValue、StringValue、StringArrayValue、IntegerValue、IntegerArrayValue、DoubleValue、DoubleArrayValue),编译阶段则由pkg/dsl/compiler/compiler.go的getArgumentTypeIfExist将 DSL 中的string/boolean/integer/double及数组后缀映射为对应的AttributeType枚举。
定义规则(Rules)
规则(rule)是允许你在模型中编写具体条件判断的结构,可以将其理解为每种编程语言都有的"简单函数":它接受参数,基于条件求值并返回true/false。
下面的示例 Schema 中,规则被用来判断给定 IP 地址是否落在指定的 IP 范围内:
entity user {} entity organization { relation admin @user attribute ip_range string[] permission view = check_ip_range(ip_range) or admin } rule check_ip_range(ip string, ip_range string[]) { context.data.ip in ip_range }Permify 的 Schema 语言基于 Common Expression Language (CEL) 设计,因此语法与 C++、Go、Java、TypeScript 中的等价表达式几乎一致。
context是规则体内的保留变量,指向请求中携带的上下文数据。
从源码来看,这一设计并非仅停留在文档层面:在 编译器 中,compileRule会为每个规则参数以及context变量创建 CEL 环境(cel.Variable),对规则表达式进行编译与类型检查,并且强制要求规则表达式的输出类型必须是布尔值(compiledExp.OutputType() != cel.BoolType时直接报错)。而在 check 引擎 的checkDirectCall中,规则被还原为 CEL AST 并用prg.Eval(arguments)实际求值——参数值来自实体属性(通过QueryAttributes读取)与请求上下文(context.data),最终只有布尔结果为true才返回 ALLOWED。
接下来,我们通过几个常见用法来剖析 ABAC 的落地模式。
布尔型条件(True/False)
对于表示二值状态(如 yes/no)的属性,Boolean是最佳选择:
entity post { attribute is_public boolean permission view = is_public }布尔属性可以直接作为权限表达式使用,无需额外编写规则(这是布尔类型独有的便捷性)。其实现由编译器的compileIdentifier(见 compiler.go)保证:当标识符引用类型为属性且类型为boolean时,它被编译为ComputedAttribute叶子节点;执行阶段则由 checkComputedAttribute/checkDirectAttribute 读取属性值并直接判定——值解析为true即 ALLOWED,否则 DENIED。
文本与对象条件(Text & Object Based Conditions)
字符串型属性适用于多种需要文本信息参与判权的场景,例如:
- 地理位置:存储
"USA"、"EU"、"Asia"等区域标识,按地理位置控制访问; - 设备类型:存储
"mobile"、"desktop"、"tablet"等设备类型; - 时区:存储
"EST"、"PST"、"GMT"等时区标识; - 星期:存储
"Monday"、"Tuesday"等,按星期几控制资源访问。
以下 Schema 在 v1.1 以上版本中可正常工作;若你使用更早版本,请参考版本差异文档 v1.0-v1.1 迁移指南。
entity user {} entity organization { relation admin @user attribute location string[] permission view = check_location(location) or admin } rule check_location(location string[]) { context.data.current_location in location }数值条件(Numerical Conditions)
整数(Integers)
整数属性适用于以下数值判断场景:
- 年龄:面向年龄受限的资源,以整数属性存储年龄;
- 安全等级:为不同安全级别的用户存储整数等级(如 1、2、3,数字越大级别越高);
- 资源大小或长度:按文档长度、文件大小等整数属性控制访问;
- 版本号:按软件版本或文档修订号等整数属性做决策。
entity content { attribute min_age integer permission view = check_age(min_age) } rule check_age(min_age integer) { context.data.age >= min_age }浮点数(Doubles)——精确数值信息
Double属性适用于需要小数精度的数值判权场景:
- 用量限额:如存储容量、下载数据量等需要十进制精度表示的限额;
- 交易金额:金融系统中需要精确表示的交易金额(如 $100.50);
- 用户评分:如 5 分制下带小数的评分(4.7);
- 地理位置坐标:如经纬度这类通常带小数点的坐标值。
entity user {} entity account { relation owner @user attribute balance double permission withdraw = check_balance(balance) and owner } rule check_balance(balance double) { (balance >= context.data.amount) && (context.data.amount <= 5000) }综合用例详解
用例一:公开/私有仓库
本模型中,is_public被定义为布尔属性。由于属性是布尔类型,它可以直接用于权限表达式,无需编写规则:
entity user {} entity post { relation owner @user attribute is_public boolean permission view = is_public or owner permission edit = owner }含义:若仓库的is_public属性为true,则所有人可查看;若为false(非公开),则只有所有者(此处为user:1)可以查看。
permission view = is_public or owner表示:只要仓库公开(is_public为 true),或者当前用户是仓库所有者,即授予 view 权限。
关系(relationships):
post:1#owner@user:1
属性(attributes):
post:1$is_public|boolean:true
Post View 的 Check 演进子查询:
→post:1#is_public→ true →post:1#admin@user:1→ true
哈希前的请求键:
check*{snapshot}*{schema*version}*{context}_post:1$is_public→ truecheck*{snapshot}*{schema*version}*{context}_post:1#admin@user:1→ true
这一模型在仓库的 public-private-repository.yaml 示例形态文件中也有完整对应实现。
用例二:按工作日放行
本模型中,要查看仓库必须满足:当天不是周末,且用户是所属组织的成员。
entity user {} entity organization { relation member @user attribute valid_weekdays string[] permission view = is_weekday(valid_weekdays) and member } entity repository { relation organization @organization permission view = organization.view } rule is_weekday(valid_weekdays string[]) { context.data.day_of_week in valid_weekdays }该模型声明:要获得仓库的view权限,必须同时满足两个条件——根据上下文数据day_of_week判定的当前日期不是周末(由is_weekday规则判定),并且用户是拥有该仓库的组织的成员。
关系:
organization:1#member@user:1
Organization View 的 Check 演进子查询:
→organization:1$is_weekday(valid_weekdays)→ true →organization:1#member@user:1→ true
哈希前的请求键:
check*{snapshot}*{schema*version}*{context}_organization:1$is_weekday(valid_weekdays)→ truecheck*{snapshot}*{schema*version}*{context}_post:1#member@user:1→ true
用例三:银行系统取款限额
该模型描述一个包含user与account两个实体的银行系统:
user:银行的客户;account:银行账户,拥有owner(类型为user)和balance(账户内金额)。
entity user {} entity account { relation owner @user attribute balance double permission withdraw = check_balance(balance) and owner } rule check_balance(balance double) { (balance >= context.data.amount) && (context.data.amount <= 5000) }check_balance 规则:验证取款金额是否小于等于账户余额,且不超过 5000(单次取款上限)。它接受两个参数:取款金额(amount,来自请求上下文)与账户当前余额(balance,来自账户属性)。
owner 检查:验证请求取款的人是否为该账户的所有者。
只有两个条件同时为真,withdraw权限才会被授予——即用户必须是账户所有者,且取款金额在账户余额之内且不超过 5000。
关系:
account:1#owner@user:1
属性:
account:1$balance|double:4000
Account Withdraw 的 Check 演进子查询:
→account:1$check_balance(balance)→ true →account:1#owner@user:1→ true
哈希前的请求键:
check*{snapshot}*{schema*version}*{context}_account:1$check_balance(balance)→ truecheck*{snapshot}*{schema*version}*{context}_account:1#owner@user:1→ true
仓库中的 banking-system.yaml 给出了可运行的完整版本,其中甚至包含两组断言场景:"账户所有者提取 3000 → 允许",以及"非所有者 steven 提取 3000 → 拒绝",可直接用于验证模型行为。
用例四:层级化使用(Hierarchical Usage)
在该模型中:
employee:权限检查中使用的 subject 类型。此例中权限逻辑不依赖 employee 的任何关系,因此任何 employee 主体都会得到相同结果;organization:代表整个组织,拥有founding_year属性。当check_founding_year规则(检查组织是否成立于 2000 年之后)返回 true 时,授予view权限;department:组织内的部门。拥有budget属性和指向父级organization的关系。当部门预算超过 10,000(由check_budget规则判定)并且organization.view权限为真时,授予view权限。
注意:在此模型中,权限可以引用更高级别的权限(如organization.view)。但你不能以这种方式使用某个关系的属性——例如不能直接在权限表达式中引用organization.founding_year。权限可以依赖关联实体的权限,但不能直接依赖关联实体的属性。
entity employee {} entity organization { attribute founding_year integer permission view = check_founding_year(founding_year) } entity department { relation organization @organization attribute budget double permission view = check_budget(budget) and organization.view } rule check_founding_year(founding_year integer) { founding_year > 2000 } rule check_budget(budget double) { budget > 10000 }关系:
department:1#organization@organization:1
属性:
department:1$budget|double:20000organization:1$founding_year|integer:2021
求值过程:
→department:1$check_budget(budget)→ true
→organization:1$check_founding_year(founding_year)→ true
→department:1#view→ true
多组织关联的并集语义:如果某个部门关联了多个组织,
organization.view会在所有关联组织上分别求值。例如department:1同时关联organization:1与organization:2,只要其中一个通过view,遍历依然成功——因为relation.permission对关联实体采用并集(union)语义。示例数据:
department:1#organization@organization:1department:1#organization@organization:2department:1$budget|double:20000organization:1$founding_year|integer:2021organization:2$founding_year|integer:1990求值结果:
department:1$check_budget(budget)→ trueorganization:1$check_founding_year(founding_year)→ trueorganization:2$check_founding_year(founding_year)→ falsedepartment:1#organization.view→ truedepartment:1#view→ true
ABAC 访问检查的求值过程
以 IP 范围检查模型为例,观察一次完整的 ABAC 判权流程:
模型
entity user {} entity organization { relation admin @user attribute ip_range string[] permission view = check_ip_range(ip_range) or admin } rule check_ip_range(ip_range string[]) { context.data.ip in ip_range }此例中的context指请求内部的上下文。你可以在请求中携带任何类型的数据,并在模型中引用它。例如:
"context": { "data": { "ip_address": "187.182.51.206", "day_of_week": "monday" }, }关系:
organization:1#admin@user:1
属性:
organization:1$ip_range|string[]:['187.182.51.206', '250.89.38.115']
Check 请求:
{ "entity": { "type": "organization", "id": "1" }, "permission": "view", "subject": { "type": "user", "id": "1" }, "context": { "data": { "ip_address": "187.182.51.206" } } }Organization View 的 Check 演进子查询:
→organization:1$check_ip_range(context.ip_address,ip_range)→ true →organization:1#admin@user:1→ true
缓存机制:缓存通过对数据库快照(snapshot)、Schema 版本(schema version)以及子查询进行哈希作为键,并存入其结果,因此与关系型子查询的缓存方式完全一致。例如:
哈希前的请求键:
check*{snapshot}*{schema*version}*{context}_organization:1#admin@user:1→ truecheck*{snapshot}*{schema*version}*{context}_organization:1$check_ip_range(ip_range)→ true
从实现上看,规则子查询与关系子查询在 check 引擎 中走的是同一条invoke通路:checkCall将权限名替换为规则名(check.go#L509-L523),随后checkDirectCall读取规则定义、拉取计算属性、构建context.data并以 CEL 求值;属性查询同样支持从请求上下文属性(contextual attributes)与持久化存储双源合并,因此请求级上下文数据可以与存储中的实体属性一起参与判权。
如何使用 ABAC:安装与模型验证
安装 Permify
docker pull ghcr.io/permify/permify:latest验证 YAML 结构
Permify 的校验文件由以下顶层字段构成:
schema: >- {string schema} relationships: - entity_name:entity_id#relation@subject_type:subject_id attributes: - entity_name:entity_id#attribute@attribute_type:attribute_value scenarios: - name: "name" description: "description" checks: - entity: "entity_name:entity_id" subject: "subject_name:subject_id" context: tuples: [] attributes: [] data: key: {value} assertions: permission: result entity_filters: - entity_type: "entity_name" subject: "subject_name:subject_id" context: tuples: [] attributes: [] data: key: {value} assertions: permission: result_array subject_filters: - subject_reference: "subject_name" entity: "entity_name:entity_id" context: tuples: [] attributes: [] data: key: {value} assertions: permission: result_array注意:context中的data字段可以以键值对形式赋予任意期望值;稍后可在模型中通过request.key引用该值。
校验文件中的示例:
context: tuples: [] attributes: [] data: day_of_week: "saturday"该 YAML 片段指定了一个不包含元组与属性的校验上下文,data字段表示当天为星期六。
模型中的示例:
permission delete = is_weekday(valid_weekdays)模型中设置了一条delete权限规则:它调用is_weekday函数,并传入关联实体上的valid_weekdays属性值。若is_weekday(["monday", "tuesday", "wednesday", "thursday", "friday"])为 true,则授予 delete 权限。
创建校验文件
下面是一个同时覆盖属性、规则、关系、上下文数据与多类断言的完整校验文件示例:
schema: >- entity user {} entity organization { relation member @user attribute credit integer permission view = check_credit(credit) and member } entity repository { relation organization @organization attribute is_public boolean attribute valid_weekdays string[] permission view = is_public permission edit = organization.view permission delete = is_weekday(valid_weekdays) } rule check_credit(credit integer) { credit > 5000 } rule is_weekday(valid_weekdays string[]) { context.data.day_of_week in valid_weekdays } relationships: - organization:1#member@user:1 - repository:1#organization@organization:1 attributes: - organization:1$credit|integer:6000 - repository:1$is_public|boolean:true scenarios: - name: "scenario 1" description: "test description" checks: - entity: "repository:1" subject: "user:1" context: assertions: view: true - entity: "repository:1" subject: "user:1" context: tuples: [] attributes: [] data: day_of_week: "saturday" assertions: view: true delete: false - entity: "organization:1" subject: "user:1" context: assertions: view: true entity_filters: - entity_type: "repository" subject: "user:1" context: assertions: view: ["1"] subject_filters: - subject_reference: "user" entity: "repository:1" context: assertions: view: ["1"] edit: ["1"]运行验证命令
docker run -v {your_config_folder}:/config ghcr.io/permify/permify-beta:latest validate /config/validation.yaml该命令在仓库中的实现位于 pkg/cmd/validate.go,其内部流程与上述 YAML 结构一一对应:解析文件(file.NewDecoderFromURL)→ 加载并解析 Schema(schema.NewSchemaLoader+parser.NewParser)→ 编译校验(compiler.NewCompiler(true, sch).Compile())→ 写入实体定义 → 逐个校验并写入关系(validate.go#L150-L186)与属性(validate.go#L191-L227)→ 对每个场景执行checks(权限检查)、entity_filters(LookupEntity)与subject_filters(LookupSubject)断言,最终输出SUCCESS或带颜色标注的FAILED明细。
需要理解的两点:
- 属性字符串的规范格式:校验文件中
entity_name:entity_id$attribute|attribute_type:attribute_value的写法与运行时完全一致,由 pkg/attribute/attribute.go#L22-L131 中的Attribute函数解析(数组类型用逗号分隔元素),并通过ValidateValue校验值与声明的属性类型匹配; - 断言的三种能力:
checks验证"某个主体对某个实体是否拥有某权限"(布尔结果),entity_filters验证"某个主体在给定条件下能访问哪些实体"(实体 ID 数组),subject_filters验证"哪些主体能访问某个实体"(主体 ID 数组)——分别对应Check、LookupEntity、LookupSubject三个 API。
总结
ABAC 为 Permify 的授权模型补上了"情境判权"这一环:attribute负责以 8 种类型承载实体属性数据,rule基于 CEL 编写可复用的布尔条件,二者既可独立工作(如布尔属性直接用于权限表达式),也可与relation组合(如check_balance(balance) and owner)形成"属性 + 关系"的混合授权。借助validate命令与场景化 YAML,你可以在不启动服务的情况下对模型进行回归验证,确保每个属性条件、每个上下文数据在判权时都符合预期。
【免费下载链接】permifyAn open-source authorization as a service inspired by Google Zanzibar, designed to build and manage fine-grained and scalable authorization systems for any application. — Permify is now part of FusionAuth 🎉项目地址: https://gitcode.com/GitHub_Trending/pe/permify
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考