关于 zenstack-access-control
面向 ZenStack 用户的访问控制技能,说明如何在 ZModel 中编写策略,并通过运行时插件在 ORM 层执行。涵盖用户身份绑定、字段策略及更新后检查。
主要功能
- 01在 ZModel 中声明允许与拒绝策略
- 02按请求绑定当前用户
- 03定义字段级读写策略
- 04检查更新后的数据状态
- 05使用关系条件表达式
适用场景
- 01限制用户只能访问自己的记录
- 02隐藏匿名用户不可读的字段
- 03阻止不符合策略的记录更新
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ zenstackhq/ skills --skill zenstack-access-control安装后在 Claude Code 中输入/zenstack-access-control确认已加载。
详细介绍
ZenStack V3 — 访问控制
ZenStack 在 ORM 层强制执行 ZModel 中声明的访问策略。策略与模型放在一起(参见 zenstack-schema-modeling);只有通过运行时插件启用后才会执行。有关客户端/查询基础知识以及数据验证,请参见 zenstack-querying。
编辑策略后,运行 zen generate 重新生成客户端(它也会验证架构,包括策略表达式)。如果只想验证而不重新生成,请运行 zen check。
运行时设置(策略生效的必要条件)
在(1)于架构中声明插件并(2)在客户端上安装插件之前,策略不会生效。
架构:
plugin policy {
provider = '@zenstackhq/plugin-policy'
}
安装软件包:npm install @zenstackhq/plugin-policy。
客户端:
import { ZenStackClient } from '@zenstackhq/orm';
import { PolicyPlugin } from '@zenstackhq/plugin-policy';
const db = new ZenStackClient(schema, { dialect }); // raw, unprotected
export const authDb = db.$use(new PolicyPlugin()); // access control enforced
ZenStackClient 实例是不可变的——$use() 和 $setAuth() 会返回新的(开销很低、浅克隆的)实例,且绝不会修改原实例。使用 db 查询可绕过策略(例如受信任的服务端代码),面向用户的访问则使用受策略强制执行的客户端。
当前用户 — auth() 和 $setAuth()
按请求绑定经过身份验证的用户;策略中的 auth() 会解析为该用户。
const userDb = authDb.$setAuth(user); // user-bound client
await userDb.post.findMany(); // policies evaluated against `user`
- 不调用
$setAuth()→ 匿名模式 →auth()为null。$setAuth(undefined)会强制进入匿名模式。 - 通过
client.$auth读取已绑定的用户。
定义 auth() 的返回值,优先级依次为:带有 @@auth 注解的 model/type,否则为字面名称为 User 的模型,否则编译错误。使用 type 可将身份验证数据结构与表解耦:
type Auth {
id Int
role String
@@auth
}
策略规则 — @@allow / @@deny
@@allow(operation, condition)
@@deny(operation, condition)
操作(用逗号分隔以组合):create、read、update、delete、post-update 和 all(= create+read+update+delete,不包括 post-update)。
求值(默认安全):任何匹配的 @@deny → 拒绝;否则,任何匹配的 @@allow → 允许;否则拒绝。read/update/delete 会静默地过滤掉不符合条件的行,而非报错;create 则在插入前检查。
model User {
id Int @id @default(autoincrement())
email String @unique
posts Post[]
@@allow('create,read', true) // anyone can sign up; profiles public
@@allow('all', auth().id == id) // the user has full access to themselves
}
model Post {
id Int @id @default(autoincrement())
title String
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
@@deny('all', auth() == null) // no anonymous access
@@allow('read', published) // published posts readable by anyone
@@allow('all', auth().id == authorId) // author has full access
}
策略表达式
-
运算符:
==!=>>=<<=、&&||!,以及字段引用。 -
一对一关系:使用点号访问 —
auth().city == profile.address.city。 -
一对多关系:集合谓词
relation?[cond]— 至少有一个匹配relation![cond]— 全部匹配relation^[cond]— 没有匹配
@@deny('delete', posts?[published == true]) // can't delete a user with published posts -
使用
this跳出集合作用域:comments?[author.verified && this.published]。 -
create规则只能引用归属关系(即模型持有其外键的关系)——创建时其他关系尚不存在。 -
check(relation [, op])将检查委托给一对一关系的策略,避免重复编写:@@allow('read', check(user))/@@allow('update', check(user, 'update'))。 -
可在表达式中使用的辅助函数:
contains、startsWith、endsWith(均可接受可选的忽略大小写标志)、has、hasSome、hasEvery、isEmpty、currentModel()、currentOperation()。
字段级策略(v3.2.0+,预览版)
字段上的单个 @ @allow / @deny,仅支持 read 和 update 操作。
model User {
id Int @id
email String @allow('update', auth() == this) // only the owner can change it
name String @deny('read', auth() == null) // hidden from anonymous readers
}
- 读取策略未通过时,字段会被置为
null(返回值为null,但仍可用于 SQL 计算)——无法区分真实的null和被策略拒绝的值。 - 更新策略未通过时,整个更新会被拒绝(
ORMError,原因是REJECTED_BY_POLICY)。
更新后规则
post-update 表示更新之后必须满足的条件;字段引用指向新值,而 before() 读取旧值。(这取代了 V2 的 future()。)
model Post {
published Boolean @default(false)
authorId Int
@@deny('post-update', published == true && auth().id != authorId) // only author can publish
@@deny('post-update', before().authorId != authorId) // authorId is immutable
}
如果模型没有 post-update 规则,更新只需通过 update 检查。如果模型有任意 post-update 规则,则至少有一个 @@allow('post-update', ...) 必须通过。
执行顺序和错误原因
每次变更都会在访问数据库之前检查策略(在完成任何输入验证之后 ——
请参阅 zenstack-querying)。与策略相关的 ORMError.reason 值:REJECTED_BY_POLICY(策略)和
NOT_FOUND(不存在,或被读取策略过滤掉)。REJECTED_BY_POLICY 还包含
rejectedByPolicyReason:NO_ACCESS、CANNOT_READ_BACK(允许变更,但无法读取回结果)或 OTHER。
限制
- ZenStack 不会对数据库级联删除/更新和触发器进行策略检查。
- 原始 SQL(
$queryRaw/$executeRaw)和查询构建器访问会绕过策略。仅当策略插件已安装时,才能通过new PolicyPlugin({ dangerouslyAllowRawSql: true })允许原始 SQL (v3.5.0+)。
参考文档
此主题的完整 ZenStack 文档位于 references/ 下:
- access-control-overview.md — 访问控制的工作原理
- write-policies.md —
@@allow/@@deny、auth()、表达式 - enforcing-policies.md — 策略插件 +
$setAuth运行时 - post-update.md —
post-update规则和before() - field-level.md — 字段级读取/更新策略
- zmodel-expression-reference.md — 策略表达式参考
- policy-plugin-reference.md — 策略插件选项