feat(identity): protect administrator bootstrap and recovery

This commit is contained in:
2026-10-11 19:22:55 +08:00
parent a7b2324a3e
commit 234ab23155
12 changed files with 642 additions and 22 deletions
+2 -2
View File
@@ -3,6 +3,7 @@
import asyncio
import os
import subprocess
import sys
from pathlib import Path
from uuid import uuid4
@@ -40,8 +41,7 @@ def postgres_url():
def migrate(url: str, command: str, target: str | None = None) -> None:
subprocess.run(
[str(Path(__file__).resolve().parents[1] / ".venv/bin/alembic"), command]
+ ([target] if target else []),
[sys.executable, "-m", "alembic", command] + ([target] if target else []),
cwd=Path(__file__).resolve().parents[1],
env=os.environ | {"KF_OJ_DATABASE_URL": url},
check=True,
+14 -5
View File
@@ -118,7 +118,7 @@ def test_authority_columns_and_import_role_are_not_runtime_write_permissions(pos
assert await connection.fetchval(
"SELECT has_column_privilege('kf_oj_platform','accounts','enabled','UPDATE')"
)
assert await connection.fetchval(
assert not await connection.fetchval(
"SELECT has_column_privilege('kf_oj_identity_operator',"
"'accounts','is_administrator','UPDATE')"
)
@@ -146,7 +146,7 @@ def test_authority_change_derives_version_and_revokes_old_session_atomically(pos
async with connection.transaction():
await connection.execute("SET LOCAL ROLE kf_oj_identity_operator")
await connection.execute(
"UPDATE accounts SET is_administrator=true WHERE id=$1", account
"SELECT * FROM public.kf_manage_account_authority('storage_user','bootstrap')"
)
assert (
await connection.fetchval(
@@ -159,7 +159,7 @@ def test_authority_change_derives_version_and_revokes_old_session_atomically(pos
async with connection.transaction():
await connection.execute("SET LOCAL ROLE kf_oj_identity_operator")
await connection.execute(
"UPDATE accounts SET is_administrator=true WHERE id=$1", account
"SELECT * FROM public.kf_manage_account_authority('storage_user','bootstrap')"
)
assert (
await connection.fetchval(
@@ -175,10 +175,19 @@ def test_authority_change_derives_version_and_revokes_old_session_atomically(pos
token_digest(fresh_csrf),
account,
)
await connection.execute(
"INSERT INTO accounts(id,username,password_hash) "
"VALUES(gen_random_uuid(),'storage_recovery',repeat('synthetic-only-',5))"
)
async with connection.transaction():
await connection.execute("SET LOCAL ROLE kf_oj_identity_operator")
await connection.execute(
"SELECT * FROM public.kf_manage_account_authority('storage_recovery','grant')"
)
with pytest.raises(RollBackAuthorityChange):
async with connection.transaction():
await connection.execute(
"UPDATE accounts SET is_administrator=false WHERE id=$1", account
"SELECT * FROM public.kf_manage_account_authority('storage_user','revoke')"
)
assert (
await connection.fetchval(
@@ -200,7 +209,7 @@ def test_authority_change_derives_version_and_revokes_old_session_atomically(pos
async with connection.transaction():
await connection.execute("SET LOCAL ROLE kf_oj_identity_operator")
await connection.execute(
"UPDATE accounts SET is_administrator=false WHERE id=$1", account
"SELECT * FROM public.kf_manage_account_authority('storage_user','revoke')"
)
assert (
await connection.fetchval(
+219
View File
@@ -0,0 +1,219 @@
"""Real local authority/CLI transactions; run privilege faults in the independent VM."""
import asyncio
import os
import subprocess
import sys
import time
from pathlib import Path
from uuid import uuid4
import asyncpg
import pytest
from conftest import connect
from sqlalchemy.engine import make_url
from kf_oj.modules.identity.authority import manage_authority
async def seed(connection, *usernames):
for name in usernames:
await connection.execute(
"INSERT INTO accounts(id,username,password_hash) VALUES($1,$2,repeat('synthetic-',6))",
uuid4(),
name,
)
async def authority(connection, username, operation):
async with connection.transaction():
await connection.execute("SET LOCAL ROLE kf_oj_identity_operator")
return await connection.fetchrow(
"SELECT * FROM public.kf_manage_account_authority($1,$2)", username, operation
)
def test_explicit_bootstrap_repeated_grants_recovery_and_last_administrator(postgres_url):
async def run():
c = await connect(postgres_url)
try:
await seed(c, "first_admin", "second_admin")
assert await c.fetchval("SELECT count(*) FROM accounts WHERE is_administrator") == 0
with pytest.raises(asyncpg.CheckViolationError):
await authority(c, "first_admin", "grant")
first = await authority(c, "first_admin", "bootstrap")
assert first["changed"] and first["authorization_version"] == 2
assert not (await authority(c, "first_admin", "bootstrap"))["changed"]
assert not (await authority(c, "first_admin", "grant"))["changed"]
with pytest.raises(asyncpg.CheckViolationError):
await authority(c, "second_admin", "bootstrap")
for operation in ("disable", "revoke"):
with pytest.raises(asyncpg.CheckViolationError):
await authority(c, "first_admin", operation)
await authority(c, "second_admin", "grant")
disabled = await authority(c, "first_admin", "disable")
assert not disabled["enabled"] and disabled["is_administrator"]
assert disabled["authorization_version"] == 3
assert not (await authority(c, "first_admin", "disable"))["changed"]
restored = await authority(c, "first_admin", "enable")
assert restored["enabled"] and restored["authorization_version"] == 4
revoked = await authority(c, "first_admin", "revoke")
assert not revoked["is_administrator"] and revoked["authorization_version"] == 5
assert not (await authority(c, "first_admin", "revoke"))["changed"]
await authority(c, "first_admin", "grant")
with pytest.raises(asyncpg.NoDataFoundError):
await authority(c, "absent_user", "grant")
for role in ("kf_oj_platform", "kf_oj_coordinator", "kf_oj_problem_import"):
assert not await c.fetchval(
"SELECT has_function_privilege($1,"
"'kf_manage_account_authority(text,text)','EXECUTE')",
role,
)
for column in ("password_hash", "is_administrator", "enabled"):
assert not await c.fetchval(
"SELECT has_column_privilege('kf_oj_identity_operator','accounts',$1,'SELECT')",
column,
)
async with c.transaction():
await c.execute("SET LOCAL ROLE kf_oj_identity_operator")
for sql in (
"SELECT password_hash FROM accounts",
"UPDATE accounts SET is_administrator=false",
"SELECT token_hash FROM sessions",
):
with pytest.raises(asyncpg.InsufficientPrivilegeError):
async with c.transaction():
await c.execute(sql)
finally:
await c.close()
asyncio.run(run())
@pytest.mark.parametrize("isolation", ("read_committed", "repeatable_read", "serializable"))
def test_concurrent_different_account_revocations_preserve_one_admin(postgres_url, isolation):
async def serialized_run():
c, other = await connect(postgres_url), await connect(postgres_url)
first_tx = second_tx = pending = None
try:
await seed(c, "first_admin", "second_admin")
await authority(c, "first_admin", "bootstrap")
await authority(c, "second_admin", "grant")
first_tx, second_tx = (
c.transaction(isolation=isolation),
other.transaction(isolation=isolation),
)
await first_tx.start()
await second_tx.start()
assert (
await other.fetchval(
"SELECT count(*) FROM accounts WHERE is_administrator AND enabled"
)
== 2
)
await authority(c, "first_admin", "revoke")
pending = asyncio.create_task(authority(other, "second_admin", "revoke"))
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
if await c.fetchval(
"SELECT EXISTS(SELECT 1 FROM pg_locks WHERE pid=$1 AND NOT granted)",
other.get_server_pid(),
):
break
await asyncio.sleep(0.01)
else:
raise AssertionError("second authority operation never blocked on PG lock")
assert not pending.done()
await first_tx.commit()
first_tx = None
with pytest.raises((asyncpg.CheckViolationError, asyncpg.SerializationError)):
await asyncio.wait_for(pending, 5)
await second_tx.rollback()
second_tx = None
assert (
await c.fetchval("SELECT count(*) FROM accounts WHERE is_administrator AND enabled")
== 1
)
assert await c.fetchval(
"SELECT is_administrator FROM accounts WHERE username='second_admin'"
)
finally:
if pending is not None and not pending.done():
pending.cancel()
await asyncio.gather(pending, return_exceptions=True)
if first_tx is not None:
await first_tx.rollback()
if second_tx is not None:
await second_tx.rollback()
await c.close()
await other.close()
asyncio.run(serialized_run())
def test_real_cli_uses_dedicated_login_and_never_prints_database_secret(postgres_url):
role = "authority_test_" + uuid4().hex
async def setup():
c = await connect(postgres_url)
try:
await seed(c, "cli_admin", "cli_backup")
await c.execute(
f"CREATE ROLE {role} LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE "
"NOREPLICATION NOBYPASSRLS"
)
await c.execute(f"GRANT kf_oj_identity_operator TO {role}")
finally:
await c.close()
asyncio.run(setup())
url = make_url(postgres_url).set(username=role).render_as_string(hide_password=False)
env = os.environ | {
"KF_OJ_DATABASE_URL": postgres_url,
"KF_OJ_IDENTITY_OPERATOR_DATABASE_URL": url,
}
cwd = Path(__file__).resolve().parents[1]
def invoke(command, username, expected=0):
result = subprocess.run(
[sys.executable, "-m", "kf_oj.modules.identity.cli", command, username],
cwd=cwd,
env=env,
capture_output=True,
text=True,
timeout=120,
)
assert result.returncode == expected, result.stderr
assert url not in result.stdout + result.stderr
assert "Traceback" not in result.stderr
return result.stdout
try:
assert "changed=True" in invoke("bootstrap-admin", "cli_admin")
assert "changed=False" in invoke("bootstrap-admin", "cli_admin")
invoke("revoke-admin", "cli_admin", 1)
invoke("grant-admin", "cli_backup")
invoke("disable", "cli_admin")
invoke("enable", "cli_admin")
invoke("revoke-admin", "cli_admin")
invoke("grant-admin", "cli_admin")
finally:
async def cleanup():
c = await connect(postgres_url)
try:
await c.execute(f"REVOKE kf_oj_identity_operator FROM {role}")
await c.execute(f"DROP ROLE {role}")
finally:
await c.close()
asyncio.run(cleanup())
def test_bad_cli_operation_rejected_without_connecting():
async def run():
for username, operation in (("invalid name", "grant"), ("valid_name", "unknown")):
with pytest.raises(ValueError):
await manage_authority(None, username, operation)
asyncio.run(run())
+2 -1
View File
@@ -12,13 +12,14 @@ def test_migration_roundtrip_and_role_safety(postgres_url):
c = await connect(postgres_url)
try:
tables = set(await c.fetch("SELECT tablename FROM pg_tables WHERE schemaname='public'"))
assert len(tables) == 14 # Thirteen business tables and Alembic history.
assert len(tables) == 15 # Fourteen domain/control tables and Alembic history.
roles = await c.fetch("SELECT * FROM pg_roles WHERE rolname LIKE 'kf_oj_%'")
assert {row["rolname"] for row in roles} == {
"kf_oj_platform",
"kf_oj_coordinator",
"kf_oj_identity_operator",
"kf_oj_problem_import",
"kf_oj_identity_authority_owner",
}
assert all(
not any(
+2 -4
View File
@@ -219,7 +219,7 @@ def test_real_concurrent_logins_keep_bounded_active_sessions(postgres_url):
asyncio.run(run())
def test_account_cli_hidden_password_and_disable(postgres_url):
def test_account_cli_hidden_password_and_session_disable(postgres_url):
password = secrets.token_hex(16)
# The CLI prompts through a real private PTY; the password is never an argument/environment.
pid, descriptor = pty.fork()
@@ -264,9 +264,7 @@ def test_account_cli_hidden_password_and_disable(postgres_url):
try:
service = IdentityService(sessions)
issued = await service.login("cli_user", password, peer="cli-fixture")
from kf_oj.modules.identity.cli import run
await run("disable", "cli_user")
assert await service.disable_account("cli_user")
with pytest.raises(AuthenticationFailed):
await service.authenticate(issued.token)
finally:
+20 -1
View File
@@ -34,10 +34,27 @@ def test_capability_wire_version_and_default_denial():
async def set_administrator(connection, account_id, enabled):
username = await connection.fetchval("SELECT username FROM accounts WHERE id=$1", account_id)
active = await connection.fetchval(
"SELECT count(*) FROM accounts WHERE enabled AND is_administrator"
)
async with connection.transaction():
await connection.execute("SET LOCAL ROLE kf_oj_identity_operator")
operation = ("grant" if active else "bootstrap") if enabled else "revoke"
await connection.execute(
"SELECT * FROM public.kf_manage_account_authority($1,$2)", username, operation
)
async def add_recovery_administrator(connection):
await connection.execute(
"INSERT INTO accounts(id,username,password_hash) "
"VALUES(gen_random_uuid(),'authority_recovery',repeat('synthetic-only-',5))"
)
async with connection.transaction():
await connection.execute("SET LOCAL ROLE kf_oj_identity_operator")
await connection.execute(
"UPDATE accounts SET is_administrator=$1 WHERE id=$2", enabled, account_id
"SELECT * FROM public.kf_manage_account_authority('authority_recovery','grant')"
)
@@ -74,6 +91,7 @@ def test_live_http_capabilities_and_old_cookies_after_grant_and_revoke(postgres_
)
assert actual.json() == user
assert actual.headers["cache-control"] == "no-store"
await add_recovery_administrator(connection)
await set_administrator(connection, account.account_id, False)
assert (await client.get("/api/v1/auth/me")).status_code == 401
refused = await client.post(
@@ -105,6 +123,7 @@ def test_write_transaction_rechecks_session_and_serializes_authority_change(post
principal = await identity.authenticate(issued.token)
assert principal.can_maintain_problems and principal.authorization_version == 2
assert principal.session_token_hash not in repr(principal)
await add_recovery_administrator(connection)
async with sessions.begin() as session:
await identity.require_problem_maintenance(session, principal)
pending = asyncio.create_task(set_administrator(other, account.account_id, False))