@yiaany/ghostapi 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/CONTRIBUTING.md +132 -119
- package/LICENSE +21 -21
- package/PROVENANCE.md +13 -0
- package/README.md +78 -80
- package/SECURITY.md +56 -53
- package/dist/actions/gateway.js +320 -72
- package/dist/actions/gateway.js.map +1 -1
- package/dist/actions/index.d.ts +2 -2
- package/dist/actions/index.js +1 -1
- package/dist/actions/index.js.map +1 -1
- package/dist/agents/agentPrompt.js +7 -5
- package/dist/agents/agentPrompt.js.map +1 -1
- package/dist/ai/aiClient.js +5 -5
- package/dist/ai/aiClient.js.map +1 -1
- package/dist/ai/aiGenerator.js +66 -26
- package/dist/ai/aiGenerator.js.map +1 -1
- package/dist/ai/genericInference.js +151 -36
- package/dist/ai/genericInference.js.map +1 -1
- package/dist/ai/prompts.js +55 -49
- package/dist/ai/prompts.js.map +1 -1
- package/dist/approvals/approvalInbox.js +649 -133
- package/dist/approvals/approvalInbox.js.map +1 -1
- package/dist/approvals/index.d.ts +2 -2
- package/dist/approvals/index.js +1 -1
- package/dist/approvals/index.js.map +1 -1
- package/dist/behavior/behaviorStore.js +34 -14
- package/dist/behavior/behaviorStore.js.map +1 -1
- package/dist/cache/index.js +14 -5
- package/dist/cache/index.js.map +1 -1
- package/dist/cli/index.js +226 -93
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/openUrl.js +17 -3
- package/dist/cli/openUrl.js.map +1 -1
- package/dist/cli/parser.js +79 -18
- package/dist/cli/parser.js.map +1 -1
- package/dist/config/dataPaths.js +4 -2
- package/dist/config/dataPaths.js.map +1 -1
- package/dist/config/localConfig.js +5 -2
- package/dist/config/localConfig.js.map +1 -1
- package/dist/config/serverConfig.js +11 -5
- package/dist/config/serverConfig.js.map +1 -1
- package/dist/contracts/contract.js +311 -83
- package/dist/contracts/contract.js.map +1 -1
- package/dist/contracts/index.d.ts +2 -2
- package/dist/contracts/index.js +1 -1
- package/dist/contracts/index.js.map +1 -1
- package/dist/credentials/credentialBroker.js +391 -79
- package/dist/credentials/credentialBroker.js.map +1 -1
- package/dist/credentials/index.d.ts +2 -2
- package/dist/credentials/index.js +1 -1
- package/dist/credentials/index.js.map +1 -1
- package/dist/dashboard/app.js +823 -685
- package/dist/dashboard/dashboard.js.map +1 -1
- package/dist/dashboard/index.html +704 -267
- package/dist/dashboard/styles.css +1040 -349
- package/dist/egress/capabilities.js +55 -25
- package/dist/egress/capabilities.js.map +1 -1
- package/dist/egress/linuxBootstrap.js +47 -11
- package/dist/egress/linuxBootstrap.js.map +1 -1
- package/dist/egress/run.js +170 -31
- package/dist/egress/run.js.map +1 -1
- package/dist/errors/errorEngine.js +15 -3
- package/dist/errors/errorEngine.js.map +1 -1
- package/dist/errors/providerErrors.js.map +1 -1
- package/dist/evals/evals.js +304 -78
- package/dist/evals/evals.js.map +1 -1
- package/dist/evals/index.d.ts +2 -2
- package/dist/evals/index.js +1 -1
- package/dist/evals/index.js.map +1 -1
- package/dist/evidence/index.d.ts +2 -2
- package/dist/evidence/index.js +1 -1
- package/dist/evidence/index.js.map +1 -1
- package/dist/evidence/report.d.ts +1 -1
- package/dist/evidence/report.js +263 -79
- package/dist/evidence/report.js.map +1 -1
- package/dist/fault/faultLab.js +24 -9
- package/dist/fault/faultLab.js.map +1 -1
- package/dist/index.d.ts +42 -42
- package/dist/index.js +21 -21
- package/dist/index.js.map +1 -1
- package/dist/inventory/analysis.d.ts +1 -1
- package/dist/inventory/analysis.js +174 -50
- package/dist/inventory/analysis.js.map +1 -1
- package/dist/inventory/index.d.ts +4 -4
- package/dist/inventory/index.js +3 -3
- package/dist/inventory/index.js.map +1 -1
- package/dist/inventory/inventoryController.js +362 -115
- package/dist/inventory/inventoryController.js.map +1 -1
- package/dist/inventory/types.js +680 -140
- package/dist/inventory/types.js.map +1 -1
- package/dist/ledger/actionLedger.js +355 -78
- package/dist/ledger/actionLedger.js.map +1 -1
- package/dist/ledger/index.d.ts +2 -2
- package/dist/ledger/index.js +1 -1
- package/dist/ledger/index.js.map +1 -1
- package/dist/mcp/server.js +26 -11
- package/dist/mcp/server.js.map +1 -1
- package/dist/policy/index.d.ts +2 -2
- package/dist/policy/index.js +1 -1
- package/dist/policy/index.js.map +1 -1
- package/dist/policy/policy.js +112 -32
- package/dist/policy/policy.js.map +1 -1
- package/dist/productTelemetry/index.js +71 -20
- package/dist/productTelemetry/index.js.map +1 -1
- package/dist/providers/conformance.js +10 -5
- package/dist/providers/conformance.js.map +1 -1
- package/dist/providers/discord.js +2 -2
- package/dist/providers/generic.js +3 -3
- package/dist/providers/github.js +2 -2
- package/dist/providers/index.d.ts +3 -3
- package/dist/providers/index.js +2 -2
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai.js +3 -3
- package/dist/providers/packs/resendPack.js +51 -25
- package/dist/providers/packs/resendPack.js.map +1 -1
- package/dist/providers/packs/stripePack.js +590 -121
- package/dist/providers/packs/stripePack.js.map +1 -1
- package/dist/providers/registry.js +4 -4
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/runtime.js +7 -6
- package/dist/providers/runtime.js.map +1 -1
- package/dist/providers/stripeWebhook.js +39 -10
- package/dist/providers/stripeWebhook.js.map +1 -1
- package/dist/providers/twilio.js +2 -2
- package/dist/proxy/cacheKey.js +9 -2
- package/dist/proxy/cacheKey.js.map +1 -1
- package/dist/proxy/providerDetector.js +22 -9
- package/dist/proxy/providerDetector.js.map +1 -1
- package/dist/proxy/proxyHandler.js +57 -22
- package/dist/proxy/proxyHandler.js.map +1 -1
- package/dist/proxy/requestNormalizer.js +4 -2
- package/dist/proxy/requestNormalizer.js.map +1 -1
- package/dist/reliability/costGovernance.js +211 -49
- package/dist/reliability/costGovernance.js.map +1 -1
- package/dist/reliability/index.d.ts +8 -8
- package/dist/reliability/index.js +4 -4
- package/dist/reliability/index.js.map +1 -1
- package/dist/reliability/reconciliation.js +274 -65
- package/dist/reliability/reconciliation.js.map +1 -1
- package/dist/reliability/runtimeHealth.js +127 -35
- package/dist/reliability/runtimeHealth.js.map +1 -1
- package/dist/reliability/slo.js +257 -50
- package/dist/reliability/slo.js.map +1 -1
- package/dist/report/safetyReport.d.ts +6 -0
- package/dist/report/safetyReport.js +56 -12
- package/dist/report/safetyReport.js.map +1 -1
- package/dist/rules/aiRules.js +5 -5
- package/dist/rules/aiRules.js.map +1 -1
- package/dist/safety/index.d.ts +2 -2
- package/dist/safety/index.js +1 -1
- package/dist/safety/index.js.map +1 -1
- package/dist/safety/safetyController.js +675 -165
- package/dist/safety/safetyController.js.map +1 -1
- package/dist/scenarios/scenarioBundle.js +204 -71
- package/dist/scenarios/scenarioBundle.js.map +1 -1
- package/dist/scenarios/scenarioStore.js +88 -29
- package/dist/scenarios/scenarioStore.js.map +1 -1
- package/dist/security/headerSanitizer.js +52 -10
- package/dist/security/headerSanitizer.js.map +1 -1
- package/dist/security/secrets.js +10 -5
- package/dist/security/secrets.js.map +1 -1
- package/dist/server/accessControl.js +37 -10
- package/dist/server/accessControl.js.map +1 -1
- package/dist/server/createServer.js +1 -1
- package/dist/server/createServer.js.map +1 -1
- package/dist/server/eventsStore.js +15 -5
- package/dist/server/eventsStore.js.map +1 -1
- package/dist/server/routes.js +67 -19
- package/dist/server/routes.js.map +1 -1
- package/dist/server/sse.js +1 -1
- package/dist/server/sse.js.map +1 -1
- package/dist/setup/sdkDetector.js +7 -2
- package/dist/setup/sdkDetector.js.map +1 -1
- package/dist/setup/setupGenerator.js +136 -48
- package/dist/setup/setupGenerator.js.map +1 -1
- package/dist/state/stateExtractor.js +3 -1
- package/dist/state/stateExtractor.js.map +1 -1
- package/dist/state/stateResolver.js +10 -7
- package/dist/state/stateResolver.js.map +1 -1
- package/dist/state/stateStore.js +13 -4
- package/dist/state/stateStore.js.map +1 -1
- package/dist/storage/fileStore.js +12 -4
- package/dist/storage/fileStore.js.map +1 -1
- package/dist/teamControl/controlPlane.js +846 -174
- package/dist/teamControl/controlPlane.js.map +1 -1
- package/dist/teamControl/deployment.js +19 -5
- package/dist/teamControl/deployment.js.map +1 -1
- package/dist/teamControl/index.d.ts +4 -4
- package/dist/teamControl/index.js +2 -2
- package/dist/teamControl/index.js.map +1 -1
- package/dist/tests/testGenerator.js +15 -7
- package/dist/tests/testGenerator.js.map +1 -1
- package/dist/trust/index.d.ts +2 -2
- package/dist/trust/index.js +1 -1
- package/dist/trust/index.js.map +1 -1
- package/dist/trust/trustLadder.js +309 -63
- package/dist/trust/trustLadder.js.map +1 -1
- package/dist/worlds/index.d.ts +2 -2
- package/dist/worlds/index.js +1 -1
- package/dist/worlds/index.js.map +1 -1
- package/dist/worlds/worlds.js +364 -65
- package/dist/worlds/worlds.js.map +1 -1
- package/docs/assets/dashboard.png +0 -0
- package/docs/assets/ghostapi-avatar.png +0 -0
- package/docs/ci.md +49 -49
- package/docs/github-actions.md +84 -84
- package/docs/mcp.md +58 -52
- package/docs/policy.md +63 -61
- package/docs/providers/authoring-packs.md +180 -175
- package/docs/providers/stripe-core-pack.md +84 -82
- package/docs/releases/README.md +13 -0
- package/docs/security/action-gateway-threat-model.md +34 -34
- package/docs/security/action-ledger-incident-replay-threat-model.md +42 -42
- package/docs/security/approval-inbox-threat-model.md +35 -35
- package/docs/security/credential-broker-threat-model.md +52 -52
- package/docs/security/egress-threat-model.md +126 -126
- package/docs/security/inventory-threat-model.md +104 -104
- package/docs/security/kill-switch-budgets-threat-model.md +37 -37
- package/docs/security/reliability-threat-model.md +111 -111
- package/docs/security/trust-ladder-threat-model.md +33 -33
- package/docs/telemetry.md +14 -0
- package/docs/usage.md +680 -571
- package/examples/README.md +10 -10
- package/examples/agent-instructions/README.md +39 -39
- package/examples/ci-smoke/README.md +21 -21
- package/examples/ci-smoke/ghostapi.policy.yaml +22 -22
- package/examples/ci-smoke/package.json +9 -9
- package/examples/ci-smoke/production-egress.mjs +4 -2
- package/examples/ci-smoke/safe.mjs +21 -17
- package/examples/evals/README.md +24 -24
- package/examples/evals/retry-after.eval.json +71 -52
- package/examples/generic-rest/README.md +27 -27
- package/examples/github-fetch/README.md +38 -35
- package/examples/openai-streaming/README.md +16 -16
- package/examples/openai-streaming/streaming-tool-call.mjs +44 -31
- package/examples/policy/ghostapi.policy.yaml +27 -27
- package/examples/record-replay/README.md +19 -19
- package/examples/record-replay/replay-requests.json +22 -22
- package/examples/record-replay/stripe-checkout.har.json +30 -30
- package/examples/resend-node/README.md +22 -22
- package/examples/stripe-node/README.md +35 -35
- package/examples/stripe-node/checkout-flow.mjs +87 -61
- package/examples/twilio-node/README.md +21 -21
- package/examples/worlds/README.md +19 -19
- package/examples/worlds/subscription-recovery.mjs +21 -12
- package/package.json +28 -6
- package/dist/landing/assets/index-BXR41fAD.css +0 -1
- package/dist/landing/assets/index-CxWsopzP.css +0 -1
- package/dist/landing/assets/index-D8_ggMRE.js +0 -55
- package/dist/landing/assets/index-DZlpyhqd.js +0 -55
- package/dist/landing/assets/index-Dk-Xezs3.js +0 -55
- package/dist/landing/index.html +0 -15
- package/dist/landing/landing.d.ts +0 -3
- package/dist/landing/landing.js +0 -12
- package/dist/landing/landing.js.map +0 -1
- package/docs/commercial/README.md +0 -29
- package/docs/commercial/data-inventory.md +0 -36
- package/docs/commercial/manual-invoicing.md +0 -44
- package/docs/commercial/metrics.md +0 -51
- package/docs/commercial/pricing.md +0 -103
- package/docs/commercial/privacy-policy-draft.md +0 -41
- package/docs/commercial/terms-topics-for-counsel.md +0 -51
- package/docs/design-partners/README.md +0 -52
- package/docs/design-partners/demo-narratives.md +0 -33
- package/docs/design-partners/design-partner-offer.md +0 -39
- package/docs/design-partners/discovery-questions.md +0 -28
- package/docs/design-partners/feedback-capture-template.md +0 -48
- package/docs/design-partners/icp-one-pager.md +0 -43
- package/docs/design-partners/onboarding-checklist.md +0 -33
- package/docs/design-partners/pilot-success-criteria.md +0 -27
- package/docs/design-partners/pricing-interview-script.md +0 -18
- package/docs/design-partners/telemetry-plan.md +0 -48
- package/docs/development/baseline.md +0 -114
- package/docs/development/onboarding-smoke.md +0 -37
- package/docs/development/verification-0.1.8.md +0 -25
- package/docs/enterprise-product-roadmap-ru.md +0 -1879
- package/docs/fundraising/README.md +0 -87
- package/docs/fundraising/data-room-checklist.md +0 -72
- package/docs/fundraising/demo-script.md +0 -52
- package/docs/fundraising/design-partners-50.md +0 -74
- package/docs/fundraising/launch-posts.md +0 -37
- package/docs/fundraising/metrics-and-evidence.md +0 -30
- package/docs/fundraising/roadmap-12-month.md +0 -25
- package/docs/fundraising/technical-due-diligence-index.md +0 -37
- package/docs/fundraising/yc-application.md +0 -25
- package/docs/hosted-pilot.md +0 -111
- package/docs/operations/disaster-recovery-runbook.md +0 -81
- package/docs/operations/kill-switch-runbook.md +0 -32
- package/docs/release-checklist.md +0 -83
- package/docs/release-migration-and-rollback.md +0 -63
- package/docs/release-readiness.md +0 -39
- package/docs/team-control-plane.md +0 -72
|
@@ -3,7 +3,7 @@ import { lstat, readFile } from "node:fs/promises";
|
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
4
|
import { getDataPaths } from "../config/dataPaths.js";
|
|
5
5
|
import { sanitizeSecretString } from "../security/secrets.js";
|
|
6
|
-
import { atomicWriteJson, ensurePrivateDirectory, withFileLock } from "../storage/fileStore.js";
|
|
6
|
+
import { atomicWriteJson, ensurePrivateDirectory, withFileLock, } from "../storage/fileStore.js";
|
|
7
7
|
const SCHEMA_VERSION = 1;
|
|
8
8
|
const MAX_STORE_BYTES = 1024 * 1024;
|
|
9
9
|
const MAX_CREDENTIALS = 1_000;
|
|
@@ -39,12 +39,19 @@ export class CredentialBroker {
|
|
|
39
39
|
this.executor = options.executor;
|
|
40
40
|
this.workloadVerifier = options.workloadVerifier;
|
|
41
41
|
this.actionReceiptVerifier = options.actionReceiptVerifier;
|
|
42
|
-
this.breakGlassAuthorizer =
|
|
42
|
+
this.breakGlassAuthorizer =
|
|
43
|
+
options.breakGlassAuthorizer ?? createDisabledBreakGlassAuthorizer();
|
|
43
44
|
}
|
|
44
45
|
async registerCredential(input) {
|
|
45
|
-
const candidate = validateCredentialMetadata({
|
|
46
|
+
const candidate = validateCredentialMetadata({
|
|
47
|
+
...input,
|
|
48
|
+
version: 1,
|
|
49
|
+
createdAt: this.timestamp(),
|
|
50
|
+
expiresAt: input.expiresAt,
|
|
51
|
+
});
|
|
46
52
|
return this.mutate((state) => {
|
|
47
|
-
if (state.credentials.some((credential) => credential.tenantId === candidate.tenantId &&
|
|
53
|
+
if (state.credentials.some((credential) => credential.tenantId === candidate.tenantId &&
|
|
54
|
+
credential.id === candidate.id))
|
|
48
55
|
throw new CredentialBrokerError("Credential already exists.");
|
|
49
56
|
state.credentials.push(candidate);
|
|
50
57
|
return clone(candidate);
|
|
@@ -64,7 +71,9 @@ export class CredentialBroker {
|
|
|
64
71
|
credential.version += 1;
|
|
65
72
|
credential.rotatedAt = now;
|
|
66
73
|
for (const grant of state.grants)
|
|
67
|
-
if (grant.tenantId === tenantId &&
|
|
74
|
+
if (grant.tenantId === tenantId &&
|
|
75
|
+
grant.credentialId === credentialId &&
|
|
76
|
+
grant.revokedAt === undefined)
|
|
68
77
|
grant.revokedAt = now;
|
|
69
78
|
return clone(credential);
|
|
70
79
|
});
|
|
@@ -77,7 +86,9 @@ export class CredentialBroker {
|
|
|
77
86
|
if (credential.revokedAt === undefined)
|
|
78
87
|
credential.revokedAt = now;
|
|
79
88
|
for (const grant of state.grants)
|
|
80
|
-
if (grant.tenantId === tenantId &&
|
|
89
|
+
if (grant.tenantId === tenantId &&
|
|
90
|
+
grant.credentialId === credentialId &&
|
|
91
|
+
grant.revokedAt === undefined)
|
|
81
92
|
grant.revokedAt = now;
|
|
82
93
|
});
|
|
83
94
|
}
|
|
@@ -89,13 +100,18 @@ export class CredentialBroker {
|
|
|
89
100
|
const credential = findCredential(state, workload.tenantId, request.credentialId);
|
|
90
101
|
assertCredentialUsable(credential, now);
|
|
91
102
|
assertRequestMatchesCredential(request, credential, workload);
|
|
92
|
-
const normalOwnerMatch = credential.ownerWorkloadId === workload.workloadId &&
|
|
93
|
-
|
|
94
|
-
|
|
103
|
+
const normalOwnerMatch = credential.ownerWorkloadId === workload.workloadId &&
|
|
104
|
+
credential.ownerWorkloadKind === workload.workloadKind;
|
|
105
|
+
const breakGlass = normalOwnerMatch
|
|
106
|
+
? undefined
|
|
107
|
+
: await this.breakGlassAuthorizer.authorize(input.breakGlassApproval, workload, request);
|
|
108
|
+
if (breakGlass !== undefined &&
|
|
109
|
+
Date.parse(expiresAt) - Date.parse(now) > MAX_BREAK_GLASS_TTL_MS)
|
|
95
110
|
throw new CredentialBrokerError("Break-glass grants must expire within five minutes.");
|
|
96
111
|
if (Date.parse(expiresAt) - Date.parse(now) > MAX_GRANT_TTL_MS)
|
|
97
112
|
throw new CredentialBrokerError("Credential grants must expire within fifteen minutes.");
|
|
98
|
-
if (Date.parse(expiresAt) > Date.parse(credential.expiresAt) ||
|
|
113
|
+
if (Date.parse(expiresAt) > Date.parse(credential.expiresAt) ||
|
|
114
|
+
Date.parse(expiresAt) > Date.parse(workload.expiresAt))
|
|
99
115
|
throw new CredentialBrokerError("Credential grant expiry exceeds its credential or workload lifetime.");
|
|
100
116
|
const grant = {
|
|
101
117
|
id: identifier(`grant-${randomUUID().replace(/-/g, "")}`, "Credential grant id"),
|
|
@@ -114,7 +130,7 @@ export class CredentialBroker {
|
|
|
114
130
|
action: clone(request.action),
|
|
115
131
|
issuedAt: now,
|
|
116
132
|
expiresAt,
|
|
117
|
-
...(breakGlass === undefined ? {} : { breakGlass })
|
|
133
|
+
...(breakGlass === undefined ? {} : { breakGlass }),
|
|
118
134
|
};
|
|
119
135
|
state.grants.push(grant);
|
|
120
136
|
return clone(grant);
|
|
@@ -133,7 +149,8 @@ export class CredentialBroker {
|
|
|
133
149
|
assertCredentialUsable(credential, now);
|
|
134
150
|
if (credential.version !== grant.credentialVersion)
|
|
135
151
|
throw new CredentialBrokerError("Credential grant was invalidated by rotation.");
|
|
136
|
-
if (credential.provider !== this.executor.provider ||
|
|
152
|
+
if (credential.provider !== this.executor.provider ||
|
|
153
|
+
!grant.scopes.every((scope) => this.executor.supportsScope(scope)))
|
|
137
154
|
throw new CredentialBrokerError("Credential executor denies the requested scope.");
|
|
138
155
|
const prior = state.receipts.find((receipt) => receipt.grantId === grant.id);
|
|
139
156
|
if (prior !== undefined) {
|
|
@@ -142,16 +159,32 @@ export class CredentialBroker {
|
|
|
142
159
|
throw new CredentialBrokerError("Prior credential execution has no retry-safe outcome and will not retry automatically.");
|
|
143
160
|
}
|
|
144
161
|
await this.actionReceiptVerifier.verify(request.action, workload);
|
|
145
|
-
const receipt = {
|
|
162
|
+
const receipt = {
|
|
163
|
+
id: identifier(`credential-use-${randomUUID().replace(/-/g, "")}`, "Credential use receipt id"),
|
|
164
|
+
grantId: grant.id,
|
|
165
|
+
credentialId: credential.id,
|
|
166
|
+
credentialVersion: credential.version,
|
|
167
|
+
tenantId: credential.tenantId,
|
|
168
|
+
workloadId: workload.workloadId,
|
|
169
|
+
action: clone(grant.action),
|
|
170
|
+
status: "executing",
|
|
171
|
+
executedAt: now,
|
|
172
|
+
};
|
|
146
173
|
state.receipts.push(receipt);
|
|
147
|
-
return {
|
|
174
|
+
return {
|
|
175
|
+
grant: clone(grant),
|
|
176
|
+
credential: clone(credential),
|
|
177
|
+
receipt: clone(receipt),
|
|
178
|
+
};
|
|
148
179
|
});
|
|
149
180
|
if ("status" in started)
|
|
150
181
|
return started;
|
|
151
182
|
let secret;
|
|
152
183
|
try {
|
|
153
184
|
secret = await this.vault.readSecret(started.credential.vaultRef);
|
|
154
|
-
if (!(secret instanceof Uint8Array) ||
|
|
185
|
+
if (!(secret instanceof Uint8Array) ||
|
|
186
|
+
secret.byteLength === 0 ||
|
|
187
|
+
secret.byteLength > 64 * 1024)
|
|
155
188
|
throw new CredentialBrokerError("Credential vault returned invalid secret material.");
|
|
156
189
|
await this.assertExecutionActive(started.grant, input.identity);
|
|
157
190
|
}
|
|
@@ -160,7 +193,12 @@ export class CredentialBroker {
|
|
|
160
193
|
throw new CredentialBrokerError("Credential execution did not start.");
|
|
161
194
|
}
|
|
162
195
|
try {
|
|
163
|
-
const result = await this.executor.execute({
|
|
196
|
+
const result = await this.executor.execute({
|
|
197
|
+
secret,
|
|
198
|
+
grant: clone(started.grant),
|
|
199
|
+
workload: clone(workload),
|
|
200
|
+
assertActive: async () => this.assertExecutionActive(started.grant, input.identity),
|
|
201
|
+
});
|
|
164
202
|
return await this.completeExecution(started.receipt.id, "executed", result.providerRequestId);
|
|
165
203
|
}
|
|
166
204
|
catch {
|
|
@@ -179,12 +217,21 @@ export class CredentialBroker {
|
|
|
179
217
|
const current = state.grants.find((candidate) => candidate.id === grant.id);
|
|
180
218
|
if (current === undefined)
|
|
181
219
|
throw new CredentialBrokerError("Credential grant was not found.");
|
|
182
|
-
assertGrantUsable(current, verifiedWorkload, {
|
|
220
|
+
assertGrantUsable(current, verifiedWorkload, {
|
|
221
|
+
credentialId: grant.credentialId,
|
|
222
|
+
provider: grant.provider,
|
|
223
|
+
scopes: grant.scopes,
|
|
224
|
+
audience: grant.audience,
|
|
225
|
+
action: grant.action,
|
|
226
|
+
}, this.timestamp());
|
|
183
227
|
const credential = findCredential(state, verifiedWorkload.tenantId, current.credentialId);
|
|
184
228
|
assertCredentialUsable(credential, this.timestamp());
|
|
185
|
-
if (credential.version !== current.credentialVersion ||
|
|
229
|
+
if (credential.version !== current.credentialVersion ||
|
|
230
|
+
credential.provider !== this.executor.provider ||
|
|
231
|
+
!current.scopes.every((scope) => this.executor.supportsScope(scope)))
|
|
186
232
|
throw new CredentialBrokerError("Credential grant is no longer executable.");
|
|
187
|
-
if (state.receipts.find((receipt) => receipt.grantId === current.id)
|
|
233
|
+
if (state.receipts.find((receipt) => receipt.grantId === current.id)
|
|
234
|
+
?.status !== "executing")
|
|
188
235
|
throw new CredentialBrokerError("Credential execution is no longer active.");
|
|
189
236
|
});
|
|
190
237
|
}
|
|
@@ -200,7 +247,8 @@ export class CredentialBroker {
|
|
|
200
247
|
findCredential(state, receipt.tenantId, receipt.credentialId).lastUsedAt = now;
|
|
201
248
|
}
|
|
202
249
|
else {
|
|
203
|
-
receipt.failureCode =
|
|
250
|
+
receipt.failureCode =
|
|
251
|
+
status === "failed" ? "execution_not_started" : "unknown_outcome";
|
|
204
252
|
}
|
|
205
253
|
return clone(receipt);
|
|
206
254
|
});
|
|
@@ -209,7 +257,15 @@ export class CredentialBroker {
|
|
|
209
257
|
const state = await this.read();
|
|
210
258
|
const candidates = state.credentials.filter((credential) => credential.revokedAt === undefined);
|
|
211
259
|
const results = await Promise.all(candidates.map(async (credential) => {
|
|
212
|
-
const active = this.workloadVerifier.isActive === undefined
|
|
260
|
+
const active = this.workloadVerifier.isActive === undefined
|
|
261
|
+
? false
|
|
262
|
+
: await this.workloadVerifier.isActive({
|
|
263
|
+
tenantId: credential.tenantId,
|
|
264
|
+
projectId: credential.projectId,
|
|
265
|
+
environment: credential.environment,
|
|
266
|
+
workloadId: credential.ownerWorkloadId,
|
|
267
|
+
workloadKind: credential.ownerWorkloadKind,
|
|
268
|
+
});
|
|
213
269
|
return active ? undefined : clone(credential);
|
|
214
270
|
}));
|
|
215
271
|
return results.filter((credential) => credential !== undefined);
|
|
@@ -260,7 +316,11 @@ export class CredentialBroker {
|
|
|
260
316
|
}
|
|
261
317
|
}
|
|
262
318
|
export function createDisabledBreakGlassAuthorizer() {
|
|
263
|
-
return {
|
|
319
|
+
return {
|
|
320
|
+
async authorize() {
|
|
321
|
+
throw new CredentialBrokerError("Break-glass authorization is not configured.");
|
|
322
|
+
},
|
|
323
|
+
};
|
|
264
324
|
}
|
|
265
325
|
export function createTestActionReceiptVerifier(reference) {
|
|
266
326
|
let calls = 0;
|
|
@@ -270,9 +330,11 @@ export function createTestActionReceiptVerifier(reference) {
|
|
|
270
330
|
calls += 1;
|
|
271
331
|
if (!sameAction(reference, candidate))
|
|
272
332
|
throw new CredentialBrokerError("Action receipt does not authorize this credential use.");
|
|
273
|
-
}
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
get calls() {
|
|
336
|
+
return calls;
|
|
274
337
|
},
|
|
275
|
-
get calls() { return calls; }
|
|
276
338
|
};
|
|
277
339
|
}
|
|
278
340
|
export function createTestCredentialVault() {
|
|
@@ -285,13 +347,15 @@ export function createTestCredentialVault() {
|
|
|
285
347
|
if (value === undefined)
|
|
286
348
|
throw new CredentialBrokerError("Credential vault reference was not found.");
|
|
287
349
|
return new Uint8Array(value);
|
|
288
|
-
}
|
|
350
|
+
},
|
|
289
351
|
},
|
|
290
352
|
put(vaultRef, material) {
|
|
291
|
-
if (!(material instanceof Uint8Array) ||
|
|
353
|
+
if (!(material instanceof Uint8Array) ||
|
|
354
|
+
material.byteLength === 0 ||
|
|
355
|
+
material.byteLength > 64 * 1024)
|
|
292
356
|
throw new CredentialBrokerError("Test credential material is invalid.");
|
|
293
357
|
values.set(reference(vaultRef, "Credential vault reference"), new Uint8Array(material));
|
|
294
|
-
}
|
|
358
|
+
},
|
|
295
359
|
};
|
|
296
360
|
}
|
|
297
361
|
export function createTestCredentialExecutor() {
|
|
@@ -301,14 +365,19 @@ export function createTestCredentialExecutor() {
|
|
|
301
365
|
provider: "test-provider",
|
|
302
366
|
supportsScope: (scope) => scope === "test.execute",
|
|
303
367
|
async execute(input) {
|
|
304
|
-
if (input.grant.audience !== "ghostapi-server" ||
|
|
368
|
+
if (input.grant.audience !== "ghostapi-server" ||
|
|
369
|
+
!input.grant.scopes.every((scope) => scope === "test.execute") ||
|
|
370
|
+
input.secret.byteLength === 0)
|
|
305
371
|
throw new CredentialBrokerError("Test provider rejected credential execution.");
|
|
306
372
|
await input.assertActive();
|
|
307
|
-
executions.push({
|
|
373
|
+
executions.push({
|
|
374
|
+
grantId: input.grant.id,
|
|
375
|
+
secretLength: input.secret.byteLength,
|
|
376
|
+
});
|
|
308
377
|
return { providerRequestId: `test-provider-${input.grant.id}` };
|
|
309
|
-
}
|
|
378
|
+
},
|
|
310
379
|
},
|
|
311
|
-
executions
|
|
380
|
+
executions,
|
|
312
381
|
};
|
|
313
382
|
}
|
|
314
383
|
export function createTestWorkloadIdentityProvider(options = {}) {
|
|
@@ -318,7 +387,9 @@ export function createTestWorkloadIdentityProvider(options = {}) {
|
|
|
318
387
|
return {
|
|
319
388
|
verifier: {
|
|
320
389
|
async authenticate(identity) {
|
|
321
|
-
if (identity === null ||
|
|
390
|
+
if (identity === null ||
|
|
391
|
+
typeof identity !== "object" ||
|
|
392
|
+
!issued.has(identity))
|
|
322
393
|
throw new CredentialBrokerError("Workload identity is not authenticated.");
|
|
323
394
|
const workload = validateWorkloadIdentity(identity, timestampFrom(now));
|
|
324
395
|
if (!active.has(workloadKey(workload)))
|
|
@@ -327,7 +398,7 @@ export function createTestWorkloadIdentityProvider(options = {}) {
|
|
|
327
398
|
},
|
|
328
399
|
async isActive(binding) {
|
|
329
400
|
return active.has(workloadKey(binding));
|
|
330
|
-
}
|
|
401
|
+
},
|
|
331
402
|
},
|
|
332
403
|
issue(input) {
|
|
333
404
|
const workload = validateWorkloadIdentity({ schemaVersion: 1, kind: "ghostapi.workload-identity", ...input }, timestampFrom(now));
|
|
@@ -341,7 +412,7 @@ export function createTestWorkloadIdentityProvider(options = {}) {
|
|
|
341
412
|
for (const key of [...active])
|
|
342
413
|
if (key.endsWith(`:${id}`))
|
|
343
414
|
active.delete(key);
|
|
344
|
-
}
|
|
415
|
+
},
|
|
345
416
|
};
|
|
346
417
|
}
|
|
347
418
|
export function createTestBreakGlassAuthorizer(options = {}) {
|
|
@@ -350,20 +421,28 @@ export function createTestBreakGlassAuthorizer(options = {}) {
|
|
|
350
421
|
return {
|
|
351
422
|
authorizer: {
|
|
352
423
|
async authorize(approval, workload, request) {
|
|
353
|
-
if (approval === null ||
|
|
424
|
+
if (approval === null ||
|
|
425
|
+
typeof approval !== "object" ||
|
|
426
|
+
!issued.has(approval))
|
|
354
427
|
throw new CredentialBrokerError("Break-glass approval is not trusted.");
|
|
355
428
|
const parsed = validateBreakGlassApproval(approval, timestampFrom(now));
|
|
356
|
-
if (parsed.approvedBy === workload.subjectId ||
|
|
429
|
+
if (parsed.approvedBy === workload.subjectId ||
|
|
430
|
+
parsed.approvedBy === workload.workloadId ||
|
|
431
|
+
!sameAction(parsed.action, request.action))
|
|
357
432
|
throw new CredentialBrokerError("Break-glass approval does not authorize this workload action.");
|
|
358
|
-
return {
|
|
359
|
-
|
|
433
|
+
return {
|
|
434
|
+
approvalId: parsed.approvalId,
|
|
435
|
+
approvedBy: parsed.approvedBy,
|
|
436
|
+
reason: parsed.reason,
|
|
437
|
+
};
|
|
438
|
+
},
|
|
360
439
|
},
|
|
361
440
|
issue(input) {
|
|
362
441
|
const approval = validateBreakGlassApproval({ schemaVersion: 1, kind: "ghostapi.break-glass-approval", ...input }, timestampFrom(now));
|
|
363
442
|
const frozen = Object.freeze(clone(approval));
|
|
364
443
|
issued.add(frozen);
|
|
365
444
|
return frozen;
|
|
366
|
-
}
|
|
445
|
+
},
|
|
367
446
|
};
|
|
368
447
|
}
|
|
369
448
|
function emptyState() {
|
|
@@ -379,8 +458,12 @@ function validateState(value) {
|
|
|
379
458
|
const grants = array(state.grants, "Credential grants", MAX_GRANTS).map(validateGrant);
|
|
380
459
|
unique(grants.map((grant) => grant.id), "Credential grant ids");
|
|
381
460
|
for (const grant of grants) {
|
|
382
|
-
const credential = credentials.find((candidate) => candidate.tenantId === grant.tenantId &&
|
|
383
|
-
|
|
461
|
+
const credential = credentials.find((candidate) => candidate.tenantId === grant.tenantId &&
|
|
462
|
+
candidate.id === grant.credentialId);
|
|
463
|
+
if (credential === undefined ||
|
|
464
|
+
grant.credentialVersion > credential.version ||
|
|
465
|
+
grant.provider !== credential.provider ||
|
|
466
|
+
!grant.scopes.every((scope) => credential.allowedScopes.includes(scope)))
|
|
384
467
|
throw new CredentialBrokerError("Credential grant references an invalid credential.");
|
|
385
468
|
}
|
|
386
469
|
const receipts = array(state.receipts, "Credential use receipts", MAX_RECEIPTS).map(validateReceipt);
|
|
@@ -388,87 +471,286 @@ function validateState(value) {
|
|
|
388
471
|
unique(receipts.map((receipt) => receipt.grantId), "Credential use receipt grants");
|
|
389
472
|
for (const receipt of receipts) {
|
|
390
473
|
const grant = grants.find((candidate) => candidate.id === receipt.grantId);
|
|
391
|
-
if (grant === undefined ||
|
|
474
|
+
if (grant === undefined ||
|
|
475
|
+
receipt.credentialId !== grant.credentialId ||
|
|
476
|
+
receipt.credentialVersion !== grant.credentialVersion ||
|
|
477
|
+
receipt.tenantId !== grant.tenantId ||
|
|
478
|
+
receipt.workloadId !== grant.workloadId ||
|
|
479
|
+
!sameAction(receipt.action, grant.action))
|
|
392
480
|
throw new CredentialBrokerError("Credential use receipt references an invalid grant.");
|
|
393
481
|
}
|
|
394
482
|
return { schemaVersion: 1, credentials, grants, receipts };
|
|
395
483
|
}
|
|
396
484
|
function validateCredentialMetadata(value) {
|
|
397
485
|
const metadata = object(value, "Credential metadata is invalid.");
|
|
398
|
-
exactKeys(metadata, [
|
|
486
|
+
exactKeys(metadata, [
|
|
487
|
+
"id",
|
|
488
|
+
"tenantId",
|
|
489
|
+
"projectId",
|
|
490
|
+
"environment",
|
|
491
|
+
"provider",
|
|
492
|
+
"ownerWorkloadId",
|
|
493
|
+
"ownerWorkloadKind",
|
|
494
|
+
"vaultRef",
|
|
495
|
+
"allowedScopes",
|
|
496
|
+
"version",
|
|
497
|
+
"createdAt",
|
|
498
|
+
"expiresAt",
|
|
499
|
+
"rotatedAt",
|
|
500
|
+
"revokedAt",
|
|
501
|
+
"lastUsedAt",
|
|
502
|
+
], "Credential metadata", ["rotatedAt", "revokedAt", "lastUsedAt"]);
|
|
399
503
|
const createdAt = timestamp(metadata.createdAt, "Credential creation time");
|
|
400
504
|
const expiresAt = timestamp(metadata.expiresAt, "Credential expiry");
|
|
401
505
|
if (Date.parse(expiresAt) <= Date.parse(createdAt))
|
|
402
506
|
throw new CredentialBrokerError("Credential expiry must follow creation.");
|
|
403
|
-
return {
|
|
507
|
+
return {
|
|
508
|
+
id: identifier(metadata.id, "Credential id"),
|
|
509
|
+
tenantId: identifier(metadata.tenantId, "Credential tenant id"),
|
|
510
|
+
projectId: identifier(metadata.projectId, "Credential project id"),
|
|
511
|
+
environment: identifier(metadata.environment, "Credential environment"),
|
|
512
|
+
provider: identifier(metadata.provider, "Credential provider"),
|
|
513
|
+
ownerWorkloadId: identifier(metadata.ownerWorkloadId, "Credential owner workload id"),
|
|
514
|
+
ownerWorkloadKind: workloadKind(metadata.ownerWorkloadKind),
|
|
515
|
+
vaultRef: reference(metadata.vaultRef, "Credential vault reference"),
|
|
516
|
+
allowedScopes: scopes(metadata.allowedScopes, "Credential allowed scopes"),
|
|
517
|
+
version: positiveInteger(metadata.version, "Credential version"),
|
|
518
|
+
createdAt,
|
|
519
|
+
expiresAt,
|
|
520
|
+
...(metadata.rotatedAt === undefined
|
|
521
|
+
? {}
|
|
522
|
+
: {
|
|
523
|
+
rotatedAt: timestamp(metadata.rotatedAt, "Credential rotation time"),
|
|
524
|
+
}),
|
|
525
|
+
...(metadata.revokedAt === undefined
|
|
526
|
+
? {}
|
|
527
|
+
: {
|
|
528
|
+
revokedAt: timestamp(metadata.revokedAt, "Credential revocation time"),
|
|
529
|
+
}),
|
|
530
|
+
...(metadata.lastUsedAt === undefined
|
|
531
|
+
? {}
|
|
532
|
+
: {
|
|
533
|
+
lastUsedAt: timestamp(metadata.lastUsedAt, "Credential last use time"),
|
|
534
|
+
}),
|
|
535
|
+
};
|
|
404
536
|
}
|
|
405
537
|
function validateGrant(value) {
|
|
406
538
|
const grant = object(value, "Credential grant is invalid.");
|
|
407
|
-
exactKeys(grant, [
|
|
539
|
+
exactKeys(grant, [
|
|
540
|
+
"id",
|
|
541
|
+
"credentialId",
|
|
542
|
+
"credentialVersion",
|
|
543
|
+
"tenantId",
|
|
544
|
+
"projectId",
|
|
545
|
+
"environment",
|
|
546
|
+
"workloadId",
|
|
547
|
+
"subjectId",
|
|
548
|
+
"workloadKind",
|
|
549
|
+
"runId",
|
|
550
|
+
"provider",
|
|
551
|
+
"scopes",
|
|
552
|
+
"audience",
|
|
553
|
+
"action",
|
|
554
|
+
"issuedAt",
|
|
555
|
+
"expiresAt",
|
|
556
|
+
"breakGlass",
|
|
557
|
+
"revokedAt",
|
|
558
|
+
], "Credential grant", ["breakGlass", "revokedAt"]);
|
|
408
559
|
const issuedAt = timestamp(grant.issuedAt, "Credential grant issue time");
|
|
409
560
|
const expiresAt = timestamp(grant.expiresAt, "Credential grant expiry");
|
|
410
|
-
if (Date.parse(expiresAt) <= Date.parse(issuedAt) ||
|
|
561
|
+
if (Date.parse(expiresAt) <= Date.parse(issuedAt) ||
|
|
562
|
+
Date.parse(expiresAt) - Date.parse(issuedAt) > MAX_GRANT_TTL_MS)
|
|
411
563
|
throw new CredentialBrokerError("Credential grant lifetime is invalid.");
|
|
412
|
-
const breakGlass = grant.breakGlass === undefined
|
|
413
|
-
|
|
564
|
+
const breakGlass = grant.breakGlass === undefined
|
|
565
|
+
? undefined
|
|
566
|
+
: breakGlassRecord(grant.breakGlass);
|
|
567
|
+
if (breakGlass !== undefined &&
|
|
568
|
+
Date.parse(expiresAt) - Date.parse(issuedAt) > MAX_BREAK_GLASS_TTL_MS)
|
|
414
569
|
throw new CredentialBrokerError("Break-glass grant lifetime is invalid.");
|
|
415
570
|
if (grant.audience !== "ghostapi-server")
|
|
416
571
|
throw new CredentialBrokerError("Credential grant audience is invalid.");
|
|
417
|
-
return {
|
|
572
|
+
return {
|
|
573
|
+
id: identifier(grant.id, "Credential grant id"),
|
|
574
|
+
credentialId: identifier(grant.credentialId, "Credential id"),
|
|
575
|
+
credentialVersion: positiveInteger(grant.credentialVersion, "Credential version"),
|
|
576
|
+
tenantId: identifier(grant.tenantId, "Credential tenant id"),
|
|
577
|
+
projectId: identifier(grant.projectId, "Credential project id"),
|
|
578
|
+
environment: identifier(grant.environment, "Credential environment"),
|
|
579
|
+
workloadId: identifier(grant.workloadId, "Credential workload id"),
|
|
580
|
+
subjectId: identifier(grant.subjectId, "Credential subject id"),
|
|
581
|
+
workloadKind: workloadKind(grant.workloadKind),
|
|
582
|
+
runId: identifier(grant.runId, "Credential run id"),
|
|
583
|
+
provider: identifier(grant.provider, "Credential provider"),
|
|
584
|
+
scopes: scopes(grant.scopes, "Credential grant scopes"),
|
|
585
|
+
audience: "ghostapi-server",
|
|
586
|
+
action: actionReference(grant.action),
|
|
587
|
+
issuedAt,
|
|
588
|
+
expiresAt,
|
|
589
|
+
...(breakGlass === undefined ? {} : { breakGlass }),
|
|
590
|
+
...(grant.revokedAt === undefined
|
|
591
|
+
? {}
|
|
592
|
+
: {
|
|
593
|
+
revokedAt: timestamp(grant.revokedAt, "Credential grant revocation time"),
|
|
594
|
+
}),
|
|
595
|
+
};
|
|
418
596
|
}
|
|
419
597
|
function validateReceipt(value) {
|
|
420
598
|
const receipt = object(value, "Credential use receipt is invalid.");
|
|
421
|
-
exactKeys(receipt, [
|
|
422
|
-
|
|
599
|
+
exactKeys(receipt, [
|
|
600
|
+
"id",
|
|
601
|
+
"grantId",
|
|
602
|
+
"credentialId",
|
|
603
|
+
"credentialVersion",
|
|
604
|
+
"tenantId",
|
|
605
|
+
"workloadId",
|
|
606
|
+
"action",
|
|
607
|
+
"status",
|
|
608
|
+
"providerRequestId",
|
|
609
|
+
"failureCode",
|
|
610
|
+
"executedAt",
|
|
611
|
+
], "Credential use receipt", ["providerRequestId", "failureCode"]);
|
|
612
|
+
if (receipt.status !== "executing" &&
|
|
613
|
+
receipt.status !== "executed" &&
|
|
614
|
+
receipt.status !== "failed" &&
|
|
615
|
+
receipt.status !== "unknown")
|
|
423
616
|
throw new CredentialBrokerError("Credential use receipt status is invalid.");
|
|
424
|
-
if (receipt.status === "executing" &&
|
|
617
|
+
if (receipt.status === "executing" &&
|
|
618
|
+
(receipt.failureCode !== undefined ||
|
|
619
|
+
receipt.providerRequestId !== undefined))
|
|
425
620
|
throw new CredentialBrokerError("Executing credential receipt is invalid.");
|
|
426
|
-
if (receipt.status === "executed" &&
|
|
621
|
+
if (receipt.status === "executed" &&
|
|
622
|
+
(receipt.failureCode !== undefined ||
|
|
623
|
+
receipt.providerRequestId === undefined))
|
|
427
624
|
throw new CredentialBrokerError("Executed credential receipt is invalid.");
|
|
428
|
-
if (receipt.status === "failed" &&
|
|
625
|
+
if (receipt.status === "failed" &&
|
|
626
|
+
(receipt.failureCode !== "execution_not_started" ||
|
|
627
|
+
receipt.providerRequestId !== undefined))
|
|
429
628
|
throw new CredentialBrokerError("Failed credential receipt is invalid.");
|
|
430
|
-
if (receipt.status === "unknown" &&
|
|
629
|
+
if (receipt.status === "unknown" &&
|
|
630
|
+
(receipt.failureCode !== "unknown_outcome" ||
|
|
631
|
+
receipt.providerRequestId !== undefined))
|
|
431
632
|
throw new CredentialBrokerError("Unknown credential receipt is invalid.");
|
|
432
|
-
return {
|
|
633
|
+
return {
|
|
634
|
+
id: identifier(receipt.id, "Credential use receipt id"),
|
|
635
|
+
grantId: identifier(receipt.grantId, "Credential grant id"),
|
|
636
|
+
credentialId: identifier(receipt.credentialId, "Credential id"),
|
|
637
|
+
credentialVersion: positiveInteger(receipt.credentialVersion, "Credential version"),
|
|
638
|
+
tenantId: identifier(receipt.tenantId, "Credential tenant id"),
|
|
639
|
+
workloadId: identifier(receipt.workloadId, "Credential workload id"),
|
|
640
|
+
action: actionReference(receipt.action),
|
|
641
|
+
status: receipt.status,
|
|
642
|
+
...(receipt.providerRequestId === undefined
|
|
643
|
+
? {}
|
|
644
|
+
: {
|
|
645
|
+
providerRequestId: identifier(receipt.providerRequestId, "Provider request id"),
|
|
646
|
+
}),
|
|
647
|
+
...(receipt.failureCode === undefined
|
|
648
|
+
? {}
|
|
649
|
+
: {
|
|
650
|
+
failureCode: receipt.failureCode,
|
|
651
|
+
}),
|
|
652
|
+
executedAt: timestamp(receipt.executedAt, "Credential execution time"),
|
|
653
|
+
};
|
|
433
654
|
}
|
|
434
655
|
function validateAccessRequest(value) {
|
|
435
656
|
const request = object(value, "Credential access request is invalid.");
|
|
436
657
|
exactKeys(request, ["credentialId", "provider", "scopes", "audience", "action"], "Credential access request");
|
|
437
658
|
if (request.audience !== "ghostapi-server")
|
|
438
659
|
throw new CredentialBrokerError("Credential access request audience is invalid.");
|
|
439
|
-
return {
|
|
660
|
+
return {
|
|
661
|
+
credentialId: identifier(request.credentialId, "Credential id"),
|
|
662
|
+
provider: identifier(request.provider, "Credential provider"),
|
|
663
|
+
scopes: scopes(request.scopes, "Credential request scopes"),
|
|
664
|
+
audience: "ghostapi-server",
|
|
665
|
+
action: actionReference(request.action),
|
|
666
|
+
};
|
|
440
667
|
}
|
|
441
668
|
function validateWorkloadIdentity(value, now) {
|
|
442
669
|
const identity = object(value, "Workload identity is invalid.");
|
|
443
|
-
exactKeys(identity, [
|
|
444
|
-
|
|
670
|
+
exactKeys(identity, [
|
|
671
|
+
"schemaVersion",
|
|
672
|
+
"kind",
|
|
673
|
+
"tenantId",
|
|
674
|
+
"projectId",
|
|
675
|
+
"environment",
|
|
676
|
+
"workloadId",
|
|
677
|
+
"subjectId",
|
|
678
|
+
"workloadKind",
|
|
679
|
+
"runId",
|
|
680
|
+
"issuedAt",
|
|
681
|
+
"expiresAt",
|
|
682
|
+
], "Workload identity");
|
|
683
|
+
if (identity.schemaVersion !== SCHEMA_VERSION ||
|
|
684
|
+
identity.kind !== "ghostapi.workload-identity")
|
|
445
685
|
throw new CredentialBrokerError("Unsupported workload identity schema.");
|
|
446
686
|
const issuedAt = timestamp(identity.issuedAt, "Workload identity issue time");
|
|
447
687
|
const expiresAt = timestamp(identity.expiresAt, "Workload identity expiry");
|
|
448
|
-
if (Date.parse(expiresAt) <= Date.parse(issuedAt) ||
|
|
688
|
+
if (Date.parse(expiresAt) <= Date.parse(issuedAt) ||
|
|
689
|
+
Date.parse(expiresAt) <= Date.parse(now))
|
|
449
690
|
throw new CredentialBrokerError("Workload identity is expired.");
|
|
450
|
-
return {
|
|
691
|
+
return {
|
|
692
|
+
schemaVersion: 1,
|
|
693
|
+
kind: "ghostapi.workload-identity",
|
|
694
|
+
tenantId: identifier(identity.tenantId, "Workload tenant id"),
|
|
695
|
+
projectId: identifier(identity.projectId, "Workload project id"),
|
|
696
|
+
environment: identifier(identity.environment, "Workload environment"),
|
|
697
|
+
workloadId: identifier(identity.workloadId, "Workload id"),
|
|
698
|
+
subjectId: identifier(identity.subjectId, "Workload subject id"),
|
|
699
|
+
workloadKind: workloadKind(identity.workloadKind),
|
|
700
|
+
runId: identifier(identity.runId, "Workload run id"),
|
|
701
|
+
issuedAt,
|
|
702
|
+
expiresAt,
|
|
703
|
+
};
|
|
451
704
|
}
|
|
452
705
|
function validateBreakGlassApproval(value, now) {
|
|
453
706
|
const approval = object(value, "Break-glass approval is invalid.");
|
|
454
|
-
exactKeys(approval, [
|
|
455
|
-
|
|
707
|
+
exactKeys(approval, [
|
|
708
|
+
"schemaVersion",
|
|
709
|
+
"kind",
|
|
710
|
+
"approvalId",
|
|
711
|
+
"action",
|
|
712
|
+
"approvedBy",
|
|
713
|
+
"reason",
|
|
714
|
+
"issuedAt",
|
|
715
|
+
"expiresAt",
|
|
716
|
+
], "Break-glass approval");
|
|
717
|
+
if (approval.schemaVersion !== SCHEMA_VERSION ||
|
|
718
|
+
approval.kind !== "ghostapi.break-glass-approval")
|
|
456
719
|
throw new CredentialBrokerError("Unsupported break-glass approval schema.");
|
|
457
720
|
const issuedAt = timestamp(approval.issuedAt, "Break-glass issue time");
|
|
458
721
|
const expiresAt = timestamp(approval.expiresAt, "Break-glass expiry");
|
|
459
|
-
if (Date.parse(issuedAt) > Date.parse(now) ||
|
|
722
|
+
if (Date.parse(issuedAt) > Date.parse(now) ||
|
|
723
|
+
Date.parse(expiresAt) <= Date.parse(now) ||
|
|
724
|
+
Date.parse(expiresAt) - Date.parse(issuedAt) > MAX_BREAK_GLASS_TTL_MS)
|
|
460
725
|
throw new CredentialBrokerError("Break-glass approval is expired or exceeds five minutes.");
|
|
461
|
-
return {
|
|
726
|
+
return {
|
|
727
|
+
schemaVersion: 1,
|
|
728
|
+
kind: "ghostapi.break-glass-approval",
|
|
729
|
+
approvalId: identifier(approval.approvalId, "Break-glass approval id"),
|
|
730
|
+
action: actionReference(approval.action),
|
|
731
|
+
approvedBy: identifier(approval.approvedBy, "Break-glass approver id"),
|
|
732
|
+
reason: text(approval.reason, "Break-glass reason", 200),
|
|
733
|
+
issuedAt,
|
|
734
|
+
expiresAt,
|
|
735
|
+
};
|
|
462
736
|
}
|
|
463
737
|
function breakGlassRecord(value) {
|
|
464
738
|
const record = object(value, "Break-glass record is invalid.");
|
|
465
739
|
exactKeys(record, ["approvalId", "approvedBy", "reason"], "Break-glass record");
|
|
466
|
-
return {
|
|
740
|
+
return {
|
|
741
|
+
approvalId: identifier(record.approvalId, "Break-glass approval id"),
|
|
742
|
+
approvedBy: identifier(record.approvedBy, "Break-glass approver id"),
|
|
743
|
+
reason: text(record.reason, "Break-glass reason", 200),
|
|
744
|
+
};
|
|
467
745
|
}
|
|
468
746
|
function actionReference(value) {
|
|
469
747
|
const action = object(value, "Credential action reference is invalid.");
|
|
470
748
|
exactKeys(action, ["actionId", "actionHash", "actionReceiptHash"], "Credential action reference");
|
|
471
|
-
return {
|
|
749
|
+
return {
|
|
750
|
+
actionId: identifier(action.actionId, "Credential action id"),
|
|
751
|
+
actionHash: hash(action.actionHash, "Credential action hash"),
|
|
752
|
+
actionReceiptHash: hash(action.actionReceiptHash, "Credential action receipt hash"),
|
|
753
|
+
};
|
|
472
754
|
}
|
|
473
755
|
function assertCredentialUsable(credential, now) {
|
|
474
756
|
if (credential.revokedAt !== undefined)
|
|
@@ -477,17 +759,31 @@ function assertCredentialUsable(credential, now) {
|
|
|
477
759
|
throw new CredentialBrokerError("Credential is expired.");
|
|
478
760
|
}
|
|
479
761
|
function assertRequestMatchesCredential(request, credential, workload) {
|
|
480
|
-
if (request.provider !== credential.provider ||
|
|
762
|
+
if (request.provider !== credential.provider ||
|
|
763
|
+
workload.tenantId !== credential.tenantId ||
|
|
764
|
+
workload.projectId !== credential.projectId ||
|
|
765
|
+
workload.environment !== credential.environment)
|
|
481
766
|
throw new CredentialBrokerError("Credential request crosses a tenant, project, environment, or provider boundary.");
|
|
482
767
|
if (!request.scopes.every((scope) => credential.allowedScopes.includes(scope)))
|
|
483
768
|
throw new CredentialBrokerError("Credential request exceeds the credential scope.");
|
|
484
769
|
}
|
|
485
770
|
function assertGrantUsable(grant, workload, request, now) {
|
|
486
|
-
if (grant.revokedAt !== undefined ||
|
|
771
|
+
if (grant.revokedAt !== undefined ||
|
|
772
|
+
Date.parse(grant.expiresAt) <= Date.parse(now))
|
|
487
773
|
throw new CredentialBrokerError("Credential grant is expired or revoked.");
|
|
488
|
-
if (grant.tenantId !== workload.tenantId ||
|
|
774
|
+
if (grant.tenantId !== workload.tenantId ||
|
|
775
|
+
grant.projectId !== workload.projectId ||
|
|
776
|
+
grant.environment !== workload.environment ||
|
|
777
|
+
grant.workloadId !== workload.workloadId ||
|
|
778
|
+
grant.subjectId !== workload.subjectId ||
|
|
779
|
+
grant.workloadKind !== workload.workloadKind ||
|
|
780
|
+
grant.runId !== workload.runId)
|
|
489
781
|
throw new CredentialBrokerError("Credential grant does not belong to this workload subject and run.");
|
|
490
|
-
if (grant.credentialId !== request.credentialId ||
|
|
782
|
+
if (grant.credentialId !== request.credentialId ||
|
|
783
|
+
grant.provider !== request.provider ||
|
|
784
|
+
grant.audience !== request.audience ||
|
|
785
|
+
!sameScopes(grant.scopes, request.scopes) ||
|
|
786
|
+
!sameAction(grant.action, request.action))
|
|
491
787
|
throw new CredentialBrokerError("Credential request differs from the granted action.");
|
|
492
788
|
}
|
|
493
789
|
function findCredential(state, tenantId, credentialId) {
|
|
@@ -505,26 +801,35 @@ function scopes(value, label) {
|
|
|
505
801
|
return parsed.sort();
|
|
506
802
|
}
|
|
507
803
|
function workloadKind(value) {
|
|
508
|
-
if (value !== "agent_run" &&
|
|
804
|
+
if (value !== "agent_run" &&
|
|
805
|
+
value !== "ci_job" &&
|
|
806
|
+
value !== "production_service")
|
|
509
807
|
throw new CredentialBrokerError("Workload kind is invalid.");
|
|
510
808
|
return value;
|
|
511
809
|
}
|
|
512
810
|
function sameScopes(left, right) {
|
|
513
|
-
return left.length === right.length &&
|
|
811
|
+
return (left.length === right.length &&
|
|
812
|
+
left.every((scope, index) => scope === right[index]));
|
|
514
813
|
}
|
|
515
814
|
function sameAction(left, right) {
|
|
516
|
-
return left.actionId === right.actionId &&
|
|
815
|
+
return (left.actionId === right.actionId &&
|
|
816
|
+
left.actionHash === right.actionHash &&
|
|
817
|
+
left.actionReceiptHash === right.actionReceiptHash);
|
|
517
818
|
}
|
|
518
819
|
function workloadKey(binding) {
|
|
519
820
|
return `${binding.tenantId}:${binding.projectId}:${binding.environment}:${binding.workloadKind}:${binding.workloadId}`;
|
|
520
821
|
}
|
|
521
822
|
function identifier(value, label) {
|
|
522
|
-
if (typeof value !== "string" ||
|
|
823
|
+
if (typeof value !== "string" ||
|
|
824
|
+
!IDENTIFIER.test(value) ||
|
|
825
|
+
sanitizeSecretString(value) !== value)
|
|
523
826
|
throw new CredentialBrokerError(`${label} must be a safe stable identifier.`);
|
|
524
827
|
return value;
|
|
525
828
|
}
|
|
526
829
|
function reference(value, label) {
|
|
527
|
-
if (typeof value !== "string" ||
|
|
830
|
+
if (typeof value !== "string" ||
|
|
831
|
+
!REFERENCE.test(value) ||
|
|
832
|
+
sanitizeSecretString(value) !== value)
|
|
528
833
|
throw new CredentialBrokerError(`${label} is invalid.`);
|
|
529
834
|
return value;
|
|
530
835
|
}
|
|
@@ -539,7 +844,9 @@ function positiveInteger(value, label) {
|
|
|
539
844
|
return value;
|
|
540
845
|
}
|
|
541
846
|
function timestamp(value, label) {
|
|
542
|
-
if (typeof value !== "string" ||
|
|
847
|
+
if (typeof value !== "string" ||
|
|
848
|
+
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) ||
|
|
849
|
+
!Number.isFinite(Date.parse(value)))
|
|
543
850
|
throw new CredentialBrokerError(`${label} must be an ISO UTC timestamp.`);
|
|
544
851
|
return value;
|
|
545
852
|
}
|
|
@@ -550,7 +857,11 @@ function futureTimestamp(value, now, label) {
|
|
|
550
857
|
return parsed;
|
|
551
858
|
}
|
|
552
859
|
function text(value, label, maxLength) {
|
|
553
|
-
if (typeof value !== "string" ||
|
|
860
|
+
if (typeof value !== "string" ||
|
|
861
|
+
value.length < 1 ||
|
|
862
|
+
value.length > maxLength ||
|
|
863
|
+
/[\u0000-\u001f]/.test(value) ||
|
|
864
|
+
sanitizeSecretString(value) !== value)
|
|
554
865
|
throw new CredentialBrokerError(`${label} is invalid.`);
|
|
555
866
|
return value;
|
|
556
867
|
}
|
|
@@ -566,7 +877,8 @@ function object(value, message) {
|
|
|
566
877
|
}
|
|
567
878
|
function exactKeys(value, keys, label, optional = []) {
|
|
568
879
|
for (const key of Object.keys(value))
|
|
569
|
-
if (!keys.includes(key) ||
|
|
880
|
+
if (!keys.includes(key) ||
|
|
881
|
+
(value[key] === undefined && !optional.includes(key)))
|
|
570
882
|
throw new CredentialBrokerError(`${label} contains unsupported field: ${key}`);
|
|
571
883
|
}
|
|
572
884
|
function unique(values, label) {
|