feat(ql3): expose cluster secret binding management

This commit is contained in:
whyour
2026-08-13 15:41:59 +08:00
parent 7016903fba
commit 73eeaed4de
13 changed files with 1127 additions and 48 deletions
+6 -1
View File
@@ -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
+1 -1
View File
@@ -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/228PostgreSQL package 311 pass/1 条外部 URL 条件 skip;完整 18-package build/test 退出 0backend 1,188 pass/2 skippackage/dependency boundary 零 findingPostgreSQL 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/228PostgreSQL package 311 pass/1 条外部 URL 条件 skip;完整 18-package build/test 退出 0backend 1,188 pass/2 skippackage/dependency boundary 零 findingPostgreSQL 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 v46PostgreSQL 追加 `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 退出 0backend 1,188 pass/2 skip,五项边界审计零 findingworkspace 仍无 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 v46PostgreSQL 追加 `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 退出 0backend 1,188 pass/2 skip,五项边界审计零 findingworkspace 仍无 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 引用逻辑 requirementmaterialization 只用当前 generation 的 D-305 binding 编译为已有、固定 version 的 Task `SecretRef`Package source 直接携带 SecretRef、缺失 binding、未批准 `secret.use`、跨 binding 引用和 optional/required 漂移全部失败关闭。binding 快照不含明文并进入 materialized revision digestLocal/Cluster 启动发布复用既有 repository/poolTask 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 退出 0backend 1,188 pass/2 条件 skip/0 fail;五项 package/dependency/edge/service-manager/local-image 审计零 findingworkspace 仍无 single-source/shallow-source package,两个有序 migration ledger 精确为 PostgreSQL 61、SQLite 95 个 sourcePostgreSQL 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 引用逻辑 requirementmaterialization 只用当前 generation 的 D-305 binding 编译为已有、固定 version 的 Task `SecretRef`Package source 直接携带 SecretRef、缺失 binding、未批准 `secret.use`、跨 binding 引用和 optional/required 漂移全部失败关闭。binding 快照不含明文并进入 materialized revision digestLocal/Cluster 启动发布复用既有 repository/poolTask 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 退出 0backend 1,188 pass/2 条件 skip/0 fail;五项 package/dependency/edge/service-manager/local-image 审计零 findingworkspace 仍无 single-source/shallow-source package,两个有序 migration ledger 精确为 PostgreSQL 61、SQLite 95 个 sourcePostgreSQL 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 Ownerhuman confirmation 后在一个 `BEGIN IMMEDIATE` transaction 内复验 Owner fence、Package provenance、精确 Secret version、content-free audit 与 immutable bindingplan 后撤权原子回滚,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 publishexecutor 只对可选 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、watchercache18 个 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 Ownerhuman confirmation 后在一个 `BEGIN IMMEDIATE` transaction 内复验 Owner fence、Package provenance、精确 Secret version、content-free audit 与 immutable bindingplan 后撤权原子回滚,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 publishexecutor 只对可选 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 退出 0cluster-admin 315 pass/3 条件 skipbackend 1,188 pass/2 条件 skip/0 failpackage/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、watchercache 或 workload18 个 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 条件 skipPostgreSQL 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 条件 skipPostgreSQL 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 审计零 findingworkspace 仍为 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 退出 0backend 1,188 pass/2 条件 skip/0 fail。package boundary、cluster dependency、cluster deployment、CloudNativePG、edge import、service-manager bridge、local image 七项审计零 findingworkspace 仍为 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();
} }
@@ -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);
@@ -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(
@@ -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,
},
],
);
});
@@ -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({
service: installManagement,
secretBinding: secretManagement,
now: () => 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, actionRef: secretActionRef,
projectId, projectId,
packageName, packageName,
assignments: [{ name: 'TOKEN', secretRef }], assignments: [{ name: 'TOKEN', secretRef }],
principal: principal(requesterSubject, `secret-owner-${suffix}`, now), },
}); },
assert.equal(planned.status, 'created'); requesterAuthentication,
now = Math.max(now, planned.plan.bindingPlan.plannedAtMs); );
const replay = await secretManagement.plan({ 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, actionRef: secretActionRef,
projectId, projectId,
packageName, packageName,
assignments: [{ name: 'TOKEN', secretRef }], assignments: [{ name: 'TOKEN', secretRef }],
principal: principal(requesterSubject, `secret-owner-${suffix}`, now), },
}); },
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(
{
schemaVersion: 1,
operation: 'plugin-package.secret-binding.propose',
request: {
actionRef: secretActionRef, actionRef: secretActionRef,
approvalRequestId: secretApprovalId, approvalRequestId: secretApprovalId,
approvalAuditEventId: randomUUID(), approvalAuditEventId: randomUUID(),
principal: principal(requesterSubject, `secret-owner-${suffix}`, now), },
}); },
requesterAuthentication,
);
now += 10; now += 10;
const secretDecision = await secretManagement.decide({ const secretDecision = await secretTransport.execute(
{
schemaVersion: 1,
operation: 'plugin-package.secret-binding.decide',
request: {
actionRef: secretActionRef, actionRef: secretActionRef,
approvalRequestId: secretApprovalId, approvalRequestId: secretApprovalId,
expectedVersion: secretProposed.approvalRequest.version, expectedVersion: secretProposed.approval.version,
decisionId: `secret-decision-${suffix}`, decisionId: `secret-decision-${suffix}`,
auditEventId: randomUUID(), auditEventId: randomUUID(),
decision: 'approved', decision: 'approved',
reasonCode: 'reviewed', reasonCode: 'reviewed',
principal: principal(reviewerSubject, `secret-reviewer-${suffix}`, now), },
}); },
{
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(
+2
View File
@@ -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 (