feat(identity): protect administrator bootstrap and recovery
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user