diff --git a/.github/workflows/security-release-gate.yml b/.github/workflows/security-release-gate.yml index 0820b5d2c0..051e4c9b7f 100644 --- a/.github/workflows/security-release-gate.yml +++ b/.github/workflows/security-release-gate.yml @@ -65,6 +65,10 @@ jobs: 'strictWireRejectsMalformedNoncanonicalNestedRawJsonAndUnknownEnvelope', 'decodedJsonLimitAllowsLargeNativeWireAndRejectsOneByteOverflow', }, + 'io.cattle.platform.iaas.api.auth.ai.catalog.AiCatalogLaunchMaterializerTest': { + 'realTransformerDynamicFieldsReachNativeRoleSchemaProjection', + 'dynamicProjectionKeepsRestrictedSchemaAndNonCreateResponseRules', + }, 'io.github.ibuildthecloud.gdapi.model.impl.CreateOnlyWrappedResourceTest': { 'trueCreationRetainsTheFirstSecretInBothApiVersions', 'postActionsCannotRevealCreateOnlyValues', diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index dbfc20779b..d7e93103a6 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -1,5 +1,20 @@ # Compatibility Contract +## Engine 0.183.366 candidate + +This candidate adds bounded `ports` and `healthCheck` parameters only to typed +Service creation. Compatible clients must keep read-only +`executionResult.assignedPorts` separate from the approved change summary. +Services are still created inactive, and private human entry of environment +values remains pending. Current role/Key checks, Full/legacy behavior, runtime +dependency coordinates and database schemas are unchanged. + +Native IPv6 port formatting now retains brackets for roundtrip parsing; +existing database values are not rewritten. Formatter coverage does not prove +host IPv6 binding or routing. Required CI/CodeQL, artifact qualification and +matching Server/native acceptance remain pending; see the +[candidate release note](docs/releases/orchestration-engine-0.183.366.md). + ## Engine 0.183.364 release The [published release](https://github.com/PastureStack/orchestration-engine/releases/tag/v0.183.364) diff --git a/README.md b/README.md index 5ec22f7360..50180d8413 100644 --- a/README.md +++ b/README.md @@ -8,35 +8,35 @@ Rancher 1.6 ecosystem. It is not affiliated with Rancher Labs or SUSE. This fork of [`rancher/cattle`](https://github.com/rancher/cattle) preserves upstream history, authorship, licenses, and dependency notices. -## Current release: Engine 0.183.364 +## Engine 0.183.366 candidate -The [0.183.364 release](https://github.com/PastureStack/orchestration-engine/releases/tag/v0.183.364) -publishes the original qualified CI bytes. Use the +This source tree prepares Engine 0.183.366; the candidate is not published. +The published baseline is +[Engine 0.183.365](https://github.com/PastureStack/orchestration-engine/releases/tag/v0.183.365). +Use the [Server installation guide](https://github.com/PastureStack/server#quick-start) for deployment; an Engine JAR alone is not a Server installation. -A CUSTOM Key authorized to delete its standalone container can now finish the -exact native private OS-image/cache cleanup. Current owner, role, Key revision, -expiry, root grant and every explicit dependency DENY still apply. Shared or -foreign resources and public Image/Volume/Pool operations receive no new grant. -External target replacement also retires an obsolete hostname map when the -new hostname is absent or different, allowing normal reconciliation to finish. - -The External before-value preview, ServiceIndex cleanup authority and official -MariaDB JDBC3.5.11 fixes are retained. Full/legacy behavior and runtime dependency -coordinates remain unchanged; the four reported JDBC CVEs remain absent from -the qualified original product and raw/effective Dapper scans. +The candidate extends human-reviewed Service creation with bounded native +`ports` and `healthCheck` fields. Preview, confirmation and apply recheck the +existing native port preflight. Created Services remain inactive; starting +one requires a separate approved plan. A successful receipt exposes the actual +assigned ports through read-only `executionResult.assignedPorts`, while the +approved change summary remains unchanged. Native IPv6 port formatting now +preserves the brackets needed to read a mapping back through `PortSpec`. +Private human input for environment-variable values is not implemented in this +candidate; `environment` remains outside resource-change parameters. | Qualification | Current evidence | |---|---| -| Component | Required CI/CodeQL passed: 3,872 tests in 416 suites, zero failures/errors, one existing skip. All 14 new cleanup/lifecycle methods passed; seven preview and eight Index methods are inherited. | -| Publication | Twelve original assets passed anonymous SHA256 readback; producer and merge/tag share the tested tree. No publication rebuild or native SLSA attestation is claimed. | -| Assembled Server and live workflows | Matching Server publication, native cleanup/External replacement and designated role/Key/cross-ID business QA remain pending. Component publication does not establish workflow acceptance. | +| Focused source checks | 60 unique unit cases passed on JDK 25 and Maven 3.9.16. Three test-fixture setup corrections were rerun alone; unaffected PASS evidence was retained. | +| Candidate qualification and publication | Required original component CI/CodeQL, complete package qualification and publication remain pending. No candidate artifact digest or release asset is claimed. | +| Assembled Server and live workflows | Matching Web Console/Server integration and native role/Key workflows remain pending. IPv6 formatter tests do not establish host IPv6 binding or routing acceptance. | Use the compatible Web Console typed contract and matching Server broker. Four independent AI opt-ins remain default-off. The -[release note](docs/releases/orchestration-engine-0.183.364.md) records artifact -identity and remaining boundaries. This does not complete all 243 frontend +[candidate release note](docs/releases/orchestration-engine-0.183.366.md) records +the exact supported fields and remaining boundaries. This does not complete all 243 frontend business groups. ## Optional AI resource workflows @@ -51,9 +51,12 @@ private keys, raw process logs or full launch configurations. Queries and opaque pagination remain subject to the underlying domain manager's authorization. `aiResourceChangePlan` supports Stack, Service and Container creation, metadata -editing and the explicitly registered lifecycle operations. Creation currently -covers the named basic fields and creates stopped workloads; it does not claim -all advanced UI form fields. A short-lived plan captures the target state and +editing and the explicitly registered lifecycle operations. In this candidate, +Service creation additionally accepts native port-specification strings and a +closed health-check object. Fixed host ports must match the actual result; +omitted host ports use native allocation and are reported only after verified +readback. It does not cover every advanced UI field or private environment-value +entry. A short-lived plan captures the target state and Key revision. A human cookie session reviews and approves it; a Key cannot approve or cancel its own plan. Apply claims one durable operation before dispatch; an uncertain dispatch is never automatically replayed. @@ -175,7 +178,7 @@ Before publishing an Engine artifact, run the complete package gate: ```sh bash scripts/check-cattle-jdk25-full-package -ENGINE_VERSION=0.183.361 bash scripts/build --release +ENGINE_VERSION=0.183.366 bash scripts/build --release bash scripts/check-release-artifact dist/artifacts/cattle.jar ``` diff --git a/code/framework/api-pub-sub-jetty/pom.xml b/code/framework/api-pub-sub-jetty/pom.xml index 6a1d631c85..3191ed3745 100644 --- a/code/framework/api-pub-sub-jetty/pom.xml +++ b/code/framework/api-pub-sub-jetty/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/api-pub-sub/pom.xml b/code/framework/api-pub-sub/pom.xml index d7a8901e1d..e5bfcd0085 100644 --- a/code/framework/api-pub-sub/pom.xml +++ b/code/framework/api-pub-sub/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/api/pom.xml b/code/framework/api/pom.xml index bbb1e8ca6a..c1521b1a8b 100644 --- a/code/framework/api/pom.xml +++ b/code/framework/api/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/archaius/pom.xml b/code/framework/archaius/pom.xml index 8119d83216..f46934b45f 100644 --- a/code/framework/archaius/pom.xml +++ b/code/framework/archaius/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-meta-parent - 0.183.365 + 0.183.366 ../../meta-parent/pom.xml diff --git a/code/framework/async/pom.xml b/code/framework/async/pom.xml index 7c3de47545..8eafe80a66 100644 --- a/code/framework/async/pom.xml +++ b/code/framework/async/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/auditing/pom.xml b/code/framework/auditing/pom.xml index 3e2f46bf53..3fdcb2c9a6 100644 --- a/code/framework/auditing/pom.xml +++ b/code/framework/auditing/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/db-loader/pom.xml b/code/framework/db-loader/pom.xml index 3b0b741b9b..936117cee1 100644 --- a/code/framework/db-loader/pom.xml +++ b/code/framework/db-loader/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/deferred/pom.xml b/code/framework/deferred/pom.xml index e5f5c91603..45d50246d1 100644 --- a/code/framework/deferred/pom.xml +++ b/code/framework/deferred/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/encryption/pom.xml b/code/framework/encryption/pom.xml index ec2883f666..ac21aae936 100644 --- a/code/framework/encryption/pom.xml +++ b/code/framework/encryption/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/engine/pom.xml b/code/framework/engine/pom.xml index 87b061fe2c..0aba006241 100644 --- a/code/framework/engine/pom.xml +++ b/code/framework/engine/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/eventing/pom.xml b/code/framework/eventing/pom.xml index 869605339f..0a2bb1c035 100644 --- a/code/framework/eventing/pom.xml +++ b/code/framework/eventing/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/events/pom.xml b/code/framework/events/pom.xml index 5c9d4e9892..c28c45845a 100644 --- a/code/framework/events/pom.xml +++ b/code/framework/events/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/extension-spring/pom.xml b/code/framework/extension-spring/pom.xml index 83f94aae92..9e59e72f3a 100644 --- a/code/framework/extension-spring/pom.xml +++ b/code/framework/extension-spring/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/extension/pom.xml b/code/framework/extension/pom.xml index 927fcf2fad..19a6b394f7 100644 --- a/code/framework/extension/pom.xml +++ b/code/framework/extension/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/java-server/pom.xml b/code/framework/java-server/pom.xml index 15208c2774..cb7e0c7a4f 100644 --- a/code/framework/java-server/pom.xml +++ b/code/framework/java-server/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/jmx/pom.xml b/code/framework/jmx/pom.xml index 929e344d7f..bcba2408e4 100644 --- a/code/framework/jmx/pom.xml +++ b/code/framework/jmx/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/jooq/pom.xml b/code/framework/jooq/pom.xml index 02f15fe7de..55619d0f1a 100644 --- a/code/framework/jooq/pom.xml +++ b/code/framework/jooq/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/json/pom.xml b/code/framework/json/pom.xml index ea9467a14c..c7eb19e829 100644 --- a/code/framework/json/pom.xml +++ b/code/framework/json/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/launcher/pom.xml b/code/framework/launcher/pom.xml index 0e0f5d3b57..3d321de0fa 100644 --- a/code/framework/launcher/pom.xml +++ b/code/framework/launcher/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/lock/pom.xml b/code/framework/lock/pom.xml index fd7277d5ac..fd9822fc88 100644 --- a/code/framework/lock/pom.xml +++ b/code/framework/lock/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/logback/pom.xml b/code/framework/logback/pom.xml index 57c4cd9282..ac27e218ab 100644 --- a/code/framework/logback/pom.xml +++ b/code/framework/logback/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-meta-parent - 0.183.365 + 0.183.366 ../../meta-parent/pom.xml diff --git a/code/framework/managed-context/pom.xml b/code/framework/managed-context/pom.xml index a775d9ae6a..e35b4d5036 100644 --- a/code/framework/managed-context/pom.xml +++ b/code/framework/managed-context/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/metrics/pom.xml b/code/framework/metrics/pom.xml index 9207cbb9be..8af58d28f3 100644 --- a/code/framework/metrics/pom.xml +++ b/code/framework/metrics/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/module/pom.xml b/code/framework/module/pom.xml index ba5c6887e6..9242eb2bad 100644 --- a/code/framework/module/pom.xml +++ b/code/framework/module/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/object/pom.xml b/code/framework/object/pom.xml index ec9d9f9426..bce652d977 100644 --- a/code/framework/object/pom.xml +++ b/code/framework/object/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/pool/pom.xml b/code/framework/pool/pom.xml index fe99a758ad..caf6095d42 100644 --- a/code/framework/pool/pom.xml +++ b/code/framework/pool/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/resource-monitor/pom.xml b/code/framework/resource-monitor/pom.xml index c209cf95dd..0fbe28ead9 100644 --- a/code/framework/resource-monitor/pom.xml +++ b/code/framework/resource-monitor/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/schema/pom.xml b/code/framework/schema/pom.xml index 338577bd46..b2cfe6c0b5 100644 --- a/code/framework/schema/pom.xml +++ b/code/framework/schema/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/server/pom.xml b/code/framework/server/pom.xml index a83000ad07..8114bedd91 100644 --- a/code/framework/server/pom.xml +++ b/code/framework/server/pom.xml @@ -4,7 +4,7 @@ cattle-meta-parent io.cattle - 0.183.365 + 0.183.366 ../../meta-parent/pom.xml diff --git a/code/framework/spring/pom.xml b/code/framework/spring/pom.xml index c846df049f..9b193bf375 100644 --- a/code/framework/spring/pom.xml +++ b/code/framework/spring/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/system-task/pom.xml b/code/framework/system-task/pom.xml index bc7a7b2dd4..b57bdf467d 100644 --- a/code/framework/system-task/pom.xml +++ b/code/framework/system-task/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/token/pom.xml b/code/framework/token/pom.xml index 4d2c48c394..64d42d2b7f 100644 --- a/code/framework/token/pom.xml +++ b/code/framework/token/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/framework/utils/pom.xml b/code/framework/utils/pom.xml index eb20d16704..e5159c176d 100644 --- a/code/framework/utils/pom.xml +++ b/code/framework/utils/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/iaas/agent-instance/pom.xml b/code/iaas/agent-instance/pom.xml index 901522f09c..d52dc7605b 100644 --- a/code/iaas/agent-instance/pom.xml +++ b/code/iaas/agent-instance/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/iaas/agent-server/pom.xml b/code/iaas/agent-server/pom.xml index 39b2979eed..28dbd1d760 100644 --- a/code/iaas/agent-server/pom.xml +++ b/code/iaas/agent-server/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/iaas/agent/pom.xml b/code/iaas/agent/pom.xml index 5b98349f90..00edfbbcdd 100644 --- a/code/iaas/agent/pom.xml +++ b/code/iaas/agent/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/iaas/allocator/pom.xml b/code/iaas/allocator/pom.xml index 711962733b..ff4066e06b 100644 --- a/code/iaas/allocator/pom.xml +++ b/code/iaas/allocator/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/iaas/api-logic/pom.xml b/code/iaas/api-logic/pom.xml index a205464292..2352f21955 100644 --- a/code/iaas/api-logic/pom.xml +++ b/code/iaas/api-logic/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/iaas/archaius-management/pom.xml b/code/iaas/archaius-management/pom.xml index a2ce48f4ba..0de183a1e2 100644 --- a/code/iaas/archaius-management/pom.xml +++ b/code/iaas/archaius-management/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/iaas/auth-logic/pom.xml b/code/iaas/auth-logic/pom.xml index 6017819e57..298472a2b1 100644 --- a/code/iaas/auth-logic/pom.xml +++ b/code/iaas/auth-logic/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchMaterializer.java b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchMaterializer.java index b1d2678e5c..301d20f890 100644 --- a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchMaterializer.java +++ b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchMaterializer.java @@ -6,6 +6,7 @@ import io.cattle.platform.core.model.tables.records.InstanceRecord; import io.cattle.platform.docker.transform.DockerTransformer; import io.cattle.platform.object.util.ObjectUtils; +import io.cattle.platform.object.util.DataUtils; import io.github.ibuildthecloud.gdapi.context.ApiContext; import io.github.ibuildthecloud.gdapi.model.ListOptions; import io.github.ibuildthecloud.gdapi.model.Pagination; @@ -55,8 +56,10 @@ PreparedGraph prepare(ApiRequest request, AiCatalogLaunchAccess.Authorization au InstanceRecord instance = new InstanceRecord(); try { transformer.transform(map(service.get("dockerInspect")), instance); } catch (RuntimeException invalid) { throw error(422, "CatalogLaunchUnsupported"); } - // Exactly the normal transform response's schema projection, under the original role. - WrappedResource resource = new WrappedResource(ApiContext.getContext().getIdFormatter(), schemas, container, instance, "POST"); + // Match the native projection's data.fields source while retaining the + // original role and non-create-response handling of create-only fields. + WrappedResource resource = new WrappedResource(ApiContext.getContext().getIdFormatter(), schemas, container, instance, + new LinkedHashMap<>(DataUtils.getFields(instance)), null, "POST"); transformed.put(name, new LinkedHashMap<>(resource.getFields())); } Map authorizedReferenceIds = new LinkedHashMap<>(); diff --git a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDescriptor.java b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDescriptor.java index 4ebd42ce04..50f5b487f8 100644 --- a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDescriptor.java +++ b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDescriptor.java @@ -23,7 +23,7 @@ private static Map descriptors() { add(result, type, "remove", "DELETE", null, "delete", Set.of()); add(result, type, "update_metadata", "PUT", null, "update", Set.of("name", "description")); add(result, type, "create", "POST", null, "create", "stack".equals(type) ? Set.of("name", "description") - : "service".equals(type) ? Set.of("name", "description", "image", "scale", "stackId", "command", "entryPoint") + : "service".equals(type) ? Set.of("name", "description", "image", "scale", "stackId", "command", "entryPoint", "ports", "healthCheck") : Set.of("name", "description", "image", "command", "entryPoint")); } add(result, "service", "scale", "PUT", null, "update", Set.of("scale")); @@ -90,6 +90,8 @@ public Map parameters(Object value) { || image.contains("@") && !image.matches(".+@sha256:[0-9a-fA-F]{64}")) throw error("ResourceChangeInputInvalid"); } case "stackId" -> string(content, 1, 128, false); + case "ports" -> content = AiServiceCreateChange.ports(content); + case "healthCheck" -> content = AiServiceCreateChange.healthCheck(content); case "command", "entryPoint" -> { if (!(content instanceof List values) || values.size() > 64) throw error("ResourceChangeInputInvalid"); List arguments = new ArrayList<>(); int length = 0; @@ -136,7 +138,8 @@ public Map body(Map parameters) { if ("service".equals(resourceType)) { body.putIfAbsent("scale", 1); Map launch = new LinkedHashMap<>(Map.of("imageUuid", image, "networkMode", "bridge")); - for (String field : List.of("command", "entryPoint")) if (body.containsKey(field)) launch.put(field, body.remove(field)); + for (String field : List.of("command", "entryPoint", "ports", "healthCheck")) + if (body.containsKey(field)) launch.put(field, body.remove(field)); body.put("launchConfig", launch); } else { body.put("imageUuid", image); body.put("networkMode", "bridge"); diff --git a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDomain.java b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDomain.java index 5b00823995..79008657bd 100644 --- a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDomain.java +++ b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDomain.java @@ -29,6 +29,7 @@ import io.cattle.platform.object.util.DataAccessor; import io.cattle.platform.object.util.ObjectUtils; import io.cattle.platform.servicediscovery.api.service.ServiceDeploymentReadiness; +import io.cattle.platform.servicediscovery.api.service.ServiceCreateValidation; import io.cattle.platform.servicediscovery.api.service.ResourceArchiveChange; import io.github.ibuildthecloud.gdapi.context.ApiContext; import io.github.ibuildthecloud.gdapi.exception.ClientVisibleException; @@ -64,6 +65,7 @@ private record AliasReference(String type, String publicId, String nativeId) { } @Inject JsonMapper json; @Inject ApiKeyTargetResolver targets; @Inject ServiceDeploymentReadiness deploymentReadiness; + @Inject ServiceCreateValidation serviceCreateValidation; @Inject ResourceArchiveChange archiveChanges; /** This marker only exists on a fresh, server-created domain request. */ @@ -126,6 +128,14 @@ public Map prepare(ApiRequest original, AiResourceChangeDescript descriptor.method(), descriptor.action(), body); return authorized(original, request, () -> { if (boundPlan != null) keys.authorizeBoundDomain(boundPlan, request); + if (descriptor.createsResource() && "service".equals(descriptor.resourceType())) { + Map validated = copyMap(request.getRequestObject()); + if (!(validated.get("launchConfig") instanceof Map launch)) throw error(403, "PermissionDenied"); + for (String field : List.of("ports", "healthCheck")) + if (parameters.containsKey(field) && !Objects.equals(parameters.get(field), launch.get(field))) + throw error(403, "PermissionDenied"); + serviceCreateValidation.validatePorts(request); + } if ("dnsService".equals(descriptor.resourceType()) && Set.of("update_targets", "remove").contains(descriptor.operation())) { Map references = aliasReferenceSnapshot(original, descriptor, publicId, parameters); if ("update_targets".equals(descriptor.operation()) && publicId != null && aliasReferences(original, descriptor, parameters).stream() @@ -385,6 +395,10 @@ public Object receiptTarget(ApiRequest request, AiResourceChangeDescriptor descr if (target == null || !Objects.equals(ObjectUtils.getAccountId(target), actor.getAccountId()) || !Objects.equals(ObjectUtils.getKind(target), descriptor.resourceType()) || current.policy().authorizeObject(target) == null) throw error(403, "PermissionDenied"); + if (descriptor.createsResource() && "service".equals(descriptor.resourceType()) + && (number(ObjectUtils.getId(target)) != number(plan.get("internalResourceId")) + || !(plan.get("createdResourceUuid") instanceof String uuid) || uuid.isBlank() + || !uuid.equals(ObjectUtils.getPropertyIgnoreErrors(target, "uuid")))) throw error(403, "PermissionDenied"); if (ObjectUtils.getRemoved(target) == null) visible(request, descriptor, (String) plan.get("resourceId")); return target; } @@ -413,8 +427,16 @@ public boolean complete(AiResourceChangeDescriptor descriptor, Object target, Ma if ("scale".equals(descriptor.operation())) return "active".equals(state) && Objects.equals(parameters.get("scale"), DataAccessor.fieldInteger(target, "scale")) && serviceReady((Service) target, true) && retiredInstancesSettled((Service) target, before); - if ("create".equals(descriptor.operation())) return "container".equals(descriptor.resourceType()) ? "stopped".equals(state) - : "stack".equals(descriptor.resourceType()) ? Set.of("active", "inactive").contains(state) : "inactive".equals(state); + if ("create".equals(descriptor.operation())) { + if ("service".equals(descriptor.resourceType())) { + Map expected = descriptor.body(parameters); + String image = (String) ((Map) expected.get("launchConfig")).get("imageUuid"); + return target instanceof Service service && AiServiceCreateChange.complete(service, expected, + internalId("stack", (String) parameters.get("stackId")), serviceCreateValidation.imageUuid(image)); + } + return "container".equals(descriptor.resourceType()) ? "stopped".equals(state) + : "stack".equals(descriptor.resourceType()) && Set.of("active", "inactive").contains(state); + } if ("container".equals(descriptor.resourceType())) return "stop".equals(descriptor.operation()) ? "stopped".equals(state) : "running".equals(state) && (!"restart".equals(descriptor.operation()) || number(ObjectUtils.getPropertyIgnoreErrors(target, "startCount")) > number(before.get("startCount"))); @@ -613,7 +635,7 @@ private Object displayed(String field, Object value) { try { value = AiExternalServiceChange.addresses(value); } catch (ClientVisibleException unsafe) { return null; } } - if (!Set.of("command", "entryPoint", "externalIpAddresses", "targetServices").contains(field)) return value; + if (!Set.of("command", "entryPoint", "ports", "healthCheck", "externalIpAddresses", "targetServices").contains(field)) return value; try { return json.writeValueAsString(value); } catch (IOException failure) { throw new IllegalStateException("Resource-change summary unavailable", failure); } } @@ -622,6 +644,13 @@ public T withArchiveChange(AiResourceChangeDescriptor descriptor, Object loc return archiveChanges.withChange(lockedTarget, "archive".equals(descriptor.operation()), change); } + /** Called only after complete() verified this created Service and its native port allocation. */ + public Map serviceCreateResult(Object target) { + Object launch = DataAccessor.field(target, "launchConfig", Object.class); + Object ports = launch instanceof Map map ? map.get("ports") : null; + return Map.of("assignedPorts", ports == null ? List.of() : AiServiceCreateChange.ports(ports)); + } + private Map change(String field, Object before, Object after) { Map change = new LinkedHashMap<>(); change.put("field", field); change.put("before", scalar(before)); change.put("after", scalar(after)); return change; diff --git a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanService.java b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanService.java index 82202e1d85..993efa9317 100644 --- a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanService.java +++ b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanService.java @@ -157,6 +157,12 @@ public Map action(String id, ApiRequest request) { long numericId = createdId instanceof Number numeric ? numeric.longValue() : domain.internalId(descriptor.resourceType(), text(createdId)); if (numericId <= 0) throw new IllegalStateException("Created resource identity unavailable"); + if ("service".equals(descriptor.resourceType())) { + Object uuid = ObjectUtils.getPropertyIgnoreErrors(result, "uuid"); + if (!(uuid instanceof String identity) || identity.isBlank()) + throw new IllegalStateException("Created Service identity unavailable"); + plan.put("createdResourceUuid", uuid); + } plan.put("internalResourceId", numericId); plan.put("resourceId", domain.publicId(descriptor.resourceType(), numericId)); } plan.put("dispatchAcknowledged", true); @@ -205,6 +211,9 @@ protected void observe(Map plan, ApiRequest request) { AiResourceChangeDescriptor descriptor = descriptor(plan); Object target = domain.receiptTarget(request, descriptor, plan); if (domain.complete(descriptor, target, map(plan.get("parameters")), map(plan.get("snapshot")))) { + if (descriptor.createsResource() && "service".equals(descriptor.resourceType()) && map(plan.get("parameters")).containsKey("ports")) { + plan.put("executionResult", domain.serviceCreateResult(target)); + } plan.put("status", "succeeded"); plan.put("verifiedResult", true); plan.put("messageCode", "ResourceChangeCompleted"); eraseSnapshot(plan); } else if (Set.of("error", "erroring").contains(Objects.toString(ObjectUtils.getState(target), ""))) { plan.put("status", "failed"); plan.put("messageCode", "ResourceChangeProcessFailed"); eraseSnapshot(plan); @@ -255,6 +264,8 @@ protected Map output(Map plan) { Map output = new LinkedHashMap<>(); for (String field : List.of("id", "resourceType", "operation", "resourceId", "targetPath", "changes", "confirmationState", "status", "operationId", "verifiedResult", "messageCode", "expiresAt")) output.put(field, plan.get(field)); + output.put("executionResult", "succeeded".equals(plan.get("status")) && Boolean.TRUE.equals(plan.get("verifiedResult")) + ? plan.get("executionResult") : null); output.put("resourceTextIsUntrusted", true); return output; } diff --git a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiServiceCreateChange.java b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiServiceCreateChange.java new file mode 100644 index 0000000000..3e49ef5996 --- /dev/null +++ b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiServiceCreateChange.java @@ -0,0 +1,147 @@ +package io.cattle.platform.iaas.api.auth.change; + +import io.cattle.platform.core.model.Service; +import io.cattle.platform.core.util.PortBindingAddress; +import io.cattle.platform.core.util.PortSpec; +import io.cattle.platform.object.util.DataAccessor; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** The reviewed Service-create subset; raw launch configuration and environment stay outside it. */ +final class AiServiceCreateChange { + static final int MAX_PORTS = 64; + private static final Set HEALTH_FIELDS = Set.of("port", "requestLine", "responseTimeout", "interval", + "healthyThreshold", "unhealthyThreshold", "initializingTimeout", "reinitializingTimeout", "strategy"); + + private AiServiceCreateChange() { } + + static List ports(Object value) { + if (!(value instanceof List values) || values.size() > MAX_PORTS) throw invalid(); + List result = new ArrayList<>(); + for (Object item : values) { + if (!(item instanceof String text) || text.isEmpty() || text.length() > 128 || text.indexOf('\0') >= 0) throw invalid(); + port(text); // Preserve the native parser's specific, safe validation code. + result.add(text); + } + return List.copyOf(result); + } + + static Map healthCheck(Object value) { + if (!(value instanceof Map input) || input.size() > HEALTH_FIELDS.size()) throw invalid(); + Map result = new LinkedHashMap<>(); + for (var entry : input.entrySet()) { + if (!(entry.getKey() instanceof String field) || !HEALTH_FIELDS.contains(field)) throw invalid(); + Object item = entry.getValue(); + if ("requestLine".equals(field)) { + if (!(item instanceof String text) || text.length() > 1024 || text.indexOf('\0') >= 0 + || text.indexOf('\r') >= 0 || text.indexOf('\n') >= 0) throw invalid(); + } else if ("strategy".equals(field)) { + if (!(item instanceof String text) || !Set.of("none", "recreate").contains(text)) throw invalid(); + } else { + int minimum = Set.of("initializingTimeout", "reinitializingTimeout").contains(field) ? 0 : 1; + int maximum = "port".equals(field) ? 65535 : Integer.MAX_VALUE; + if (!(item instanceof Number number) || !Double.isFinite(number.doubleValue()) + || number.doubleValue() != number.longValue() || number.longValue() < minimum + || number.longValue() > maximum) throw invalid(); + item = number.intValue(); + } + result.put(field, item); + } + if (!result.containsKey("port")) throw invalid(); + result.putIfAbsent("strategy", "recreate"); // InstanceHealthCheck's native schema default. + return java.util.Collections.unmodifiableMap(result); + } + + static boolean complete(Service service, Map expected, long stackId, String imageUuid) { + if (!"service".equals(service.getKind()) || service.getRemoved() != null || !"inactive".equals(service.getState()) + || !Objects.equals(service.getName(), expected.get("name")) + || !Objects.equals(service.getDescription(), expected.get("description")) + || !Objects.equals(service.getStackId(), stackId) + || !sameNumber(DataAccessor.field(service, "scale", Object.class), expected.get("scale")) + || !Boolean.FALSE.equals(DataAccessor.field(service, "startOnCreate", Object.class))) return false; + if (!(expected.get("launchConfig") instanceof Map wanted) + || !(DataAccessor.field(service, "launchConfig", Object.class) instanceof Map actual) + || !Objects.equals(imageUuid, actual.get("imageUuid")) || !"bridge".equals(actual.get("networkMode"))) return false; + for (String field : List.of("command", "entryPoint")) { + Object value = actual.get(field), target = wanted.get(field); + if (!Objects.equals(target, value)) return false; + } + try { + return portsMatch(wanted.get("ports"), actual.get("ports")) + && Objects.equals(canonicalHealth(wanted.get("healthCheck")), canonicalHealth(actual.get("healthCheck"))); + } catch (IllegalArgumentException | io.github.ibuildthecloud.gdapi.exception.ClientVisibleException invalidReadback) { + return false; + } + } + + private static boolean sameNumber(Object actual, Object expected) { + return actual instanceof Number first && expected instanceof Number second + && first.doubleValue() == first.longValue() && second.doubleValue() == second.longValue() + && first.longValue() == second.longValue(); + } + + private record BoundPort(String bind, Integer publicPort, int privatePort, String protocol) { } + + private static boolean portsMatch(Object expected, Object actual) { + List wanted = new ArrayList<>(canonicalPorts(expected)); + List remaining = new ArrayList<>(canonicalPorts(actual)); + if (wanted.size() != remaining.size()) return false; + for (int i = 0; i < remaining.size(); i++) for (int j = i + 1; j < remaining.size(); j++) { + BoundPort left = remaining.get(i), right = remaining.get(j); + if (left.publicPort() != null && left.publicPort().equals(right.publicPort()) + && left.protocol().equals(right.protocol()) && PortBindingAddress.overlaps(left.bind(), right.bind())) return false; + } + // ServiceCreate retains fixed mappings first, then allocates every unspecified public port. + // Match fixed entries first so a random request cannot consume a specifically approved mapping. + wanted.sort(java.util.Comparator.comparing(port -> port.publicPort() == null)); + for (BoundPort port : wanted) { + int match = -1; + for (int i = 0; i < remaining.size(); i++) { + BoundPort candidate = remaining.get(i); + if (candidate.publicPort() != null && candidate.publicPort() >= 1 && candidate.publicPort() <= 65535 + && (port.publicPort() == null || port.publicPort().equals(candidate.publicPort())) + && port.privatePort() == candidate.privatePort() && port.protocol().equals(candidate.protocol()) + && port.bind().equals(candidate.bind())) { match = i; break; } + } + if (match < 0) return false; + remaining.remove(match); + } + return remaining.isEmpty(); + } + + private static List canonicalPorts(Object value) { + if (value == null) return List.of(); + List result = new ArrayList<>(); + for (String text : ports(value)) { + PortSpec spec = port(text); + result.add(new BoundPort(PortBindingAddress.normalize(spec.getIpAddress()), spec.getPublicPort(), + spec.getPrivatePort(), spec.getProtocol())); + } + return result; + } + + private static Map canonicalHealth(Object value) { + if (value == null) return null; + if (!(value instanceof Map input)) throw invalid(); + Map fields = new LinkedHashMap<>(); + for (var entry : input.entrySet()) { + if (!(entry.getKey() instanceof String key)) throw invalid(); + if ("type".equals(key) && "instanceHealthCheck".equals(entry.getValue())) continue; + if (entry.getValue() != null) fields.put(key, entry.getValue()); + } + return healthCheck(fields); + } + + private static PortSpec port(String text) { + try { return new PortSpec(text); } + catch (NumberFormatException invalidNativeNumber) { throw AiResourceChangeDescriptor.error(PortSpec.WRONG_FORMAT); } + } + + private static io.github.ibuildthecloud.gdapi.exception.ClientVisibleException invalid() { + return AiResourceChangeDescriptor.error("ResourceChangeInputInvalid"); + } +} diff --git a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchMaterializerTest.java b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchMaterializerTest.java index e406f621d0..f999f2afca 100644 --- a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchMaterializerTest.java +++ b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchMaterializerTest.java @@ -5,20 +5,38 @@ import static org.mockito.Mockito.*; import io.cattle.platform.api.auth.Policy; +import io.cattle.platform.api.formatter.DefaultIdFormatter; +import io.cattle.platform.core.addon.LogConfig; import io.cattle.platform.core.model.Account; +import io.cattle.platform.core.model.Instance; import io.cattle.platform.core.model.tables.records.AccountRecord; +import io.cattle.platform.core.model.tables.records.InstanceRecord; import io.cattle.platform.core.model.tables.records.SecretRecord; import io.cattle.platform.docker.transform.DockerTransformer; +import io.cattle.platform.docker.transform.DockerTransformerImpl; import io.cattle.platform.iaas.api.auth.impl.ApiAuthenticator; import io.cattle.platform.json.JacksonJsonMapper; import io.cattle.platform.object.ObjectManager; +import io.cattle.platform.object.util.DataAccessor; +import io.cattle.platform.object.util.DataUtils; +import io.cattle.platform.schema.processor.AuthOverlayPostProcessor; +import io.cattle.platform.schema.processor.JpaWritablePostProcessor; +import io.cattle.platform.schema.processor.JsonFileOverlayPostProcessor; +import io.cattle.platform.schema.processor.SchemaOverlayImpl; +import io.cattle.platform.schema.processor.StripSuffixPostProcessor; import io.github.ibuildthecloud.gdapi.context.ApiContext; import io.github.ibuildthecloud.gdapi.factory.SchemaFactory; +import io.github.ibuildthecloud.gdapi.factory.impl.SchemaFactoryImpl; +import io.github.ibuildthecloud.gdapi.factory.impl.SubSchemaFactory; import io.github.ibuildthecloud.gdapi.id.IdFormatter; +import io.github.ibuildthecloud.gdapi.json.JacksonMapper; +import io.github.ibuildthecloud.gdapi.model.impl.FieldImpl; import io.github.ibuildthecloud.gdapi.model.impl.SchemaImpl; import io.github.ibuildthecloud.gdapi.request.ApiRequest; import io.github.ibuildthecloud.gdapi.request.resource.ResourceManager; import java.util.ArrayList; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -89,4 +107,149 @@ public class AiCatalogLaunchMaterializerTest { assertThrows(UnsupportedOperationException.class, () -> prepared.authorizedReferenceIds().put("other", "1se8")); assertThrows(UnsupportedOperationException.class, () -> prepared.nativeGraph().put("resolvedSecrets", Map.of())); } + + @Test public void realTransformerDynamicFieldsReachNativeRoleSchemaProjection() throws Exception { + var fixture = new ProjectionFixture(false); + Map projected = fixture.prepare(); + assertEquals("docker:busybox:1.37.0", projected.get("imageUuid")); + assertEquals(Map.of("PUBLIC_TEXT", "one=two"), projected.get("environment")); + assertEquals(List.of("18080:80/tcp"), projected.get("ports")); + assertEquals(Map.of("driver", "json-file", "config", Map.of("max-size", "10m")), projected.get("logConfig")); + assertEquals("app", projected.get("name")); + assertFalse(projected.containsKey("notInNativeSchema")); + assertSame(fixture.schemas, fixture.authority.current().schemas()); + // Native bean accessors remain real; the four dynamic fields have no + // InstanceRecord getter and must come from the data.fields value source. + var fields = fixture.schemas.getSchema("container").getResourceFields(); + assertNotNull(((FieldImpl) fields.get("name")).getReadMethod()); + for (String field : List.of("imageUuid", "environment", "ports", "logConfig")) { + assertNull(field, ((FieldImpl) fields.get(field)).getReadMethod()); + assertNotNull(field, DataUtils.getFields(fixture.transformer.instance).get(field)); + } + // WrappedResource consumes only the fresh copy, never InstanceRecord's map. + assertEquals(Map.of("PUBLIC_TEXT", "one=two"), DataUtils.getFields(fixture.transformer.instance).get("environment")); + } + + @Test public void dynamicProjectionKeepsRestrictedSchemaAndNonCreateResponseRules() throws Exception { + var fixture = new ProjectionFixture(true); + Map projected = fixture.prepare(); + assertEquals("docker:busybox:1.37.0", projected.get("imageUuid")); + assertEquals("app", projected.get("name")); + assertFalse("A hidden schema field cannot reappear from additionalFields", projected.containsKey("environment")); + assertNull("POST alone must not disclose a read-on-create-only field", projected.get("ports")); + assertNull(projected.get("logConfig")); + assertFalse(projected.containsKey("notInNativeSchema")); + assertEquals(Map.of("PUBLIC_TEXT", "one=two"), DataUtils.getFields(fixture.transformer.instance).get("environment")); + assertNotNull(DataUtils.getFields(fixture.transformer.instance).get("ports")); + assertNotNull(DataUtils.getFields(fixture.transformer.instance).get("logConfig")); + } + + private static final class RecordingTransformer extends DockerTransformerImpl { + InstanceRecord instance; + @Override public void transform(Map inspect, Instance target) { + super.transform(inspect, target); + instance = (InstanceRecord) target; + DataAccessor.setField(target, "notInNativeSchema", "MUST_NOT_ESCAPE_SCHEMA"); + } + } + + private static final class ProjectionFixture { + final JacksonJsonMapper json = new JacksonJsonMapper(); + final SchemaFactory schemas; + final AiCatalogLaunchAccess.Authorization authority; + final RecordingTransformer transformer = new RecordingTransformer(); + final AiCatalogLaunchMaterializer materializer = new AiCatalogLaunchMaterializer(); + Map projection; + + ProjectionFixture(boolean restrict) throws Exception { + schemas = nativeSchemas(json); + if (restrict) { + // Narrow a copy of the real role schema, retaining its real + // accessors. These controls must still win over dynamic data. + var fields = schemas.getSchema("container").getResourceFields(); + ((FieldImpl) fields.get("environment")).setIncludeInList(false); + ((FieldImpl) fields.get("ports")).setReadOnCreateOnly(true); + ((FieldImpl) fields.get("logConfig")).setReadOnCreateOnly(true); + } + Policy policy = mock(Policy.class); when(policy.getAccountId()).thenReturn(5L); + authority = new AiCatalogLaunchAccess.Authorization(new ApiAuthenticator.CurrentAuthorization(policy, schemas), null); + var formatter = new DefaultIdFormatter(); formatter.setSchemaFactory(schemas); + formatter.setTypeMappings(Map.of("account", "a", "instance", "i", "stack", "st", "service", "s")); + ApiContext.newContext().setIdFormatter(formatter); + ObjectManager objects = mock(ObjectManager.class); + AccountRecord project = new AccountRecord(); project.setId(5L); project.setName("Default"); + when(objects.loadResource(Account.class, 5L)).thenReturn(project); + materializer.access = new AiCatalogLaunchAccess() { + @Override void preflightChild(ApiRequest original, Authorization selected, Map plan, + String type, Map body) { assertSame(authority, selected); assertEquals("service", type); } + @Override void childActions(Authorization selected, String type, boolean starts) { + assertSame(authority, selected); assertEquals("service", type); assertFalse(starts); + } + }; + materializer.access.objects = objects; + var mapper = DockerTransformerImpl.class.getDeclaredField("jsonMapper"); + mapper.setAccessible(true); mapper.set(transformer, json); + materializer.transformer = transformer; + materializer.graphs = new AiCatalogLaunchGraph(); materializer.graphs.json = json; + materializer.runner = mock(AiCatalogLaunchRunner.class); + Map inspect = Map.of("name", "app", "dockerInspect", Map.of("Name", "app", + "Config", Map.of("Image", "busybox:1.37.0", "Env", List.of("PUBLIC_TEXT=one=two"), + "ExposedPorts", Map.of("80/tcp", Map.of())), + "HostConfig", Map.of("NetworkMode", "bridge", + "PortBindings", Map.of("80/tcp", List.of(Map.of("HostIp", "", "HostPort", "18080"))), + "LogConfig", Map.of("Type", "json-file", "Config", Map.of("max-size", "10m"))))); + when(materializer.runner.run(anyMap())).thenAnswer(call -> { + Map input = call.getArgument(0); + if (!"finalize".equals(input.get("phase"))) return Map.of("services", List.of(inspect)); + Map configs = AiCatalogLaunchInput.map(input.get("transformedLaunchConfigs")); + projection = json.readValue(json.writeValueAsString(configs.get("app"))); + return Map.of("phase", "finalize", "services", List.of(inspect), "serviceRequests", List.of(Map.of( + "name", "app", "resourceType", "service", "createBody", Map.of("name", "app", "stackId", "$createdStack"), + "setServiceLinks", Map.of("serviceLinks", List.of()))), "volumeRequests", List.of(), + "dockerCompose", "services: {}", "rancherCompose", "", "variables", Map.of()); + }); + } + + Map prepare() { + ApiRequest request = new ApiRequest(null, schemas); request.setMethod("POST"); + materializer.prepare(request, authority, Map.of("name", "chosen", "startOnCreate", false), + Map.of("files", Map.of("docker-compose.yml", "services: {}")), Map.of()); + assertNotNull(projection); + return projection; + } + } + + private static SchemaFactory nativeSchemas(JacksonJsonMapper json) throws Exception { + Path root = Path.of("").toAbsolutePath(); + while (root != null && !Files.isDirectory(root.resolve("resources/content/schema"))) root = root.getParent(); + assertNotNull("Native source schema definitions are required", root); + final Path definitions = root.resolve("resources/content/schema"); + var schemaJson = new JacksonMapper(); schemaJson.init(); + // Same native field/parent construction and overlay implementation as + // CoreSchemaFactory. Only source-tree URL discovery is local to this test. + var base = new JsonFileOverlayPostProcessor() { + @Override public SchemaImpl postProcessRegister(SchemaImpl schema, SchemaFactory factory) { return schema; } + @Override public SchemaImpl postProcess(SchemaImpl schema, SchemaFactory factory) { + Path file = definitions.resolve("base/"+schema.getId()+".json"); + if (!Files.isRegularFile(file)) return schema; + try { + byte[] content = Files.readAllBytes(file); + processSchema(schema, schemaJson.readValue(content, SchemaOverlayImpl.class), json.readValue(content)); + return schema; + } catch (Exception failure) { throw new IllegalStateException(failure); } + } + }; + var suffix = new StripSuffixPostProcessor(); suffix.setSuffix("Record"); + var core = new SchemaFactoryImpl(); core.setTypes(List.of(InstanceRecord.class, LogConfig.class)); + core.setTypeNames(List.of("container,parent=instance")); + core.setPostProcessors(List.of(suffix, new JpaWritablePostProcessor(), base)); core.init(); + var role = new AuthOverlayPostProcessor(); role.setJsonMapper(json); + role.setResources(List.of(definitions.resolve("project/project-auth.json").toUri().toURL())); role.init(); + var selected = new SubSchemaFactory(); selected.setId("native-project-projection-test"); selected.setSchemaFactory(core); + selected.setPostProcessors(List.of(role)); selected.init(); + assertNotNull(selected.getSchema("container")); + for (String field : List.of("imageUuid", "environment", "ports", "logConfig", "name")) + assertNotNull(field, selected.getSchema("container").getResourceFields().get(field)); + return selected; + } } diff --git a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDescriptorTest.java b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDescriptorTest.java index c443841d19..c3713f0e1a 100644 --- a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDescriptorTest.java +++ b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDescriptorTest.java @@ -96,6 +96,64 @@ public class AiResourceChangeDescriptorTest { assertEquals(command, http.readValue(((String) wireCommand.get("after")).getBytes(java.nio.charset.StandardCharsets.UTF_8), ArrayList.class)); } + @Test public void serviceCreatePortsAndHealthCheckUseOnlyTheReviewedNativeLaunchFields() throws Exception { + var descriptor = AiResourceChangeDescriptor.require("service", "create"); + var input = new LinkedHashMap(Map.of("name", "probe", "image", "busybox", "stackId", "1st3", + "ports", List.of("18080:80/tcp", "15353:53/udp"), "healthCheck", Map.of("port", 80, "requestLine", "GET / HTTP/1.0", + "interval", 2000, "healthyThreshold", 2))); + var parameters = descriptor.parameters(input); var body = descriptor.body(parameters); + Map launch = (Map) body.get("launchConfig"); + assertEquals(input.get("ports"), launch.get("ports")); + assertEquals(Map.of("port", 80, "requestLine", "GET / HTTP/1.0", "interval", 2000, "healthyThreshold", 2, + "strategy", "recreate"), launch.get("healthCheck")); + assertFalse(body.containsKey("ports")); assertFalse(body.containsKey("healthCheck")); + assertEquals(false, body.get("startOnCreate")); assertEquals("bridge", launch.get("networkMode")); + var domain = new AiResourceChangeDomain(); domain.json = new JacksonJsonMapper(); + var changes = domain.changes(descriptor, null, parameters); + for (String field : List.of("ports", "healthCheck")) { + Object summary = changes.stream().filter(change -> field.equals(change.get("field"))).findFirst().orElseThrow().get("after"); + Object decoded = new JacksonJsonMapper().readValue((String) summary, Object.class); + assertEquals(parameters.get(field), decoded); + } + for (String type : List.of("container", "stack", "externalService", "dnsService")) + for (String field : List.of("ports", "healthCheck")) { + var unrelated = new LinkedHashMap(Map.of("name", "probe", "image", "busybox", "stackId", "1st3")); + unrelated.put(field, input.get(field)); + assertCode("ResourceChangeInputInvalid", () -> AiResourceChangeDescriptor.require(type, "create").parameters(unrelated)); + } + for (String field : List.of("environment", "launchConfig", "startOnCreate", "ports", "healthCheck")) + assertCode("ResourceChangeInputInvalid", () -> AiResourceChangeDescriptor.require("service", "update_metadata") + .parameters(Map.of(field, "forbidden"))); + input.put("environment", Map.of("TOKEN", "must-never-enter-plan")); + assertCode("ResourceChangeInputInvalid", () -> descriptor.parameters(input)); + } + + @Test public void servicePortAndHealthInputsRejectUnknownTypesAndExcessiveValuesAndKeepNativePortCodes() { + for (Object invalid : List.of("80:80", List.of(80), List.of(""), java.util.Collections.nCopies(65, "18080:80"), + List.of("1".repeat(129)))) assertCode("ResourceChangeInputInvalid", () -> AiServiceCreateChange.ports(invalid)); + assertCode("PortInvalidPrivatePort", () -> AiServiceCreateChange.ports(List.of("18080:65536"))); + assertCode("PortInvalidPublicPort", () -> AiServiceCreateChange.ports(List.of("65536:80"))); + assertCode("PortInvalidProtocol", () -> AiServiceCreateChange.ports(List.of("18080:80/sctp"))); + assertCode("PortWrongFormat", () -> AiServiceCreateChange.ports(List.of("9999999999999999999999:80"))); + for (Object invalid : List.of("80", Map.of(), Map.of("port", "80"), Map.of("port", 1.5), Map.of("port", 0), + Map.of("port", 65536), Map.of("port", 80, "unknown", true), Map.of("port", 80, "strategy", "recreateOnQuorum"), + Map.of("port", 80, "requestLine", "GET /\r\nX: injected"), Map.of("port", 80, "requestLine", "x".repeat(1025)))) + assertCode("ResourceChangeInputInvalid", () -> AiServiceCreateChange.healthCheck(invalid)); + for (String field : List.of("interval", "responseTimeout", "healthyThreshold", "unhealthyThreshold")) { + assertCode("ResourceChangeInputInvalid", () -> AiServiceCreateChange.healthCheck(Map.of("port", 80, field, 0))); + assertEquals(1, AiServiceCreateChange.healthCheck(Map.of("port", 80, field, 1)).get(field)); + } + for (String field : List.of("interval", "responseTimeout", "healthyThreshold", "unhealthyThreshold", "initializingTimeout", "reinitializingTimeout")) { + assertCode("ResourceChangeInputInvalid", () -> AiServiceCreateChange.healthCheck(Map.of("port", 80, field, -1))); + assertCode("ResourceChangeInputInvalid", () -> AiServiceCreateChange.healthCheck(Map.of("port", 80, field, 2147483648L))); + assertEquals(Integer.MAX_VALUE, AiServiceCreateChange.healthCheck(Map.of("port", 65535, field, 2147483647L)).get(field)); + } + for (String field : List.of("initializingTimeout", "reinitializingTimeout")) + assertEquals(0, AiServiceCreateChange.healthCheck(Map.of("port", 1, field, 0)).get(field)); + var nullable = new LinkedHashMap(); nullable.put("port", 80); nullable.put("strategy", null); + assertCode("ResourceChangeInputInvalid", () -> AiServiceCreateChange.healthCheck(nullable)); + } + private static void assertCode(String code, Runnable action) { assertEquals(code, assertThrows(ClientVisibleException.class, action::run).getCode()); } diff --git a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanServiceTest.java b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanServiceTest.java index c1f5d8801e..587a94497b 100644 --- a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanServiceTest.java +++ b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanServiceTest.java @@ -185,6 +185,68 @@ public class AiResourceChangePlanServiceTest { f.complete = true; assertEquals(true, f.service.get(f.id, f.request(null, false)).get("verifiedResult")); } + @Test public void serviceExecutionEvidenceIsPublishedOnlyAfterActualReadbackAndNeverChangesTheApprovedDiff() { + Fixture f = new Fixture(); f.previewServiceCreate(false); f.confirm(); + f.target.setUuid("created-service"); f.target.setStackId(3L); f.target.setState("inactive"); + Map launch = new LinkedHashMap<>(Map.of("imageUuid", "docker:busybox:1.37.0", "networkMode", "bridge", + "ports", List.of("31001:80/tcp"), "healthCheck", Map.of("port", 80, "strategy", "recreate"))); + f.target.setDescription(null); f.target.setData(Map.of("fields", Map.of("launchConfig", launch, "scale", 1L, "startOnCreate", false))); + doAnswer(call -> { f.dispatched++; return f.target; }).when(f.domain).dispatch(any(), any(), isNull(), anyMap(), anyString()); + when(f.domain.complete(any(), any(), anyMap(), anyMap())).thenAnswer(call -> AiServiceCreateChange.complete(f.target, + ((AiResourceChangeDescriptor) call.getArgument(0)).body(call.getArgument(2)), 3L, "docker:busybox:1.37.0")); + when(f.domain.serviceCreateResult(any())).thenCallRealMethod(); + Object approvedChanges = f.documents.get(f.id).get("changes"); + assertNull(f.apply(false).get("executionResult")); + assertEquals("created-service", f.documents.get(f.id).get("createdResourceUuid")); + launch.put("ports", List.of("31001:81/tcp")); + var running = f.service.get(f.id, f.request(null, false)); + assertEquals("running", running.get("status")); assertNull(running.get("executionResult")); + launch.put("ports", List.of("31001:80/tcp")); + var completed = f.service.get(f.id, f.request(null, false)); + assertEquals("succeeded", completed.get("status")); assertEquals(true, completed.get("verifiedResult")); + assertEquals(Map.of("assignedPorts", List.of("31001:80/tcp")), completed.get("executionResult")); + assertEquals(approvedChanges, completed.get("changes")); assertEquals(1, f.claims); assertEquals(1, f.dispatched); + f.apply(false); assertEquals(1, f.dispatched); + f.documents.get(f.id).put(AiResourceChangePlanStore.PRIVATE_FAILURE, AiResourceChangePlanStore.AUTHORIZATION_DENIED); + var failed = f.service.get(f.id, f.request(null, false)); + assertEquals("failed", failed.get("status")); assertEquals(false, failed.get("verifiedResult")); assertNull(failed.get("executionResult")); + } + + @Test public void serviceLostCreateResponseOrMissingUuidRetainsOneUnknownClaimAndNoExecutionEvidence() { + for (boolean missingUuid : List.of(false, true)) { + Fixture f = new Fixture(); f.previewServiceCreate(false); f.confirm(); + doAnswer(call -> { + f.dispatched++; + if (!missingUuid) throw new IllegalStateException("native create succeeded but response was lost"); + return f.target; + }).when(f.domain).dispatch(any(), any(), isNull(), anyMap(), anyString()); + var unknown = f.apply(false); assertEquals("unknown", unknown.get("status")); + assertNull(unknown.get("executionResult")); + assertNull(f.documents.get(f.id).get("internalResourceId")); + assertEquals("unknown", f.apply(false).get("status")); + assertEquals("unknown", f.service.get(f.id, f.request(null, false)).get("status")); + assertEquals(1, f.claims); assertEquals(1, f.dispatched); + verify(f.domain, never()).complete(any(), any(), anyMap(), anyMap()); + } + } + + @Test public void successfulServiceDispatchWithLostReceiptSaveKeepsTheDurableClaimUnknownAndCannotReplay() { + Fixture f = new Fixture(); f.previewServiceCreate(false); f.confirm(); f.target.setUuid("created-service"); + doAnswer(call -> { f.dispatched++; return f.target; }).when(f.domain).dispatch(any(), any(), isNull(), anyMap(), anyString()); + var writes = spy(f.service.store); f.service.store = writes; + doAnswer(call -> { + Map plan = call.getArgument(0); + if ("accepted".equals(plan.get("status"))) throw new IllegalStateException("receipt write disconnected"); + return call.callRealMethod(); + }).when(writes).save(anyMap()); + assertThrows(IllegalStateException.class, () -> f.apply(false)); + assertEquals("dispatching", f.documents.get(f.id).get("status")); + assertEquals("unknown", f.apply(false).get("status")); + assertEquals("unknown", f.service.get(f.id, f.request(null, false)).get("status")); + assertNull(f.documents.get(f.id).get("executionResult")); assertEquals(1, f.claims); assertEquals(1, f.dispatched); + verify(f.domain, never()).complete(any(), any(), anyMap(), anyMap()); + } + @Test public void featureDisabledIsRejectedBeforeAnyPersistenceOrDomainRead() { Object previous = io.cattle.platform.archaius.util.ArchaiusUtil.getConfiguration().getProperty("api.ai.resource.change.enabled"); try { @@ -327,6 +389,16 @@ Map preview(boolean key) { Map result = service.preview(request); id = (String) result.get("id"); return result; } void confirm() { service.action(id, request("confirm", false)); } + void previewServiceCreate(boolean key) { previewServiceCreate(key, List.of("80")); } + void previewServiceCreate(boolean key, List ports) { + ServiceRecord parent = new ServiceRecord(); parent.setId(3L); parent.setKind("stack"); parent.setName("Default"); parent.setState("active"); + when(domain.parent(any(), any(), anyMap())).thenReturn(parent); + when(domain.lock(eq("stack"), anyLong())).thenReturn(parent); + ApiRequest preview = request(null, key); preview.setId(null); + preview.setRequestObject(Map.of("resourceType", "service", "operation", "create", "parameters", + Map.of("name", "probe", "image", "busybox:1.37.0", "stackId", "1st3", "ports", ports, "healthCheck", Map.of("port", 80)))); + id = (String) service.preview(preview).get("id"); + } Map previewArchive(String operation) { ApiRequest preview = request(null, false); preview.setId(null); preview.setRequestObject(Map.of("resourceType", "service", "operation", operation, "resourceId", "1s7", "parameters", Map.of())); diff --git a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeReceiptAuthorizationTest.java b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeReceiptAuthorizationTest.java index dbc4b7f52c..a44d717962 100644 --- a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeReceiptAuthorizationTest.java +++ b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeReceiptAuthorizationTest.java @@ -109,6 +109,26 @@ public class AiResourceChangeReceiptAuthorizationTest { verifyNoInteractions(f.manager); } + @Test public void createdServiceReceiptsBindTheActualIdUuidKindAndOwnerBeforeReadback() throws Exception { + Fixture f = new Fixture(); var descriptor = AiResourceChangeDescriptor.require("service", "create"); + ServiceRecord service = (ServiceRecord) f.objects.loadResource("service", 890L); + service.setUuid("created-service"); service.setState("inactive"); + Map plan = new LinkedHashMap<>(Map.of("internalResourceId", 890L, "resourceId", "1s890", + "createdResourceUuid", "created-service")); + assertSame(service, f.domain.receiptTarget(f.request, descriptor, plan)); + plan.remove("createdResourceUuid"); + assertEquals("PermissionDenied", rejected(() -> f.domain.receiptTarget(f.request, descriptor, plan))); + plan.put("createdResourceUuid", "created-service"); service.setUuid("replacement-service"); + assertEquals("PermissionDenied", rejected(() -> f.domain.receiptTarget(f.request, descriptor, plan))); + service.setUuid(null); assertEquals("PermissionDenied", rejected(() -> f.domain.receiptTarget(f.request, descriptor, plan))); + service.setUuid("created-service"); service.setId(891L); + assertEquals("PermissionDenied", rejected(() -> f.domain.receiptTarget(f.request, descriptor, plan))); + service.setId(890L); service.setAccountId(99L); + assertEquals("PermissionDenied", rejected(() -> f.domain.receiptTarget(f.request, descriptor, plan))); + service.setAccountId(5L); service.setKind("externalService"); + assertEquals("PermissionDenied", rejected(() -> f.domain.receiptTarget(f.request, descriptor, plan))); + } + private static String rejected(Runnable action) { return assertThrows(ClientVisibleException.class, action::run).getCode(); } @Test public void terminalFailureProjectionStillRequiresTheActualLiveReceiptGuards() throws Exception { Fixture f = new Fixture(); String id = "aichange-11111111-1111-1111-1111-111111111111"; diff --git a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiServiceCreateChangeTest.java b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiServiceCreateChangeTest.java new file mode 100644 index 0000000000..20860468e6 --- /dev/null +++ b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiServiceCreateChangeTest.java @@ -0,0 +1,137 @@ +package io.cattle.platform.iaas.api.auth.change; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; +import io.cattle.platform.core.model.tables.records.ServiceRecord; +import io.cattle.platform.core.util.PortSpec; +import io.cattle.platform.iaas.api.auth.apikey.ApiKeyTargetResolver; +import io.cattle.platform.json.JacksonJsonMapper; +import io.cattle.platform.object.impl.JooqObjectManager; +import io.cattle.platform.object.util.DataAccessor; +import io.cattle.platform.servicediscovery.api.filter.ServiceValidationFilter; +import io.cattle.platform.storage.service.StorageService; +import io.github.ibuildthecloud.gdapi.model.impl.FieldImpl; +import io.github.ibuildthecloud.gdapi.model.impl.SchemaImpl; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.Test; + +public class AiServiceCreateChangeTest { + @Test public void nativeObjectMappingKeepsStartOnCreateInThePersistedServiceDataField() { + var schema = new SchemaImpl(); schema.setResourceFields(Map.of("startOnCreate", new FieldImpl())); + var service = new ServiceRecord(); + var nativeObjects = new JooqObjectManager() { + void mapCreateField() { setField(schema, service, "startOnCreate", false); } + }; + nativeObjects.mapCreateField(); + assertEquals(false, DataAccessor.field(service, "startOnCreate", Object.class)); + assertEquals(false, ((Map) service.getData().get("fields")).get("startOnCreate")); + } + + @Test public void actualNativeReadbackAcceptsAllocatedPortsAndNativeNormalizationWithoutUsingTheRequestBodyAsEvidence() throws Exception { + Fixture f = new Fixture(); + assertTrue(f.complete()); + var assigned = f.domain.serviceCreateResult(f.service); + assertEquals(List.of("18080:80/tcp", "127.0.0.1:31001:80/tcp", "31002:80/udp"), + assigned.get("assignedPorts")); + // The approved input remains the request for native allocation, rather than the observed assigned values. + assertEquals(List.of("127.0.0.1::80/tcp", "18080:080", "80/udp"), f.parameters.get("ports")); + } + + @Test public void anyDifferentPrivateProtocolIpFixedPublicMissingOrDuplicateMappingCannotComplete() throws Exception { + for (List ports : List.of( + List.of("18080:81/tcp", "127.0.0.1:31001:80/tcp", "31002:80/udp"), + List.of("18080:80/udp", "127.0.0.1:31001:80/tcp", "31002:80/udp"), + List.of("18080:80/tcp", "127.0.0.2:31001:80/tcp", "31002:80/udp"), + List.of("18081:80/tcp", "127.0.0.1:31001:80/tcp", "31002:80/udp"), + List.of("18080:80/tcp", "127.0.0.1:31001:80/tcp"), + List.of("18080:80/tcp", "127.0.0.1:31001:80/tcp", "127.0.0.1:31001:80/tcp"), + List.of("18080:80/tcp", "127.0.0.1:80/tcp", "31002:80/udp"), + List.of("18080:80/tcp", "127.0.0.1:0:80/tcp", "31002:80/udp"))) { + Fixture f = new Fixture(); f.launch.put("ports", ports); assertFalse(ports.toString(), f.complete()); + } + Fixture missing = new Fixture(); missing.launch.remove("ports"); assertFalse(missing.complete()); + Fixture extra = new Fixture(); + List ports = new ArrayList<>((List) extra.launch.get("ports")); ports.add("32000:9000/tcp"); + extra.launch.put("ports", ports); assertFalse(extra.complete()); + } + + @Test public void fixedMappingsAreMatchedBeforeRandomRequestsForTheSamePrivatePort() throws Exception { + Fixture f = new Fixture(); + f.parameters.put("ports", List.of("80", "18080:80")); + f.launch.put("ports", List.of("18080:80/tcp", "31001:80/tcp")); assertTrue(f.complete()); + f.launch.put("ports", List.of("31001:80/tcp", "31002:80/tcp")); assertFalse(f.complete()); + f.parameters.put("ports", List.of("80", "80")); + f.launch.put("ports", List.of("31001:80/tcp")); assertFalse(f.complete()); + f.launch.put("ports", List.of("31001:80/tcp", "31001:80/tcp")); assertFalse(f.complete()); + f.launch.put("ports", List.of("31001:80/tcp", "31002:80/tcp")); assertTrue(f.complete()); + } + + @Test public void ipv6ReadbackUsesTheNativeBracketedFixedAndAllocatedPortRepresentation() throws Exception { + Fixture f = new Fixture(); + f.parameters.put("ports", List.of("[::1]:18080:80", "[2001:db8::1]:53/udp")); + PortSpec fixed = new PortSpec("[::1]:18080:80"); + PortSpec allocated = new PortSpec("[2001:db8::1]:53/udp"); allocated.setPublicPort(31001); + f.launch.put("ports", List.of(fixed.toSpec(), allocated.toSpec())); + assertEquals(List.of("[::1]:18080:80/tcp", "[2001:db8::1]:31001:53/udp"), f.launch.get("ports")); + assertTrue(f.complete()); + f.launch.put("ports", List.of("[::1]:18080:80/tcp", "[2001:db8::2]:31001:53/udp")); assertFalse(f.complete()); + } + + @Test public void healthThresholdPortRequestStrategyAndUnexpectedConfigurationMustMatch() throws Exception { + for (var entry : Map.of("port", 81, "healthyThreshold", 3, "interval", 3000, + "requestLine", "GET /changed HTTP/1.0", "strategy", "none").entrySet()) { + Fixture f = new Fixture(); f.health.put(entry.getKey(), entry.getValue()); assertFalse(entry.getKey(), f.complete()); + } + Fixture missing = new Fixture(); missing.launch.remove("healthCheck"); assertFalse(missing.complete()); + Fixture unexpected = new Fixture(); unexpected.health.put("recreateOnQuorumStrategyConfig", Map.of("quorum", 1)); + assertFalse(unexpected.complete()); + Fixture nativeDefault = new Fixture(); nativeDefault.health.remove("strategy"); assertTrue(nativeDefault.complete()); + } + + @Test public void nativeInactiveStateAloneCannotProveImageParentProgramsOrMetadataWereCreatedCorrectly() throws Exception { + Fixture image = new Fixture(); image.launch.put("imageUuid", "docker:registry.test/team/other:1"); assertFalse(image.complete()); + Fixture stack = new Fixture(); stack.service.setStackId(4L); assertFalse(stack.complete()); + Fixture command = new Fixture(); command.launch.put("command", List.of("wrong")); assertFalse(command.complete()); + Fixture entryPoint = new Fixture(); entryPoint.launch.put("entryPoint", List.of("/wrong")); assertFalse(entryPoint.complete()); + Fixture name = new Fixture(); name.service.setName("other"); assertFalse(name.complete()); + Fixture description = new Fixture(); description.service.setDescription("other"); assertFalse(description.complete()); + Fixture network = new Fixture(); network.launch.put("networkMode", "host"); assertFalse(network.complete()); + Fixture scale = new Fixture(); scale.fields.put("scale", 2L); assertFalse(scale.complete()); + Fixture started = new Fixture(); started.fields.put("startOnCreate", true); assertFalse(started.complete()); + Fixture active = new Fixture(); active.service.setState("active"); assertFalse(active.complete()); + } + + private static class Fixture { + final AiResourceChangeDomain domain = new AiResourceChangeDomain(); + final AiResourceChangeDescriptor descriptor = AiResourceChangeDescriptor.require("service", "create"); + final ServiceRecord service = new ServiceRecord(); + final Map fields = new LinkedHashMap<>(), launch = new LinkedHashMap<>(), health = new LinkedHashMap<>(); + final Map parameters; + Fixture() throws Exception { + parameters = new LinkedHashMap<>(descriptor.parameters(Map.of("name", "probe", "image", "registry.test/team/app:1", "stackId", "1st3", + "command", List.of("serve", "--foreground"), "entryPoint", List.of("/app"), + "ports", List.of("127.0.0.1::80/tcp", "18080:080", "80/udp"), + "healthCheck", Map.of("port", 80, "interval", 2000, "healthyThreshold", 2, "requestLine", "GET / HTTP/1.0")))); + // A native record independently represents the persisted output of ServiceCreate.setPorts: + // fixed mapping first (PortSpec.toSpec), then the allocated public ports, preserving private/protocol/IP. + service.setId(91L); service.setUuid("created-service-uuid"); service.setAccountId(77L); service.setKind("service"); + service.setName("probe"); service.setStackId(3L); service.setState("inactive"); + launch.put("imageUuid", "docker:registry.test/team/app:1"); launch.put("networkMode", "bridge"); + launch.put("command", List.of("serve", "--foreground")); launch.put("entryPoint", List.of("/app")); + launch.put("ports", List.of("18080:80/tcp", "127.0.0.1:31001:80/tcp", "31002:80/udp")); + health.put("port", 80L); health.put("interval", 2000L); health.put("healthyThreshold", 2L); + health.put("requestLine", "GET / HTTP/1.0"); health.put("strategy", "recreate"); launch.put("healthCheck", health); + fields.put("scale", 1L); fields.put("startOnCreate", false); fields.put("launchConfig", launch); + service.setData(Map.of("fields", fields)); + domain.targets = mock(ApiKeyTargetResolver.class); when(domain.targets.parseScopeId("stack", "1st3")).thenReturn(3L); + var nativeFilter = new ServiceValidationFilter(); StorageService storage = mock(StorageService.class); + when(storage.isValidUUID(anyString())).thenReturn(true); + var injection = ServiceValidationFilter.class.getDeclaredField("storageService"); injection.setAccessible(true); injection.set(nativeFilter, storage); + domain.serviceCreateValidation = nativeFilter; domain.json = new JacksonJsonMapper(); + } + boolean complete() { return domain.complete(descriptor, service, parameters, Map.of()); } + } +} diff --git a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiServiceCreateNativePreflightTest.java b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiServiceCreateNativePreflightTest.java new file mode 100644 index 0000000000..d27e28b51c --- /dev/null +++ b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiServiceCreateNativePreflightTest.java @@ -0,0 +1,146 @@ +package io.cattle.platform.iaas.api.auth.change; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import io.cattle.platform.api.formatter.DefaultIdFormatter; +import io.cattle.platform.archaius.util.ArchaiusUtil; +import io.cattle.platform.core.model.Stack; +import io.cattle.platform.core.model.tables.records.HostRecord; +import io.cattle.platform.core.model.tables.records.ServiceRecord; +import io.cattle.platform.core.model.tables.records.StackRecord; +import io.cattle.platform.iaas.api.port.PortPreflightDao; +import io.cattle.platform.iaas.api.port.PortPreflightService; +import io.cattle.platform.servicediscovery.api.filter.ServiceValidationFilter; +import io.github.ibuildthecloud.gdapi.context.ApiContext; +import io.github.ibuildthecloud.gdapi.exception.ClientVisibleException; +import io.github.ibuildthecloud.gdapi.model.Field; +import io.github.ibuildthecloud.gdapi.model.Schema; +import io.github.ibuildthecloud.gdapi.model.impl.FieldImpl; +import io.github.ibuildthecloud.gdapi.request.resource.ResourceManager; +import io.github.ibuildthecloud.gdapi.request.resource.ResourceManagerLocator; +import io.github.ibuildthecloud.gdapi.validation.ValidationHandler; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** The real schema validator and the same native filter/parser/preflight used by manager.create. */ +public class AiServiceCreateNativePreflightTest { + private Object enabled; + @Before public void enable() { + enabled = ArchaiusUtil.getConfiguration().getProperty("api.ai.bridge.enabled"); + ArchaiusUtil.getConfiguration().setProperty("api.ai.bridge.enabled", true); + } + @After public void cleanup() { + ApiContext.remove(); + if (enabled == null) ArchaiusUtil.getConfiguration().clearProperty("api.ai.bridge.enabled"); + else ArchaiusUtil.getConfiguration().setProperty("api.ai.bridge.enabled", enabled); + } + + @Test public void realNativeBodyUsesConvertedParentAndHealthFieldsAndDispatchStillUsesTheFilteredManager() throws Exception { + Fixture f = new Fixture(); + var parameters = f.parameters(List.of("80/tcp")); + var body = f.domain.prepare(f.request, f.descriptor, null, parameters); + assertEquals(3, ((Number) body.get("stackId")).intValue()); + assertEquals(false, body.get("startOnCreate")); assertEquals(1, ((Number) body.get("scale")).intValue()); + Map launch = (Map) body.get("launchConfig"); + assertEquals("bridge", launch.get("networkMode")); assertEquals("docker:busybox:1.37.0", launch.get("imageUuid")); + assertEquals(List.of("80/tcp"), launch.get("ports")); + assertEquals(Map.of("port", 80, "interval", 2000, "strategy", "recreate"), launch.get("healthCheck")); + assertFalse(body.containsKey("ports")); assertFalse(body.containsKey("healthCheck")); + verify(f.nativeInventory).getEligibleHosts(77L); verify(f.nativeInventory).getPortOwners(77L); + verifyNoInteractions(f.manager); + ServiceRecord created = new ServiceRecord(); created.setId(91L); + when(f.manager.create(eq("service"), any())).thenReturn(created); + assertSame(created, f.domain.dispatch(f.request, f.descriptor, null, parameters, "plan")); + verify(f.manager, times(1)).create(eq("service"), any()); + assertSame(f.request, ApiContext.getContext().getApiRequest()); + } + + @Test public void realNativeDuplicatePortConflictFailsBeforeAnyManagerCreateAndParserCodesRemainNative() throws Exception { + Fixture f = new Fixture(); + var denied = assertThrows(ClientVisibleException.class, () -> f.domain.prepare(f.request, f.descriptor, null, + f.parameters(List.of("18080:80/tcp", "18080:81/tcp")))); + assertEquals("PortConflict", denied.getApiError().getCode()); assertEquals(422, denied.getApiError().getStatus()); + var malformed = new LinkedHashMap<>(f.parameters(List.of("80"))); malformed.put("ports", List.of("18080:0")); + var parser = assertThrows(ClientVisibleException.class, () -> f.domain.prepare(f.request, f.descriptor, null, malformed)); + assertEquals("PortInvalidPrivatePort", parser.getCode()); verifyNoInteractions(f.manager); + assertSame(f.request, ApiContext.getContext().getApiRequest()); + } + + @Test public void nativeNestedRoleFieldRemovalCannotSilentlyTurnApprovedHealthCheckIntoAnotherConfiguration() throws Exception { + Fixture f = new Fixture(); f.healthFields.get("interval").setCreate(false); + var denied = assertThrows(ClientVisibleException.class, () -> f.domain.prepare(f.request, f.descriptor, null, f.parameters(List.of("80")))); + assertEquals("PermissionDenied", denied.getCode()); verifyNoInteractions(f.manager, f.nativeInventory); + } + + @Test public void liveNativePortAvailabilityIsRepeatedAtConfirmationAndImmediatelyBeforeTheDurableClaim() throws Exception { + Fixture nativeDomain = new Fixture(); var available = nativeDomain.nativeInventory.getEligibleHosts(77L); + var formatter = ApiContext.getContext().getIdFormatter(); + var workflow = new AiResourceChangePlanServiceTest.Fixture(); + ApiContext.getContext().setIdFormatter(formatter); + when(workflow.domain.prepare(any(), any(), isNull(), anyMap())).thenAnswer(call -> + nativeDomain.domain.prepare(nativeDomain.request, call.getArgument(1), null, call.getArgument(3))); + when(workflow.domain.prepare(any(), any(), isNull(), anyMap(), anyMap())).thenAnswer(call -> + nativeDomain.domain.prepare(nativeDomain.request, call.getArgument(1), null, call.getArgument(3), call.getArgument(4))); + workflow.previewServiceCreate(false, List.of("18080:80/tcp")); + when(nativeDomain.nativeInventory.getEligibleHosts(77L)).thenReturn(List.of()); + var confirm = assertThrows(ClientVisibleException.class, workflow::confirm); + assertEquals("PortConflict", confirm.getCode()); assertEquals(422, confirm.getStatus()); + assertEquals("pending", workflow.documents.get(workflow.id).get("confirmationState")); + when(nativeDomain.nativeInventory.getEligibleHosts(77L)).thenReturn(available); workflow.confirm(); + when(nativeDomain.nativeInventory.getEligibleHosts(77L)).thenReturn(List.of()); + var apply = assertThrows(ClientVisibleException.class, () -> workflow.apply(false)); + assertEquals("PortConflict", apply.getCode()); assertEquals(422, apply.getStatus()); + assertEquals("preview", workflow.documents.get(workflow.id).get("status")); + when(nativeDomain.nativeInventory.getEligibleHosts(77L)).thenReturn(available); + doThrow(new ClientVisibleException(403, "ApiKeyRevoked")).when(nativeDomain.domain.keys).authorizeBoundDomain(anyMap(), any()); + assertEquals("ApiKeyRevoked", assertThrows(ClientVisibleException.class, () -> workflow.apply(false)).getCode()); + assertEquals(0, workflow.claims); assertEquals(0, workflow.dispatched); verifyNoInteractions(nativeDomain.manager); + } + + private static class Fixture extends AiResourceChangeDomainTest.Fixture { + final AiResourceChangeDescriptor descriptor = AiResourceChangeDescriptor.require("service", "create"); + final PortPreflightDao nativeInventory = mock(PortPreflightDao.class); + final ResourceManager manager = mock(ResourceManager.class); + final Map healthFields = new LinkedHashMap<>(); + Fixture() throws Exception { + when(schema.getCollectionMethods()).thenReturn(List.of("POST")); + Map fields = new LinkedHashMap<>(); + for (String field : List.of("name", "description", "scale", "stackId", "launchConfig", "startOnCreate")) + fields.put(field, field(switch (field) { case "scale" -> "int"; case "stackId" -> "reference[stack]"; + case "launchConfig" -> "container"; case "startOnCreate" -> "boolean"; default -> "string"; })); + when(schema.getResourceFields()).thenReturn(fields); + Schema launch = mock(Schema.class); Map launchFields = new LinkedHashMap<>(); + for (String field : List.of("imageUuid", "networkMode", "ports", "healthCheck")) + launchFields.put(field, field("ports".equals(field) ? "array[string]" : "healthCheck".equals(field) ? "instanceHealthCheck" : "string")); + when(launch.getResourceFields()).thenReturn(launchFields); when(schemas.getSchema("container")).thenReturn(launch); + Schema health = mock(Schema.class); + for (String field : List.of("port", "interval", "strategy")) healthFields.put(field, field("strategy".equals(field) ? "string" : "int")); + when(health.getResourceFields()).thenReturn((Map) healthFields); when(schemas.getSchema("instanceHealthCheck")).thenReturn(health); + var formatter = new DefaultIdFormatter(); formatter.setSchemaFactory(schemas); formatter.setTypeMappings(Map.of("stack", "st")); + ApiContext.getContext().setIdFormatter(formatter); + var validator = new ValidationHandler(); validator.init(); domain.validation = validator; + var stack = new StackRecord(); stack.setId(3L); stack.setAccountId(77L); + when(domain.objects.loadResource(Stack.class, 3L)).thenReturn(stack); + var host = new HostRecord(); host.setId(1L); host.setAccountId(77L); + when(nativeInventory.getEligibleHosts(77L)).thenReturn(List.of(host)); when(nativeInventory.getPortOwners(77L)).thenReturn(List.of()); + var nativePreflight = new PortPreflightService(); inject(nativePreflight, "dao", nativeInventory); inject(nativePreflight, "objectManager", domain.objects); + var nativeFilter = new ServiceValidationFilter(); inject(nativeFilter, "objectManager", domain.objects); + inject(nativeFilter, "jsonMapper", domain.json); inject(nativeFilter, "portPreflightService", nativePreflight); + domain.serviceCreateValidation = nativeFilter; + domain.managers = mock(ResourceManagerLocator.class); when(domain.managers.getResourceManagerByType("service")).thenReturn(manager); + } + Map parameters(List ports) { + return descriptor.parameters(Map.of("name", "probe", "image", "busybox:1.37.0", "stackId", "1st3", "ports", ports, + "healthCheck", Map.of("port", 80, "interval", 2000))); + } + private static FieldImpl field(String type) { var result = new FieldImpl(); result.setType(type); result.setCreate(true); return result; } + private static void inject(Object target, String field, Object value) throws Exception { + var injection = target.getClass().getDeclaredField(field); injection.setAccessible(true); injection.set(target, value); + } + } +} diff --git a/code/iaas/bootstrap/pom.xml b/code/iaas/bootstrap/pom.xml index dbf1e0c2b3..b71bb9724f 100644 --- a/code/iaas/bootstrap/pom.xml +++ b/code/iaas/bootstrap/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/iaas/config-item/api/pom.xml b/code/iaas/config-item/api/pom.xml index acbde8ec9b..9ee50b1f0e 100644 --- a/code/iaas/config-item/api/pom.xml +++ b/code/iaas/config-item/api/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../../parent/pom.xml diff --git a/code/iaas/config-item/common/pom.xml b/code/iaas/config-item/common/pom.xml index 316c7a7b10..d56f3f6094 100644 --- a/code/iaas/config-item/common/pom.xml +++ b/code/iaas/config-item/common/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../../parent/pom.xml diff --git a/code/iaas/config-item/server/pom.xml b/code/iaas/config-item/server/pom.xml index 462c951a4f..1cb2b0894c 100644 --- a/code/iaas/config-item/server/pom.xml +++ b/code/iaas/config-item/server/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../../parent/pom.xml diff --git a/code/iaas/engine-jooq/pom.xml b/code/iaas/engine-jooq/pom.xml index 21c44130a4..e34f3c5d55 100644 --- a/code/iaas/engine-jooq/pom.xml +++ b/code/iaas/engine-jooq/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/iaas/events/pom.xml b/code/iaas/events/pom.xml index 4b1b024ece..527187149b 100644 --- a/code/iaas/events/pom.xml +++ b/code/iaas/events/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/iaas/external-handler/pom.xml b/code/iaas/external-handler/pom.xml index 955185ea5c..f40f7551b2 100644 --- a/code/iaas/external-handler/pom.xml +++ b/code/iaas/external-handler/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/iaas/ha/pom.xml b/code/iaas/ha/pom.xml index 6717b6641f..9fd6c4fcbc 100644 --- a/code/iaas/ha/pom.xml +++ b/code/iaas/ha/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/iaas/healthcheck/pom.xml b/code/iaas/healthcheck/pom.xml index b156d0d891..184e59e237 100644 --- a/code/iaas/healthcheck/pom.xml +++ b/code/iaas/healthcheck/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/iaas/labels/pom.xml b/code/iaas/labels/pom.xml index a37ad42849..6a6d72feda 100644 --- a/code/iaas/labels/pom.xml +++ b/code/iaas/labels/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/iaas/logic-common/pom.xml b/code/iaas/logic-common/pom.xml index 489deb4ebc..6c987e79bd 100644 --- a/code/iaas/logic-common/pom.xml +++ b/code/iaas/logic-common/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/iaas/logic/pom.xml b/code/iaas/logic/pom.xml index f3f51a3575..7274f69796 100644 --- a/code/iaas/logic/pom.xml +++ b/code/iaas/logic/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/iaas/metadata/pom.xml b/code/iaas/metadata/pom.xml index ec8a6b5e9c..db3df2f448 100644 --- a/code/iaas/metadata/pom.xml +++ b/code/iaas/metadata/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/iaas/model/pom.xml b/code/iaas/model/pom.xml index 3ebac4ed34..4f10cfe668 100644 --- a/code/iaas/model/pom.xml +++ b/code/iaas/model/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/iaas/model/src/main/java/io/cattle/platform/core/util/PortSpec.java b/code/iaas/model/src/main/java/io/cattle/platform/core/util/PortSpec.java index 0d7ee9ef2b..773ff1f57c 100644 --- a/code/iaas/model/src/main/java/io/cattle/platform/core/util/PortSpec.java +++ b/code/iaas/model/src/main/java/io/cattle/platform/core/util/PortSpec.java @@ -126,7 +126,11 @@ public String toSpec() { String publicPort = this.publicPort != null ? this.publicPort.toString() + ":" : ""; String bindIP = ""; if (StringUtils.isNotBlank(this.ipAddress)) { - bindIP = this.ipAddress + ":"; + String address = this.ipAddress; + if (address.contains(":") && !(address.startsWith("[") && address.endsWith("]"))) { + address = "[" + address + "]"; + } + bindIP = address + ":"; } return bindIP + publicPort + privatePortProto; } diff --git a/code/iaas/model/src/test/java/io/cattle/platform/core/util/PortSpecTest.java b/code/iaas/model/src/test/java/io/cattle/platform/core/util/PortSpecTest.java index 23d5f11174..081a54eac5 100644 --- a/code/iaas/model/src/test/java/io/cattle/platform/core/util/PortSpecTest.java +++ b/code/iaas/model/src/test/java/io/cattle/platform/core/util/PortSpecTest.java @@ -111,4 +111,29 @@ public void testBadFormats() { assertEquals(PortSpec.INVALID_PROTOCOL, e.getCode()); } } + + @Test + public void nativeFormattingKeepsExistingIpv4NoBindAndAllocatedPortOutputs() { + assertEquals("80/tcp", new PortSpec("80").toSpec()); + assertEquals("81:80/tcp", new PortSpec("81:80").toSpec()); + assertEquals("127.0.0.1:81:80/udp", new PortSpec("127.0.0.1:81:80/udp").toSpec()); + PortSpec allocated = new PortSpec("127.0.0.1::80"); allocated.setPublicPort(31001); + assertEquals("127.0.0.1:31001:80/tcp", allocated.toSpec()); + PortSpec unbound = new PortSpec("80/udp"); unbound.setPublicPort(31002); + assertEquals("31002:80/udp", unbound.toSpec()); + } + + @Test + public void ipv6FixedAndNativeAllocatedMappingsRoundTripThroughTheSameParser() { + for (String input : new String[] { "[::1]:81:80", "[2001:db8::1]:53/udp", "[::]:65535:65535/tcp" }) { + PortSpec expected = new PortSpec(input); PortSpec actual = new PortSpec(expected.toSpec()); + assertEquals(expected.getIpAddress(), actual.getIpAddress()); assertEquals(expected.getPublicPort(), actual.getPublicPort()); + assertEquals(expected.getPrivatePort(), actual.getPrivatePort()); assertEquals(expected.getProtocol(), actual.getProtocol()); + } + PortSpec allocated = new PortSpec("[2001:db8::1]:53/udp"); allocated.setPublicPort(31001); + assertEquals("[2001:db8::1]:31001:53/udp", allocated.toSpec()); + PortSpec read = new PortSpec(allocated.toSpec()); + assertEquals("2001:db8::1", read.getIpAddress()); assertEquals(Integer.valueOf(31001), read.getPublicPort()); + assertEquals(53, read.getPrivatePort()); assertEquals("udp", read.getProtocol()); + } } diff --git a/code/iaas/resource-pool/pom.xml b/code/iaas/resource-pool/pom.xml index 88a5363554..1a63af9992 100644 --- a/code/iaas/resource-pool/pom.xml +++ b/code/iaas/resource-pool/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/iaas/service-discovery/api/pom.xml b/code/iaas/service-discovery/api/pom.xml index 41f12c98ef..3d75f86672 100644 --- a/code/iaas/service-discovery/api/pom.xml +++ b/code/iaas/service-discovery/api/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.365 + 0.183.366 ../../../parent/pom.xml diff --git a/code/iaas/service-discovery/api/src/main/java/io/cattle/platform/servicediscovery/api/filter/ServiceValidationFilter.java b/code/iaas/service-discovery/api/src/main/java/io/cattle/platform/servicediscovery/api/filter/ServiceValidationFilter.java index dfb59af32d..762bd86ba2 100644 --- a/code/iaas/service-discovery/api/src/main/java/io/cattle/platform/servicediscovery/api/filter/ServiceValidationFilter.java +++ b/code/iaas/service-discovery/api/src/main/java/io/cattle/platform/servicediscovery/api/filter/ServiceValidationFilter.java @@ -26,6 +26,7 @@ import io.cattle.platform.object.util.DataUtils; import io.cattle.platform.object.util.ObjectUtils; import io.cattle.platform.servicediscovery.api.util.ServiceDiscoveryUtil; +import io.cattle.platform.servicediscovery.api.service.ServiceCreateValidation; import io.cattle.platform.servicediscovery.api.util.selector.SelectorUtils; import io.cattle.platform.storage.api.filter.ExternalTemplateInstanceFilter; import io.cattle.platform.storage.service.StorageService; @@ -52,7 +53,7 @@ import org.apache.commons.lang3.StringUtils; @Named -public class ServiceValidationFilter extends AbstractDefaultResourceManagerFilter { +public class ServiceValidationFilter extends AbstractDefaultResourceManagerFilter implements ServiceCreateValidation { @Inject ObjectManager objectManager; @@ -198,10 +199,22 @@ static Map launchConfigMap(Object launchConfigObj) { public void validatePorts(Service service, String type, ApiRequest request) { validateLBPortRules(type, request); - validatePorts(request); + validateLaunchConfigPorts(request); } - private void validatePorts(ApiRequest request) { + @Override + public String imageUuid(String image) { + return ExternalTemplateInstanceFilter.getImageUuid(image, storageService); + } + + @Override + public void validatePorts(ApiRequest request) { + Service service = request.proxyRequestObject(Service.class); + validatePorts(service, request.getType(), request); + assertPortsAvailable(service, objectManager.loadResource(Stack.class, service.getStackId()), request); + } + + private void validateLaunchConfigPorts(ApiRequest request) { Map data = CollectionUtils.toMap(request.getRequestObject()); if (data.get(ServiceConstants.FIELD_LAUNCH_CONFIG) == null) { return; diff --git a/code/iaas/service-discovery/api/src/main/java/io/cattle/platform/servicediscovery/api/service/ServiceCreateValidation.java b/code/iaas/service-discovery/api/src/main/java/io/cattle/platform/servicediscovery/api/service/ServiceCreateValidation.java new file mode 100644 index 0000000000..87cab8d6b7 --- /dev/null +++ b/code/iaas/service-discovery/api/src/main/java/io/cattle/platform/servicediscovery/api/service/ServiceCreateValidation.java @@ -0,0 +1,12 @@ +package io.cattle.platform.servicediscovery.api.service; + +import io.github.ibuildthecloud.gdapi.request.ApiRequest; + +/** Native create validation for an already authorized request; performs no resource mutation. */ +public interface ServiceCreateValidation { + /** Reuses the normal Service create port parser and live port preflight. */ + void validatePorts(ApiRequest authorizedRequest); + + /** Uses the same image normalization as the final native Service create filter. */ + String imageUuid(String image); +} diff --git a/code/iaas/service-discovery/server/pom.xml b/code/iaas/service-discovery/server/pom.xml index db4e2b30a4..74646202fa 100644 --- a/code/iaas/service-discovery/server/pom.xml +++ b/code/iaas/service-discovery/server/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.365 + 0.183.366 ../../../parent/pom.xml diff --git a/code/iaas/ssh-common/pom.xml b/code/iaas/ssh-common/pom.xml index 08acc736ac..d1747be0cc 100644 --- a/code/iaas/ssh-common/pom.xml +++ b/code/iaas/ssh-common/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/iaas/storage-service/pom.xml b/code/iaas/storage-service/pom.xml index 591caeb95c..42da5524ea 100644 --- a/code/iaas/storage-service/pom.xml +++ b/code/iaas/storage-service/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/iaas/task-jooq/pom.xml b/code/iaas/task-jooq/pom.xml index ca2757363c..0986ad7465 100644 --- a/code/iaas/task-jooq/pom.xml +++ b/code/iaas/task-jooq/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/implementation/activity-log/pom.xml b/code/implementation/activity-log/pom.xml index c13d967c6e..f87fc08300 100644 --- a/code/implementation/activity-log/pom.xml +++ b/code/implementation/activity-log/pom.xml @@ -5,7 +5,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/implementation/agent-instance-impl/pom.xml b/code/implementation/agent-instance-impl/pom.xml index 0f523c1f2c..a8ea291f23 100644 --- a/code/implementation/agent-instance-impl/pom.xml +++ b/code/implementation/agent-instance-impl/pom.xml @@ -3,7 +3,7 @@ cattle-parent io.cattle - 0.183.365 + 0.183.366 ../../parent/pom.xml cattle-agent-instance-impl diff --git a/code/implementation/docker/api/pom.xml b/code/implementation/docker/api/pom.xml index 22c32189de..1a1b5fd886 100644 --- a/code/implementation/docker/api/pom.xml +++ b/code/implementation/docker/api/pom.xml @@ -5,7 +5,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../../parent/pom.xml diff --git a/code/implementation/docker/common/pom.xml b/code/implementation/docker/common/pom.xml index 243977df4b..00beb670ad 100644 --- a/code/implementation/docker/common/pom.xml +++ b/code/implementation/docker/common/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../../parent/pom.xml diff --git a/code/implementation/docker/compute/pom.xml b/code/implementation/docker/compute/pom.xml index cabefd357d..5b184843b3 100644 --- a/code/implementation/docker/compute/pom.xml +++ b/code/implementation/docker/compute/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../../parent/pom.xml diff --git a/code/implementation/docker/machine/pom.xml b/code/implementation/docker/machine/pom.xml index ea749104e5..1a6ffbfce9 100644 --- a/code/implementation/docker/machine/pom.xml +++ b/code/implementation/docker/machine/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../../parent/pom.xml diff --git a/code/implementation/docker/storage/pom.xml b/code/implementation/docker/storage/pom.xml index 1ea9ee97d9..84d675a4c3 100644 --- a/code/implementation/docker/storage/pom.xml +++ b/code/implementation/docker/storage/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../../parent/pom.xml @@ -21,12 +21,12 @@ io.cattle cattle-docker-common - 0.183.365 + 0.183.366 io.cattle cattle-iaas-allocator - 0.183.365 + 0.183.366 diff --git a/code/implementation/extension-api/pom.xml b/code/implementation/extension-api/pom.xml index 6f4037b16b..75a95aa50b 100644 --- a/code/implementation/extension-api/pom.xml +++ b/code/implementation/extension-api/pom.xml @@ -3,7 +3,7 @@ cattle-parent io.cattle - 0.183.365 + 0.183.366 ../../parent/pom.xml cattle-extension-api diff --git a/code/implementation/hazelcast/common/pom.xml b/code/implementation/hazelcast/common/pom.xml index 128a4934c4..259d0ea9af 100644 --- a/code/implementation/hazelcast/common/pom.xml +++ b/code/implementation/hazelcast/common/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../../parent/pom.xml diff --git a/code/implementation/hazelcast/eventing/pom.xml b/code/implementation/hazelcast/eventing/pom.xml index 3772300201..a0da48f1d6 100644 --- a/code/implementation/hazelcast/eventing/pom.xml +++ b/code/implementation/hazelcast/eventing/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../../parent/pom.xml diff --git a/code/implementation/hazelcast/lock/pom.xml b/code/implementation/hazelcast/lock/pom.xml index 2803d7afae..1e8c1e169d 100644 --- a/code/implementation/hazelcast/lock/pom.xml +++ b/code/implementation/hazelcast/lock/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../../parent/pom.xml diff --git a/code/implementation/host-api/pom.xml b/code/implementation/host-api/pom.xml index e726b81760..ac68db6433 100644 --- a/code/implementation/host-api/pom.xml +++ b/code/implementation/host-api/pom.xml @@ -5,7 +5,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/implementation/host-stats/pom.xml b/code/implementation/host-stats/pom.xml index 2bfaa4ba6f..2b490e0441 100644 --- a/code/implementation/host-stats/pom.xml +++ b/code/implementation/host-stats/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/implementation/register/pom.xml b/code/implementation/register/pom.xml index fc11d48006..3bc63b5967 100644 --- a/code/implementation/register/pom.xml +++ b/code/implementation/register/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/implementation/sample-setup/pom.xml b/code/implementation/sample-setup/pom.xml index faf0261b0b..bd5b4b28df 100644 --- a/code/implementation/sample-setup/pom.xml +++ b/code/implementation/sample-setup/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/implementation/settings-api/pom.xml b/code/implementation/settings-api/pom.xml index 3a20d9350b..1285c31c1b 100644 --- a/code/implementation/settings-api/pom.xml +++ b/code/implementation/settings-api/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/implementation/simulator/agent-connection/pom.xml b/code/implementation/simulator/agent-connection/pom.xml index d6ae3c26c7..9e1818bdb3 100644 --- a/code/implementation/simulator/agent-connection/pom.xml +++ b/code/implementation/simulator/agent-connection/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../../parent/pom.xml diff --git a/code/implementation/simulator/storage/pom.xml b/code/implementation/simulator/storage/pom.xml index 2122fce6ea..a1d3ab02c8 100644 --- a/code/implementation/simulator/storage/pom.xml +++ b/code/implementation/simulator/storage/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../../parent/pom.xml diff --git a/code/implementation/system-stack/pom.xml b/code/implementation/system-stack/pom.xml index c960cdf3a6..8267914723 100644 --- a/code/implementation/system-stack/pom.xml +++ b/code/implementation/system-stack/pom.xml @@ -5,7 +5,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/implementation/vm/pom.xml b/code/implementation/vm/pom.xml index 09393b59d2..2c911fe7c6 100644 --- a/code/implementation/vm/pom.xml +++ b/code/implementation/vm/pom.xml @@ -3,7 +3,7 @@ cattle-parent io.cattle - 0.183.365 + 0.183.366 ../../parent/pom.xml cattle-vm diff --git a/code/meta-parent/pom.xml b/code/meta-parent/pom.xml index a94b6eb894..7efd519e8f 100644 --- a/code/meta-parent/pom.xml +++ b/code/meta-parent/pom.xml @@ -9,7 +9,7 @@ 4.0.0 io.cattle cattle-meta-parent - 0.183.365 + 0.183.366 pom PastureStack Orchestration Engine Compatibility orchestration engine for the PastureStack server. diff --git a/code/packaging/app-config/pom.xml b/code/packaging/app-config/pom.xml index cf3dba0617..4b2a15d504 100644 --- a/code/packaging/app-config/pom.xml +++ b/code/packaging/app-config/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml jar diff --git a/code/packaging/app/pom.xml b/code/packaging/app/pom.xml index 57e8cb0971..96bf47b679 100644 --- a/code/packaging/app/pom.xml +++ b/code/packaging/app/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.365 + 0.183.366 ../../parent/pom.xml war diff --git a/code/packaging/bundle/pom.xml b/code/packaging/bundle/pom.xml index 27f3eb1f81..b64e459aa2 100644 --- a/code/packaging/bundle/pom.xml +++ b/code/packaging/bundle/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/packaging/dev/pom.xml b/code/packaging/dev/pom.xml index 5a63b5268f..cfc8924f36 100644 --- a/code/packaging/dev/pom.xml +++ b/code/packaging/dev/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/packaging/meta/pom.xml b/code/packaging/meta/pom.xml index 18fe950142..c35840114a 100644 --- a/code/packaging/meta/pom.xml +++ b/code/packaging/meta/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.365 + 0.183.366 ../../parent/pom.xml diff --git a/code/parent/pom.xml b/code/parent/pom.xml index 61a00ea8c4..e8d3f4f92d 100644 --- a/code/parent/pom.xml +++ b/code/parent/pom.xml @@ -5,7 +5,7 @@ io.cattle cattle-meta-parent ../meta-parent/pom.xml - 0.183.365 + 0.183.366 pom diff --git a/docs/releases/orchestration-engine-0.183.366.md b/docs/releases/orchestration-engine-0.183.366.md new file mode 100644 index 0000000000..643c07a978 --- /dev/null +++ b/docs/releases/orchestration-engine-0.183.366.md @@ -0,0 +1,105 @@ +# Engine 0.183.366 candidate + +This candidate is not published. Required original component CI/CodeQL, +complete artifact qualification and matching Web Console/Server native +acceptance remain pending. The published baseline is Engine 0.183.365. + +## Service creation + +The human-reviewed `service.create` parameters add `ports` and `healthCheck`: + +The example is an internal Engine plan payload. WebMCP/UI must first use +`find_resources` to obtain an opaque `parentRef`, which the trusted adapter +resolves; users and models do not supply raw resource IDs such as `stackId`. + +```json +{ + "name": "web", + "image": "nginx:stable", + "stackId": "1st3", + "ports": ["18080:80/tcp", "53/udp"], + "healthCheck": {"port": 80, "interval": 2000, "healthyThreshold": 2} +} +``` + +`ports` contains at most 64 native `PortSpec` strings, each at most 128 +characters. Fixed host-port mappings and native allocation for omitted host +ports are both supported. Preview, human confirmation and apply before the +durable dispatch claim reuse the native parser and port preflight. Native +validation codes are retained, and the final filtered manager remains the +last validation boundary. + +The closed health-check subset requires integer `port` in 1..65535. Optional +`interval` and `responseTimeout` are milliseconds; these and `healthyThreshold` +and `unhealthyThreshold` require integers in 1..2147483647. Optional +`initializingTimeout` and `reinitializingTimeout` accept 0..2147483647 +milliseconds. Optional `requestLine` is at most 1024 characters without NUL, +CR or LF. `strategy` accepts `none` or `recreate`; omission uses the native +`recreate` default. Other optional numeric fields retain omission rather than +inventing defaults. Health-check name and quorum strategy configuration are +outside this subset; unknown fields and incorrect types are rejected. + +The parameters map to native `launchConfig` fields with bridge networking and +`startOnCreate=false`. Creation remains inactive. A later start requires its +own human-approved plan; metadata updates remain limited to name/description. +Private human entry of environment-variable values is not part of this batch. +Neither `environment` nor raw `launchConfig` is accepted as plan input. + +## Catalog native projection correction + +Catalog's real Docker transformer stores dynamic values such as `imageUuid`, +`environment`, `ports` and `logConfig` in `InstanceRecord.data.fields`. The +Materializer previously omitted that map when wrapping the transformed +instance, so the native role-schema projection lost required business values. +It now passes a fresh copy of `DataUtils.getFields(instance)` to the existing +`WrappedResource` projection. The current role schema, ID formatter, `POST` +method and non-create-response handling remain unchanged. + +Direct tests use the real transformer and native base/project-role schemas to +verify the exact image, environment value with an embedded equals sign, port +mapping, log driver/configuration and bean-backed name. Narrowed schemas, +read-on-create-only fields and unlisted dynamic fields still restrict output, +and the source dynamic map retains its business values. This corrects the +existing Catalog projection; it adds no permission or new environment-input +channel. Matching artifact and native workflow acceptance remain pending. + +## Verified results and compatibility + +Completion verifies the actual created Service identity/UUID, owner, parent, +image, scale, programs, network mode, ports and health check. Fixed port +mappings must match. Native allocated mappings must preserve private port, +protocol, bind address and multiplicity, with valid assigned host ports. +Missing, changed or conflicting mappings cannot establish completion. + +Only a succeeded, verified receipt exposes read-only +`executionResult.assignedPorts`. This reports native persisted values without +changing the approved `changes` or treating an observed assignment as new +consent. An uncertain dispatch or receipt write retains its durable claim and +is never automatically replayed. Current role, project, Key revision/expiry, +explicit DENY and audit requirements remain in force. + +Native `PortSpec.toSpec()` now brackets IPv6 bind addresses, preserving +roundtrip parsing for fixed and allocated mappings. Existing IPv4 and unbound +mapping output is retained. This affects native Service, load-balancer and +port-update formatting too; it does not rewrite existing database values. +Host IPv6 binding and routing still require native runtime acceptance. + +The four AI opt-ins remain independently default-off. Full/legacy Key behavior, +runtime dependency coordinates and database schemas are unchanged. Clients +must understand these typed fields and the read-only execution result. +Consume a qualified component through a matching published Server release. + +## Focused evidence + +60 unique focused cases passed with JDK 25 and Maven 3.9.16. They cover the +native parser/preflight path, post-preview conflicts and revocation before +claim, bounded input, actual readback, UUID rebinding, uncertain operations, +execution-result separation and IPv6 formatting. Three test-fixture setup +corrections were rerun alone; unaffected PASS evidence was retained. These +source results do not establish full CI, published artifact or native workflow +acceptance, and no final CI total or artifact digest is claimed. + +The Catalog projection correction passed two new real-schema regression +methods and one directly affected existing Materializer method. Its C03 +source review independently re-extracts all 1396 operation tuples and their +semantic/admission dispositions; those classifications remain unchanged. diff --git a/pom.xml b/pom.xml index 827d0a8e0d..1d90edc768 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ io.cattle cattle-parent - 0.183.365 + 0.183.366 code/parent/pom.xml cattle diff --git a/resources/content/schema/admin/admin-auth.json b/resources/content/schema/admin/admin-auth.json index f6ef957182..2d2dd8d6c0 100644 --- a/resources/content/schema/admin/admin-auth.json +++ b/resources/content/schema/admin/admin-auth.json @@ -39,6 +39,7 @@ "aiResourceChangePlan.parameters": "cr", "aiResourceChangePlan.targetPath": "r", "aiResourceChangePlan.changes": "r", + "aiResourceChangePlan.executionResult": "r", "aiResourceChangePlan.confirmationState": "r", "aiResourceChangePlan.status": "r", "aiResourceChangePlan.operationId": "r", diff --git a/resources/content/schema/base/aiResourceChangePlan.json b/resources/content/schema/base/aiResourceChangePlan.json index 5eddbe8756..30318cad84 100644 --- a/resources/content/schema/base/aiResourceChangePlan.json +++ b/resources/content/schema/base/aiResourceChangePlan.json @@ -10,6 +10,7 @@ "parameters": { "type": "json", "create": true, "required": true }, "targetPath": { "type": "string", "create": false, "update": false }, "changes": { "type": "array[json]", "create": false, "update": false }, + "executionResult": { "type": "json", "create": false, "update": false, "nullable": true }, "confirmationState": { "type": "string", "create": false, "update": false }, "status": { "type": "string", "create": false, "update": false }, "operationId": { "type": "string", "create": false, "update": false }, diff --git a/resources/content/schema/project/project-auth.json b/resources/content/schema/project/project-auth.json index bb45cdeb36..ba690ea3fa 100644 --- a/resources/content/schema/project/project-auth.json +++ b/resources/content/schema/project/project-auth.json @@ -39,6 +39,7 @@ "aiResourceChangePlan.parameters": "cr", "aiResourceChangePlan.targetPath": "r", "aiResourceChangePlan.changes": "r", + "aiResourceChangePlan.executionResult": "r", "aiResourceChangePlan.confirmationState": "r", "aiResourceChangePlan.status": "r", "aiResourceChangePlan.operationId": "r", diff --git a/resources/content/schema/user/user-auth.json b/resources/content/schema/user/user-auth.json index bad9e9c084..82f2cc83de 100644 --- a/resources/content/schema/user/user-auth.json +++ b/resources/content/schema/user/user-auth.json @@ -39,6 +39,7 @@ "aiResourceChangePlan.parameters": "cr", "aiResourceChangePlan.targetPath": "r", "aiResourceChangePlan.changes": "r", + "aiResourceChangePlan.executionResult": "r", "aiResourceChangePlan.confirmationState": "r", "aiResourceChangePlan.status": "r", "aiResourceChangePlan.operationId": "r", diff --git a/resources/pom.xml b/resources/pom.xml index 65a0815398..49a41b9f26 100644 --- a/resources/pom.xml +++ b/resources/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.365 + 0.183.366 ../code/parent/pom.xml diff --git a/scripts/api-key-operation-contract.json b/scripts/api-key-operation-contract.json index 4b2f870771..23c96a856e 100644 --- a/scripts/api-key-operation-contract.json +++ b/scripts/api-key-operation-contract.json @@ -9,8 +9,8 @@ "scope": "Declared v1/v2 and source handler/process union; source-only review, not current artifact packaging, live role/state availability or asynchronous completion.", "sourceInputs": { "algorithm": "c03-source-inputs-v1", - "count": 432, - "sha256": "fdf53022efdab0e56edb292da037216be051643770565c090424124be8b115ea", + "count": 441, + "sha256": "7ce17c8b05ccfba961414808148d8638bf54a9ad3bbc7de00facfcba4b26ac39", "anchors": [ "AbstractSchemaFactory.java", "AiAliasServiceChange.java", @@ -57,6 +57,7 @@ "AiResourceChangeProcessObserver.java", "AiResourceViewResourceManager.java", "AiResourceViewService.java", + "AiServiceCreateChange.java", "ApiAuditPayload.java", "ApiAuthenticator.java", "ApiCookieAuthentication.java", @@ -83,12 +84,20 @@ "GenericWhitelistedProxy.java", "IaasApiConfig.java", "NotWritablePostProcessor.java", + "PortBindingAddress.java", + "PortPreflightInputs.java", + "PortPreflightService.java", + "PortSpec.java", "ProcessAuthorization.java", "ProcessEventDecorator.java", "RequestReRouterHandler.java", "ResourceManagerRequestHandler.java", "SchemaFactoryImpl.java", "ServiceConsumeMapDaoImpl.java", + "ServiceCreate.java", + "ServiceCreateValidation.java", + "ServiceDiscoveryServiceImpl.java", + "ServiceValidationFilter.java", "TokenAuthLookup.java", "TransformInspect.java" ], @@ -233,64 +242,164 @@ "tupleCount": 1396, "tupleSha256": "8f1c0fb378663b0b3d272efba2f211cb210eb6f17cb8035aefa33b56feeaf36a", "extensionReview": { - "version": "unreleased-365-catalog-private-native-wire-fix", + "version": "unreleased-366-service-create-native-ports-healthcheck-and-catalog-projection", "status": "SOURCE_ONLY_NOT_RUNTIME_ACCEPTANCE", - "scope": "The fixed-JSON Catalog private-envelope consumer decodes exactly one canonical standard Base64 layer returned by the existing Go Secrets create/rewrap contract, then parses JSON and rechecks version plus exact plan/actor/project/original-Key binding. Global SecretsService.decrypt and its legacy consumers are unchanged. The decoded JSON remains limited to 512 KiB; Base64 wire has its separate 4*ceil(limit/3) bound. Malformed, nested, raw JSON, oversized or rebound input still produces the fixed safe 503. This adds no authorization relaxation. The prior substantive standalone cleanup authorization fix and Alias safe 422 validation remain unchanged; the whole 365 candidate is not zero authorization-semantic delta. Historical producer355 and complete previous reviews remain preserved.", + "scope": "Complete 365/366 production schema-handler-process unions were independently re-extracted. All 1396 HTTP tuples, operation semantics and registered/unsupported dispositions remain exact. Existing typed Service create adds bounded ports/healthCheck inputs and reuses native preflight before preview/confirmation/claim; normal manager validation remains. Three current role projections add only read access to the new server-produced executionResult, with existing plan owner/current authorization and successful verified Service UUID binding retained. No existing role/action/CRUD grant is changed. Native IPv6 formatting and stricter create readback are functional changes, not new public operation grants. Environment/private human input remains unsupported. Nine direct native/helper source owners are explicitly bound. Catalog Materializer now supplies the native dynamic data.fields map to its existing current-role schema projection; real transformer/native-schema tests verify business values and hidden/create-only/unlisted field exclusions. Production-only selection and historical producer355 remain unchanged.", + "materializerDynamicProjection": { + "source": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchMaterializer.java", + "rootCause": "DockerTransformer stores imageUuid/environment/ports/logConfig in InstanceRecord data.fields; the five-argument WrappedResource constructor did not receive that dynamic value map, so the real role-schema projection omitted required business values.", + "fix": "Pass a fresh LinkedHashMap copy of DataUtils.getFields(instance) to the seven-argument WrappedResource constructor. Keep the existing current-role SchemaFactory, ID formatter, POST and createResponse=false behavior.", + "preservedBusinessValues": [ + "native docker imageUuid", + "environment exact scalar including embedded equals", + "fixed native ports", + "logConfig driver/config", + "bean-backed service name" + ], + "visibilityBoundary": "Current native role schema remains authoritative. Hidden environment, read-on-create-only ports/logConfig and unlisted dynamic fields remain absent; no role or Key authority expansion, raw bypass or new environment-input channel.", + "focusedTests": { + "newMethods": [ + "dynamicProjectionKeepsRestrictedSchemaAndNonCreateResponseRules", + "realTransformerDynamicFieldsReachNativeRoleSchemaProjection" + ], + "existingAffectedMethods": [ + "preparedGraphSeparatesNativeMutationFromAuthorizedPublicReferenceIds" + ], + "tests": 3, + "failures": 0, + "errors": 0, + "skipped": 0, + "evidenceFile": "engine366-materializer-dynamic-fields-focused-20261102.json", + "evidenceSha256": "24ea39c3caf6f6664735e67308246db34a07c59f384cccea48d9e2b171cbd35a", + "usesRealDockerTransformerAndNativeSchemas": true + }, + "runtimeAcceptance": "PENDING_MATCHING_ARTIFACT_AND_NATIVE_QA; no live pass claimed" + }, "previous": { - "version": "unreleased-365-native-validation-cleanup-fixes", + "version": "unreleased-365-catalog-private-native-wire-fix", "status": "SOURCE_ONLY_NOT_RUNTIME_ACCEPTANCE", - "scope": "The existing HTTP tuple union and operation classifications remain exact. Derived native standalone cleanup authorization changes substantively: a CUSTOM native container DELETE may finish only its original private device-0 OS allocation map after the exact verified instance.remove -> volume.remove (optionally volume.deallocate) scheduling spine. The live spine pins complete UUID/FK/owner references; detached work rechecks that same immutable edge and current actor, role, Key revision/expiry, root grant and explicit dependency DENY. Only this pinned job may read historical removing/removed/purging/purged Instance/Volume ancestors; generic resource resolvers, unbound old jobs and public grants are unchanged. Alias native validation exceptions preserve the established safe 422 InvalidBody response instead of becoming a 500; this adds no grant. This is not zero authorization-semantic delta. Historical producer355 is unchanged.", + "scope": "The fixed-JSON Catalog private-envelope consumer decodes exactly one canonical standard Base64 layer returned by the existing Go Secrets create/rewrap contract, then parses JSON and rechecks version plus exact plan/actor/project/original-Key binding. Global SecretsService.decrypt and its legacy consumers are unchanged. The decoded JSON remains limited to 512 KiB; Base64 wire has its separate 4*ceil(limit/3) bound. Malformed, nested, raw JSON, oversized or rebound input still produces the fixed safe 503. This adds no authorization relaxation. The prior substantive standalone cleanup authorization fix and Alias safe 422 validation remain unchanged; the whole 365 candidate is not zero authorization-semantic delta. Historical producer355 and complete previous reviews remain preserved.", "previous": { - "version": "unreleased-364-native-lifecycle-fixes", + "version": "unreleased-365-native-validation-cleanup-fixes", "status": "SOURCE_ONLY_NOT_RUNTIME_ACCEPTANCE", - "scope": "Exact extraction retains the existing HTTP tuples and operation classifications. Derived native private-image cleanup authorization changes substantively: a CUSTOM container DELETE may authorize only its proven private OS-image/cache dependency graph under verified instance.remove and volume.remove execution frames, with current owner, Key revision, expiry, root grant and explicit dependency deny checks retained. External target reconciliation marks an absent or different exact desired hostname as a bad unit for existing expose-map cleanup, instead of throwing on null or retaining substring matches. This is not a zero authorization-semantic-delta claim. Historical producer identity is unchanged.", + "scope": "The existing HTTP tuple union and operation classifications remain exact. Derived native standalone cleanup authorization changes substantively: a CUSTOM native container DELETE may finish only its original private device-0 OS allocation map after the exact verified instance.remove -> volume.remove (optionally volume.deallocate) scheduling spine. The live spine pins complete UUID/FK/owner references; detached work rechecks that same immutable edge and current actor, role, Key revision/expiry, root grant and explicit dependency DENY. Only this pinned job may read historical removing/removed/purging/purged Instance/Volume ancestors; generic resource resolvers, unbound old jobs and public grants are unchanged. Alias native validation exceptions preserve the established safe 422 InvalidBody response instead of becoming a 500; this adds no grant. This is not zero authorization-semantic delta. Historical producer355 is unchanged.", "previous": { - "version": "unreleased-363-external-review-before", + "version": "unreleased-364-native-lifecycle-fixes", "status": "SOURCE_ONLY_NOT_RUNTIME_ACCEPTANCE", - "scope": "External Service review before values read the existing hostname/externalIpAddresses dynamic data.fields using DataAccessor, retaining the existing safe displayed projection. No HTTP declaration, grant, operation classification, owner or CAS contract changes. Historical producer identity is unchanged.", + "scope": "Exact extraction retains the existing HTTP tuples and operation classifications. Derived native private-image cleanup authorization changes substantively: a CUSTOM container DELETE may authorize only its proven private OS-image/cache dependency graph under verified instance.remove and volume.remove execution frames, with current owner, Key revision, expiry, root grant and explicit dependency deny checks retained. External target reconciliation marks an absent or different exact desired hostname as a bad unit for existing expose-map cleanup, instead of throwing on null or retaining substring matches. This is not a zero authorization-semantic-delta claim. Historical producer identity is unchanged.", "previous": { - "version": "unreleased-362-service-index-cleanup", + "version": "unreleased-363-external-review-before", "status": "SOURCE_ONLY_NOT_RUNTIME_ACCEPTANCE", - "scope": "The native Service planner and remove owner may schedule only exact serviceindex.remove cleanup under their original update/delete operation. Current actor/role/Key/revision/expiry/revocation, precise Index/Service/Stack UUID/account/FK pins and explicit child DENY remain required. First trusted scheduling binds private references; detached workers cannot invent them. No HTTP declaration, public ServiceIndex scope grant, method or operation classification is added or changed. Historical producer identity is unchanged.", + "scope": "External Service review before values read the existing hostname/externalIpAddresses dynamic data.fields using DataAccessor, retaining the existing safe displayed projection. No HTTP declaration, grant, operation classification, owner or CAS contract changes. Historical producer identity is unchanged.", "previous": { - "version": "unreleased-361-runtime-workflows", + "version": "unreleased-362-service-index-cleanup", "status": "SOURCE_ONLY_NOT_RUNTIME_ACCEPTANCE", - "scope": "Certificate metadata uses the existing typed plan and native PUT. Durable typed decision audit admits exactly the reviewed native domains. The fixed audit CLI receives only a validated local operator upstream reconstructed from fixed HTTP(S)/loopback literals and a bounded numeric port; no credential environment or caller URL is admitted. Standalone Instance DELETE retains only its verified native remove -> retired NIC remove -> exact IP deactivate ownership graph, current actor/Key ceilings and explicit child DENY. Typed child AUTHORIZATION_DENIED remains terminal instead of UNKNOWN retries. No HTTP declaration or operation classification is added or changed; historical producer identity is unchanged.", + "scope": "The native Service planner and remove owner may schedule only exact serviceindex.remove cleanup under their original update/delete operation. Current actor/role/Key/revision/expiry/revocation, precise Index/Service/Stack UUID/account/FK pins and explicit child DENY remain required. First trusted scheduling binds private references; detached workers cannot invent them. No HTTP declaration, public ServiceIndex scope grant, method or operation classification is added or changed. Historical producer identity is unchanged.", "previous": { - "version": "unreleased-360-domains", + "version": "unreleased-361-runtime-workflows", "status": "SOURCE_ONLY_NOT_RUNTIME_ACCEPTANCE", - "scope": "ExternalService and DNS Alias typed owner workflows add no raw HTTP route; exact dnsservice.setservicelinks is update, service factory/ownership checks remain native. One fixed audit-query read POST; existing safe-view source input expands to four closed native read sources. Complete baseline/current schema-handler-process unions are re-extracted. Exactly one existing dnsService.setservicelinks classification changes to update; all other existing tuples remain unchanged and producer history remains unchanged. No audit raw payload, management operation, serviceFull identity, SQL reader or arbitrary proxy is added.", + "scope": "Certificate metadata uses the existing typed plan and native PUT. Durable typed decision audit admits exactly the reviewed native domains. The fixed audit CLI receives only a validated local operator upstream reconstructed from fixed HTTP(S)/loopback literals and a bounded numeric port; no credential environment or caller URL is admitted. Standalone Instance DELETE retains only its verified native remove -> retired NIC remove -> exact IP deactivate ownership graph, current actor/Key ceilings and explicit child DENY. Typed child AUTHORIZATION_DENIED remains terminal instead of UNKNOWN retries. No HTTP declaration or operation classification is added or changed; historical producer identity is unchanged.", "previous": { - "version": "0.183.359", - "scope": "Source-only Catalog launch plus stopped-resource Archive/restore extension; Archive adds no HTTP declaration: full immutable 358 schema/handler/process union re-extracted. Historical producer unchanged. Fixed metadata/options reads and durable create-plan routes are new; only exact Service.setservicelinks changes to update. Typed launch still requires original live role and Key read/create/update at the destination, references, source CAS, Cookie-human-only completion/approval, normal managers and per-child exact-graph authorization. Neither classification nor local tests prove runtime acceptance.", + "version": "unreleased-360-domains", + "status": "SOURCE_ONLY_NOT_RUNTIME_ACCEPTANCE", + "scope": "ExternalService and DNS Alias typed owner workflows add no raw HTTP route; exact dnsservice.setservicelinks is update, service factory/ownership checks remain native. One fixed audit-query read POST; existing safe-view source input expands to four closed native read sources. Complete baseline/current schema-handler-process unions are re-extracted. Exactly one existing dnsService.setservicelinks classification changes to update; all other existing tuples remain unchanged and producer history remains unchanged. No audit raw payload, management operation, serviceFull identity, SQL reader or arbitrary proxy is added.", "previous": { - "version": "0.183.358", - "scope": "Source-only managed lifecycle and native-name-query fixes. All 1386 HTTP declarations and semantic tuples are unchanged. Original producer identity is unchanged. Not runtime acceptance.", + "version": "0.183.359", + "scope": "Source-only Catalog launch plus stopped-resource Archive/restore extension; Archive adds no HTTP declaration: full immutable 358 schema/handler/process union re-extracted. Historical producer unchanged. Fixed metadata/options reads and durable create-plan routes are new; only exact Service.setservicelinks changes to update. Typed launch still requires original live role and Key read/create/update at the destination, references, source CAS, Cookie-human-only completion/approval, normal managers and per-child exact-graph authorization. Neither classification nor local tests prove runtime acceptance.", "previous": { - "version": "0.183.357", - "scope": "Source-only B1/B2 runtime read fixes. All 1386 HTTP declarations and semantic tuples are unchanged. Original producer identity is unchanged. Not runtime acceptance.", + "version": "0.183.358", + "scope": "Source-only managed lifecycle and native-name-query fixes. All 1386 HTTP declarations and semantic tuples are unchanged. Original producer identity is unchanged. Not runtime acceptance.", "previous": { - "version": "0.183.356", - "scope": "Source-only B1/B2 extension: re-extracted the complete baseline and current schema/handler/process union. Original producer identity is unchanged. Seven typed API tuples are added; only the two exact stack service actions change classification. Classification does not grant admission or prove asynchronous completion.", + "version": "0.183.357", + "scope": "Source-only B1/B2 runtime read fixes. All 1386 HTTP declarations and semantic tuples are unchanged. Original producer identity is unchanged. Not runtime acceptance.", "previous": { - "version": "0.183.355", - "scope": "The original 1371 operation tuples are unchanged. Eight declarations were re-extracted from the three explicit AI resource schemas and reviewed against ApiKeyOperations. Classification does not imply Key admission: plan collection GET is human-only, and confirm/cancel reject every API Key.", + "version": "0.183.356", + "scope": "Source-only B1/B2 extension: re-extracted the complete baseline and current schema/handler/process union. Original producer identity is unchanged. Seven typed API tuples are added; only the two exact stack service actions change classification. Classification does not grant admission or prove asynchronous completion.", + "previous": { + "version": "0.183.355", + "scope": "The original 1371 operation tuples are unchanged. Eight declarations were re-extracted from the three explicit AI resource schemas and reviewed against ApiKeyOperations. Classification does not imply Key admission: plan collection GET is human-only, and confirm/cancel reject every API Key.", + "schemas": [ + "resources/content/schema/base/aiServiceDescriptionUpdate.json", + "resources/content/schema/base/aiServiceUpgradePlan.json", + "resources/content/schema/base/aiToolCallEvent.json" + ], + "addedTuples": [ + [ + "aiServiceDescriptionUpdate", + "collection", + "POST", + "", + "", + "update" + ], + [ + "aiServiceUpgradePlan", + "collection", + "GET", + "", + "", + "read" + ], + [ + "aiServiceUpgradePlan", + "collection", + "POST", + "", + "", + "upgrade" + ], + [ + "aiServiceUpgradePlan", + "resource", + "GET", + "", + "", + "read" + ], + [ + "aiServiceUpgradePlan", + "resource", + "POST", + "confirm", + "", + "ai-plan:confirm" + ], + [ + "aiServiceUpgradePlan", + "resource", + "POST", + "apply", + "", + "upgrade" + ], + [ + "aiServiceUpgradePlan", + "resource", + "POST", + "cancel", + "", + "ai-plan:cancel" + ], + [ + "aiToolCallEvent", + "collection", + "POST", + "", + "", + "create" + ] + ] + }, "schemas": [ "resources/content/schema/base/aiServiceDescriptionUpdate.json", "resources/content/schema/base/aiServiceUpgradePlan.json", - "resources/content/schema/base/aiToolCallEvent.json" + "resources/content/schema/base/aiToolCallEvent.json", + "resources/content/schema/base/aiResourceView.json", + "resources/content/schema/base/aiResourceChangePlan.json" ], "addedTuples": [ [ - "aiServiceDescriptionUpdate", - "collection", - "POST", - "", - "", - "update" - ], - [ - "aiServiceUpgradePlan", + "aiResourceChangePlan", "collection", "GET", "", @@ -298,15 +407,15 @@ "read" ], [ - "aiServiceUpgradePlan", + "aiResourceChangePlan", "collection", "POST", "", "", - "upgrade" + "update" ], [ - "aiServiceUpgradePlan", + "aiResourceChangePlan", "resource", "GET", "", @@ -314,37 +423,134 @@ "read" ], [ - "aiServiceUpgradePlan", + "aiResourceChangePlan", "resource", "POST", - "confirm", + "apply", "", - "ai-plan:confirm" + "update" ], [ - "aiServiceUpgradePlan", + "aiResourceChangePlan", "resource", "POST", - "apply", + "cancel", "", - "upgrade" + "ai-change:cancel" ], [ - "aiServiceUpgradePlan", + "aiResourceChangePlan", "resource", "POST", - "cancel", + "confirm", "", - "ai-plan:cancel" + "ai-change:confirm" ], [ - "aiToolCallEvent", + "aiResourceView", "collection", "POST", "", "", - "create" + "read" ] + ], + "changedTuples": [ + { + "before": [ + "stack", + "resource", + "POST", + "activateservices", + "", + "action:activateservices" + ], + "after": [ + "stack", + "resource", + "POST", + "activateservices", + "", + "update" + ], + "rationale": "Existing Stack child-service lifecycle handler; fixed typed pair, live child authorization remains required." + }, + { + "before": [ + "stack", + "resource", + "POST", + "deactivateservices", + "", + "action:deactivateservices" + ], + "after": [ + "stack", + "resource", + "POST", + "deactivateservices", + "", + "update" + ], + "rationale": "Existing Stack child-service lifecycle handler; fixed typed pair, live child authorization remains required." + } + ], + "authorizationRationale": { + "aiResourceView": "Read semantic POST. Synthetic admission is followed by authorized domain reads and safe projection, never raw GET expansion.", + "aiResourceChangePlan": "Preview/apply are classified update admission only. Fixed domain adapter rechecks the actual create/update/delete operation and Key references. Collection GET and confirm/cancel are human-only; every Key is denied confirmation/cancellation.", + "stackServiceActions": "The two exact Stack batch actions preflight every persisted child with current owner and same-project/Stack checks. An authorized root grant is inherited only for dependency evaluation; explicit child DENY still wins before manager dispatch. The same recheck applies to human approval of bound-Key plans, and queued execution retains its independent live child checks. General Full and legacy admission paths are unchanged." + }, + "sourceInputChanges": [ + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyAuthorizationService.java", + "before": "8062f242159f18d5c21fc14ab65ce42c6cbb8a3bd6cb30dc0cb461bc9a282fbb", + "after": "9ceda0f5118d38fc45fc83bd4a16eb92717a20f7837bcabb804975600c59d2c5" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyOperations.java", + "before": "5b13ab34ee167596cc7f4b664dcc6afc6ab18f12442ed6d44a35b58ad08b40b9", + "after": "494a57d48564da85f14de564a82906444fa732f1adb201fe27d8c0ca88ea65e5" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyTargetResolver.java", + "before": "391918c715b70d8fd9a607da01ac9acc85a1e3e04901c12cc21feb6ba1d98c2e", + "after": "b31333ffc5de2ec8fbee9d38daacd83b91578525d1fe1b61a73abac6355aa009" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanResourceManager.java", + "before": null, + "after": "37da81bdccdb528c1d7f1b18217d3896cabd0ad601e6a3809fd50913b6613050" + }, + { + "path": "resources/content/schema/admin/admin-auth.json", + "before": "be3f0ba2a4fa51a1c42fbf4fc7abe457b65420e8664301fa462474e015a6796f", + "after": "79a757118dd66055c6e7e4c03ded4b0dd8ef1965887c50a4c106e15836d4938f" + }, + { + "path": "resources/content/schema/base/aiResourceChangePlan.json", + "before": null, + "after": "ec4b5ceb31e42dceb71c2fb0f475daf868a19a08ad9644dcf17ee8c06c769786" + }, + { + "path": "resources/content/schema/base/aiResourceView.json", + "before": null, + "after": "024fb697f3d5e1ed511544ff6922f3b78c351f01e74df1a36faeee3e7f3dba95" + }, + { + "path": "resources/content/schema/base/aiToolCallEvent.json", + "before": "58333f82f8842bf13ed0780251a21000a0f59a56a1ef1bc4354dd03aca4cd7e8", + "after": "6d4cfccd6f4901d71e7ba8ec0818de0f769f3bd8e41794dc4ca9616a3794bfbf" + }, + { + "path": "resources/content/schema/project/project-auth.json", + "before": "c939bcf67064be956929820f561a5dde9512facf16eb18dc4f7910ab16e852c3", + "after": "96dcb3b12d6ba0a15e5532df71c68683b2fe87a8946ecb71c10109dea4f17585" + }, + { + "path": "resources/content/schema/user/user-auth.json", + "before": "9080a03720c6c9a9bdc84d3d9fb82ed5e2d0cbbcf16d5774a2a842d8677731f7", + "after": "91c44ff87f288c967d38cfe0c6c1dd04e4ac09919cd6c6ba8407806a61168a2f" + } ] }, "schemas": [ @@ -352,161 +558,24 @@ "resources/content/schema/base/aiServiceUpgradePlan.json", "resources/content/schema/base/aiToolCallEvent.json", "resources/content/schema/base/aiResourceView.json", - "resources/content/schema/base/aiResourceChangePlan.json" - ], - "addedTuples": [ - [ - "aiResourceChangePlan", - "collection", - "GET", - "", - "", - "read" - ], - [ - "aiResourceChangePlan", - "collection", - "POST", - "", - "", - "update" - ], - [ - "aiResourceChangePlan", - "resource", - "GET", - "", - "", - "read" - ], - [ - "aiResourceChangePlan", - "resource", - "POST", - "apply", - "", - "update" - ], - [ - "aiResourceChangePlan", - "resource", - "POST", - "cancel", - "", - "ai-change:cancel" - ], - [ - "aiResourceChangePlan", - "resource", - "POST", - "confirm", - "", - "ai-change:confirm" - ], - [ - "aiResourceView", - "collection", - "POST", - "", - "", - "read" - ] - ], - "changedTuples": [ - { - "before": [ - "stack", - "resource", - "POST", - "activateservices", - "", - "action:activateservices" - ], - "after": [ - "stack", - "resource", - "POST", - "activateservices", - "", - "update" - ], - "rationale": "Existing Stack child-service lifecycle handler; fixed typed pair, live child authorization remains required." - }, - { - "before": [ - "stack", - "resource", - "POST", - "deactivateservices", - "", - "action:deactivateservices" - ], - "after": [ - "stack", - "resource", - "POST", - "deactivateservices", - "", - "update" - ], - "rationale": "Existing Stack child-service lifecycle handler; fixed typed pair, live child authorization remains required." - } + "resources/content/schema/base/aiResourceChangePlan.json", + "resources/content/schema/base/processPool.json" ], + "addedTuples": [], + "changedTuples": [], "authorizationRationale": { - "aiResourceView": "Read semantic POST. Synthetic admission is followed by authorized domain reads and safe projection, never raw GET expansion.", - "aiResourceChangePlan": "Preview/apply are classified update admission only. Fixed domain adapter rechecks the actual create/update/delete operation and Key references. Collection GET and confirm/cancel are human-only; every Key is denied confirmation/cancellation.", - "stackServiceActions": "The two exact Stack batch actions preflight every persisted child with current owner and same-project/Stack checks. An authorized root grant is inherited only for dependency evaluation; explicit child DENY still wins before manager dispatch. The same recheck applies to human approval of bound-Key plans, and queued execution retains its independent live child checks. General Full and legacy admission paths are unchanged." + "runtimeReadFixes": "ProcessPool owner supplies stable executor bean identity, distinct from native null database IDs and nonunique display names; the opted-in metadata field is read-only and adds no methods. AI projects only its opaque digest. activeSetting is not a list-capable AI type; native schemas are unchanged. Existing domain validator-decoded decimal IDs are consumed directly as internal row IDs, while public parseScopeId retains strict wire round-trip and normal owner/Key grants/denials remain authoritative." }, "sourceInputChanges": [ - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyAuthorizationService.java", - "before": "8062f242159f18d5c21fc14ab65ce42c6cbb8a3bd6cb30dc0cb461bc9a282fbb", - "after": "9ceda0f5118d38fc45fc83bd4a16eb92717a20f7837bcabb804975600c59d2c5" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyOperations.java", - "before": "5b13ab34ee167596cc7f4b664dcc6afc6ab18f12442ed6d44a35b58ad08b40b9", - "after": "494a57d48564da85f14de564a82906444fa732f1adb201fe27d8c0ca88ea65e5" - }, { "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyTargetResolver.java", - "before": "391918c715b70d8fd9a607da01ac9acc85a1e3e04901c12cc21feb6ba1d98c2e", - "after": "b31333ffc5de2ec8fbee9d38daacd83b91578525d1fe1b61a73abac6355aa009" + "before": "b31333ffc5de2ec8fbee9d38daacd83b91578525d1fe1b61a73abac6355aa009", + "after": "fcd8d3724c1a37b2b77cab3e60ee3e973b349462833c1bf0bcc8f099d6f3567c" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanResourceManager.java", + "path": "resources/content/schema/base/processPool.json", "before": null, - "after": "37da81bdccdb528c1d7f1b18217d3896cabd0ad601e6a3809fd50913b6613050" - }, - { - "path": "resources/content/schema/admin/admin-auth.json", - "before": "be3f0ba2a4fa51a1c42fbf4fc7abe457b65420e8664301fa462474e015a6796f", - "after": "79a757118dd66055c6e7e4c03ded4b0dd8ef1965887c50a4c106e15836d4938f" - }, - { - "path": "resources/content/schema/base/aiResourceChangePlan.json", - "before": null, - "after": "ec4b5ceb31e42dceb71c2fb0f475daf868a19a08ad9644dcf17ee8c06c769786" - }, - { - "path": "resources/content/schema/base/aiResourceView.json", - "before": null, - "after": "024fb697f3d5e1ed511544ff6922f3b78c351f01e74df1a36faeee3e7f3dba95" - }, - { - "path": "resources/content/schema/base/aiToolCallEvent.json", - "before": "58333f82f8842bf13ed0780251a21000a0f59a56a1ef1bc4354dd03aca4cd7e8", - "after": "6d4cfccd6f4901d71e7ba8ec0818de0f769f3bd8e41794dc4ca9616a3794bfbf" - }, - { - "path": "resources/content/schema/project/project-auth.json", - "before": "c939bcf67064be956929820f561a5dde9512facf16eb18dc4f7910ab16e852c3", - "after": "96dcb3b12d6ba0a15e5532df71c68683b2fe87a8946ecb71c10109dea4f17585" - }, - { - "path": "resources/content/schema/user/user-auth.json", - "before": "9080a03720c6c9a9bdc84d3d9fb82ed5e2d0cbbcf16d5774a2a842d8677731f7", - "after": "91c44ff87f288c967d38cfe0c6c1dd04e4ac09919cd6c6ba8407806a61168a2f" + "after": "d055fc0e3bc30ffeb9567ca6a2c8cee7b7382200fe410c2e6154353305134b1b" } ] }, @@ -521,18 +590,14 @@ "addedTuples": [], "changedTuples": [], "authorizationRationale": { - "runtimeReadFixes": "ProcessPool owner supplies stable executor bean identity, distinct from native null database IDs and nonunique display names; the opted-in metadata field is read-only and adds no methods. AI projects only its opaque digest. activeSetting is not a list-capable AI type; native schemas are unchanged. Existing domain validator-decoded decimal IDs are consumed directly as internal row IDs, while public parseScopeId retains strict wire round-trip and normal owner/Key grants/denials remain authoritative." + "managedLifecycle": "Closed engine-captured scheduling edges preserve exact managed retirement ancestry while live owner/Key/revision/explicit-DENY remains authoritative; ordinary ancestry is not broadened and named/file storage does not inherit destructive removal.", + "nativeNameSearch": "Closed native name conditions narrow the already-authorized owner query before its bounded page; literal query and cursor/parent restrictions are retained. Memory projections filter before taking a page." }, "sourceInputChanges": [ { "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyTargetResolver.java", - "before": "b31333ffc5de2ec8fbee9d38daacd83b91578525d1fe1b61a73abac6355aa009", - "after": "fcd8d3724c1a37b2b77cab3e60ee3e973b349462833c1bf0bcc8f099d6f3567c" - }, - { - "path": "resources/content/schema/base/processPool.json", - "before": null, - "after": "d055fc0e3bc30ffeb9567ca6a2c8cee7b7382200fe410c2e6154353305134b1b" + "before": "fcd8d3724c1a37b2b77cab3e60ee3e973b349462833c1bf0bcc8f099d6f3567c", + "after": "645652a1352b74cf6ad4c2b1a9bbdfee1d168cef9df7c59e459d84c17d547c17" } ] }, @@ -542,19 +607,319 @@ "resources/content/schema/base/aiToolCallEvent.json", "resources/content/schema/base/aiResourceView.json", "resources/content/schema/base/aiResourceChangePlan.json", - "resources/content/schema/base/processPool.json" + "resources/content/schema/base/processPool.json", + "resources/content/schema/base/aiCatalogMetadataView.json", + "resources/content/schema/base/aiCatalogStackLaunchOptions.json", + "resources/content/schema/base/aiCatalogStackLaunchPlan.json", + "resources/content/schema/base/aiCatalogStackLaunchInputs.json" + ], + "addedTuples": [ + [ + "aiCatalogMetadataView", + "collection", + "POST", + "", + "", + "read" + ], + [ + "aiCatalogStackLaunchOptions", + "collection", + "POST", + "", + "", + "read" + ], + [ + "aiCatalogStackLaunchPlan", + "collection", + "GET", + "", + "", + "read" + ], + [ + "aiCatalogStackLaunchPlan", + "collection", + "POST", + "", + "", + "create" + ], + [ + "aiCatalogStackLaunchPlan", + "resource", + "GET", + "", + "", + "read" + ], + [ + "aiCatalogStackLaunchPlan", + "resource", + "POST", + "apply", + "", + "create" + ], + [ + "aiCatalogStackLaunchPlan", + "resource", + "POST", + "cancel", + "", + "ai-catalog-launch:cancel" + ], + [ + "aiCatalogStackLaunchPlan", + "resource", + "POST", + "completeInputs", + "", + "ai-catalog-launch:completeInputs" + ], + [ + "aiCatalogStackLaunchPlan", + "resource", + "POST", + "confirm", + "", + "ai-catalog-launch:confirm" + ] + ], + "changedTuples": [ + { + "before": [ + "service", + "resource", + "POST", + "setservicelinks", + "", + "action:setservicelinks" + ], + "after": [ + "service", + "resource", + "POST", + "setservicelinks", + "", + "update" + ], + "rationale": "Only the existing Service.setservicelinks owner is update. The typed launch preflights original live destination update and each delegated exact action; create does not imply update. Unknown plugin roots remain denied." + } ], - "addedTuples": [], - "changedTuples": [], "authorizationRationale": { - "managedLifecycle": "Closed engine-captured scheduling edges preserve exact managed retirement ancestry while live owner/Key/revision/explicit-DENY remains authoritative; ordinary ancestry is not broadened and named/file storage does not inherit destructive removal.", - "nativeNameSearch": "Closed native name conditions narrow the already-authorized owner query before its bounded page; literal query and cursor/parent restrictions are retained. Memory projections filter before taking a page." + "aiCatalogStackLaunchOptions": "Read-only fixed owner projection; no create grant or secret defaults.", + "aiCatalogStackLaunchPlan": "Dedicated durable plan. Preview/confirm/apply recheck original live destination read/create/update and references; completeInputs/confirm/cancel are verified Cookie-only, including for GENERAL FULL Keys. Apply claims once and normal Stack/Compose owners execute an exact per-event graph, with per-request actor/Key/source revalidation; private answers and proof never enter public DTO or audit payload.", + "serviceSetServiceLinks": "Exact Service action classified update; role schema, original live Key target scope and explicit DENY still apply. No plugin or create-to-update grant.", + "privateOwnerChannel": "Public Catalog/raw proxy initial and redirect targets reject canonical v1-catalog/ai namespace before body/upstream; fixed typed owner loopback calls retain their independent live authorization.", + "aiCatalogMetadataView": "Fixed metadata read POST. Fresh active owner/current-project membership plus live Key PLATFORM catalogTemplate read precede a fixed loopback Catalog projection. No raw proxy/accounts grant, project ancestry inference, forwarded credentials or new JWT. Fine-grained selectors fail closed; template/version logical identities are not cache row IDs." }, "sourceInputChanges": [ + { + "path": "code/framework/api/src/main/java/io/cattle/platform/api/auth/ApiAuditPayload.java", + "before": null, + "after": "697bb6f57b26354f336e14f905bc2a87f8a41acfdf725500652100e90d4e4381" + }, + { + "path": "code/framework/api/src/main/java/io/cattle/platform/api/auth/ApiCookieAuthentication.java", + "before": null, + "after": "b49fe3b141ac1689e49a583bab05caf7a449dc2b3495b0ba391534bc5812d3c4" + }, + { + "path": "code/framework/api/src/main/java/io/cattle/platform/api/auth/CatalogLaunchTransformAuthorization.java", + "before": null, + "after": "b104bb0aed1ae18dc7485aa1f7c7eddf3b6eb9176b7e7209cb2b89534600e4c6" + }, + { + "path": "code/framework/auditing/src/main/java/io/cattle/platform/iaas/api/auditing/AuditServiceImpl.java", + "before": null, + "after": "1f834204996aabfd6e6d3d5bb520d35b80547c5e77b8edea8f25d8d6010cfd4e" + }, + { + "path": "code/framework/eventing/src/main/java/io/cattle/platform/eventing/model/EventDiagnostics.java", + "before": null, + "after": "affeccbf8445e1d61b525df683504de7c084642a52cec8e6fb9371ca935c3034" + }, + { + "path": "code/iaas/api-logic/src/main/java/io/cattle/platform/iaas/api/request/handler/GenericWhitelistedProxy.java", + "before": null, + "after": "9606a7e0f76cb4c7eba033afc897d6d9e5d61045262d582ee94604169817608a" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchAccess.java", + "before": null, + "after": "639ceb2151b6668af77ff2f71ead2cd7699d76d45c17521e46a90af624286b52" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchBinding.java", + "before": null, + "after": "9f75784fa67b7341439d3ffecf8bada16ef0e55a7db3bbb41d6eceed4db90e8d" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchDelegation.java", + "before": null, + "after": "e75f911701471e63f85b70df645040a4142704f8da94f3008ed2187c43b35a1d" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchGateway.java", + "before": null, + "after": "9a12293de31831d7dda5f2e787b809351c347bf95e7b95f6798bb7e18d5ae12c" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchGraph.java", + "before": null, + "after": "26618418c19bd7eef3dc7e5e4cd526072f3a614535a04b914e0559a20f12d875" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchInput.java", + "before": null, + "after": "3db05d510e3e057ae5d10340e0e313111de0a5a7e5558a477e91395f1f4280de" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchMaterializer.java", + "before": null, + "after": "00945aae0ccd8091f6835168bd0d0ff7615be2f00cc86c27f48218520d680f50" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchObservation.java", + "before": null, + "after": "e3ecb29c1585002c61a4c3a6e14e94868c4e9de6da94e5098eb5d2f14b93d82d" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchOptionsResourceManager.java", + "before": null, + "after": "fb2977ff6cbd4819f5ecaa75afdcd5c3d9a3c43add3b5c2d4614379349e74552" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchOptionsService.java", + "before": null, + "after": "318bb0e18d8a7292ff57a0b281d53eb5040e7c4a5fef388baea4aac4acf30d5d" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchOptionsStore.java", + "before": null, + "after": "b4dbbfffcb333ce11c559e10eee78ce2ae9dc1fa5d4a958c475f5a893b270c2b" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPlanResourceManager.java", + "before": null, + "after": "ef70bb6e029fdb446358ad81e9f791f14e892c28bda754b8ce479077fb85ff98" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPlanService.java", + "before": null, + "after": "77b8f8b2b4ade110064ba0a874db04c1d5adbfe9df68655b8760c395270a05e3" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPlanStore.java", + "before": null, + "after": "8716e8f4038bad48098f16c9009446c4e77f379cba50d3049897aa2442fd7b22" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPrivate.java", + "before": null, + "after": "df91328e28fca5f15aceda4faaf57be47c8165dfc5cbf36894abd12935c8d3b2" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchResponseFilter.java", + "before": null, + "after": "0e35935d8e3ce19a843d3770b9a574b0c2d501612a8f6211f077834f7975bb60" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchRunner.java", + "before": null, + "after": "e6e530a39cc6ca2d270038f0084ed6c104f45d69ebfe7a307391e90918c0f6c9" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyAuthorizationService.java", + "before": "9ceda0f5118d38fc45fc83bd4a16eb92717a20f7837bcabb804975600c59d2c5", + "after": "d3cac29f7b6f61be0bda38f643115d08877d59e99b9312575f064290e73ede27" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyOperations.java", + "before": "494a57d48564da85f14de564a82906444fa732f1adb201fe27d8c0ca88ea65e5", + "after": "28847a8d6873d8e8fe1644a19813e8f8f9d1917a5c1b369dab8abcb882e6a9b6" + }, { "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyTargetResolver.java", - "before": "fcd8d3724c1a37b2b77cab3e60ee3e973b349462833c1bf0bcc8f099d6f3567c", - "after": "645652a1352b74cf6ad4c2b1a9bbdfee1d168cef9df7c59e459d84c17d547c17" + "before": "645652a1352b74cf6ad4c2b1a9bbdfee1d168cef9df7c59e459d84c17d547c17", + "after": "95e9c38861b984e995770e5ddf6d7af21c96d1bff1d8848d1ad2e3a54722efd2" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/impl/ApiAuthenticator.java", + "before": "7a3e331dc9e23ae1689495b6ce4066a9decb8293def66fbffa6e0e404697db12", + "after": "d412d32cf7032b4bb0dde546a9e22193fc29ecae71b5c1efcbb052bd4590945d" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/integration/internal/rancher/TokenAuthLookup.java", + "before": null, + "after": "11c66db998415be5deab8a34df362dcb299a7b1a8df938ef1afb6f6602b71c91" + }, + { + "path": "code/iaas/logic-common/src/main/java/io/cattle/platform/process/common/handler/EventBasedProcessHandler.java", + "before": null, + "after": "d784906d3bd0e0399dc38cd38ddc5ee38c49850e7a51ea07d2a4a158dcb068de" + }, + { + "path": "code/iaas/logic-common/src/main/java/io/cattle/platform/process/common/handler/ProcessEventDecorator.java", + "before": null, + "after": "b0dc3d894c3ae0345e374cda8af588ac2e6c17c74f25501e6689a28d4ef934e5" + }, + { + "path": "code/iaas/service-discovery/api/src/main/java/io/cattle/platform/servicediscovery/api/filter/ArchiveValidationFilter.java", + "before": "63fbdcc3d0ac73186b181afc55d0e0aae4424a347a367724348802246cad823c", + "after": "908e6e0bf0d665d2392481f6d6e59d27c3cd89b4b35471fe587d6ed14e591890" + }, + { + "path": "code/implementation/docker/api/src/main/java/io/cattle/platform/docker/api/transform/TransformInspect.java", + "before": null, + "after": "c67974b0abbb3b7c7801b51ca95a649e95e0853fba9c1500da6b4b5977a0afb2" + }, + { + "path": "code/packaging/app-config/src/main/java/io/cattle/platform/app/IaasApiConfig.java", + "before": null, + "after": "02184dc6ae781547e850c80cf51ddb4f33cbc7b2f799aba3ca1e59c8eb9cf34f" + }, + { + "path": "resources/content/schema/admin/admin-auth.json", + "before": "79a757118dd66055c6e7e4c03ded4b0dd8ef1965887c50a4c106e15836d4938f", + "after": "6ea49e1130a2b9218e6c060038c73c89be5b9e05b039d3a0b81475e892d21504" + }, + { + "path": "resources/content/schema/base/aiCatalogMetadataView.json", + "before": null, + "after": "5581897314b1cfe9135783fb8846db50ff6f9d62289b5ab35e60ab7ca8b36515" + }, + { + "path": "resources/content/schema/base/aiCatalogStackLaunchInputs.json", + "before": null, + "after": "05aa9a7b459a82efcf0331b3d6c8262e1b5c879643aef6384c67e3a12980bf4c" + }, + { + "path": "resources/content/schema/base/aiCatalogStackLaunchOptions.json", + "before": null, + "after": "30137b75ac50b6b04adec688ca56359fd7d49843f8342c423a906b998da5c12a" + }, + { + "path": "resources/content/schema/base/aiCatalogStackLaunchPlan.json", + "before": null, + "after": "5886387a8b25893d4afc5bb127dd8305646748e08a86b65002d8e0d8e05b328d" + }, + { + "path": "resources/content/schema/base/aiResourceChangePlan.json", + "before": "ec4b5ceb31e42dceb71c2fb0f475daf868a19a08ad9644dcf17ee8c06c769786", + "after": "21b4acfd55e2aa64b725bacd520655daf7f707141e4226343c16a5b6fae47d8c" + }, + { + "path": "resources/content/schema/project/project-auth.json", + "before": "96dcb3b12d6ba0a15e5532df71c68683b2fe87a8946ecb71c10109dea4f17585", + "after": "a924ebe3e6ea7a39374d7d162fd020807cb94900c849c82d286df121db27ee6c" + }, + { + "path": "resources/content/schema/user/user-auth.json", + "before": "91c44ff87f288c967d38cfe0c6c1dd04e4ac09919cd6c6ba8407806a61168a2f", + "after": "0f977be3a4e6718753ba8a248e73f1014886afbee1d5387249e762b4146b7b73" } ] }, @@ -568,315 +933,229 @@ "resources/content/schema/base/aiCatalogMetadataView.json", "resources/content/schema/base/aiCatalogStackLaunchOptions.json", "resources/content/schema/base/aiCatalogStackLaunchPlan.json", - "resources/content/schema/base/aiCatalogStackLaunchInputs.json" + "resources/content/schema/base/aiCatalogStackLaunchInputs.json", + "resources/content/schema/base/aiAuditQuery.json", + "resources/content/schema/base/aiAuditSourcePageInput.json" ], "addedTuples": [ [ - "aiCatalogMetadataView", - "collection", - "POST", - "", - "", - "read" - ], - [ - "aiCatalogStackLaunchOptions", + "aiAuditQuery", "collection", "POST", "", "", "read" - ], - [ - "aiCatalogStackLaunchPlan", - "collection", - "GET", - "", - "", - "read" - ], - [ - "aiCatalogStackLaunchPlan", - "collection", - "POST", - "", - "", - "create" - ], - [ - "aiCatalogStackLaunchPlan", - "resource", - "GET", - "", - "", - "read" - ], - [ - "aiCatalogStackLaunchPlan", - "resource", - "POST", - "apply", - "", - "create" - ], - [ - "aiCatalogStackLaunchPlan", - "resource", - "POST", - "cancel", - "", - "ai-catalog-launch:cancel" - ], - [ - "aiCatalogStackLaunchPlan", - "resource", - "POST", - "completeInputs", - "", - "ai-catalog-launch:completeInputs" - ], - [ - "aiCatalogStackLaunchPlan", - "resource", - "POST", - "confirm", - "", - "ai-catalog-launch:confirm" ] ], "changedTuples": [ - { - "before": [ - "service", + [ + [ + "dnsService", "resource", "POST", "setservicelinks", "", "action:setservicelinks" ], - "after": [ - "service", + [ + "dnsService", "resource", "POST", "setservicelinks", "", "update" - ], - "rationale": "Only the existing Service.setservicelinks owner is update. The typed launch preflights original live destination update and each delegated exact action; create does not imply update. Unknown plugin roots remain denied." - } + ] + ] ], "authorizationRationale": { - "aiCatalogStackLaunchOptions": "Read-only fixed owner projection; no create grant or secret defaults.", - "aiCatalogStackLaunchPlan": "Dedicated durable plan. Preview/confirm/apply recheck original live destination read/create/update and references; completeInputs/confirm/cancel are verified Cookie-only, including for GENERAL FULL Keys. Apply claims once and normal Stack/Compose owners execute an exact per-event graph, with per-request actor/Key/source revalidation; private answers and proof never enter public DTO or audit payload.", - "serviceSetServiceLinks": "Exact Service action classified update; role schema, original live Key target scope and explicit DENY still apply. No plugin or create-to-update grant.", - "privateOwnerChannel": "Public Catalog/raw proxy initial and redirect targets reject canonical v1-catalog/ai namespace before body/upstream; fixed typed owner loopback calls retain their independent live authorization.", - "aiCatalogMetadataView": "Fixed metadata read POST. Fresh active owner/current-project membership plus live Key PLATFORM catalogTemplate read precede a fixed loopback Catalog projection. No raw proxy/accounts grant, project ancestry inference, forwarded credentials or new JWT. Fine-grained selectors fail closed; template/version logical identities are not cache row IDs." + "aiAuditQuery": "Read-semantic metadata admission only. Original native role/schema/current Key and SQL/row filters authorize each source page. Fixed installed broker read-only CLI aggregates using private original credentials; Engine observes complete owner output and persists canonical ALLOW/DENY/error through existing ApiKeyAuditSink. No client-reported result is authoritative.", + "aiAuditSourcePages": "Exactly project/account/apiKey/auditLog with bounded typed ID/time/page input; no independent input route. Original account/project selection, current membership and each native GET apply. Restricted Keys cannot mutate/export credentials; target-Key query requires original actual Key metadata read. Every Key source page pins revision; native API direct/raw routes retain their prior purpose guard and Full/legacy behavior." }, "sourceInputChanges": [ { "path": "code/framework/api/src/main/java/io/cattle/platform/api/auth/ApiAuditPayload.java", - "before": null, - "after": "697bb6f57b26354f336e14f905bc2a87f8a41acfdf725500652100e90d4e4381" + "before": "697bb6f57b26354f336e14f905bc2a87f8a41acfdf725500652100e90d4e4381", + "after": "16122e192f490b7866d3b0af3ed4981184c938d8446ec9b7d4c892d68cf2d67d" }, { - "path": "code/framework/api/src/main/java/io/cattle/platform/api/auth/ApiCookieAuthentication.java", + "path": "code/framework/api/src/main/java/io/cattle/platform/api/auth/ApiKeyAuditSink.java", "before": null, - "after": "b49fe3b141ac1689e49a583bab05caf7a449dc2b3495b0ba391534bc5812d3c4" + "after": "c04eec56c774f68254c832cc8916b57ec000d5fce3bd07e5cafb65260a48fe9b" }, { - "path": "code/framework/api/src/main/java/io/cattle/platform/api/auth/CatalogLaunchTransformAuthorization.java", + "path": "code/framework/auditing/src/main/java/io/cattle/platform/iaas/api/auditing/AuditLogOutPutFilter.java", "before": null, - "after": "b104bb0aed1ae18dc7485aa1f7c7eddf3b6eb9176b7e7209cb2b89534600e4c6" + "after": "368c21b043bdfc149f3d5a8663cf12b6ad93f0efdec0df6b7e065d62200f4ac7" }, { "path": "code/framework/auditing/src/main/java/io/cattle/platform/iaas/api/auditing/AuditServiceImpl.java", - "before": null, - "after": "1f834204996aabfd6e6d3d5bb520d35b80547c5e77b8edea8f25d8d6010cfd4e" + "before": "1f834204996aabfd6e6d3d5bb520d35b80547c5e77b8edea8f25d8d6010cfd4e", + "after": "a10a5d8e7ca754601ea2c5a876c318069b6639c073e616d283ae71694bc95179" }, { - "path": "code/framework/eventing/src/main/java/io/cattle/platform/eventing/model/EventDiagnostics.java", + "path": "code/framework/engine/src/main/java/io/cattle/platform/engine/process/ProcessAuthorization.java", "before": null, - "after": "affeccbf8445e1d61b525df683504de7c084642a52cec8e6fb9371ca935c3034" + "after": "a717b03de1ec528f7047161679be436535d68fb416b573c3aaf5b09a2e44b520" }, { - "path": "code/iaas/api-logic/src/main/java/io/cattle/platform/iaas/api/request/handler/GenericWhitelistedProxy.java", - "before": null, - "after": "9606a7e0f76cb4c7eba033afc897d6d9e5d61045262d582ee94604169817608a" + "path": "code/framework/java-server/src/main/java/io/github/ibuildthecloud/gdapi/request/resource/ResourceManager.java", + "before": "8f5f72c17541e73cab9f59ad8d36c06593baef517628a923967bc4c18f82b999", + "after": "ef08c305584fe79e2218d360dbc2ce6f3b29b64f4123ac325c8473430bc0a1f1" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchAccess.java", - "before": null, - "after": "639ceb2151b6668af77ff2f71ead2cd7699d76d45c17521e46a90af624286b52" + "path": "code/framework/java-server/src/main/java/io/github/ibuildthecloud/gdapi/request/resource/impl/FilteredResourceManager.java", + "before": "a140040f0be95fd7872eb7f7e78f5824329725a109d7bc0c79d960dbc0709e77", + "after": "d2c6a28809df4912b2b22ce22e9f4e48304b91c993144867d05fa5d65f07ae76" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchBinding.java", + "path": "code/framework/schema/src/main/java/io/cattle/platform/schema/processor/NotWritablePostProcessor.java", "before": null, - "after": "9f75784fa67b7341439d3ffecf8bada16ef0e55a7db3bbb41d6eceed4db90e8d" + "after": "c42a2b1aea75d4daae5671c702e3242c9cd1e7d32e7a5280fc3a0f6415ca829a" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchDelegation.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQuery.java", "before": null, - "after": "e75f911701471e63f85b70df645040a4142704f8da94f3008ed2187c43b35a1d" + "after": "6ef0559fdec19c2fe76742587dd5b5c88b5ec904ee3544f70dd6937fffe1e455" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchGateway.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryResourceManager.java", "before": null, - "after": "9a12293de31831d7dda5f2e787b809351c347bf95e7b95f6798bb7e18d5ae12c" + "after": "bdeaebe2fbc3f84be1c9bc19b0bcc7d534a5d43888cc3a95ec446d300458c8c2" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchGraph.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryRunner.java", "before": null, - "after": "26618418c19bd7eef3dc7e5e4cd526072f3a614535a04b914e0559a20f12d875" + "after": "608fa5f5b412dccdf154290094ff1d67204c092cfb60b850d8d7a3124f53c812" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchInput.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryService.java", "before": null, - "after": "3db05d510e3e057ae5d10340e0e313111de0a5a7e5558a477e91395f1f4280de" + "after": "4067cc8685399b2b2bfb9e7b5eaddd10b906f63fccf286ef7e118622e4e80ea6" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchMaterializer.java", - "before": null, - "after": "00945aae0ccd8091f6835168bd0d0ff7615be2f00cc86c27f48218520d680f50" + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchResponseFilter.java", + "before": "0e35935d8e3ce19a843d3770b9a574b0c2d501612a8f6211f077834f7975bb60", + "after": "9884cc5bc272d0a872ab7b48f5365863dfc8154b0e6c291fe418bdc14541be73" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchObservation.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/view/AiAuditSourcePage.java", "before": null, - "after": "e3ecb29c1585002c61a4c3a6e14e94868c4e9de6da94e5098eb5d2f14b93d82d" + "after": "4e47ab9f1fef9108e68a0471b633d3d99f5f5069591e7dcecfe57018f09b6b78" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchOptionsResourceManager.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/view/AiAuditSourcePageInput.java", "before": null, - "after": "fb2977ff6cbd4819f5ecaa75afdcd5c3d9a3c43add3b5c2d4614379349e74552" + "after": "771bdb99ca900a268c900c957d476f3f8d96b31ce45d56583e7c572f3abe73c5" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchOptionsService.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/view/AiResourceViewResourceManager.java", "before": null, - "after": "318bb0e18d8a7292ff57a0b281d53eb5040e7c4a5fef388baea4aac4acf30d5d" + "after": "ea08a26aee7cfa32df3485dd9c4e2e7bd63ca5d2e680cdbabdfcdc5f3c63b8dd" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchOptionsStore.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/view/AiResourceViewService.java", "before": null, - "after": "b4dbbfffcb333ce11c559e10eee78ce2ae9dc1fa5d4a958c475f5a893b270c2b" + "after": "54f9aeaeecbf135e34210417f5cf7ec128a90e05b1971fc351ed4bdd76bf8585" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPlanResourceManager.java", - "before": null, - "after": "ef70bb6e029fdb446358ad81e9f791f14e892c28bda754b8ce479077fb85ff98" + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyAuthorizationService.java", + "before": "d3cac29f7b6f61be0bda38f643115d08877d59e99b9312575f064290e73ede27", + "after": "85a065246a8f40e1d9a64d984f49368d488e5d9b712029b5e3e306ca37aeab47" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPlanService.java", - "before": null, - "after": "77b8f8b2b4ade110064ba0a874db04c1d5adbfe9df68655b8760c395270a05e3" + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyOperations.java", + "before": "28847a8d6873d8e8fe1644a19813e8f8f9d1917a5c1b369dab8abcb882e6a9b6", + "after": "662bbef806ecf3b266699bb00809fe62b712c9610b36788cfb76e244de37940a" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPlanStore.java", - "before": null, - "after": "8716e8f4038bad48098f16c9009446c4e77f379cba50d3049897aa2442fd7b22" + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyProcessAuthorization.java", + "before": "65052468195ad36213f68dc585d63d1cebc13409f7a92704d53297e2efcb482e", + "after": "1bf968d28d977725fa426fe7f32b00d5bc322979c0bfe1e0501fc2c34c03100e" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPrivate.java", - "before": null, - "after": "df91328e28fca5f15aceda4faaf57be47c8165dfc5cbf36894abd12935c8d3b2" + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyTargetResolver.java", + "before": "95e9c38861b984e995770e5ddf6d7af21c96d1bff1d8848d1ad2e3a54722efd2", + "after": "3c299d683055c0301f93b4bc8d23252215d1d9c0746d0aaafa0b52447d3eb26b" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchResponseFilter.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiAliasServiceChange.java", "before": null, - "after": "0e35935d8e3ce19a843d3770b9a574b0c2d501612a8f6211f077834f7975bb60" + "after": "fa69385c6ffee09261466e995e59474b632302225088729de3242ccaf2b2118b" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchRunner.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiExternalServiceChange.java", "before": null, - "after": "e6e530a39cc6ca2d270038f0084ed6c104f45d69ebfe7a307391e90918c0f6c9" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyAuthorizationService.java", - "before": "9ceda0f5118d38fc45fc83bd4a16eb92717a20f7837bcabb804975600c59d2c5", - "after": "d3cac29f7b6f61be0bda38f643115d08877d59e99b9312575f064290e73ede27" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyOperations.java", - "before": "494a57d48564da85f14de564a82906444fa732f1adb201fe27d8c0ca88ea65e5", - "after": "28847a8d6873d8e8fe1644a19813e8f8f9d1917a5c1b369dab8abcb882e6a9b6" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyTargetResolver.java", - "before": "645652a1352b74cf6ad4c2b1a9bbdfee1d168cef9df7c59e459d84c17d547c17", - "after": "95e9c38861b984e995770e5ddf6d7af21c96d1bff1d8848d1ad2e3a54722efd2" + "after": "d9497a9c37842c09e833d434e5f361585f721a72eff9976c5b66902181f392e5" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/impl/ApiAuthenticator.java", - "before": "7a3e331dc9e23ae1689495b6ce4066a9decb8293def66fbffa6e0e404697db12", - "after": "d412d32cf7032b4bb0dde546a9e22193fc29ecae71b5c1efcbb052bd4590945d" + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDescriptor.java", + "before": null, + "after": "2ef7555c7864a2ce91a7c28951e895da412f68849b6cc3ec04350a75b35186f8" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/integration/internal/rancher/TokenAuthLookup.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDomain.java", "before": null, - "after": "11c66db998415be5deab8a34df362dcb299a7b1a8df938ef1afb6f6602b71c91" + "after": "aafe0ea7a5f60f7e9dc926067318cb2d470ec066cc1e5986b76b2c79122570a4" }, { - "path": "code/iaas/logic-common/src/main/java/io/cattle/platform/process/common/handler/EventBasedProcessHandler.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanService.java", "before": null, - "after": "d784906d3bd0e0399dc38cd38ddc5ee38c49850e7a51ea07d2a4a158dcb068de" + "after": "d0605f3e94757996e699e4083b1cd314d6d68acf623358a92f9841bc75384fb9" }, { - "path": "code/iaas/logic-common/src/main/java/io/cattle/platform/process/common/handler/ProcessEventDecorator.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanStore.java", "before": null, - "after": "b0dc3d894c3ae0345e374cda8af588ac2e6c17c74f25501e6689a28d4ef934e5" + "after": "9862ed000b4b40a6a7643968313c250fad9b1e718a684dccbab8f35c3a29cd5f" }, { - "path": "code/iaas/service-discovery/api/src/main/java/io/cattle/platform/servicediscovery/api/filter/ArchiveValidationFilter.java", - "before": "63fbdcc3d0ac73186b181afc55d0e0aae4424a347a367724348802246cad823c", - "after": "908e6e0bf0d665d2392481f6d6e59d27c3cd89b4b35471fe587d6ed14e591890" + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeProcessObserver.java", + "before": null, + "after": "6f7898590bb64e55cb815c9b1a51f4ce00540a6268e8fa4914ef1b6b445f5a70" }, { - "path": "code/implementation/docker/api/src/main/java/io/cattle/platform/docker/api/transform/TransformInspect.java", + "path": "code/iaas/service-discovery/server/src/main/java/io/cattle/platform/servicediscovery/dao/impl/ServiceConsumeMapDaoImpl.java", "before": null, - "after": "c67974b0abbb3b7c7801b51ca95a649e95e0853fba9c1500da6b4b5977a0afb2" + "after": "a127711511a3f1271d441235f9889d63e06a5c2d457974e4a45bd04f1e7e5747" }, { "path": "code/packaging/app-config/src/main/java/io/cattle/platform/app/IaasApiConfig.java", - "before": null, - "after": "02184dc6ae781547e850c80cf51ddb4f33cbc7b2f799aba3ca1e59c8eb9cf34f" + "before": "02184dc6ae781547e850c80cf51ddb4f33cbc7b2f799aba3ca1e59c8eb9cf34f", + "after": "59b633a3f6614f811b5f14b0eeeeb1f99683d319a2c28bea2093f48a33a53130" }, { "path": "resources/content/schema/admin/admin-auth.json", - "before": "79a757118dd66055c6e7e4c03ded4b0dd8ef1965887c50a4c106e15836d4938f", - "after": "6ea49e1130a2b9218e6c060038c73c89be5b9e05b039d3a0b81475e892d21504" + "before": "6ea49e1130a2b9218e6c060038c73c89be5b9e05b039d3a0b81475e892d21504", + "after": "27630fdb4d5cf7a1dcd7e693b84d0b31513d627a64a5d2365119075aca6d4f85" }, { - "path": "resources/content/schema/base/aiCatalogMetadataView.json", + "path": "resources/content/schema/base/aiAuditQuery.json", "before": null, - "after": "5581897314b1cfe9135783fb8846db50ff6f9d62289b5ab35e60ab7ca8b36515" + "after": "c12e694997a08c3e9849413f45b96d7d9cf2471289d35be54a60903d22e609eb" }, { - "path": "resources/content/schema/base/aiCatalogStackLaunchInputs.json", + "path": "resources/content/schema/base/aiAuditSourcePageInput.json", "before": null, - "after": "05aa9a7b459a82efcf0331b3d6c8262e1b5c879643aef6384c67e3a12980bf4c" + "after": "2aca5cba9323daae33a7171ac5f3bec2591cbdbffbdc32cf263ac8398a75e444" }, { - "path": "resources/content/schema/base/aiCatalogStackLaunchOptions.json", - "before": null, - "after": "30137b75ac50b6b04adec688ca56359fd7d49843f8342c423a906b998da5c12a" - }, - { - "path": "resources/content/schema/base/aiCatalogStackLaunchPlan.json", - "before": null, - "after": "5886387a8b25893d4afc5bb127dd8305646748e08a86b65002d8e0d8e05b328d" + "path": "resources/content/schema/base/aiResourceChangePlan.json", + "before": "21b4acfd55e2aa64b725bacd520655daf7f707141e4226343c16a5b6fae47d8c", + "after": "90db2324422f25129edf155127da8e29303b74fe2467797d4459b1df451e9439" }, { - "path": "resources/content/schema/base/aiResourceChangePlan.json", - "before": "ec4b5ceb31e42dceb71c2fb0f475daf868a19a08ad9644dcf17ee8c06c769786", - "after": "21b4acfd55e2aa64b725bacd520655daf7f707141e4226343c16a5b6fae47d8c" + "path": "resources/content/schema/base/aiResourceView.json", + "before": "024fb697f3d5e1ed511544ff6922f3b78c351f01e74df1a36faeee3e7f3dba95", + "after": "5bd8e7f91b44165d8f57ebb5b17689c40b2c236d1d6a80af0ade78dba552cc73" }, { "path": "resources/content/schema/project/project-auth.json", - "before": "96dcb3b12d6ba0a15e5532df71c68683b2fe87a8946ecb71c10109dea4f17585", - "after": "a924ebe3e6ea7a39374d7d162fd020807cb94900c849c82d286df121db27ee6c" + "before": "a924ebe3e6ea7a39374d7d162fd020807cb94900c849c82d286df121db27ee6c", + "after": "c752d35a5b2b2bdc65af5914d89071948e5b10e41772736ade7f89dfd2554a59" }, { "path": "resources/content/schema/user/user-auth.json", - "before": "91c44ff87f288c967d38cfe0c6c1dd04e4ac09919cd6c6ba8407806a61168a2f", - "after": "0f977be3a4e6718753ba8a248e73f1014886afbee1d5387249e762b4146b7b73" + "before": "0f977be3a4e6718753ba8a248e73f1014886afbee1d5387249e762b4146b7b73", + "after": "0f96a7e195647b967d4f83cab74d6a0fc5c442a896baa54384937af765bc2d9e" } ] }, @@ -894,225 +1173,60 @@ "resources/content/schema/base/aiAuditQuery.json", "resources/content/schema/base/aiAuditSourcePageInput.json" ], - "addedTuples": [ - [ - "aiAuditQuery", - "collection", - "POST", - "", - "", - "read" - ] - ], - "changedTuples": [ - [ - [ - "dnsService", - "resource", - "POST", - "setservicelinks", - "", - "action:setservicelinks" - ], - [ - "dnsService", - "resource", - "POST", - "setservicelinks", - "", - "update" - ] - ] - ], + "addedTuples": [], + "changedTuples": [], "authorizationRationale": { - "aiAuditQuery": "Read-semantic metadata admission only. Original native role/schema/current Key and SQL/row filters authorize each source page. Fixed installed broker read-only CLI aggregates using private original credentials; Engine observes complete owner output and persists canonical ALLOW/DENY/error through existing ApiKeyAuditSink. No client-reported result is authoritative.", - "aiAuditSourcePages": "Exactly project/account/apiKey/auditLog with bounded typed ID/time/page input; no independent input route. Original account/project selection, current membership and each native GET apply. Restricted Keys cannot mutate/export credentials; target-Key query requires original actual Key metadata read. Every Key source page pins revision; native API direct/raw routes retain their prior purpose guard and Full/legacy behavior." + "certificate": "Only name/description; current native method/field schema, row lock, safe UUID/public fingerprint CAS, Cookie-human approval and original Key revision remain required. No PEM/key/chain field or raw proxy is admitted.", + "nativeNetworkRemoval": "Actual immutable Java execution frames include native processName. Only Instance DELETE/delete, exact persisted owner/UUID/FK and all IP maps qualify for native nic.remove reentry. Missing/stop frames, foreign/shared/reassigned children and explicit NIC/IP/map/service DENY remain denied. Live paths and Full/legacy retain their original owner behavior.", + "terminalDenial": "Only the structured synchronous child AUTHORIZATION_DENIED terminates its parent. Other exit/retry semantics and durable completion-audit replay are unchanged.", + "auditOwnerTopology": "The fixed private CLI shares the configured broker upstream. Java reconstructs only HTTP(S) loopback origins and bounded ports from its exact nonsecret operator setting after clearing all other environment; credentials stay private stdin. No HTTP or tool-specified URL is accepted.", + "audit": "Existing durable metadata-only decision owner includes the explicitly reviewed certificate/externalService/dnsService domains. It does not authorize their operation or log private material." }, "sourceInputChanges": [ - { - "path": "code/framework/api/src/main/java/io/cattle/platform/api/auth/ApiAuditPayload.java", - "before": "697bb6f57b26354f336e14f905bc2a87f8a41acfdf725500652100e90d4e4381", - "after": "16122e192f490b7866d3b0af3ed4981184c938d8446ec9b7d4c892d68cf2d67d" - }, - { - "path": "code/framework/api/src/main/java/io/cattle/platform/api/auth/ApiKeyAuditSink.java", - "before": null, - "after": "c04eec56c774f68254c832cc8916b57ec000d5fce3bd07e5cafb65260a48fe9b" - }, - { - "path": "code/framework/auditing/src/main/java/io/cattle/platform/iaas/api/auditing/AuditLogOutPutFilter.java", - "before": null, - "after": "368c21b043bdfc149f3d5a8663cf12b6ad93f0efdec0df6b7e065d62200f4ac7" - }, { "path": "code/framework/auditing/src/main/java/io/cattle/platform/iaas/api/auditing/AuditServiceImpl.java", - "before": "1f834204996aabfd6e6d3d5bb520d35b80547c5e77b8edea8f25d8d6010cfd4e", - "after": "a10a5d8e7ca754601ea2c5a876c318069b6639c073e616d283ae71694bc95179" + "before": "a10a5d8e7ca754601ea2c5a876c318069b6639c073e616d283ae71694bc95179", + "after": "cb6bb0a8ed3dda48bedd3ae89ed78a490f644c1040c5aa16463e1e701250988e" }, { - "path": "code/framework/engine/src/main/java/io/cattle/platform/engine/process/ProcessAuthorization.java", + "path": "code/framework/engine/src/main/java/io/cattle/platform/engine/context/EngineContext.java", "before": null, - "after": "a717b03de1ec528f7047161679be436535d68fb416b573c3aaf5b09a2e44b520" - }, - { - "path": "code/framework/java-server/src/main/java/io/github/ibuildthecloud/gdapi/request/resource/ResourceManager.java", - "before": "8f5f72c17541e73cab9f59ad8d36c06593baef517628a923967bc4c18f82b999", - "after": "ef08c305584fe79e2218d360dbc2ce6f3b29b64f4123ac325c8473430bc0a1f1" + "after": "ba91b4da42d1f2ea5a2b9709818c2a8f4b967c2c730d5681b53d9dbe94ce8968" }, { - "path": "code/framework/java-server/src/main/java/io/github/ibuildthecloud/gdapi/request/resource/impl/FilteredResourceManager.java", - "before": "a140040f0be95fd7872eb7f7e78f5824329725a109d7bc0c79d960dbc0709e77", - "after": "d2c6a28809df4912b2b22ce22e9f4e48304b91c993144867d05fa5d65f07ae76" - }, - { - "path": "code/framework/schema/src/main/java/io/cattle/platform/schema/processor/NotWritablePostProcessor.java", + "path": "code/framework/engine/src/main/java/io/cattle/platform/engine/process/impl/DefaultProcessInstanceImpl.java", "before": null, - "after": "c42a2b1aea75d4daae5671c702e3242c9cd1e7d32e7a5280fc3a0f6415ca829a" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQuery.java", - "before": null, - "after": "6ef0559fdec19c2fe76742587dd5b5c88b5ec904ee3544f70dd6937fffe1e455" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryResourceManager.java", - "before": null, - "after": "bdeaebe2fbc3f84be1c9bc19b0bcc7d534a5d43888cc3a95ec446d300458c8c2" + "after": "986e14156ce42ac72033fad21289db4e3732469378cb6c9b0ae1d0a4c36376b0" }, { "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryRunner.java", - "before": null, - "after": "608fa5f5b412dccdf154290094ff1d67204c092cfb60b850d8d7a3124f53c812" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryService.java", - "before": null, - "after": "4067cc8685399b2b2bfb9e7b5eaddd10b906f63fccf286ef7e118622e4e80ea6" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchResponseFilter.java", - "before": "0e35935d8e3ce19a843d3770b9a574b0c2d501612a8f6211f077834f7975bb60", - "after": "9884cc5bc272d0a872ab7b48f5365863dfc8154b0e6c291fe418bdc14541be73" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/view/AiAuditSourcePage.java", - "before": null, - "after": "4e47ab9f1fef9108e68a0471b633d3d99f5f5069591e7dcecfe57018f09b6b78" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/view/AiAuditSourcePageInput.java", - "before": null, - "after": "771bdb99ca900a268c900c957d476f3f8d96b31ce45d56583e7c572f3abe73c5" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/view/AiResourceViewResourceManager.java", - "before": null, - "after": "ea08a26aee7cfa32df3485dd9c4e2e7bd63ca5d2e680cdbabdfcdc5f3c63b8dd" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/view/AiResourceViewService.java", - "before": null, - "after": "54f9aeaeecbf135e34210417f5cf7ec128a90e05b1971fc351ed4bdd76bf8585" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyAuthorizationService.java", - "before": "d3cac29f7b6f61be0bda38f643115d08877d59e99b9312575f064290e73ede27", - "after": "85a065246a8f40e1d9a64d984f49368d488e5d9b712029b5e3e306ca37aeab47" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyOperations.java", - "before": "28847a8d6873d8e8fe1644a19813e8f8f9d1917a5c1b369dab8abcb882e6a9b6", - "after": "662bbef806ecf3b266699bb00809fe62b712c9610b36788cfb76e244de37940a" + "before": "608fa5f5b412dccdf154290094ff1d67204c092cfb60b850d8d7a3124f53c812", + "after": "684fa84a259118a13d5a1ef20f76e05592ef867492e3fc54c1dcb94c06bb5758" }, { "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyProcessAuthorization.java", - "before": "65052468195ad36213f68dc585d63d1cebc13409f7a92704d53297e2efcb482e", - "after": "1bf968d28d977725fa426fe7f32b00d5bc322979c0bfe1e0501fc2c34c03100e" + "before": "1bf968d28d977725fa426fe7f32b00d5bc322979c0bfe1e0501fc2c34c03100e", + "after": "39ac76abfff5b7a8d95f722f72a4869443ec9c4e99cac754b2690f7ab5be382d" }, { "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyTargetResolver.java", - "before": "95e9c38861b984e995770e5ddf6d7af21c96d1bff1d8848d1ad2e3a54722efd2", - "after": "3c299d683055c0301f93b4bc8d23252215d1d9c0746d0aaafa0b52447d3eb26b" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiAliasServiceChange.java", - "before": null, - "after": "fa69385c6ffee09261466e995e59474b632302225088729de3242ccaf2b2118b" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiExternalServiceChange.java", - "before": null, - "after": "d9497a9c37842c09e833d434e5f361585f721a72eff9976c5b66902181f392e5" + "before": "3c299d683055c0301f93b4bc8d23252215d1d9c0746d0aaafa0b52447d3eb26b", + "after": "e755cdf32b48df5392cc6700e0d86890393828da25f957cdfc60c9e6ce157968" }, { "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDescriptor.java", - "before": null, - "after": "2ef7555c7864a2ce91a7c28951e895da412f68849b6cc3ec04350a75b35186f8" + "before": "2ef7555c7864a2ce91a7c28951e895da412f68849b6cc3ec04350a75b35186f8", + "after": "542f4d8b53b7360a4b72570466f63f027469d5bf2f0e3ce66ea3d6b6ceb6f417" }, { "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDomain.java", - "before": null, - "after": "aafe0ea7a5f60f7e9dc926067318cb2d470ec066cc1e5986b76b2c79122570a4" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanService.java", - "before": null, - "after": "d0605f3e94757996e699e4083b1cd314d6d68acf623358a92f9841bc75384fb9" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanStore.java", - "before": null, - "after": "9862ed000b4b40a6a7643968313c250fad9b1e718a684dccbab8f35c3a29cd5f" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeProcessObserver.java", - "before": null, - "after": "6f7898590bb64e55cb815c9b1a51f4ce00540a6268e8fa4914ef1b6b445f5a70" - }, - { - "path": "code/iaas/service-discovery/server/src/main/java/io/cattle/platform/servicediscovery/dao/impl/ServiceConsumeMapDaoImpl.java", - "before": null, - "after": "a127711511a3f1271d441235f9889d63e06a5c2d457974e4a45bd04f1e7e5747" - }, - { - "path": "code/packaging/app-config/src/main/java/io/cattle/platform/app/IaasApiConfig.java", - "before": "02184dc6ae781547e850c80cf51ddb4f33cbc7b2f799aba3ca1e59c8eb9cf34f", - "after": "59b633a3f6614f811b5f14b0eeeeb1f99683d319a2c28bea2093f48a33a53130" - }, - { - "path": "resources/content/schema/admin/admin-auth.json", - "before": "6ea49e1130a2b9218e6c060038c73c89be5b9e05b039d3a0b81475e892d21504", - "after": "27630fdb4d5cf7a1dcd7e693b84d0b31513d627a64a5d2365119075aca6d4f85" - }, - { - "path": "resources/content/schema/base/aiAuditQuery.json", - "before": null, - "after": "c12e694997a08c3e9849413f45b96d7d9cf2471289d35be54a60903d22e609eb" - }, - { - "path": "resources/content/schema/base/aiAuditSourcePageInput.json", - "before": null, - "after": "2aca5cba9323daae33a7171ac5f3bec2591cbdbffbdc32cf263ac8398a75e444" + "before": "aafe0ea7a5f60f7e9dc926067318cb2d470ec066cc1e5986b76b2c79122570a4", + "after": "2fc7645b2ce573ae02ebc2ec7e20548dda2e0a738d8e65d056650a6c85883c3e" }, { "path": "resources/content/schema/base/aiResourceChangePlan.json", - "before": "21b4acfd55e2aa64b725bacd520655daf7f707141e4226343c16a5b6fae47d8c", - "after": "90db2324422f25129edf155127da8e29303b74fe2467797d4459b1df451e9439" - }, - { - "path": "resources/content/schema/base/aiResourceView.json", - "before": "024fb697f3d5e1ed511544ff6922f3b78c351f01e74df1a36faeee3e7f3dba95", - "after": "5bd8e7f91b44165d8f57ebb5b17689c40b2c236d1d6a80af0ade78dba552cc73" - }, - { - "path": "resources/content/schema/project/project-auth.json", - "before": "a924ebe3e6ea7a39374d7d162fd020807cb94900c849c82d286df121db27ee6c", - "after": "c752d35a5b2b2bdc65af5914d89071948e5b10e41772736ade7f89dfd2554a59" - }, - { - "path": "resources/content/schema/user/user-auth.json", - "before": "0f977be3a4e6718753ba8a248e73f1014886afbee1d5387249e762b4146b7b73", - "after": "0f96a7e195647b967d4f83cab74d6a0fc5c442a896baa54384937af765bc2d9e" + "before": "90db2324422f25129edf155127da8e29303b74fe2467797d4459b1df451e9439", + "after": "7acb469f2ffbd0454e7e4e071aa672d3c444140583f41798a6d3cb02a632ba5e" } ] }, @@ -1133,57 +1247,25 @@ "addedTuples": [], "changedTuples": [], "authorizationRationale": { - "certificate": "Only name/description; current native method/field schema, row lock, safe UUID/public fingerprint CAS, Cookie-human approval and original Key revision remain required. No PEM/key/chain field or raw proxy is admitted.", - "nativeNetworkRemoval": "Actual immutable Java execution frames include native processName. Only Instance DELETE/delete, exact persisted owner/UUID/FK and all IP maps qualify for native nic.remove reentry. Missing/stop frames, foreign/shared/reassigned children and explicit NIC/IP/map/service DENY remain denied. Live paths and Full/legacy retain their original owner behavior.", - "terminalDenial": "Only the structured synchronous child AUTHORIZATION_DENIED terminates its parent. Other exit/retry semantics and durable completion-audit replay are unchanged.", - "auditOwnerTopology": "The fixed private CLI shares the configured broker upstream. Java reconstructs only HTTP(S) loopback origins and bounded ports from its exact nonsecret operator setting after clearing all other environment; credentials stay private stdin. No HTTP or tool-specified URL is accepted.", - "audit": "Existing durable metadata-only decision owner includes the explicitly reviewed certificate/externalService/dnsService domains. It does not authorize their operation or log private material." + "serviceIndexRemoval": "Only actual verified service.activate/service.update/service.remove native owners can capture the serviceindex.remove edge. Fresh persisted Index/Service/Stack identities and exact ownership relations must match the original Key-bound root; queued work matches all private identity pins. Missing/wrong parent, foreign/rebound UUID/FK/account, changed Key revision or revoked/expired authority and explicit Index/Service/Stack DENY remain rejected. Full/legacy/no-Key owner behavior is unchanged.", + "operation": "The child retains the original root update/delete operation, rather than obtaining a new independent delete or project-wide resource grant. ServiceIndex is not added to the generic resolver or PROJECT_ONLY.", + "owner": "Planner and Service/Stack remove use the existing native owner. ServiceSuffixRemove keeps its normal pool-only IP allocation release; no new NIC/IP/network or HTTP proxy permission is admitted." }, "sourceInputChanges": [ { - "path": "code/framework/auditing/src/main/java/io/cattle/platform/iaas/api/auditing/AuditServiceImpl.java", - "before": "a10a5d8e7ca754601ea2c5a876c318069b6639c073e616d283ae71694bc95179", - "after": "cb6bb0a8ed3dda48bedd3ae89ed78a490f644c1040c5aa16463e1e701250988e" - }, - { - "path": "code/framework/engine/src/main/java/io/cattle/platform/engine/context/EngineContext.java", - "before": null, - "after": "ba91b4da42d1f2ea5a2b9709818c2a8f4b967c2c730d5681b53d9dbe94ce8968" - }, - { - "path": "code/framework/engine/src/main/java/io/cattle/platform/engine/process/impl/DefaultProcessInstanceImpl.java", - "before": null, - "after": "986e14156ce42ac72033fad21289db4e3732469378cb6c9b0ae1d0a4c36376b0" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryRunner.java", - "before": "608fa5f5b412dccdf154290094ff1d67204c092cfb60b850d8d7a3124f53c812", - "after": "684fa84a259118a13d5a1ef20f76e05592ef867492e3fc54c1dcb94c06bb5758" + "path": "code/framework/engine/src/main/java/io/cattle/platform/engine/process/ProcessAuthorization.java", + "before": "a717b03de1ec528f7047161679be436535d68fb416b573c3aaf5b09a2e44b520", + "after": "ae96df57002f8102f1bf5eb3c813574a3563ec9ef52133f3499299037fb29ed3" }, { "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyProcessAuthorization.java", - "before": "1bf968d28d977725fa426fe7f32b00d5bc322979c0bfe1e0501fc2c34c03100e", - "after": "39ac76abfff5b7a8d95f722f72a4869443ec9c4e99cac754b2690f7ab5be382d" + "before": "39ac76abfff5b7a8d95f722f72a4869443ec9c4e99cac754b2690f7ab5be382d", + "after": "b2bf0a5939e030de833957f634622fccea75bb8f00f86e454b83f910f695682d" }, { "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyTargetResolver.java", - "before": "3c299d683055c0301f93b4bc8d23252215d1d9c0746d0aaafa0b52447d3eb26b", - "after": "e755cdf32b48df5392cc6700e0d86890393828da25f957cdfc60c9e6ce157968" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDescriptor.java", - "before": "2ef7555c7864a2ce91a7c28951e895da412f68849b6cc3ec04350a75b35186f8", - "after": "542f4d8b53b7360a4b72570466f63f027469d5bf2f0e3ce66ea3d6b6ceb6f417" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDomain.java", - "before": "aafe0ea7a5f60f7e9dc926067318cb2d470ec066cc1e5986b76b2c79122570a4", - "after": "2fc7645b2ce573ae02ebc2ec7e20548dda2e0a738d8e65d056650a6c85883c3e" - }, - { - "path": "resources/content/schema/base/aiResourceChangePlan.json", - "before": "90db2324422f25129edf155127da8e29303b74fe2467797d4459b1df451e9439", - "after": "7acb469f2ffbd0454e7e4e071aa672d3c444140583f41798a6d3cb02a632ba5e" + "before": "e755cdf32b48df5392cc6700e0d86890393828da25f957cdfc60c9e6ce157968", + "after": "51b21c88e5ec55dbf2a8708b4890127b9ab9668b3b1659d09a71942f11d25403" } ] }, @@ -1204,25 +1286,15 @@ "addedTuples": [], "changedTuples": [], "authorizationRationale": { - "serviceIndexRemoval": "Only actual verified service.activate/service.update/service.remove native owners can capture the serviceindex.remove edge. Fresh persisted Index/Service/Stack identities and exact ownership relations must match the original Key-bound root; queued work matches all private identity pins. Missing/wrong parent, foreign/rebound UUID/FK/account, changed Key revision or revoked/expired authority and explicit Index/Service/Stack DENY remain rejected. Full/legacy/no-Key owner behavior is unchanged.", - "operation": "The child retains the original root update/delete operation, rather than obtaining a new independent delete or project-wide resource grant. ServiceIndex is not added to the generic resolver or PROJECT_ONLY.", - "owner": "Planner and Service/Stack remove use the existing native owner. ServiceSuffixRemove keeps its normal pool-only IP allocation release; no new NIC/IP/network or HTTP proxy permission is admitted." + "externalReviewBefore": "Only the two existing before-value reads change from absent Java getters to the existing dynamic field accessor. The existing hostname/IP syntax filtering, safe scalar projection, and explicit clearing of the other target mode remain unchanged.", + "operation": "No method, action, public resource, scope grant or operation classification is added or changed.", + "owner": "The native External Service owner, authorization and full data snapshot/CAS remain unchanged." }, "sourceInputChanges": [ { - "path": "code/framework/engine/src/main/java/io/cattle/platform/engine/process/ProcessAuthorization.java", - "before": "a717b03de1ec528f7047161679be436535d68fb416b573c3aaf5b09a2e44b520", - "after": "ae96df57002f8102f1bf5eb3c813574a3563ec9ef52133f3499299037fb29ed3" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyProcessAuthorization.java", - "before": "39ac76abfff5b7a8d95f722f72a4869443ec9c4e99cac754b2690f7ab5be382d", - "after": "b2bf0a5939e030de833957f634622fccea75bb8f00f86e454b83f910f695682d" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyTargetResolver.java", - "before": "e755cdf32b48df5392cc6700e0d86890393828da25f957cdfc60c9e6ce157968", - "after": "51b21c88e5ec55dbf2a8708b4890127b9ab9668b3b1659d09a71942f11d25403" + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDomain.java", + "before": "2fc7645b2ce573ae02ebc2ec7e20548dda2e0a738d8e65d056650a6c85883c3e", + "after": "141fa3773edceec3c7f489d10964ebc4db4c8ff4271976f6ee58ac0f342e96a8" } ] }, @@ -1243,15 +1315,25 @@ "addedTuples": [], "changedTuples": [], "authorizationRationale": { - "externalReviewBefore": "Only the two existing before-value reads change from absent Java getters to the existing dynamic field accessor. The existing hostname/IP syntax filtering, safe scalar projection, and explicit clearing of the other target mode remain unchanged.", - "operation": "No method, action, public resource, scope grant or operation classification is added or changed.", - "owner": "The native External Service owner, authorization and full data snapshot/CAS remain unchanged." + "derivedCleanup": "The new path is limited to standalone native container DELETE, its unique current device-0 OS volume, and a NULL-account private image/cache whose complete live reference graph is proven. It does not create a public Image grant or choose FULL policy; explicit root/dependency denies and fresh owner/Key checks still apply.", + "externalTargets": "The collector intentionally includes nonremoved old target maps. Missing or different exact desired hostname is stale/error so the existing bad-unit cleanup removes that map; missing maps remain not started and stop/remove semantics are retained.", + "operation": "The actual re-extracted HTTP tuple set, semantic classification map and frozen role schemas remain identical. Derived authorization and lifecycle behavior changes are reviewed separately and are not described as zero semantic delta." }, "sourceInputChanges": [ { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDomain.java", - "before": "2fc7645b2ce573ae02ebc2ec7e20548dda2e0a738d8e65d056650a6c85883c3e", - "after": "141fa3773edceec3c7f489d10964ebc4db4c8ff4271976f6ee58ac0f342e96a8" + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyProcessAuthorization.java", + "before": "b2bf0a5939e030de833957f634622fccea75bb8f00f86e454b83f910f695682d", + "after": "e7301721f6b7eb54084b46b31eeec364aeecd3c1c95cc646eef0b9a3ea20b82c" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyTargetResolver.java", + "before": "51b21c88e5ec55dbf2a8708b4890127b9ab9668b3b1659d09a71942f11d25403", + "after": "0f836a821b080d7395a1c19f3740a7259112eb8e0126a2a356c233b699091be9" + }, + { + "path": "code/iaas/service-discovery/server/src/main/java/io/cattle/platform/servicediscovery/deployment/impl/unit/ExternalDeploymentUnitInstance.java", + "before": null, + "after": "66182deff7e2e7aefa36beae2558690c81a130e2ad43025fd899cfa96f06955f" } ] }, @@ -1272,25 +1354,31 @@ "addedTuples": [], "changedTuples": [], "authorizationRationale": { - "derivedCleanup": "The new path is limited to standalone native container DELETE, its unique current device-0 OS volume, and a NULL-account private image/cache whose complete live reference graph is proven. It does not create a public Image grant or choose FULL policy; explicit root/dependency denies and fresh owner/Key checks still apply.", - "externalTargets": "The collector intentionally includes nonremoved old target maps. Missing or different exact desired hostname is stale/error so the existing bad-unit cleanup removes that map; missing maps remain not started and stop/remove semantics are retained.", - "operation": "The actual re-extracted HTTP tuple set, semantic classification map and frozen role schemas remain identical. Derived authorization and lifecycle behavior changes are reviewed separately and are not described as zero semantic delta." + "derivedCleanup": "Only the standalone private OS map job is extended. Direct and nested true parent frames pin instance/volume/map/pool UUIDs and account/stack/FKs before detachment. Historical ancestors are read only after complete original pins and a trusted persisted child agree. Current owner/role/schema, fresh credential/revision/expiry and every root/dependency explicit DENY remain enforced. Missing refs, rebound graph, shared/named/managed storage and old canceled unbound jobs gain no authority. Existing live ancillary paths keep their prior generic authorization.", + "aliasValidation": "Existing native request validation errors retain 422 InvalidBody at the typed preview boundary, before plan allocation/apply. It does not grant access to foreign resources or add a route.", + "operation": "The actual re-extracted production HTTP tuple union, classifications and frozen role schemas remain identical. The authority c03-source-inputs-v1 selector is unchanged except the explicit EngineContext.java anchor.", + "regressionEvidence": "Regression test sources are separate proof, excluded from production authority HTTP input count and tuple extraction. Exact path/SHA: code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/apikey/StandaloneVolumePoolCleanupTest.java=e0017c0d1aa2d453cac4ca8a214e6d37e9f414ef7c1d19e599763a46755c305a; code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDomainTest.java=920ff179d4a0345708c32b516f974a6b8620bd48516cab98062be9d12402402f; code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanServiceTest.java=d09ba2736e5f77a68dbd6dc3f365567cddf22fefef5372deabd934c19341adcd. Required original CI executed methods and qualified wholeTree are the official execution/source evidence, not this metadata review." }, "sourceInputChanges": [ + { + "path": "code/framework/engine/src/main/java/io/cattle/platform/engine/context/EngineContext.java", + "before": "ba91b4da42d1f2ea5a2b9709818c2a8f4b967c2c730d5681b53d9dbe94ce8968", + "after": "17df7120c6b4a5e78ca643d0ea26f0fa30023bc277f2eb3b155680a97bef6b3b" + }, { "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyProcessAuthorization.java", - "before": "b2bf0a5939e030de833957f634622fccea75bb8f00f86e454b83f910f695682d", - "after": "e7301721f6b7eb54084b46b31eeec364aeecd3c1c95cc646eef0b9a3ea20b82c" + "before": "e7301721f6b7eb54084b46b31eeec364aeecd3c1c95cc646eef0b9a3ea20b82c", + "after": "0a102044c31c53be38095526812e8c36aa463ada9e32f5e58042a20e51e3216f" }, { "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyTargetResolver.java", - "before": "51b21c88e5ec55dbf2a8708b4890127b9ab9668b3b1659d09a71942f11d25403", - "after": "0f836a821b080d7395a1c19f3740a7259112eb8e0126a2a356c233b699091be9" + "before": "0f836a821b080d7395a1c19f3740a7259112eb8e0126a2a356c233b699091be9", + "after": "76742db7c623e670415d14790daa3913f51ae1182737caa459c10d83d3a4abd4" }, { - "path": "code/iaas/service-discovery/server/src/main/java/io/cattle/platform/servicediscovery/deployment/impl/unit/ExternalDeploymentUnitInstance.java", - "before": null, - "after": "66182deff7e2e7aefa36beae2558690c81a130e2ad43025fd899cfa96f06955f" + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanService.java", + "before": "d0605f3e94757996e699e4083b1cd314d6d68acf623358a92f9841bc75384fb9", + "after": "bfeb2e89f4a3049ec70292e14e29b2a74b4302f8756e80954edb2fdbe6497f0d" } ] }, @@ -1311,31 +1399,15 @@ "addedTuples": [], "changedTuples": [], "authorizationRationale": { - "derivedCleanup": "Only the standalone private OS map job is extended. Direct and nested true parent frames pin instance/volume/map/pool UUIDs and account/stack/FKs before detachment. Historical ancestors are read only after complete original pins and a trusted persisted child agree. Current owner/role/schema, fresh credential/revision/expiry and every root/dependency explicit DENY remain enforced. Missing refs, rebound graph, shared/named/managed storage and old canceled unbound jobs gain no authority. Existing live ancillary paths keep their prior generic authorization.", - "aliasValidation": "Existing native request validation errors retain 422 InvalidBody at the typed preview boundary, before plan allocation/apply. It does not grant access to foreign resources or add a route.", - "operation": "The actual re-extracted production HTTP tuple union, classifications and frozen role schemas remain identical. The authority c03-source-inputs-v1 selector is unchanged except the explicit EngineContext.java anchor.", - "regressionEvidence": "Regression test sources are separate proof, excluded from production authority HTTP input count and tuple extraction. Exact path/SHA: code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/apikey/StandaloneVolumePoolCleanupTest.java=e0017c0d1aa2d453cac4ca8a214e6d37e9f414ef7c1d19e599763a46755c305a; code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDomainTest.java=920ff179d4a0345708c32b516f974a6b8620bd48516cab98062be9d12402402f; code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanServiceTest.java=d09ba2736e5f77a68dbd6dc3f365567cddf22fefef5372deabd934c19341adcd. Required original CI executed methods and qualified wholeTree are the official execution/source evidence, not this metadata review." + "privateWire": "Consumer compatibility correction only; existing current actor, owner, role, Key revision/expiry, approval, dependency DENY and dispatch gates are unchanged. No raw JSON or nested Base64 fallback.", + "operation": "Existing production-only c03-source-inputs-v1 selector, explicit anchors, HTTP tuples, classifications and role schemas remain exact.", + "regressionEvidence": "Separate test proof, excluded from production authority HTTP count: code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchDelegationTest.java=78f4a0cc3d366ed83987f113b8212e1019943d12539947dda6c6760f07cb6c9b; code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPrivateTest.java=1f855b3b8d832cce524aa7743ce24db98265e051dcb9e97ec056baf0157e804e; code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPrivateWireTest.java=257599b8217e60d99c1cd1ce182540b5361ac64539b02685425ecfd55e94154b; code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchWorkflowTest.java=d73681c1e2c8b42e19e4228635005543bf1a2d708d1603eda6f07407620e0d5d. Official execution proof remains required original CI plus qualified wholeTree; local synthetic loopback tests are not runtime acceptance." }, "sourceInputChanges": [ { - "path": "code/framework/engine/src/main/java/io/cattle/platform/engine/context/EngineContext.java", - "before": "ba91b4da42d1f2ea5a2b9709818c2a8f4b967c2c730d5681b53d9dbe94ce8968", - "after": "17df7120c6b4a5e78ca643d0ea26f0fa30023bc277f2eb3b155680a97bef6b3b" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyProcessAuthorization.java", - "before": "e7301721f6b7eb54084b46b31eeec364aeecd3c1c95cc646eef0b9a3ea20b82c", - "after": "0a102044c31c53be38095526812e8c36aa463ada9e32f5e58042a20e51e3216f" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyTargetResolver.java", - "before": "0f836a821b080d7395a1c19f3740a7259112eb8e0126a2a356c233b699091be9", - "after": "76742db7c623e670415d14790daa3913f51ae1182737caa459c10d83d3a4abd4" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanService.java", - "before": "d0605f3e94757996e699e4083b1cd314d6d68acf623358a92f9841bc75384fb9", - "after": "bfeb2e89f4a3049ec70292e14e29b2a74b4302f8756e80954edb2fdbe6497f0d" + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPrivate.java", + "before": "df91328e28fca5f15aceda4faaf57be47c8165dfc5cbf36894abd12935c8d3b2", + "after": "a1f341587843e47beacf4c8d220ea3724f30c779a057555e7eccb33a2dfd2c71" } ] }, @@ -1356,17 +1428,152 @@ "addedTuples": [], "changedTuples": [], "authorizationRationale": { - "privateWire": "Consumer compatibility correction only; existing current actor, owner, role, Key revision/expiry, approval, dependency DENY and dispatch gates are unchanged. No raw JSON or nested Base64 fallback.", - "operation": "Existing production-only c03-source-inputs-v1 selector, explicit anchors, HTTP tuples, classifications and role schemas remain exact.", - "regressionEvidence": "Separate test proof, excluded from production authority HTTP count: code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchDelegationTest.java=78f4a0cc3d366ed83987f113b8212e1019943d12539947dda6c6760f07cb6c9b; code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPrivateTest.java=1f855b3b8d832cce524aa7743ce24db98265e051dcb9e97ec056baf0157e804e; code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPrivateWireTest.java=257599b8217e60d99c1cd1ce182540b5361ac64539b02685425ecfd55e94154b; code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchWorkflowTest.java=d73681c1e2c8b42e19e4228635005543bf1a2d708d1603eda6f07407620e0d5d. Official execution proof remains required original CI plus qualified wholeTree; local synthetic loopback tests are not runtime acceptance." + "typedCreate": "Only ports and the closed healthCheck subset enter the existing Service create adapter. Current actor/project/role/key max/revision/expiry/CAS, human approval, audit and one durable claim remain; environment and raw launchConfig remain rejected.", + "executionResult": "admin/project/user gain r only on a new read-only result field, never create/update. Existing receipt access still reauthorizes the actual same-account Service and pins ID/UUID. assignedPorts is emitted only after exact readback succeeds; approved changes remain immutable.", + "nativeOwners": "ServiceValidationFilter, PortSpec, PortPreflightInputs/Service, PortBindingAddress, ServiceCreate and ServiceDiscoveryServiceImpl are now source-bound alongside AiServiceCreateChange and ServiceCreateValidation. The same parser, preflight and persisted allocation owner remain authoritative.", + "operation": "1396 exact tuples and every derived semantic/registered/unsupported/human-only disposition are unchanged. Frozen v1 schemas and all existing role permissions are unchanged except the three explicit new r projection entries.", + "catalogProjection": "A fresh copy of InstanceRecord data.fields feeds the existing native role-schema projection with the same formatter/POST and non-create-response rules; it preserves already-authorized business values without introducing a new permission or transport.", + "acceptance": "Source extraction is not artifact, role-matrix, HTTP or asynchronous runtime acceptance; the original PR149/CI failure remains preserved." }, "sourceInputChanges": [ { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPrivate.java", - "before": "df91328e28fca5f15aceda4faaf57be47c8165dfc5cbf36894abd12935c8d3b2", - "after": "a1f341587843e47beacf4c8d220ea3724f30c779a057555e7eccb33a2dfd2c71" + "path": "code/iaas/api-logic/src/main/java/io/cattle/platform/iaas/api/port/PortPreflightInputs.java", + "before": null, + "after": "237060330f0ae0c092b11018159b6bf9fa214e1e2564d57f6b139dee34c38e13", + "baselineSourceSha256": "237060330f0ae0c092b11018159b6bf9fa214e1e2564d57f6b139dee34c38e13", + "newlyBound": true, + "sourceBytesChanged": false + }, + { + "path": "code/iaas/api-logic/src/main/java/io/cattle/platform/iaas/api/port/PortPreflightService.java", + "before": null, + "after": "6a64ad664eee2f5817e6157b5564f72ac3381cb7f0881e2ef73bab6b80c3a110", + "baselineSourceSha256": "6a64ad664eee2f5817e6157b5564f72ac3381cb7f0881e2ef73bab6b80c3a110", + "newlyBound": true, + "sourceBytesChanged": false + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchMaterializer.java", + "before": "00945aae0ccd8091f6835168bd0d0ff7615be2f00cc86c27f48218520d680f50", + "after": "26aa70893a8182f191f9b4d02d0f68a04ec4da6017b83e1d4726a8327ee93781", + "baselineSourceSha256": "00945aae0ccd8091f6835168bd0d0ff7615be2f00cc86c27f48218520d680f50", + "newlyBound": false, + "sourceBytesChanged": true + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDescriptor.java", + "before": "542f4d8b53b7360a4b72570466f63f027469d5bf2f0e3ce66ea3d6b6ceb6f417", + "after": "c60650a74476af5faadd8cc76bcab4d073fd294dfac28d8dc176c07daeb7d4dc", + "baselineSourceSha256": "542f4d8b53b7360a4b72570466f63f027469d5bf2f0e3ce66ea3d6b6ceb6f417", + "newlyBound": false, + "sourceBytesChanged": true + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDomain.java", + "before": "141fa3773edceec3c7f489d10964ebc4db4c8ff4271976f6ee58ac0f342e96a8", + "after": "fc2245d42e31f3426fef91d68bbe8039f67f50b0a47b9e60cd003c83d1776ad0", + "baselineSourceSha256": "141fa3773edceec3c7f489d10964ebc4db4c8ff4271976f6ee58ac0f342e96a8", + "newlyBound": false, + "sourceBytesChanged": true + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanService.java", + "before": "bfeb2e89f4a3049ec70292e14e29b2a74b4302f8756e80954edb2fdbe6497f0d", + "after": "1d52b18a6b3efc5f86b30e52cb1ab57bb27f788e28703a4e7864d473dd7aa071", + "baselineSourceSha256": "bfeb2e89f4a3049ec70292e14e29b2a74b4302f8756e80954edb2fdbe6497f0d", + "newlyBound": false, + "sourceBytesChanged": true + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiServiceCreateChange.java", + "before": null, + "after": "ac42a7cf1108ae29791ba3198c7602f5fdc4f786e5eb9cd278581fe23831be4f", + "baselineSourceSha256": null, + "newlyBound": true, + "sourceBytesChanged": true + }, + { + "path": "code/iaas/model/src/main/java/io/cattle/platform/core/util/PortBindingAddress.java", + "before": null, + "after": "e263eaaaeacc6bad7d09a9e21b48cafa098ced410857b8ab55dc1bab21ffccd4", + "baselineSourceSha256": "e263eaaaeacc6bad7d09a9e21b48cafa098ced410857b8ab55dc1bab21ffccd4", + "newlyBound": true, + "sourceBytesChanged": false + }, + { + "path": "code/iaas/model/src/main/java/io/cattle/platform/core/util/PortSpec.java", + "before": null, + "after": "66d88b6af51001ce9a49d0b47d9031c2fb0c076ff60b75b4f6b30473da64bd9e", + "baselineSourceSha256": "19c50738b291e64f425a05e58d781389cba151933d9741e9ab7f0b5a11a0d734", + "newlyBound": true, + "sourceBytesChanged": true + }, + { + "path": "code/iaas/service-discovery/api/src/main/java/io/cattle/platform/servicediscovery/api/filter/ServiceValidationFilter.java", + "before": null, + "after": "39d55237e452e7dbec7122eed74bee22d01f4db79e0f5c70ea26c28509f83107", + "baselineSourceSha256": "57e69c59889ca0d9cb5956659755625f923e0f67af57d428471f0247b2683575", + "newlyBound": true, + "sourceBytesChanged": true + }, + { + "path": "code/iaas/service-discovery/api/src/main/java/io/cattle/platform/servicediscovery/api/service/ServiceCreateValidation.java", + "before": null, + "after": "663e4a276a68636e4d5284cfb7b4fd80301d733f4a410775e4f6d7ae34e9a143", + "baselineSourceSha256": null, + "newlyBound": true, + "sourceBytesChanged": true + }, + { + "path": "code/iaas/service-discovery/server/src/main/java/io/cattle/platform/servicediscovery/process/ServiceCreate.java", + "before": null, + "after": "4c04eed9b5cc6cab4cf46b3c165021c9d15c3feecaf418f44377261d4e415157", + "baselineSourceSha256": "4c04eed9b5cc6cab4cf46b3c165021c9d15c3feecaf418f44377261d4e415157", + "newlyBound": true, + "sourceBytesChanged": false + }, + { + "path": "code/iaas/service-discovery/server/src/main/java/io/cattle/platform/servicediscovery/service/impl/ServiceDiscoveryServiceImpl.java", + "before": null, + "after": "0300fb72cd8f5a1a06c3d26509a9d74c79c644d1aab0c1ce16fa46357a8b1039", + "baselineSourceSha256": "0300fb72cd8f5a1a06c3d26509a9d74c79c644d1aab0c1ce16fa46357a8b1039", + "newlyBound": true, + "sourceBytesChanged": false + }, + { + "path": "resources/content/schema/admin/admin-auth.json", + "before": "27630fdb4d5cf7a1dcd7e693b84d0b31513d627a64a5d2365119075aca6d4f85", + "after": "f713ba64c53fb498b2cbe7f96b5c794ed35d093a74a78cafcffe3f19b59ef5df", + "baselineSourceSha256": "27630fdb4d5cf7a1dcd7e693b84d0b31513d627a64a5d2365119075aca6d4f85", + "newlyBound": false, + "sourceBytesChanged": true + }, + { + "path": "resources/content/schema/base/aiResourceChangePlan.json", + "before": "7acb469f2ffbd0454e7e4e071aa672d3c444140583f41798a6d3cb02a632ba5e", + "after": "113f44df62f09f822b7f97f4cfdcab96de2f0f70995c821632f801c20134110c", + "baselineSourceSha256": "7acb469f2ffbd0454e7e4e071aa672d3c444140583f41798a6d3cb02a632ba5e", + "newlyBound": false, + "sourceBytesChanged": true + }, + { + "path": "resources/content/schema/project/project-auth.json", + "before": "c752d35a5b2b2bdc65af5914d89071948e5b10e41772736ade7f89dfd2554a59", + "after": "ce1ebde019bfd57d6ddd5c2b77d119a76e1df38bf887371e765478c3d96388cf", + "baselineSourceSha256": "c752d35a5b2b2bdc65af5914d89071948e5b10e41772736ade7f89dfd2554a59", + "newlyBound": false, + "sourceBytesChanged": true + }, + { + "path": "resources/content/schema/user/user-auth.json", + "before": "0f96a7e195647b967d4f83cab74d6a0fc5c442a896baa54384937af765bc2d9e", + "after": "66a82fa448806fb826ecd7a11bc9dc6cc9598e7a221d63191113457fd3376091", + "baselineSourceSha256": "0f96a7e195647b967d4f83cab74d6a0fc5c442a896baa54384937af765bc2d9e", + "newlyBound": false, + "sourceBytesChanged": true } - ] + ], + "reviewManifest": "scripts/api-key-operation-review-0.183.366.json" }, "tuples": [ ["account","collection","GET","","","read"], diff --git a/scripts/api-key-operation-inventory.json b/scripts/api-key-operation-inventory.json index f69e1e898b..3e3cb48d9e 100644 --- a/scripts/api-key-operation-inventory.json +++ b/scripts/api-key-operation-inventory.json @@ -220,15 +220,16 @@ "defaultProcesses": "ebf41ab873c349656c307511712674e0db434a9c4986e08d03ec35d92bdcd6a7" }, "currentSourceReview": { - "version": "unreleased-365-catalog-private-native-wire-fix", + "version": "unreleased-366-service-create-native-ports-healthcheck-and-catalog-projection", "status": "SOURCE_REEXTRACTED_NOT_SIGNED_NOT_RUNTIME_ACCEPTANCE", - "baseCommit": "aae91b38e1448ee4739cd4d361480e3fe633d925", + "baseCommit": "259ef59e8110fab71488d5dbd8c9affda4279217", + "sourceCandidate": "9efe87becb752287072e2257c949680fed75282d", "contract": "scripts/api-key-operation-contract.json", - "contractSha256": "fe3148ca74f9373fddc617be7ee518ac4d3618e38eed4038c227cb440c6347f1", + "contractSha256": "8af458ac13a30c611fde008992297ad77b6fb40e237d615b7397e74e4302c820", "sourceInputs": { "algorithm": "c03-source-inputs-v1", - "count": 432, - "sha256": "fdf53022efdab0e56edb292da037216be051643770565c090424124be8b115ea", + "count": 441, + "sha256": "7ce17c8b05ccfba961414808148d8638bf54a9ad3bbc7de00facfcba4b26ac39", "anchors": [ "AbstractSchemaFactory.java", "AiAliasServiceChange.java", @@ -275,6 +276,7 @@ "AiResourceChangeProcessObserver.java", "AiResourceViewResourceManager.java", "AiResourceViewService.java", + "AiServiceCreateChange.java", "ApiAuditPayload.java", "ApiAuthenticator.java", "ApiCookieAuthentication.java", @@ -301,12 +303,20 @@ "GenericWhitelistedProxy.java", "IaasApiConfig.java", "NotWritablePostProcessor.java", + "PortBindingAddress.java", + "PortPreflightInputs.java", + "PortPreflightService.java", + "PortSpec.java", "ProcessAuthorization.java", "ProcessEventDecorator.java", "RequestReRouterHandler.java", "ResourceManagerRequestHandler.java", "SchemaFactoryImpl.java", "ServiceConsumeMapDaoImpl.java", + "ServiceCreate.java", + "ServiceCreateValidation.java", + "ServiceDiscoveryServiceImpl.java", + "ServiceValidationFilter.java", "TokenAuthLookup.java", "TransformInspect.java" ], @@ -326,18 +336,68 @@ "addedTuples": [], "removedTuples": [], "changedTuples": [], + "roleProjectionDelta": [ + { + "path": "resources/content/schema/admin/admin-auth.json", + "pointer": "/authorize/aiResourceChangePlan.executionResult", + "before": null, + "after": "r" + }, + { + "path": "resources/content/schema/project/project-auth.json", + "pointer": "/authorize/aiResourceChangePlan.executionResult", + "before": null, + "after": "r" + }, + { + "path": "resources/content/schema/user/user-auth.json", + "pointer": "/authorize/aiResourceChangePlan.executionResult", + "before": null, + "after": "r" + } + ], + "materializerDynamicProjection": { + "source": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchMaterializer.java", + "rootCause": "DockerTransformer stores imageUuid/environment/ports/logConfig in InstanceRecord data.fields; the five-argument WrappedResource constructor did not receive that dynamic value map, so the real role-schema projection omitted required business values.", + "fix": "Pass a fresh LinkedHashMap copy of DataUtils.getFields(instance) to the seven-argument WrappedResource constructor. Keep the existing current-role SchemaFactory, ID formatter, POST and createResponse=false behavior.", + "preservedBusinessValues": [ + "native docker imageUuid", + "environment exact scalar including embedded equals", + "fixed native ports", + "logConfig driver/config", + "bean-backed service name" + ], + "visibilityBoundary": "Current native role schema remains authoritative. Hidden environment, read-on-create-only ports/logConfig and unlisted dynamic fields remain absent; no role or Key authority expansion, raw bypass or new environment-input channel.", + "focusedTests": { + "newMethods": [ + "dynamicProjectionKeepsRestrictedSchemaAndNonCreateResponseRules", + "realTransformerDynamicFieldsReachNativeRoleSchemaProjection" + ], + "existingAffectedMethods": [ + "preparedGraphSeparatesNativeMutationFromAuthorizedPublicReferenceIds" + ], + "tests": 3, + "failures": 0, + "errors": 0, + "skipped": 0, + "evidenceFile": "engine366-materializer-dynamic-fields-focused-20261102.json", + "evidenceSha256": "24ea39c3caf6f6664735e67308246db34a07c59f384cccea48d9e2b171cbd35a", + "usesRealDockerTransformerAndNativeSchemas": true + }, + "runtimeAcceptance": "PENDING_MATCHING_ARTIFACT_AND_NATIVE_QA; no live pass claimed" + }, "historicalInventoryPreserved": true, - "producerCommit": "NOT_ASSIGNED_UNSIGNED_OVERLAY", + "producerCommit": "NOT_ASSIGNED_UNSIGNED_C03_REVIEW", "previousReview": { - "version": "unreleased-365-native-validation-cleanup-fixes", + "version": "unreleased-365-catalog-private-native-wire-fix", "status": "SOURCE_REEXTRACTED_NOT_SIGNED_NOT_RUNTIME_ACCEPTANCE", - "baseCommit": "a335cf8513d8ddb61d1e90f19a9a70769b8ede04", + "baseCommit": "aae91b38e1448ee4739cd4d361480e3fe633d925", "contract": "scripts/api-key-operation-contract.json", - "contractSha256": "a1b07004f7e6d40c91c60a7cf88bab15d99ea5602f5347333717027098946bcb", + "contractSha256": "fe3148ca74f9373fddc617be7ee518ac4d3618e38eed4038c227cb440c6347f1", "sourceInputs": { "algorithm": "c03-source-inputs-v1", "count": 432, - "sha256": "5a8f5d8159d22c989733c92568916e936915a7d5a81724e3f082af3b65523f43", + "sha256": "fdf53022efdab0e56edb292da037216be051643770565c090424124be8b115ea", "anchors": [ "AbstractSchemaFactory.java", "AiAliasServiceChange.java", @@ -436,17 +496,17 @@ "removedTuples": [], "changedTuples": [], "historicalInventoryPreserved": true, - "producerCommit": "NOT_ASSIGNED_UNSIGNED_WORKTREE", + "producerCommit": "NOT_ASSIGNED_UNSIGNED_OVERLAY", "previousReview": { - "version": "unreleased-364-native-lifecycle-fixes", + "version": "unreleased-365-native-validation-cleanup-fixes", "status": "SOURCE_REEXTRACTED_NOT_SIGNED_NOT_RUNTIME_ACCEPTANCE", - "baseCommit": "fcc23550a937f473b92e3adf5014bc3fa1096aef", + "baseCommit": "a335cf8513d8ddb61d1e90f19a9a70769b8ede04", "contract": "scripts/api-key-operation-contract.json", - "contractSha256": "0456695780b3b53bdbd034dd640dc21776b0242b5828f2aff56ab230eb722c6a", + "contractSha256": "a1b07004f7e6d40c91c60a7cf88bab15d99ea5602f5347333717027098946bcb", "sourceInputs": { "algorithm": "c03-source-inputs-v1", "count": 432, - "sha256": "169fe350bed9358c2d9cbdac24f75c8636258325f2a92305507ad06e79c993e1", + "sha256": "5a8f5d8159d22c989733c92568916e936915a7d5a81724e3f082af3b65523f43", "anchors": [ "AbstractSchemaFactory.java", "AiAliasServiceChange.java", @@ -547,15 +607,15 @@ "historicalInventoryPreserved": true, "producerCommit": "NOT_ASSIGNED_UNSIGNED_WORKTREE", "previousReview": { - "version": "unreleased-363-external-review-before", + "version": "unreleased-364-native-lifecycle-fixes", "status": "SOURCE_REEXTRACTED_NOT_SIGNED_NOT_RUNTIME_ACCEPTANCE", - "baseCommit": "4c69935fa8177e9f9ae675cdd00702a45be72f33", + "baseCommit": "fcc23550a937f473b92e3adf5014bc3fa1096aef", "contract": "scripts/api-key-operation-contract.json", - "contractSha256": "1d0f71fa4913f3725b076ad0675f804e6297af9c7ef4a1036fd4acabd10da2c7", + "contractSha256": "0456695780b3b53bdbd034dd640dc21776b0242b5828f2aff56ab230eb722c6a", "sourceInputs": { "algorithm": "c03-source-inputs-v1", - "count": 431, - "sha256": "5bd56f773b6d57827ee04cfb5fb51f3e9ff95969a152edbbbff40abf3bb338fc", + "count": 432, + "sha256": "169fe350bed9358c2d9cbdac24f75c8636258325f2a92305507ad06e79c993e1", "anchors": [ "AbstractSchemaFactory.java", "AiAliasServiceChange.java", @@ -622,6 +682,7 @@ "EngineContext.java", "EventBasedProcessHandler.java", "EventDiagnostics.java", + "ExternalDeploymentUnitInstance.java", "ExternalDynamicExtensionHandler.java", "FileSchemaFactory.java", "GenericWhitelistedProxy.java", @@ -655,15 +716,15 @@ "historicalInventoryPreserved": true, "producerCommit": "NOT_ASSIGNED_UNSIGNED_WORKTREE", "previousReview": { - "version": "unreleased-362-service-index-cleanup", + "version": "unreleased-363-external-review-before", "status": "SOURCE_REEXTRACTED_NOT_SIGNED_NOT_RUNTIME_ACCEPTANCE", - "baseCommit": "06d5bf0dac57bf3be63fa7ce868c8d5fec2508ce", + "baseCommit": "4c69935fa8177e9f9ae675cdd00702a45be72f33", "contract": "scripts/api-key-operation-contract.json", - "contractSha256": "0c28b642db338e44a8fac584f5502d77b18f591c31e5a28ea744afc850a114fd", + "contractSha256": "1d0f71fa4913f3725b076ad0675f804e6297af9c7ef4a1036fd4acabd10da2c7", "sourceInputs": { "algorithm": "c03-source-inputs-v1", "count": 431, - "sha256": "28e69f831778a7355fbcdbcb66a6af18467a15404861492fb4baa57ca64c5048", + "sha256": "5bd56f773b6d57827ee04cfb5fb51f3e9ff95969a152edbbbff40abf3bb338fc", "anchors": [ "AbstractSchemaFactory.java", "AiAliasServiceChange.java", @@ -763,14 +824,15 @@ "historicalInventoryPreserved": true, "producerCommit": "NOT_ASSIGNED_UNSIGNED_WORKTREE", "previousReview": { + "version": "unreleased-362-service-index-cleanup", "status": "SOURCE_REEXTRACTED_NOT_SIGNED_NOT_RUNTIME_ACCEPTANCE", - "baseCommit": "48c052d569364e962938f1d2b6442330456113c3", + "baseCommit": "06d5bf0dac57bf3be63fa7ce868c8d5fec2508ce", "contract": "scripts/api-key-operation-contract.json", - "contractSha256": "f7c76b5500654d6e05adf869c50d67d7d376e93119b5a93db933edd4c7d0c7b4", + "contractSha256": "0c28b642db338e44a8fac584f5502d77b18f591c31e5a28ea744afc850a114fd", "sourceInputs": { "algorithm": "c03-source-inputs-v1", "count": 431, - "sha256": "ef3f2b2d2fd8b8ec62e6231d8318a7bc9d69dfe6d6949588ca397373d8bb9308", + "sha256": "28e69f831778a7355fbcdbcb66a6af18467a15404861492fb4baa57ca64c5048", "anchors": [ "AbstractSchemaFactory.java", "AiAliasServiceChange.java", @@ -868,11 +930,121 @@ "removedTuples": [], "changedTuples": [], "historicalInventoryPreserved": true, - "producerCommit": "NOT_ASSIGNED_UNSIGNED_WORKTREE" + "producerCommit": "NOT_ASSIGNED_UNSIGNED_WORKTREE", + "previousReview": { + "status": "SOURCE_REEXTRACTED_NOT_SIGNED_NOT_RUNTIME_ACCEPTANCE", + "baseCommit": "48c052d569364e962938f1d2b6442330456113c3", + "contract": "scripts/api-key-operation-contract.json", + "contractSha256": "f7c76b5500654d6e05adf869c50d67d7d376e93119b5a93db933edd4c7d0c7b4", + "sourceInputs": { + "algorithm": "c03-source-inputs-v1", + "count": 431, + "sha256": "ef3f2b2d2fd8b8ec62e6231d8318a7bc9d69dfe6d6949588ca397373d8bb9308", + "anchors": [ + "AbstractSchemaFactory.java", + "AiAliasServiceChange.java", + "AiAuditQuery.java", + "AiAuditQueryResourceManager.java", + "AiAuditQueryRunner.java", + "AiAuditQueryService.java", + "AiAuditSourcePage.java", + "AiAuditSourcePageInput.java", + "AiCatalogLaunchAccess.java", + "AiCatalogLaunchAccessTest.java", + "AiCatalogLaunchBinding.java", + "AiCatalogLaunchBindingTest.java", + "AiCatalogLaunchDelegation.java", + "AiCatalogLaunchDelegationTest.java", + "AiCatalogLaunchGateway.java", + "AiCatalogLaunchGraph.java", + "AiCatalogLaunchHumanInputTest.java", + "AiCatalogLaunchInput.java", + "AiCatalogLaunchMaterializer.java", + "AiCatalogLaunchMaterializerTest.java", + "AiCatalogLaunchObservation.java", + "AiCatalogLaunchObservationTest.java", + "AiCatalogLaunchOptionsResourceManager.java", + "AiCatalogLaunchOptionsService.java", + "AiCatalogLaunchOptionsStore.java", + "AiCatalogLaunchOptionsStoreTest.java", + "AiCatalogLaunchOwnerContractTest.java", + "AiCatalogLaunchPlanResourceManager.java", + "AiCatalogLaunchPlanService.java", + "AiCatalogLaunchPlanStore.java", + "AiCatalogLaunchPlanStoreTest.java", + "AiCatalogLaunchPrivate.java", + "AiCatalogLaunchPrivateTest.java", + "AiCatalogLaunchProspectiveParentTest.java", + "AiCatalogLaunchResponseFilter.java", + "AiCatalogLaunchRunner.java", + "AiCatalogLaunchWorkflowTest.java", + "AiExternalServiceChange.java", + "AiResourceChangeDescriptor.java", + "AiResourceChangeDomain.java", + "AiResourceChangePlanService.java", + "AiResourceChangePlanStore.java", + "AiResourceChangeProcessObserver.java", + "AiResourceViewResourceManager.java", + "AiResourceViewService.java", + "ApiAuditPayload.java", + "ApiAuthenticator.java", + "ApiCookieAuthentication.java", + "ApiKeyAuditSink.java", + "ApiKeyAuthorizationService.java", + "ApiKeyOperations.java", + "ApiKeyProcessAuthorization.java", + "ApiKeyQueryScopes.java", + "ApiKeyTargetResolver.java", + "ApiServerConfig.java", + "AuditLogOutPutFilter.java", + "AuditServiceImpl.java", + "CatalogLaunchTransformAuthorization.java", + "DefaultAuthorizationProvider.java", + "DefaultObjectMetaDataManager.java", + "DefaultProcessInstanceImpl.java", + "DefaultProcessesParser.java", + "EngineContext.java", + "EventBasedProcessHandler.java", + "EventDiagnostics.java", + "ExternalDynamicExtensionHandler.java", + "FileSchemaFactory.java", + "GenericWhitelistedProxy.java", + "IaasApiConfig.java", + "NotWritablePostProcessor.java", + "ProcessAuthorization.java", + "ProcessEventDecorator.java", + "RequestReRouterHandler.java", + "ResourceManagerRequestHandler.java", + "SchemaFactoryImpl.java", + "ServiceConsumeMapDaoImpl.java", + "TokenAuthLookup.java", + "TransformInspect.java" + ], + "markers": [ + "@Type", + "@Actions", + " getLinkInternal(", + " getLink(", + " resourceActionInternal(", + " collectionActionInternal(", + " resourceAction(", + " collectionAction(" + ] + }, + "tupleCount": 1396, + "tupleSha256": "8f1c0fb378663b0b3d272efba2f211cb210eb6f17cb8035aefa33b56feeaf36a", + "addedTuples": [], + "removedTuples": [], + "changedTuples": [], + "historicalInventoryPreserved": true, + "producerCommit": "NOT_ASSIGNED_UNSIGNED_WORKTREE" + } } } } } - } + }, + "reviewManifest": "scripts/api-key-operation-review-0.183.366.json", + "reviewManifestSha256": "44e30398885ac2eee8af7aef8bcc576c2b3d90b347e30cef6ecc96469934f0c7" } } diff --git a/scripts/api-key-operation-review-0.183.366.json b/scripts/api-key-operation-review-0.183.366.json new file mode 100644 index 0000000000..6ec3c2ec9f --- /dev/null +++ b/scripts/api-key-operation-review-0.183.366.json @@ -0,0 +1,278 @@ +{ + "schemaVersion": 1, + "version": "0.183.366", + "scope": "Source-only C03 re-extraction, bounded Service-create/result-field review and native Catalog dynamic-field projection correction; no CI/artifact/runtime acceptance promotion.", + "reviewedBaseCommit": "259ef59e8110fab71488d5dbd8c9affda4279217", + "frozenFailedCandidate": "9efe87becb752287072e2257c949680fed75282d", + "retainedFailedCiRun": "41321815966", + "sourceInputs": { + "algorithm": "c03-source-inputs-v1", + "baseline": { + "count": 432, + "sha256": "fdf53022efdab0e56edb292da037216be051643770565c090424124be8b115ea" + }, + "failedCandidateWithPriorSelector": { + "count": 432, + "sha256": "61ee8d703f9ebe3d86bfe542f36043bc4520bb074843f358404d39c81dd7178a" + }, + "reviewed": { + "count": 441, + "sha256": "7ce17c8b05ccfba961414808148d8638bf54a9ad3bbc7de00facfcba4b26ac39" + }, + "addedAnchors": [ + "AiServiceCreateChange.java", + "PortBindingAddress.java", + "PortPreflightInputs.java", + "PortPreflightService.java", + "PortSpec.java", + "ServiceCreate.java", + "ServiceCreateValidation.java", + "ServiceDiscoveryServiceImpl.java", + "ServiceValidationFilter.java" + ], + "priorUnpublishedReview": { + "count": 441, + "sha256": "e6a4aa91585e4460577c7f0e40f7bbd60d97fcdccaa33d524533ffe2b0105b7d", + "reviewManifestSha256": "9e57f32ef41c57564641a39feb039a44529246e68f30e628a8883a0801ac1a0c" + } + }, + "tupleReview": { + "baseline": 1396, + "candidate": 1396, + "added": 0, + "removed": 0, + "semanticChanges": 0, + "dispositionChanges": 0, + "sha256": "8f1c0fb378663b0b3d272efba2f211cb210eb6f17cb8035aefa33b56feeaf36a", + "perTupleSemanticDispositionReviewSha256": "d00eaff4a7b136e9058fdaaf5d693ae0a23386bac796943c2617fc04b1997eae" + }, + "delta": { + "sourceInputs": [ + { + "path": "code/iaas/api-logic/src/main/java/io/cattle/platform/iaas/api/port/PortPreflightInputs.java", + "before": null, + "after": "237060330f0ae0c092b11018159b6bf9fa214e1e2564d57f6b139dee34c38e13", + "baselineSourceSha256": "237060330f0ae0c092b11018159b6bf9fa214e1e2564d57f6b139dee34c38e13", + "newlyBound": true, + "sourceBytesChanged": false + }, + { + "path": "code/iaas/api-logic/src/main/java/io/cattle/platform/iaas/api/port/PortPreflightService.java", + "before": null, + "after": "6a64ad664eee2f5817e6157b5564f72ac3381cb7f0881e2ef73bab6b80c3a110", + "baselineSourceSha256": "6a64ad664eee2f5817e6157b5564f72ac3381cb7f0881e2ef73bab6b80c3a110", + "newlyBound": true, + "sourceBytesChanged": false + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchMaterializer.java", + "before": "00945aae0ccd8091f6835168bd0d0ff7615be2f00cc86c27f48218520d680f50", + "after": "26aa70893a8182f191f9b4d02d0f68a04ec4da6017b83e1d4726a8327ee93781", + "baselineSourceSha256": "00945aae0ccd8091f6835168bd0d0ff7615be2f00cc86c27f48218520d680f50", + "newlyBound": false, + "sourceBytesChanged": true + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDescriptor.java", + "before": "542f4d8b53b7360a4b72570466f63f027469d5bf2f0e3ce66ea3d6b6ceb6f417", + "after": "c60650a74476af5faadd8cc76bcab4d073fd294dfac28d8dc176c07daeb7d4dc", + "baselineSourceSha256": "542f4d8b53b7360a4b72570466f63f027469d5bf2f0e3ce66ea3d6b6ceb6f417", + "newlyBound": false, + "sourceBytesChanged": true + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDomain.java", + "before": "141fa3773edceec3c7f489d10964ebc4db4c8ff4271976f6ee58ac0f342e96a8", + "after": "fc2245d42e31f3426fef91d68bbe8039f67f50b0a47b9e60cd003c83d1776ad0", + "baselineSourceSha256": "141fa3773edceec3c7f489d10964ebc4db4c8ff4271976f6ee58ac0f342e96a8", + "newlyBound": false, + "sourceBytesChanged": true + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanService.java", + "before": "bfeb2e89f4a3049ec70292e14e29b2a74b4302f8756e80954edb2fdbe6497f0d", + "after": "1d52b18a6b3efc5f86b30e52cb1ab57bb27f788e28703a4e7864d473dd7aa071", + "baselineSourceSha256": "bfeb2e89f4a3049ec70292e14e29b2a74b4302f8756e80954edb2fdbe6497f0d", + "newlyBound": false, + "sourceBytesChanged": true + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiServiceCreateChange.java", + "before": null, + "after": "ac42a7cf1108ae29791ba3198c7602f5fdc4f786e5eb9cd278581fe23831be4f", + "baselineSourceSha256": null, + "newlyBound": true, + "sourceBytesChanged": true + }, + { + "path": "code/iaas/model/src/main/java/io/cattle/platform/core/util/PortBindingAddress.java", + "before": null, + "after": "e263eaaaeacc6bad7d09a9e21b48cafa098ced410857b8ab55dc1bab21ffccd4", + "baselineSourceSha256": "e263eaaaeacc6bad7d09a9e21b48cafa098ced410857b8ab55dc1bab21ffccd4", + "newlyBound": true, + "sourceBytesChanged": false + }, + { + "path": "code/iaas/model/src/main/java/io/cattle/platform/core/util/PortSpec.java", + "before": null, + "after": "66d88b6af51001ce9a49d0b47d9031c2fb0c076ff60b75b4f6b30473da64bd9e", + "baselineSourceSha256": "19c50738b291e64f425a05e58d781389cba151933d9741e9ab7f0b5a11a0d734", + "newlyBound": true, + "sourceBytesChanged": true + }, + { + "path": "code/iaas/service-discovery/api/src/main/java/io/cattle/platform/servicediscovery/api/filter/ServiceValidationFilter.java", + "before": null, + "after": "39d55237e452e7dbec7122eed74bee22d01f4db79e0f5c70ea26c28509f83107", + "baselineSourceSha256": "57e69c59889ca0d9cb5956659755625f923e0f67af57d428471f0247b2683575", + "newlyBound": true, + "sourceBytesChanged": true + }, + { + "path": "code/iaas/service-discovery/api/src/main/java/io/cattle/platform/servicediscovery/api/service/ServiceCreateValidation.java", + "before": null, + "after": "663e4a276a68636e4d5284cfb7b4fd80301d733f4a410775e4f6d7ae34e9a143", + "baselineSourceSha256": null, + "newlyBound": true, + "sourceBytesChanged": true + }, + { + "path": "code/iaas/service-discovery/server/src/main/java/io/cattle/platform/servicediscovery/process/ServiceCreate.java", + "before": null, + "after": "4c04eed9b5cc6cab4cf46b3c165021c9d15c3feecaf418f44377261d4e415157", + "baselineSourceSha256": "4c04eed9b5cc6cab4cf46b3c165021c9d15c3feecaf418f44377261d4e415157", + "newlyBound": true, + "sourceBytesChanged": false + }, + { + "path": "code/iaas/service-discovery/server/src/main/java/io/cattle/platform/servicediscovery/service/impl/ServiceDiscoveryServiceImpl.java", + "before": null, + "after": "0300fb72cd8f5a1a06c3d26509a9d74c79c644d1aab0c1ce16fa46357a8b1039", + "baselineSourceSha256": "0300fb72cd8f5a1a06c3d26509a9d74c79c644d1aab0c1ce16fa46357a8b1039", + "newlyBound": true, + "sourceBytesChanged": false + }, + { + "path": "resources/content/schema/admin/admin-auth.json", + "before": "27630fdb4d5cf7a1dcd7e693b84d0b31513d627a64a5d2365119075aca6d4f85", + "after": "f713ba64c53fb498b2cbe7f96b5c794ed35d093a74a78cafcffe3f19b59ef5df", + "baselineSourceSha256": "27630fdb4d5cf7a1dcd7e693b84d0b31513d627a64a5d2365119075aca6d4f85", + "newlyBound": false, + "sourceBytesChanged": true + }, + { + "path": "resources/content/schema/base/aiResourceChangePlan.json", + "before": "7acb469f2ffbd0454e7e4e071aa672d3c444140583f41798a6d3cb02a632ba5e", + "after": "113f44df62f09f822b7f97f4cfdcab96de2f0f70995c821632f801c20134110c", + "baselineSourceSha256": "7acb469f2ffbd0454e7e4e071aa672d3c444140583f41798a6d3cb02a632ba5e", + "newlyBound": false, + "sourceBytesChanged": true + }, + { + "path": "resources/content/schema/project/project-auth.json", + "before": "c752d35a5b2b2bdc65af5914d89071948e5b10e41772736ade7f89dfd2554a59", + "after": "ce1ebde019bfd57d6ddd5c2b77d119a76e1df38bf887371e765478c3d96388cf", + "baselineSourceSha256": "c752d35a5b2b2bdc65af5914d89071948e5b10e41772736ade7f89dfd2554a59", + "newlyBound": false, + "sourceBytesChanged": true + }, + { + "path": "resources/content/schema/user/user-auth.json", + "before": "0f96a7e195647b967d4f83cab74d6a0fc5c442a896baa54384937af765bc2d9e", + "after": "66a82fa448806fb826ecd7a11bc9dc6cc9598e7a221d63191113457fd3376091", + "baselineSourceSha256": "0f96a7e195647b967d4f83cab74d6a0fc5c442a896baa54384937af765bc2d9e", + "newlyBound": false, + "sourceBytesChanged": true + } + ], + "schemaFields": [ + { + "path": "resources/content/schema/base/aiResourceChangePlan.json", + "field": "executionResult", + "before": null, + "after": { + "type": "json", + "create": false, + "update": false, + "nullable": true + } + } + ], + "newReadOnlyRoleProjectionEntries": [ + { + "path": "resources/content/schema/admin/admin-auth.json", + "pointer": "/authorize/aiResourceChangePlan.executionResult", + "before": null, + "after": "r" + }, + { + "path": "resources/content/schema/project/project-auth.json", + "pointer": "/authorize/aiResourceChangePlan.executionResult", + "before": null, + "after": "r" + }, + { + "path": "resources/content/schema/user/user-auth.json", + "pointer": "/authorize/aiResourceChangePlan.executionResult", + "before": null, + "after": "r" + } + ], + "addedTuples": [], + "removedTuples": [], + "changedSemanticTuples": [], + "changedAdmissionDispositions": [], + "fromPreviousUnpublishedReview": [ + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchMaterializer.java", + "before": "00945aae0ccd8091f6835168bd0d0ff7615be2f00cc86c27f48218520d680f50", + "after": "26aa70893a8182f191f9b4d02d0f68a04ec4da6017b83e1d4726a8327ee93781" + } + ] + }, + "authorizationBoundary": { + "changedExistingRolePermissions": 0, + "newReadOnlyRoleProjectionEntries": 3, + "mutationGrantDelta": 0, + "review": "The only role-schema additions expose executionResult as r for admin/project/user. No prior permission changes, new action/CRUD grant, Key authority or raw transport is added. Existing receipt owner/current authorization and successful ID/UUID readback guard this server-produced field. ports/healthCheck expand the closed typed Service-create parameters within the existing native create authorization; environment remains rejected. Catalog Materializer now copies native InstanceRecord data.fields into its existing current-role schema projection. Existing fields retain their exact values; hidden/read-on-create-only/unlisted fields remain excluded. Schema/formatter/POST and createResponse=false are unchanged; no role, Key, operation or raw-input authority is added." + }, + "frozenV1SchemaFilesUnchanged": 16, + "extractor": { + "constructionSha256": "078ffe48881469fe5eb3cb43a15fa41b480fe70e11bd5f63102ded6b35a6d076", + "parserSha256": "7cc8e70d9f3ea391eb74fef2490f4407aa47833c585b6729c62b3d9e5829ac2b", + "extractorSha256": "112d2ff723d88acbe7bb45ff48c9c9018feb93b08031a3e86f867030871477fc" + }, + "historicalProducerAndReviewChainsPreserved": true, + "artifactQualificationClaimed": false, + "runtimeAcceptanceClaimed": false, + "catalogMaterializerDynamicProjection": { + "source": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchMaterializer.java", + "rootCause": "DockerTransformer stores imageUuid/environment/ports/logConfig in InstanceRecord data.fields; the five-argument WrappedResource constructor did not receive that dynamic value map, so the real role-schema projection omitted required business values.", + "fix": "Pass a fresh LinkedHashMap copy of DataUtils.getFields(instance) to the seven-argument WrappedResource constructor. Keep the existing current-role SchemaFactory, ID formatter, POST and createResponse=false behavior.", + "preservedBusinessValues": [ + "native docker imageUuid", + "environment exact scalar including embedded equals", + "fixed native ports", + "logConfig driver/config", + "bean-backed service name" + ], + "visibilityBoundary": "Current native role schema remains authoritative. Hidden environment, read-on-create-only ports/logConfig and unlisted dynamic fields remain absent; no role or Key authority expansion, raw bypass or new environment-input channel.", + "focusedTests": { + "newMethods": [ + "dynamicProjectionKeepsRestrictedSchemaAndNonCreateResponseRules", + "realTransformerDynamicFieldsReachNativeRoleSchemaProjection" + ], + "existingAffectedMethods": [ + "preparedGraphSeparatesNativeMutationFromAuthorizedPublicReferenceIds" + ], + "tests": 3, + "failures": 0, + "errors": 0, + "skipped": 0, + "evidenceFile": "engine366-materializer-dynamic-fields-focused-20261102.json", + "evidenceSha256": "24ea39c3caf6f6664735e67308246db34a07c59f384cccea48d9e2b171cbd35a", + "usesRealDockerTransformerAndNativeSchemas": true + }, + "runtimeAcceptance": "PENDING_MATCHING_ARTIFACT_AND_NATIVE_QA; no live pass claimed" + } +} diff --git a/scripts/build b/scripts/build index 007b1dc3e7..33a1d0023e 100755 --- a/scripts/build +++ b/scripts/build @@ -16,7 +16,7 @@ fi SOURCE_REVISION=${SOURCE_REVISION:-$(git rev-parse HEAD)} SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH:-$(git show -s --format=%ct HEAD)} -ENGINE_VERSION=${ENGINE_VERSION:-0.183.365} +ENGINE_VERSION=${ENGINE_VERSION:-0.183.366} case "$SOURCE_REVISION" in ''|*[!0-9a-f]*) diff --git a/scripts/check-cattle-jdk25-standalone b/scripts/check-cattle-jdk25-standalone index 916640ecf8..189a10602f 100755 --- a/scripts/check-cattle-jdk25-standalone +++ b/scripts/check-cattle-jdk25-standalone @@ -6,7 +6,7 @@ cd "$repo_root" java_cmd=${JAVA_CMD:-java} runner_image=${RC16_CATTLE_STANDALONE_JDK_IMAGE:-maven:3.9.16-eclipse-temurin-25@sha256:1b1fc6d0168ea616afd1c861d6f32ec37c9ec2ffe88a0351b3771dd4ad86b0d8} -expected_version=${EXPECTED_ENGINE_VERSION:-${ENGINE_VERSION:-0.183.365}} +expected_version=${EXPECTED_ENGINE_VERSION:-${ENGINE_VERSION:-0.183.366}} artifact=${RC16_CATTLE_STANDALONE_JAR:-dist/artifacts/cattle.jar} # Full-package invokes Maven directly, without scripts/build's final copy. diff --git a/scripts/check-pasturestack-source b/scripts/check-pasturestack-source index 314bf2dbb1..c67747cf4e 100755 --- a/scripts/check-pasturestack-source +++ b/scripts/check-pasturestack-source @@ -51,7 +51,7 @@ fi project_version=$(sed -n 's/^[[:space:]]*\([^<]*\)<\/version>[[:space:]]*$/\1/p' code/meta-parent/pom.xml | head -n 1) [[ "$project_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail non_numeric_project_version -require_line code/meta-parent/pom.xml ' 0.183.365' +require_line code/meta-parent/pom.xml ' 0.183.366' require_line code/meta-parent/pom.xml ' 33.7.2-jre' require_line ubuntu-security-packages.lock 'libbrotli1=1.2.0-3ubuntu0.1' for recipe in Dockerfile Dockerfile.dapper; do @@ -475,4 +475,4 @@ require_line README.md 'PastureStack is an independent community effort to prese require_line README.md 'Rancher 1.6 ecosystem. It is not affiliated with Rancher Labs or SUSE.' require_line ORIGIN.md '- Preserved upstream boundary: `82d154a53f4089fecfb9f320caad826bb4f6055f`' -printf 'PASTURESTACK_SOURCE_GATE_OK version=0.183.365 runtime_sources=github_release images=digest_pinned ubuntu=26.04 ubuntu_snapshot=20260826T000000Z jdk=25.0.5 go=1.27.3 curl=8.18.0-1ubuntu2.8 openssl=3.5.5-1ubuntu3.7 maven=3.9.16 patched_hazelcast=5.7.6 docker_cli=29.9.2 docker_host_29_8_0=exact credential_secret_capacity=mediumtext port_preflight=authoritative volume_preflight=runtime_resolution_aligned volume_preflight_project_schema=authorized volume_preflight_type_set=registered v1_hardware_schema=container-and-launchConfig network_driver_rollback=launch-config-restored stack_driver_rollback=child-launch-config-restored auth_token_session_binding=authorized-create-only auth_token_transport=bare-or-bearer-normalized auth_token_frozen_v1_schema=base-superadmin-token oidc_external_types=validated-before-mutation-owned-stable-account oidc_account_activation=sync-before-mfa oidc_v1_project_member_schema=core-options-merged-scoped oidc_identity_link_owner=explicit-and-verified legacy_token_link_repair=exact-match-only oidc_required_local_recovery=active-admin-only default_project=shared-idempotent-role-preserving,atomic-identity-set oidc_restricted_project_membership=stable-account-aware-required-allowlist-only mfa_policy_confirmation=actor-purpose-digest-single-use auth_config_proxy_identity=caller-platform-credential project_member_collection_acl=requested-project-checked-before-load project_template_v1_public=readonly-frozen-field-merged network_purge=retry-removing dev_artifact=forbidden\n' +printf 'PASTURESTACK_SOURCE_GATE_OK version=0.183.366 runtime_sources=github_release images=digest_pinned ubuntu=26.04 ubuntu_snapshot=20260826T000000Z jdk=25.0.5 go=1.27.3 curl=8.18.0-1ubuntu2.8 openssl=3.5.5-1ubuntu3.7 maven=3.9.16 patched_hazelcast=5.7.6 docker_cli=29.9.2 docker_host_29_8_0=exact credential_secret_capacity=mediumtext port_preflight=authoritative volume_preflight=runtime_resolution_aligned volume_preflight_project_schema=authorized volume_preflight_type_set=registered v1_hardware_schema=container-and-launchConfig network_driver_rollback=launch-config-restored stack_driver_rollback=child-launch-config-restored auth_token_session_binding=authorized-create-only auth_token_transport=bare-or-bearer-normalized auth_token_frozen_v1_schema=base-superadmin-token oidc_external_types=validated-before-mutation-owned-stable-account oidc_account_activation=sync-before-mfa oidc_v1_project_member_schema=core-options-merged-scoped oidc_identity_link_owner=explicit-and-verified legacy_token_link_repair=exact-match-only oidc_required_local_recovery=active-admin-only default_project=shared-idempotent-role-preserving,atomic-identity-set oidc_restricted_project_membership=stable-account-aware-required-allowlist-only mfa_policy_confirmation=actor-purpose-digest-single-use auth_config_proxy_identity=caller-platform-credential project_member_collection_acl=requested-project-checked-before-load project_template_v1_public=readonly-frozen-field-merged network_purge=retry-removing dev_artifact=forbidden\n' diff --git a/scripts/check-release-artifact b/scripts/check-release-artifact index f347d03052..04dd7631cd 100755 --- a/scripts/check-release-artifact +++ b/scripts/check-release-artifact @@ -4,7 +4,7 @@ set -euo pipefail cd "$(dirname "$0")/.." artifact=${1:-dist/artifacts/cattle.jar} -expected_version=${EXPECTED_ENGINE_VERSION:-0.183.365} +expected_version=${EXPECTED_ENGINE_VERSION:-0.183.366} test -f "$artifact" artifact=$(realpath "$artifact")