PostgreSQL Row-Level Security 入門 : 行単位でデータアクセスを制御する方法

この記事は個人ブログと同じ内容です


はじめに

「このデータはあのユーザーには見せたくない」「マルチテナントアプリでユーザーには自分の会社のデータだけ表示したい」 データベースを運用していると、こんな悩みが出てくることはありませんか? PostgreSQL の行レベルセキュリティ (Row Level Security, RLS) は、そんな課題を解決するための機能です。

行レベルセキュリティ (RLS) とは?

行レベルセキュリティ (RLS) とは、データベースのテーブルに対して、どの行をどのユーザーが表示・操作できるかを細かく制御する仕組みです。 例えば、以下のようなケースで非常に有効です。

  • 個人のプライバシー保護: ユーザーは自分の個人情報のみを編集できるようにする。
  • ユーザーロールに応じたデータ表示: 営業担当者は自分の担当顧客のデータのみ、マネージャーは部署全体の顧客データを閲覧できるようにする。
  • マルチテナントアプリケーション: 複数の顧客企業が同じデータベースを共有しているが、各企業は自社のデータにしかアクセスできないようにする。

基本的な使い方

実際に PostgreSQL で RLS の基本的な使い方を見ていきましょう。

サンプルテーブルとユーザーを作成

まず、サンプルとなるテーブルと、動作確認用のユーザーを作成します。 お手元の環境で psql などを使って以下の SQL を実行してください。

-- テスト用のユーザーを作成 (パスワードも設定)
CREATE ROLE taro LOGIN PASSWORD 'taro_password';
CREATE ROLE jiro LOGIN PASSWORD 'jiro_password';
CREATE ROLE saburo LOGIN PASSWORD 'saburo_password';

-- サンプルテーブル: 社員情報
CREATE TABLE employees (
  id SERIAL PRIMARY KEY,
  name VARCHAR(50) NOT NULL,
  department TEXT NOT NULL,
  age INTEGER
);

-- テーブルへの基本的な操作権限をユーザーに付与
GRANT SELECT, INSERT, UPDATE, DELETE ON employees TO taro, jiro, saburo;

-- サンプルデータの挿入
INSERT INTO employees (name, department, age) VALUES
  ('taro', '営業', 30),
  ('jiro', '開発', 25),
  ('saburo', '開発', 28);

これで、employees テーブルと3人のユーザー (taro, jiro, saburo) が準備できました。 現時点では、どのユーザーも全ての社員情報を閲覧・操作できてしまいます。 試しに taro として SELECT してみましょう。

-- taro にロールを切り替えて確認
SET ROLE taro;
SELECT * FROM employees;
-- id | name  | department | age
-- ----+-------+------------+-----
--   1 | taro  | 営業       |  30
--   2 | jiro  | 開発       |  25
--   3 | saburo | 開発       |  28

-- 元のロールに戻る
RESET ROLE;

このように、現時点では taro も他のユーザーのデータを全て見ることができてしまいます。

RLS を有効化

employees テーブルに対して RLS を有効化します。

ALTER TABLE employees ENABLE ROW LEVEL SECURITY;

ENABLE ROW LEVEL SECURITY を実行しただけでは、まだ何もポリシーが定義されていないため、デフォルトでは全ての行へのアクセスが拒否されます。 試しに再度 taro として SELECT してみましょう。

-- taro にロールを切り替えて確認
SET ROLE taro;
SELECT * FROM employees;
-- id | name  | department | age
-- ----+-------+------------+-----
-- (0 rows)

-- 元のロールに戻る
RESET ROLE;

期待通り、taro は何もデータを閲覧できません。

ポリシーの作成

RLS の中心となるのは「ポリシー (Policy)」です。ポリシーとは、特定のテーブルに対して、あるユーザーが特定の操作 (SELECT, INSERT, UPDATE, DELETE) を行う際に、どの行が対象となるかを定義するルールのことです。

ポリシーには主に以下の2つの要素があります。

  • USING 句: どの行が表示可能か、または操作の対象となるかを定義する条件式を指定します。SELECT, UPDATE, DELETE 操作で主に使われます。この条件を満たさない行は、まるで存在しないかのように扱われます。
  • WITH CHECK 句: 新しい行が INSERT される際や、既存の行が UPDATE される際に、その行が満たすべき条件を指定します。この条件を満たさないデータ操作はエラーとなります。

これらのポリシーをテーブルに適用することで、データベースエンジンが自動的に行レベルでのアクセス制御を行ってくれます。

最初のポリシーとして、「社員は自分の情報のみを閲覧できる」というルールを作成してみましょう。 現在のデータベースユーザー名 (current_user) と社員名 (name カラム) を比較して、同じ場合のみ行を表示できるようにします。

CREATE POLICY user_can_see_own_data
ON employees
FOR SELECT
USING (name = current_user); -- name カラムの値が現在のユーザー名と一致する行のみ
  • ON employees: employees テーブルに対するポリシーです
  • FOR SELECT: SELECT 操作に対するポリシーです
  • USING (name = current_user): name カラムの値が、現在接続しているユーザー名 (current_user) と一致する行のみを対象とします

動作確認

ポリシーを作成したら、再度 taro として SELECT してみましょう。

-- taro にロールを切り替えて確認
SET ROLE taro;
SELECT * FROM employees;
-- id | name | department | age
-- ----+------+------------+-----
--  1 | taro | 営業       |  30
-- (1 row)

-- 元のロールに戻る
RESET ROLE;

taro は自分の情報のみを閲覧できるようになりました。 jiro と saburo も同様に確認してみましょう。

-- jiro にロールを切り替えて確認
SET ROLE jiro;
SELECT * FROM employees;
-- id | name | department | age
-- ----+------+------------+-----
--  2 | jiro | 開発       |  25
-- (1 row)

-- saburo にロールを切り替えて確認
SET ROLE saburo;
SELECT * FROM employees;
-- id | name  | department | age
-- ----+-------+------------+-----
--  3 | saburo | 開発       |  28
-- (1 row)

-- 元のロールに戻る
RESET ROLE;

これで、各ユーザーは自分の情報のみを閲覧できるようになりました。

マルチテナントで活用する

RLS はマルチテナントアプリケーションで非常に有効です。 例えば、複数のテナントが同じデータベースを共有している場合、各テナントのユーザーには自テナントのデータのみを表示させることができます。 検証用に以下のテーブルとデータを用意します。

-- マルチテナント用のテーブルとデータを作成
CREATE TABLE tenants (
  id serial PRIMARY KEY,
  name text NOT NULL
);

CREATE TABLE posts (
  id SERIAL PRIMARY KEY,
  tenant_id INTEGER REFERENCES tenants(id),
  title VARCHAR(100) NOT NULL,
  content TEXT NOT NULL
);

-- テーブルへの基本的な操作権限をユーザーに付与
GRANT SELECT, INSERT, UPDATE, DELETE ON tenants TO taro, jiro, saburo;
GRANT SELECT, INSERT, UPDATE, DELETE ON posts TO taro, jiro, saburo;

-- サンプルデータの挿入
INSERT INTO tenants (name) VALUES
  ('Tenant A'),
  ('Tenant B');

INSERT INTO posts (tenant_id, title, content) VALUES
  (1, 'Post 1 for Tenant A', 'Content of post 1 for tenant A'),
  (1, 'Post 2 for Tenant A', 'Content of post 2 for tenant A'),
  (2, 'Post 1 for Tenant B', 'Content of post 1 for tenant B');

RLS ポリシー内で現在のユーザーがどのテナントに所属しているかを特定する必要があります。 これにはいくつかの方法がありますが、ここではセッションごとの設定パラメータ(current_setting)を利用する方法を紹介します。 まず RLS を有効化しポリシーを作成します。

ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_can_see_own_posts
ON posts
FOR SELECT
USING (tenant_id = current_setting('app.current_tenant_id')::INTEGER);

current_setting('app.current_tenant_id')::INTEGER は、セッション変数から tenant_id を取得し、posts テーブルの tenant_id と比較します。 このポリシーにより、現在のセッションで設定された tenant_id と一致する行のみが表示されるようになります。

次に、アプリケーション側でユーザーが認証された際に、そのユーザーの tenant_id をセッション変数として設定します。

-- 例えば、Tenant A のユーザーがログインした場合
SET app.current_tenant_id = '1';

これで、Tenant A のユーザーが posts テーブルを SELECT すると、Tenant A の投稿のみが表示されます。

SET ROLE taro; -- Tenant A のユーザーとしてロールを切り替え
SELECT * FROM posts;
-- id | tenant_id | title                     | content
-- ----+------------+---------------------------+-----------------------------------
--  1 |          1 | Post 1 for Tenant A       | Content of post 1 for tenant A
--  2 |          1 | Post 2 for Tenant A       | Content of post 2 for tenant A
-- (2 rows)
  
-- 元のロールに戻る
RESET ROLE;

このように、RLS とセッション変数を組み合わせることで、マルチテナントアプリケーションでも各テナントのデータを安全に分離することができます。

FORCE ROW LEVEL SECURITY での強制

デフォルトでは、スーパーユーザーやテーブルの所有者、 BYPASSRLS 権限を持つユーザーは RLS ポリシーを無視して全ての行にアクセスできます。 しかし FORCE ROW LEVEL SECURITY を使用すると、これらのユーザーでも RLS ポリシーを強制的に適用することができます。

ALTER TABLE posts FORCE ROW LEVEL SECURITY;

これにより、スーパーユーザーや所有者であっても、RLS ポリシーに従って行のアクセスが制限されます。

まとめ

PostgreSQL の行レベルセキュリティ (RLS) を使うことで、データベースのテーブルに対して細かいアクセス制御を行うことができます。 アプリケーションのバグや実装ミスによって、意図せず他のユーザーやテナントのデータが見えてしまう、といった事故を根本から防げることや、アクセス制御のロジックをデータベース側で一貫して制御できることもメリットです。 よりセキュアなアプリケーションの構築を目指す際には、RLS の活用を検討してみてください。