Auth and Permissions
plasma には 2 つの auth 境界があります。リクエスト境界 は HTTP 呼び出しごとに 1 回、呼び出し側を識別します。行境界 — table ごとの述語のペア — は、この呼び出し側が読み書きできる具体的な行を決定します。
リクエスト auth — SyncHandlerOptions.auth
Section titled “リクエスト auth — SyncHandlerOptions.auth”sync ハンドラは /sync/* へのすべてのリクエストで auth 関数を呼び出します。受け入れて mutator にコンテキストを渡すには { ok: true, ... } を、拒否するには { ok: false, reason } を返します:
createSyncHandler({ schema, mutators, executor, schemaVersion: SCHEMA_VERSION, auth: async (req) => { const token = req.headers.get("authorization")?.replace("Bearer ", "") if (!token) return { ok: false, reason: "missing token" }
const user = await verifyToken(token) if (!user) return { ok: false, reason: "invalid token" }
return { ok: true, clientGroupID: user.id, clientID: req.headers.get("x-client") ?? "unknown", ctx: { userId: user.id, role: user.role }, } },})返されるオブジェクトの形状:
clientGroupID— 論理的なインストール。クライアントが自身のclientGroupIDとして送ったものと一致しなければなりません。一致しない場合、sync ハンドラは 400 を返します。clientID— タブごとの id。クライアントが送ったものと一致しなければなりません。ctx— 後続のすべての mutator 呼び出しに第 3 パラメータとして、またすべてのauth.read/auth.write述語に渡されます。
行 auth — TableOptions.auth
Section titled “行 auth — TableOptions.auth”すべての table は行ごとの述語を宣言できます:
const todos = table("todos", { id: id(), title: text(), userId: ref(() => users.id),}, { auth: { read: (ctx, row) => row.userId === ctx.userId, write: (ctx, row) => row.userId === ctx.userId, },})read(ctx, row)— pull 時に呼び出し側がrowを見られるかを決定します。false→ その行は pull レスポンスから除外されます。write(ctx, row)— 呼び出し側がrowを insert / update / delete できるかを決定します。false→ mutator がPlasmaAuthorizationErrorを throw し、トランザクションがロールバックし、_plasma_client_mutations.last_mutation_idは依然として進むため、クライアントの outbox は汚染エントリを破棄します。
両方の述語は同期的です — async はなく、外部呼び出しもありません。auth() ハンドラでキャッシュした ctx のフィールドを使ってください。
row の中身
Section titled “row の中身”insert の場合、row はデフォルト適用後の具体的な行です。update の場合、write は 新しい 行(更新後)で呼ばれます。delete の場合、write は 現在の 行(削除前)で呼ばれます。
pull 時の read では、row は change log に存在する行(_plasma_changes.value をデコードしたもの)です。auth は pull サーバーで実行されるため、呼び出し側が read で拒否された行を目にすることはありません — その行は静かに除外されます。
同型性の落とし穴
Section titled “同型性の落とし穴”auth.read / auth.write は TableOptions に宣言され、これは同型 schema の一部です。つまり、同じ述語が技術的には両エンジンで実行されます。実際には:
- サーバー: plasma はこれらをすべての push / pull で実行します。これが権威的なチェックです。
- クライアント: plasma はデフォルトではこれらを実行しません。クライアントは optimistic に動作します。自身の
auth.writeが拒否するような行を書くと、ローカルの mutation は IDB に着地し、サーバーが push 時に拒否します。onErrorハンドラが発火し、次のrebuildOptimisticでローカルビューが取り消されます。
その理由: クライアントで行レベルの auth を強制すると、すべての mutation で述語を実行し、ユーザーが正当なことをしている場合でもコードパスのコストを払うことになります。optimistic パスを通してまれな例外をサーバーに拒否させる方が安価です。
blob 読み取り auth — コンテンツアドレス指定
Section titled “blob 読み取り auth — コンテンツアドレス指定”file() column は auth.read を直接使いません。代わりに、サーバーは _plasma_blob_refs(hash, table, row_id, column) — 各 blob ハッシュに言及するすべての行の逆引きインデックス — を追跡します。
クライアントが GET /sync/blob/:hash をリクエストすると、サーバーは:
- そのハッシュに対するすべての ref をフェッチします(最大
readAuthMaxRefs、デフォルト 128)。 - 各 ref について、参照している行をフェッチし、その table の
auth.read(ctx, row)を実行します。 - 参照している行の いずれか が
auth.readを通過すれば、blob が提供されます。
これは重複排除を扱います: 2 人のユーザーがそれぞれ同じ画像を添付すると、両者で共有される 1 つの blob になり、それぞれ自身の行がアクセスを許可するため読み取れます。
ユーザーが blob ハッシュを 知っている 場合、自身の行から参照する({ hash: ..., size, mime } を指すノートを挿入する)ことで、自らに read を許可できます。これは重複排除 vs 失効のトレードオフです: plasma は重複排除を最適化します。機密性の高い blob は、SyncHandlerOptions.blobs が table ごとのアダプターをサポートしたら、table ごとにより厳格な auth を持つ別の storageRef バケット下に置くべきです。
reason 付きで拒否する
Section titled “reason 付きで拒否する”auth が { ok: false, reason } を返すと、ワイヤー上で 401 になります — クライアントでは status: 401 を持つ push-http / pull-http として現れます。mutator 本体内の PlasmaAuthorizationError は client.onError には 届きません: push は依然として { ok: true } を返し、_plasma_client_mutations.last_mutation_id は依然として進み(そのため汚染が破棄される)、失敗した mutation の行は次の rebuildOptimistic で視覚的に取り消されます。2 つの異なる失敗モードには 2 つの異なる可視シグネチャがあります。
次に読むべきもの
Section titled “次に読むべきもの”- Concepts / clientID vs clientGroupID —
authが返す識別子 - Files and blobs — 完全な blob ストレージフロー
- Concepts / Push, Pull, Rebase — 各フェーズで
authがどこに位置するか