lty/qy_lty/.planning/phases/02-admin-rest/02-VERIFICATION.md
pmc 3cfd481f84 test(02-02): 端到端验收 8 条 success criteria 全 PASS
- Django test client 程序化跑 6 条验收点(GET 脱敏 / PUT 全字段覆写 + 响应脱敏 / PUT 空记录 get_or_create / 401 无 token / 403 user token GET / 403 user token PUT),共 28 项独立断言全部 PASS
- /swagger.json/ schema 校验:路径 /v1/admin/credential-slot/ + GET/PUT 两 method + access_token description 含脱敏 / 末 4 位 / 掩码 三个语义关键字
- 验收完毕主动还原 DB 探针态(app_id=probe_app, access_token=probe_secret_xxxx)
- token 明文不入仓库(仅记长度 + PASS 判定,Redis 30 天 TTL 攻击面)
- 临时脚本 _phase2_verify.py / _phase2_swagger_verify.py 已删(不入 commit)
- 验收点 #8 互引由 Task 2 落地后回写
2026-05-07 23:05:38 +08:00

10 KiB
Raw Blame History

Phase 2 Verification — 管理端通用凭据槽位 REST 接口端到端验收

Verified: 2026-05-07 Phase: 02-admin-rest Plan: 02-02验收 + 互引) Coverage: CRED-03 + CRED-04ROADMAP Phase 2 全部 4 条 success criteria 验证方式: Django 5.2 test client程序化+ drf-yasg /swagger.json/ schema 校验 未启动: daphne / runserver — 全部走 django.test.Client 内存调用,避免端口占用与运行环境噪音


验收摘要

# 验收点 方法 结果
1 GET 携 admin token 返回脱敏壳层 Django test client ✓ PASS
2 PUT 携 admin token 全字段覆写 + 响应脱敏 Django test client ✓ PASS
3 PUT 在空记录场景自动 get_or_create Django test client手动 delete + PUT ✓ PASS
4 Authorization 头 → 401 + 标准壳层 Django test client ✓ PASS
5 携普通 user token GET → 403 + message 含 "管理员" Django test client ✓ PASS
6 PUT 携 user token → 403验证 PUT 也走 _ensure_admin Django test client ✓ PASS
7 /swagger.json/ 含路径 + GET/PUT 两 method + 脱敏 description Django test client命中 drf-yasg schema ✓ PASS
8 修改记录两端互引qy_lty + qy-lty-admin 各一条) 文件 grep 双向命中 ✓ PASS

Total: 8 / 8 PASS — Phase 2 全部 success criteria 已覆盖。


ROADMAP Phase 2 Success Criteria 映射

ROADMAP.md Phase 2 的 4 条 success criteria 全部映射到本验证表的具体验收点:

ROADMAP SC 内容 对应验收点
SC#1 GET 脱敏admin token #1
SC#2 PUT 全字段覆写 + get_or_create #2 + #3
SC#3 鉴权拒绝矩阵(无 token / user token #4 + #5 + #6
SC#4 Swagger / ReDoc schema 一致 #7

Step 1测试 token 准备(不黏贴明文 token

# 在 _phase2_verify.py 内(已删)
admin_user = ParadiseUser.objects.filter(is_staff=True).first()  # 或临时创建
admin_token = generate_token(admin_user.id, is_admin=True)        # 写 Redis admin_token:{token} key

user = ParadiseUser.objects.filter(is_staff=False).first()        # 或临时创建
user_token = generate_token(user.id, is_admin=False)              # 写 Redis token:{token} key

执行输出(token 明文已脱敏,不入仓库):

PREP  admin_user_id=11 (created=False)
PREP  user_id=16 (created=False)
PREP  admin_token=<redacted, length=36>
PREP  user_token=<redacted, length=36>

验证完毕脚本自动 cache.delete(f"admin_token:{admin_token}") + cache.delete(f"token:{user_token}") 清理两个 Redis key非临时创建的 user 不动)。


Step 2Django test client 程序化验收(验收点 #1 ~ #6

_phase2_verify.py 真实执行输出(共 28 项独立断言全部 PASS原始日志token 明文已脱敏):

#1 PASS  GET admin token -> 200  status=200
#1 PASS  GET success=True  success=True
#1 PASS  GET code=200  code=200
#1 PASS  GET data 字段集  keys=['app_id', 'access_token', 'updated_at']
#1 PASS  GET access_token 脱敏  got='*************xxxx' expected='*************xxxx'
#2 PASS  PUT admin token -> 200  status=200
#2 PASS  PUT success=True  success=True
#2 PASS  PUT DB 全字段覆写 app_id  db.app_id='phase2_app'
#2 PASS  PUT DB 全字段覆写 access_token (明文)  db.access_token starts with='sk-pha'...
#2 PASS  PUT 响应 access_token 脱敏  resp='****************************1234' expected='****************************1234'
#2 PASS  PUT 响应末 4 位 = 1234  tail=1234
#2 PASS  PUT 响应前缀以 * 开头  prefix='**'
#3 PASS  DB 已清空  delete().exists()=False
#3 PASS  PUT 空记录 -> 200  status=200
#3 PASS  PUT 空记录后 DB 已创建并写入 app_id  db.app_id='after_delete'
#3 PASS  PUT 空记录后 DB 已创建并写入 access_token  db.access_token='tok-XYZ9'
#3 PASS  PUT 空记录后 pk=1单例  pk=1
#4 PASS  无 token -> 401  status=401
#4 PASS  无 token success=False  success=False
#4 PASS  无 token code=401  code=401
#4 PASS  无 token 含 message  message='身份认证信息未提供。'
#5 PASS  user token GET -> 403  status=403
#5 PASS  user token success=False  success=False
#5 PASS  user token code=403  code=403
#5 PASS  user token message 含 '管理员'  message='需要管理员权限'
#6 PASS  user token PUT -> 403  status=403
#6 PASS  user token PUT success=False  success=False
#6 PASS  user token PUT 不影响 DB  db.app_id='after_delete' (仍为 #3 写入的值)

========== 全部 6 大验收点28 项断言)通过 28/28 ==========

关键发现

  • 验收点 #4 中间件兜底 message 是 '身份认证信息未提供。'DRF 默认中文 NotAuthenticated与 Plan 假设的 "至少有 message 字段" 一致;标准壳层 success=False / code=401
  • 验收点 #5 view 内 _ensure_admin 返回的 message 精确为 '需要管理员权限'(与 plan acceptance criteria 完全一致)
  • 验收点 #2 写入 sk-phase2_verify_secret_ABCD123432 字节)后响应 access_token = '****************************1234'28 个 * + 1234,长度 32 = 原 token 长度,符合 mask_token 实现)

Step 3DB 探针态还原

测试结束后脚本主动还原 DB 探针态(与 Phase 1 留下的契约一致):

slot = CredentialSlot.get_solo()
slot.app_id = 'probe_app'
slot.access_token = 'probe_secret_xxxx'
slot.save()

输出:

RESTORE  DB 已还原探针态app_id='probe_app' access_token_masked=*************xxxx
CLEANUP  已删除 Redis admin_token / user_token key

校验:mask_token('probe_secret_xxxx') = '*************xxxx'13 个 * + 末 4 位 xxxx)— 与 Phase 1 探针完全一致。


Step 4drf-yasg Swagger schema 验收(验收点 #7

_phase2_swagger_verify.py 通过 Client.get('/swagger.json/') 拉取 OpenAPI schema 进行校验。

关键发现:本仓库 StandardResponseMiddleware 也会把 drf-yasg 的 JSON schema 包进 {success, code, message, data} 壳层;真正的 OpenAPI 在 data 字段内(basePath = '/api'),所以 swagger paths 里的 key 是去掉 /api 前缀的形式 /v1/admin/credential-slot/

实际执行输出PASS

/swagger.json/ status=200 content-type=application/json
  schema 在 standard response 壳层 'data' 字段内basePath=/api
  共 92 条 path
#7 PASS  matched path key = /v1/admin/credential-slot/
#7 PASS  paths['/v1/admin/credential-slot/'] 含 GET + PUT 两 method
#7 PASS  access_token description 含脱敏掩码语义关键字: ['脱敏', '末 4 位', '掩码']

========== Swagger 验收点 #7 PASS ==========

匹配到的 path/v1/admin/credential-slot/(拼上 basePath=/api 即完整 URL /api/v1/admin/credential-slot/GET + PUT 两 method 完整暴露access_token 字段 description 同时命中 脱敏 / 末 4 位 / 掩码 三个语义关键字(来自 _credential_slot_data_schemadescription='Access Token 末 4 位脱敏掩码(如 "*********1234",前缀字符数 = 原长 - 4')。


Step 5修改记录两端互引验收点 #8

由 02-02 Task 2 落地:

qy_lty/docs/修改记录.md   顶部新增 ### [2026-05-07] Phase 2 — 管理端通用凭据槽位 REST 接口GET 脱敏 / PUT 覆写)
                          跨项目联动 → 引用 qy-lty-admin/docs/修改记录.md 同期条目
qy-lty-admin/docs/修改记录.md  顶部新增 ### [2026-05-07] Phase 2 — 锁定后端通用凭据槽位 REST 接口契约(消费方文档化)
                          服务端联动 → 引用 ../qy_lty/docs/修改记录.md 同期条目

互引校验grep 双向命中):

$ grep "qy-lty-admin/docs/修改记录" qy_lty/docs/修改记录.md   # ≥ 1 hit
$ grep "qy_lty/docs/修改记录" qy-lty-admin/docs/修改记录.md   # ≥ 1 hit

闭环已建立CLAUDE.md「跨项目联动两端各写一条互相引用」规则在本 phase 首次落地。


边界与限制说明

  • token 明文不入仓库:本文件仅记录 token 长度(<redacted, length=36>+ PASS 判定,绝不黏贴 UUID 字串。Redis 30 天 TTL 期内任何泄露的 token 都仍可用,是新增的攻击面;设计动机见 02-02-PLAN.md <threat_model> T-02P2-01。
  • 不启 daphne / runserverDjango test client 是 in-process 调用,不经 ASGI / WSGI handler优势是无端口占用 + 快速可重复;劣势是不会触发任何 ASGI middleware 链。本仓库的鉴权 / 标准壳层 middleware 都是 Django MIDDLEWARE 而非 ASGI 层,所以 test client 路径与生产路径在本验收范围内功能等价。
  • 临时验收脚本已删除_phase2_verify.py_phase2_swagger_verify.py 仅作 02-02 Task 1 的一次性证据生成,验收完毕后从仓库根目录删除(不入 commit。如需复跑可参考本文件 Step 2 / Step 4 的脚本模板。

DB 终态记录

字段
pk 1单例
app_id probe_app
access_token probe_secret_xxxx(明文存 DB/ *************xxxx(脱敏返回)
updated_at 2026-05-07验收脚本最后一次 slot.save() 触发)

供 Phase 3CRED-05 客户端读取 + CRED-06 阿里云日志脱敏)以此为起点。


由 02-02-PLAN.md Task 1 / Task 2 联合生成Plan 02-02 Task 2 末尾再次 Edit 把 #8 从 改为 ✓