{"id":13879226,"url":"https://github.com/kuroda/multi_tenancy","last_synced_at":"2025-04-22T08:14:42.517Z","repository":{"id":138710694,"uuid":"118687850","full_name":"kuroda/multi_tenancy","owner":"kuroda","description":"Sample multi-tenant Rails application using PostgreSQL's Row Level Security (RLS)","archived":false,"fork":false,"pushed_at":"2018-10-27T15:38:28.000Z","size":54,"stargazers_count":36,"open_issues_count":2,"forks_count":4,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-20T00:53:05.497Z","etag":null,"topics":["multi-tenant-applications","postgresql","rails"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kuroda.png","metadata":{"files":{"readme":"README.ja.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-01-24T00:16:52.000Z","updated_at":"2022-04-24T14:59:15.000Z","dependencies_parsed_at":null,"dependency_job_id":"c0400e03-64d7-45aa-90ba-8cd937686032","html_url":"https://github.com/kuroda/multi_tenancy","commit_stats":{"total_commits":28,"total_committers":1,"mean_commits":28.0,"dds":0.0,"last_synced_commit":"5e3047de094e966185376df7b472e41ca0c1d404"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kuroda%2Fmulti_tenancy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kuroda%2Fmulti_tenancy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kuroda%2Fmulti_tenancy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kuroda%2Fmulti_tenancy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kuroda","download_url":"https://codeload.github.com/kuroda/multi_tenancy/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249834785,"owners_count":21331988,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["multi-tenant-applications","postgresql","rails"],"created_at":"2024-08-06T08:02:14.176Z","updated_at":"2025-04-20T00:53:10.673Z","avatar_url":"https://github.com/kuroda.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# multi_tenancy\n\n## 概要\n\nPostgreSQL の Row Level Security (RLS) を利用したマルチテナント Rails アプリケーションのサンプルです。\n\nこのアプリケーションでは、各テナントが複数のユーザーを抱え、それぞれのユーザーが複数の記事（articles）を持っています。\n\nあるテナントのユーザーとしてこのアプリケーションにログインした場合、別のテナントのユーザーや記事は参照できません。\nまた、そのユーザーは同じテナントの別のユーザーの記事を参照できますが、挿入・更新・削除はできません。\n\nこのようなアクセス制限をアプリケーション側に委ねると、情報漏えいや情報喪失を招くバグが混入しやすくなります。\nしかし、データベース側で制限をすれば、その種のバグが起こりえなくなります。\n\nなお、PostgreSQL ではマルチテナントシステムの構築に [Citus](https://www.citusdata.com/product/community) という拡張機能がしばしば使われますが、このサンプルでは使用していません。Citus の主目的はシステムの「スケーラビリティ」の向上です。Citus は「シャーディング」という技法により巨大なデータベースを複数の PostgreSQL インスタンスに分散させます。\n\nCitusの採用にはさまざまな利点がありますが、アプリケーションの開発者は「シャーディング」の仕組みをよく理解してプログラミングをしないと、思わぬエラーやパフォーマンスの低下を引き起こします。\n\n作ろうとしている Web アプリケーションの規模がシャーディングを利用するほどに巨大にならないことがわかっているならば、このサンプルのように PostgreSQL の標準機能だけを用いてマルチテナントシステムを構築できます。\n\n## 動作環境\n\n* Ubuntu Server 16.04\n* PostgreSQL 10\n* Ruby 2.4\n* Ruby on Rails 5.1.4\n\n## マイグレーションヘルパーメソッド `add_policies`\n\n### `users` テーブルのマイグレーションスクリプト\n\n```\n  def up\n    create_table :users do |t|\n      t.references :tenant, null: false\n      t.string :name, null: false\n\n      t.timestamps\n    end\n\n    add_foreign_key :users, :tenants\n\n    add_policies(\"users\")\n  end\n```\n\n### `articles` テーブルのマイグレーションスクリプト\n\n```\n    create_table :articles do |t|\n      t.references :tenant, null: false\n      t.references :user, null: false\n      t.string :title, null: false\n      t.text :body\n      t.integer :pages, null: false, default: 0\n\n      t.timestamps\n    end\n\n    add_foreign_key :articles, :tenants\n    add_foreign_key :articles, :users\n\n    add_policies(\n      \"articles\",\n      [\n        { table_name: \"users\", foreign_key: \"user_id\" }\n      ]\n    )\n  end\n```\n\n## Row Level Security について\n\nRow Level Security （以下、「RLS」と呼ぶ）は、2016 年 1 月にリリースされた PostgreSQL 9.5 で導入された機能です。\n日本語では「行単位セキュリティ」と訳されることもあります。\n\n従来の `GRANT` 文ではテーブル単位でのアクセス制御しかできませんでしたが、RLS を利用すれば行単位でのアクセス制御が可能となります。\n\n`CREATE POLICY` 文によってセキュリティポリシーを定義することにより RLS が設定されます。次の例では `alice` ユーザーに対して `articles` テーブルの一部に対する参照を許可しています。\n\n```\nCREATE POLICY alice_policy ON articles\n  FOR SELECT\n  TO alice\n  USING (user_id = (SELECT id FROM users WHERE name = 'alice'))\n```\n\n行への参照が許可される条件は `USING` 節の中に記述されます。上の例では、副問い合わせを用いて `users` テーブルから `name` 列の値が `'alice'` である行の `id` 列の値を取得し、それと `user_id` 列の値が一致する `articles` テーブルの行への参照を許しています。\n\nただし、初期状態では RLS は無効になっています。`ALTER TABLE` 文を用いてテーブル単位で有効にする必要があります。\n\n```\nALTER TABLE articles ENABLE ROW LEVEL SECURITY\n```\n\nいま `users` テーブルに次のような行が保存されているとします。\n\n```\nid | name\n---+-----\n1  | alice\n2  | bob\n3  | charlie\n```\n\nそして、 `articles` テーブルに次のような行が保存されているとします。\n\n```\nid | user_id | title\n---+---------|------\n1  | 1       | W\n2  | 1       | X\n3  | 2       | Y\n4  | 3       | Z\n```\n\n以上のような状態で、`alice` ユーザーが次のようなクエリを発行したとします。\n\n```\nSELECT title FROM articles\n```\n\nすると、RLS が無効であれば 4 個の行が検索されることになりますが、RLS が有効となっていれば 2 個しか検索されません。\n\n## `current_setting` 関数\n\nこの Rails アプリケーションでは、マルチテナントシステムにおけるセキュリティを高めるために PostgreSQL の `current_setting` 関数を利用しています。\n\nPostgreSQL では `SET` 文を用いてカスタム変数に値をセットできます。次の例では、変数 `foo.bar` に `'X'` という文字列をセットしています。カスタム変数名には必ずドットを含む必要があります。\n\n```\nSET foo.bar = 'X'\n```\n\nカスタム変数の値は `current_setting` 関数を用いて取得できます。\n\n```\ncurrent_setting('foo.bar')\n```\n\nこの関数は `CREATE POLICY` 文の `USING` 節の中でも使えます。次の例をご覧ください。\n\n```\nCREATE POLICY tenant_policy ON articles\n  FOR SELECT\n  TO CURRENT_USER\n  USING (tenant_id::text = current_setting('session.current_tenant_id'))\n```\n\nここでは `articles` テーブルに `tenant_id` という整数型の列があると仮定しています。\n\n以上のような状態で、あるユーザーが次のようなクエリを発行したとします。\n\n```\nSET session.current_tenant_id = '1';\nSELECT title FROM articles;\n```\n\nすると、このユーザーには `tenant_id` の値が 1 である行だけが見えることになります。\n\n## `WITH CHECK` 節\n\n`CREATE POLICY` 文に `WITH CHECK` 節を加えると、行の挿入・更新時にレコードがある条件を満たすかどうかが確認されます。条件を満たさない `INSERT` 文や `UPDATE` 文が発行されると、エラーとなります。\n\n次の例をご覧ください。\n\n```\nCREATE POLICY tenant_policy ON articles\n  FOR SELECT, INSERT, UPDATE, DELETE\n  TO CURRENT_USER\n  USING (tenant_id::text = current_setting('session.current_tenant_id'))\n  WITH CHECK (\n    SELECT EXISTS (\n      SELECT id FROM users\n      WHERE id = articles.user_id\n      AND tenant_id::text = current_setting('session.current_tenant_id')\n    )\n  )\n```\n\n上記のようにセキュリティポリシーが設定されると、`articles` テーブルに対する行の挿入・更新時に `users` テーブルで次のふたつの条件を満たす行の有無が調べられ、なければエラーとなります。\n\n* `id` 列の値と *k* が等しい。\n* `tenant_id` 列の値を文字列に変換すると、カスタム変数 `session.current_tenant_id` の値に等しくなる。\n\nただし、*k* は `articles` テーブルに挿入・更新される行の `user_id` 列の値とします。\n\n## RLS の利用上の注意\n\n`SUPERUSER` および `BYPASSRLS` 属性を持つロールに対して RLS は常に無効です。また、テーブルのオーナーに対して、RLS はデフォルトで無効ですが、次のように `ALTER TABLE` 文を用いて有効化できます。\n\n```\nALTER TABLE articles FORCE ROW LEVEL SECURITY\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkuroda%2Fmulti_tenancy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkuroda%2Fmulti_tenancy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkuroda%2Fmulti_tenancy/lists"}