zenstack-access-control について
ZenStack ユーザー向けのアクセス制御スキルです。ZModel のポリシーをランタイムプラグインで ORM 層に適用する方法を説明し、ユーザーの紐付け、フィールド単位のポリシー、更新後の検査を扱います。
主な機能
- 01ZModel で許可・拒否ポリシーを宣言
- 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 は、ZModel で宣言されたアクセス ポリシーを ORM 層で適用します。ポリシーはモデルの隣に記述します(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]— 1 つ以上一致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
}
- read ポリシーに違反すると、フィールドはnull 化されます(返される値は
nullですが、SQL の計算には引き続き使用できます)。実際のnullと拒否された値を区別することはできません。 - update ポリシーに違反すると、更新全体が拒否されます(
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 の条件を満たすだけで十分です。ルールがある場合は、少なくとも 1 つの @@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)とクエリビルダーによるアクセスは、ポリシーをバイパスします。ポリシープラグインのインストール中に生 SQL を許可するには、new PolicyPlugin({ dangerouslyAllowRawSql: true })を使用してください(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 — ポリシープラグインのオプション