📝

GoでPostgreSQLのRLSを安全に扱うための軽量ライブラリ「mtc」を作った

に公開

この記事は ChatGPT を使って書かれた


最近、PostgreSQLのRow Level Security(RLS)をGoアプリケーションで安全に扱うためのライブラリ「mtc」を作りました。

この記事では、mtcを作った背景、設計の考え方、そして実際の使い方を紹介します。


背景

PostgreSQLのRLSは非常に強力なマルチテナント機能ですが、アプリケーション側の実装を間違えると逆に危険です。

特にGoのdatabase/sqlでは、内部でコネクションプールを管理しているため、SET app.tenant_id = 'tenant_a' のようなセッションスコープの変数を設定すると、別のリクエストにテナント情報が漏れてしまうという致命的な問題が起こります。

これを防ぐには:

  • トランザクションローカル(set_config(..., true))でのみ tenant_id を適用する
  • コネクションプール再利用時に確実に RESET する

この2つをアプリ側で徹底する必要があります。そこで作ったのが mtc(Multi-Tenant Connector)です。


コンセプト

mtcは、database/sql/driver.Connector を実装した極小ライブラリです。

ポイントはただ一つ:

トランザクション開始時にのみ set_config('app.tenant_id', $1, true) を実行する。

これにより、設定はトランザクション内に閉じ込められ、他のリクエストへ漏れることがありません。

また、コネクションプールから再利用される際は自動的に RESET app.tenant_id が呼ばれるので、常にクリーンな状態で再利用されます。


使い方

import (
    "context"
    "database/sql"
    _ "github.com/lib/pq"
    "github.com/mickamy/mtc"
)

func tenantIDFromContext(ctx context.Context) (string, error) {
    v := ctx.Value("tenant_id")
    if v == nil {
        return "", fmt.Errorf("no tenant_id in context")
    }
    return v.(string), nil
}

func main() {
    connector := mtc.New(&pq.Driver{},
        "postgres://user:pass@localhost/dbname?sslmode=disable",
        tenantIDFromContext,
    )

    db := sql.OpenDB(connector)
    ctx := context.WithValue(context.Background(), "tenant_id", "tenant_a")

    var id string
    _ = db.QueryRowContext(ctx, `SELECT current_setting('app.tenant_id', true)`).Scan(&id)
    fmt.Println(id) // => tenant_a
}

これだけで、トランザクション内で安全にRLSが効くようになります。


内部構造

✅ Connectでは設定しない

Connect()は物理コネクションを新規作成するときに呼ばれます。ここでSETすると、セッションスコープに残ってしまうため危険です。

✅ ResetSessionでRESETする

コネクションがプールから再利用されるたびに RESET app.tenant_id を実行します。これにより、前回のテナント情報が必ず消去されます。

✅ BeginTxでのみset_configする

トランザクション開始直後に SELECT set_config('app.tenant_id', $1, true) を実行し、そのトランザクション内でのみ有効になります。


PostgreSQL側のRLS設定例

ALTER TABLE employees ENABLE ROW LEVEL SECURITY;

CREATE POLICY employees_isolation
ON employees
USING (tenant_id = current_setting('app.tenant_id', true));

CREATE POLICY employees_write
ON employees FOR INSERT
WITH CHECK (tenant_id = current_setting('app.tenant_id', true));

このように設定すると、アプリケーションは tenant_id を指定するだけで安全にマルチテナントを実現できます。


設定名のカスタマイズ

app.tenant_id 以外の変数名を使いたい場合は WithSettingName() を使います。

connector := mtc.New(&pq.Driver{}, dsn, tenantIDFromContext,
    mtc.WithSettingName("app.customer_id"))

PostgreSQL側では current_setting('app.customer_id', true) に合わせるだけでOKです。


おわりに

mtcは「RLSを安全に扱うための最小単位」を目指しています。

  • コネクションプールとの整合性を保ちながらRLSを確実に適用
  • セッションスコープリークを完全に防止
  • 依存ゼロで軽量

PostgreSQLのマルチテナント運用をGoで行っている人にとって、地味だけど欠かせないピースになると思います。

👉 リポジトリはこちら:github.com/mickamy/mtc

Discussion