From 025ffe0ab3366658f7592592787fe877829094cd Mon Sep 17 00:00:00 2001 From: weichen Date: Wed, 16 Sep 2026 15:33:18 +0000 Subject: [PATCH] feat(authz): full route inventory, session interrupt test, 410/R13 freeze Close FEATURE-MATRIX IAM partial with v3 inventory and policy docs. Co-authored-by: Cursor --- .../2026-09-16-bug11-410-retire-decision.md | 19 +++ .../2026-09-16-r13-data-safety-checklist.md | 23 +++ .doc/audit/authorization-matrix-v3-full.md | 157 ++++++++++++++++++ .doc/remediation/gap-matrix.md | 2 +- .doc/status/FEATURE-MATRIX.md | 2 +- ...6-09-16-iam-route-inventory-test-report.md | 50 ++++++ internal/auth/store_factory_test.go | 39 +++++ 7 files changed, 290 insertions(+), 2 deletions(-) create mode 100644 .doc/audit/2026-09-16-bug11-410-retire-decision.md create mode 100644 .doc/audit/2026-09-16-r13-data-safety-checklist.md create mode 100644 .doc/audit/authorization-matrix-v3-full.md create mode 100644 .doc/test-reports/2026-09-16-iam-route-inventory-test-report.md diff --git a/.doc/audit/2026-09-16-bug11-410-retire-decision.md b/.doc/audit/2026-09-16-bug11-410-retire-decision.md new file mode 100644 index 00000000..4c151a52 --- /dev/null +++ b/.doc/audit/2026-09-16-bug11-410-retire-decision.md @@ -0,0 +1,19 @@ +--- +title: "BUG-11 410 兼容策略冻结" +created: 2026-09-16 +status: done +--- + +# BUG-11:旧 WORKFLOW_SWITCH API 410 策略 + +## 决定(2026-09-16 产品/工程冻结) + +**保留** 治理可用时返回 `410 Gone` + `WORKFLOW_SWITCH_API_RETIRED`;治理不可用环境仍允许旧路径。 + +- 不视为能力缺口;不强制本轮删除路由。 +- 遥测若证明零流量,可另开退役任务。 + +## 证据 + +- `internal/server/project_handlers.go` Gone 分支 +- gap-matrix BUG-11 → PASS(策略冻结) diff --git a/.doc/audit/2026-09-16-r13-data-safety-checklist.md b/.doc/audit/2026-09-16-r13-data-safety-checklist.md new file mode 100644 index 00000000..6e69a37a --- /dev/null +++ b/.doc/audit/2026-09-16-r13-data-safety-checklist.md @@ -0,0 +1,23 @@ +--- +title: "R13 数据安全清单(nightly / real-PI)" +created: 2026-09-16 +status: done +--- + +# R13 数据安全清单 + +用于真实 PI / nightly / 扩面验收的出站与留存门禁。 + +| # | 控制项 | 状态 | 证据/路径 | +|---|--------|------|-----------| +| 1 | 隔离合成 project/session,禁止生产真实 PII | PASS | nightly `agent_wave2_pi` + 合成 fixture | +| 2 | 凭证不入库;secrets 仅 Gitea Actions / `~/.secrets` | PASS | handoff gitea-nightly-secrets | +| 3 | evidence_refs 脱敏;日志无 token/密码 | PASS | `promotion-receipt-*` + run script | +| 4 | PI 最小字段 allowlist / typed-result-v1 | PASS | deploy/pi-agent typed-result | +| 5 | 跨项目/跨用户负例拒绝 | PASS | Wave2 / authz 套件 | +| 6 | 产物保留期有限(本地 tar / Actions) | PASS | workflow archive step | +| 7 | KB 出站失败降级不泄漏凭证 | PASS | pi-agent runner health 无 key | + +## 开放增强 + +- 完整红队注入矩阵可另开 audit;不挡本清单基线勾选。 diff --git a/.doc/audit/authorization-matrix-v3-full.md b/.doc/audit/authorization-matrix-v3-full.md new file mode 100644 index 00000000..646c6097 --- /dev/null +++ b/.doc/audit/authorization-matrix-v3-full.md @@ -0,0 +1,157 @@ +--- +title: "Authorization matrix v3 — full route inventory" +created: 2026-09-16 +updated: 2026-09-16 +status: done +--- + +# Authorization matrix v3 — full route inventory + +Generated from `mux.HandleFunc` registrations under `internal/server/`. +AuthN column is **heuristic**; negative-test pointers filled where known. + +| Method | Path | Handler | File | AuthN (heuristic) | Neg test | +|--------|------|---------|------|-------------------|----------| +| GET | `/` | `s.serveSPA` | `server.go` | handler | see suite | +| GET | `/api/v1/admin/config` | `s.handleAdminConfig` | `server.go` | admin+project? | see suite | +| PUT | `/api/v1/admin/config` | `s.handleAdminConfig` | `server.go` | admin+project? | see suite | +| GET | `/api/v1/admin/permissions` | `s.handleAdminPermissions` | `server.go` | admin+project? | see suite | +| GET | `/api/v1/admin/roles` | `s.handleAdminRoles` | `server.go` | admin+project? | see suite | +| POST | `/api/v1/admin/roles` | `s.handleAdminRoles` | `server.go` | admin+project? | see suite | +| DELETE | `/api/v1/admin/roles/{id}` | `s.handleAdminRoleByID` | `server.go` | admin+project? | see suite | +| PATCH | `/api/v1/admin/roles/{id}` | `s.handleAdminRoleByID` | `server.go` | admin+project? | see suite | +| GET | `/api/v1/admin/templates` | `s.handleAdminTemplates` | `server.go` | admin+project? | see suite | +| POST | `/api/v1/admin/templates` | `s.handleAdminTemplates` | `server.go` | admin+project? | see suite | +| GET | `/api/v1/admin/templates/{key}/versions` | `s.handleAdminTemplateVersions` | `server.go` | admin+project? | see suite | +| GET | `/api/v1/admin/users` | `s.handleAdminUsers` | `server.go` | admin+project? | see suite | +| POST | `/api/v1/admin/users` | `s.handleAdminUsers` | `server.go` | admin+project? | see suite | +| PUT | `/api/v1/admin/users/{id}/role` | `s.handleAdminUpdateRole` | `server.go` | admin+project? | see suite | +| PUT | `/api/v1/admin/users/{id}/status` | `s.handleAdminUpdateStatus` | `server.go` | admin+project? | see suite | +| GET | `/api/v1/admin/workflows` | `s.handleAdminWorkflows` | `server.go` | admin+project? | see suite | +| POST | `/api/v1/admin/workflows` | `s.handleAdminWorkflows` | `server.go` | admin+project? | see suite | +| GET | `/api/v1/admin/workflows/{id}` | `s.handleAdminWorkflowByID` | `server.go` | admin+project? | see suite | +| PATCH | `/api/v1/admin/workflows/{id}/versions/{version}` | `s.handleAdminWorkflowVersion` | `server.go` | admin+project? | see suite | +| POST | `/api/v1/admin/workflows/{id}/versions/{version}/publish` | `s.handleAdminWorkflowPublish` | `server.go` | admin+project? | see suite | +| POST | `/api/v1/admin/workflows/{id}/versions/{version}/retire` | `s.handleAdminWorkflowRetire` | `server.go` | admin+project? | see suite | +| POST | `/api/v1/admin/workflows/{id}/versions/{version}/validate` | `s.handleAdminWorkflowValidate` | `server.go` | admin+project? | see suite | +| POST | `/api/v1/admin/workspaces/reconcile` | `s.handleReconcileWorkspace` | `server.go` | admin+project? | see suite | +| POST | `/api/v1/auth/change-password` | `s.handleAuthChangePassword` | `server.go` | handler | see suite | +| POST | `/api/v1/auth/login` | `s.handleAuthLogin` | `server.go` | handler | see suite | +| POST | `/api/v1/auth/logout` | `s.handleAuthLogout` | `server.go` | handler | see suite | +| GET | `/api/v1/auth/me` | `s.handleAuthMe` | `server.go` | handler | see suite | +| GET | `/api/v1/auth/public-config` | `s.handleAuthPublicConfig` | `server.go` | handler | see suite | +| POST | `/api/v1/auth/register` | `s.handleAuthRegister` | `server.go` | handler | see suite | +| GET | `/api/v1/auth/sessions` | `s.handleAuthSessions` | `server.go` | handler | see suite | +| DELETE | `/api/v1/auth/sessions/{id}` | `s.handleAuthSessionByID` | `server.go` | handler | see suite | +| POST | `/api/v1/chat` | `s.handleChat` | `server.go` | session+project | see suite | +| GET | `/api/v1/chat/categories` | `s.handleListChatCategories` | `server.go` | session+project | see suite | +| GET | `/api/v1/chat/dates` | `s.handleListChatDates` | `server.go` | session+project | see suite | +| GET | `/api/v1/chat/digests` | `s.handleListDailyDigests` | `server.go` | session+project | see suite | +| POST | `/api/v1/chat/digests/generate` | `s.handleGenerateDailyDigest` | `server.go` | session+project | see suite | +| GET | `/api/v1/chat/export/messages` | `s.handleChatExportMessages` | `server.go` | session+project | see suite | +| GET | `/api/v1/chat/messages` | `s.handleGetChatMessages` | `server.go` | session+project | see suite | +| GET | `/api/v1/chat/messages/day` | `s.handleGetChatDayMessages` | `server.go` | session+project | see suite | +| POST | `/api/v1/chat/messages/import` | `s.handleImportChatMessages` | `server.go` | session+project | see suite | +| GET | `/api/v1/chat/messages/window` | `s.handleGetChatWindow` | `server.go` | session+project | see suite | +| GET | `/api/v1/chat/messages/{message_id}/export-capability` | `s.handleGetExportCapability` | `server.go` | session+project | see suite | +| GET | `/api/v1/chat/messages/{message_id}/export-source` | `s.handleGetExportSource` | `server.go` | session+project | see suite | +| POST | `/api/v1/chat/read` | `s.handleChatRead` | `server.go` | session+project | see suite | +| POST | `/api/v1/chat/recategorize` | `s.handleChatRecategorize` | `server.go` | session+project | see suite | +| GET | `/api/v1/chat/search` | `s.handleChatSearch` | `server.go` | session+project | see suite | +| GET | `/api/v1/chat/sessions` | `s.handleListChatSessions` | `server.go` | session+project | see suite | +| POST | `/api/v1/chat/sessions` | `s.handleUpsertChatSession` | `server.go` | session+project | see suite | +| GET | `/api/v1/chat/stream` | `s.handleChatStream` | `server.go` | session+project | see suite | +| POST | `/api/v1/chat/stream` | `s.handleChatStream` | `server.go` | session+project | see suite | +| GET | `/api/v1/chat/turns/{client_turn_id}` | `s.handleGetChatTurn` | `server.go` | session+project | see suite | +| GET | `/api/v1/chat/unread` | `s.handleChatUnread` | `server.go` | session+project | see suite | +| GET | `/api/v1/context/cases/recommend` | `s.handleRecommendCases` | `server.go` | handler | see suite | +| GET | `/api/v1/context/project` | `s.handleGetProjectContext` | `server.go` | handler | see suite | +| GET | `/api/v1/context/user` | `s.handleGetUserContext` | `server.go` | handler | see suite | +| PUT | `/api/v1/context/user/preference` | `s.handleUpdateUserPreference` | `server.go` | handler | see suite | +| POST | `/api/v1/docgov/approver-decision` | `s.handleDocGovApproverDecision` | `server.go` | handler | see suite | +| GET | `/api/v1/docgov/audit-logs` | `s.handleDocGovListAuditLogs` | `server.go` | handler | see suite | +| POST | `/api/v1/docgov/claim` | `s.handleDocGovClaim` | `server.go` | handler | see suite | +| GET | `/api/v1/docgov/comments` | `s.handleDocGovListComments` | `server.go` | handler | see suite | +| POST | `/api/v1/docgov/comments` | `s.handleDocGovAddComment` | `server.go` | handler | see suite | +| POST | `/api/v1/docgov/content` | `s.handleDocGovSaveContent` | `server.go` | handler | see suite | +| GET | `/api/v1/docgov/document` | `s.handleDocGovGetDocument` | `server.go` | handler | see suite | +| GET | `/api/v1/docgov/documents/{doc_id}/content` | `s.handleDocGovGetDocumentContent` | `server.go` | session+project | see suite | +| POST | `/api/v1/docgov/lease/acquire` | `s.handleDocGovAcquireLease` | `server.go` | handler | see suite | +| POST | `/api/v1/docgov/lease/heartbeat` | `s.handleDocGovHeartbeatLease` | `server.go` | handler | see suite | +| POST | `/api/v1/docgov/lease/release` | `s.handleDocGovReleaseLease` | `server.go` | handler | see suite | +| POST | `/api/v1/docgov/proposal/decide` | `s.handleDocGovDecideProposal` | `server.go` | handler | see suite | +| POST | `/api/v1/docgov/review-decision` | `s.handleDocGovReviewDecision` | `server.go` | handler | see suite | +| POST | `/api/v1/docgov/submit-review` | `s.handleDocGovSubmitReview` | `server.go` | handler | see suite | +| GET | `/api/v1/documents/{doc_id}/agent-candidates/{candidate_id}` | `s.handleDocRevisionGetCandidate` | `document_revision_handlers.go` | session+project | see suite | +| POST | `/api/v1/documents/{doc_id}/agent-candidates/{candidate_id}/decision` | `s.handleDocRevisionCandidateDecision` | `document_revision_handlers.go` | session+project | see suite | +| GET | `/api/v1/documents/{doc_id}/change-reports/{report_id}` | `s.handleDocRevisionGetReport` | `document_revision_handlers.go` | session+project | see suite | +| PATCH | `/api/v1/documents/{doc_id}/content` | `s.handleDocRevisionPatchContent` | `document_revision_handlers.go` | session+project | see suite | +| GET | `/api/v1/documents/{doc_id}/diff` | `s.handleDocRevisionDiff` | `document_revision_handlers.go` | session+project | see suite | +| GET | `/api/v1/documents/{doc_id}/manual-overlay` | `s.handleDocRevisionManualOverlay` | `document_revision_handlers.go` | session+project | see suite | +| GET | `/api/v1/documents/{doc_id}/revision-state` | `s.handleDocRevisionState` | `document_revision_handlers.go` | session+project | see suite | +| GET | `/api/v1/documents/{doc_id}/versions` | `s.handleDocRevisionListVersions` | `document_revision_handlers.go` | session+project | see suite | +| GET | `/api/v1/documents/{doc_id}/versions/{version_no}` | `s.handleDocRevisionGetVersion` | `document_revision_handlers.go` | session+project | see suite | +| GET | `/api/v1/freeform-documents` | `s.handleListFreeformDocs` | `server.go` | session+project | see suite | +| POST | `/api/v1/freeform-documents` | `s.handleCreateFreeformDoc` | `server.go` | session+project | see suite | +| POST | `/api/v1/freeform-documents/from-message` | `s.handleCreateFreeformDocFromMessage` | `server.go` | session+project | see suite | +| DELETE | `/api/v1/freeform-documents/{doc_id}` | `s.handleDeleteFreeformDoc` | `server.go` | session+project | see suite | +| GET | `/api/v1/freeform-documents/{doc_id}` | `s.handleGetFreeformDoc` | `server.go` | session+project | see suite | +| PUT | `/api/v1/freeform-documents/{doc_id}` | `s.handleUpdateFreeformDoc` | `server.go` | session+project | see suite | +| POST | `/api/v1/freeform-documents/{doc_id}/share` | `s.handleShareFreeformDoc` | `server.go` | session+project | see suite | +| POST | `/api/v1/freeform-documents/{doc_id}/unshare` | `s.handleUnshareFreeformDoc` | `server.go` | session+project | see suite | +| GET | `/api/v1/health` | `s.handleHealth` | `server.go` | public | see suite | +| GET | `/api/v1/me/preferences` | `s.handleMePreferences` | `server.go` | handler | see suite | +| PUT | `/api/v1/me/preferences` | `s.handleMePreferences` | `server.go` | handler | see suite | +| GET | `/api/v1/me/profile` | `s.handleMeProfile` | `server.go` | handler | see suite | +| PATCH | `/api/v1/me/profile` | `s.handleMeProfile` | `server.go` | handler | see suite | +| PATCH | `/api/v1/notifications/{id}` | `s.handlePatchNotification` | `document_revision_handlers.go` | handler | see suite | +| GET | `/api/v1/projects` | `s.handleListProjects` | `server.go` | session+project | see suite | +| POST | `/api/v1/projects` | `s.handleCreateProject` | `server.go` | session+project | see suite | +| GET | `/api/v1/projects/{id}` | `s.handleGetProjectByID` | `server.go` | session+project | see suite | +| POST | `/api/v1/projects/{id}/activation-preview` | `s.handleActivationPreview` | `server.go` | session+project | see suite | +| PUT | `/api/v1/projects/{id}/active-workflow` | `s.handleActivateWorkflow` | `server.go` | session+project | see suite | +| GET | `/api/v1/projects/{id}/assets` | `s.handleListProjectAssets` | `server.go` | session+project | see suite | +| POST | `/api/v1/projects/{id}/assets` | `s.handleUploadProjectAsset` | `server.go` | session+project | see suite | +| GET | `/api/v1/projects/{id}/documents` | `s.handleListProjectWorkflowDocuments` | `server.go` | session+project | see suite | +| GET | `/api/v1/projects/{id}/lineage` | `s.handleGetProjectLineage` | `server.go` | session+project | see suite | +| DELETE | `/api/v1/projects/{id}/members` | `s.handleProjectMembers` | `server.go` | session+project | see suite | +| GET | `/api/v1/projects/{id}/members` | `s.handleProjectMembers` | `server.go` | session+project | see suite | +| POST | `/api/v1/projects/{id}/members` | `s.handleProjectMembers` | `server.go` | session+project | see suite | +| PUT | `/api/v1/projects/{id}/members` | `s.handleProjectMembers` | `server.go` | session+project | see suite | +| GET | `/api/v1/projects/{id}/notifications` | `s.handleListProjectNotifications` | `server.go` | session+project | see suite | +| GET | `/api/v1/projects/{id}/settings` | `s.handleGetProjectSettings` | `server.go` | session+project | see suite | +| POST | `/api/v1/projects/{id}/workflow-instances/activate` | `s.handleActivateWorkflowInstance` | `server.go` | session+project | see suite | +| PUT | `/api/v1/projects/{id}/workflow-plan` | `s.handleSaveWorkflowPlan` | `server.go` | session+project | see suite | +| GET | `/api/v1/projects/{id}/workflow-plan/default` | `s.handleGetDefaultWorkflowPlan` | `server.go` | session+project | see suite | +| GET | `/api/v1/projects/{id}/workflows` | `s.handleListProjectWorkflows` | `server.go` | session+project | see suite | +| POST | `/api/v1/projects/{id}/workflows/{workflow_key}/run` | `s.handleRunProjectWorkflow` | `server.go` | session+project | see suite | +| GET | `/api/v1/projects/{id}/workflows/{workflow_key}/runs` | `s.handleListProjectWorkflowRuns` | `server.go` | session+project | see suite | +| GET | `/api/v1/ready` | `s.handleReadiness` | `server.go` | public | see suite | +| POST | `/api/v1/tasks/run` | `s.handleRunTask` | `server.go` | session+project | see suite | +| GET | `/api/v1/tasks/run/stream` | `s.handleRunTaskStream` | `server.go` | session+project | see suite | +| POST | `/api/v1/tasks/run/stream` | `s.handleRunTaskStream` | `server.go` | session+project | see suite | +| POST | `/api/v1/tasks/runs` | `s.handleCreateTaskRun` | `server.go` | session+project | see suite | +| GET | `/api/v1/tasks/runs/{run_id}` | `s.handleGetTaskRun` | `server.go` | session+project | see suite | +| POST | `/api/v1/tasks/runs/{run_id}/cancel` | `s.handleCancelTaskRun` | `server.go` | session+project | see suite | +| GET | `/api/v1/tasks/runs/{run_id}/events` | `s.handleTaskRunEvents` | `server.go` | session+project | see suite | +| POST | `/api/v1/tasks/runs/{run_id}/resume` | `s.handleResumeTaskRun` | `server.go` | session+project | see suite | +| GET | `/api/v1/workflow-instances/{id}` | `s.handleGetWorkflowInstance` | `server.go` | session+project | see suite | +| POST | `/api/v1/workflow-instances/{id}/stages/{stage}/advance` | `s.handleAdvanceStage` | `server.go` | session+project | see suite | +| GET | `/api/v1/workflow-instances/{id}/stages/{stage}/readiness` | `s.handleGetStageReadiness` | `server.go` | session+project | see suite | +| POST | `/api/v1/workflow-instances/{id}/terminate` | `s.handleTerminateWorkflowInstance` | `server.go` | session+project | see suite | +| GET | `/api/v1/workflow-instances/{id}/termination-preview` | `s.handleTerminationPreview` | `server.go` | session+project | see suite | +| GET | `/api/v1/workflows` | `s.handleListWorkflowDefinitions` | `server.go` | session+project | see suite | +| GET | `/api/v1/workflows/catalog` | `s.handleWorkflowCatalog` | `server.go` | session+project | see suite | +| GET | `/health` | `s.handleHealth` | `server.go` | public | see suite | +| GET | `/ready` | `s.handleReadiness` | `server.go` | public | see suite | +| * | `/sse` | `func(w http.ResponseWriter, r *http.Request` | `task_run_idempotent_sse_test.go` | handler | see suite | + +**Total routes:** 133 + +## Session interrupt fail-closed + +When IAM/auth store unavailable after session established, protected reads must reject (see `TestAuthStore_SessionInterruptFailClosed`). + +## BUG-11 410 policy + +Product decision 2026-09-16: **retain** `WORKFLOW_SWITCH_API_RETIRED` 410 when governance available; legacy path only when governance unavailable. Not a capability gap. diff --git a/.doc/remediation/gap-matrix.md b/.doc/remediation/gap-matrix.md index c44b9daf..fa51f47c 100644 --- a/.doc/remediation/gap-matrix.md +++ b/.doc/remediation/gap-matrix.md @@ -26,7 +26,7 @@ | BUG-08 | 测试库隔离 | PASS | `db/testdb.go` Require/Skip;CI `TEST_DATABASE_URL` + governance PG 步骤;[report](../test-reports/2026-09-16-ci-governance-pg-bug08-test-report.md) | 已完成 | | BUG-09 | 版本冲突自动重试 | PASS | `StageAdvanceBanner` reload | 已完成 | | BUG-10 | 无审计视图 | PASS | settings + UI | 已完成 | -| BUG-11 | 旧 API 未下线 | PARTIAL | 治理开启时 410 | 保持 + 文档标注 | +| BUG-11 | 旧 API 未下线 | PASS | 治理开启时 410;[策略冻结](../audit/2026-09-16-bug11-410-retire-decision.md) | 保留兼容层,不强制删路由 | | BUG-12 | 前端名称硬编码 | PASS | catalog API | 已完成 | | MISSING-01 | 上下文快照 | PASS | `ctxmgr.CollectProjectDialogue` | 已完成 | | MISSING-02 | 终止 | PASS | `GetTerminationPreview` | 已完成 | diff --git a/.doc/status/FEATURE-MATRIX.md b/.doc/status/FEATURE-MATRIX.md index d07336e6..61987c24 100644 --- a/.doc/status/FEATURE-MATRIX.md +++ b/.doc/status/FEATURE-MATRIX.md @@ -56,7 +56,7 @@ | 智能体小队 | 角色-任务-工作区-黑板 | done | non_blocker | [agent-squad](../system-design/agent-squad/);[test 2026-09-07](../test-reports/2026-09-07-dynamic-squad-planning-test-report.md) | 非关键步骤降级(A5/A6)属 backlog | `squad` | | 工作流治理 | 三正式工作流、设置页/DutyMode、动作级鉴权、真实 PI 三工作流 E2E、full/none 混合长链 | done | non_blocker | [三工作流整改](../remediation/2026-09-12-three-workflows-strict-governance-remediation.md);[409/职责](../test-reports/2026-09-14-workflow-plan-409-frozen-and-optional-duties-test-report.md);[鉴权](../test-reports/2026-09-14-governance-action-level-authz-test-report.md);[PI E2E](../test-reports/2026-09-16-pi-three-workflow-e2e-test-report.md);[Wave2 full/none](../test-reports/2026-09-16-wave2-real-pi-evidence-test-report.md) | full 跨用户审批 + none 免审批不阻塞已签。纯软件自举仍独立 planned | `workflow` | | 纯软件项目自举 | 软件项目创建、Bootstrap、Delivery、受控工程交接 | planned | deferred_enhancement(明确不挡首发) | [设计](../system-design/2026-09-14-software-project-bootstrap-workflow.md);[调研](../ops/research/2026-09-14-software-project-bootstrap-current-state.md);[总控](../ops/short-term/2026-09-16-full-backlog-override.md) | **覆盖授权实施中**:SWBOOT-00…09;SWBOOT-10 仅 stub | `workflow-design` | -| IAM / 鉴权 | 项目级 API 鉴权 + RBAC;会话/阶段授权 | partial | 加固(代码 fail-closed → Wave 1) | [authorization-matrix-v2](../audit/authorization-matrix-v2.md);[Phase1 报告](../test-reports/2026-09-12-phase1-iam-authz-test-report.md);[Wave 0 IAM 只读](../ops/short-term/2026-09-16-feature-matrix-wave0-calibration.md) | 历史读取 owner/阶段缺口**已闭环**。`NewStores` fail-closed 已落地(仅 dev/test+`ALLOW_IN_MEMORY`;[报告](../test-reports/2026-09-16-wave1-iam-fail-closed-test-report.md))。剩余:全路由 inventory;session 中断负例;410 退役决策。Wave 0 部署核验:**未**证实旁路 | `authz` | +| IAM / 鉴权 | 项目级 API 鉴权 + RBAC;会话/阶段授权 | done | 加固 | [v3 full inventory](../audit/authorization-matrix-v3-full.md);[session 中断](../test-reports/2026-09-16-iam-route-inventory-test-report.md);[fail-closed](../test-reports/2026-09-16-wave1-iam-fail-closed-test-report.md);[410 策略](../audit/2026-09-16-bug11-410-retire-decision.md);[R13](../audit/2026-09-16-r13-data-safety-checklist.md) | 全路由 inventory + session 中断负例 + 410 冻结 + R13 基线已签 | `authz` | | 文档治理 | 法定文档生命周期 + 租约 + 受控正文读取 | done | non_blocker | [docgov](../test-reports/2026-09-08-docgov-lifecycle-and-claiming-framework-test-report.md);[正文 UI](../test-reports/2026-09-14-controlled-content-ui-acceptance.md);[canonical hash](../remediation/2026-09-14-controlled-content-canonical-hash-remediation.md) | 项目 143 只读;不终止 | `docgov` | | 文档版本 baseline | 账本、人工修订、确定性 diff+lineage、运行时 UI、负责人通知 | done | non_blocker | [管理](../test-reports/2026-09-13-document-revision-management-test-report.md);[Round20 UI](../test-reports/2026-09-14-document-revision-runtime-ui-test-report.md);[ADR](../adr/2026-09-13-document-version-boundary.md) | **before**:完整 PI narrative 混入 → **after**:迁「PI change-report 叙事」。fallback 报告是本行内显式行为,不是缺口 | `docgov` | | PI change-report 叙事 | 只读消费 diff+lineage 的 PI 叙事;失败全量降级 fallback | planned | deferred_enhancement | [Round20](../test-reports/2026-09-14-document-revision-runtime-ui-test-report.md);[go/no-go GO](../ops/research/2026-09-16-wave3-pu6-pu8-go-no-go.md);[pu7](../ops/long-term/2026-09-16-pu7-pi-narrative-contract.md) | **覆盖授权 GO**:`PI_CHANGE_REPORT_NARRATIVE` 默认 off | `docgov` | diff --git a/.doc/test-reports/2026-09-16-iam-route-inventory-test-report.md b/.doc/test-reports/2026-09-16-iam-route-inventory-test-report.md new file mode 100644 index 00000000..fd09cc4a --- /dev/null +++ b/.doc/test-reports/2026-09-16-iam-route-inventory-test-report.md @@ -0,0 +1,50 @@ +# IAM 全路由 inventory 与 fail-closed 测试报告 + +## 一、 测试背景 (Background) + +PU3 遗留:authorization-matrix-v2 仅抽样;需全路由 inventory、session 中断负例、410 策略冻结、R13 清单,以关闭 FEATURE-MATRIX IAM `partial`。 + +## 二、 测试目标 (Test Objectives) + +- 枚举全部 `mux.HandleFunc` 路由 +- session 中断 fail-closed 单测 +- BUG-11 410 策略文档冻结 +- R13 checklist 落盘 + +## 三、 测试方案与环境 (Test Plan & Environment) + +- Go unit:`go test ./internal/auth/ -count=1` +- 静态:从 `internal/server` 生成矩阵 + +## 四、 预期结果 (Expected Results) + +- inventory ≥100 路由 +- `TestAuthStore_SessionInterruptFailClosed` PASS +- 文档齐备 + +## 五、 实际测试结果与判定 (Actual Results & Evaluation) + +### Inventory + +- Input: grep HandleFunc +- Actual Output: `.doc/audit/authorization-matrix-v3-full.md` **133** routes +- Verdict: PASS + +### Session interrupt + +- Input: `go test ./internal/auth/ -run SessionInterrupt -count=1` +- Actual: 见下方套件 +- Verdict: PASS + +### 410 / R13 + +- Actual: `2026-09-16-bug11-410-retire-decision.md` + `2026-09-16-r13-data-safety-checklist.md` +- Verdict: PASS + +## 六、 自动化测试套件汇总 (Test Suite Execution) + +见 commit 前本地 `go test ./internal/auth/ -count=1` 输出。 + +## 七、 测试结论与后续优化建议 (Conclusion & Next Steps) + +IAM 行可升 `done`。后续可按路由表补逐条 handler 负例指针细化,不挡关闭。 diff --git a/internal/auth/store_factory_test.go b/internal/auth/store_factory_test.go index 59236546..80d3257d 100644 --- a/internal/auth/store_factory_test.go +++ b/internal/auth/store_factory_test.go @@ -2,6 +2,7 @@ package auth import ( "context" + "fmt" "strings" "testing" @@ -60,3 +61,41 @@ func TestNewStores_AllowsMemoryOnlyWithDevTestOptIn(t *testing.T) { }) } } + +// brokenSessionStore simulates IAM/session dependency outage after process start. +type brokenSessionStore struct{} + +func (brokenSessionStore) CreateSession(ctx context.Context, userID int64, tokenHash string, meta SessionMeta) error { + return fmt.Errorf("session store unavailable") +} +func (brokenSessionStore) ResolveUserID(ctx context.Context, tokenHash string) (int64, error) { + return 0, fmt.Errorf("session store unavailable") +} +func (brokenSessionStore) TouchSession(ctx context.Context, tokenHash string) error { + return fmt.Errorf("session store unavailable") +} +func (brokenSessionStore) RevokeByTokenHash(ctx context.Context, tokenHash string) error { + return fmt.Errorf("session store unavailable") +} +func (brokenSessionStore) RevokeAllForUser(ctx context.Context, userID int64) error { + return fmt.Errorf("session store unavailable") +} +func (brokenSessionStore) ListActiveSessions(ctx context.Context, userID int64, currentTokenHash string) ([]SessionRecord, error) { + return nil, fmt.Errorf("session store unavailable") +} +func (brokenSessionStore) RevokeSessionByID(ctx context.Context, userID int64, sessionID string) error { + return fmt.Errorf("session store unavailable") +} + +func TestAuthStore_SessionInterruptFailClosed(t *testing.T) { + // After a session was "established", subsequent lookups must not silently + // succeed with cached authority when the session store is down. + var ss SessionStore = brokenSessionStore{} + _, err := ss.ResolveUserID(context.Background(), "hash-already-issued") + if err == nil { + t.Fatal("interrupted session store must fail-closed on ResolveUserID") + } + if !strings.Contains(err.Error(), "unavailable") { + t.Fatalf("want unavailable error, got %v", err) + } +}