実務・技術解説
RBAC(ロールベースアクセス制御)の基本:AIコードで陥りやすい権限設計の落とし穴
RBACの概要と、AIで作ったコードで権限設計が壊れやすい理由を解説。本番化前に確認すべきロール設計のチェックポイントを紹介します。
あるスタートアップが、vibe codingで作った社内ツールを本番公開して2日後のことだ。一般ユーザーとして登録したアカウントで、他のユーザーのデータを編集できることが発覚した。「管理者専用」と書いてあるボタンは表示されていなかったが、APIのエンドポイントURLを直接叩くと、誰でも全ユーザーのデータを書き換えられる状態だった。
これはRBACの実装が「フロントエンドだけ」にしか存在しなかったことが原因だ。
RBACとは何か
RBAC(Role-Based Access Control、ロールベースアクセス制御)は「役割(ロール)ごとに、できることを決める」仕組みだ。
管理者は全データを閲覧・編集できる。一般ユーザーは自分のデータだけ見られる。ゲストは参照のみ。こういったルールをシステム全体で一貫して強制するのがRBACだ。
反対に「誰が何にアクセスできるかがコードのあちこちに散らばっている」状態は、穴が生まれやすい。AIが生成するコードはまさにこの状態になりやすい。
AIコードが権限設計で失敗するパターン
パターン1:フロントエンドのみの権限制御
最も多いのがこれだ。ボタンを非表示にする、メニューを出し分ける——これ自体は正しいが、APIの呼び出し元がサーバーサイドでチェックされていなければ意味がない。
// ❌ よくある実装:UIだけ制御して終わり
{user.role === 'admin' && (
<button onClick={deleteUser}>ユーザーを削除</button>
)}
// このボタンが非表示でも、APIを直接叩けば誰でも削除できる
// fetch('/api/users/123', { method: 'DELETE' })
ブラウザのDevToolsを開けばAPIのURLは簡単にわかる。URLさえわかれば、curlやPostmanで直接叩ける。UIに頼った権限制御は「鍵がかかっていないのに鍵穴が見えない」だけの状態だ。
パターン2:SupabaseのRLS未設定
SupabaseはクライアントからDBに直接アクセスできる仕組みがある。これが便利な反面、RLS(Row Level Security)が設定されていないと、フロントエンドのコードさえあれば誰でも全テーブルを読み書きできる。
AIが「動くコードを作る」ために、RLSを無効にしたまま開発を進めることがある。本番公開のタイミングでこれが残っていると、全データが公開状態だ。
パターン3:管理者チェックがJavaScriptの条件分岐だけ
// ❌ 危険:roleがフロントから改ざんできる場合
const isAdmin = localStorage.getItem('role') === 'admin'
if (isAdmin) {
// 管理者専用処理
}
localStorage の値はユーザーが自由に書き換えられる。DevToolsのConsoleで localStorage.setItem('role', 'admin') を実行するだけで管理者になれてしまう。
正しいRBACの実装:Next.js + Supabase
ステップ1:DBにロールカラムを持たせる
-- usersテーブルにroleカラムを追加
ALTER TABLE users ADD COLUMN role text NOT NULL DEFAULT 'user'
CHECK (role IN ('admin', 'user', 'guest'));
-- 最初の管理者を設定(service_role権限で実行)
UPDATE users SET role = 'admin' WHERE id = 'xxxxx';
ロールの変更はサーバーサイドのみから行うように制限する。ユーザー自身が自分のロールを変更できないよう、RLSポリシーを設定する。
ステップ2:SupabaseのRLSポリシーを設定する
-- usersテーブルのRLSを有効化
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
-- 自分のレコードだけ読める
CREATE POLICY "users_select_own"
ON users FOR SELECT
USING (auth.uid() = id);
-- 管理者は全レコードを読める
CREATE POLICY "admins_select_all"
ON users FOR SELECT
USING (
EXISTS (
SELECT 1 FROM users
WHERE id = auth.uid() AND role = 'admin'
)
);
-- roleカラムは自分では変更できない
CREATE POLICY "users_update_own_except_role"
ON users FOR UPDATE
USING (auth.uid() = id)
WITH CHECK (auth.uid() = id AND role = (SELECT role FROM users WHERE id = auth.uid()));
重要なのは、ロールの判定をDB側(Supabase)で行っている点だ。クライアントがどんな値を送ってきても、DBレベルで弾かれる。
ステップ3:Next.jsのミドルウェアでルート保護
// middleware.ts
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(req: NextRequest) {
const res = NextResponse.next()
const supabase = createMiddlewareClient({ req, res })
const {
data: { session },
} = await supabase.auth.getSession()
// 未ログインユーザーを/loginにリダイレクト
if (!session && req.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', req.url))
}
return res
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*'],
}
ステップ4:APIルートでサーバーサイドのロール検証
// app/api/admin/users/route.ts
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'
export async function DELETE(req: Request) {
const supabase = createRouteHandlerClient({ cookies })
// セッション確認
const { data: { session } } = await supabase.auth.getSession()
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// サーバーサイドでロール確認(DBから直接取得)
const { data: currentUser } = await supabase
.from('users')
.select('role')
.eq('id', session.user.id)
.single()
if (currentUser?.role !== 'admin') {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
// 管理者のみここに到達できる
// ... 削除処理
}
UIとAPIの両方でチェックする二重構造が重要だ。UIのチェックは「ユーザー体験のため」、APIのチェックは「セキュリティのため」という役割分担だ。
MVPで使う3種類のロール
ほとんどのMVPは3種類のロールで十分対応できる。
| ロール | できること | 実装上の注意 |
|---|---|---|
| public(未ログイン) | 公開コンテンツの閲覧のみ | RLSで匿名アクセスを明示的に制限 |
| user(一般ユーザー) | 自分のデータのCRUD、公開機能の利用 | auth.uid() = user_id で自己所有チェック |
| admin(管理者) | 全データのCRUD、設定変更 | ロール変更はservice_roleのみ可能にする |
4つ目、5つ目のロール(モデレーター、プレミアムユーザー等)は、公開後に実際に必要になってから追加するのが現実的だ。最初から複雑な権限設計を作ると実装が膨らみ、バグの温床になる。
AIコードの権限を監査する5つの質問
本番公開前に、以下を必ず確認する。
- 「管理者専用API」は本当にサーバーサイドで検証しているか? — APIルートのコードを開いて、ロールチェックの処理があるか確認する。
- SupabaseのRLSはすべてのテーブルで有効か? —
pg_tablesを確認するか、ダッシュボードの「Authentication → Policies」で確認する。 - ロールの値はDBから取得しているか? —
localStorageや URLパラメータからロールを判断していないか確認する。 - service role keyはサーバーサイドのみで使われているか? —
NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEYという変数名があれば即刻危険信号だ。 - 未ログインでアクセスできるURLはどこか? — Next.jsのミドルウェアで保護されているルートの一覧を確認する。
権限設計のミスは「動いているから気づかない」タイプの問題だ。ユーザーから「他の人のデータが見えます」と報告されるまで発覚しないことが多い。AIで作ったコードは特にこの部分が弱いため、本番公開前に意識的に確認するプロセスが必要だ。