From 2dbe4cf6758cf61bb38f62021418a2cbe2f2304a Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 30 Oct 2026 18:49:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(db):=20=E8=BF=81=E7=A7=BB=E9=87=8C?= =?UTF-8?q?=E7=9A=84=E6=95=B0=E6=8D=AE=E5=BA=93=E8=A7=92=E8=89=B2=E5=90=8D?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E7=8E=AF=E5=A2=83=E5=8F=98=E9=87=8F=E6=B3=A8?= =?UTF-8?q?=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/alembic/versions/0001_init.py | 6 ++-- backend/alembic/versions/0002_tasks.py | 4 +-- backend/alembic/versions/0003_tickets.py | 4 +-- backend/alembic/versions/0004_knowledge.py | 4 +-- backend/alembic/versions/0005_attachments.py | 4 ++- backend/alembic/versions/0006_chunks.py | 2 +- .../versions/0007_knowledge_categories.py | 2 +- backend/app/roles.py | 28 +++++++++++++++++++ 8 files changed, 42 insertions(+), 12 deletions(-) create mode 100644 backend/app/roles.py diff --git a/backend/alembic/versions/0001_init.py b/backend/alembic/versions/0001_init.py index 1ce5c97..882c5ad 100644 --- a/backend/alembic/versions/0001_init.py +++ b/backend/alembic/versions/0001_init.py @@ -12,13 +12,13 @@ import sqlalchemy as sa from alembic import op from sqlalchemy.dialects import postgresql +from app.roles import APP_ROLE, OWNER_ROLE + revision: str = "0001" down_revision: str | None = None branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None -APP_ROLE = "dadao_app" - # 说明:asyncpg 走"预备语句"执行,**一条 execute 里只能有一条 SQL**, # 多条语句会报 `cannot insert multiple commands into a prepared statement`。 # 所以这里按语句拆成元组,逐条执行。 @@ -76,7 +76,7 @@ POLICY_STATEMENTS: tuple[str, ...] = ( f"workspace, app_user, member, invitation, audit_log TO {APP_ROLE};" ), ( - "ALTER DEFAULT PRIVILEGES FOR ROLE dadao_owner IN SCHEMA public " + f"ALTER DEFAULT PRIVILEGES FOR ROLE {OWNER_ROLE} IN SCHEMA public " f"GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO {APP_ROLE};" ), ) diff --git a/backend/alembic/versions/0002_tasks.py b/backend/alembic/versions/0002_tasks.py index 6a4bf27..21ded76 100644 --- a/backend/alembic/versions/0002_tasks.py +++ b/backend/alembic/versions/0002_tasks.py @@ -15,13 +15,13 @@ import sqlalchemy as sa from alembic import op from sqlalchemy.dialects import postgresql +from app.roles import APP_ROLE + revision: str = "0002" down_revision: str | None = "0001" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None -APP_ROLE = "dadao_app" - TENANT_TABLES_0002: tuple[str, ...] = ("item", "tag", "item_tag", "comment", "activity") _TIMESTAMP_COLUMNS = ( diff --git a/backend/alembic/versions/0003_tickets.py b/backend/alembic/versions/0003_tickets.py index 8161bba..aab9432 100644 --- a/backend/alembic/versions/0003_tickets.py +++ b/backend/alembic/versions/0003_tickets.py @@ -19,13 +19,13 @@ import sqlalchemy as sa from alembic import op from sqlalchemy.dialects import postgresql +from app.roles import APP_ROLE + revision: str = "0003" down_revision: str | None = "0002" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None -APP_ROLE = "dadao_app" - TENANT_TABLES_0003: tuple[str, ...] = ("ticket_ext", "item_link", "notification") TASK_STATUSES = ("inbox", "todo", "doing", "done", "blocked", "canceled") diff --git a/backend/alembic/versions/0004_knowledge.py b/backend/alembic/versions/0004_knowledge.py index 897bba3..e546690 100644 --- a/backend/alembic/versions/0004_knowledge.py +++ b/backend/alembic/versions/0004_knowledge.py @@ -21,13 +21,13 @@ import sqlalchemy as sa from alembic import op from sqlalchemy.dialects import postgresql +from app.roles import APP_ROLE + revision: str = "0004" down_revision: str | None = "0003" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None -APP_ROLE = "dadao_app" - TENANT_TABLES_0004: tuple[str, ...] = ("knowledge_doc",) TASK_STATUSES = ("inbox", "todo", "doing", "done", "blocked", "canceled") diff --git a/backend/alembic/versions/0005_attachments.py b/backend/alembic/versions/0005_attachments.py index cfa8d5d..600b9e0 100644 --- a/backend/alembic/versions/0005_attachments.py +++ b/backend/alembic/versions/0005_attachments.py @@ -11,6 +11,8 @@ from sqlalchemy.dialects.postgresql import UUID as PgUUID from alembic import op +from app.roles import APP_ROLE + revision = "0005" down_revision = "0004" branch_labels = None @@ -55,7 +57,7 @@ def upgrade() -> None: "ALTER TABLE attachment ENABLE ROW LEVEL SECURITY", "CREATE POLICY attachment_isolation ON attachment " "USING (workspace_id = app_ws_id()) WITH CHECK (workspace_id = app_ws_id())", - "GRANT SELECT, INSERT, UPDATE, DELETE ON attachment TO dadao_app", + f"GRANT SELECT, INSERT, UPDATE, DELETE ON attachment TO {APP_ROLE}", ): op.execute(statement) diff --git a/backend/alembic/versions/0006_chunks.py b/backend/alembic/versions/0006_chunks.py index 4efdfc9..8656cd7 100644 --- a/backend/alembic/versions/0006_chunks.py +++ b/backend/alembic/versions/0006_chunks.py @@ -82,7 +82,7 @@ POLICY_SQL = ( "ALTER TABLE chunk ENABLE ROW LEVEL SECURITY", "CREATE POLICY chunk_isolation ON chunk " "USING (workspace_id = app_ws_id()) WITH CHECK (workspace_id = app_ws_id())", - "GRANT SELECT, INSERT, UPDATE, DELETE ON chunk TO dadao_app", + f"GRANT SELECT, INSERT, UPDATE, DELETE ON chunk TO {APP_ROLE}", ) diff --git a/backend/alembic/versions/0007_knowledge_categories.py b/backend/alembic/versions/0007_knowledge_categories.py index e79a476..152959f 100644 --- a/backend/alembic/versions/0007_knowledge_categories.py +++ b/backend/alembic/versions/0007_knowledge_categories.py @@ -60,7 +60,7 @@ POLICY_SQL = ( "ALTER TABLE knowledge_category ENABLE ROW LEVEL SECURITY", "CREATE POLICY knowledge_category_isolation ON knowledge_category " "USING (workspace_id = app_ws_id()) WITH CHECK (workspace_id = app_ws_id())", - "GRANT SELECT, INSERT, UPDATE, DELETE ON knowledge_category TO dadao_app", + f"GRANT SELECT, INSERT, UPDATE, DELETE ON knowledge_category TO {APP_ROLE}", ) diff --git a/backend/app/roles.py b/backend/app/roles.py new file mode 100644 index 0000000..88dbf9b --- /dev/null +++ b/backend/app/roles.py @@ -0,0 +1,28 @@ +"""迁移里用到的数据库角色名。 + +为什么要有这个模块:RLS 的隔离强度取决于「应用角色不是表 owner、没有 BYPASSRLS」。 +开发和生产的库在同一个 PG 实例上(fnsvr-01:8432),如果两边共用 `dadao_app` 角色, +拿到开发口令的人就能直连生产库——所以生产用独立角色,由迁移时的环境变量注入。 + +默认值就是开发/测试的角色名,所以本地和 CI 行为不变。 +""" + +from __future__ import annotations + +import os +import re + +# PostgreSQL 标识符:小写字母/数字/下划线,且不能以数字开头 +_ROLE_RE = re.compile(r"^[a-z_][a-z0-9_]{0,62}$") + + +def _role_from_env(env_name: str, default: str) -> str: + value = os.getenv(env_name, default) + if not _ROLE_RE.match(value): + raise RuntimeError(f"{env_name} 不是合法的 PostgreSQL 角色名:{value!r}") + return value + + +# 迁移角色(owner,可建表,绕过 RLS)与应用角色(只能读写数据,受 RLS 约束) +OWNER_ROLE = _role_from_env("OWNER_DB_ROLE", "dadao_owner") +APP_ROLE = _role_from_env("APP_DB_ROLE", "dadao_app")