From 09af8629572f5c87e8a54eba41461c77cab5f109 Mon Sep 17 00:00:00 2001 From: kronecker <2026692687@qq.com> Date: Mon, 12 Oct 2026 23:06:38 +0800 Subject: [PATCH] fix: inherit scoped function ownership for Alembic migrations --- scripts/release/README.md | 6 +++++- scripts/release/database.py | 11 +++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/scripts/release/README.md b/scripts/release/README.md index 36ccb2e..53d02ca 100644 --- a/scripts/release/README.md +++ b/scripts/release/README.md @@ -66,7 +66,11 @@ refuses unexpected existing authority. There is no application table DDL or automatic account bootstrap: the migration login owns the database without global superuser/create-role/create-db/bypass-RLS rights; Backend Alembic remains the only schema/grant migration entry. Runtime logins cannot grant/set group -authority. See [PostgreSQL role membership](https://www.postgresql.org/docs/18/role-membership.html) +authority. The migration login inherits the narrow NOLOGIN function-owner role +and can SET that role, so unchanged Alembic can grant/rename its functions after +ownership transfer. Membership has no ADMIN option, adds no global role-creation +privilege, and is never given to runtime logins. See +[PostgreSQL role membership](https://www.postgresql.org/docs/18/role-membership.html) and [function ownership](https://www.postgresql.org/docs/18/sql-alterfunction.html). Always bootstrap validation from a separately trusted tool copy and an out-of-band diff --git a/scripts/release/database.py b/scripts/release/database.py index f43c80f..a43c959 100644 --- a/scripts/release/database.py +++ b/scripts/release/database.py @@ -114,16 +114,13 @@ async def _provision(operator: Path, endpoint_path: Path) -> dict: ) ] expected = ( - [] - if not login - else [(MEMBERSHIP[name], False, name != "kf_migrator", name == "kf_migrator")] + [] if not login else [(MEMBERSHIP[name], False, True, name == "kf_migrator")] ) if memberships and memberships != expected: raise DeliveryError("existing DB role membership differs") for login, group in MEMBERSHIP.items(): - inherit = "FALSE" if login == "kf_migrator" else "TRUE" set_role = "TRUE" if login == "kf_migrator" else "FALSE" - for option, value in (("ADMIN", "FALSE"), ("INHERIT", inherit), ("SET", set_role)): + for option, value in (("ADMIN", "FALSE"), ("INHERIT", "TRUE"), ("SET", set_role)): # Each command uses the documented single membership-option # form; all three changes are in the same transaction. await db.execute(f"GRANT {group} TO {login} WITH {option} {value}") @@ -164,7 +161,9 @@ def provision(operator: Path, endpoint_path: Path) -> dict: Pre-created NOLOGIN groups allow the unchanged Backend Alembic migrations to run under a separate non-superuser database owner. Only this migration - login can SET the authority-function owner; ordinary logins inherit their + login inherits and can SET the narrow authority-function owner, allowing + unchanged Alembic to grant/rename its already owned functions without global + superuser/create-role privileges. Ordinary logins inherit their own one group and cannot grant it. Unexpected existing authority is refused. """ if os.geteuid() != 0: