mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 18:08:20 +08:00
feat(ql3): expose cluster secret binding management
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
+9
@@ -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);
|
||||
|
||||
+212
-1
@@ -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(
|
||||
|
||||
+63
-4
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user