Heroku環境でBypass RLSを実現する方法(activerecord-tenant-level-securityにモンキーパッチす

2024/04/15に公開

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を利用できます。

https://github.com/kufu/activerecord-tenant-level-security

テナント横断でアクセスするには?

ここで、上記のポリシーで追加される条件式では、テナント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"のような大雑把な設定しかできません。

image.png

解決法:ポリシーを書き換える

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

Discussion