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
@@ -14,6 +14,15 @@
登录接口为 `/api/v1/auth/login`,退出 `/logout`,当前主体 `/me`。会话使用随机 256-bit token,
数据库只保存 token / CSRF 摘要;默认 12 小时有效,单账号保留至多 10 个有效会话。
账号禁用立即拒绝登录和旧会话。请求只采用 ASGI 连接 peer,不读取客户端转发地址。
`0005_identity_authority` 为旧账号保留普通用户默认值,并加入管理员标记与服务端派生的授权版本。
启用状态或管理员标记实际变化时,同一事务递增授权版本并撤销旧会话;事务回滚也回滚撤销。
平台账号仅可插入普通账号列、更新启用状态,不能写管理员标记或授权版本;管理员的启用状态
需要独立本地运维身份。`kf_oj_identity_operator` 与 `kf_oj_problem_import` 为无登录的独立权限组,
口令与登录身份由运维另行配置,平台不继承运维组。导入组目前没有表权限,后续导入迁移只授予
队列与候选所需权限,不授予评测状态写入权。迁移不创建默认管理员;管理员 CLI、公开能力端口
及管理 API 尚待接通。保留授权或撤销历史后拒绝回退这次迁移。
受控启动使用 `--no-proxy-headers`,避免服务器中间件预先替换 peer;反向代理后的用户共享
该连接预算,引入可信代理的独立限流策略之前不能直接打开客户端转发头信任。
每 peer 至多 10 次登录/15 分钟,预算通过真实数据库串行更新,失败尝试也消耗预算。
+19 -1
View File
@@ -3,7 +3,17 @@
from datetime import datetime
from uuid import UUID
from sqlalchemy import Boolean, CheckConstraint, DateTime, ForeignKey, Index, String, Text
from sqlalchemy import (
BigInteger,
Boolean,
CheckConstraint,
DateTime,
ForeignKey,
Index,
String,
Text,
text,
)
from sqlalchemy.orm import Mapped, mapped_column
from kf_oj.infra.db.base import Base
@@ -15,12 +25,20 @@ class Account(CreatedAt, Base):
__table_args__ = (
CheckConstraint("username ~ '^[a-z0-9_]{3,24}$'", name="canonical_username"),
CheckConstraint("char_length(password_hash) BETWEEN 40 AND 256", name="password_hash_size"),
CheckConstraint("authorization_version >= 1", name="authorization_version"),
Index(
"ix_accounts_active_administrators",
"id",
postgresql_where=text("is_administrator AND enabled"),
),
)
id: Mapped[UUID] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(String(24), unique=True)
password_hash: Mapped[str] = mapped_column(Text)
enabled: Mapped[bool] = mapped_column(Boolean, server_default="true")
is_administrator: Mapped[bool] = mapped_column(Boolean, server_default="false")
authorization_version: Mapped[int] = mapped_column(BigInteger, server_default="1")
class Session(CreatedAt, Base):