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
@@ -3,6 +3,7 @@ import { Agent as HttpsAgent, request as httpsRequest } from 'node:https';
import { Duplex } from 'node:stream';
import { connect as tlsConnect } from 'node:tls';
import { TextDecoder } from 'node:util';
import { parseSecretRef } from '@qinglong/runtime-core/secret-reference';
import {
ClusterPluginPackageManagementClientConfigurationError,
isReviewedClusterAuthenticatedManagementClientProtocol,
@@ -290,6 +291,21 @@ const LIFECYCLE_PLAN_KEYS = Object.freeze([
'blockingReferences',
'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 {
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(
value: unknown,
command: Readonly<ClusterPluginPackageManagementCommand>,
): 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') {
const result = exactResponseObject(value, [
'schemaVersion',
@@ -617,6 +834,12 @@ function validateResult(
}
if (result.approval !== null) {
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') {
throw new ClusterPluginPackageManagementClientRequestError();
}
@@ -28,6 +28,7 @@ import {
} from '../../management-support/pluginPackageIdentityKeyset';
import { createClusterPluginPackageManagementService } from './pluginPackageManagement';
import { createClusterPluginPackageLifecycleManagementService } from '../lifecycle/pluginPackageLifecycleManagement';
import { createClusterPluginPackageSecretBindingManagementService } from '../secret-binding/pluginPackageSecretBindingManagement';
import {
loadClusterPluginPackagePublisherTrustFileEvidence,
type ClusterPluginPackagePublisherTrustFileEvidence,
@@ -635,10 +636,18 @@ export async function startClusterPluginPackageManagementProcess(
now,
quota,
});
const secretBinding =
createClusterPluginPackageSecretBindingManagementService({
pool: database.pool,
approvalLifetimeMs: config.approvalLifetimeMs,
now,
quota,
});
const transport = createClusterPluginPackageManagementTransport({
service,
lifecycle,
publisherTrust,
secretBinding,
now,
});
const privateKey = readTlsFile(config.privateKeyFile, true);
@@ -14,6 +14,8 @@ import type {
} from '@qinglong/runtime-core/plugin-package-management';
import type { PluginPackageInstallProposal } from '@qinglong/runtime-core/plugin-package-proposal';
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 {
normalizeSecurityPrincipal,
type SecurityPrincipal,
@@ -25,6 +27,7 @@ import type {
InspectClusterPluginPackagePublisherRevocationResult,
InspectClusterPluginPackagePublisherTrustTransitionResult,
} from '../publisher/pluginPackagePublisherTrustManagement';
import type { ClusterPluginPackageSecretBindingManagementService } from '../secret-binding/pluginPackageSecretBindingManagement';
const STRONG_CLUSTER_ASSURANCES = new Set(['multi_factor', 'hardware']);
@@ -166,6 +169,39 @@ export interface InspectClusterPluginPackagePublisherTrustTransitionCommand {
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 =
| ProposeClusterPluginPackageCommand
| DecideClusterPluginPackageCommand
@@ -180,7 +216,11 @@ export type ClusterPluginPackageManagementCommand =
| InspectClusterPluginPackagePublisherRevocationCommand
| ProposeClusterPluginPackagePublisherTrustTransitionCommand
| DecideClusterPluginPackagePublisherTrustTransitionCommand
| InspectClusterPluginPackagePublisherTrustTransitionCommand;
| InspectClusterPluginPackagePublisherTrustTransitionCommand
| PlanClusterPluginPackageSecretBindingCommand
| ProposeClusterPluginPackageSecretBindingCommand
| DecideClusterPluginPackageSecretBindingCommand
| InspectClusterPluginPackageSecretBindingCommand;
export type ClusterPluginPackageManagementTransportResult =
| Readonly<{
@@ -276,6 +316,32 @@ export type ClusterPluginPackageManagementTransportResult =
typeof publisherTrustTransitionProposalSummary
> | 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 {
@@ -289,6 +355,7 @@ export interface ClusterPluginPackageManagementTransportOptions {
readonly service: ClusterPluginPackageManagementService;
readonly lifecycle?: ClusterPluginPackageLifecycleManagementService;
readonly publisherTrust?: ClusterPluginPackagePublisherTrustManagementService;
readonly secretBinding?: ClusterPluginPackageSecretBindingManagementService;
readonly now?: () => number;
}
@@ -528,6 +595,50 @@ export function normalizeClusterPluginPackageManagementCommand(
'publisher trust transition inspection request',
);
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:
throw new ClusterPluginPackageManagementTransportRequestError(
'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(
proposal: NonNullable<
InspectClusterPluginPackagePublisherRevocationResult['proposal']
@@ -694,6 +825,7 @@ function exactDecisionReplay(
| DecideClusterPluginPackageLifecycleCommand
| DecideClusterPluginPackagePublisherRevocationCommand
| DecideClusterPluginPackagePublisherTrustTransitionCommand
| DecideClusterPluginPackageSecretBindingCommand
>,
principal: Readonly<SecurityPrincipal>,
): Readonly<DecideApprovalRequestResult> | null {
@@ -726,6 +858,7 @@ export function createClusterPluginPackageManagementTransport(
key !== 'service' &&
key !== 'lifecycle' &&
key !== 'publisherTrust' &&
key !== 'secretBinding' &&
key !== 'now',
) ||
!options.service ||
@@ -745,6 +878,12 @@ export function createClusterPluginPackageManagementTransport(
typeof options.publisherTrust.propose !== 'function' ||
typeof options.publisherTrust.inspect !== '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')
) {
throw new ClusterPluginPackageManagementTransportConfigurationError(
@@ -941,6 +1080,78 @@ export function createClusterPluginPackageManagementTransport(
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': {
if (!options.publisherTrust) {
throw new ClusterPluginPackageManagementTransportConfigurationError(
@@ -15,8 +15,11 @@ import {
import {
PluginPackageManagementAuthorizationError,
PluginPackageManagementConflictError,
PluginPackageManagementQuotaExceededError,
PluginPackageManagementRequestError,
PluginPackageManagementUnavailableError,
type PluginPackageManagementQuotaOperation,
type PluginPackageManagementQuotaPort,
} from '@qinglong/runtime-core/plugin-package-management';
import { createPluginPackageResourceGenerationFromReferences } from '@qinglong/runtime-core/plugin-package-resource-generation';
import type { PluginPackageSecretBindingAssignment } from '@qinglong/runtime-core/plugin-package-secret-binding';
@@ -111,6 +114,7 @@ export interface ClusterPluginPackageSecretBindingManagementOptions {
readonly now?: () => number;
readonly planLifetimeMs?: number;
readonly approvalLifetimeMs?: number;
readonly quota?: PluginPackageManagementQuotaPort;
}
function exact(value: unknown, keys: readonly string[], label: string): void {
@@ -226,12 +230,15 @@ export function createClusterPluginPackageSecretBindingManagementService(
key !== 'pool' &&
key !== 'now' &&
key !== 'planLifetimeMs' &&
key !== 'approvalLifetimeMs',
key !== 'approvalLifetimeMs' &&
key !== 'quota',
) ||
!options.pool ||
typeof options.pool.query !== '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(
'cluster Plugin Package Secret binding management options are invalid',
@@ -265,6 +272,28 @@ export function createClusterPluginPackageSecretBindingManagementService(
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 (
principalValue: SecurityPrincipal,
projectId: string,
@@ -346,6 +375,12 @@ export function createClusterPluginPackageSecretBindingManagementService(
currentTime(now),
);
const requestedActionRef = actionRef(request.actionRef);
await consumeQuota(
projectId,
authorization.principal,
'plugin-package.propose',
requestedActionRef,
);
let existingValue;
try {
existingValue = await plans.findByActionRef(requestedActionRef);
@@ -458,6 +493,12 @@ export function createClusterPluginPackageSecretBindingManagementService(
'secret.manage',
observedAtMs,
);
await consumeQuota(
plan.bindingPlan.target.projectId,
authorization.principal,
'plugin-package.propose',
approvalRequestId,
);
if (!sameSubject(plan.requestedBy, authorization.principal.subject)) {
throw new PluginPackageManagementAuthorizationError();
}
@@ -572,6 +613,12 @@ export function createClusterPluginPackageSecretBindingManagementService(
'approval.decide',
observedAtMs,
);
await consumeQuota(
approval.projectId,
authorization.principal,
'plugin-package.decide',
decisionId,
);
if (
approval.decisionId === decisionId &&
approval.decision === request.decision &&
@@ -636,19 +683,31 @@ export function createClusterPluginPackageSecretBindingManagementService(
plan?.bindingPlan.target.projectId ?? approval?.projectId;
if (!projectId) throw new PluginPackageManagementUnavailableError();
const observedAtMs = currentTime(now);
let authorization;
try {
await authorize(request.principal, projectId, 'secret.manage', observedAtMs);
authorization = await authorize(
request.principal,
projectId,
'secret.manage',
observedAtMs,
);
} catch (error) {
if (!(error instanceof PluginPackageManagementAuthorizationError)) {
throw error;
}
await authorize(
authorization = await authorize(
request.principal,
projectId,
'approval.decide',
observedAtMs,
);
}
await consumeQuota(
projectId,
authorization.principal,
'plugin-package.inspect',
request.inspectionId,
);
return Object.freeze({
plan,
approvalRequest: approval,
@@ -199,6 +199,47 @@ function commands() {
operation: 'plugin-package.publisher-trust-transition.inspect',
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) {
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') {
return {
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 fixture = await startServer((request, response) => {
const chunks = [];
@@ -684,7 +788,7 @@ test('permits and validates exactly the fourteen public management operations',
return true;
},
);
assert.equal(received.length, 14);
assert.equal(received.length, 18);
} finally {
await fixture.close();
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 () => {
const files = createClientFiles(443);
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() {
return {
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, []);
});
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 () => {
const management = fakeService();
const publisherTrust = fakePublisherTrust();
@@ -379,3 +379,71 @@ test('rejects weak requester, self-decision and semantic actionRef replay drift'
(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 {
createClusterPluginPackageManagementService,
} = require('@qinglong/cluster-admin/plugin-package-management');
const {
createClusterPluginPackageManagementTransport,
} = require('@qinglong/cluster-admin/plugin-package-management-transport');
const {
createClusterPluginPackageApprovedActionDispatcher,
} = require('@qinglong/cluster-admin/plugin-package-approved-action');
@@ -363,42 +366,105 @@ if (!MIGRATION_URL || !MANAGER_URL || !EXECUTOR_URL) {
planLifetimeMs: 60_000,
approvalLifetimeMs: 60_000,
});
const planned = await secretManagement.plan({
actionRef: secretActionRef,
projectId,
packageName,
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 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,
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');
now += 10;
const secretProposed = await secretManagement.propose({
actionRef: secretActionRef,
approvalRequestId: secretApprovalId,
approvalAuditEventId: randomUUID(),
principal: principal(requesterSubject, `secret-owner-${suffix}`, now),
});
const secretProposed = await secretTransport.execute(
{
schemaVersion: 1,
operation: 'plugin-package.secret-binding.propose',
request: {
actionRef: secretActionRef,
approvalRequestId: secretApprovalId,
approvalAuditEventId: randomUUID(),
},
},
requesterAuthentication,
);
now += 10;
const secretDecision = await secretManagement.decide({
actionRef: secretActionRef,
approvalRequestId: secretApprovalId,
expectedVersion: secretProposed.approvalRequest.version,
decisionId: `secret-decision-${suffix}`,
auditEventId: randomUUID(),
decision: 'approved',
reasonCode: 'reviewed',
principal: principal(reviewerSubject, `secret-reviewer-${suffix}`, now),
});
const secretDecision = await secretTransport.execute(
{
schemaVersion: 1,
operation: 'plugin-package.secret-binding.decide',
request: {
actionRef: secretActionRef,
approvalRequestId: secretApprovalId,
expectedVersion: secretProposed.approval.version,
decisionId: `secret-decision-${suffix}`,
auditEventId: randomUUID(),
decision: 'approved',
reasonCode: 'reviewed',
},
},
{
async authenticate() {
return principal(
reviewerSubject,
`secret-reviewer-${suffix}`,
now,
);
},
},
);
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;
assert.deepEqual(
await consumeClusterPluginPackageSecretBindingApprovals({
@@ -432,7 +498,7 @@ if (!MIGRATION_URL || !MANAGER_URL || !EXECUTOR_URL) {
).inspect(pendingSecretExecution.dispatch),
{
status: 'ready',
actionDigest: planned.plan.approvalPlanDigest,
actionDigest: plannedPublic.plan.approvalPlanDigest,
},
);
id = 0;
@@ -448,12 +514,15 @@ if (!MIGRATION_URL || !MANAGER_URL || !EXECUTOR_URL) {
assert.equal(secretDispatch.succeeded, 1);
const bindings = new PostgresPluginPackageSecretBindingRepository(executor.pool);
const binding = await bindings.find(
planned.plan.bindingPlan.target.generationDigest,
plannedPublic.plan.generationDigest,
);
assert.ok(binding);
assert.equal(binding.authority.kind, 'approved-action-execution');
assert.equal(binding.authority.evidenceDigest, planned.plan.approvalPlanDigest);
assert.deepEqual(binding.entries, planned.plan.bindingPlan.entries);
assert.equal(
binding.authority.evidenceDigest,
plannedPublic.plan.approvalPlanDigest,
);
assert.deepEqual(binding.entries, plannedPublic.plan.entries);
assert.doesNotMatch(JSON.stringify(binding), /secret-value/);
assert.equal((await secretDispatcher.dispatchBatch({ limit: 4 })).scanned, 0);
await assert.rejects(