EcAuthDocs

Row-Level Security (RLS) 導入分析

マルチテナント分離における Row Level Security の適用方針と EF Core での実装案。

概要

このドキュメントでは、EcAuth IdentityProviderにおけるSQL Server Row-Level Security (RLS) の導入可否について分析する。

現在のマルチテナント実装

アーキテクチャ

HTTP Request
    ↓
TenantMiddleware (ホスト名からテナント特定)
    ↓
ITenantService.SetTenant(tenantName)
    ↓
EcAuthDbContext (EF Core グローバルクエリフィルター適用)
    ↓
SQL Server

実装詳細

TenantMiddleware

リクエストのホスト名からテナント名を抽出し、ITenantServiceに設定する。

// Middlewares/TenantMiddleware.cs
public async Task InvokeAsync(HttpContext context, ITenantService tenantService)
{
    var host = context.Request.Host.Host;
    var tenantName = ExtractTenantNameFromHost(host);
    tenantService.SetTenant(finalTenantName);
    await _next(context);
}

EF Core グローバルクエリフィルター

EcAuthDbContextで各エンティティにテナントフィルターを適用。

// Models/EcAuthDbContext.cs
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Organization>()
        .HasQueryFilter(o => o.TenantName == _tenantService.TenantName);

    modelBuilder.Entity<EcAuthUser>()
        .HasQueryFilter(u => u.Organization != null &&
                            u.Organization.TenantName == _tenantService.TenantName);

    modelBuilder.Entity<ExternalIdpMapping>()
        .HasQueryFilter(m => m.EcAuthUser != null &&
                            m.EcAuthUser.Organization != null &&
                            m.EcAuthUser.Organization.TenantName == _tenantService.TenantName);

    modelBuilder.Entity<AuthorizationCode>()
        .HasQueryFilter(ac => ac.EcAuthUser != null &&
                             ac.EcAuthUser.Organization != null &&
                             ac.EcAuthUser.Organization.TenantName == _tenantService.TenantName);

    modelBuilder.Entity<AccessToken>()
        .HasQueryFilter(at => at.EcAuthUser != null &&
                             at.EcAuthUser.Organization != null &&
                             at.EcAuthUser.Organization.TenantName == _tenantService.TenantName);

    modelBuilder.Entity<ExternalIdpToken>()
        .HasQueryFilter(eit => eit.EcAuthUser != null &&
                              eit.EcAuthUser.Organization != null &&
                              eit.EcAuthUser.Organization.TenantName == _tenantService.TenantName);
}

対象エンティティ(6テーブル)

エンティティ フィルター方式
Organization 直接TenantNameでフィルター
EcAuthUser Organization経由でフィルター
ExternalIdpMapping EcAuthUser.Organization経由
AuthorizationCode EcAuthUser.Organization経由
AccessToken EcAuthUser.Organization経由
ExternalIdpToken EcAuthUser.Organization経由

SQL Server RLS の概要

Row-Level Security とは

SQL Server 2016で導入されたデータベース層でのセキュリティ機能。テーブルへのアクセス時に自動的にフィルター述語が適用され、ユーザーがアクセス可能な行のみを返す。

主要コンポーネント

  1. フィルター述語関数: アクセス可能な行を判定するインライン テーブル値関数
  2. セキュリティポリシー: テーブルとフィルター述語関数を紐付ける
  3. SESSION_CONTEXT: 接続ごとのコンテキスト情報(テナント識別子等)を保持

RLS 導入パターン

データベース層の実装

-- スキーマ作成
CREATE SCHEMA Security;
GO

-- フィルター述語関数
CREATE FUNCTION Security.fn_TenantFilter(@TenantName NVARCHAR(255))
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
    SELECT 1 AS result
    WHERE @TenantName = CAST(SESSION_CONTEXT(N'TenantName') AS NVARCHAR(255));
GO

-- セキュリティポリシー(organizationテーブル)
CREATE SECURITY POLICY Security.OrganizationPolicy
    ADD FILTER PREDICATE Security.fn_TenantFilter(tenant_name) ON dbo.organization
    WITH (STATE = ON);
GO

-- セキュリティポリシー(ec_auth_userテーブル - organization_idを経由)
CREATE FUNCTION Security.fn_UserTenantFilter(@OrganizationId INT)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
    SELECT 1 AS result
    WHERE EXISTS (
        SELECT 1 FROM dbo.organization o
        WHERE o.id = @OrganizationId
        AND o.tenant_name = CAST(SESSION_CONTEXT(N'TenantName') AS NVARCHAR(255))
    );
GO

CREATE SECURITY POLICY Security.EcAuthUserPolicy
    ADD FILTER PREDICATE Security.fn_UserTenantFilter(organization_id) ON dbo.ec_auth_user
    WITH (STATE = ON);
GO

EF Core側の実装

// DbConnectionInterceptorでSESSION_CONTEXTを設定
public class TenantSessionInterceptor : DbConnectionInterceptor
{
    private readonly ITenantService _tenantService;

    public TenantSessionInterceptor(ITenantService tenantService)
    {
        _tenantService = tenantService;
    }

    public override async ValueTask<InterceptionResult> ConnectionOpeningAsync(
        DbConnection connection,
        ConnectionEventData eventData,
        InterceptionResult result,
        CancellationToken cancellationToken = default)
    {
        return result;
    }

    public override async Task ConnectionOpenedAsync(
        DbConnection connection,
        ConnectionOpenedEventData eventData,
        CancellationToken cancellationToken = default)
    {
        if (!string.IsNullOrEmpty(_tenantService.TenantName))
        {
            using var command = connection.CreateCommand();
            command.CommandText = "EXEC sp_set_session_context @key=N'TenantName', @value=@tenant";
            var param = command.CreateParameter();
            param.ParameterName = "@tenant";
            param.Value = _tenantService.TenantName;
            command.Parameters.Add(param);
            await command.ExecuteNonQueryAsync(cancellationToken);
        }
    }
}

// Program.cs での設定
builder.Services.AddDbContext<EcAuthDbContext>((sp, options) =>
{
    var tenantService = sp.GetRequiredService<ITenantService>();
    options.UseSqlServer(connectionString)
           .AddInterceptors(new TenantSessionInterceptor(tenantService));
});

比較分析

セキュリティ特性

観点 EF Core グローバルクエリフィルター SQL Server RLS
強制レベル アプリケーション層 データベース層
IgnoreQueryFilters()でのバイパス 可能 不可
直接SQL実行時の分離 なし あり
SSMSからの直接アクセス フィルターなし フィルター適用
ADO.NET直接使用時 フィルターなし フィルター適用

パフォーマンス

観点 EF Core グローバルクエリフィルター SQL Server RLS
クエリ最適化 EF Coreが最適化 SQL Serverが最適化
インデックス活用 通常通り 述語関数内でのJOINに注意
追加オーバーヘッド 最小限 SESSION_CONTEXT設定 + 述語評価

運用・保守性

観点 EF Core グローバルクエリフィルター SQL Server RLS
実装複雑度 低(C#コードのみ) 高(SQL + C#)
マイグレーション EF Core標準 セキュリティポリシーの別管理必要
テスト容易性 モック可能 テスト環境での設定必要
デバッグ Visual Studioで完結 SQL Profiler等も必要

導入判断

RLSを導入すべきケース

  1. コンプライアンス要件

    • 監査でデータベース層での分離証明が必要
    • 業界規制(金融、医療等)でDB層セキュリティが必須
  2. 運用要件

    • DBA/開発者がSSMSから本番DBに直接アクセスする運用
    • 複数チームや外部委託がDB直接操作を行う
  3. アプリケーション複雑度

    • 大規模コードベースでIgnoreQueryFilters()の誤用リスクが高い
    • 複数アプリケーションが同一DBを共有

EcAuthでの結論: 現時点では不要

理由

  1. 直接DB操作の限定性

    • 本番環境ではAPI経由のアクセスのみ
    • DB直接操作は開発・緊急時のメンテナンスに限定
  2. コードベースの規模

    • 小規模なコードベース
    • IgnoreQueryFilters()の使用箇所を容易に把握可能
    • コードレビューでの漏れリスクが低い
  3. 個人情報最小化設計

    • EcAuthは個人情報を最小限しか保持しない(ハッシュ化メールアドレス、UUID)
    • 万が一のテナント間漏洩でも影響が限定的
  4. 現在の実装の十分性

    • EF Coreグローバルクエリフィルターで全エンティティに適用済み
    • OrganizationFilterによるリクエスト単位の検証も実施
  5. 実装・保守コスト

    • RLS導入による複雑度増加に見合うメリットがない
    • マイグレーション管理の複雑化を避けられる

将来の再検討トリガー

以下の状況が発生した場合、RLS導入を再検討する:

  1. 規模拡大

    • 開発チームの増加(外部委託含む)
    • 複数アプリケーションからの同一DB接続
  2. コンプライアンス要件

    • SOC 2、ISO 27001等の認証取得
    • 金融機関向けサービス展開
  3. 運用変更

    • DBAによる本番DB直接操作の定常化
    • BIツール等からの直接DB接続
  4. セキュリティインシデント

    • アプリケーションバグによるテナント間データ漏洩

参考資料

更新履歴

日付 内容
2025-12-01 初版作成