From c2ca5e987f3b399b11df11163b8960cd3f6708af Mon Sep 17 00:00:00 2001 From: k2safe Date: Sun, 27 Sep 2026 12:29:31 +0800 Subject: [PATCH] docs: complete console application scope acceptance and deployment guidance --- README.md | 3 ++- deploy/docker-compose.ha.yml | 1 + docs/api/README.md | 3 ++- docs/api/console-scope.md | 2 ++ docs/reports/console-scope.md | 19 +++++++++++++++++-- internal/consoleauth/scope.go | 1 + internal/transport/httpserver/console/app.js | 14 ++++++++++++-- .../transport/httpserver/console_scope.go | 8 ++++++-- .../httpserver/console_scope_test.go | 3 ++- platform/auth/token.go | 2 +- 10 files changed, 46 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 40246f3..b35a90e 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ | 业务结果 Inbox | 事件去重与业务更新同事务,提交成功后确认交付 | [result-inbox](examples/result-inbox/README.md) | | HTTP 服务完整生命周期 | 业务接口、Outbox、Worker、结果消费、就绪检查和有界关闭放进同一业务服务 | [http-service](examples/http-service/README.md) | | 平台身份与凭据 | 应用/命名空间/队列授权范围、凭据轮换/撤销;SDK 不直连平台数据库 | [鉴权](docs/api/authentication.md) · [凭据管理](docs/api/authentication.md) | -| 账号密码控制台 | 初始化管理员,登录查看统计、任务/计划/流程/交付详情和管理操作,未登录跳转登录页 | [Console 手册](docs/console.md) | +| 账号密码控制台 | 初始化管理员,按应用/环境/队列查看任务、计划、流程、交付与归档,列表和详情显示应用归属 | [Console 手册](docs/console.md) · [应用范围](docs/api/console-scope.md) | | 保留策略与周期归档 | 配置各类数据保留天数,持久化归档计划/作业,分页、续跑、重试与文件核验 | [保留策略](docs/api/retention.md) · [周期归档](docs/api/archive-scheduler.md) | | 运维与观测 | 健康/readiness、独立指标端口、迁移核验、负载工具、备份恢复与故障演练 | [观测](docs/api/observability.md) · [运维](docs/20-operations-runbook.md) | @@ -223,6 +223,7 @@ fmt.Println(current.ID, current.State) | `ORBIT_MYSQL_DSN` | 平台数据库连接,迁移账号与运行账号按部署手册分开 | | `ORBIT_HTTP_ADDR` | 服务进程监听地址,例 `127.0.0.1:8080` | | `ORBIT_CONSOLE_QUEUE_ID` | 控制台默认队列;不授予账号额外权限 | +| `ORBIT_CONSOLE_APPLICATION_ADMIN` | 指定唯一可接入和管理各应用的控制台登录名;空值不开放跨应用权限,多节点保持一致 | | `ORBIT_DB_MAX_OPEN` / `ORBIT_DB_MAX_IDLE` | 数据库连接池上限 | | `ORBIT_SHUTDOWN_TIMEOUT` | SIGTERM 关闭窗口,默认 10s | | `ORBIT_WORKFLOW_TICK_INTERVAL` / `ORBIT_SCHEDULE_TICK_INTERVAL` | 流程/计划后台扫描间隔,默认 1s | diff --git a/deploy/docker-compose.ha.yml b/deploy/docker-compose.ha.yml index 169203e..41388e1 100644 --- a/deploy/docker-compose.ha.yml +++ b/deploy/docker-compose.ha.yml @@ -24,6 +24,7 @@ x-orbit-environment: &orbit-environment ORBIT_RETENTION_INBOX_DAYS: ${ORBIT_RETENTION_INBOX_DAYS:-90} ORBIT_RETENTION_AUDIT_DAYS: ${ORBIT_RETENTION_AUDIT_DAYS:-2555} ORBIT_CONSOLE_QUEUE_ID: ${ORBIT_CONSOLE_QUEUE_ID:-} + ORBIT_CONSOLE_APPLICATION_ADMIN: ${ORBIT_CONSOLE_APPLICATION_ADMIN:-} ORBIT_METRICS_ADDR: ${ORBIT_METRICS_ADDR:-} services: diff --git a/docs/api/README.md b/docs/api/README.md index 268b9c0..b70009c 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -1,6 +1,6 @@ # Orbit HTTP / WebSocket 接口手册 -更新:2026-09-18。接口实现状态随 [开发计划](../08-development-plan.md) 更新。 +更新:2026-09-27。接口实现状态随 [开发计划](../08-development-plan.md) 更新。 ## 当前接口状态 @@ -9,6 +9,7 @@ | `GET /health/live` | 已实现 | [健康检查](health.md) | | `GET /health/ready` | 已实现,验证数据库、迁移记录及零外键要求 | [健康检查](health.md) | | `GET /version` | 已实现,返回版本、提交、构建时间及协议版本,不包含数据库、凭据或租户信息 | [健康检查](health.md) | +| `GET /console/scope` 与控制台显式应用范围 | 已实现单管理员应用/环境切换、队列分页和列表归属;SDK 不可覆盖范围 | [应用范围](console-scope.md) | | 独立监听的 `GET /metrics` | 已实现基础运行指标,默认关闭 | [运行观测](observability.md) | | `/v1/tasks` 提交、查询、分页与待执行取消 | 已实现,P3扩展业务键与创建时间筛选 | [任务接口](tasks.md) / [查询契约](task-queries.md) | | `GET /v1/tasks/{task_id}/source` | 已实现普通、人工重试、计划固定版本及流程历史激活/轮次来源 | [来源查询](task-source.md) | diff --git a/docs/api/console-scope.md b/docs/api/console-scope.md index 6a67163..d67a51e 100644 --- a/docs/api/console-scope.md +++ b/docs/api/console-scope.md @@ -50,3 +50,5 @@ X-Orbit-Namespace: 22222222222222222222222222222222 每个业务事务重新核验管理员账号、原会话、绑定凭据有效性以及原/目标应用启用状态,再执行原有的资源归属、版本、幂等、核查证据和审计规则。管理审计操作者为 `console:`,操作归入目标命名空间。读取其他范围的资源 ID 不会越权返回数据。不存在的范围、未授权范围不自动回退。 缺少 Cookie 或会话失效返回 401;不允许的范围切换、混用 SDK Bearer、跨站写操作返回 403;目标应用停用或资源不属于当前范围返回 404。非法范围 ID 返回 400。页面切换采用完整导航,旧详情和请求不会在新应用页面呈现;URL 只保留非敏感 ID。 + +任务列表沿用原有 SQL 筛选语义:请求其他应用的队列会得到空列表;该队列或任务的单条详情返回 404。页面会先核对 URL 中的队列归属,错误队列不会被当成正常空列表。 diff --git a/docs/reports/console-scope.md b/docs/reports/console-scope.md index 341ca61..d3472c4 100644 --- a/docs/reports/console-scope.md +++ b/docs/reports/console-scope.md @@ -4,6 +4,21 @@ 任务、计划、流程、订阅和归档列表显示应用名称与环境;保留策略及详情显示归属。管理员可选择应用环境和队列,范围保存在 URL,页面切换隔离旧数据。后台按 Cookie 会话授权,普通 SDK 不接受范围覆盖。无表结构变更,无新增外键。 -## 验证记录 +## 验证记录(2026-09-27) -验证进行中,最终结果在本次交付前更新。 +| 检查 | 结果 | +| --- | --- | +| `make test vet build` | 通过,服务端、SDK 和各独立示例/适配模块;最后的 HTTP 范围边界测试及编译检查也通过 | +| `make integration` | 通过:真实 MySQL 全量存储回归、HTTP/WS、SDK、Inbox/Outbox/HTTP 宿主示例、进程中断及多节点流程恢复;存储回归约 307 秒,最终 SDK 进程故障组约 131 秒 | +| `TestIntegrationConsoleScope` | 原始/目标元数据、普通账号队列白名单、二进制 ID 游标、SDK/非指定管理员拒绝、目标队列隔离、仅管理不受理新工作、目标保留策略原键重放、原应用不变、人类审计、停用目标/退出后旧范围拒绝、零外键断言 | +| `make console-e2e` | 通过:隔离 MySQL 和实际 Orbit 容器;新建应用、任务/计划/流程/订阅/保留/归档范围读取,订阅管理及重放保留原 SDK 归属,跨应用单条 404、列表无泄漏,Bearer 混用/跨站写入拒绝,退出后 401;原任务执行、计划和归档端到端也通过 | +| HTTP 范围边界 | 通过 11 个 Cookie/Bearer/CSRF/读写入口场景;Worker、结果消费和资源创建不能借管理范围获得权限 | +| 静态与配置检查 | JS 语法、Python 编译、Markdown 链接、`git diff --check` 通过;单节点 + HA Compose 配置解析通过,双节点使用同一管理员环境配置 | +| 浏览器实际页面 | 任务列和订阅列表/详情显示应用标签;应用及队列切换、无数据范围、保留策略和归档范围提示、非法范围不降级、重置范围恢复均已检查 | +| 本地部署 | 本地 Compose 已部署本批代码;现有两个应用分别读取自己的范围,原 SDK Token 仍有效,无业务接入参数变更 | + +应用、队列名称来自授权后的数据库元数据;队列分页首屏不使用空 ID,白名单使用 BINARY ID 参数。URL 中显式指定其他应用的队列会校验失败,不静默切换队列。打开详情、策略未保存或操作结果未确认时,页面阻止切换范围。 + +## 边界 + +当前按所选应用环境分别查看,不提供跨应用混合分页或全库统计。任务、计划、订阅按队列筛选;流程、保留与归档按命名空间展示。应用目录 50 项一页、队列 100 项一页。普通账号保留原绑定权限;只有部署指定的单管理员具有显式管理范围。此回归不替代生产部署的容量和跨故障域验收。 diff --git a/internal/consoleauth/scope.go b/internal/consoleauth/scope.go index d5a08e4..b9589a7 100644 --- a/internal/consoleauth/scope.go +++ b/internal/consoleauth/scope.go @@ -2,6 +2,7 @@ package consoleauth import ( "context" + core "git.yuntuo.hk/k2safe/orbit/model" "git.yuntuo.hk/k2safe/orbit/platform/auth" ) diff --git a/internal/transport/httpserver/console/app.js b/internal/transport/httpserver/console/app.js index f4361c2..fb6e1d0 100644 --- a/internal/transport/httpserver/console/app.js +++ b/internal/transport/httpserver/console/app.js @@ -23,10 +23,15 @@ } function setView(view, updateHash = true) { const next = pageNames[view] ? view : 'overview'; + const leavingApplications = state.scopeReady && $('application-scope').hidden && next !== 'applications'; document.querySelectorAll('.page-view').forEach(page => page.classList.toggle('active', page.dataset.page === next)); document.querySelectorAll('.nav-item').forEach(item => item.classList.toggle('active', item.dataset.view === next)); text('page-title', pageNames[next]); $('application-scope').hidden = next === 'applications'; + if (leavingApplications && state.scope?.can_switch) { + scopeUI.nextApplication = ''; + loadScopeApplications().catch(error => showToast(`应用目录读取失败:${error.message}`)); + } text('scope-hint', ['workflows', 'retention', 'archives'].includes(next) ? '当前应用环境 · 不受队列筛选影响' : '任务、计划和订阅按所选队列显示'); if (updateHash && location.hash !== `#/${next}`) location.hash = `/${next}`; window.scrollTo({ top: 0, behavior: 'smooth' }); @@ -131,6 +136,11 @@ state.scopeReady = true; renderConnectionSummary(); } catch (error) { state.queue = ''; state.scopeReady = false; + text('scope-summary', '范围不可用'); + $('scope-application').replaceChildren(new Option('范围不可用', '')); + $('scope-queue').replaceChildren(new Option('未加载', '')); + $('scope-application').disabled = true; $('scope-queue').disabled = true; + $('task-table').innerHTML = '◇当前范围未加载请检查应用范围,或点击“重置范围”'; text('scope-error', `当前应用范围不可用:${error.message}。可点击“重置范围”返回默认应用。`); } } @@ -783,9 +793,9 @@ renderSchedules(scheduleItems || []); if (!scheduleItems) $('schedule-list').textContent = '定时计划读取失败,请刷新重试或检查权限'; renderWorkflows(workflowItems || []); - if (!workflowItems) $('workflow-list').textContent = '流程实例读取失败,请刷新重试或检查权限'; + if (!workflowItems) { $('workflow-list').textContent = '流程实例读取失败,请刷新重试或检查权限'; text('metric-workflows', '—'); text('metric-workflows-sub', '读取失败'); } renderSubscriptions(subscriptionItems || []); - if (!subscriptionItems) $('subscription-list').textContent = '结果订阅读取失败,请刷新重试或检查权限'; + if (!subscriptionItems) { $('subscription-list').textContent = '结果订阅读取失败,请刷新重试或检查权限'; text('metric-subscriptions', '—'); text('metric-subscriptions-sub', '读取失败'); } setConnection(true); renderRuntimeStats(taskItems, scheduleItems, workflowItems, subscriptionItems); text('last-updated', `刚刚更新 · ${formatTime(new Date())}`); text('metric-health-sub', '数据库与 Schema 正常'); } catch (error) { setConnection(false); showToast(`连接失败:${error.message}`); text('last-updated', '刷新失败'); } finally { state.loading = false; $('refresh-button').classList.remove('spinning'); } diff --git a/internal/transport/httpserver/console_scope.go b/internal/transport/httpserver/console_scope.go index 13ed8c6..cd5f120 100644 --- a/internal/transport/httpserver/console_scope.go +++ b/internal/transport/httpserver/console_scope.go @@ -1,12 +1,13 @@ package httpserver import ( + "net/http" + "strings" + "git.yuntuo.hk/k2safe/orbit/internal/consoleauth" "git.yuntuo.hk/k2safe/orbit/internal/fault" core "git.yuntuo.hk/k2safe/orbit/model" "git.yuntuo.hk/k2safe/orbit/platform/auth" - "net/http" - "strings" ) type consoleScopeKey struct{} @@ -40,6 +41,9 @@ func consoleScopePath(r *http.Request) bool { } func RegisterConsoleScopes(server *http.Server, store consoleauth.Scopes) { + if server == nil { + return + } mux, ok := server.Handler.(*http.ServeMux) if !ok || store == nil { return diff --git a/internal/transport/httpserver/console_scope_test.go b/internal/transport/httpserver/console_scope_test.go index 70b4fb7..03df98a 100644 --- a/internal/transport/httpserver/console_scope_test.go +++ b/internal/transport/httpserver/console_scope_test.go @@ -39,7 +39,8 @@ func TestConsoleScopeOnlyBridgesCookieManagementRequests(t *testing.T) { {"anonymous", "GET", "/v1/tasks", "", "", false, true, 401}, {"missing-marker", "GET", "/v1/tasks", "", "", true, false, 403}, {"cross-origin", "POST", "/v1/tasks/task:cancel", "https://other.invalid", "", true, true, 403}, - {"worker", "GET", "/v1/workers/connect", "", "", true, true, 403}, + {"worker", "GET", "/v1/worker/connect", "", "", true, true, 403}, + {"consumer", "GET", "/v1/events/connect", "", "", true, true, 403}, {"create", "POST", "/v1/tasks", "", "", true, true, 403}, {"onboarding", "POST", "/console/applications/prepare", "", "", true, true, 403}, } { diff --git a/platform/auth/token.go b/platform/auth/token.go index 5d5ea4f..7ca5365 100644 --- a/platform/auth/token.go +++ b/platform/auth/token.go @@ -13,7 +13,7 @@ import ( // Token 是已解析但尚未认证的身份,服务端仍须核对数据库摘要与有效期。 type Token struct { - // NamespaceID 凭据所属命名空间,不能由请求体覆盖。 + // NamespaceID 凭据所属命名空间;仅服务端已核验的 Console 身份可指定管理范围。 NamespaceID core.ID // CredentialID 凭据记录标识。 CredentialID core.ID