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
+36 -6
View File
@@ -8,28 +8,58 @@ from sqlalchemy.exc import IntegrityError
from kf_oj.core.settings import Settings
from kf_oj.infra.db.session import build_engine, build_session_factory
from kf_oj.modules.identity.authority import manage_authority
from kf_oj.modules.identity.service import IdentityService
async def run(command: str, username: str, password: str | None = None) -> None:
engine = build_engine(Settings())
settings = Settings()
authority_commands = {
"bootstrap-admin": "bootstrap",
"grant-admin": "grant",
"revoke-admin": "revoke",
"enable": "enable",
"disable": "disable",
}
if command != "create" and command not in authority_commands:
raise ValueError("unknown account command")
# Ordinary account creation uses the platform identity; authority management
# always requires a separately configured login and never falls back to it.
if command in authority_commands:
if settings.identity_operator_database_url is None:
raise ValueError("KF_OJ_IDENTITY_OPERATOR_DATABASE_URL is required")
if settings.identity_operator_database_url == settings.database_url:
raise ValueError("operator and platform database identities must be separate")
settings = settings.model_copy(
update={"database_url": settings.identity_operator_database_url}
)
engine = build_engine(settings)
try:
if command in authority_commands:
result = await manage_authority(
build_session_factory(engine), username, authority_commands[command]
)
print(
f"Account {result.username}: enabled={result.enabled}, "
f"administrator={result.is_administrator}, "
f"authorization_version={result.authorization_version}, changed={result.changed}."
)
return
service = IdentityService(build_session_factory(engine))
if command == "create":
assert password is not None
await service.create_account(username, password)
print("Account created.")
else:
if not await service.disable_account(username):
raise ValueError("account not found")
print("Account disabled; sessions revoked.")
finally:
await engine.dispose()
def main() -> None:
parser = argparse.ArgumentParser(description="Manage local OJ accounts")
parser.add_argument("command", choices=("create", "disable"))
parser.add_argument(
"command",
choices=("create", "disable", "enable", "bootstrap-admin", "grant-admin", "revoke-admin"),
)
parser.add_argument("username")
args = parser.parse_args()
password = None