feat(db): 迁移里的数据库角色名改为环境变量注入
This commit is contained in:
@@ -12,13 +12,13 @@ import sqlalchemy as sa
|
|||||||
from alembic import op
|
from alembic import op
|
||||||
from sqlalchemy.dialects import postgresql
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
from app.roles import APP_ROLE, OWNER_ROLE
|
||||||
|
|
||||||
revision: str = "0001"
|
revision: str = "0001"
|
||||||
down_revision: str | None = None
|
down_revision: str | None = None
|
||||||
branch_labels: str | Sequence[str] | None = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: str | Sequence[str] | None = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
APP_ROLE = "dadao_app"
|
|
||||||
|
|
||||||
# 说明:asyncpg 走"预备语句"执行,**一条 execute 里只能有一条 SQL**,
|
# 说明:asyncpg 走"预备语句"执行,**一条 execute 里只能有一条 SQL**,
|
||||||
# 多条语句会报 `cannot insert multiple commands into a prepared statement`。
|
# 多条语句会报 `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};"
|
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};"
|
f"GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO {APP_ROLE};"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ import sqlalchemy as sa
|
|||||||
from alembic import op
|
from alembic import op
|
||||||
from sqlalchemy.dialects import postgresql
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
from app.roles import APP_ROLE
|
||||||
|
|
||||||
revision: str = "0002"
|
revision: str = "0002"
|
||||||
down_revision: str | None = "0001"
|
down_revision: str | None = "0001"
|
||||||
branch_labels: str | Sequence[str] | None = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: 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")
|
TENANT_TABLES_0002: tuple[str, ...] = ("item", "tag", "item_tag", "comment", "activity")
|
||||||
|
|
||||||
_TIMESTAMP_COLUMNS = (
|
_TIMESTAMP_COLUMNS = (
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ import sqlalchemy as sa
|
|||||||
from alembic import op
|
from alembic import op
|
||||||
from sqlalchemy.dialects import postgresql
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
from app.roles import APP_ROLE
|
||||||
|
|
||||||
revision: str = "0003"
|
revision: str = "0003"
|
||||||
down_revision: str | None = "0002"
|
down_revision: str | None = "0002"
|
||||||
branch_labels: str | Sequence[str] | None = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: 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")
|
TENANT_TABLES_0003: tuple[str, ...] = ("ticket_ext", "item_link", "notification")
|
||||||
|
|
||||||
TASK_STATUSES = ("inbox", "todo", "doing", "done", "blocked", "canceled")
|
TASK_STATUSES = ("inbox", "todo", "doing", "done", "blocked", "canceled")
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ import sqlalchemy as sa
|
|||||||
from alembic import op
|
from alembic import op
|
||||||
from sqlalchemy.dialects import postgresql
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
from app.roles import APP_ROLE
|
||||||
|
|
||||||
revision: str = "0004"
|
revision: str = "0004"
|
||||||
down_revision: str | None = "0003"
|
down_revision: str | None = "0003"
|
||||||
branch_labels: str | Sequence[str] | None = None
|
branch_labels: str | Sequence[str] | None = None
|
||||||
depends_on: str | Sequence[str] | None = None
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
APP_ROLE = "dadao_app"
|
|
||||||
|
|
||||||
TENANT_TABLES_0004: tuple[str, ...] = ("knowledge_doc",)
|
TENANT_TABLES_0004: tuple[str, ...] = ("knowledge_doc",)
|
||||||
|
|
||||||
TASK_STATUSES = ("inbox", "todo", "doing", "done", "blocked", "canceled")
|
TASK_STATUSES = ("inbox", "todo", "doing", "done", "blocked", "canceled")
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ from sqlalchemy.dialects.postgresql import UUID as PgUUID
|
|||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
|
|
||||||
|
from app.roles import APP_ROLE
|
||||||
|
|
||||||
revision = "0005"
|
revision = "0005"
|
||||||
down_revision = "0004"
|
down_revision = "0004"
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
@@ -55,7 +57,7 @@ def upgrade() -> None:
|
|||||||
"ALTER TABLE attachment ENABLE ROW LEVEL SECURITY",
|
"ALTER TABLE attachment ENABLE ROW LEVEL SECURITY",
|
||||||
"CREATE POLICY attachment_isolation ON attachment "
|
"CREATE POLICY attachment_isolation ON attachment "
|
||||||
"USING (workspace_id = app_ws_id()) WITH CHECK (workspace_id = app_ws_id())",
|
"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)
|
op.execute(statement)
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ POLICY_SQL = (
|
|||||||
"ALTER TABLE chunk ENABLE ROW LEVEL SECURITY",
|
"ALTER TABLE chunk ENABLE ROW LEVEL SECURITY",
|
||||||
"CREATE POLICY chunk_isolation ON chunk "
|
"CREATE POLICY chunk_isolation ON chunk "
|
||||||
"USING (workspace_id = app_ws_id()) WITH CHECK (workspace_id = app_ws_id())",
|
"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}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ POLICY_SQL = (
|
|||||||
"ALTER TABLE knowledge_category ENABLE ROW LEVEL SECURITY",
|
"ALTER TABLE knowledge_category ENABLE ROW LEVEL SECURITY",
|
||||||
"CREATE POLICY knowledge_category_isolation ON knowledge_category "
|
"CREATE POLICY knowledge_category_isolation ON knowledge_category "
|
||||||
"USING (workspace_id = app_ws_id()) WITH CHECK (workspace_id = app_ws_id())",
|
"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}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
Reference in New Issue
Block a user