mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): expose cluster secret binding management
This commit is contained in:
@@ -451,7 +451,12 @@ become trusted by restarting every replica. Server TLS certificate rotation
|
|||||||
requires a controlled Pod rollout because TLS material is loaded only at
|
requires a controlled Pod rollout because TLS material is loaded only at
|
||||||
process startup.
|
process startup.
|
||||||
|
|
||||||
This deployment exposes only `propose`, `decide`, and low-sensitive `inspect`.
|
This deployment exposes only reviewed Package proposal/decision/inspection,
|
||||||
|
installation inventory, lifecycle review, publisher trust review, and
|
||||||
|
content-free Secret binding `plan|propose|decide|inspect`. Secret binding plans
|
||||||
|
derive generation and digests from PostgreSQL; the caller supplies only logical
|
||||||
|
requirements and fixed-version same-Project Secret references. The management
|
||||||
|
Pods never receive projected Secret values or package-executor database authority.
|
||||||
It has no Kubernetes API token or RBAC, no Registry credential, and no Package
|
It has no Kubernetes API token or RBAC, no Registry credential, and no Package
|
||||||
executor/admin/runtime database identity. Process-local rate limiting is a
|
executor/admin/runtime database identity. Process-local rate limiting is a
|
||||||
bounded anonymous-traffic shield, not a distributed quota. Authenticated
|
bounded anonymous-traffic shield, not a distributed quota. Authenticated
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
- D-304/ADR-0392(已接受):Plugin Package 进入安全 quarantine 时,Workflow/Prompt automation publication 不再仅依赖运行时 start guard 间接拒绝,而是与 quarantine event、Package-owned Task disabled revisions、Project Tool snapshot 和 withdrawal receipt 在同一 SQLite/PostgreSQL 事务中收敛为 `withdrawn`。原先仅能引用普通 lifecycle event 的外键升级为 append-only disposition-event 联合引用,历史 migration 与 publication digest schema 保持不变;SQLite edge/standalone 崩溃矩阵覆盖 automation publication insert 后、event/task/receipt/COMMIT 前后,PostgreSQL 通过触发器在既有 `SECURITY DEFINER` quarantine commit 内登记 disposition,再由同一外层 SERIALIZABLE transaction CAS publication head。能力位为 `plugin_package_automation_security_withdrawal@1`;不新增 package、daemon、timer、连接或常驻缓存,适用于低配路由设备和集群节点。SQLite 全量 228/228;PostgreSQL package 311 pass/1 条外部 URL 条件 skip;完整 18-package build/test 退出 0,backend 1,188 pass/2 skip,package/dependency boundary 零 finding;PostgreSQL 18.4 arm64 HA 125 项 gate 全绿、timeline `1→2`,报告 SHA-256 为 `ab156901b9c96ec5a62259c44d83d24ded011e0616dc827d928f3e13efd11786`。
|
- D-304/ADR-0392(已接受):Plugin Package 进入安全 quarantine 时,Workflow/Prompt automation publication 不再仅依赖运行时 start guard 间接拒绝,而是与 quarantine event、Package-owned Task disabled revisions、Project Tool snapshot 和 withdrawal receipt 在同一 SQLite/PostgreSQL 事务中收敛为 `withdrawn`。原先仅能引用普通 lifecycle event 的外键升级为 append-only disposition-event 联合引用,历史 migration 与 publication digest schema 保持不变;SQLite edge/standalone 崩溃矩阵覆盖 automation publication insert 后、event/task/receipt/COMMIT 前后,PostgreSQL 通过触发器在既有 `SECURITY DEFINER` quarantine commit 内登记 disposition,再由同一外层 SERIALIZABLE transaction CAS publication head。能力位为 `plugin_package_automation_security_withdrawal@1`;不新增 package、daemon、timer、连接或常驻缓存,适用于低配路由设备和集群节点。SQLite 全量 228/228;PostgreSQL package 311 pass/1 条外部 URL 条件 skip;完整 18-package build/test 退出 0,backend 1,188 pass/2 skip,package/dependency boundary 零 finding;PostgreSQL 18.4 arm64 HA 125 项 gate 全绿、timeline `1→2`,报告 SHA-256 为 `ab156901b9c96ec5a62259c44d83d24ded011e0616dc827d928f3e13efd11786`。
|
||||||
- D-305/ADR-0393(已接受):Plugin Package Manifest 的逻辑 Secret requirement 获得按 resource generation 固定的不可变 binding。binding 精确覆盖 Manifest requirements,只保存同 Project、显式 version 的 `qlsecret://` 引用与 `approved-action-execution|local-owner-confirmation` authority evidence digest,不保存 Secret 明文;required 不可为空,optional 可显式为 `null`。发布由当前 active installation head、lock、generation 与 Manifest digest 联合 fencing,相同事实幂等、不同事实冲突;domain-separated digest、最多 64 项和 64 KiB 单行预算同时约束 Local 与 Cluster。SQLite 追加 `0091` ledger 与 capability v46,PostgreSQL 追加 `pg-0059`、capability v58,并只向 package executor 授予 `SELECT, INSERT`。不新增 package、daemon、timer、watcher、连接、缓存或集群 workload,低配路由设备只承担一个有界表和三个索引。D-305 不冒充 Secret 已进入执行路径:现有 materialization 拒绝仍保留,D-306 再完成用户授权、Secret resolution、runtime consumption 与 lifecycle/rebinding 语义。core 509/509、SQLite 232/232、PostgreSQL 316 pass/1 条件 skip;完整 18-package clean build/test 退出 0,backend 1,188 pass/2 skip,五项边界审计零 finding,workspace 仍无 single-source/shallow-source package。PostgreSQL 18.4 arm64 HA 125 项 gate 全绿、timeline `1→2`,报告 SHA-256 为 `acf0fea7ca7699989dfe70f5dd0061cdf5fb1968c691094331fea06ce01b96dc`。
|
- D-305/ADR-0393(已接受):Plugin Package Manifest 的逻辑 Secret requirement 获得按 resource generation 固定的不可变 binding。binding 精确覆盖 Manifest requirements,只保存同 Project、显式 version 的 `qlsecret://` 引用与 `approved-action-execution|local-owner-confirmation` authority evidence digest,不保存 Secret 明文;required 不可为空,optional 可显式为 `null`。发布由当前 active installation head、lock、generation 与 Manifest digest 联合 fencing,相同事实幂等、不同事实冲突;domain-separated digest、最多 64 项和 64 KiB 单行预算同时约束 Local 与 Cluster。SQLite 追加 `0091` ledger 与 capability v46,PostgreSQL 追加 `pg-0059`、capability v58,并只向 package executor 授予 `SELECT, INSERT`。不新增 package、daemon、timer、watcher、连接、缓存或集群 workload,低配路由设备只承担一个有界表和三个索引。D-305 不冒充 Secret 已进入执行路径:现有 materialization 拒绝仍保留,D-306 再完成用户授权、Secret resolution、runtime consumption 与 lifecycle/rebinding 语义。core 509/509、SQLite 232/232、PostgreSQL 316 pass/1 条件 skip;完整 18-package clean build/test 退出 0,backend 1,188 pass/2 skip,五项边界审计零 finding,workspace 仍无 single-source/shallow-source package。PostgreSQL 18.4 arm64 HA 125 项 gate 全绿、timeline `1→2`,报告 SHA-256 为 `acf0fea7ca7699989dfe70f5dd0061cdf5fb1968c691094331fea06ce01b96dc`。
|
||||||
- D-306A/ADR-0394(已接受):Package Task source 以 `package-secret` placeholder 引用逻辑 requirement,materialization 只用当前 generation 的 D-305 binding 编译为已有、固定 version 的 Task `SecretRef`;Package source 直接携带 SecretRef、缺失 binding、未批准 `secret.use`、跨 binding 引用和 optional/required 漂移全部失败关闭。binding 快照不含明文并进入 materialized revision digest,Local/Cluster 启动发布复用既有 repository/pool,Task dispatch、Local 短时解密和 Cluster offer/lease-fenced delivery 不另造协议。SQLite/PostgreSQL INSERT trigger 同时防止直接写库绕过;Local 只读 readiness 继续不加载 DDL。Local contract v47、Cluster v59;不新增 package、表、索引、连接、daemon、watcher、timer、cache 或 workload。完整 18-package clean build/test 退出 0;backend 1,188 pass/2 条件 skip/0 fail;五项 package/dependency/edge/service-manager/local-image 审计零 finding,workspace 仍无 single-source/shallow-source package,两个有序 migration ledger 精确为 PostgreSQL 61、SQLite 95 个 source;PostgreSQL 18.4 arm64 HA 125 项 gate 全绿、timeline `1→2`,报告 SHA-256 为 `f9107e8e54892a788779758f0573ac8d6a80f6d086516a1f5f5bbacb59bbb4be`。D-306A 不冒充产品闭环:Local bind/rebind 命令、Cluster Approved Action/API 与新 generation rotation/revocation 编排属于 D-306B。
|
- D-306A/ADR-0394(已接受):Package Task source 以 `package-secret` placeholder 引用逻辑 requirement,materialization 只用当前 generation 的 D-305 binding 编译为已有、固定 version 的 Task `SecretRef`;Package source 直接携带 SecretRef、缺失 binding、未批准 `secret.use`、跨 binding 引用和 optional/required 漂移全部失败关闭。binding 快照不含明文并进入 materialized revision digest,Local/Cluster 启动发布复用既有 repository/pool,Task dispatch、Local 短时解密和 Cluster offer/lease-fenced delivery 不另造协议。SQLite/PostgreSQL INSERT trigger 同时防止直接写库绕过;Local 只读 readiness 继续不加载 DDL。Local contract v47、Cluster v59;不新增 package、表、索引、连接、daemon、watcher、timer、cache 或 workload。完整 18-package clean build/test 退出 0;backend 1,188 pass/2 条件 skip/0 fail;五项 package/dependency/edge/service-manager/local-image 审计零 finding,workspace 仍无 single-source/shallow-source package,两个有序 migration ledger 精确为 PostgreSQL 61、SQLite 95 个 source;PostgreSQL 18.4 arm64 HA 125 项 gate 全绿、timeline `1→2`,报告 SHA-256 为 `f9107e8e54892a788779758f0573ac8d6a80f6d086516a1f5f5bbacb59bbb4be`。D-306A 不冒充产品闭环:Local bind/rebind 命令、Cluster Approved Action/API 与新 generation rotation/revocation 编排属于 D-306B。
|
||||||
- D-306B1/ADR-0395(进行中):先完成当前 active、尚未绑定 Package generation 的首次 Secret binding,不允许原地 rebind。共享 content-free plan 由服务端从 installation/proposal/lock/Manifest/generation 重建,调用方不能注入 generation 或 digest。Local 已复用短生命周期 `ql3-package` 私有 command-file 和单 SQLite authority:强 `local-console` User 必须同时具备 `secret.manage` 与当前 Project Owner,human confirmation 后在一个 `BEGIN IMMEDIATE` transaction 内复验 Owner fence、Package provenance、精确 Secret version、content-free audit 与 immutable binding;plan 后撤权原子回滚,exact replay 收敛,不同 audit identity 或当前代重复绑定失败关闭。Cluster 核心 authority 已补齐 package-manager `plan→propose→separation-of-duty decide`、package-executor 有界 approved queue consumer、Approved Action handler 与 immutable PostgreSQL binding publish;executor 只对可选 read-only Kubernetes Secret 投影执行 metadata-only existence check,不读取 Secret value,缺投影失败关闭。PostgreSQL 18 三角色真实纵切面完整执行 install/activate→plan/propose/decide→consume/dispatch/bind 并发现、修复了 binding INSERT placeholder typmod 缺失;cluster-admin/cluster-postgres 最终全包 638 pass/2 条件 skip,完整 18-package 串行门禁与 backend 1,188 pass/2 条件 skip/0 fail 均通过,package/dependency/deployment/CloudNativePG 审计零 finding。最新 PostgreSQL 18.4 arm64 physical HA 通过 125 gate、timeline `1→2`,报告 SHA-256 为 `7d54b2194420f9f14919aec7b67d744ba51ef5f3e3b681bbda8b44cc00e4f3f6`。未新增 package、migration、表、索引、依赖、常驻进程、连接、listener、timer、watcher 或 cache;18 个 workspace package 仍无 single-source/shallow-source package。Cluster Secret binding 的产品 HTTP/CLI transport、Kubernetes live exercise 与 B2 rebind/rotation/revocation 仍待完成,所以 ADR 保持 Proposed。
|
- D-306B1/ADR-0395(进行中):先完成当前 active、尚未绑定 Package generation 的首次 Secret binding,不允许原地 rebind。共享 content-free plan 由服务端从 installation/proposal/lock/Manifest/generation 重建,调用方不能注入 generation 或 digest。Local 已复用短生命周期 `ql3-package` 私有 command-file 和单 SQLite authority:强 `local-console` User 必须同时具备 `secret.manage` 与当前 Project Owner,human confirmation 后在一个 `BEGIN IMMEDIATE` transaction 内复验 Owner fence、Package provenance、精确 Secret version、content-free audit 与 immutable binding;plan 后撤权原子回滚,exact replay 收敛,不同 audit identity 或当前代重复绑定失败关闭。Cluster 核心 authority 已补齐 package-manager `plan→propose→separation-of-duty decide`、package-executor 有界 approved queue consumer、Approved Action handler 与 immutable PostgreSQL binding publish;executor 只对可选 read-only Kubernetes Secret 投影执行 metadata-only existence check,不读取 Secret value,缺投影失败关闭。产品入口已复用既有 `/api/v3/plugin-packages/management`、mTLS/OIDC 强 User 身份、同一个 package-manager Pool、分布式 quota 与 `ql3-cluster-admin package` 一次性 client,新增严格 exact-shape 的 `plugin-package.secret-binding.plan|propose|decide|inspect` 四命令;成功响应仅含 target、固定 version SecretRef、审批与 digest 摘要,客户端会拒绝跨 Project SecretRef、重复 requirement、错误 action/approval/digest 绑定和扩展响应。该入口不把 Secret value、executor、Kubernetes 或 binding 表直读 authority 装进 management 进程,也不增加 listener、连接或 workload。PostgreSQL 18 三角色真实纵切面已从正式 transport 完整执行 install/activate→plan/replay/propose/decide/inspect→consume/dispatch/bind,并证明 manager 不能读取最终 binding 表。完整 18-package clean build/test 退出 0,cluster-admin 315 pass/3 条件 skip,backend 1,188 pass/2 条件 skip/0 fail;package/dependency/deployment/CloudNativePG/edge/service-manager/local-image 七项审计零 finding。最新 PostgreSQL 18.4 arm64 physical HA 通过 125 gate、timeline `1→2`,报告 SHA-256 为 `6506b74721891cfd5709b1661562c204a310e99ed06131a25c464128f4ded4c6`。未新增 package、migration、表、索引、依赖、常驻进程、连接、listener、timer、watcher、cache 或 workload;18 个 workspace package仍无 single-source/shallow-source package。Kubernetes live exercise 与 B2 rebind/rotation/revocation 仍待完成,所以 ADR 保持 Proposed。
|
||||||
- D-302/ADR-0390(已接受)
|
- D-302/ADR-0390(已接受)
|
||||||
Cluster operator context 增加无网络、无 mutation 的内建 `ql3-cluster-admin context validate` 预检。它先复用 owner-private context
|
Cluster operator context 增加无网络、无 mutation 的内建 `ql3-cluster-admin context validate` 预检。它先复用 owner-private context
|
||||||
reader,再让每个 entry 经过与真实请求相同的 production HTTPS/Kubernetes configuration preparation,验证精确 route、hostname、CA、
|
reader,再让每个 entry 经过与真实请求相同的 production HTTPS/Kubernetes configuration preparation,验证精确 route、hostname、CA、
|
||||||
|
|||||||
@@ -40,4 +40,6 @@ QingLong 3.0 同时面向低配路由设备和集群节点。本机产品入口
|
|||||||
- Runtime Core、Local SQLite、Local Owner CLI closure type-check 已通过;Local 产品定向纵切面 3/3。
|
- Runtime Core、Local SQLite、Local Owner CLI closure type-check 已通过;Local 产品定向纵切面 3/3。
|
||||||
- 18-package clean build/test 中除受限沙箱禁止绑定 `127.0.0.1` 导致的 Worker TLS 三项外,其余已执行测试均为 0 fail;同一 Worker 完整包在允许回环监听的环境为 133/133。
|
- 18-package clean build/test 中除受限沙箱禁止绑定 `127.0.0.1` 导致的 Worker TLS 三项外,其余已执行测试均为 0 fail;同一 Worker 完整包在允许回环监听的环境为 133/133。
|
||||||
- Cluster 核心 authority 已完成:package-manager durable plan/proposal/decision、package-executor 有界 consumer/dispatch/handler、只读可选 Secret 投影与 immutable binding publish。`cluster-admin` 与 `cluster-postgres` 最终全包合计 638 pass/2 条件 skip;PostgreSQL 18 三角色真实纵切面 1/1,覆盖真实 install/activate、plan/propose/decide、consume/dispatch/bind、exact replay、manager 对 binding 表拒绝和 content-free 断言,并发现、修复 INSERT placeholder typmod 缺失。
|
- Cluster 核心 authority 已完成:package-manager durable plan/proposal/decision、package-executor 有界 consumer/dispatch/handler、只读可选 Secret 投影与 immutable binding publish。`cluster-admin` 与 `cluster-postgres` 最终全包合计 638 pass/2 条件 skip;PostgreSQL 18 三角色真实纵切面 1/1,覆盖真实 install/activate、plan/propose/decide、consume/dispatch/bind、exact replay、manager 对 binding 表拒绝和 content-free 断言,并发现、修复 INSERT placeholder typmod 缺失。
|
||||||
- package boundary、cluster dependency、cluster deployment、CloudNativePG 审计零 finding;workspace 仍为 18 包且没有 single-source/shallow-source package。完整 18-package 串行测试与 backend 1,188 pass/2 条件 skip/0 fail 均通过。PostgreSQL 18.4 arm64 physical HA 125 gate、timeline `1→2` 已通过,报告 SHA-256 为 `7d54b2194420f9f14919aec7b67d744ba51ef5f3e3b681bbda8b44cc00e4f3f6`。Cluster Secret binding 产品 HTTP/CLI transport 与 Kubernetes live exercise 仍待完成,因此本 ADR 保持 Proposed。
|
- Cluster 产品入口已完成:复用既有 `/api/v3/plugin-packages/management` HTTPS route、mTLS/OIDC 强 User 身份、package-manager Pool、分布式 quota 与 `ql3-cluster-admin package` 一次性 client,开放 exact-shape 的 `plugin-package.secret-binding.plan|propose|decide|inspect`。响应仅含 content-free target、固定 version SecretRef、审批和 digest 摘要;client 会拒绝跨 Project、重复 requirement、action/approval/digest 漂移和扩展响应。management 进程不取得 Secret value、Kubernetes、executor 或最终 binding 表 authority,也不新增 listener、连接、进程或 workload。
|
||||||
|
- 当前 Cluster Admin runtime 镜像的统一产品 facade 已在 UID `10001:10001`、128 MiB、0.25 CPU、32 PIDs、read-only root、无网络、全 capability drop 和 `no-new-privileges` 下完成 live contract;七个子命令、operator context preflight/readiness 与 `package` 分派均通过。这是制品/CLI 资源门,不冒充 Kubernetes Secret projection 或多 Pod live exercise。
|
||||||
|
- 正式 transport 经 PostgreSQL 18 三角色真实纵切面 1/1,覆盖 install/activate、plan/exact replay/propose/separation-of-duty decide/authorized inspect、consumer/dispatch/bind、manager 最终表拒绝与 content-free 输出。`cluster-admin` 315 pass/3 条件 skip;完整 18-package clean build/test 退出 0,backend 1,188 pass/2 条件 skip/0 fail。package boundary、cluster dependency、cluster deployment、CloudNativePG、edge import、service-manager bridge、local image 七项审计零 finding;workspace 仍为 18 包且没有 single-source/shallow-source package。PostgreSQL 18.4 arm64 physical HA 125 gate、timeline `1→2` 已通过,报告 SHA-256 为 `6506b74721891cfd5709b1661562c204a310e99ed06131a25c464128f4ded4c6`。Kubernetes live exercise 仍待完成,因此本 ADR 保持 Proposed。
|
||||||
|
|||||||
@@ -52,17 +52,49 @@ chmod 600 /absolute/path/client.json \
|
|||||||
## 执行
|
## 执行
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
ql3-plugin-package-client \
|
ql3-cluster-admin package \
|
||||||
--config=/absolute/path/client.json \
|
--config=/absolute/path/client.json \
|
||||||
--command=/absolute/path/command.json \
|
--command=/absolute/path/command.json \
|
||||||
--assertion=/absolute/path/assertion.jwt
|
--assertion=/absolute/path/assertion.jwt
|
||||||
```
|
```
|
||||||
|
|
||||||
可用 operation 只有以下九个:
|
统一产品入口会在同一 Cluster Admin 制品内委派给
|
||||||
|
`ql3-plugin-package-client`;后者保留为直接 binary 兼容入口,两者使用同一协议和
|
||||||
|
authority,不会启动第二个管理进程。
|
||||||
|
|
||||||
|
可用 operation 只有以下十八个:
|
||||||
|
|
||||||
- `plugin-package.propose|decide|inspect`
|
- `plugin-package.propose|decide|inspect`
|
||||||
|
- `plugin-package.installation.inspect|list`
|
||||||
|
- `plugin-package.lifecycle.propose|decide|inspect`
|
||||||
- `plugin-package.publisher-revocation.propose|decide|inspect`
|
- `plugin-package.publisher-revocation.propose|decide|inspect`
|
||||||
- `plugin-package.publisher-trust-transition.propose|decide|inspect`
|
- `plugin-package.publisher-trust-transition.propose|decide|inspect`
|
||||||
|
- `plugin-package.secret-binding.plan|propose|decide|inspect`
|
||||||
|
|
||||||
|
首次 Secret binding 必须先提交 `plan`,由服务端从当前 active installation、lock、
|
||||||
|
Manifest 和 generation 重建目标。调用方只提供逻辑 requirement 到同 Project、固定
|
||||||
|
version `qlsecret:v1:` 引用的 assignment;不得提交 generation、digest、Manifest 或
|
||||||
|
Secret value。`plan` 返回 content-free target、requirement、digest 和有效期;随后以同一
|
||||||
|
`actionRef` 执行 `propose`,由另一名强认证 User 执行 `decide`。审批后的实际 binding 由
|
||||||
|
短生命周期 package executor 消费,management client 不持有 executor authority。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"operation": "plugin-package.secret-binding.plan",
|
||||||
|
"request": {
|
||||||
|
"actionRef": "secret-binding:example:1",
|
||||||
|
"projectId": "project-1",
|
||||||
|
"packageName": "example",
|
||||||
|
"assignments": [
|
||||||
|
{
|
||||||
|
"name": "TOKEN",
|
||||||
|
"secretRef": "qlsecret:v1:REPLACE_WITH_FIXED_VERSION_REFERENCE"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
成功时 stdout 只有服务端审查过的低敏 result。失败时 stderr 只有稳定 code,以及可能的
|
成功时 stdout 只有服务端审查过的低敏 result。失败时 stderr 只有稳定 code,以及可能的
|
||||||
HTTP status、response code、request ID 和 Retry-After;不会输出 assertion、路径、
|
HTTP status、response code、request ID 和 Retry-After;不会输出 assertion、路径、
|
||||||
@@ -114,7 +146,7 @@ port-forward 是 Kubernetes RBAC 控制的独立私有入口,不是 labelled-P
|
|||||||
执行:
|
执行:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
ql3-plugin-package-client-kubernetes \
|
ql3-cluster-admin package-kubernetes \
|
||||||
--config=/absolute/path/client.json \
|
--config=/absolute/path/client.json \
|
||||||
--command=/absolute/path/command.json \
|
--command=/absolute/path/command.json \
|
||||||
--assertion=/absolute/path/assertion.jwt \
|
--assertion=/absolute/path/assertion.jwt \
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Agent as HttpsAgent, request as httpsRequest } from 'node:https';
|
|||||||
import { Duplex } from 'node:stream';
|
import { Duplex } from 'node:stream';
|
||||||
import { connect as tlsConnect } from 'node:tls';
|
import { connect as tlsConnect } from 'node:tls';
|
||||||
import { TextDecoder } from 'node:util';
|
import { TextDecoder } from 'node:util';
|
||||||
|
import { parseSecretRef } from '@qinglong/runtime-core/secret-reference';
|
||||||
import {
|
import {
|
||||||
ClusterPluginPackageManagementClientConfigurationError,
|
ClusterPluginPackageManagementClientConfigurationError,
|
||||||
isReviewedClusterAuthenticatedManagementClientProtocol,
|
isReviewedClusterAuthenticatedManagementClientProtocol,
|
||||||
@@ -290,6 +291,21 @@ const LIFECYCLE_PLAN_KEYS = Object.freeze([
|
|||||||
'blockingReferences',
|
'blockingReferences',
|
||||||
'impactDigest',
|
'impactDigest',
|
||||||
]);
|
]);
|
||||||
|
const SECRET_BINDING_PLAN_KEYS = Object.freeze([
|
||||||
|
'actionRef',
|
||||||
|
'projectId',
|
||||||
|
'packageName',
|
||||||
|
'installationId',
|
||||||
|
'generation',
|
||||||
|
'generationDigest',
|
||||||
|
'lockDigest',
|
||||||
|
'manifestDigest',
|
||||||
|
'entries',
|
||||||
|
'plannedAtMs',
|
||||||
|
'expiresAtMs',
|
||||||
|
'planDigest',
|
||||||
|
'approvalPlanDigest',
|
||||||
|
]);
|
||||||
|
|
||||||
function validateScalarSummary(value: unknown, keys: readonly string[]): void {
|
function validateScalarSummary(value: unknown, keys: readonly string[]): void {
|
||||||
const record = exactResponseObject(value, keys);
|
const record = exactResponseObject(value, keys);
|
||||||
@@ -463,10 +479,211 @@ function validateLifecyclePlanSummary(value: unknown): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateSecretBindingPlanSummary(
|
||||||
|
value: unknown,
|
||||||
|
command: Readonly<
|
||||||
|
Extract<
|
||||||
|
ClusterPluginPackageManagementCommand,
|
||||||
|
{ readonly operation: `plugin-package.secret-binding.${string}` }
|
||||||
|
>
|
||||||
|
>,
|
||||||
|
): void {
|
||||||
|
const summary = exactResponseObject(value, SECRET_BINDING_PLAN_KEYS);
|
||||||
|
if (
|
||||||
|
typeof summary.actionRef !== 'string' ||
|
||||||
|
summary.actionRef.length < 1 ||
|
||||||
|
summary.actionRef.length > 255 ||
|
||||||
|
typeof summary.projectId !== 'string' ||
|
||||||
|
summary.projectId.length < 1 ||
|
||||||
|
summary.projectId.length > 128 ||
|
||||||
|
typeof summary.packageName !== 'string' ||
|
||||||
|
!PACKAGE_NAME_PATTERN.test(summary.packageName) ||
|
||||||
|
typeof summary.installationId !== 'string' ||
|
||||||
|
summary.installationId.length < 1 ||
|
||||||
|
summary.installationId.length > 128 ||
|
||||||
|
!Number.isSafeInteger(summary.generation) ||
|
||||||
|
(summary.generation as number) < 1 ||
|
||||||
|
!Number.isSafeInteger(summary.plannedAtMs) ||
|
||||||
|
!Number.isSafeInteger(summary.expiresAtMs) ||
|
||||||
|
(summary.expiresAtMs as number) <= (summary.plannedAtMs as number) ||
|
||||||
|
!Array.isArray(summary.entries) ||
|
||||||
|
summary.entries.length > 64 ||
|
||||||
|
new Set(
|
||||||
|
summary.entries.map((entry) =>
|
||||||
|
entry && typeof entry === 'object' && !Array.isArray(entry)
|
||||||
|
? (entry as JsonObject).name
|
||||||
|
: undefined,
|
||||||
|
),
|
||||||
|
).size !== summary.entries.length
|
||||||
|
) {
|
||||||
|
throw new ClusterPluginPackageManagementClientRequestError();
|
||||||
|
}
|
||||||
|
for (const key of [
|
||||||
|
'generationDigest',
|
||||||
|
'lockDigest',
|
||||||
|
'manifestDigest',
|
||||||
|
'planDigest',
|
||||||
|
'approvalPlanDigest',
|
||||||
|
]) {
|
||||||
|
if (
|
||||||
|
typeof summary[key] !== 'string' ||
|
||||||
|
!DIGEST_PATTERN.test(summary[key] as string)
|
||||||
|
) {
|
||||||
|
throw new ClusterPluginPackageManagementClientRequestError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const entryValue of summary.entries) {
|
||||||
|
const entry = exactResponseObject(entryValue, [
|
||||||
|
'name',
|
||||||
|
'required',
|
||||||
|
'secretRef',
|
||||||
|
]);
|
||||||
|
if (
|
||||||
|
typeof entry.name !== 'string' ||
|
||||||
|
!/^[A-Z_][A-Z0-9_]{0,127}$/.test(entry.name) ||
|
||||||
|
typeof entry.required !== 'boolean' ||
|
||||||
|
(entry.secretRef !== null &&
|
||||||
|
(typeof entry.secretRef !== 'string' ||
|
||||||
|
entry.secretRef.length > 2_048 ||
|
||||||
|
CONTROL_PATTERN.test(entry.secretRef))) ||
|
||||||
|
(entry.required === true && entry.secretRef === null)
|
||||||
|
) {
|
||||||
|
throw new ClusterPluginPackageManagementClientRequestError();
|
||||||
|
}
|
||||||
|
if (entry.secretRef !== null) {
|
||||||
|
try {
|
||||||
|
const reference = parseSecretRef(entry.secretRef);
|
||||||
|
if (
|
||||||
|
reference.projectId !== summary.projectId ||
|
||||||
|
typeof reference.version !== 'number' ||
|
||||||
|
!Number.isSafeInteger(reference.version) ||
|
||||||
|
reference.version < 1
|
||||||
|
) {
|
||||||
|
throw new ClusterPluginPackageManagementClientRequestError();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ClusterPluginPackageManagementClientRequestError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new ClusterPluginPackageManagementClientRequestError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
summary.actionRef !== command.request.actionRef ||
|
||||||
|
command.operation === 'plugin-package.secret-binding.plan' &&
|
||||||
|
(summary.projectId !== command.request.projectId ||
|
||||||
|
summary.packageName !== command.request.packageName ||
|
||||||
|
summary.entries.length !== command.request.assignments.length ||
|
||||||
|
command.request.assignments.some((assignment) => {
|
||||||
|
const responseEntry = (summary.entries as JsonObject[]).find(
|
||||||
|
(entry) => entry.name === assignment.name,
|
||||||
|
);
|
||||||
|
return !responseEntry || responseEntry.secretRef !== assignment.secretRef;
|
||||||
|
}))
|
||||||
|
) {
|
||||||
|
throw new ClusterPluginPackageManagementClientRequestError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function validateResult(
|
function validateResult(
|
||||||
value: unknown,
|
value: unknown,
|
||||||
command: Readonly<ClusterPluginPackageManagementCommand>,
|
command: Readonly<ClusterPluginPackageManagementCommand>,
|
||||||
): Readonly<ClusterPluginPackageManagementTransportResult> {
|
): Readonly<ClusterPluginPackageManagementTransportResult> {
|
||||||
|
if (command.operation === 'plugin-package.secret-binding.plan') {
|
||||||
|
const result = exactResponseObject(value, [
|
||||||
|
'schemaVersion',
|
||||||
|
'operation',
|
||||||
|
'status',
|
||||||
|
'plan',
|
||||||
|
]);
|
||||||
|
if (
|
||||||
|
result.schemaVersion !== 1 ||
|
||||||
|
result.operation !== command.operation ||
|
||||||
|
!['created', 'existing'].includes(String(result.status))
|
||||||
|
) {
|
||||||
|
throw new ClusterPluginPackageManagementClientRequestError();
|
||||||
|
}
|
||||||
|
validateSecretBindingPlanSummary(result.plan, command);
|
||||||
|
return Object.freeze(
|
||||||
|
result as unknown as ClusterPluginPackageManagementTransportResult,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (command.operation === 'plugin-package.secret-binding.propose') {
|
||||||
|
const result = exactResponseObject(value, [
|
||||||
|
'schemaVersion',
|
||||||
|
'operation',
|
||||||
|
'approvalStatus',
|
||||||
|
'plan',
|
||||||
|
'approval',
|
||||||
|
]);
|
||||||
|
if (
|
||||||
|
result.schemaVersion !== 1 ||
|
||||||
|
result.operation !== command.operation ||
|
||||||
|
!['created', 'existing'].includes(String(result.approvalStatus))
|
||||||
|
) {
|
||||||
|
throw new ClusterPluginPackageManagementClientRequestError();
|
||||||
|
}
|
||||||
|
validateSecretBindingPlanSummary(result.plan, command);
|
||||||
|
validateScalarSummary(result.approval, APPROVAL_KEYS);
|
||||||
|
const plan = result.plan as JsonObject;
|
||||||
|
const approval = result.approval as JsonObject;
|
||||||
|
if (
|
||||||
|
approval.id !== command.request.approvalRequestId ||
|
||||||
|
approval.projectId !== plan.projectId ||
|
||||||
|
approval.actionDigest !== plan.approvalPlanDigest ||
|
||||||
|
approval.previewDigest !== plan.planDigest
|
||||||
|
) {
|
||||||
|
throw new ClusterPluginPackageManagementClientRequestError();
|
||||||
|
}
|
||||||
|
return Object.freeze(
|
||||||
|
result as unknown as ClusterPluginPackageManagementTransportResult,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (command.operation === 'plugin-package.secret-binding.inspect') {
|
||||||
|
const result = exactResponseObject(value, [
|
||||||
|
'schemaVersion',
|
||||||
|
'operation',
|
||||||
|
'plan',
|
||||||
|
'approval',
|
||||||
|
'stale',
|
||||||
|
]);
|
||||||
|
if (
|
||||||
|
result.schemaVersion !== 1 ||
|
||||||
|
result.operation !== command.operation ||
|
||||||
|
typeof result.stale !== 'boolean' ||
|
||||||
|
result.plan === null && result.approval === null
|
||||||
|
) {
|
||||||
|
throw new ClusterPluginPackageManagementClientRequestError();
|
||||||
|
}
|
||||||
|
if (result.plan !== null) {
|
||||||
|
validateSecretBindingPlanSummary(result.plan, command);
|
||||||
|
}
|
||||||
|
if (result.approval !== null) {
|
||||||
|
validateScalarSummary(result.approval, APPROVAL_KEYS);
|
||||||
|
if (
|
||||||
|
(result.approval as JsonObject).id !==
|
||||||
|
command.request.approvalRequestId
|
||||||
|
) {
|
||||||
|
throw new ClusterPluginPackageManagementClientRequestError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.plan !== null && result.approval !== null) {
|
||||||
|
const plan = result.plan as JsonObject;
|
||||||
|
const approval = result.approval as JsonObject;
|
||||||
|
if (
|
||||||
|
approval.id !== command.request.approvalRequestId ||
|
||||||
|
approval.projectId !== plan.projectId ||
|
||||||
|
approval.actionDigest !== plan.approvalPlanDigest ||
|
||||||
|
approval.previewDigest !== plan.planDigest
|
||||||
|
) {
|
||||||
|
throw new ClusterPluginPackageManagementClientRequestError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Object.freeze(
|
||||||
|
result as unknown as ClusterPluginPackageManagementTransportResult,
|
||||||
|
);
|
||||||
|
}
|
||||||
if (command.operation === 'plugin-package.installation.inspect') {
|
if (command.operation === 'plugin-package.installation.inspect') {
|
||||||
const result = exactResponseObject(value, [
|
const result = exactResponseObject(value, [
|
||||||
'schemaVersion',
|
'schemaVersion',
|
||||||
@@ -617,6 +834,12 @@ function validateResult(
|
|||||||
}
|
}
|
||||||
if (result.approval !== null) {
|
if (result.approval !== null) {
|
||||||
validateScalarSummary(result.approval, APPROVAL_KEYS);
|
validateScalarSummary(result.approval, APPROVAL_KEYS);
|
||||||
|
if (
|
||||||
|
command.operation === 'plugin-package.secret-binding.decide' &&
|
||||||
|
(result.approval as JsonObject).id !== command.request.approvalRequestId
|
||||||
|
) {
|
||||||
|
throw new ClusterPluginPackageManagementClientRequestError();
|
||||||
|
}
|
||||||
} else if (kind !== 'inspect') {
|
} else if (kind !== 'inspect') {
|
||||||
throw new ClusterPluginPackageManagementClientRequestError();
|
throw new ClusterPluginPackageManagementClientRequestError();
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -28,6 +28,7 @@ import {
|
|||||||
} from '../../management-support/pluginPackageIdentityKeyset';
|
} from '../../management-support/pluginPackageIdentityKeyset';
|
||||||
import { createClusterPluginPackageManagementService } from './pluginPackageManagement';
|
import { createClusterPluginPackageManagementService } from './pluginPackageManagement';
|
||||||
import { createClusterPluginPackageLifecycleManagementService } from '../lifecycle/pluginPackageLifecycleManagement';
|
import { createClusterPluginPackageLifecycleManagementService } from '../lifecycle/pluginPackageLifecycleManagement';
|
||||||
|
import { createClusterPluginPackageSecretBindingManagementService } from '../secret-binding/pluginPackageSecretBindingManagement';
|
||||||
import {
|
import {
|
||||||
loadClusterPluginPackagePublisherTrustFileEvidence,
|
loadClusterPluginPackagePublisherTrustFileEvidence,
|
||||||
type ClusterPluginPackagePublisherTrustFileEvidence,
|
type ClusterPluginPackagePublisherTrustFileEvidence,
|
||||||
@@ -635,10 +636,18 @@ export async function startClusterPluginPackageManagementProcess(
|
|||||||
now,
|
now,
|
||||||
quota,
|
quota,
|
||||||
});
|
});
|
||||||
|
const secretBinding =
|
||||||
|
createClusterPluginPackageSecretBindingManagementService({
|
||||||
|
pool: database.pool,
|
||||||
|
approvalLifetimeMs: config.approvalLifetimeMs,
|
||||||
|
now,
|
||||||
|
quota,
|
||||||
|
});
|
||||||
const transport = createClusterPluginPackageManagementTransport({
|
const transport = createClusterPluginPackageManagementTransport({
|
||||||
service,
|
service,
|
||||||
lifecycle,
|
lifecycle,
|
||||||
publisherTrust,
|
publisherTrust,
|
||||||
|
secretBinding,
|
||||||
now,
|
now,
|
||||||
});
|
});
|
||||||
const privateKey = readTlsFile(config.privateKeyFile, true);
|
const privateKey = readTlsFile(config.privateKeyFile, true);
|
||||||
|
|||||||
+212
-1
@@ -14,6 +14,8 @@ import type {
|
|||||||
} from '@qinglong/runtime-core/plugin-package-management';
|
} from '@qinglong/runtime-core/plugin-package-management';
|
||||||
import type { PluginPackageInstallProposal } from '@qinglong/runtime-core/plugin-package-proposal';
|
import type { PluginPackageInstallProposal } from '@qinglong/runtime-core/plugin-package-proposal';
|
||||||
import type { PluginPackageLifecyclePlan } from '@qinglong/runtime-core/plugin-package-lifecycle-plan';
|
import type { PluginPackageLifecyclePlan } from '@qinglong/runtime-core/plugin-package-lifecycle-plan';
|
||||||
|
import type { PluginPackageSecretBindingAssignment } from '@qinglong/runtime-core/plugin-package-secret-binding';
|
||||||
|
import type { PluginPackageSecretBindingApprovalPlan } from '@qinglong/runtime-core/plugin-package-secret-binding-approval-plan';
|
||||||
import {
|
import {
|
||||||
normalizeSecurityPrincipal,
|
normalizeSecurityPrincipal,
|
||||||
type SecurityPrincipal,
|
type SecurityPrincipal,
|
||||||
@@ -25,6 +27,7 @@ import type {
|
|||||||
InspectClusterPluginPackagePublisherRevocationResult,
|
InspectClusterPluginPackagePublisherRevocationResult,
|
||||||
InspectClusterPluginPackagePublisherTrustTransitionResult,
|
InspectClusterPluginPackagePublisherTrustTransitionResult,
|
||||||
} from '../publisher/pluginPackagePublisherTrustManagement';
|
} from '../publisher/pluginPackagePublisherTrustManagement';
|
||||||
|
import type { ClusterPluginPackageSecretBindingManagementService } from '../secret-binding/pluginPackageSecretBindingManagement';
|
||||||
|
|
||||||
const STRONG_CLUSTER_ASSURANCES = new Set(['multi_factor', 'hardware']);
|
const STRONG_CLUSTER_ASSURANCES = new Set(['multi_factor', 'hardware']);
|
||||||
|
|
||||||
@@ -166,6 +169,39 @@ export interface InspectClusterPluginPackagePublisherTrustTransitionCommand {
|
|||||||
readonly request: InspectClusterPluginPackageCommand['request'];
|
readonly request: InspectClusterPluginPackageCommand['request'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PlanClusterPluginPackageSecretBindingCommand {
|
||||||
|
readonly schemaVersion: 1;
|
||||||
|
readonly operation: 'plugin-package.secret-binding.plan';
|
||||||
|
readonly request: {
|
||||||
|
readonly actionRef: string;
|
||||||
|
readonly projectId: string;
|
||||||
|
readonly packageName: string;
|
||||||
|
readonly assignments: readonly Readonly<PluginPackageSecretBindingAssignment>[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProposeClusterPluginPackageSecretBindingCommand {
|
||||||
|
readonly schemaVersion: 1;
|
||||||
|
readonly operation: 'plugin-package.secret-binding.propose';
|
||||||
|
readonly request: {
|
||||||
|
readonly actionRef: string;
|
||||||
|
readonly approvalRequestId: string;
|
||||||
|
readonly approvalAuditEventId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DecideClusterPluginPackageSecretBindingCommand {
|
||||||
|
readonly schemaVersion: 1;
|
||||||
|
readonly operation: 'plugin-package.secret-binding.decide';
|
||||||
|
readonly request: DecideClusterPluginPackageCommand['request'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InspectClusterPluginPackageSecretBindingCommand {
|
||||||
|
readonly schemaVersion: 1;
|
||||||
|
readonly operation: 'plugin-package.secret-binding.inspect';
|
||||||
|
readonly request: InspectClusterPluginPackageCommand['request'];
|
||||||
|
}
|
||||||
|
|
||||||
export type ClusterPluginPackageManagementCommand =
|
export type ClusterPluginPackageManagementCommand =
|
||||||
| ProposeClusterPluginPackageCommand
|
| ProposeClusterPluginPackageCommand
|
||||||
| DecideClusterPluginPackageCommand
|
| DecideClusterPluginPackageCommand
|
||||||
@@ -180,7 +216,11 @@ export type ClusterPluginPackageManagementCommand =
|
|||||||
| InspectClusterPluginPackagePublisherRevocationCommand
|
| InspectClusterPluginPackagePublisherRevocationCommand
|
||||||
| ProposeClusterPluginPackagePublisherTrustTransitionCommand
|
| ProposeClusterPluginPackagePublisherTrustTransitionCommand
|
||||||
| DecideClusterPluginPackagePublisherTrustTransitionCommand
|
| DecideClusterPluginPackagePublisherTrustTransitionCommand
|
||||||
| InspectClusterPluginPackagePublisherTrustTransitionCommand;
|
| InspectClusterPluginPackagePublisherTrustTransitionCommand
|
||||||
|
| PlanClusterPluginPackageSecretBindingCommand
|
||||||
|
| ProposeClusterPluginPackageSecretBindingCommand
|
||||||
|
| DecideClusterPluginPackageSecretBindingCommand
|
||||||
|
| InspectClusterPluginPackageSecretBindingCommand;
|
||||||
|
|
||||||
export type ClusterPluginPackageManagementTransportResult =
|
export type ClusterPluginPackageManagementTransportResult =
|
||||||
| Readonly<{
|
| Readonly<{
|
||||||
@@ -276,6 +316,32 @@ export type ClusterPluginPackageManagementTransportResult =
|
|||||||
typeof publisherTrustTransitionProposalSummary
|
typeof publisherTrustTransitionProposalSummary
|
||||||
> | null;
|
> | null;
|
||||||
approval: ReturnType<typeof approvalSummary> | null;
|
approval: ReturnType<typeof approvalSummary> | null;
|
||||||
|
}>
|
||||||
|
| Readonly<{
|
||||||
|
schemaVersion: 1;
|
||||||
|
operation: 'plugin-package.secret-binding.plan';
|
||||||
|
status: 'created' | 'existing';
|
||||||
|
plan: ReturnType<typeof secretBindingPlanSummary>;
|
||||||
|
}>
|
||||||
|
| Readonly<{
|
||||||
|
schemaVersion: 1;
|
||||||
|
operation: 'plugin-package.secret-binding.propose';
|
||||||
|
approvalStatus: 'created' | 'existing';
|
||||||
|
plan: ReturnType<typeof secretBindingPlanSummary>;
|
||||||
|
approval: ReturnType<typeof approvalSummary>;
|
||||||
|
}>
|
||||||
|
| Readonly<{
|
||||||
|
schemaVersion: 1;
|
||||||
|
operation: 'plugin-package.secret-binding.decide';
|
||||||
|
status: 'decided' | 'existing';
|
||||||
|
approval: ReturnType<typeof approvalSummary>;
|
||||||
|
}>
|
||||||
|
| Readonly<{
|
||||||
|
schemaVersion: 1;
|
||||||
|
operation: 'plugin-package.secret-binding.inspect';
|
||||||
|
plan: ReturnType<typeof secretBindingPlanSummary> | null;
|
||||||
|
approval: ReturnType<typeof approvalSummary> | null;
|
||||||
|
stale: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export interface ClusterPluginPackageManagementTransport {
|
export interface ClusterPluginPackageManagementTransport {
|
||||||
@@ -289,6 +355,7 @@ export interface ClusterPluginPackageManagementTransportOptions {
|
|||||||
readonly service: ClusterPluginPackageManagementService;
|
readonly service: ClusterPluginPackageManagementService;
|
||||||
readonly lifecycle?: ClusterPluginPackageLifecycleManagementService;
|
readonly lifecycle?: ClusterPluginPackageLifecycleManagementService;
|
||||||
readonly publisherTrust?: ClusterPluginPackagePublisherTrustManagementService;
|
readonly publisherTrust?: ClusterPluginPackagePublisherTrustManagementService;
|
||||||
|
readonly secretBinding?: ClusterPluginPackageSecretBindingManagementService;
|
||||||
readonly now?: () => number;
|
readonly now?: () => number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,6 +595,50 @@ export function normalizeClusterPluginPackageManagementCommand(
|
|||||||
'publisher trust transition inspection request',
|
'publisher trust transition inspection request',
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case 'plugin-package.secret-binding.plan':
|
||||||
|
exactObject(
|
||||||
|
value.request,
|
||||||
|
['actionRef', 'assignments', 'packageName', 'projectId'],
|
||||||
|
'Secret binding plan request',
|
||||||
|
);
|
||||||
|
if (!Array.isArray(value.request.assignments)) {
|
||||||
|
throw new ClusterPluginPackageManagementTransportRequestError(
|
||||||
|
'Secret binding assignments are invalid',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const assignment of value.request.assignments) {
|
||||||
|
exactObject(assignment, ['name', 'secretRef'], 'Secret binding assignment');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'plugin-package.secret-binding.propose':
|
||||||
|
exactObject(
|
||||||
|
value.request,
|
||||||
|
['actionRef', 'approvalAuditEventId', 'approvalRequestId'],
|
||||||
|
'Secret binding proposal request',
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'plugin-package.secret-binding.decide':
|
||||||
|
exactObject(
|
||||||
|
value.request,
|
||||||
|
[
|
||||||
|
'actionRef',
|
||||||
|
'approvalRequestId',
|
||||||
|
'expectedVersion',
|
||||||
|
'decisionId',
|
||||||
|
'auditEventId',
|
||||||
|
'decision',
|
||||||
|
'reasonCode',
|
||||||
|
],
|
||||||
|
'Secret binding decision request',
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'plugin-package.secret-binding.inspect':
|
||||||
|
exactObject(
|
||||||
|
value.request,
|
||||||
|
['actionRef', 'approvalRequestId', 'inspectionId'],
|
||||||
|
'Secret binding inspection request',
|
||||||
|
);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new ClusterPluginPackageManagementTransportRequestError(
|
throw new ClusterPluginPackageManagementTransportRequestError(
|
||||||
'operation is not publicly available',
|
'operation is not publicly available',
|
||||||
@@ -640,6 +751,26 @@ function lifecyclePlanSummary(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function secretBindingPlanSummary(
|
||||||
|
plan: Readonly<PluginPackageSecretBindingApprovalPlan>,
|
||||||
|
) {
|
||||||
|
return Object.freeze({
|
||||||
|
actionRef: plan.actionRef,
|
||||||
|
projectId: plan.bindingPlan.target.projectId,
|
||||||
|
packageName: plan.bindingPlan.target.packageName,
|
||||||
|
installationId: plan.bindingPlan.target.installationId,
|
||||||
|
generation: plan.bindingPlan.target.generation,
|
||||||
|
generationDigest: plan.bindingPlan.target.generationDigest,
|
||||||
|
lockDigest: plan.bindingPlan.target.lockDigest,
|
||||||
|
manifestDigest: plan.bindingPlan.target.manifestDigest,
|
||||||
|
entries: plan.bindingPlan.entries,
|
||||||
|
plannedAtMs: plan.bindingPlan.plannedAtMs,
|
||||||
|
expiresAtMs: plan.expiresAtMs,
|
||||||
|
planDigest: plan.bindingPlan.planDigest,
|
||||||
|
approvalPlanDigest: plan.approvalPlanDigest,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function publisherRevocationProposalSummary(
|
function publisherRevocationProposalSummary(
|
||||||
proposal: NonNullable<
|
proposal: NonNullable<
|
||||||
InspectClusterPluginPackagePublisherRevocationResult['proposal']
|
InspectClusterPluginPackagePublisherRevocationResult['proposal']
|
||||||
@@ -694,6 +825,7 @@ function exactDecisionReplay(
|
|||||||
| DecideClusterPluginPackageLifecycleCommand
|
| DecideClusterPluginPackageLifecycleCommand
|
||||||
| DecideClusterPluginPackagePublisherRevocationCommand
|
| DecideClusterPluginPackagePublisherRevocationCommand
|
||||||
| DecideClusterPluginPackagePublisherTrustTransitionCommand
|
| DecideClusterPluginPackagePublisherTrustTransitionCommand
|
||||||
|
| DecideClusterPluginPackageSecretBindingCommand
|
||||||
>,
|
>,
|
||||||
principal: Readonly<SecurityPrincipal>,
|
principal: Readonly<SecurityPrincipal>,
|
||||||
): Readonly<DecideApprovalRequestResult> | null {
|
): Readonly<DecideApprovalRequestResult> | null {
|
||||||
@@ -726,6 +858,7 @@ export function createClusterPluginPackageManagementTransport(
|
|||||||
key !== 'service' &&
|
key !== 'service' &&
|
||||||
key !== 'lifecycle' &&
|
key !== 'lifecycle' &&
|
||||||
key !== 'publisherTrust' &&
|
key !== 'publisherTrust' &&
|
||||||
|
key !== 'secretBinding' &&
|
||||||
key !== 'now',
|
key !== 'now',
|
||||||
) ||
|
) ||
|
||||||
!options.service ||
|
!options.service ||
|
||||||
@@ -745,6 +878,12 @@ export function createClusterPluginPackageManagementTransport(
|
|||||||
typeof options.publisherTrust.propose !== 'function' ||
|
typeof options.publisherTrust.propose !== 'function' ||
|
||||||
typeof options.publisherTrust.inspect !== 'function' ||
|
typeof options.publisherTrust.inspect !== 'function' ||
|
||||||
typeof options.publisherTrust.inspectAuthorized !== 'function')) ||
|
typeof options.publisherTrust.inspectAuthorized !== 'function')) ||
|
||||||
|
(options.secretBinding !== undefined &&
|
||||||
|
(!options.secretBinding ||
|
||||||
|
typeof options.secretBinding.plan !== 'function' ||
|
||||||
|
typeof options.secretBinding.propose !== 'function' ||
|
||||||
|
typeof options.secretBinding.decide !== 'function' ||
|
||||||
|
typeof options.secretBinding.inspectAuthorized !== 'function')) ||
|
||||||
(options.now !== undefined && typeof options.now !== 'function')
|
(options.now !== undefined && typeof options.now !== 'function')
|
||||||
) {
|
) {
|
||||||
throw new ClusterPluginPackageManagementTransportConfigurationError(
|
throw new ClusterPluginPackageManagementTransportConfigurationError(
|
||||||
@@ -941,6 +1080,78 @@ export function createClusterPluginPackageManagementTransport(
|
|||||||
stale: result.stale,
|
stale: result.stale,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
case 'plugin-package.secret-binding.plan': {
|
||||||
|
if (!options.secretBinding) {
|
||||||
|
throw new ClusterPluginPackageManagementTransportConfigurationError(
|
||||||
|
'Secret binding management is not configured',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const result = await options.secretBinding.plan({
|
||||||
|
...command.request,
|
||||||
|
principal,
|
||||||
|
});
|
||||||
|
return Object.freeze({
|
||||||
|
schemaVersion: 1 as const,
|
||||||
|
operation: command.operation,
|
||||||
|
status: result.status,
|
||||||
|
plan: secretBindingPlanSummary(result.plan),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case 'plugin-package.secret-binding.propose': {
|
||||||
|
if (!options.secretBinding) {
|
||||||
|
throw new ClusterPluginPackageManagementTransportConfigurationError(
|
||||||
|
'Secret binding management is not configured',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const result = await options.secretBinding.propose({
|
||||||
|
...command.request,
|
||||||
|
principal,
|
||||||
|
});
|
||||||
|
return Object.freeze({
|
||||||
|
schemaVersion: 1 as const,
|
||||||
|
operation: command.operation,
|
||||||
|
approvalStatus: result.approvalStatus,
|
||||||
|
plan: secretBindingPlanSummary(result.plan),
|
||||||
|
approval: approvalSummary(result.approvalRequest),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case 'plugin-package.secret-binding.decide': {
|
||||||
|
if (!options.secretBinding) {
|
||||||
|
throw new ClusterPluginPackageManagementTransportConfigurationError(
|
||||||
|
'Secret binding management is not configured',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const result = await options.secretBinding.decide({
|
||||||
|
...command.request,
|
||||||
|
principal,
|
||||||
|
});
|
||||||
|
return Object.freeze({
|
||||||
|
schemaVersion: 1 as const,
|
||||||
|
operation: command.operation,
|
||||||
|
status: result.status,
|
||||||
|
approval: approvalSummary(result.request),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case 'plugin-package.secret-binding.inspect': {
|
||||||
|
if (!options.secretBinding) {
|
||||||
|
throw new ClusterPluginPackageManagementTransportConfigurationError(
|
||||||
|
'Secret binding management is not configured',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const result = await options.secretBinding.inspectAuthorized({
|
||||||
|
...command.request,
|
||||||
|
principal,
|
||||||
|
});
|
||||||
|
return Object.freeze({
|
||||||
|
schemaVersion: 1 as const,
|
||||||
|
operation: command.operation,
|
||||||
|
plan: result.plan ? secretBindingPlanSummary(result.plan) : null,
|
||||||
|
approval: result.approvalRequest
|
||||||
|
? approvalSummary(result.approvalRequest)
|
||||||
|
: null,
|
||||||
|
stale: result.stale,
|
||||||
|
});
|
||||||
|
}
|
||||||
case 'plugin-package.publisher-revocation.propose': {
|
case 'plugin-package.publisher-revocation.propose': {
|
||||||
if (!options.publisherTrust) {
|
if (!options.publisherTrust) {
|
||||||
throw new ClusterPluginPackageManagementTransportConfigurationError(
|
throw new ClusterPluginPackageManagementTransportConfigurationError(
|
||||||
|
|||||||
+63
-4
@@ -15,8 +15,11 @@ import {
|
|||||||
import {
|
import {
|
||||||
PluginPackageManagementAuthorizationError,
|
PluginPackageManagementAuthorizationError,
|
||||||
PluginPackageManagementConflictError,
|
PluginPackageManagementConflictError,
|
||||||
|
PluginPackageManagementQuotaExceededError,
|
||||||
PluginPackageManagementRequestError,
|
PluginPackageManagementRequestError,
|
||||||
PluginPackageManagementUnavailableError,
|
PluginPackageManagementUnavailableError,
|
||||||
|
type PluginPackageManagementQuotaOperation,
|
||||||
|
type PluginPackageManagementQuotaPort,
|
||||||
} from '@qinglong/runtime-core/plugin-package-management';
|
} from '@qinglong/runtime-core/plugin-package-management';
|
||||||
import { createPluginPackageResourceGenerationFromReferences } from '@qinglong/runtime-core/plugin-package-resource-generation';
|
import { createPluginPackageResourceGenerationFromReferences } from '@qinglong/runtime-core/plugin-package-resource-generation';
|
||||||
import type { PluginPackageSecretBindingAssignment } from '@qinglong/runtime-core/plugin-package-secret-binding';
|
import type { PluginPackageSecretBindingAssignment } from '@qinglong/runtime-core/plugin-package-secret-binding';
|
||||||
@@ -111,6 +114,7 @@ export interface ClusterPluginPackageSecretBindingManagementOptions {
|
|||||||
readonly now?: () => number;
|
readonly now?: () => number;
|
||||||
readonly planLifetimeMs?: number;
|
readonly planLifetimeMs?: number;
|
||||||
readonly approvalLifetimeMs?: number;
|
readonly approvalLifetimeMs?: number;
|
||||||
|
readonly quota?: PluginPackageManagementQuotaPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
function exact(value: unknown, keys: readonly string[], label: string): void {
|
function exact(value: unknown, keys: readonly string[], label: string): void {
|
||||||
@@ -226,12 +230,15 @@ export function createClusterPluginPackageSecretBindingManagementService(
|
|||||||
key !== 'pool' &&
|
key !== 'pool' &&
|
||||||
key !== 'now' &&
|
key !== 'now' &&
|
||||||
key !== 'planLifetimeMs' &&
|
key !== 'planLifetimeMs' &&
|
||||||
key !== 'approvalLifetimeMs',
|
key !== 'approvalLifetimeMs' &&
|
||||||
|
key !== 'quota',
|
||||||
) ||
|
) ||
|
||||||
!options.pool ||
|
!options.pool ||
|
||||||
typeof options.pool.query !== 'function' ||
|
typeof options.pool.query !== 'function' ||
|
||||||
typeof options.pool.connect !== 'function' ||
|
typeof options.pool.connect !== 'function' ||
|
||||||
(options.now !== undefined && typeof options.now !== 'function')
|
(options.now !== undefined && typeof options.now !== 'function') ||
|
||||||
|
(options.quota !== undefined &&
|
||||||
|
(!options.quota || typeof options.quota.consume !== 'function'))
|
||||||
) {
|
) {
|
||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
'cluster Plugin Package Secret binding management options are invalid',
|
'cluster Plugin Package Secret binding management options are invalid',
|
||||||
@@ -265,6 +272,28 @@ export function createClusterPluginPackageSecretBindingManagementService(
|
|||||||
new PostgresProjectPolicyRepository(options.pool),
|
new PostgresProjectPolicyRepository(options.pool),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const consumeQuota = async (
|
||||||
|
projectId: string,
|
||||||
|
principal: Readonly<SecurityPrincipal>,
|
||||||
|
operation: PluginPackageManagementQuotaOperation,
|
||||||
|
idempotencyKey: string,
|
||||||
|
): Promise<void> => {
|
||||||
|
if (!options.quota) return;
|
||||||
|
try {
|
||||||
|
await options.quota.consume({
|
||||||
|
projectId,
|
||||||
|
subject: principal.subject,
|
||||||
|
operation,
|
||||||
|
idempotencyKey,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof PluginPackageManagementQuotaExceededError) throw error;
|
||||||
|
throw new PluginPackageManagementUnavailableError({
|
||||||
|
cause: error instanceof Error ? error : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const authorize = async (
|
const authorize = async (
|
||||||
principalValue: SecurityPrincipal,
|
principalValue: SecurityPrincipal,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@@ -346,6 +375,12 @@ export function createClusterPluginPackageSecretBindingManagementService(
|
|||||||
currentTime(now),
|
currentTime(now),
|
||||||
);
|
);
|
||||||
const requestedActionRef = actionRef(request.actionRef);
|
const requestedActionRef = actionRef(request.actionRef);
|
||||||
|
await consumeQuota(
|
||||||
|
projectId,
|
||||||
|
authorization.principal,
|
||||||
|
'plugin-package.propose',
|
||||||
|
requestedActionRef,
|
||||||
|
);
|
||||||
let existingValue;
|
let existingValue;
|
||||||
try {
|
try {
|
||||||
existingValue = await plans.findByActionRef(requestedActionRef);
|
existingValue = await plans.findByActionRef(requestedActionRef);
|
||||||
@@ -458,6 +493,12 @@ export function createClusterPluginPackageSecretBindingManagementService(
|
|||||||
'secret.manage',
|
'secret.manage',
|
||||||
observedAtMs,
|
observedAtMs,
|
||||||
);
|
);
|
||||||
|
await consumeQuota(
|
||||||
|
plan.bindingPlan.target.projectId,
|
||||||
|
authorization.principal,
|
||||||
|
'plugin-package.propose',
|
||||||
|
approvalRequestId,
|
||||||
|
);
|
||||||
if (!sameSubject(plan.requestedBy, authorization.principal.subject)) {
|
if (!sameSubject(plan.requestedBy, authorization.principal.subject)) {
|
||||||
throw new PluginPackageManagementAuthorizationError();
|
throw new PluginPackageManagementAuthorizationError();
|
||||||
}
|
}
|
||||||
@@ -572,6 +613,12 @@ export function createClusterPluginPackageSecretBindingManagementService(
|
|||||||
'approval.decide',
|
'approval.decide',
|
||||||
observedAtMs,
|
observedAtMs,
|
||||||
);
|
);
|
||||||
|
await consumeQuota(
|
||||||
|
approval.projectId,
|
||||||
|
authorization.principal,
|
||||||
|
'plugin-package.decide',
|
||||||
|
decisionId,
|
||||||
|
);
|
||||||
if (
|
if (
|
||||||
approval.decisionId === decisionId &&
|
approval.decisionId === decisionId &&
|
||||||
approval.decision === request.decision &&
|
approval.decision === request.decision &&
|
||||||
@@ -636,19 +683,31 @@ export function createClusterPluginPackageSecretBindingManagementService(
|
|||||||
plan?.bindingPlan.target.projectId ?? approval?.projectId;
|
plan?.bindingPlan.target.projectId ?? approval?.projectId;
|
||||||
if (!projectId) throw new PluginPackageManagementUnavailableError();
|
if (!projectId) throw new PluginPackageManagementUnavailableError();
|
||||||
const observedAtMs = currentTime(now);
|
const observedAtMs = currentTime(now);
|
||||||
|
let authorization;
|
||||||
try {
|
try {
|
||||||
await authorize(request.principal, projectId, 'secret.manage', observedAtMs);
|
authorization = await authorize(
|
||||||
|
request.principal,
|
||||||
|
projectId,
|
||||||
|
'secret.manage',
|
||||||
|
observedAtMs,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!(error instanceof PluginPackageManagementAuthorizationError)) {
|
if (!(error instanceof PluginPackageManagementAuthorizationError)) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
await authorize(
|
authorization = await authorize(
|
||||||
request.principal,
|
request.principal,
|
||||||
projectId,
|
projectId,
|
||||||
'approval.decide',
|
'approval.decide',
|
||||||
observedAtMs,
|
observedAtMs,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
await consumeQuota(
|
||||||
|
projectId,
|
||||||
|
authorization.principal,
|
||||||
|
'plugin-package.inspect',
|
||||||
|
request.inspectionId,
|
||||||
|
);
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
plan,
|
plan,
|
||||||
approvalRequest: approval,
|
approvalRequest: approval,
|
||||||
|
|||||||
@@ -199,6 +199,47 @@ function commands() {
|
|||||||
operation: 'plugin-package.publisher-trust-transition.inspect',
|
operation: 'plugin-package.publisher-trust-transition.inspect',
|
||||||
request: inspection,
|
request: inspection,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
schemaVersion: 1,
|
||||||
|
operation: 'plugin-package.secret-binding.plan',
|
||||||
|
request: {
|
||||||
|
actionRef: 'secret-binding:cluster-monitor:1',
|
||||||
|
projectId: 'project-1',
|
||||||
|
packageName: 'cluster-monitor',
|
||||||
|
assignments: [{
|
||||||
|
name: 'TOKEN',
|
||||||
|
secretRef:
|
||||||
|
'qlsecret:v1:eyJwcm9qZWN0SWQiOiJwcm9qZWN0LTEiLCJuYW1lIjoicnVudGltZS10b2tlbiIsInZlcnNpb24iOjJ9',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
schemaVersion: 1,
|
||||||
|
operation: 'plugin-package.secret-binding.propose',
|
||||||
|
request: {
|
||||||
|
actionRef: 'secret-binding:cluster-monitor:1',
|
||||||
|
approvalRequestId: 'approval-secret-binding-1',
|
||||||
|
approvalAuditEventId: 'audit-secret-binding-approval-1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
schemaVersion: 1,
|
||||||
|
operation: 'plugin-package.secret-binding.decide',
|
||||||
|
request: {
|
||||||
|
...decision,
|
||||||
|
actionRef: 'secret-binding:cluster-monitor:1',
|
||||||
|
approvalRequestId: 'approval-secret-binding-1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
schemaVersion: 1,
|
||||||
|
operation: 'plugin-package.secret-binding.inspect',
|
||||||
|
request: {
|
||||||
|
actionRef: 'secret-binding:cluster-monitor:1',
|
||||||
|
approvalRequestId: 'approval-secret-binding-1',
|
||||||
|
inspectionId: 'inspection-secret-binding-1',
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,7 +368,70 @@ function lifecyclePlanSummary() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function secretBindingPlanSummary() {
|
||||||
|
return {
|
||||||
|
actionRef: 'secret-binding:cluster-monitor:1',
|
||||||
|
projectId: 'project-1',
|
||||||
|
packageName: 'cluster-monitor',
|
||||||
|
installationId: 'install-cluster-monitor-1',
|
||||||
|
generation: 1,
|
||||||
|
generationDigest: '9'.repeat(64),
|
||||||
|
lockDigest: 'a'.repeat(64),
|
||||||
|
manifestDigest: 'b'.repeat(64),
|
||||||
|
entries: [{
|
||||||
|
name: 'TOKEN',
|
||||||
|
required: true,
|
||||||
|
secretRef:
|
||||||
|
'qlsecret:v1:eyJwcm9qZWN0SWQiOiJwcm9qZWN0LTEiLCJuYW1lIjoicnVudGltZS10b2tlbiIsInZlcnNpb24iOjJ9',
|
||||||
|
}],
|
||||||
|
plannedAtMs: 1_000,
|
||||||
|
expiresAtMs: 10_000,
|
||||||
|
planDigest: 'c'.repeat(64),
|
||||||
|
approvalPlanDigest: 'd'.repeat(64),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function successfulResult(operation) {
|
function successfulResult(operation) {
|
||||||
|
const secretApproval = {
|
||||||
|
...approvalSummary(),
|
||||||
|
id: 'approval-secret-binding-1',
|
||||||
|
actionDigest: 'd'.repeat(64),
|
||||||
|
previewDigest: 'c'.repeat(64),
|
||||||
|
};
|
||||||
|
if (operation === 'plugin-package.secret-binding.plan') {
|
||||||
|
return {
|
||||||
|
schemaVersion: 1,
|
||||||
|
operation,
|
||||||
|
status: 'created',
|
||||||
|
plan: secretBindingPlanSummary(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (operation === 'plugin-package.secret-binding.propose') {
|
||||||
|
return {
|
||||||
|
schemaVersion: 1,
|
||||||
|
operation,
|
||||||
|
approvalStatus: 'created',
|
||||||
|
plan: secretBindingPlanSummary(),
|
||||||
|
approval: secretApproval,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (operation === 'plugin-package.secret-binding.inspect') {
|
||||||
|
return {
|
||||||
|
schemaVersion: 1,
|
||||||
|
operation,
|
||||||
|
plan: secretBindingPlanSummary(),
|
||||||
|
approval: secretApproval,
|
||||||
|
stale: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (operation === 'plugin-package.secret-binding.decide') {
|
||||||
|
return {
|
||||||
|
schemaVersion: 1,
|
||||||
|
operation,
|
||||||
|
status: 'decided',
|
||||||
|
approval: secretApproval,
|
||||||
|
};
|
||||||
|
}
|
||||||
if (operation === 'plugin-package.installation.inspect') {
|
if (operation === 'plugin-package.installation.inspect') {
|
||||||
return {
|
return {
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
@@ -639,7 +743,7 @@ test('readiness probe rejects unreviewed status and bounded response drift', asy
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('permits and validates exactly the fourteen public management operations', async () => {
|
test('permits and validates exactly the eighteen public management operations', async () => {
|
||||||
const received = [];
|
const received = [];
|
||||||
const fixture = await startServer((request, response) => {
|
const fixture = await startServer((request, response) => {
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
@@ -684,7 +788,7 @@ test('permits and validates exactly the fourteen public management operations',
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
assert.equal(received.length, 14);
|
assert.equal(received.length, 18);
|
||||||
} finally {
|
} finally {
|
||||||
await fixture.close();
|
await fixture.close();
|
||||||
rmSync(files.directory, { recursive: true, force: true });
|
rmSync(files.directory, { recursive: true, force: true });
|
||||||
@@ -756,6 +860,137 @@ test('rejects installation inventory responses outside the requested project and
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('rejects Secret binding response drift before reporting success', async () => {
|
||||||
|
const command = commands().find(
|
||||||
|
({ operation }) => operation === 'plugin-package.secret-binding.propose',
|
||||||
|
);
|
||||||
|
const invalidResults = [
|
||||||
|
{
|
||||||
|
...successfulResult(command.operation),
|
||||||
|
plan: {
|
||||||
|
...secretBindingPlanSummary(),
|
||||||
|
actionRef: 'secret-binding:another-package:1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...successfulResult(command.operation),
|
||||||
|
plan: {
|
||||||
|
...secretBindingPlanSummary(),
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
...secretBindingPlanSummary().entries[0],
|
||||||
|
secretRef:
|
||||||
|
'qlsecret:v1:eyJwcm9qZWN0SWQiOiJhbm90aGVyLXByb2plY3QiLCJuYW1lIjoicnVudGltZS10b2tlbiIsInZlcnNpb24iOjJ9',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...successfulResult(command.operation),
|
||||||
|
plan: {
|
||||||
|
...secretBindingPlanSummary(),
|
||||||
|
entries: [
|
||||||
|
...secretBindingPlanSummary().entries,
|
||||||
|
...secretBindingPlanSummary().entries,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...successfulResult(command.operation),
|
||||||
|
approval: {
|
||||||
|
...successfulResult(command.operation).approval,
|
||||||
|
actionDigest: 'e'.repeat(64),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const fixture = await startServer((_request, response) => {
|
||||||
|
sendJson(response, 200, {
|
||||||
|
schemaVersion: 1,
|
||||||
|
requestId: `request-invalid-secret-binding-${invalidResults.length}`,
|
||||||
|
result: invalidResults.shift(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const files = createClientFiles(fixture.port, command);
|
||||||
|
try {
|
||||||
|
for (let index = 0; index < 4; index += 1) {
|
||||||
|
await assert.rejects(
|
||||||
|
executeClusterPluginPackageManagementClient(files.paths),
|
||||||
|
ClusterPluginPackageManagementClientRequestError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await fixture.close();
|
||||||
|
rmSync(files.directory, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('binds Secret binding plan and plan-less inspection to the exact request', async () => {
|
||||||
|
const planCommand = commands().find(
|
||||||
|
({ operation }) => operation === 'plugin-package.secret-binding.plan',
|
||||||
|
);
|
||||||
|
const inspectCommand = commands().find(
|
||||||
|
({ operation }) => operation === 'plugin-package.secret-binding.inspect',
|
||||||
|
);
|
||||||
|
const invalidResponses = [
|
||||||
|
{
|
||||||
|
command: planCommand,
|
||||||
|
result: {
|
||||||
|
...successfulResult(planCommand.operation),
|
||||||
|
plan: {
|
||||||
|
...secretBindingPlanSummary(),
|
||||||
|
entries: [{
|
||||||
|
...secretBindingPlanSummary().entries[0],
|
||||||
|
secretRef:
|
||||||
|
'qlsecret:v1:eyJwcm9qZWN0SWQiOiJwcm9qZWN0LTEiLCJuYW1lIjoiYW5vdGhlci10b2tlbiIsInZlcnNpb24iOjJ9',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: inspectCommand,
|
||||||
|
result: {
|
||||||
|
...successfulResult(inspectCommand.operation),
|
||||||
|
plan: null,
|
||||||
|
approval: {
|
||||||
|
...successfulResult(inspectCommand.operation).approval,
|
||||||
|
id: 'approval-secret-binding-another',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: inspectCommand,
|
||||||
|
result: {
|
||||||
|
...successfulResult(inspectCommand.operation),
|
||||||
|
plan: null,
|
||||||
|
approval: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const fixture = await startServer((_request, response) => {
|
||||||
|
sendJson(response, 200, {
|
||||||
|
schemaVersion: 1,
|
||||||
|
requestId: `request-exact-secret-binding-${invalidResponses.length}`,
|
||||||
|
result: invalidResponses[0].result,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
for (const invalid of invalidResponses) {
|
||||||
|
invalidResponses[0] = invalid;
|
||||||
|
const files = createClientFiles(fixture.port, invalid.command);
|
||||||
|
try {
|
||||||
|
await assert.rejects(
|
||||||
|
executeClusterPluginPackageManagementClient(files.paths),
|
||||||
|
ClusterPluginPackageManagementClientRequestError,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
rmSync(files.directory, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await fixture.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('rejects non-private, symlinked, and non-exact input files before transport', async () => {
|
test('rejects non-private, symlinked, and non-exact input files before transport', async () => {
|
||||||
const files = createClientFiles(443);
|
const files = createClientFiles(443);
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -322,6 +322,94 @@ function authentication(value = principal()) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function secretBindingPlan() {
|
||||||
|
return {
|
||||||
|
schema: 'qinglong/plugin-package-secret-binding-approval-plan@v1',
|
||||||
|
actionRef: 'secret-binding:cluster-monitor:1',
|
||||||
|
bindingPlan: {
|
||||||
|
schema: 'qinglong/plugin-package-secret-binding-plan@v1',
|
||||||
|
target: {
|
||||||
|
installationId: 'cluster-monitor-installation',
|
||||||
|
projectId: 'default',
|
||||||
|
packageName: 'cluster-monitor',
|
||||||
|
lockDigest: '1'.repeat(64),
|
||||||
|
generation: 1,
|
||||||
|
generationDigest: '2'.repeat(64),
|
||||||
|
manifestDigest: '3'.repeat(64),
|
||||||
|
},
|
||||||
|
entries: [{
|
||||||
|
name: 'TOKEN',
|
||||||
|
required: true,
|
||||||
|
secretRef:
|
||||||
|
'qlsecret:v1:eyJwcm9qZWN0SWQiOiJkZWZhdWx0IiwibmFtZSI6InJ1bnRpbWUtdG9rZW4iLCJ2ZXJzaW9uIjoyfQ',
|
||||||
|
}],
|
||||||
|
plannedAtMs: NOW - 10,
|
||||||
|
planDigest: '4'.repeat(64),
|
||||||
|
},
|
||||||
|
requestedBy: { type: 'user', id: 'cluster-reviewer' },
|
||||||
|
expiresAtMs: NOW + 10_000,
|
||||||
|
approvalPlanDigest: '5'.repeat(64),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function fakeSecretBinding() {
|
||||||
|
const calls = { plan: [], propose: [], decide: [], inspectAuthorized: [] };
|
||||||
|
return {
|
||||||
|
calls,
|
||||||
|
service: {
|
||||||
|
async plan(request) {
|
||||||
|
calls.plan.push(request);
|
||||||
|
return { status: 'created', plan: secretBindingPlan() };
|
||||||
|
},
|
||||||
|
async propose(request) {
|
||||||
|
calls.propose.push(request);
|
||||||
|
return {
|
||||||
|
plan: secretBindingPlan(),
|
||||||
|
approvalStatus: 'created',
|
||||||
|
approvalRequest: approval({
|
||||||
|
id: request.approvalRequestId,
|
||||||
|
action: {
|
||||||
|
permission: 'secret.manage',
|
||||||
|
actionType: 'plugin_package.secret_binding.bind',
|
||||||
|
actionRef: request.actionRef,
|
||||||
|
actionDigest: '5'.repeat(64),
|
||||||
|
previewDigest: '4'.repeat(64),
|
||||||
|
},
|
||||||
|
requestedBy: request.principal.subject,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async decide(request) {
|
||||||
|
calls.decide.push(request);
|
||||||
|
return {
|
||||||
|
status: 'decided',
|
||||||
|
request: approval({
|
||||||
|
id: request.approvalRequestId,
|
||||||
|
version: 2,
|
||||||
|
state: request.decision,
|
||||||
|
decisionId: request.decisionId,
|
||||||
|
decision: request.decision,
|
||||||
|
decisionReasonCode: request.reasonCode,
|
||||||
|
decidedBy: request.principal.subject,
|
||||||
|
decisionAuthenticationId: request.principal.authenticationId,
|
||||||
|
decisionAssurance: request.principal.assurance,
|
||||||
|
decidedAtMs: NOW,
|
||||||
|
decisionFence: { projectVersion: 1, bindingVersion: 1 },
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async inspectAuthorized(request) {
|
||||||
|
calls.inspectAuthorized.push(request);
|
||||||
|
return {
|
||||||
|
plan: secretBindingPlan(),
|
||||||
|
approvalRequest: null,
|
||||||
|
stale: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function lifecyclePlan() {
|
function lifecyclePlan() {
|
||||||
return {
|
return {
|
||||||
schema: 'qinglong/plugin-package-lifecycle-plan@v1',
|
schema: 'qinglong/plugin-package-lifecycle-plan@v1',
|
||||||
@@ -818,6 +906,82 @@ test('routes lifecycle review without exposing executor mutation authority', asy
|
|||||||
assert.deepEqual(management.calls.dispatch, []);
|
assert.deepEqual(management.calls.dispatch, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('routes content-free Secret binding review without executor authority', async () => {
|
||||||
|
const management = fakeService();
|
||||||
|
const secretBinding = fakeSecretBinding();
|
||||||
|
const transport = createClusterPluginPackageManagementTransport({
|
||||||
|
service: management.service,
|
||||||
|
secretBinding: secretBinding.service,
|
||||||
|
now: () => NOW,
|
||||||
|
});
|
||||||
|
const planCommand = {
|
||||||
|
schemaVersion: 1,
|
||||||
|
operation: 'plugin-package.secret-binding.plan',
|
||||||
|
request: {
|
||||||
|
actionRef: secretBindingPlan().actionRef,
|
||||||
|
projectId: 'default',
|
||||||
|
packageName: 'cluster-monitor',
|
||||||
|
assignments: secretBindingPlan().bindingPlan.entries.map(
|
||||||
|
({ name, secretRef }) => ({ name, secretRef }),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const planned = await transport.execute(
|
||||||
|
planCommand,
|
||||||
|
authentication().authority,
|
||||||
|
);
|
||||||
|
assert.equal(planned.status, 'created');
|
||||||
|
assert.deepEqual(Object.keys(planned.plan).sort(), [
|
||||||
|
'actionRef', 'approvalPlanDigest', 'entries', 'expiresAtMs', 'generation',
|
||||||
|
'generationDigest', 'installationId', 'lockDigest', 'manifestDigest',
|
||||||
|
'packageName', 'planDigest', 'plannedAtMs', 'projectId',
|
||||||
|
]);
|
||||||
|
assert.equal(JSON.stringify(planned).includes('authenticationId'), false);
|
||||||
|
|
||||||
|
const proposed = await transport.execute({
|
||||||
|
schemaVersion: 1,
|
||||||
|
operation: 'plugin-package.secret-binding.propose',
|
||||||
|
request: {
|
||||||
|
actionRef: secretBindingPlan().actionRef,
|
||||||
|
approvalRequestId: 'approval-secret-binding-1',
|
||||||
|
approvalAuditEventId: 'audit-secret-binding-approval-1',
|
||||||
|
},
|
||||||
|
}, authentication().authority);
|
||||||
|
assert.equal(proposed.approvalStatus, 'created');
|
||||||
|
|
||||||
|
const decided = await transport.execute({
|
||||||
|
schemaVersion: 1,
|
||||||
|
operation: 'plugin-package.secret-binding.decide',
|
||||||
|
request: {
|
||||||
|
actionRef: secretBindingPlan().actionRef,
|
||||||
|
approvalRequestId: 'approval-secret-binding-1',
|
||||||
|
expectedVersion: 1,
|
||||||
|
decisionId: 'decision-secret-binding-1',
|
||||||
|
auditEventId: 'audit-secret-binding-decision-1',
|
||||||
|
decision: 'approved',
|
||||||
|
reasonCode: 'reviewed',
|
||||||
|
},
|
||||||
|
}, authentication().authority);
|
||||||
|
assert.equal(decided.status, 'decided');
|
||||||
|
|
||||||
|
const inspected = await transport.execute({
|
||||||
|
schemaVersion: 1,
|
||||||
|
operation: 'plugin-package.secret-binding.inspect',
|
||||||
|
request: {
|
||||||
|
actionRef: secretBindingPlan().actionRef,
|
||||||
|
approvalRequestId: 'approval-secret-binding-1',
|
||||||
|
inspectionId: 'inspection-secret-binding-1',
|
||||||
|
},
|
||||||
|
}, authentication().authority);
|
||||||
|
assert.equal(inspected.stale, false);
|
||||||
|
assert.equal(secretBinding.calls.plan.length, 1);
|
||||||
|
assert.equal(secretBinding.calls.propose.length, 1);
|
||||||
|
assert.equal(secretBinding.calls.decide.length, 1);
|
||||||
|
assert.equal(secretBinding.calls.inspectAuthorized.length, 1);
|
||||||
|
assert.deepEqual(management.calls.consume, []);
|
||||||
|
assert.deepEqual(management.calls.dispatch, []);
|
||||||
|
});
|
||||||
|
|
||||||
test('routes publisher revocation proposal with derived-only low-sensitive output', async () => {
|
test('routes publisher revocation proposal with derived-only low-sensitive output', async () => {
|
||||||
const management = fakeService();
|
const management = fakeService();
|
||||||
const publisherTrust = fakePublisherTrust();
|
const publisherTrust = fakePublisherTrust();
|
||||||
|
|||||||
@@ -379,3 +379,71 @@ test('rejects weak requester, self-decision and semantic actionRef replay drift'
|
|||||||
(error) => error?.name === 'ApprovalSeparationOfDutyError',
|
(error) => error?.name === 'ApprovalSeparationOfDutyError',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('consumes durable quota with stable operation identities', async () => {
|
||||||
|
const state = fixture();
|
||||||
|
const consumed = [];
|
||||||
|
const service = createClusterPluginPackageSecretBindingManagementService({
|
||||||
|
pool: state.pool,
|
||||||
|
now: () => 210,
|
||||||
|
planLifetimeMs: 1_000,
|
||||||
|
quota: {
|
||||||
|
async consume(command) {
|
||||||
|
consumed.push(command);
|
||||||
|
return { remaining: 9, resetAtMs: 2_000, observedAtMs: 210 };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const created = await service.plan(planRequest());
|
||||||
|
const proposed = await service.propose({
|
||||||
|
actionRef: created.plan.actionRef,
|
||||||
|
approvalRequestId: 'approval-secret-binding-quota',
|
||||||
|
approvalAuditEventId: '123e4567-e89b-42d3-a456-426614175211',
|
||||||
|
principal: REQUESTER,
|
||||||
|
});
|
||||||
|
await service.decide({
|
||||||
|
actionRef: created.plan.actionRef,
|
||||||
|
approvalRequestId: proposed.approvalRequest.id,
|
||||||
|
expectedVersion: 1,
|
||||||
|
decisionId: 'decision-secret-binding-quota',
|
||||||
|
auditEventId: '123e4567-e89b-42d3-a456-426614175212',
|
||||||
|
decision: 'approved',
|
||||||
|
reasonCode: 'reviewed',
|
||||||
|
principal: REVIEWER,
|
||||||
|
});
|
||||||
|
await service.inspectAuthorized({
|
||||||
|
actionRef: created.plan.actionRef,
|
||||||
|
approvalRequestId: proposed.approvalRequest.id,
|
||||||
|
inspectionId: 'inspection-secret-binding-quota',
|
||||||
|
principal: REVIEWER,
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
consumed.map(({ operation, idempotencyKey, subject }) => ({
|
||||||
|
operation,
|
||||||
|
idempotencyKey,
|
||||||
|
subject,
|
||||||
|
})),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
operation: 'plugin-package.propose',
|
||||||
|
idempotencyKey: created.plan.actionRef,
|
||||||
|
subject: REQUESTER.subject,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operation: 'plugin-package.propose',
|
||||||
|
idempotencyKey: proposed.approvalRequest.id,
|
||||||
|
subject: REQUESTER.subject,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operation: 'plugin-package.decide',
|
||||||
|
idempotencyKey: 'decision-secret-binding-quota',
|
||||||
|
subject: REVIEWER.subject,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operation: 'plugin-package.inspect',
|
||||||
|
idempotencyKey: 'inspection-secret-binding-quota',
|
||||||
|
subject: REVIEWER.subject,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
+104
-35
@@ -44,6 +44,9 @@ const {
|
|||||||
const {
|
const {
|
||||||
createClusterPluginPackageManagementService,
|
createClusterPluginPackageManagementService,
|
||||||
} = require('@qinglong/cluster-admin/plugin-package-management');
|
} = require('@qinglong/cluster-admin/plugin-package-management');
|
||||||
|
const {
|
||||||
|
createClusterPluginPackageManagementTransport,
|
||||||
|
} = require('@qinglong/cluster-admin/plugin-package-management-transport');
|
||||||
const {
|
const {
|
||||||
createClusterPluginPackageApprovedActionDispatcher,
|
createClusterPluginPackageApprovedActionDispatcher,
|
||||||
} = require('@qinglong/cluster-admin/plugin-package-approved-action');
|
} = require('@qinglong/cluster-admin/plugin-package-approved-action');
|
||||||
@@ -363,42 +366,105 @@ if (!MIGRATION_URL || !MANAGER_URL || !EXECUTOR_URL) {
|
|||||||
planLifetimeMs: 60_000,
|
planLifetimeMs: 60_000,
|
||||||
approvalLifetimeMs: 60_000,
|
approvalLifetimeMs: 60_000,
|
||||||
});
|
});
|
||||||
const planned = await secretManagement.plan({
|
const secretTransport = createClusterPluginPackageManagementTransport({
|
||||||
actionRef: secretActionRef,
|
service: installManagement,
|
||||||
projectId,
|
secretBinding: secretManagement,
|
||||||
packageName,
|
now: () => now,
|
||||||
assignments: [{ name: 'TOKEN', secretRef }],
|
|
||||||
principal: principal(requesterSubject, `secret-owner-${suffix}`, now),
|
|
||||||
});
|
|
||||||
assert.equal(planned.status, 'created');
|
|
||||||
now = Math.max(now, planned.plan.bindingPlan.plannedAtMs);
|
|
||||||
const replay = await secretManagement.plan({
|
|
||||||
actionRef: secretActionRef,
|
|
||||||
projectId,
|
|
||||||
packageName,
|
|
||||||
assignments: [{ name: 'TOKEN', secretRef }],
|
|
||||||
principal: principal(requesterSubject, `secret-owner-${suffix}`, now),
|
|
||||||
});
|
});
|
||||||
|
const requesterAuthentication = {
|
||||||
|
async authenticate() {
|
||||||
|
return principal(requesterSubject, `secret-owner-${suffix}`, now);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const plannedPublic = await secretTransport.execute(
|
||||||
|
{
|
||||||
|
schemaVersion: 1,
|
||||||
|
operation: 'plugin-package.secret-binding.plan',
|
||||||
|
request: {
|
||||||
|
actionRef: secretActionRef,
|
||||||
|
projectId,
|
||||||
|
packageName,
|
||||||
|
assignments: [{ name: 'TOKEN', secretRef }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
requesterAuthentication,
|
||||||
|
);
|
||||||
|
assert.equal(plannedPublic.status, 'created');
|
||||||
|
assert.equal(plannedPublic.plan.actionRef, secretActionRef);
|
||||||
|
assert.deepEqual(plannedPublic.plan.entries, [
|
||||||
|
{ name: 'TOKEN', required: false, secretRef },
|
||||||
|
]);
|
||||||
|
assert.equal(Object.hasOwn(plannedPublic.plan, 'authenticationId'), false);
|
||||||
|
now = Math.max(now, plannedPublic.plan.plannedAtMs);
|
||||||
|
const replay = await secretTransport.execute(
|
||||||
|
{
|
||||||
|
schemaVersion: 1,
|
||||||
|
operation: 'plugin-package.secret-binding.plan',
|
||||||
|
request: {
|
||||||
|
actionRef: secretActionRef,
|
||||||
|
projectId,
|
||||||
|
packageName,
|
||||||
|
assignments: [{ name: 'TOKEN', secretRef }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
requesterAuthentication,
|
||||||
|
);
|
||||||
assert.equal(replay.status, 'existing');
|
assert.equal(replay.status, 'existing');
|
||||||
now += 10;
|
now += 10;
|
||||||
const secretProposed = await secretManagement.propose({
|
const secretProposed = await secretTransport.execute(
|
||||||
actionRef: secretActionRef,
|
{
|
||||||
approvalRequestId: secretApprovalId,
|
schemaVersion: 1,
|
||||||
approvalAuditEventId: randomUUID(),
|
operation: 'plugin-package.secret-binding.propose',
|
||||||
principal: principal(requesterSubject, `secret-owner-${suffix}`, now),
|
request: {
|
||||||
});
|
actionRef: secretActionRef,
|
||||||
|
approvalRequestId: secretApprovalId,
|
||||||
|
approvalAuditEventId: randomUUID(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
requesterAuthentication,
|
||||||
|
);
|
||||||
now += 10;
|
now += 10;
|
||||||
const secretDecision = await secretManagement.decide({
|
const secretDecision = await secretTransport.execute(
|
||||||
actionRef: secretActionRef,
|
{
|
||||||
approvalRequestId: secretApprovalId,
|
schemaVersion: 1,
|
||||||
expectedVersion: secretProposed.approvalRequest.version,
|
operation: 'plugin-package.secret-binding.decide',
|
||||||
decisionId: `secret-decision-${suffix}`,
|
request: {
|
||||||
auditEventId: randomUUID(),
|
actionRef: secretActionRef,
|
||||||
decision: 'approved',
|
approvalRequestId: secretApprovalId,
|
||||||
reasonCode: 'reviewed',
|
expectedVersion: secretProposed.approval.version,
|
||||||
principal: principal(reviewerSubject, `secret-reviewer-${suffix}`, now),
|
decisionId: `secret-decision-${suffix}`,
|
||||||
});
|
auditEventId: randomUUID(),
|
||||||
|
decision: 'approved',
|
||||||
|
reasonCode: 'reviewed',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
async authenticate() {
|
||||||
|
return principal(
|
||||||
|
reviewerSubject,
|
||||||
|
`secret-reviewer-${suffix}`,
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
assert.equal(secretDecision.status, 'decided');
|
assert.equal(secretDecision.status, 'decided');
|
||||||
|
const inspected = await secretTransport.execute(
|
||||||
|
{
|
||||||
|
schemaVersion: 1,
|
||||||
|
operation: 'plugin-package.secret-binding.inspect',
|
||||||
|
request: {
|
||||||
|
actionRef: secretActionRef,
|
||||||
|
approvalRequestId: secretApprovalId,
|
||||||
|
inspectionId: `secret-inspection-${suffix}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
requesterAuthentication,
|
||||||
|
);
|
||||||
|
assert.deepEqual(inspected.plan, plannedPublic.plan);
|
||||||
|
assert.equal(inspected.approval?.state, 'approved');
|
||||||
|
assert.equal(inspected.approval?.decision, 'approved');
|
||||||
|
assert.equal(inspected.stale, false);
|
||||||
now += 10;
|
now += 10;
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
await consumeClusterPluginPackageSecretBindingApprovals({
|
await consumeClusterPluginPackageSecretBindingApprovals({
|
||||||
@@ -432,7 +498,7 @@ if (!MIGRATION_URL || !MANAGER_URL || !EXECUTOR_URL) {
|
|||||||
).inspect(pendingSecretExecution.dispatch),
|
).inspect(pendingSecretExecution.dispatch),
|
||||||
{
|
{
|
||||||
status: 'ready',
|
status: 'ready',
|
||||||
actionDigest: planned.plan.approvalPlanDigest,
|
actionDigest: plannedPublic.plan.approvalPlanDigest,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
id = 0;
|
id = 0;
|
||||||
@@ -448,12 +514,15 @@ if (!MIGRATION_URL || !MANAGER_URL || !EXECUTOR_URL) {
|
|||||||
assert.equal(secretDispatch.succeeded, 1);
|
assert.equal(secretDispatch.succeeded, 1);
|
||||||
const bindings = new PostgresPluginPackageSecretBindingRepository(executor.pool);
|
const bindings = new PostgresPluginPackageSecretBindingRepository(executor.pool);
|
||||||
const binding = await bindings.find(
|
const binding = await bindings.find(
|
||||||
planned.plan.bindingPlan.target.generationDigest,
|
plannedPublic.plan.generationDigest,
|
||||||
);
|
);
|
||||||
assert.ok(binding);
|
assert.ok(binding);
|
||||||
assert.equal(binding.authority.kind, 'approved-action-execution');
|
assert.equal(binding.authority.kind, 'approved-action-execution');
|
||||||
assert.equal(binding.authority.evidenceDigest, planned.plan.approvalPlanDigest);
|
assert.equal(
|
||||||
assert.deepEqual(binding.entries, planned.plan.bindingPlan.entries);
|
binding.authority.evidenceDigest,
|
||||||
|
plannedPublic.plan.approvalPlanDigest,
|
||||||
|
);
|
||||||
|
assert.deepEqual(binding.entries, plannedPublic.plan.entries);
|
||||||
assert.doesNotMatch(JSON.stringify(binding), /secret-value/);
|
assert.doesNotMatch(JSON.stringify(binding), /secret-value/);
|
||||||
assert.equal((await secretDispatcher.dispatchBatch({ limit: 4 })).scanned, 0);
|
assert.equal((await secretDispatcher.dispatchBatch({ limit: 4 })).scanned, 0);
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
|
|||||||
@@ -1263,6 +1263,8 @@ function auditSourceImports(root, packagePath, findings) {
|
|||||||
'@qinglong/runtime-core/plugin-package-lifecycle-plan',
|
'@qinglong/runtime-core/plugin-package-lifecycle-plan',
|
||||||
'@qinglong/runtime-core/plugin-package-management',
|
'@qinglong/runtime-core/plugin-package-management',
|
||||||
'@qinglong/runtime-core/plugin-package-proposal',
|
'@qinglong/runtime-core/plugin-package-proposal',
|
||||||
|
'@qinglong/runtime-core/plugin-package-secret-binding',
|
||||||
|
'@qinglong/runtime-core/plugin-package-secret-binding-approval-plan',
|
||||||
'@qinglong/runtime-core/security',
|
'@qinglong/runtime-core/security',
|
||||||
]);
|
]);
|
||||||
if (
|
if (
|
||||||
|
|||||||
Reference in New Issue
Block a user