Skip to content

Commit 3e5f19c

Browse files
committed
feat(applications): bind approved contracts to applied platforms (#93)
1 parent 6e44e1e commit 3e5f19c

13 files changed

Lines changed: 845 additions & 47 deletions

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
CREATE TABLE lzc.application_platform_sources (
2+
revision uuid PRIMARY KEY,
3+
tenant_id uuid NOT NULL REFERENCES lzc.tenants(id),
4+
apply_run_id uuid NOT NULL REFERENCES lzc.plan_runs(id),
5+
state_key text NOT NULL REFERENCES lzc.platform_states(state_key),
6+
state_version bigint NOT NULL CHECK(state_version>0),
7+
source_contract_revision uuid NOT NULL,
8+
document_sha256 text NOT NULL CHECK(document_sha256 ~ '^[0-9a-f]{64}$'),
9+
approval_session_id uuid NOT NULL,
10+
credential_profile_id uuid NOT NULL,
11+
credential_version integer NOT NULL CHECK(credential_version>0),
12+
credential_key_id text NOT NULL,
13+
FOREIGN KEY(tenant_id,revision) REFERENCES lzc.application_platform_contracts(tenant_id,revision),
14+
UNIQUE(tenant_id,apply_run_id,document_sha256,credential_profile_id,credential_version,credential_key_id)
15+
);
16+
ALTER TABLE lzc.application_platform_sources ENABLE ROW LEVEL SECURITY;
17+
ALTER TABLE lzc.application_platform_sources FORCE ROW LEVEL SECURITY;
18+
CREATE POLICY application_platform_source_read ON lzc.application_platform_sources FOR SELECT TO configurator_app
19+
USING(tenant_id=lzc.current_tenant_id() AND (lzc.application_role('platform-engineer') OR lzc.application_role('application-owner')));
20+
CREATE POLICY application_platform_source_insert ON lzc.application_platform_sources FOR INSERT TO configurator_app
21+
WITH CHECK(tenant_id=lzc.current_tenant_id() AND lzc.application_role('platform-engineer'));
22+
CREATE POLICY application_platform_source_migration ON lzc.application_platform_sources TO configurator_migration USING(true) WITH CHECK(true);
23+
GRANT SELECT,INSERT ON lzc.application_platform_sources TO configurator_app;
24+
25+
CREATE FUNCTION lzc.guard_application_platform_source() RETURNS trigger
26+
LANGUAGE plpgsql SECURITY DEFINER SET search_path=pg_catalog AS $$
27+
DECLARE contract lzc.application_platform_contracts; prepared jsonb;
28+
BEGIN
29+
PERFORM lzc_auth.authorize_application(NEW.approval_session_id,lzc.current_tenant_id(),'publish');
30+
SELECT * INTO STRICT contract FROM lzc.application_platform_contracts WHERE revision=NEW.revision;
31+
IF contract.tenant_id IS DISTINCT FROM NEW.tenant_id OR contract.approved_by IS DISTINCT FROM lzc.current_user_id()
32+
OR contract.credential_profile_id IS DISTINCT FROM NEW.credential_profile_id
33+
OR contract.credential_version IS DISTINCT FROM NEW.credential_version
34+
OR contract.credential_key_id IS DISTINCT FROM NEW.credential_key_id THEN
35+
RAISE EXCEPTION 'application_platform_source_invalid' USING ERRCODE='42501';
36+
END IF;
37+
PERFORM 1 FROM lzc.plan_runs run JOIN lzc.platform_states state ON state.state_key=run.state_key
38+
WHERE run.id=NEW.apply_run_id AND run.tenant_id=NEW.tenant_id AND run.owner_user_id=lzc.current_user_id()
39+
AND run.operation='apply' AND run.status='succeeded' AND run.applied_state_version=NEW.state_version
40+
AND state.tenant_id=run.tenant_id AND state.owner_user_id=run.owner_user_id AND state.version=NEW.state_version
41+
AND state.state_key=NEW.state_key AND state.lock_run_id IS NULL
42+
FOR SHARE OF run,state;
43+
IF NOT FOUND THEN RAISE EXCEPTION 'application_platform_source_changed' USING ERRCODE='40001'; END IF;
44+
SELECT preparation.manifest INTO STRICT prepared FROM lzc.deployment_preparations preparation
45+
JOIN lzc.plan_runs run ON run.preparation_id=preparation.id WHERE run.id=NEW.apply_run_id;
46+
IF prepared->'organization'->>'id' IS DISTINCT FROM contract.organization_id::text THEN
47+
RAISE EXCEPTION 'application_platform_source_invalid' USING ERRCODE='42501';
48+
END IF;
49+
PERFORM 1 FROM lzc.stackit_organization_access access
50+
JOIN lzc.stackit_identities identity ON identity.user_id=access.user_id
51+
JOIN lzc.tenants tenant ON tenant.id=access.tenant_id
52+
WHERE access.tenant_id=NEW.tenant_id AND access.user_id=lzc.current_user_id()
53+
AND tenant.organization_verified AND tenant.archived_at IS NULL
54+
AND tenant.organization_id=contract.organization_id AND access.organization_id=contract.organization_id
55+
AND access.valid_until>now() AND identity.valid_until>now() AND identity.revoked_at IS NULL
56+
AND access.verified_at>=identity.verified_at
57+
FOR SHARE OF access,identity,tenant;
58+
IF NOT FOUND THEN RAISE EXCEPTION 'application_platform_source_invalid' USING ERRCODE='42501'; END IF;
59+
RETURN NEW;
60+
END $$;
61+
CREATE TRIGGER application_platform_source BEFORE INSERT ON lzc.application_platform_sources FOR EACH ROW EXECUTE FUNCTION lzc.guard_application_platform_source();
62+
CREATE TRIGGER immutable_application_platform_source BEFORE UPDATE OR DELETE ON lzc.application_platform_sources FOR EACH ROW EXECUTE FUNCTION lzc.immutable_application_version();
63+
REVOKE ALL ON FUNCTION lzc.guard_application_platform_source() FROM PUBLIC;

‎landing-zone-configurator/app/apps/api/src/applications/routes.ts‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ export function registerApplications(
124124
| "createGroup"
125125
| "setGroupMembers"
126126
| "setTemplateGroups"
127+
| "appliedPlatformsEnabled"
128+
| "listAppliedPlatforms"
129+
| "previewAppliedPlatform"
130+
| "approveAppliedPlatform"
127131
>
128132
>,
129133
) {
@@ -183,6 +187,35 @@ export function registerApplications(
183187
return applications.approvePlatformContract(session, request.body);
184188
},
185189
);
190+
if (applications.appliedPlatformsEnabled?.()) {
191+
routes.get(
192+
"/api/v1/applications/applied-platforms",
193+
async (request, reply) => {
194+
const session = sessions.get(request);
195+
if (!session) return reply.code(401).send();
196+
return {
197+
platforms: await applications.listAppliedPlatforms?.(session),
198+
};
199+
},
200+
);
201+
routes.get(
202+
"/api/v1/applications/applied-platforms/:id/preview",
203+
async (request, reply) => {
204+
const session = sessions.get(request);
205+
if (!session) return reply.code(401).send();
206+
const { id } = z.object({ id: z.uuid() }).parse(request.params);
207+
return applications.previewAppliedPlatform?.(session, id);
208+
},
209+
);
210+
routes.post(
211+
"/api/v1/applications/applied-platforms/approve",
212+
async (request, reply) => {
213+
const session = sessions.get(request);
214+
if (!session) return reply.code(401).send();
215+
return applications.approveAppliedPlatform?.(session, request.body);
216+
},
217+
);
218+
}
186219
if (applications.listGroups)
187220
routes.get("/api/v1/applications/groups", async (request, reply) => {
188221
const session = sessions.get(request);
@@ -228,6 +261,9 @@ export function registerApplications(
228261
versions: await applications.listTemplates(session),
229262
retirementEnabled: true,
230263
deploymentPolicyEnabled: true,
264+
...(applications.appliedPlatformsEnabled?.()
265+
? { appliedPlatformsEnabled: true }
266+
: {}),
231267
...(applications.listGroups && applications.setTemplateGroups
232268
? { groupAccessEnabled: true }
233269
: {}),

‎landing-zone-configurator/app/apps/api/src/applications/service.ts‎

Lines changed: 189 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { createHash, randomBytes, randomUUID } from "node:crypto";
22
import {
33
type ApplicationInstance,
4+
type AppliedPlatformSource,
45
applicationInstanceSchema,
6+
appliedPlatformSourceSchema,
57
planResultSchema,
68
runnerArtifactSchema,
79
s3BackendDescriptorSchema,
@@ -30,6 +32,8 @@ import type { CredentialSecrets } from "../credentials/secrets.js";
3032
import type { Backends } from "../deployments/backends.js";
3133
import type { PlanRunner } from "../plans/cloud-foundry.js";
3234
import { type ArtifactCrypto, canonicalBase64 } from "../plans/crypto.js";
35+
import { canonicalJson, sha256 } from "../plans/execution.js";
36+
import type { Plans } from "../plans/service.js";
3337
import { withTenant } from "../storage/database.js";
3438

3539
export class ApplicationError extends Error {
@@ -69,13 +73,23 @@ type ContractRow = {
6973
document: unknown;
7074
approved_by: string;
7175
approved_at: Date;
76+
source?: unknown;
7277
};
7378

79+
const contractSelection = `SELECT contracts.*,CASE WHEN source.revision IS NULL THEN NULL ELSE jsonb_build_object(
80+
'applyRunId',source.apply_run_id,'stateKey',source.state_key,'stateVersion',source.state_version::text,
81+
'contractRevision',source.source_contract_revision,'documentSha256',source.document_sha256) END AS source
82+
FROM lzc.application_platform_contracts contracts LEFT JOIN lzc.application_platform_sources source
83+
ON source.tenant_id=contracts.tenant_id AND source.revision=contracts.revision`;
84+
7485
function contract(row: ContractRow) {
7586
return {
7687
document: platformContractSchema.parse(row.document),
7788
approvedBy: row.approved_by,
7889
approvedAt: row.approved_at.toISOString(),
90+
...(row.source
91+
? { source: appliedPlatformSourceSchema.parse(row.source) }
92+
: {}),
7993
};
8094
}
8195

@@ -142,6 +156,7 @@ export class Applications {
142156
private readonly backends?: Pick<Backends, "runner">,
143157
private readonly dispatch?: { runner: PlanRunner; origin: string },
144158
private readonly artifactCrypto?: ArtifactCrypto,
159+
private readonly platforms?: Pick<Plans, "outputs">,
145160
) {}
146161

147162
private work<T>(
@@ -320,12 +335,111 @@ export class Applications {
320335
listPlatformContracts(session: Session) {
321336
return this.work(session, "read", async (client) => {
322337
const result = await client.query<ContractRow>(
323-
"SELECT * FROM lzc.application_platform_contracts ORDER BY approved_at DESC,revision DESC LIMIT 200",
338+
`${contractSelection} ORDER BY contracts.approved_at DESC,contracts.revision DESC LIMIT 200`,
324339
);
325340
return result.rows.map(contract);
326341
});
327342
}
328343

344+
appliedPlatformsEnabled() {
345+
return Boolean(this.platforms);
346+
}
347+
348+
listAppliedPlatforms(session: Session, applyRunId?: string) {
349+
if (!this.platforms)
350+
throw new ApplicationError(
351+
503,
352+
"application_applied_platforms_unavailable",
353+
);
354+
if (applyRunId) z.uuid().parse(applyRunId);
355+
return this.work(session, "publish", async (client) => {
356+
const result = await client.query<{
357+
id: string;
358+
state_key: string;
359+
state_version: string;
360+
organization_id: string;
361+
finished_at: Date;
362+
}>(
363+
"SELECT run.id,run.state_key,state.version::text AS state_version,preparation.manifest->'organization'->>'id' AS organization_id,run.finished_at FROM lzc.plan_runs run JOIN lzc.platform_states state ON state.state_key=run.state_key JOIN lzc.deployment_preparations preparation ON preparation.id=run.preparation_id JOIN lzc.tenants tenant ON tenant.id=run.tenant_id WHERE run.operation='apply' AND run.status='succeeded' AND run.finished_at IS NOT NULL AND run.applied_state_version=state.version AND state.lock_run_id IS NULL AND tenant.organization_verified AND tenant.archived_at IS NULL AND tenant.organization_id::text=preparation.manifest->'organization'->>'id' AND EXISTS(SELECT 1 FROM lzc.stackit_organization_access access JOIN lzc.stackit_identities identity ON identity.user_id=access.user_id WHERE access.tenant_id=run.tenant_id AND access.user_id=lzc.current_user_id() AND access.organization_id=tenant.organization_id AND access.valid_until>now() AND identity.revoked_at IS NULL AND identity.valid_until>now() AND access.verified_at>=identity.verified_at) AND ($1::uuid IS NULL OR run.id=$1) ORDER BY run.finished_at DESC,run.id DESC LIMIT 100",
364+
[applyRunId ?? null],
365+
);
366+
return result.rows.map((row) => ({
367+
id: row.id,
368+
stateKey: row.state_key,
369+
stateVersion: row.state_version,
370+
organizationId: row.organization_id,
371+
finishedAt: row.finished_at.toISOString(),
372+
}));
373+
});
374+
}
375+
376+
async previewAppliedPlatform(session: Session, applyRunId: string) {
377+
const candidate = (
378+
await this.listAppliedPlatforms(session, z.uuid().parse(applyRunId))
379+
)[0];
380+
if (!candidate || !this.platforms)
381+
throw new ApplicationError(
382+
404,
383+
"application_platform_source_unavailable",
384+
);
385+
await this.work(session, "publish", (client) =>
386+
this.publisherProof(client, session, candidate.organizationId),
387+
);
388+
let exported: Awaited<ReturnType<Plans["outputs"]>>;
389+
try {
390+
exported = await this.platforms.outputs(session, applyRunId, true);
391+
} catch {
392+
throw new ApplicationError(
393+
409,
394+
"application_platform_source_unavailable",
395+
);
396+
}
397+
const document = platformContractSchema.parse(
398+
exported.applicationPlatformContract,
399+
);
400+
const source = appliedPlatformSourceSchema.parse(exported.source);
401+
if (
402+
document.tenant_id !== session.tenantId ||
403+
document.organization_id !== candidate.organizationId ||
404+
source.applyRunId !== candidate.id ||
405+
source.stateKey !== candidate.stateKey ||
406+
source.stateVersion !== candidate.stateVersion ||
407+
source.contractRevision !== document.revision ||
408+
source.documentSha256 !== sha256(canonicalJson(document))
409+
)
410+
throw new ApplicationError(409, "application_platform_source_changed");
411+
return { document, source };
412+
}
413+
414+
async approveAppliedPlatform(session: Session, input: unknown) {
415+
const request = z
416+
.strictObject({
417+
source: appliedPlatformSourceSchema,
418+
confirmApproval: z.literal(true),
419+
credentialProfileId: z.uuid().optional(),
420+
})
421+
.parse(input);
422+
const current = await this.previewAppliedPlatform(
423+
session,
424+
request.source.applyRunId,
425+
);
426+
if (canonicalJson(current.source) !== canonicalJson(request.source))
427+
throw new ApplicationError(409, "application_platform_source_changed");
428+
return this.approvePlatformContract(
429+
session,
430+
{
431+
schema_version: current.document.schema_version,
432+
organization_id: current.document.organization_id,
433+
targets: current.document.targets,
434+
confirmApproval: true,
435+
...(request.credentialProfileId
436+
? { credentialProfileId: request.credentialProfileId }
437+
: {}),
438+
},
439+
current.source,
440+
);
441+
}
442+
329443
private async publisherProof(
330444
client: pg.PoolClient,
331445
session: Session,
@@ -342,7 +456,11 @@ export class Applications {
342456
);
343457
}
344458

345-
async approvePlatformContract(session: Session, input: unknown) {
459+
async approvePlatformContract(
460+
session: Session,
461+
input: unknown,
462+
source?: AppliedPlatformSource,
463+
) {
346464
assertBoundedJson(input);
347465
const request = platformContractSchema
348466
.omit({ tenant_id: true, revision: true })
@@ -379,8 +497,59 @@ export class Applications {
379497
checked.check.organizationId !== request.organization_id
380498
)
381499
throw new ApplicationError(422, "application_technical_access_failed");
500+
if (source) {
501+
const current = await this.previewAppliedPlatform(
502+
session,
503+
source.applyRunId,
504+
);
505+
if (
506+
canonicalJson(current.source) !== canonicalJson(source) ||
507+
sha256(
508+
canonicalJson({
509+
schema_version: request.schema_version,
510+
tenant_id: session.tenantId,
511+
revision: source.contractRevision,
512+
organization_id: request.organization_id,
513+
targets: request.targets,
514+
}),
515+
) !== source.documentSha256
516+
)
517+
throw new ApplicationError(409, "application_platform_source_changed");
518+
}
382519
return this.work(session, "publish", async (client) => {
383520
await this.publisherProof(client, session, request.organization_id);
521+
if (source) {
522+
await client
523+
.query(
524+
"SELECT 1 FROM lzc.plan_runs run JOIN lzc.platform_states state ON state.state_key=run.state_key WHERE run.id=$1 AND run.operation='apply' AND run.status='succeeded' AND run.applied_state_version=$2::bigint AND state.version=$2::bigint AND state.state_key=$3 AND state.lock_run_id IS NULL FOR SHARE OF run,state",
525+
[source.applyRunId, source.stateVersion, source.stateKey],
526+
)
527+
.then((result) => {
528+
if (result.rowCount !== 1)
529+
throw new ApplicationError(
530+
409,
531+
"application_platform_source_changed",
532+
);
533+
});
534+
await client.query(
535+
"SELECT pg_advisory_xact_lock(hashtextextended($1,0))",
536+
[
537+
`application-platform-source:${session.tenantId}:${source.applyRunId}:${source.documentSha256}:${profile.id}:${checked.version}:${checked.keyId}`,
538+
],
539+
);
540+
const existing = await client.query<ContractRow>(
541+
`${contractSelection} WHERE contracts.tenant_id=$1 AND source.apply_run_id=$2 AND source.document_sha256=$3 AND contracts.credential_profile_id=$4 AND contracts.credential_version=$5 AND contracts.credential_key_id=$6`,
542+
[
543+
session.tenantId,
544+
source.applyRunId,
545+
source.documentSha256,
546+
profile.id,
547+
checked.version,
548+
checked.keyId,
549+
],
550+
);
551+
if (existing.rows[0]) return contract(existing.rows[0]);
552+
}
384553
const document = platformContractSchema.parse({
385554
schema_version: request.schema_version,
386555
tenant_id: session.tenantId,
@@ -405,7 +574,24 @@ export class Applications {
405574
const row = result.rows[0];
406575
if (!row)
407576
throw new ApplicationError(503, "platform_contract_approval_failed");
408-
return contract(row);
577+
if (source)
578+
await client.query(
579+
"INSERT INTO lzc.application_platform_sources(revision,tenant_id,apply_run_id,state_key,state_version,source_contract_revision,document_sha256,approval_session_id,credential_profile_id,credential_version,credential_key_id) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)",
580+
[
581+
document.revision,
582+
session.tenantId,
583+
source.applyRunId,
584+
source.stateKey,
585+
source.stateVersion,
586+
source.contractRevision,
587+
source.documentSha256,
588+
session.id,
589+
profile.id,
590+
checked.version,
591+
checked.keyId,
592+
],
593+
);
594+
return contract({ ...row, ...(source ? { source } : {}) });
409595
});
410596
}
411597

‎landing-zone-configurator/app/apps/api/src/main.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ if (process.env.LZC_AUTH_ENABLED === "true") {
111111
backends,
112112
undefined,
113113
artifactCrypto,
114+
artifactCrypto ? plans : undefined,
114115
);
115116
}
116117
auth = {

0 commit comments

Comments
 (0)