From b7b9f50d005a6e499db46efb372b0a59af5cc9a6 Mon Sep 17 00:00:00 2001 From: iven Date: Mon, 27 Apr 2026 18:40:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(db):=20RLS=20=E7=AD=96=E7=95=A5=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=20=E2=80=94=2080=20=E5=BC=A0=20tenant=5Fid=20?= =?UTF-8?q?=E8=A1=A8=E5=90=AF=E7=94=A8=E8=A1=8C=E7=BA=A7=E5=AE=89=E5=85=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 所有含 tenant_id 的表(基础 34 + 健康 28 + 其他 18)启用 RLS - 策略:未设置 app.current_tenant_id 时允许全部,设置后按 tenant_id 过滤 - down 方法完整回退(DROP POLICY + DISABLE ROW LEVEL SECURITY) --- crates/erp-server/migration/src/lib.rs | 2 + .../m20260427_000086_enable_rls_all_tables.rs | 144 ++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 crates/erp-server/migration/src/m20260427_000086_enable_rls_all_tables.rs diff --git a/crates/erp-server/migration/src/lib.rs b/crates/erp-server/migration/src/lib.rs index f68b5cc..df0cbfb 100644 --- a/crates/erp-server/migration/src/lib.rs +++ b/crates/erp-server/migration/src/lib.rs @@ -85,6 +85,7 @@ mod m20260427_000082_seed_ai_prompts; mod m20260427_000083_create_follow_up_template; mod m20260427_000084_domain_events_cleanup; mod m20260427_000085_processed_events; +mod m20260427_000086_enable_rls_all_tables; pub struct Migrator; @@ -177,6 +178,7 @@ impl MigratorTrait for Migrator { Box::new(m20260427_000083_create_follow_up_template::Migration), Box::new(m20260427_000084_domain_events_cleanup::Migration), Box::new(m20260427_000085_processed_events::Migration), + Box::new(m20260427_000086_enable_rls_all_tables::Migration), ] } } diff --git a/crates/erp-server/migration/src/m20260427_000086_enable_rls_all_tables.rs b/crates/erp-server/migration/src/m20260427_000086_enable_rls_all_tables.rs new file mode 100644 index 0000000..26433a6 --- /dev/null +++ b/crates/erp-server/migration/src/m20260427_000086_enable_rls_all_tables.rs @@ -0,0 +1,144 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +/// 所有含 tenant_id 的表 — RLS 策略统一启用 +const TENANT_TABLES: &[&str] = &[ + // ── 基础层 (ERP Core) ────────────────────────── + "users", + "user_credentials", + "user_tokens", + "roles", + "permissions", + "role_permissions", + "user_roles", + "organizations", + "departments", + "positions", + "dictionaries", + "dictionary_items", + "menus", + "menu_roles", + "settings", + "numbering_rules", + "process_definitions", + "process_instances", + "tokens", + "tasks", + "process_variables", + "message_templates", + "messages", + "message_subscriptions", + "audit_logs", + "domain_events", + "plugins", + "plugin_entities", + "plugin_entity_columns", + "user_departments", + "plugin_user_views", + "plugin_market_reviews", + "tenant_crypto_keys", + "domain_events_archive", + // ── 健康层 (erp-health) ──────────────────────── + "patient", + "patient_family_member", + "patient_tag", + "patient_tag_relation", + "doctor_profile", + "patient_doctor_relation", + "health_record", + "vital_signs", + "lab_report", + "health_trend", + "appointment", + "doctor_schedule", + "follow_up_task", + "follow_up_record", + "consultation_session", + "consultation_message", + "dialysis_record", + "daily_monitoring", + "diagnosis", + "critical_value_threshold", + "consent", + "device_readings", + "vital_signs_hourly", + "patient_devices", + "alert_rules", + "alerts", + "medication_record", + "dialysis_prescription", + // ── AI + 积分 + 内容 + 微信 ──────────────────── + "ai_prompts", + "ai_analysis_results", + "ai_usage_logs", + "points_account", + "points_rule", + "points_transaction", + "points_product", + "points_order", + "points_checkin", + "offline_event", + "offline_event_registration", + "wechat_users", + "article", + "article_category", + "article_tag", + "article_revision", + "follow_up_template", + "follow_up_template_step", +]; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let conn = manager.get_connection(); + + // 创建自定义配置参数(仅用于文档,PostgreSQL 自动支持 current_setting) + // 策略设计: + // - current_setting('app.current_tenant_id', true) = '' → 未设置,允许所有(兼容迁移/后台任务) + // - 设置为 UUID → 仅允许匹配 tenant_id 的行 + + for table in TENANT_TABLES { + // 启用 RLS + conn.execute_unprepared(&format!( + "ALTER TABLE {table} ENABLE ROW LEVEL SECURITY" + )).await?; + + // 移除旧策略(如果存在,支持重复执行迁移) + conn.execute_unprepared(&format!( + "DROP POLICY IF EXISTS tenant_isolation ON {table}" + )).await?; + + // 租户隔离策略 — 未设置参数时允许所有,设置后按 tenant_id 过滤 + conn.execute_unprepared(&format!( + "CREATE POLICY tenant_isolation ON {table} USING ( + current_setting('app.current_tenant_id', true) = '' + OR tenant_id = current_setting('app.current_tenant_id', true)::uuid + )" + )).await?; + + // 超级用户绕过策略(数据库 owner 直接绕过 RLS) + // PostgreSQL 默认表 owner 绕过 RLS,无需额外策略 + } + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let conn = manager.get_connection(); + + for table in TENANT_TABLES { + conn.execute_unprepared(&format!( + "DROP POLICY IF EXISTS tenant_isolation ON {table}" + )).await?; + + conn.execute_unprepared(&format!( + "ALTER TABLE {table} DISABLE ROW LEVEL SECURITY" + )).await?; + } + + Ok(()) + } +}