mcpField
Z
zenstack-access-control

zenstack-access-control

github.com/zenstackhqセキュリティ3463 回インストール1 日前に更新
リポジトリ

zenstack-access-control について

ZenStack ユーザー向けのアクセス制御スキルです。ZModel のポリシーをランタイムプラグインで ORM 層に適用する方法を説明し、ユーザーの紐付け、フィールド単位のポリシー、更新後の検査を扱います。

主な機能

  1. 01ZModel で許可・拒否ポリシーを宣言
  2. 02リクエストごとに現在のユーザーを紐付け
  3. 03フィールド単位の読み取り・更新ルール
  4. 04更新後のデータ状態を検査
  5. 05リレーションを使ったポリシー式

活用シーン

  1. 01ユーザー自身のレコードにアクセスを制限
  2. 02匿名ユーザーからフィールドを隠す
  3. 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/ に含まれています。

コメント