記事の内容
Saasの案件にあたってマルチテナント対応をする必要があり、色々と調べたことで程度知見が得られたので、記事に残そうと思いました。
RLSとは
そもそもキックオフ当初はRLS(行レベルセキュリティ)のことを聞いたことあるぐらいでした知りませんでした。
RLS(Row Level Security): 行レベルセキュリティ(Row Level Security)はデータの読み込み時にテーブルの行レベルでアクセスを制御する機能。マルチテナントのデータ操作を安全に扱う方法。
マルチテナントデータベース設計
以下の3パターンが考えられ、案件ではその中のMulti Tenants in 1 Databaseを採用しました。
1. Multi Tenants in 1 Database: 複数企業のデータを一つのデータベースで管理
2. 1 Tenant in 1 Database: テナントごとにデータベースを分けて管理。物理的なインスタンスは同じ。
3. 1 Tenant in 1 Instance: テナントごとにデータベースを分けて管理。物理的なインスタンスも分け企業ごとにデータベースインスタンスを作成。
Multi Tenants in 1 Databaseはすべてのテナントのデータを一つのインスタンス・DBで取り扱うため金銭的コストがかからず、マイグレーションも一度で済むため扱いやすいです。
しかし、ひとつのDBのためテナントをまたいで情報が漏れてしまう可能性があり、適切にwhere句を扱う必要があります。
ただ、ヒューマンエラー等により実装漏れが発生してしまう可能性がありそれを防ぐのがRLSです。
参考: https://times.hrbrain.co.jp/entry/postgresql-row-level-security
RLSを使用するためには以下を考慮する必要がある
-
RLSを使用できるDBを選定:行レベルセキュリティを扱うことができるDBを採用する必要があります。
-
ポリシーの設定:RLSを有効にするためにはテーブルの各行に対するSELECT, UPDATE, DELETEを制限するセキュリティポリシーを定義する必要があります。
tenant_level_security.tenant_idと行のtenant_idが等しい場合のみユーザに表示・変更を許可する。そのため、RLSを効かせたいテーブルに対してtenant_idカラムをもたせる必要があります。ポリシーは以下のような感じです。
api_server_staging=> SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual, with_check
api_server_staging-> FROM pg_policies
api_server_staging-> WHERE tablename = 'branches';
schemaname | tablename | policyname | permissive | roles | cmd | qual | with_check
------------+-----------+---------------+------------+----------+-----+----------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------
----
public | branches | tenant_policy | PERMISSIVE | {public} | ALL | (tenant_id = (NULLIF(current_setting('tenant_level_security.tenant_id'::text), ''::text))::bigint) | (tenant_id = (NULLIF(current_setting('tenant_level_security.tenant_id'::text), ''::text))::bigi
nt)
(1 row)
参考: https://www.postgresql.jp/docs/9.5/ddl-rowsecurity.html
-
role(ユーザ)の切り替え:PostgreSQLインストール時にデフォルトで作成されるpostgres(スーパーユーザ)はRLSをバイパスするためRLSを効かせるためにはDB接続ユーザを変更する必要があります。
実際の対応
ここまではRLSを使用するに当たっての前提知識で、ここからは案件ではどのように対応したかです。
案件での対応
前提:案件ではとある事情で3種類のクライアントが存在し、それぞれRLSの適用有無が異なっていました。
①master:マスター管理画面で操作するユーザ。Tenantをまたがった情報の取得を許可。
②admin:ユーザ管理画面で操作するユーザ。Tenantにまたがった情報の取得を禁止。RLS適用対象。
③mobileユーザ:mobileアプリで操作するユーザ。Tenantをまたがった情報の取得を許可。
クライアントによってRLSの適用を切り替える必要がありかなりネックでした。
-
RLSを使用できるDBを選定:PostgreSQLを採用しました。
-
ポリシーの設定:ポリシーについては、activerecord-tenant-level-securityを使用して設定しました。
RLSを効かせたいテーブルについては、以下のようにしてtenant_idと
create_policy
の記述をするだけでGemによってマイグレーション時にポリシーをつけてくれます。
class CreateBranches < ActiveRecord::Migration[7.0]
def change
create_table :branches, do |t|
t.references :tenant, null: false, foreign_key: true
t.timestamps
t.index [:tenant_id], unique: true
end
create_policy :branches # ←これでマイグレーション実行時ポリシーがつく。
end
end
tenant_level_security.tenant_idの値の設定について、今回の案件では前提にもあるように特殊でadminにのみRLSを適用させたいためApi::V1::Admin::AdminApplicationControllerのようなクラスを作成し、このクラス(コントローラ)を継承をしたコントローラーのアクションにてRLSを使用するようにしました。
また、activerecord-tenant-level-securityのReadmeよりactiverecord-multi-tenantを同時に使用することを推奨しているためこちらも使用しています。
Gemの簡単な解説
activerecord-tenant-level-security
→ポリシー作成を簡易化、DBでレコードのtenant_idとtenant_level_security.tenant_idを設定する。DB側でのRLSを実現するためのGem。
以下のように初期化する必要がある。
# activerecord-tenant-level-security用の初期化。存在しないtenant_idを設定。
TenantLevelSecurity.current_tenant_id { 0 }
activerecord-multi-tenant
→アプリケーション側ですべてのクエリにwhere句を自動で付与するGem。
以下のような記述でwhere句を適用させる。
class Site < ActiveRecord::Base
multi_tenant :customer #←これ
has_many :page_views
# ...
end
下の参考にも記載してあるとおり、片方の設定に頼るとライブラリにバグがあった際RLSが効かなくなってしまうため、案件でも2重で制御している。
参考:https://tech.smarthr.jp/entry/2022/02/15/202241
-
role(ユーザ)の切り替え:Postgres(super user)ではRLSがバイパスされてしまうため、別のユーザで接続する必要があります。
案件では「db_admin」roleを作成し、RLSが必要な際は「db_admin」で接続するようにしました。
db_adminの作成方法・GRANTは以下のコマンドでできます。
// db_adminユーザー作成(postgresユーザーで実行)
CREATE USER db_admin WITH PASSWORD 'password123';
// 既存テーブルに対して権限付与(postgresユーザーで実行)
GRANT ALL ON ALL TABLES IN SCHEMA public TO db_admin;
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO db_admin;
// これから作成されるテーブルに対して権限付与(postgresユーザーで実行)
alter default privileges for role postgres in schema public grant all privileges on tables to db_admin;
alter default privileges for role postgres in schema public grant all privileges on sequences to db_admin;
tenant_idを設定するのと同様にApi::V1::Admin::AdminApplicationController を継承したコントローラーのアクションではdb_adminで接続しています。
参考:https://www.postgresql.jp/document/8.1/html/sql-set-role.html
(Active recordのestablish_connectionではDBのpoolが足りずエラーになるため、connection errorとなるため不採用とした)
(before_actionでadminでAPIを叩く際にかならず呼ばれるようにする)
def set_db_admin_and_tenant_id
ActiveRecord::Base.connection.execute("SET ROLE 'db_admin';")
MultiTenant.current_tenant = @current_user.tenant_id #認証したユーザの所属するtenant_idを使用
# ここでwhere句で絞る際のtenant_idを設定。
# この処理移行multi_tenant :customerのように指定したモデルに対してのクエリに指定したtenant_idによるwhere句がつく。(activerecord-multi-tenant)
TenantLevelSecurity.current_tenant_id { @current_user.tenant_id }
# ここでDB側で絞る際のtenant_idを設定。
end
終わりに
今回の対応でRLSについて、少しは知見が深まりましたがまだまだベストプラクティスがわかってなく、特にDB接続ユーザの切り替えはSET ROLEでいいのか正解があまりわかってなく経験された方がいればぜひ教えていただきたいです。
参考にも載せましたがSmartHRさんの記事が大変参考になりました。