Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions landing-zone-configurator/app/README.de.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ npm run check

`check` führt Biome, TypeScript-Prüfung einschließlich Tests, Produktionsbuild und Vitest aus. `npm run format` korrigiert Formatierung und Importreihenfolge.

`npm run test:plan` prüft zusätzlich den echten gepinnten STACKIT-Netzwerkprovider
über `scripts/test-application-network.sh`. Die Prüfung verwendet frischen
temporären lokalen State, eine bereinigte Umgebung, Dummy-Authentifizierung und
einen Loopback-IaaS-Endpunkt. Sie deckt fehlende Präfixe mit gefüllter/leerer
DNS-Liste, SNA-Standard `/28`, explizite `/24` und `/26`, verzögerte Präfixauflösung
sowie automatische Größe ohne konfigurierte DNS-Server ab. Gewöhnliche Provider-Mocks umgehen diese Validierung; es gibt
keinen Apply und keinen Zugriff auf eine Kunden-Cloud.

Falls die passende Runtime lokal noch fehlt, funktioniert ohne globale Installation beispielsweise:

```sh
Expand Down
8 changes: 8 additions & 0 deletions landing-zone-configurator/app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ npm run check

`check` runs Biome, TypeScript checking including tests, the production build and Vitest. `npm run format` corrects formatting and import order.

`npm run test:plan` additionally exercises the real pinned STACKIT network
provider through `scripts/test-application-network.sh`. It uses fresh temporary
local state, a sanitized environment, dummy authentication and a loopback IaaS
endpoint. The cases cover missing prefixes with populated/empty DNS lists,
SNA-default `/28`, explicit `/24` and `/26`, deferred prefix resolution, and
automatic sizing without configured DNS. Ordinary
provider mocks bypass this validation; no Apply or customer cloud access is used.

If the matching runtime is not yet available locally, the following works without a global installation, for example:

```sh
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-- Allow the reviewed Corporate automatic-prefix source for new jobs.
-- Existing template versions, jobs, State keys and backend delegations remain unchanged.
ALTER TABLE lzc.application_job_grants DROP CONSTRAINT application_job_grants_accelerator_revision_check;
ALTER TABLE lzc.application_job_grants ADD CONSTRAINT application_job_grants_accelerator_revision_check
CHECK(accelerator_revision IN ('4d15d7870afa323badd93559d8b37c5a8d138dcf','c4b43c36af198985980b17626c48d357795e3fbd','88149782bf8e91dcdbb43a203b54337886023f7f','57ad1f6a651c1787694b74ff8aa8b241a3dcd16f','9e7fb46c329aca32e878ce1f0ea3b944ea240354','1ed855609d4dcbfded6759c7c8b4fabf7bd6725d'));
ALTER TABLE lzc.application_runner_tickets DROP CONSTRAINT application_runner_tickets_accelerator_revision_check;
ALTER TABLE lzc.application_runner_tickets ADD CONSTRAINT application_runner_tickets_accelerator_revision_check
CHECK(accelerator_revision IN ('c4b43c36af198985980b17626c48d357795e3fbd','88149782bf8e91dcdbb43a203b54337886023f7f','57ad1f6a651c1787694b74ff8aa8b241a3dcd16f','9e7fb46c329aca32e878ce1f0ea3b944ea240354','1ed855609d4dcbfded6759c7c8b4fabf7bd6725d'));

DO $$
DECLARE signature text; definition text; old_source text := '''9e7fb46c329aca32e878ce1f0ea3b944ea240354''';
BEGIN
FOREACH signature IN ARRAY ARRAY[
'lzc.issue_application_job_grant()',
'lzc_auth.reserve_application_dispatch(uuid,uuid)',
'lzc_auth.delegated_application_plan_operation(uuid,uuid,text,jsonb)'
] LOOP
definition := pg_get_functiondef(signature::regprocedure);
IF strpos(definition,old_source)=0 THEN RAISE EXCEPTION 'application_source_guard_missing'; END IF;
EXECUTE replace(definition,old_source,old_source || ',''1ed855609d4dcbfded6759c7c8b4fabf7bd6725d''');
END LOOP;
END $$;
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
-- Complete an order created before its owner's first valid human proof. Never
-- rebind a known identity or change approved/executed order evidence.
CREATE TABLE lzc.application_order_identity_completions (
instance_id uuid PRIMARY KEY,
tenant_id uuid NOT NULL REFERENCES lzc.tenants(id),
completed_by uuid NOT NULL REFERENCES lzc_auth.users(id),
issuer text NOT NULL,
subject text NOT NULL,
email text NOT NULL,
initial_settings jsonb NOT NULL,
completed_settings jsonb NOT NULL,
initial_blockers jsonb NOT NULL,
completed_blockers jsonb NOT NULL,
completed_at timestamptz NOT NULL DEFAULT now(),
FOREIGN KEY(tenant_id,instance_id) REFERENCES lzc.application_instances(tenant_id,id)
);
ALTER TABLE lzc.application_order_identity_completions ENABLE ROW LEVEL SECURITY;
ALTER TABLE lzc.application_order_identity_completions FORCE ROW LEVEL SECURITY;
CREATE POLICY application_identity_completion_read ON lzc.application_order_identity_completions FOR SELECT TO configurator_app
USING(tenant_id=lzc.current_tenant_id() AND ((completed_by=lzc.current_user_id() AND lzc.application_role('application-owner')) OR lzc.application_role('platform-engineer')));
CREATE POLICY application_identity_completion_migration ON lzc.application_order_identity_completions TO configurator_migration USING(true) WITH CHECK(true);
GRANT SELECT ON lzc.application_order_identity_completions TO configurator_app;
CREATE TRIGGER immutable_application_identity_completion BEFORE UPDATE OR DELETE ON lzc.application_order_identity_completions
FOR EACH ROW EXECUTE FUNCTION lzc.immutable_application_version();

CREATE FUNCTION lzc_auth.complete_application_order_identity(p_session uuid,p_instance uuid,p_settings jsonb)
RETURNS lzc.application_instances LANGUAGE plpgsql SECURITY DEFINER SET search_path=pg_catalog AS $$
DECLARE ordered lzc.application_instances; human lzc.stackit_identities; published lzc.application_template_versions;
expected_settings jsonb; expected_roles jsonb; added_roles jsonb; role_policy jsonb; blockers jsonb;
BEGIN
PERFORM lzc_auth.authorize_application(p_session,lzc.current_tenant_id(),'order');
-- Decision/deletion operations take this same row lock. Existing jobs are an
-- absolute boundary; their inputs, grants, artifacts and State never get rebound.
SELECT * INTO ordered FROM lzc.application_instances
WHERE id=p_instance AND tenant_id=lzc.current_tenant_id() AND requested_by=lzc.current_user_id() FOR UPDATE;
IF NOT FOUND OR EXISTS(SELECT 1 FROM lzc.application_order_deletions WHERE instance_id=p_instance) THEN
RAISE EXCEPTION 'application_identity_completion_access_denied' USING ERRCODE='42501';
END IF;
SELECT * INTO human FROM lzc.stackit_identities
WHERE user_id=ordered.requested_by AND issuer='https://accounts.stackit.cloud' AND revoked_at IS NULL AND valid_until>now() FOR SHARE;
IF NOT FOUND THEN RAISE EXCEPTION 'application_identity_completion_access_denied' USING ERRCODE='42501'; END IF;
IF ordered.resolved_settings->>'owner_email' IS NOT NULL THEN
IF ordered.resolved_settings->>'owner_email' IS DISTINCT FROM human.email THEN
RAISE EXCEPTION 'application_owner_identity_changed' USING ERRCODE='40001';
END IF;
RETURN ordered;
END IF;
IF EXISTS(SELECT 1 FROM lzc.application_jobs WHERE instance_id=p_instance) OR
EXISTS(SELECT 1 FROM lzc.application_order_decisions WHERE instance_id=p_instance) OR
EXISTS(SELECT 1 FROM lzc.application_order_identity_completions WHERE instance_id=p_instance) THEN
RAISE EXCEPTION 'application_order_identity_completion_unavailable' USING ERRCODE='40001';
END IF;
SELECT * INTO published FROM lzc.application_template_versions WHERE id=ordered.version_id AND tenant_id=ordered.tenant_id;
IF NOT FOUND OR NOT lzc.application_template_access(ordered.version_id) THEN RAISE EXCEPTION 'application_identity_completion_access_denied' USING ERRCODE='42501'; END IF;
expected_settings:=ordered.resolved_settings || jsonb_build_object('owner_email',human.email);
role_policy:=published.document#>'{parameterPolicy,fields,role_assignments}';
IF role_policy->>'source'='context' AND role_policy->>'variable'='verified-project-owner' THEN
expected_roles:=coalesce(ordered.resolved_settings->'role_assignments','[]'::jsonb);
SELECT coalesce(jsonb_agg(jsonb_build_object('role',role.value,'subject',human.email) ORDER BY role.position),'[]'::jsonb)
INTO added_roles FROM jsonb_array_elements_text(role_policy->'roles') WITH ORDINALITY AS role(value,position)
WHERE NOT EXISTS(SELECT 1 FROM jsonb_array_elements(expected_roles) assignment WHERE assignment->>'role'=role.value AND assignment->>'subject'=human.email);
expected_settings:=jsonb_set(expected_settings,'{role_assignments}',expected_roles || added_roles);
END IF;
-- The normal domain resolver supplies the candidate. SQL independently limits
-- completion to this verified email and the template's declared context roles.
IF p_settings IS DISTINCT FROM expected_settings THEN
RAISE EXCEPTION 'application_identity_completion_access_denied' USING ERRCODE='42501';
END IF;
SELECT coalesce(jsonb_agg(item.value ORDER BY item.position),'[]'::jsonb) INTO blockers
FROM jsonb_array_elements(ordered.qualification_blockers) WITH ORDINALITY item(value,position)
WHERE item.value NOT IN (
'"Verifizierte STACKIT-Benutzeridentität für diese Bestellung fehlt."'::jsonb,
'"Die STACKIT-Identität der projektverantwortlichen Person wird erst bei der Instanziierung verifiziert."'::jsonb
);
INSERT INTO lzc.application_order_identity_completions(instance_id,tenant_id,completed_by,issuer,subject,email,initial_settings,completed_settings,initial_blockers,completed_blockers)
VALUES(ordered.id,ordered.tenant_id,ordered.requested_by,human.issuer,human.subject,human.email,ordered.resolved_settings,expected_settings,ordered.qualification_blockers,blockers);
UPDATE lzc.application_instances SET resolved_settings=expected_settings,qualification_blockers=blockers
WHERE id=ordered.id AND tenant_id=ordered.tenant_id RETURNING * INTO ordered;
RETURN ordered;
END $$;
REVOKE ALL ON FUNCTION lzc_auth.complete_application_order_identity(uuid,uuid,jsonb) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION lzc_auth.complete_application_order_identity(uuid,uuid,jsonb) TO configurator_app;
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export const applicationRunnerPackageSchema = z.strictObject({
"88149782bf8e91dcdbb43a203b54337886023f7f",
"57ad1f6a651c1787694b74ff8aa8b241a3dcd16f",
"9e7fb46c329aca32e878ce1f0ea3b944ea240354",
"1ed855609d4dcbfded6759c7c8b4fabf7bd6725d",
]),
providerLockSha256: z.literal(
"d40debbff204aee590c2a76d09f6ad3234643329b438fd5c6497de60687f6fa5",
Expand Down Expand Up @@ -1690,6 +1691,7 @@ export class Applications {
"88149782bf8e91dcdbb43a203b54337886023f7f",
"57ad1f6a651c1787694b74ff8aa8b241a3dcd16f",
"9e7fb46c329aca32e878ce1f0ea3b944ea240354",
"1ed855609d4dcbfded6759c7c8b4fabf7bd6725d",
].includes(prepared.plan.acceleratorRevision)
)
throw new ApplicationError(409, "application_runner_revision_required");
Expand Down Expand Up @@ -2482,6 +2484,7 @@ export class Applications {
"88149782bf8e91dcdbb43a203b54337886023f7f",
"57ad1f6a651c1787694b74ff8aa8b241a3dcd16f",
"9e7fb46c329aca32e878ce1f0ea3b944ea240354",
"1ed855609d4dcbfded6759c7c8b4fabf7bd6725d",
]).parse(acceleratorRevision);
if (!this.profiles || !this.secrets)
throw new ApplicationError(
Expand Down Expand Up @@ -2586,7 +2589,7 @@ export class Applications {
"SELECT i.* FROM lzc.application_instances i WHERE i.id=$1 AND i.requested_by=$2 AND NOT EXISTS(SELECT 1 FROM lzc.application_order_deletions deleted WHERE deleted.instance_id=i.id AND deleted.tenant_id=i.tenant_id)",
[id, session.userId],
);
const row = instances.rows[0];
let row = instances.rows[0];
if (!row) throw new ApplicationError(404, "application_instance_not_found");
const versions = await client.query<VersionRow>(
"SELECT * FROM lzc.application_template_versions WHERE id=$1",
Expand All @@ -2609,7 +2612,10 @@ export class Applications {
).rows[0];
if (!identity)
throw new ApplicationError(403, "verified_application_identity_required");
if (row.resolved_settings.owner_email !== identity.email)
if (
row.resolved_settings.owner_email != null &&
row.resolved_settings.owner_email !== identity.email
)
throw new ApplicationError(409, "application_owner_identity_changed");
const contracts = await client.query<ContractRow>(
"SELECT * FROM lzc.application_platform_contracts WHERE revision=$1",
Expand All @@ -2629,6 +2635,40 @@ export class Applications {
},
{ verifiedStackitEmail: identity.email },
);
if (row.resolved_settings.owner_email == null) {
try {
const completed = await client.query<InstanceRow>(
"SELECT completed.* FROM lzc_auth.complete_application_order_identity($1,$2,$3::jsonb) completed",
[
session.id,
row.id,
JSON.stringify({
...resolved.resolution.settings,
owner_email: identity.email,
}),
],
);
const completedRow = completed.rows[0];
if (!completedRow)
throw new ApplicationError(
409,
"application_order_identity_completion_unavailable",
);
row = completedRow;
} catch (cause) {
if (
cause instanceof Error &&
"code" in cause &&
cause.code === "40001" &&
[
"application_owner_identity_changed",
"application_order_identity_completion_unavailable",
].includes(cause.message)
)
throw new ApplicationError(409, cause.message);
throw cause;
}
}
if (resolved.resolution.qualificationBlockers.length)
throw new ApplicationError(409, "application_parameters_not_qualified");
const settings = resolved.resolution.settings;
Expand Down
Loading
Loading