本文へ移動
Database · Authorization at the data layer

データベースと認可境界Database

松葉は社外の取引先もログインして使うため、「他社・他取引先のファイルが見えてしまう」事態は絶対に避けなければなりません。多くのシステムはアプリのコードだけでアクセス制御をしますが、それだとコードに 1 か所抜けがあった瞬間に漏えいします。松葉は最後の砦として、データベース自身が「あなたが見てよい行か」を判定する仕組み(行レベルセキュリティ、RLS)を入れ、DB へ触る入口も 3 つの経路に限定しました。

実装済み: 3 経路・RLS・追記専用監査の土台 今後: 各業務処理を 3 経路の上に載せる
01

DB へ触る入口は 3 つだけThree routes & RLS

データベースへ触る方法を、用途別の 3 つの経路に限定しました。それぞれが、データベース側で別々の「立場(ロール)」を名乗ります。立場ごとに「見える行・触れる行」が RLS で決まるため、アプリのどこに抜けがあっても、立場に許されない行は返ってきません。

図 01 · 利用者の立場 → 3 経路 → データベースが最終判定(RLS)
利用者の立場3 つの経路(入口)DB ロール認証ユーザ社内・社外(ログイン済)dbFor通常の全操作authenticated自分の Box のみ共有リンク受取人ログイン不要dbForShareLink受取専用anonそのリンクの対象だけバッチ・Workerシステム自身dbAsSystem定期処理・連携service_roleRLS を越える権限PostgresRLS行ごとに可否判定この 3 経路以外で DB に直接触ることは、コード品質チェックで禁止しています(認証システムの内部処理を除く)。アプリにバグがあっても、立場に許されない行は DB が返さない = 多層防御の最後の砦。

立場(ロール)の名乗り方: 各経路は、データベースとの 1 回のやり取りの中で「自分は今どの立場か」を宣言します。認証ユーザなら authenticated、共有リンク受取なら anon、システム処理なら service_role です。RLS は宣言された立場をもとに、行ごとに見えるかどうかを判定します。service_role だけは定期処理・連携のため RLS を越える権限を持つので、その経路は限られた場所からしか使えないようにしています。

このアプリでの例社外の取引先がログインしても、自分が参加している Box(共有の単位)のファイルしか見えません。仮にアプリ側のチェックに抜けがあって、別の Box のファイル ID を指定するリクエストが届いても、データベースが authenticated の立場で「あなたはその行を見られない」と判定し、データを返しません。

呼び方の注意: 松葉のこの仕組みは「マルチテナント」とは呼ばず、「Box 認可境界」と呼んでいます。会社単位ではなく、共有の単位である Box ごとに「見える・見えない」の境界を引いているためです。資料の中でも「Box 境界」「Box 越境(境界を越えてしまうこと)」という言い方で統一しています。

02

「見てよいか」を判定する部品Helpers & roles

RLS が行ごとに判定するとき、その根拠になる小さな判定関数を 8 種類用意しました。ログイン時に発行される証明書(トークン)の中身を読み取り、「この人は誰か」「どの Box でどの権限か」を返します。

判定関数何を判定するか
current_user_id今アクセスしているのは誰か(ユーザ ID)
current_user_box_idsその人が参加している Box の一覧
current_user_role_for_boxある Box でのその人の権限(4 ロールのどれか)
is_system_adminシステム管理者かどうか
is_internal_user社内ユーザかどうか(社外と区別)
current_share_link_id共有リンク経由のとき、どのリンクか
can_view_box_via_visibilityBox の公開設定上、その Box 自体を閲覧できるか
can_read_box_contents_via_visibilityBox の公開設定上、中のファイル・フォルダを閲覧できるか

Box の中での権限は 4 段階です。下にいくほどできることが増えます。これは認証システム(Better Auth)の仕組みを使い、Box を 1 つのテーブルで一元管理しています。

権限(ロール)できること
viewer(閲覧者)ファイルの閲覧・ダウンロード
uploader(投稿者)閲覧に加えて、ファイルのアップロード
editor(編集者)投稿に加えて、ファイル・フォルダの編集・削除
owner(管理者)編集に加えて、Box のメンバー管理・設定変更

社内と社外で入口が違います: 社内ユーザは会社の Google アカウントでログイン(@example.com 限定)。社外の取引先は招待制で、メールアドレス + パスワードに加えてワンタイムコード(OTP)による二段階認証を必須にしています。どちらの立場でも、上の RLS と 4 ロールによる制御は同じように効きます。

03

追記専用の監査記録Append-only audit

「誰が・いつ・何をしたか」の記録(監査ログ)は、後から書き換えたり消したりできては意味がありません。松葉の監査ログは、追記(新しい記録の追加)だけを許可し、更新・削除をデータベースの権限レベルで拒否しています。

図 02 · 監査ログは「追記」だけ許可、更新・削除は拒否
audit_logs監査記録テーブル複合キー (occurred_at, id)月ごとに自動分割追記(INSERT)○ 許可 — 新しい記録を足す更新(UPDATE)× 拒否削除(DELETE)× 拒否✕✕古い月の記録 → GCS へ退避長期保管・コスト最適化

三重の守り: ①更新・削除のポリシーを作らないことで RLS が拒否し、さらに②権限(GRANT)レベルでも更新・削除を取り消して二重に防ぎます。③記録は月ごとに自動分割し、古い月の分はストレージ(GCS)へ退避して、長期にわたり安全・低コストに保管します。記録は専用の書き込み関数を介さず、各処理が通常の追記として残すシンプルな方式です。

正直な注記: ここで説明した RLS・追記専用ルールは、データベースに対して人手で慎重に適用する運用にしています(自動適用はしません)。また現時点では、これらの「守りの仕組み」が先に組まれた段階で、実際にデータを読み書きする業務処理は今後の開発で載せていきます。土台としての守りを先に固めておくことが狙いです。