From 2f860a17c9e4595523f294354acf9ee13bd90a23 Mon Sep 17 00:00:00 2001 From: kronecker <2026692687@qq.com> Date: Mon, 12 Oct 2026 03:15:51 +0800 Subject: [PATCH] docs(admin): document bounded problem maintenance operations --- README.md | 7 +- backend/migrations/README.md | 16 +++++ deploy/README.md | 3 + deploy/admin-problems.md | 126 +++++++++++++++++++++++++++++++++++ 4 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 deploy/admin-problems.md diff --git a/README.md b/README.md index 08dc992..043ea74 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,11 @@ helper 工厂已接通 C17/C++20、固定 CPython 3.13 与 OpenJDK 21 的普通题评测;登录、题库、提交和个人成绩页面 使用真实接口。训练、比赛、社区等扩展页面仍为开发预览。 +管理控制台使用同源 Cookie 和服务端维护权限,支持有界 ZIP 题包上传、候选预览、 +不可变版本发布、题目下架/恢复及维护审计。初次授权与恢复走独立运维身份的本地 CLI; +导入 Worker 仅解析数据,不执行题包内容或学生代码。操作、限额和恢复步骤见 +[题目维护运维说明](deploy/admin-problems.md)。 + 题目工作台支持当前版本的“本题我的提交”、右侧详情与完整个人详情。 性能展示读取受验证最终 AC 的 CPU 总和与内存峰值,只比较同版本、语言、精确工具链、 预算与可信测量环境的最近有效记录。缺测、旧环境无法验证或其他记录不足五条时明确显示 @@ -17,7 +22,7 @@ helper 工厂已接通 C17/C++20、固定 CPython 3.13 与 OpenJDK 21 的普通 | 目录 | 职责 | | --- | --- | -| `backend/` | Python 模块化单体、平台 API、学习 Worker、待投递事件发布器及唯一数据库迁移入口 | +| `backend/` | Python 模块化单体、平台 API、题包导入 Worker、待投递事件发布器及唯一数据库迁移入口 | | `judge/` | Rust workspace:协议、沙箱、判题编排、判题 Worker 和协调器 | | `frontend/` | 页面、路由、请求生命周期与生成的 API 类型 | | `contracts/` | 版本化 JSON Schema 与 OpenAPI 快照 | diff --git a/backend/migrations/README.md b/backend/migrations/README.md index 3a6ed9a..c20bec2 100644 --- a/backend/migrations/README.md +++ b/backend/migrations/README.md @@ -9,6 +9,22 @@ upgrade head`,API 不在启动时创建表。迁移账号拥有该数据库对 协调器组领取和更新评测、插入 attempt / 用例成绩及恢复通知,不能修改账号、题库或提交身份。 Worker 没有数据库角色,也不需要数据库连接配置。 +后续迁移另建 `kf_oj_identity_operator` 与 `kf_oj_problem_import` 两个受限 `NOLOGIN` +权限组。前者只通过固定账号授权函数维护管理员能力/启用状态,不能读取口令哈希、 +直接修改角色字段或绕过最后一名启用管理员保护;后者仅处理已授权导入、候选/工件与 +追加审计,不能写评测状态、attempt、终态或表现指标。判题 Worker 继续无数据库身份。 +平台写入权按列收敛;当前会话与授权版本在每次维护事务内重新核对。 + +可信表现指标由 Coordinator 在有效最终报告事务中派生,平台、导入器和普通 Worker +不得自行写入。候选与发布版本/用例不可变,成功维护回执及审计只追加;清理保留所有 +已发布版本、合法旧提交和未结束候选的工件引用。 + +升级前停止新维护写入、备份专用数据库与私有工件,并用独立迁移账号执行升级和 +`uv run --locked alembic check`。既有登录角色继续绑定各自最小权限组,迁移账号不进入 +运行服务配置。降级不作为丢弃候选或回滚一次发布的手段;含新语言/已引用数据时的 +降级拒绝须先核对原因,不能清空历史来通过检查。账号 bootstrap/恢复步骤见 +[维护运维说明](../../deploy/admin-problems.md)。 + 服务使用各自独立的普通 `LOGIN` 角色,分别加入对应权限组;登录角色不能拥有数据库/表、 不能加入迁移账号或其他高权限组,且必须为 `NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS`。 初始迁移拒绝已有的高权限同名组和继承其他角色的同名组。应用启动时不运行 migration。 diff --git a/deploy/README.md b/deploy/README.md index b24fdf5..386f75b 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -1,5 +1,8 @@ # 本地开发依赖 +管理员账号 bootstrap/恢复、有界题包解析服务、不可变发布/下架、审计与回收步骤见 +[题目维护运维说明](admin-problems.md)。该说明与服务模板用于受控部署准备,不表示已部署生产。 + `compose.dev.yml` 只提供 PostgreSQL 和 RabbitMQ,端口绑定 localhost。 先复制 `.env.example` 为 `.env`,然后在仓库根目录运行: diff --git a/deploy/admin-problems.md b/deploy/admin-problems.md new file mode 100644 index 0000000..601d418 --- /dev/null +++ b/deploy/admin-problems.md @@ -0,0 +1,126 @@ +# 题目维护的运行与恢复 + +本说明描述当前工程接口与受限服务配置。独立 VM 验收使用合成数据与仅 VM 有效的 +临时口令,生产迁移、安装服务和部署仍需单独安排。运行服务用项目锁文件、Python +3.13.16 和 uv;请求不调用 uvx/pip 或联网安装依赖。 + +## 迁移、账号和授权 + +Backend Alembic 是唯一迁移入口。先停止新维护写入并备份专用数据库/私有工件, +以数据库所有者的独立配置执行下列命令;随后切换到平台配置创建普通账号: + +```sh +cd backend +uv run --locked alembic upgrade head +uv run --locked alembic check +uv run --locked kf-accounts create maintenance_admin +``` + +用户名是示例,可按实际账号替换;密码仅在终端交互读取,不写入命令、仓库或浏览器 +存储。迁移、平台、Coordinator、授权运维和导入服务分别使用专用登录身份,均不共享 +高权限账号。普通登录角色为 `NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS`, +只继承对应 `NOLOGIN` 权限组。判题 Worker 无数据库连接。 + +通过仓库外的私有配置设置 `KF_OJ_IDENTITY_OPERATOR_DATABASE_URL`,只为授权 CLI +提供继承 `kf_oj_identity_operator` 的专用身份;它必须与平台数据库身份不同。然后执行: + +```sh +uv run --locked kf-accounts bootstrap-admin maintenance_admin +uv run --locked kf-accounts grant-admin recovery_admin +uv run --locked kf-accounts revoke-admin maintenance_admin +uv run --locked kf-accounts enable maintenance_admin +``` + +恢复账号必须先存在。bootstrap 只用于没有启用管理员时;后续授权使用 grant。 +数据库在固定操作函数中序列化变更,禁止撤销或禁用最后一名启用管理员。授权版本 +变化会使旧会话失效,管理员须重新登录。不要直接更新管理员字段来绕过保护。 + +## 私有存储与导入服务 + +API 与普通 Python 导入 Worker 使用同一服务用户可访问的私有工件/暂存目录,目录 +归该用户所有、权限 0700;数据库登录身份分别继承 `kf_oj_platform` 和 +`kf_oj_problem_import`。导入 Worker 不执行上传内容、参考解或学生代码,不调用 sudo, +不写评测数据库中的执行状态/成绩,也不持有 Coordinator 或 SSH/GitHub 凭据。 + +外部私有配置需提供 `KF_OJ_DATABASE_URL`、`KF_OJ_ARTIFACT_ROOT`、 +`KF_OJ_IMPORT_STAGING_ROOT`、可信 `KF_OJ_JUDGE_ADMISSION` 与 +`KF_OJ_IMPORT_WORKER_CGROUP`。工件根、精确固定镜像及准入须与平台一致,网页题包 +不能改写工具链/镜像/权限。示例摘要不能作为真实镜像摘要使用。 + +[导入服务模板](problem-import-worker.service.example) 使用普通 `User=kf-oj`, +128 MiB 内存、swap 0、PIDs 16、最多一核、文件 64 MiB、FD 64、core 0、 +`NoNewPrivileges`、整组停止和 30 秒故障重启间隔。部署方先核对实际用户、路径和 +环境文件再安装为 `kf-problem-import.service`;其 PrivateNetwork/AF_UNIX 要求使用 +可访问的 PostgreSQL Unix socket。API 的连接路径可不同,但必须连到同一专用数据库。 +配置好实际受限 cgroup 后,组合入口为 `uv run --locked kf-problem-import-worker`。 +仅解析配置或存在模板不代表实际资源隔离已经验证;启动及每次解析均读回必要上限, +身份、目录、上限或新鲜资源证明缺失时拒绝处理并报告不可用。 + +在已安装且路径核对的服务上,可信运维可执行: + +```sh +systemctl start kf-problem-import.service +systemctl show kf-problem-import.service -p MainPID -p ControlGroup -p MemoryMax -p MemorySwapMax -p TasksMax -p CPUQuotaPerSecUSec +systemctl stop kf-problem-import.service +``` + +停止后读回对应 cgroup 的 `pids.current=0`,再处理自己确认的临时文件;不得只观察 +父 PID。父进程异常退出时,活解析子进程仍持有内核文件锁,GC 不能提前释放或复用 +该槽位。恢复使用原服务配置,让有界租约/重试收敛;不通过删除任务记录或放宽上限 +恢复。错误配置的 Worker 不写就绪证明,网页维护能力明确不可用。 + +## 题包、候选与发布 + +管理入口为同源 `/admin/index.html#/problems`,共享安全 Cookie/CSRF 与实时账号权限。 +普通用户没有维护能力。题包只接受 stored/deflate 单层 ZIP,`problem.json` 清单、 +UTF-8 题面、显式公开样例与隐藏输入/答案必须分别声明。只接受有界 ASCII 普通文件名; +拒绝链接、设备、路径穿越、重复名称、加密/嵌套/未知成员、损坏 CRC 和虚假大小。 +不接收外部下载 URL、生成器、脚本或 SPJ,不执行包内内容。 + +压缩及实际解包分别最多 64 MiB,最多 534 个文件;上传最多 4096 帧、每帧 1 MiB、 +整体 30 秒;解析整体 60 秒。每次暂存预约 128 MiB,总预约 256 MiB,上传/解析 +全局并发 1、每管理员 1,未结束候选本人最多 10 个/全实例 32 个,操作 24 小时到期, +解析死亡最多领取两次。能力接口返回实际固定边界,管理员不能在清单中放宽。 +题包规范与固定语言入口见[题目模块](../backend/src/kf_oj/modules/problems/README.md)。 + +先以稳定 UUID、实际内容 SHA-256 和字节数建立导入,再向同一操作流式 PUT。 +双击、超时、响应丢失和刷新沿用原 UUID;变更内容创建新操作。浏览器只保存账号、 +UUID、摘要、字节数和前置条件,不保存包正文、口令、会话或管理员能力。 +自动状态查询有界,失败后手动核对原操作。`ready` 是未发布候选,学生不能读取。 +候选预览只含公开题面/样例/预算,隐藏答案/工件引用和宿主路径不会下发。 + +发布使用独立稳定 UUID、本人候选/导入 UUID 与预期活动版本;事务内复核当前授权、 +准入和工件实际内容,原子创建新 revision、切换活动版本并追加审计。修改题目通过 +新包发布新不可变版本;不得修改已发布内容或硬删被提交引用的旧版本。 +发布响应不确定时读回原维护回执与导入状态,不能先在页面声称成功。409 后读回 +最新活动版本/状态,再由操作者决定是否按新前置条件操作,不静默覆盖。 + +下架关闭公开读取和新提交,合法已受理任务继续通过有效租约读取原已发布版本; +本人历史/成绩保留。恢复只切换可用性,不重评旧提交。题目内提交与源码仍只对本人 +开放;性能只比较同不可变版本、语言、精确镜像/工具链/预算和可信测量环境的最终 AC, +缺测/无法证明/其他样本不足五条明确不可比较。 + +## 审计、故障和回收 + +管理端有限分页展示真实题目、版本历史、本人导入/维护回执和维护审计。 +审计含实际主体、对象、结果、时间和操作 UUID,不记录口令、隐藏正文;应用角色没有 +UPDATE/DELETE 审计权限。失败/回滚不会创建成功回执、半发布版本或成功审计。 +比赛、节点、站点设置等管理区仍明确未实现。 + +408 表示实际上传期限超出;409 表示内容/版本/状态冲突;413/415 为字节或格式拒绝; +503 可能是资源槽位占用、配置/服务不可用。先检查原 UUID、真实状态、Worker 心跳、 +数据库预约及实际 cgroup,按原因有限重试,不能密集重试或把错误当成成功。 +解析服务/API 重启后保留持久化操作和原 UUID,让有界恢复完成。丢弃、失败和到期 +先清理暂存字节再释放预约;未知/仍被活解析进程锁定的资源保持拒绝并供运维核对。 + +工件收集器每次只处理有界老且无引用的数据: + +```sh +uv run --locked kf-artifacts --limit 100 +``` + +按返回的 metadata cursor 分次继续核对;候选、已发布用例、旧提交/成绩的引用必须 +保留。不要手工清空私有工件目录、删除审计或通过改写预约数字腾容量。 +业务数据、口令、题包、镜像/构建产物及本地验收记录不进入工程提交。 +本地阶段保存使用明确工程分支与串行分次推送,CI/CD 如需运行只使用约定的 GitHub +私有仓库和明确并发限制;Gitea 不执行 workflow。