Row Level Security(RLS)は、PostgreSQLが提供する機能で、接続ユーザーに対して操作できる行を制限できる機能で、1テーブルに複数の顧客データが混在するマルチテナントSaaSでは必須と言えるものです。
こういうRLSポリシーを設定すると......
CREATE POLICY tenant_policy ON tickets
AS PERMISSIVE
FOR ALL
TO PUBLIC
USING (tenant_id::text = current_setting('tenant_level_security.tenant_id')) -- SELECT文等に追加される条件式
WITH CHECK (tenant_id::text = current_setting('tenant_level_security.tenant_id')) -- UPDATE文等に追加される条件式
SELECT文のWHERE句にtenant_id
のチェックが強制的に追加され、テナントIDの行しかクエリ結果に含まれなくなります(指定しなければクエリ結果が空になる)。そのため、「テナントAのユーザーに、テナントBのチケットが見えちゃった!」といった情報漏洩を防ぐことができます。
なお、上記のポリシーは activerecord-tenant-level-security で設定されるポリシーです。Railsではactiverecord-tenant-level-securityを使えば簡単にRLSを利用できます。
テナント横断でアクセスするには?
ここで、上記のポリシーで追加される条件式では、テナントIDは1つしか指定できません(指定しなければクエリ結果が空になる)。
USING (tenant_id::text = current_setting('tenant_level_security.tenant_id')) -- SELECT文等に追加される条件式
なので、ETLなどでテナント横断でクエリしたい場合に困ります。
そのため、PostgreSQLではBYPASSRLS
属性を付けることでRLSを無視してクエリできます。
CREATE ROLE etl_user WITH BYPASSRLS; -- RLSを無視できるユーザー
でもHerokuではBYPASSRLS
を設定できない!
しかし、HerokuではBYPASSRLS
が使えません。"Read-only", "Read and write"のような大雑把な設定しかできません。
解決法:ポリシーを書き換える
BYPASSRLSが使えないなら仕方ない、ポリシーを書き換えてなんとかするしかありません。
現在のユーザーが etl_read
の時のみテナントIDのチェックをしないようにします。
CREATE POLICY tenant_policy ON tickets
AS PERMISSIVE
FOR ALL
TO PUBLIC
USING (CURRENT_USER = 'etl_read' OR tenant_id = NULLIF(current_setting('tenant_level_security.tenant_id', true), '')::#{tenant_id_data_type})
WITH CHECK (CURRENT_USER = 'etl_read' OR tenant_id = NULLIF(current_setting('tenant_level_security.tenant_id', true), '')::#{tenant_id_data_type})
本当は CURRENT_USER = 'etl_read'
のようなby nameの指定はよくない(ユーザー名に依存しない方法にしたい)のですが、他に方法が思い浮かびませんでした!
activerecord-tenant-level-security にモンキーパッチを当てる
では、activerecord-tenant-level-securityで上記のポリシーを使うにはどうすればいいのか?TenantLevelSecurity::SchemaStatements::create_policy
でポリシーを設定しているので、それを書き換えればよろしい。
# config/initializers/tenant_level_security.rb
module TenantLevelSecurity::SchemaStatements
# TenantLevelSecurityにモンキーパッチして、ポリシーの条件式を変更
#
# 特定のユーザー(kickflow_read)でのみ全件取得可能にする
# 本来はユーザーのBypassRLS属性を設定すべきだが、herokuではそれができないため
def create_policy(table_name)
execute <<~SQL.squish
ALTER TABLE #{table_name} ENABLE ROW LEVEL SECURITY;
ALTER TABLE #{table_name} FORCE ROW LEVEL SECURITY;
SQL
tenant_id_data_type = get_tenant_id_data_type(table_name)
execute <<~SQL.squish
CREATE POLICY tenant_policy ON #{table_name}
AS PERMISSIVE
FOR ALL
TO PUBLIC
USING (CURRENT_USER = 'kickflow_read' OR tenant_id = NULLIF(current_setting('tenant_level_security.tenant_id', true), '')::#{tenant_id_data_type})
WITH CHECK (CURRENT_USER = 'kickflow_read' OR tenant_id = NULLIF(current_setting('tenant_level_security.tenant_id', true), '')::#{tenant_id_data_type})
SQL
end
end