本文へ移動
runlot

RLS とユーザーの識別

RLS をオンにすると、すべての env.db ステートメントはコードに入ってきた主体として実行されます。ログイン中のリクエストはそのユーザーとして実行されるので、コードに身元を渡さなくても runlot_auth.uid() を使う行レベルセキュリティのポリシーが絞り込みます。

ユーザーテーブルはあなたのものです

ユーザーテーブルはプロジェクトのデータベースの runlot_auth スキーマにあります。通常のテーブルと同じように結合して使えます。

select p.id, p.title, u.email
from posts p
join runlot_auth.users u on u.id = p.owner_id
order by p.id desc
limit 20;

マイグレーションからの外部キー

アプリがまだ一度もリクエストを受けていない新しいプロジェクトでも、マイグレーションから runlot_auth.users を直接参照できます。

-- migrations/0002_posts.sql
create table posts (
  id bigserial primary key,
  owner_id uuid not null references runlot_auth.users(id) on delete cascade,
  title text not null
);

未適用のマイグレーションに runlot_auth が含まれていると、runlot pg migrate と runlot deploy は同じ接続で先にスキーマをインストールし、auth schema ensured と表示します。プロジェクトのログイン機能がオンである必要があり、オフの場合はマイグレーションが止まり、その理由を表示します。

RLS をオンにする

RLS は既定でオフで、そのとき env.db はどこでもプロジェクトのオーナーとして実行されます。runlot.json でオンにすると runlot deploy が適用します。

{
  "auth": { "rls": true }
}

CLI や、ダッシュボードのアプリログインタブにある RLS スイッチでもオンにできます。

runlot auth set rls on
runlot auth status   # RLS On と表示されます

ステートメントは誰として実行されるか

RLS がオンのとき、プラットフォームはコードへの入り方からデータベースのロールを選びます。コードが身元を指定しないので、他人になりすますことはできません。

入口env.db の実行主体
ログイン中の env.auth セッション (Cookie または Bearer) がある fetchそのユーザー (runlot_authenticated)。runlot_auth.uid() はユーザーの id
セッションがない、または無効な fetch匿名 (runlot_anon)
scheduled (cron)オーナー
queueオーナー
Durable Object のアラームオーナー
email (受信メール)匿名 (runlot_anon)
ctx.waitUntil とストリーミングの本文それを始めたリクエストのロール
マイグレーション、runlot pg shell、runlot pg executeオーナー

開くまでは閉じています

RLS がオンのとき、アプリのユーザーが触れられるテーブルは、あなたが意図して開いたものだけです。

テーブルログインユーザーや匿名ユーザーが受け取る結果
行レベルセキュリティなし42501 permission denied
行レベルセキュリティあり、ポリシーなし行なし。書き込みは拒否されます
行レベルセキュリティとポリシーありポリシーが許す範囲

マイグレーションの後、runlot pg migrate と runlot deploy は権限を同期し、まだ閉じているテーブルをすべて表示します。

  RLS grants synced: 1 table open to app users
  drafts has no row level security — closed to app users

runlot_auth 自体がアプリのユーザーに開かれることはありません。ユーザー情報は env.auth で読んでください。

ポリシー付きのマイグレーション

-- migrations/0003_posts_rls.sql
create table posts (
  id bigserial primary key,
  owner_id uuid not null default runlot_auth.uid() references runlot_auth.users(id) on delete cascade,
  title text not null,
  created_at timestamptz not null default now()
);
alter table posts enable row level security;

-- 各ユーザーは自分の記事だけを読み書きします。
create policy own_posts on posts
  using (owner_id = runlot_auth.uid())
  with check (owner_id = runlot_auth.uid());

-- ログインしていない訪問者もすべての記事を読めます。非公開にするならこの行を外してください。
create policy public_read on posts for select to runlot_anon using (true);

default runlot_auth.uid() が作成者を埋めるので、insert で渡す必要はありません。

export default {
  async fetch(request, env) {
    if (request.method === "POST") {
      const { title } = await request.json();
      await env.db.exec("insert into posts (title) values ($1)", [title]);
    }
    return Response.json(await env.db.exec("select id, title from posts order by id desc"));
  },
  async scheduled(event, env) {
    // オーナーとして実行され、すべての行が見えます。
    await env.db.exec("delete from posts where created_at < now() - interval '1 year'");
  },
};

コードのどこにもユーザー id は出てきません。fetch からのすべてのステートメントをポリシーが絞り込みます。

関数の定義は次のとおりです。

create or replace function runlot_auth.uid() returns uuid language sql stable as $$
  select (nullif(current_setting('request.jwt.claims', true), '')::json->>'sub')::uuid
$$;

request.jwt.claims は PostgREST や Supabase が使うものと同じ設定なので、そちら向けに書いたポリシーがそのまま動きます。

このページの目次