0
0

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

Last updated at Posted at 2024-04-01

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"のような大雑把な設定しかできません。

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
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0