fix(m0): 迁移可执行 + 测试全绿(33 条)
CI / backend (push) Canceled after 0s

跑通迁移与测试后修掉的真问题:

- alembic 0001:asyncpg 走预备语句,一次 execute 只能一条 SQL。
  CREATE FUNCTION / CREATE POLICY / GRANT 全部拆成单语句逐条执行,
  否则报 "cannot insert multiple commands into a prepared statement"。
- 创建工作区:ORM flush 只按 relationship 排序,裸外键不算数,
  member 可能先于 workspace 落地撞 FK。改为先 flush workspace 再插 member。
- datetime.UTC 是 3.11+ 才有的,本机 3.10 直接 ImportError。改用 timezone.utc。
- 幂等中间件:原来会缓存 4xx。校验失败被缓存后,用户改对参数仍拿到同一个 422。
  改为只缓存 <400。
- 发码节流窗口改为可配置 SMS_THROTTLE_SECONDS(测试置 0),
  节流用例自己显式打开,避免"整库关掉节流"导致节流从未被验证。
- 租户隔离用例:pytest.raises(Exception) 改为精确的 ProgrammingError,
  否则任何异常都会让"应用角色不能建表"这条断言假通过。

验收:pytest 33 passed;ruff check / format 通过;真机跑 uvicorn,
/health、/version、登录、建工作区、列表、/auth/me 全通。
This commit is contained in:
Steven
2026-10-28 03:22:59 +08:00
parent 0c53aa7062
commit 2b493bd9f8
14 changed files with 160 additions and 136 deletions
+67 -47
View File
@@ -19,57 +19,76 @@ depends_on: str | Sequence[str] | None = None
APP_ROLE = "dadao_app"
HELPER_SQL = """
CREATE OR REPLACE FUNCTION app_ws_id() RETURNS uuid
LANGUAGE sql STABLE AS $$
SELECT nullif(current_setting('app.ws_id', true), '')::uuid
$$;
# 说明:asyncpg 走"预备语句"执行,**一条 execute 里只能有一条 SQL**,
# 多条语句会报 `cannot insert multiple commands into a prepared statement`。
# 所以这里按语句拆成元组,逐条执行。
HELPER_STATEMENTS: tuple[str, ...] = (
"""
CREATE OR REPLACE FUNCTION app_ws_id() RETURNS uuid
LANGUAGE sql STABLE AS $$
SELECT nullif(current_setting('app.ws_id', true), '')::uuid
$$
""",
"""
CREATE OR REPLACE FUNCTION app_user_id() RETURNS uuid
LANGUAGE sql STABLE AS $$
SELECT nullif(current_setting('app.user_id', true), '')::uuid
$$
""",
)
CREATE OR REPLACE FUNCTION app_user_id() RETURNS uuid
LANGUAGE sql STABLE AS $$
SELECT nullif(current_setting('app.user_id', true), '')::uuid
$$;
"""
POLICY_STATEMENTS: tuple[str, ...] = (
# ── workspace:本租户 OR 我是其成员(用于"我加入了哪些工作区")──
"ALTER TABLE workspace ENABLE ROW LEVEL SECURITY;",
"""
CREATE POLICY workspace_isolation ON workspace
USING (
id = app_ws_id()
OR id IN (SELECT m.workspace_id FROM member m WHERE m.user_id = app_user_id())
)
WITH CHECK (id = app_ws_id());
""",
# ── member:本租户成员 OR 我自己那条 ──
"ALTER TABLE member ENABLE ROW LEVEL SECURITY;",
"""
CREATE POLICY member_isolation ON member
USING (workspace_id = app_ws_id() OR user_id = app_user_id())
WITH CHECK (workspace_id = app_ws_id());
""",
# ── invitation ──
"ALTER TABLE invitation ENABLE ROW LEVEL SECURITY;",
"""
CREATE POLICY invitation_isolation ON invitation
USING (workspace_id = app_ws_id())
WITH CHECK (workspace_id = app_ws_id());
""",
# ── audit_log:只增不改,租户内可见 ──
"ALTER TABLE audit_log ENABLE ROW LEVEL SECURITY;",
"""
CREATE POLICY audit_log_isolation ON audit_log
USING (workspace_id = app_ws_id())
WITH CHECK (workspace_id = app_ws_id());
""",
# ── 授权:应用角色没有 DDL、没有 BYPASSRLS,只能读写到自己该看的数据 ──
f"GRANT USAGE ON SCHEMA public TO {APP_ROLE};",
(
"GRANT SELECT, INSERT, UPDATE, DELETE ON "
f"workspace, app_user, member, invitation, audit_log TO {APP_ROLE};"
),
(
"ALTER DEFAULT PRIVILEGES FOR ROLE dadao_owner IN SCHEMA public "
f"GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO {APP_ROLE};"
),
)
POLICY_SQL = f"""
-- ── workspace:本租户 OR 我是其成员(用于"我加入了哪些工作区")──
ALTER TABLE workspace ENABLE ROW LEVEL SECURITY;
CREATE POLICY workspace_isolation ON workspace
USING (
id = app_ws_id()
OR id IN (SELECT m.workspace_id FROM member m WHERE m.user_id = app_user_id())
)
WITH CHECK (id = app_ws_id());
-- ── member:本租户成员 OR 我自己那条 ──
ALTER TABLE member ENABLE ROW LEVEL SECURITY;
CREATE POLICY member_isolation ON member
USING (workspace_id = app_ws_id() OR user_id = app_user_id())
WITH CHECK (workspace_id = app_ws_id());
-- ── invitation ──
ALTER TABLE invitation ENABLE ROW LEVEL SECURITY;
CREATE POLICY invitation_isolation ON invitation
USING (workspace_id = app_ws_id())
WITH CHECK (workspace_id = app_ws_id());
-- ── audit_log:只增不改,租户内可见 ──
ALTER TABLE audit_log ENABLE ROW LEVEL SECURITY;
CREATE POLICY audit_log_isolation ON audit_log
USING (workspace_id = app_ws_id())
WITH CHECK (workspace_id = app_ws_id());
-- ── 授权:应用角色没有 DDL、没有 BYPASSRLS,只能读写到自己该看的数据 ──
GRANT USAGE ON SCHEMA public TO {APP_ROLE};
GRANT SELECT, INSERT, UPDATE, DELETE ON
workspace, app_user, member, invitation, audit_log TO {APP_ROLE};
ALTER DEFAULT PRIVILEGES FOR ROLE dadao_owner IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO {APP_ROLE};
"""
def _exec_each(statements: tuple[str, ...]) -> None:
for statement in statements:
op.execute(statement)
def upgrade() -> None:
op.execute(HELPER_SQL)
_exec_each(HELPER_STATEMENTS)
op.create_table(
"app_user",
@@ -184,7 +203,7 @@ def upgrade() -> None:
)
op.create_index("ix_audit_ws_created", "audit_log", ["workspace_id", "created_at"])
op.execute(POLICY_SQL)
_exec_each(POLICY_STATEMENTS)
def downgrade() -> None:
@@ -194,4 +213,5 @@ def downgrade() -> None:
op.drop_table("member")
op.drop_table("workspace")
op.drop_table("app_user")
op.execute("DROP FUNCTION IF EXISTS app_ws_id(); DROP FUNCTION IF EXISTS app_user_id();")
op.execute("DROP FUNCTION IF EXISTS app_ws_id();")
op.execute("DROP FUNCTION IF EXISTS app_user_id();")