コンテンツにスキップ

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 述語に渡されます。

すべての 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 のフィールドを使ってください。

insert の場合、row はデフォルト適用後の具体的な行です。update の場合、write新しい 行(更新後)で呼ばれます。delete の場合、write現在の 行(削除前)で呼ばれます。

pull 時の read では、row は change log に存在する行(_plasma_changes.value をデコードしたもの)です。auth は pull サーバーで実行されるため、呼び出し側が read で拒否された行を目にすることはありません — その行は静かに除外されます。

auth.read / auth.writeTableOptions に宣言され、これは同型 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 をリクエストすると、サーバーは:

  1. そのハッシュに対するすべての ref をフェッチします(最大 readAuthMaxRefs、デフォルト 128)。
  2. 各 ref について、参照している行をフェッチし、その table の auth.read(ctx, row) を実行します。
  3. 参照している行の いずれかauth.read を通過すれば、blob が提供されます。

これは重複排除を扱います: 2 人のユーザーがそれぞれ同じ画像を添付すると、両者で共有される 1 つの blob になり、それぞれ自身の行がアクセスを許可するため読み取れます。

ユーザーが blob ハッシュを 知っている 場合、自身の行から参照する({ hash: ..., size, mime } を指すノートを挿入する)ことで、自らに read を許可できます。これは重複排除 vs 失効のトレードオフです: plasma は重複排除を最適化します。機密性の高い blob は、SyncHandlerOptions.blobs が table ごとのアダプターをサポートしたら、table ごとにより厳格な auth を持つ別の storageRef バケット下に置くべきです。

auth{ ok: false, reason } を返すと、ワイヤー上で 401 になります — クライアントでは status: 401 を持つ push-http / pull-http として現れます。mutator 本体内の PlasmaAuthorizationErrorclient.onError には 届きません: push は依然として { ok: true } を返し、_plasma_client_mutations.last_mutation_id は依然として進み(そのため汚染が破棄される)、失敗した mutation の行は次の rebuildOptimistic で視覚的に取り消されます。2 つの異なる失敗モードには 2 つの異なる可視シグネチャがあります。