feat(identity): persist administrator authority with scoped database roles

This commit is contained in:
2026-10-11 12:20:10 +08:00
parent 8143a7f665
commit 3ed5b72459
5 changed files with 420 additions and 3 deletions
+235
View File
@@ -0,0 +1,235 @@
"""Actual temporary PG migration and authority storage; API/CLI acceptance is separate."""
import asyncio
import subprocess
from datetime import timedelta
from uuid import uuid4
import asyncpg
import pytest
from conftest import connect, migrate
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from test_database_foundation import fixture_rows
from kf_oj.modules.identity.public import AuthenticationFailed, Principal
from kf_oj.modules.identity.service import IdentityService, token_digest
class RollBackAuthorityChange(Exception):
"""Abort the test transaction before any authority change commits."""
async def submission_snapshot(connection):
# Synthetic queued metadata only; this does not claim real program execution.
return {
table: list(
await connection.fetch(
f"SELECT to_jsonb(t)::text FROM {table} t ORDER BY to_jsonb(t)::text"
)
)
for table in (
"artifacts",
"problems",
"problem_versions",
"problem_cases",
"submissions",
"evaluations",
"outbox",
)
}
async def account_and_session(connection):
account = uuid4()
token, csrf = uuid4().hex * 2, uuid4().hex * 2
await connection.execute(
"INSERT INTO accounts(id,username,password_hash) VALUES($1,'storage_user',$2)",
account,
"synthetic-irreversible-placeholder-hash-" * 2,
)
now = await connection.fetchval("SELECT clock_timestamp()")
await connection.execute(
"INSERT INTO sessions(token_hash,csrf_hash,account_id,expires_at) VALUES($1,$2,$3,$4)",
token_digest(token),
token_digest(csrf),
account,
now + timedelta(hours=1),
)
return account, token, csrf
def test_upgrade_preserves_ordinary_account_and_current_session(postgres_url):
migrate(postgres_url, "downgrade", "0004_performance_watermark")
async def seed():
connection = await connect(postgres_url)
try:
account, token, csrf = await account_and_session(connection)
await fixture_rows(connection)
return account, token, csrf, await submission_snapshot(connection)
finally:
await connection.close()
account, token, csrf, before = asyncio.run(seed())
migrate(postgres_url, "upgrade", "head")
migrate(postgres_url, "check")
async def inspect():
connection = await connect(postgres_url)
engine = create_async_engine(postgres_url)
try:
row = await connection.fetchrow("SELECT * FROM accounts WHERE id=$1", account)
assert row["enabled"] and not row["is_administrator"]
assert row["authorization_version"] == 1
assert await submission_snapshot(connection) == before
service = IdentityService(async_sessionmaker(engine, expire_on_commit=False))
assert await service.authenticate(token, csrf_token=csrf) == Principal(
account, "storage_user"
)
finally:
await connection.close()
await engine.dispose()
asyncio.run(inspect())
def test_authority_columns_and_import_role_are_not_runtime_write_permissions(postgres_url):
async def inspect():
connection = await connect(postgres_url)
try:
for role in ("kf_oj_platform", "kf_oj_coordinator", "kf_oj_problem_import"):
for column in ("is_administrator", "authorization_version"):
assert not await connection.fetchval(
"SELECT has_column_privilege($1,'accounts',$2,'UPDATE')", role, column
)
assert not await connection.fetchval(
"SELECT has_column_privilege($1,'accounts',$2,'INSERT')", role, column
)
for table in (
"evaluations",
"judge_attempts",
"case_results",
"evaluation_performance",
):
for privilege in ("SELECT", "INSERT", "UPDATE", "DELETE"):
assert not await connection.fetchval(
"SELECT has_table_privilege('kf_oj_problem_import',$1,$2)", table, privilege
)
assert await connection.fetchval(
"SELECT has_column_privilege('kf_oj_platform','accounts','enabled','UPDATE')"
)
assert await connection.fetchval(
"SELECT has_column_privilege('kf_oj_identity_operator',"
"'accounts','is_administrator','UPDATE')"
)
assert not await connection.fetchval(
"SELECT has_column_privilege('kf_oj_identity_operator',"
"'accounts','authorization_version','UPDATE')"
)
assert not await connection.fetchval(
"SELECT pg_has_role('kf_oj_platform','kf_oj_identity_operator','USAGE')"
)
finally:
await connection.close()
asyncio.run(inspect())
def test_authority_change_derives_version_and_revokes_old_session_atomically(postgres_url):
async def inspect():
connection = await connect(postgres_url)
engine = create_async_engine(postgres_url)
try:
account, token, csrf = await account_and_session(connection)
service = IdentityService(async_sessionmaker(engine, expire_on_commit=False))
await service.authenticate(token, csrf_token=csrf)
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
)
assert (
await connection.fetchval(
"SELECT authorization_version FROM accounts WHERE id=$1", account
)
== 2
)
with pytest.raises(AuthenticationFailed):
await service.authenticate(token, csrf_token=csrf)
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
)
assert (
await connection.fetchval(
"SELECT authorization_version FROM accounts WHERE id=$1", account
)
== 2
)
fresh_token, fresh_csrf = uuid4().hex * 2, uuid4().hex * 2
await connection.execute(
"INSERT INTO sessions(token_hash,csrf_hash,account_id,expires_at) "
"VALUES($1,$2,$3,clock_timestamp()+interval '1 hour')",
token_digest(fresh_token),
token_digest(fresh_csrf),
account,
)
with pytest.raises(RollBackAuthorityChange):
async with connection.transaction():
await connection.execute(
"UPDATE accounts SET is_administrator=false WHERE id=$1", account
)
assert (
await connection.fetchval(
"SELECT authorization_version FROM accounts WHERE id=$1", account
)
== 3
)
raise RollBackAuthorityChange
assert await connection.fetchval(
"SELECT is_administrator FROM accounts WHERE id=$1", account
)
assert (
await connection.fetchval(
"SELECT authorization_version FROM accounts WHERE id=$1", account
)
== 2
)
await service.authenticate(fresh_token, csrf_token=fresh_csrf)
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
)
assert (
await connection.fetchval(
"SELECT authorization_version FROM accounts WHERE id=$1", account
)
== 3
)
with pytest.raises(AuthenticationFailed):
await service.authenticate(fresh_token, csrf_token=fresh_csrf)
finally:
await connection.close()
await engine.dispose()
asyncio.run(inspect())
def test_authority_history_cannot_be_erased_by_downgrade(postgres_url):
async def change():
connection = await connect(postgres_url)
try:
account, _, _ = await account_and_session(connection)
await connection.execute("UPDATE accounts SET enabled=false WHERE id=$1", account)
with pytest.raises(asyncpg.CheckViolationError):
await connection.execute(
"UPDATE accounts SET authorization_version=1 WHERE id=$1", account
)
finally:
await connection.close()
asyncio.run(change())
with pytest.raises(subprocess.CalledProcessError):
migrate(postgres_url, "downgrade", "0004_performance_watermark")
+7 -2
View File
@@ -12,9 +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) == 13 # Twelve business tables and Alembic history.
assert len(tables) == 14 # Thirteen business 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"}
assert {row["rolname"] for row in roles} == {
"kf_oj_platform",
"kf_oj_coordinator",
"kf_oj_identity_operator",
"kf_oj_problem_import",
}
assert all(
not any(
r[k]