目次
「データベースのアクセス制御、行単位まで設定できていますか?」
多くのシステムでは、テーブル単位やカラム単位での権限管理は実装されていても、行(レコード)単位でのアクセス制御はアプリケーション側で複雑なロジックを組んでいるケースが少なくありません。しかし、この方法では実装コストが高く、セキュリティホールも生まれやすくなります。
そこで注目されているのが、**RLS(Row Level Security)**です。PostgreSQLに標準搭載されているこの機能を使えば、データベース層で行レベルのアクセス制御を実現でき、SaaS型サービスのテナント分離や部署ごとのデータ閲覧制限などを安全かつシンプルに実装できます。
本記事では、RLSの基本概念から具体的なポリシー設定方法、実務で使える実装パターンまで、中小企業の技術者の方にもわかりやすく解説します。
RLS(Row Level Security)とは?
RLS(Row Level Security)は、データベースのテーブル内で行(レコード)単位でアクセス制御を行う仕組みです。PostgreSQLがバージョン9.5(2016年)から標準搭載している機能で、「このユーザーはこの行だけを見られる」といった細かいレベルでのセキュリティ設定が可能になります。
従来のアクセス制御との違い
従来のデータベースアクセス制御は、テーブル全体やカラム単位での制御が中心でした。例えば「usersテーブルにSELECT権限を付与」すると、テーブル内のすべてのレコードにアクセスできてしまいます。
行レベルの制御が必要な場合、これまではアプリケーション側で以下のようなコードを毎回実装していました:
-- すべてのクエリにWHERE句を追加
SELECT * FROM orders WHERE user_id = current_user_id;
この方法には以下の課題がありました:
- 実装漏れのリスク:WHERE句を忘れると情報漏洩につながる
- 保守性の低下:アクセス制御ロジックがコード全体に散らばる
- テストの複雑化:すべてのクエリで権限チェックをテストする必要がある
RLSを使えば、これらの課題をデータベース層で一元的に解決できます。
RLSが必要とされる場面
RLSは特に以下のような場面で威力を発揮します:
マルチテナント型SaaSサービス
複数の企業が同じデータベースを共有する環境では、テナント間のデータ分離が最重要課題です。RLSを使えば「ログイン中のテナントIDと一致する行のみアクセス可能」と設定するだけで、確実なデータ分離を実現できます。
組織階層に応じたアクセス制御
部署、チーム、プロジェクト単位でデータへのアクセスを制限したい場合にも有効です。営業部のユーザーは営業データのみ、開発部のユーザーは開発データのみを閲覧できるようにするといった制御が、ポリシー設定だけで実現できます。
個人情報保護が求められるシステム
医療、金融、人事システムなど、個人情報を扱うシステムでは「担当者のみがその患者・顧客のデータを見られる」といった厳格なアクセス制御が必要です。RLSはこうした要件に対する標準的なソリューションとなります。
RLSの最大の役割は、アプリケーションコードに依存せず、データベース層でデータの安全性を担保できることです。万が一アプリケーション側にバグがあっても、データベースレベルで不正なアクセスをブロックできます。
RLSの仕組み:ポリシーによるアクセス制御
RLSの核となるのが「ポリシー」という概念です。ポリシーとは、「どのユーザーが、どの条件で、どの行にアクセスできるか」を定義したルールのことです。
ポリシーの基本構造
ポリシーは、SQL文の実行時に自動的に適用される条件式(WHERE句のようなもの)として機能します。基本構造は以下の要素で構成されます:
- ポリシー名:ポリシーを識別する名前
- 対象テーブル:ポリシーを適用するテーブル
- 対象コマンド:SELECT、INSERT、UPDATE、DELETEのいずれか
- 対象ロール:ポリシーを適用するユーザーやロール
- 条件式:どの行にアクセスできるかを定義(USING句とWITH CHECK句)
例えば、「一般ユーザーは自分が作成した記事のみ閲覧できる」というポリシーは以下のように表現されます:
CREATE POLICY user_articles ON articles
FOR SELECT
USING (author_id = current_user_id());
このポリシーが設定されていると、ユーザーが SELECT * FROM articles; を実行した際、PostgreSQLは自動的に WHERE author_id = current_user_id() という条件を追加して実行します。
RLSが動作する流れ
クエリ実行時の内部処理は以下の順序で行われます:
- クエリの実行:ユーザーがSQLクエリを実行
- RLS有効化の確認:対象テーブルでRLSが有効化されているかチェック
- 適用可能なポリシーの検索:実行ユーザーのロールと実行コマンドに適用されるポリシーを検索
- ポリシーの評価:該当するポリシーのUSING句に定義された条件式を評価(複数のポリシーはOR条件で結合)
- 結果の返却:ポリシーの条件を満たす行のみが結果として返される
この一連の処理はすべて自動的に行われるため、アプリケーション側で特別な処理を実装する必要はありません。
重要な注意点:PostgreSQLのスーパーユーザーとテーブルの所有者は、デフォルトでRLSの制約を受けません。これらのユーザーにもRLSを適用したい場合は、FORCE ROW LEVEL SECURITYオプションを使用します。
ポリシーの種類と使い分け
RLSでは、データベース操作の種類ごとに異なるポリシーを設定できます。
SELECTポリシー(読み取り制御)
どの行を閲覧できるかを制御します。最も基本的で、よく使われるポリシーです。
CREATE POLICY select_own_data ON users
FOR SELECT
USING (id = current_user_id());
INSERTポリシー(挿入制御)
新しい行を挿入する際の制御を行います。WITH CHECK句を使用して、挿入されるデータが条件を満たすかをチェックします。
CREATE POLICY insert_own_tenant ON documents
FOR INSERT
WITH CHECK (tenant_id = current_tenant_id());
UPDATEポリシー(更新制御)
USING句で「どの行を更新できるか」、WITH CHECK句で「更新後のデータが条件を満たすか」を制御できます。
CREATE POLICY update_own_posts ON posts
FOR UPDATE
USING (author_id = current_user_id())
WITH CHECK (author_id = current_user_id());
DELETEポリシー(削除制御)
行の削除を制御します。USING句のみを使用します。
CREATE POLICY delete_own_comments ON comments
FOR DELETE
USING (user_id = current_user_id());
USING句とWITH CHECK句の違い
ポリシーを設定する際に混乱しやすいのが、この2つの句の使い分けです。
USING句:既存データへのアクセス条件
「どの行にアクセスできるか」を定義します。SELECT、UPDATE、DELETEの操作で評価されます。
USING (department_id = current_user_department())
-- 意味:自分の部署のデータのみアクセス可能
WITH CHECK句:新規・更新データの妥当性チェック
「挿入または更新されるデータが条件を満たすか」をチェックします。INSERT、UPDATEの操作で評価されます。
WITH CHECK (department_id = current_user_department())
-- 意味:自分の部署以外のdepartment_idは設定できない
実務では、「閲覧は広く、編集は制限」といった柔軟な権限設計が可能です:
-- 閲覧は全プロジェクト可能だが、編集は管理者のみ
CREATE POLICY view_all_select ON projects
FOR SELECT
USING (true);
CREATE POLICY edit_own_update ON projects
FOR UPDATE
USING (manager_id = current_user_id())
WITH CHECK (manager_id = current_user_id());
WITH CHECK句を省略すると、USING句の条件がWITH CHECK句としても使用されます。明示的に指定することが推奨されます。
PostgreSQLでRLSを設定する手順
実際にPostgreSQLでRLSを設定する具体的な手順を見ていきます。
RLSの有効化
RLSを使用するには、まずテーブルに対してRLS機能を有効化する必要があります。
-- 社員テーブルの作成
CREATE TABLE employees (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100),
department_id INTEGER,
salary INTEGER,
created_by INTEGER
);
-- RLSの有効化
ALTER TABLE employees ENABLE ROW LEVEL SECURITY;
ポリシーを設定しない限り、一般ユーザーはこのテーブルから一切データを取得できなくなります。これは「ポリシーが明示的に許可しない限りアクセスを拒否する」という安全性優先の設計です。
RLSの状態を確認するには:
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public';
rowsecurityカラムがtrueになっていれば、RLSが有効化されています。
ポリシーの作成
RLSを有効化したら、次にポリシーを作成します。
基本構文
CREATE POLICY ポリシー名 ON テーブル名
[FOR { ALL | SELECT | INSERT | UPDATE | DELETE }]
[TO { ロール名 | PUBLIC }]
[USING (条件式)]
[WITH CHECK (条件式)];
実例1:ユーザーは自分のデータのみ閲覧可能
CREATE POLICY view_own_data ON employees
FOR SELECT
USING (created_by = current_user_id());
実例2:部署ごとのアクセス制御
-- セッション変数から現在の部署IDを取得する関数
CREATE OR REPLACE FUNCTION current_department_id()
RETURNS INTEGER AS $$
SELECT NULLIF(current_setting('app.department_id', TRUE), '')::INTEGER;
$$ LANGUAGE SQL STABLE;
-- 同じ部署のデータのみアクセス可能
CREATE POLICY department_isolation ON employees
FOR ALL
USING (department_id = current_department_id())
WITH CHECK (department_id = current_department_id());
実例3:管理者はすべてのデータにアクセス可能
CREATE POLICY admin_all_access ON employees
FOR ALL
TO admin_role
USING (true)
WITH CHECK (true);
実例4:複数条件を組み合わせたポリシー
CREATE POLICY flexible_access ON employees
FOR SELECT
USING (
created_by = current_user_id() -- 自分が作成したデータ
OR department_id = current_department_id() -- または同じ部署のデータ
OR current_user_is_admin() -- または管理者
);
作成したポリシーは以下のクエリで確認できます:
SELECT schemaname, tablename, policyname, roles, cmd, qual
FROM pg_policies
WHERE tablename = 'employees';
ロールとセッション変数の設定
RLSを効果的に活用するには、ロール(ユーザー)とセッション変数を適切に設定する必要があります。
ロールの作成と権限付与
-- 一般ユーザーロールの作成
CREATE ROLE general_user LOGIN PASSWORD 'password123';
-- テーブルへの基本権限を付与(RLSとは別に必要)
GRANT SELECT, INSERT, UPDATE, DELETE ON employees TO general_user;
セッション変数の設定
アプリケーションから接続する際、セッション変数を設定することでポリシーで使用する情報を渡します:
-- 接続時に現在のユーザーIDを設定
SET app.current_user_id = '123';
-- 現在の部署IDを設定
SET app.department_id = '5';
アプリケーションコードからの設定例(Node.js):
// 接続後にセッション変数を設定
await client.query("SET app.current_user_id = $1", [userId]);
await client.query("SET app.tenant_id = $1", [tenantId]);
// その後の全クエリでRLSが適用される
const result = await client.query("SELECT * FROM employees");
動作確認の方法
RLSが正しく動作しているかを確認しましょう。
-- テストデータの準備(スーパーユーザーとして実行)
INSERT INTO employees (name, email, department_id, created_by) VALUES
('田中太郎', 'tanaka@example.com', 1, 1),
('佐藤花子', 'sato@example.com', 1, 1),
('鈴木一郎', 'suzuki@example.com', 2, 2);
-- ユーザー1として接続
SET app.current_user_id = '1';
SET ROLE general_user;
SELECT * FROM employees;
-- 結果:created_by = 1のデータのみ表示される
-- ユーザー2として接続
SET app.current_user_id = '2';
SELECT * FROM employees;
-- 結果:created_by = 2のデータのみ表示される
実務で使えるRLSポリシーの設定パターン
実際のビジネスシーンで活用できる具体的なポリシー設定パターンを紹介します。
パターン1:マルチテナント型SaaSのデータ分離
複数の企業(テナント)が同じデータベースを共有する環境での実装例です。
-- テナントIDによる完全なデータ分離
CREATE POLICY tenant_isolation ON documents
FOR ALL
USING (tenant_id = current_setting('app.tenant_id')::int)
WITH CHECK (tenant_id = current_setting('app.tenant_id')::int);
このポリシーにより、各テナントは自分のデータのみにアクセスでき、他のテナントのデータは一切見えなくなります。アプリケーション側で毎回テナントIDの条件を書く必要がなくなり、実装漏れによる情報漏洩リスクを大幅に削減できます。
パターン2:組織階層に応じたアクセス制御
役職や部署に応じて段階的にアクセス範囲を変える実装例です。
-- 一般社員は自分のデータ、管理職は部署全体、経営層は全社
CREATE POLICY hierarchical_access ON sales_data
FOR SELECT
USING (
CASE current_setting('app.user_role')
WHEN 'executive' THEN true -- 経営層は全データ
WHEN 'manager' THEN department_id = current_setting('app.department_id')::int -- 管理職は部署内
ELSE user_id = current_setting('app.current_user_id')::int -- 一般社員は自分のみ
END
);
パターン3:時限的なアクセス権限
プロジェクトメンバーや期間限定の共有など、動的に変化するアクセス権限の実装例です。
-- 共有設定テーブルを用意
CREATE TABLE data_sharing (
resource_id INT,
shared_with_user_id INT,
expires_at TIMESTAMP
);
-- 作成者または共有されたユーザーがアクセス可能
CREATE POLICY flexible_sharing ON documents
FOR SELECT
USING (
created_by = current_setting('app.current_user_id')::int
OR id IN (
SELECT resource_id FROM data_sharing
WHERE shared_with_user_id = current_setting('app.current_user_id')::int
AND (expires_at IS NULL OR expires_at > NOW())
)
);
パターン4:公開・非公開の制御
一般ユーザーは公開データのみ、作成者は自分のすべてのデータにアクセスできる実装例です。
-- 公開データまたは自分が作成したデータを閲覧可能
CREATE POLICY public_or_own ON articles
FOR SELECT
USING (
is_public = true
OR author_id = current_setting('app.current_user_id')::int
);
-- 編集は作成者のみ
CREATE POLICY edit_own ON articles
FOR UPDATE
USING (author_id = current_setting('app.current_user_id')::int)
WITH CHECK (author_id = current_setting('app.current_user_id')::int);
RLS導入時の注意点とトラブルシューティング
RLSを実際に導入する際に遭遇しやすい問題と対策を解説します。
パフォーマンスへの影響と対策
RLSを有効化すると、すべてのクエリに対してポリシーの条件が追加されるため、パフォーマンスへの影響を考慮する必要があります。
インデックス設計の重要性
ポリシーで使用する列には必ず適切なインデックスを設定しましょう:
-- ポリシーで使用する列にインデックスを作成
CREATE INDEX idx_employees_created_by ON employees(created_by);
CREATE INDEX idx_employees_department_id ON employees(department_id);
CREATE INDEX idx_employees_tenant_id ON employees(tenant_id);
実行計画の確認
ポリシーが適用された状態でのクエリ実行計画を確認することが重要です:
SET ROLE general_user;
SET app.current_user_id = '1';
EXPLAIN ANALYZE
SELECT * FROM employees WHERE department_id = 1;
確認ポイント:
- Seq Scan(全表スキャン)が発生していないか
- Index Scanが使用されているか
- 実行時間が許容範囲内か
パフォーマンス改善のヒント
- ポリシーの条件を簡潔にする(複雑なサブクエリは避ける)
- 大量データの場合、テーブルパーティショニングと組み合わせる
- マテリアライズドビューの活用を検討する
ポリシーが効かない場合のチェックリスト
「RLSを設定したのにすべてのデータが見えてしまう」という問題の確認手順です。
チェック1:RLSが有効化されているか
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public' AND tablename = 'employees';
チェック2:ポリシーが作成されているか
SELECT tablename, policyname, cmd, qual
FROM pg_policies
WHERE tablename = 'employees';
チェック3:セッション変数が正しく設定されているか
-- 現在のセッション変数を確認
SELECT current_setting('app.current_user_id', true);
SELECT current_setting('app.tenant_id', true);
チェック4:ロールに基本権限が付与されているか
SELECT grantee, privilege_type
FROM information_schema.role_table_grants
WHERE table_name = 'employees';
チェック5:スーパーユーザーで確認していないか
スーパーユーザーとテーブル所有者は、デフォルトでRLSをバイパスします。動作確認は必ず一般ユーザーで行いましょう。
-- テスト用ユーザーで確認
SET ROLE general_user;
SELECT * FROM employees;
アプリケーション側での考慮事項
RLSを実務で活用する際は、アプリケーション側の実装も重要です。
接続プールでの注意点
データベース接続プールを使用している場合、接続がリサイクルされるため、セッション変数が意図せず保持される可能性があります:
// 良い例:トランザクション内で設定し、確実にリセット
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query("SET LOCAL app.current_user_id = $1", [userId]);
// クエリ実行
await client.query('COMMIT');
} catch (e) {
await client.query('ROLLBACK');
throw e;
} finally {
client.release();
}
エラーハンドリング
RLSによってアクセスが拒否された場合、適切にエラーを処理する必要があります:
try {
const result = await client.query(
"UPDATE employees SET salary = $1 WHERE id = $2",
[newSalary, employeeId]
);
// RLSで更新が拒否された場合、rowCountが0になる
if (result.rowCount === 0) {
throw new Error('アクセス権限がないか、データが存在しません');
}
} catch (error) {
logger.warn('RLS violation attempt', {
userId: currentUserId,
targetId: employeeId
});
throw new Error('この操作を実行する権限がありません');
}
RLSを活用したサービス設計
RLSを活用することで、セキュリティとコード量削減の両立が可能になります。
Supabaseでの活用
Supabaseは、PostgreSQLをベースにしたBaaSで、RLSを非常に使いやすい形で提供しています。
Supabaseの特徴
- GUIでポリシー設定が可能
- 認証機能との統合(auth.uid()関数でログインユーザーのIDを取得)
- リアルタイム機能との統合
-- Supabaseでは認証情報を簡単に利用できる
CREATE POLICY "Users can view their own data"
ON employees
FOR SELECT
USING (auth.uid() = user_id);
クライアント側の実装も簡単です:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
// ログイン後、自動的にRLSが適用される
const { data } = await supabase
.from('employees')
.select('*')
// ログインユーザーがアクセス可能なデータのみ取得される
マルチテナントSaaSでのメリット
複数の企業が同じデータベースを共有するSaaSアプリケーションでは、RLSが非常に有効です:
-- テナントIDによるデータ分離
CREATE POLICY tenant_isolation ON all_tables
FOR ALL
USING (tenant_id = current_setting('app.tenant_id')::uuid);
メリット:
- アプリケーションコードでテナントIDの条件を書く必要がない
- 開発者のミスによるデータ漏洩リスクを大幅に削減
- すべてのテーブルに一貫したセキュリティポリシーを適用できる
APIエンドポイントの簡素化
RLSを使用することで、APIのコードが大幅にシンプルになります:
// RLS なしの場合
app.get('/api/employees', async (req, res) => {
const userId = req.user.id;
const departmentId = req.user.departmentId;
// アクセス制御をアプリケーション側で実装
const employees = await db.query(`
SELECT * FROM employees
WHERE department_id = $1 AND created_by = $2
`, [departmentId, userId]);
res.json(employees);
});
// RLS ありの場合
app.get('/api/employees', async (req, res) => {
// セッション変数を設定するだけ
await db.query("SET app.current_user_id = $1", [req.user.id]);
// シンプルなクエリでOK
const employees = await db.query("SELECT * FROM employees");
res.json(employees);
});
中小企業がRLSを導入する際のポイント
実際に中小企業が導入する際には、技術だけでなく運用面や組織面での配慮も重要です。
自社に合ったアクセス制御の設計
RLSは強力な機能ですが、過度に複雑にすると運用が困難になります。シンプルな設計から始めましょう。
パターン1:作成者のみアクセス可能(最もシンプル)
CREATE POLICY simple_owner_policy ON documents
FOR ALL
USING (created_by = current_setting('app.current_user_id')::int);
適用例:営業担当者が自分の顧客情報のみ閲覧、個人のタスク管理、自分の経費申請のみ編集
パターン2:部署単位でのアクセス制御
CREATE POLICY department_policy ON projects
FOR ALL
USING (department_id = current_setting('app.department_id')::int);
適用例:部署ごとのプロジェクト管理、部門別の在庫管理、チーム単位の情報共有
現場の運用に合わせた設計
- 現状の業務フローを把握する
- 例外処理を考慮する(特定の案件だけは他部署と共有など)
- 将来の拡張性を考える
段階的な導入の進め方
いきなりすべてのテーブルにRLSを適用するのではなく、段階的に導入することでリスクを最小化できます。
フェーズ1:影響範囲の小さいテーブルで検証(1〜2週間)
まずは新しく作成するテーブルや、影響範囲の小さいテーブルから始めます。例えば社内の日報テーブルなど。
検証項目:
- アプリケーションからの接続が正常に動作するか
- パフォーマンスに問題がないか
- ユーザーから操作上の問題が報告されないか
フェーズ2:重要度の高いテーブルに適用(1〜2ヶ月)
顧客情報や売上データなど、セキュリティ要件の高いテーブルに適用します。既存データへの影響を最小限にするため、最初は緩めのポリシーから始め、動作確認後に徐々に制限を厳しくします。
フェーズ3:全社展開と最適化(3〜6ヶ月)
すべてのテーブルにRLSを適用し、パフォーマンスチューニング、ポリシーの見直しと最適化、運用マニュアルの整備を行います。
外部の専門家に相談するタイミング
以下のような状況では、外部の専門家に相談することを検討しましょう:
- パフォーマンス問題が解決できない:クエリが遅くなった、インデックスを追加しても改善しない
- 複雑な権限要件がある:部署、役職、プロジェクト、地域など多次元の権限管理が必要
- 既存システムへの影響が大きい:大量のデータを持つテーブルへの適用、複雑なクエリを多用しているシステム
- セキュリティ要件が厳しい:個人情報保護法への対応、業界固有の規制、監査対応が必要
私たちHarmonic Societyでは、中小企業の「ちょうどいい」デジタル化を支援しています。RLSの導入についても、過度に複雑にせず、現場の運用に合わせた実践的な設計と導入支援を行っています。
まとめ
RLS(Row Level Security)は、PostgreSQLが提供する強力な行レベルのアクセス制御機能です。データベース層でセキュリティを担保できるため、アプリケーションコードの実装漏れによる情報漏洩リスクを大幅に削減できます。
RLSの主なメリット:
- アプリケーションコードに依存しない確実なセキュリティ
- 開発生産性の向上(アクセス制御ロジックの一元管理)
- マルチテナントSaaSやB2Bシステムでの柔軟なデータ分離
- 保守性の向上(セキュリティポリシーの変更が容易)
導入のポイント:
- シンプルな設計から始める
- 段階的に導入してリスクを最小化
- インデックス設計とパフォーマンス確認を忘れずに
- 現場の業務フローに合わせた柔軟な設計
RLSを活用することで、セキュアで保守性の高いシステムを、短期間・低コストで構築できます。まずは小さなテーブルから試してみて、徐々に適用範囲を広げていくことをお勧めします。
