プログラミング

RLS(Row Level Security)とは?PostgreSQLのポリシー設定をわかりやすく解説

目次

「データベースのアクセス制御、行単位まで設定できていますか?」

多くのシステムでは、テーブル単位やカラム単位での権限管理は実装されていても、行(レコード)単位でのアクセス制御はアプリケーション側で複雑なロジックを組んでいるケースが少なくありません。しかし、この方法では実装コストが高く、セキュリティホールも生まれやすくなります。

そこで注目されているのが、**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が動作する流れ

クエリ実行時の内部処理は以下の順序で行われます:

  1. クエリの実行:ユーザーがSQLクエリを実行
  2. RLS有効化の確認:対象テーブルでRLSが有効化されているかチェック
  3. 適用可能なポリシーの検索:実行ユーザーのロールと実行コマンドに適用されるポリシーを検索
  4. ポリシーの評価:該当するポリシーのUSING句に定義された条件式を評価(複数のポリシーはOR条件で結合)
  5. 結果の返却:ポリシーの条件を満たす行のみが結果として返される

この一連の処理はすべて自動的に行われるため、アプリケーション側で特別な処理を実装する必要はありません。

重要な注意点: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);

適用例:部署ごとのプロジェクト管理、部門別の在庫管理、チーム単位の情報共有

現場の運用に合わせた設計

  1. 現状の業務フローを把握する
  2. 例外処理を考慮する(特定の案件だけは他部署と共有など)
  3. 将来の拡張性を考える

段階的な導入の進め方

いきなりすべてのテーブルにRLSを適用するのではなく、段階的に導入することでリスクを最小化できます。

フェーズ1:影響範囲の小さいテーブルで検証(1〜2週間)

まずは新しく作成するテーブルや、影響範囲の小さいテーブルから始めます。例えば社内の日報テーブルなど。

検証項目:

  • アプリケーションからの接続が正常に動作するか
  • パフォーマンスに問題がないか
  • ユーザーから操作上の問題が報告されないか

フェーズ2:重要度の高いテーブルに適用(1〜2ヶ月)

顧客情報や売上データなど、セキュリティ要件の高いテーブルに適用します。既存データへの影響を最小限にするため、最初は緩めのポリシーから始め、動作確認後に徐々に制限を厳しくします。

フェーズ3:全社展開と最適化(3〜6ヶ月)

すべてのテーブルにRLSを適用し、パフォーマンスチューニング、ポリシーの見直しと最適化、運用マニュアルの整備を行います。

外部の専門家に相談するタイミング

以下のような状況では、外部の専門家に相談することを検討しましょう:

  1. パフォーマンス問題が解決できない:クエリが遅くなった、インデックスを追加しても改善しない
  2. 複雑な権限要件がある:部署、役職、プロジェクト、地域など多次元の権限管理が必要
  3. 既存システムへの影響が大きい:大量のデータを持つテーブルへの適用、複雑なクエリを多用しているシステム
  4. セキュリティ要件が厳しい:個人情報保護法への対応、業界固有の規制、監査対応が必要

私たちHarmonic Societyでは、中小企業の「ちょうどいい」デジタル化を支援しています。RLSの導入についても、過度に複雑にせず、現場の運用に合わせた実践的な設計と導入支援を行っています。

まとめ

RLS(Row Level Security)は、PostgreSQLが提供する強力な行レベルのアクセス制御機能です。データベース層でセキュリティを担保できるため、アプリケーションコードの実装漏れによる情報漏洩リスクを大幅に削減できます。

RLSの主なメリット

  • アプリケーションコードに依存しない確実なセキュリティ
  • 開発生産性の向上(アクセス制御ロジックの一元管理)
  • マルチテナントSaaSやB2Bシステムでの柔軟なデータ分離
  • 保守性の向上(セキュリティポリシーの変更が容易)

導入のポイント

  • シンプルな設計から始める
  • 段階的に導入してリスクを最小化
  • インデックス設計とパフォーマンス確認を忘れずに
  • 現場の業務フローに合わせた柔軟な設計

RLSを活用することで、セキュアで保守性の高いシステムを、短期間・低コストで構築できます。まずは小さなテーブルから試してみて、徐々に適用範囲を広げていくことをお勧めします。

師田 賢人

一橋大学商学部を卒業後、Accenture Japanに新卒入社し、ITコンサルタントとして大手企業のシステム導入・業務改善プロジェクトに従事。その後、Webエンジニアとしての実務経験を積み、2016年に独立。 独立後は、企業向けのWebシステム開発・業務効率化ツール構築を中心に、80件以上のプロジェクトを担当し、100社以上の企業と取引実績を持つ。技術領域ではブロックチェーン分野にも精通し、200名以上の専門家への取材・記事執筆を経験。 2023年にHarmonic Society株式会社を設立し、現在はAI駆動のWebサイト制作・業務システム開発・自動化ソリューションを提供。 中小企業から教育機関まで、幅広いクライアントのDXを支援している。

ちょっとした業務の悩みも、気軽にご相談ください。

コメントを残す