zenstack-access-control

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

ZenStack V3 — Access Control

ZenStack V3——访问控制

ZenStack enforces access policies declared in ZModel at the ORM layer. Policies live next to your models (see
zenstack-schema-modeling
); enforcement is opt-in via a runtime plugin. For client/query basics — and for data validation — see
zenstack-querying
.
After editing policies, run
zen generate
to regenerate the client (it also validates the schema, policy expressions included). If you only want to validate without regenerating, run
zen check
.
ZenStack 在 ORM 层强制执行 ZModel 中声明的访问策略。策略与模型放在一起(参见
zenstack-schema-modeling
);可通过运行时插件选择是否启用策略强制执行。有关客户端和查询基础用法以及数据验证,请参阅
zenstack-querying
。
修改策略后,运行
zen generate
重新生成客户端(该命令还会验证 schema,包括策略表达式)。如果只想验证而不重新生成,请运行
zen check
。

Runtime setup (required for policies to apply)

运行时配置(应用策略时必需)

Policies do nothing until you (1) declare the plugin in the schema and (2) install it on the client.
Schema:
zmodel
plugin policy {
    provider = '@zenstackhq/plugin-policy'
}
Install the package:
npm install @zenstackhq/plugin-policy
.
Client:
ts
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
instances are immutable —
$use()
and
$setAuth()
return new (cheap, shallow- clone) instances and never mutate the original. Query with
db
to bypass policies (e.g. trusted server code) and with the policy-enforced client for user-facing access.
在完成以下两项操作之前,策略不会生效:(1) 在 schema 中声明插件;(2) 在客户端上安装插件。
Schema:
zmodel
plugin policy {
    provider = '@zenstackhq/plugin-policy'
}
安装软件包:
npm install @zenstackhq/plugin-policy
。
客户端:
ts
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
查询可绕过策略(例如受信任的服务器端代码),面向用户的访问则应使用启用策略的客户端。

The current user —
auth()
and
$setAuth()

当前用户——
auth()
和
$setAuth()

Bind the authenticated user per request;
auth()
in policies resolves to it.
ts
const userDb = authDb.$setAuth(user);   // user-bound client
await userDb.post.findMany();           // policies evaluated against `user`
  • No
    $setAuth()
    → anonymous mode →
    auth()
    is
    null
    .
    $setAuth(undefined)
    forces anonymous.
  • Read the bound user back via
    client.$auth
    .
Define what
auth()
returns
, by priority: a
model
/
type
annotated
@@auth
, otherwise a model literally named
User
, otherwise a compile error. Using a
type
decouples the auth shape from your tables:
zmodel
type Auth {
    id   Int
    role String
    @@auth
}
按请求绑定已认证的用户;策略中的
auth()
将解析为该用户。
ts
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
可以将身份验证的数据结构与表解耦:
zmodel
type Auth {
    id   Int
    role String
    @@auth
}

Policy rules —
@@allow
/
@@deny

策略规则——
@@allow
/
@@deny

@@allow(operation, condition)
@@deny(operation, condition)
Operations (comma-separate to combine):
create
,
read
,
update
,
delete
,
post-update
, and
all
(= create+read+update+delete, not post-update).
Evaluation (secure by default): any matching
@@deny
→ denied; else any matching
@@allow
→ allowed; else denied.
read
/
update
/
delete
silently filter out rows that fail, rather than erroring;
create
is checked before insert.
zmodel
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
}
@@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
会在插入前进行检查。
zmodel
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
}

Policy expressions

策略表达式

  • Operators:
    ==
    !=
    >
    >=
    <
    <=
    ,
    &&
    ||
    !
    , and field references.
  • To-one relations: dot access —
    auth().city == profile.address.city
    .
  • To-many relations: collection predicates
    • relation?[cond]
      — some match
    • relation![cond]
      — all match
    • relation^[cond]
      — none match
    zmodel
    @@deny('delete', posts?[published == true])   // can't delete a user with published posts
  • Use
    this
    to escape the collection scope:
    comments?[author.verified && this.published]
    .
  • create
    rules
    can only reference owned relations (those whose FK the model holds) — other relations don't exist yet at creation time.
  • check(relation [, op])
    delegates the check to a to-one relation's policies, avoiding duplication:
    @@allow('read', check(user))
    /
    @@allow('update', check(user, 'update'))
    .
  • Helper functions usable in expressions:
    contains
    ,
    startsWith
    ,
    endsWith
    (each takes an optional case-insensitive flag),
    has
    ,
    hasSome
    ,
    hasEvery
    ,
    isEmpty
    ,
    currentModel()
    ,
    currentOperation()
    .
  • 运算符:
    ==
    !=
    >
    >=
    <
    <=
    、
    &&
    ||
    !
    ,以及字段引用。
  • To-one 关系:使用点号访问——
    auth().city == profile.address.city
    。
  • To-many 关系:使用集合谓词
    • relation?[cond]
      ——部分匹配
    • relation![cond]
      ——全部匹配
    • relation^[cond]
      ——无一项匹配
    zmodel
    @@deny('delete', posts?[published == true])   // can't delete a user with published posts
  • 使用
    this
    退出集合作用域:
    comments?[author.verified && this.published]
    。
  • create
    规则
    只能引用所属关系(即模型持有其 FK 的关系)——其他关系在创建时还不存在。
  • check(relation [, op])
    将检查委托给 to-one 关系的策略,从而避免重复:
    @@allow('read', check(user))
    /
    @@allow('update', check(user, 'update'))
    。
  • 表达式中可使用的辅助函数:
    contains
    、
    startsWith
    、
    endsWith
    (每个函数均可选择是否忽略大小写)、
    has
    、
    hasSome
    、
    hasEvery
    、
    isEmpty
    、
    currentModel()
    、
    currentOperation()
    。

Field-level policies (v3.2.0+, preview)

字段级策略(v3.2.0+,预览功能)

Single-
@
@allow
/
@deny
on a field, operations
read
and
update
only.
zmodel
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
}
  • Failing a read policy nullifies the field (returned as
    null
    , still usable in SQL computations) — you can't distinguish a real
    null
    from a denied one.
  • Failing an update policy rejects the whole update (
    ORMError
    , reason
    REJECTED_BY_POLICY
    ).
在字段上使用单
@
的
@allow
/
@deny
,仅支持
read
和
update
操作。
zmodel
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
}
  • read 策略检查失败会将字段置为空值(返回
    null
    ,但仍可用于 SQL 计算)——无法区分真实的
    null
    与因策略拒绝而返回的空值。
  • update 策略检查失败会拒绝整个更新(
    ORMError
    ,原因为
    REJECTED_BY_POLICY
    )。

Post-update rules

Post-update 规则

post-update
expresses conditions that must hold after an update; field references see the new values, and
before()
reads the old values. (This replaces V2's
future()
.)
zmodel
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
}
If a model has no
post-update
rule, an update only needs to pass
update
. If it has any, at least one
@@allow('post-update', ...)
must pass.
post-update
用于表达更新后必须满足的条件;字段引用读取的是新值,
before()
读取的是旧值。(这取代了 V2 中的
future()
。)
zmodel
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', ...)
通过。

Execution order & error reasons

执行顺序与错误原因

Per mutation, policies are checked before the database is touched (after any input validation — see
zenstack-querying
). Policy-related
ORMError.reason
values:
REJECTED_BY_POLICY
(policy) and
NOT_FOUND
(missing or filtered out by a read policy).
REJECTED_BY_POLICY
further carries
rejectedByPolicyReason
:
NO_ACCESS
,
CANNOT_READ_BACK
(mutation allowed but result not readable back), or
OTHER
.
每次 mutation 都会在访问数据库之前检查策略(在任何输入验证之后执行——参见
zenstack-querying
)。与策略相关的
ORMError.reason
值包括:
REJECTED_BY_POLICY
(策略拒绝)和
NOT_FOUND
(对象不存在或已被 read 策略过滤掉)。
REJECTED_BY_POLICY
还会携带
rejectedByPolicyReason
:
NO_ACCESS
、
CANNOT_READ_BACK
(允许执行 mutation,但无法读回结果)或
OTHER
。

Limitations

限制

  • Database-level cascade deletes/updates and triggers are not policed by ZenStack.
  • Raw SQL (
    $queryRaw
    /
    $executeRaw
    ) and query-builder access bypass policies. Allow raw SQL while the policy plugin is installed only via
    new PolicyPlugin({ dangerouslyAllowRawSql: true })
    (v3.5.0+).
  • 数据库级级联删除/更新和触发器不会受到 ZenStack 策略控制。
  • 原始 SQL(
    $queryRaw
    /
    $executeRaw
    )和 query builder 访问会绕过策略。在安装 policy 插件期间,只有通过
    new PolicyPlugin({ dangerouslyAllowRawSql: true })
    才能允许原始 SQL(v3.5.0+)。

Reference docs

参考文档

Full ZenStack documentation for this topic is bundled under
references/
:
  • access-control-overview.md — how access control works
  • write-policies.md —
    @@allow
    /
    @@deny
    ,
    auth()
    , expressions
  • enforcing-policies.md — policy plugin +
    $setAuth
    runtime
  • post-update.md —
    post-update
    rules and
    before()
  • field-level.md — field-level read/update policies
  • zmodel-expression-reference.md — policy expression reference
  • policy-plugin-reference.md — policy plugin options
有关此主题的完整 ZenStack 文档已打包在
references/
中:
  • access-control-overview.md——访问控制的工作原理
  • write-policies.md——
    @@allow
    /
    @@deny
    、
    auth()
    、表达式
  • enforcing-policies.md——policy 插件和
    $setAuth
    运行时
  • post-update.md——
    post-update
    规则和
    before()
  • field-level.md——字段级 read/update 策略
  • zmodel-expression-reference.md——策略表达式参考
  • policy-plugin-reference.md——policy 插件选项