Permify 属性访问控制(ABAC)实战指南:用 attributes 与 rules 构建基于属性的细粒度授权
2026/9/17 22:21:02 网站建设 项目流程

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 中通过attributerule两大核心构件描述用户、资源与环境的属性条件(如地理位置、访问时段、账户余额、设备信息),如何结合关系型授权(ReBAC/RBAC)组合出灵活且可追溯的访问策略,以及如何使用validate命令与场景化 YAML 对 ABAC 模型进行自动化验证。读完本文,你可以直接在自己的授权模型中落地基于布尔、字符串、整数、浮点数的条件判权。


什么是 ABAC:从游乐园到访问控制

ABAC(Attribute-Based Access Control)是一种基于"属性"做出访问决策的访问控制模型。它像一名保安,根据请求者、目标资源乃至当前环境所具备的**特定特征(attributes)**来决定是否放行——这些属性可以归属于用户(subject)、资源(object)或环境(environment),其取值会直接影响一次访问请求的最终结果。

用一个直观的类比来理解:想象一个游乐园里有三个不同的游乐设施,每个设施对游客有不同要求:

  1. 设施一:身高必须超过 6 英尺;
  2. 设施二:体重必须低于 200 磅;
  3. 设施三:年龄必须在 12~18 岁之间。

ABAC 的工作方式与此完全相同——它检查你在用户、资源或环境上定义的某些"品质"是否满足预设条件,满足则授予访问,否则拒绝。

为什么需要 ABAC

答案很朴素:有些场景用 ReBAC 或 RBAC 并不合适。就好比在炎热的沙漠公路上装雪地胎、或在暴风雪中换夏季胎——工具与场景不匹配。ABAC 适合以下典型场景:

  1. 地理限制(Geographically Restricted):像夜店门口只放行来自特定城镇的客人一样,例如流媒体服务可以根据国家/地区决定哪些影片可见,以符合各地的播放许可规则。
  2. 时间限制(Time-Based):像家长为孩子设定电脑使用时间一样,例如系统只允许在办公时段内执行某些操作。
  3. 隐私法规合规(Compliance with Privacy Regulations):例如医院系统需要根据患者授权、访问目的以及访问者身份三重条件,限制谁能查看患者数据。
  4. 数值限额(Limit Range):用规则定义数量上限或区间,例如银行系统对转账或取款金额设置限额。
  5. 设备信息(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.goTypeUrlToString所支持的 proto 值类型一一对应(BooleanValueBooleanArrayValueStringValueStringArrayValueIntegerValueIntegerArrayValueDoubleValueDoubleArrayValue),编译阶段则由pkg/dsl/compiler/compiler.gogetArgumentTypeIfExist将 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→ true
  • check*{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)→ true
  • check*{snapshot}*{schema*version}*{context}_post:1#member@user:1→ true

用例三:银行系统取款限额

该模型描述一个包含useraccount两个实体的银行系统:

  1. user:银行的客户;
  2. 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)→ true
  • check*{snapshot}*{schema*version}*{context}_account:1#owner@user:1→ true

仓库中的 banking-system.yaml 给出了可运行的完整版本,其中甚至包含两组断言场景:"账户所有者提取 3000 → 允许",以及"非所有者 steven 提取 3000 → 拒绝",可直接用于验证模型行为。

用例四:层级化使用(Hierarchical Usage)

在该模型中:

  1. employee:权限检查中使用的 subject 类型。此例中权限逻辑不依赖 employee 的任何关系,因此任何 employee 主体都会得到相同结果;
  2. organization:代表整个组织,拥有founding_year属性。当check_founding_year规则(检查组织是否成立于 2000 年之后)返回 true 时,授予view权限;
  3. 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:20000
  • organization: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:1organization:2,只要其中一个通过view,遍历依然成功——因为relation.permission对关联实体采用并集(union)语义。

示例数据:

  • department:1#organization@organization:1
  • department:1#organization@organization:2
  • department:1$budget|double:20000
  • organization:1$founding_year|integer:2021
  • organization:2$founding_year|integer:1990

求值结果:

  • department:1$check_budget(budget)→ true
  • organization:1$check_founding_year(founding_year)→ true
  • organization:2$check_founding_year(founding_year)→ false
  • department:1#organization.view→ true
  • department: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→ true
  • check*{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_filtersLookupEntity)与subject_filtersLookupSubject)断言,最终输出SUCCESS或带颜色标注的FAILED明细。

需要理解的两点:

  1. 属性字符串的规范格式:校验文件中entity_name:entity_id$attribute|attribute_type:attribute_value的写法与运行时完全一致,由 pkg/attribute/attribute.go#L22-L131 中的Attribute函数解析(数组类型用逗号分隔元素),并通过ValidateValue校验值与声明的属性类型匹配;
  2. 断言的三种能力checks验证"某个主体对某个实体是否拥有某权限"(布尔结果),entity_filters验证"某个主体在给定条件下能访问哪些实体"(实体 ID 数组),subject_filters验证"哪些主体能访问某个实体"(主体 ID 数组)——分别对应CheckLookupEntityLookupSubject三个 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询