forked from xiongyuxing/tiku-backend.net
feat: 添加租户隔离保护触发器的管理方法,并更新迁移文档
This commit is contained in:
18
README.md
18
README.md
@@ -218,6 +218,24 @@ Tiku.Infrastructure/Persistence/Migrations/20260728031410_InitialSchema.cs
|
||||
- 不新增 Supabase provider、Supabase URL 拼接、Supabase Storage bucket 逻辑或 Supabase Auth 兼容层。
|
||||
- EF Core 管实体和 migration 生命周期;跨表租户不变量不能指望 ORM 自动推导。比如“题目引用只能指向平台或本租户”“分类父节点只能属于平台或本租户”,必须用集中 PostgreSQL trigger / constraint trigger SQL helper + migration 调用 + 真实 PostgreSQL 测试兜底,禁止去数据库手工补。
|
||||
|
||||
## 数据库边界与 ORM 分工
|
||||
|
||||
本项目仍然采用 EF Core code-first migration 管理数据库基线:实体、索引、外键、普通唯一约束和普通 check constraint 都应优先通过 `IEntityTypeConfiguration` 表达,并随 migration 进入代码库。
|
||||
|
||||
但 EF Core 不会、也不应该自动推导跨表业务不变量。以下规则必须保留为 PostgreSQL guard:
|
||||
|
||||
- `TenantQuestionReference` 只能引用平台主体公共题或当前租户私题,不能引用其他租户私题。
|
||||
- `TaxonomyNode` 的父节点只能属于平台主体或当前租户,不能挂到其他租户节点。
|
||||
- 需要读取 `tenants.mode` 或按 owner 关系做条件判断的规则。
|
||||
|
||||
这些 guard 的维护约定固定如下:
|
||||
|
||||
1. 触发器 SQL 只放在 `Tiku.Infrastructure/Persistence/PostgreSqlTenantConstraintSql.cs`。
|
||||
2. Migration 不直接复制触发器 SQL,只调用 `migrationBuilder.EnsureTenantIsolationGuards()` 或 `migrationBuilder.DropTenantIsolationGuards()`。
|
||||
3. 重建或压缩 `InitialSchema` 后,必须在 `Up()` 末尾调用 `EnsureTenantIsolationGuards()`,在 `Down()` 开头调用 `DropTenantIsolationGuards()`。
|
||||
4. 新增类似规则时,先判断能否用 EF FK / unique index / check constraint 表达;表达不了才新增 PostgreSQL guard。
|
||||
5. 每个 guard 必须同时有 migration script 断言和真实 PostgreSQL 越权测试。
|
||||
|
||||
## 还剩多少待迁移
|
||||
|
||||
运行时契约基线显示,旧 NestJS 有 342 个操作,当前 .NET 有 237 个操作:
|
||||
|
||||
@@ -9200,14 +9200,13 @@ namespace Tiku.Infrastructure.Persistence.Migrations
|
||||
principalColumns: new[] { "tenant_id", "id" },
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
|
||||
migrationBuilder.Sql(PostgreSqlTenantConstraintSql.CreateTenantQuestionReferenceGuard);
|
||||
migrationBuilder.Sql(PostgreSqlTenantConstraintSql.CreateTaxonomyParentGuard);
|
||||
migrationBuilder.EnsureTenantIsolationGuards();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(PostgreSqlTenantConstraintSql.DropTenantGuards);
|
||||
migrationBuilder.DropTenantIsolationGuards();
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_content_entries_regions_tenant_id_region_id",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Tiku.Infrastructure.Persistence.Migrations;
|
||||
|
||||
internal static class MigrationBuilderTenantGuardExtensions
|
||||
{
|
||||
public static void EnsureTenantIsolationGuards(this MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(PostgreSqlTenantConstraintSql.DropTenantGuards);
|
||||
migrationBuilder.Sql(PostgreSqlTenantConstraintSql.CreateTenantQuestionReferenceGuard);
|
||||
migrationBuilder.Sql(PostgreSqlTenantConstraintSql.CreateTaxonomyParentGuard);
|
||||
}
|
||||
|
||||
public static void DropTenantIsolationGuards(this MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(PostgreSqlTenantConstraintSql.DropTenantGuards);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Tiku.IntegrationTests.Api;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
@@ -22,4 +24,22 @@ public sealed class MigrationExecutionTests
|
||||
Assert.NotEmpty(appliedMigrations);
|
||||
Assert.True(await dbContext.Database.CanConnectAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Migration_script_contains_tenant_isolation_guard_triggers()
|
||||
{
|
||||
await using var factory = new ApiTestFactory();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
|
||||
var migrator = dbContext.GetService<IMigrator>();
|
||||
|
||||
var script = migrator.GenerateScript();
|
||||
|
||||
Assert.Contains("drop trigger if exists trg_tenant_question_references_platform_or_self_owner", script);
|
||||
Assert.Contains("create or replace function tiku_guard_tenant_question_reference()", script);
|
||||
Assert.Contains("create trigger trg_tenant_question_references_platform_or_self_owner", script);
|
||||
Assert.Contains("drop trigger if exists trg_taxonomy_nodes_parent_platform_or_self_owner", script);
|
||||
Assert.Contains("create or replace function tiku_guard_taxonomy_parent_owner()", script);
|
||||
Assert.Contains("create trigger trg_taxonomy_nodes_parent_platform_or_self_owner", script);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,8 +179,10 @@ EF Core 负责实体、Fluent Configuration、Migration 生成和迁移执行入
|
||||
实现要求:
|
||||
|
||||
- 触发器 SQL 必须集中在基础设施层,例如 `Tiku.Infrastructure/Persistence/PostgreSqlTenantConstraintSql.cs`。
|
||||
- Migration 只调用集中 SQL helper,例如 `migrationBuilder.Sql(PostgreSqlTenantConstraintSql.CreateTenantQuestionReferenceGuard)`。
|
||||
- Migration 只调用集中 helper:`migrationBuilder.EnsureTenantIsolationGuards()` / `migrationBuilder.DropTenantIsolationGuards()`。
|
||||
- 重建或压缩 `InitialSchema` 后,必须在 `Up()` 末尾调用 `EnsureTenantIsolationGuards()`,在 `Down()` 开头调用 `DropTenantIsolationGuards()`。
|
||||
- 不允许把触发器 SQL 零散复制到多个 migration。
|
||||
- Migration script 测试必须断言 guard function 和 trigger 存在,避免重建基线时漏掉数据库硬防线。
|
||||
- 每个触发器必须有真实 PostgreSQL 集成测试覆盖允许路径和拒绝路径。
|
||||
- 如果将来新增类似“平台或本租户”的 owner 规则,优先补集中 SQL helper 和模型/集成测试,不要只靠 Service 手写校验。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user