diff --git a/.github/workflows/security-release-gate.yml b/.github/workflows/security-release-gate.yml index 051e4c9b7f..73c2dce1b9 100644 --- a/.github/workflows/security-release-gate.yml +++ b/.github/workflows/security-release-gate.yml @@ -59,6 +59,48 @@ jobs: import xml.etree.ElementTree as ET required = { + 'io.cattle.platform.iaas.api.auth.ai.catalog.AiCatalogLaunchLockOrderTest': { + 'nativeActionPrefetchAndConcurrentNativeGetCompleteWithApplicationBeforeRowLock', + 'onlyExactInternalActionOperandUsesSnapshotAndOrdinaryGetStillChecksSource', + 'snapshotRetainsOwnerProjectRoleKeyBindingAndDoesNotAuthorizeUnconfirmedApply', + }, + 'io.cattle.platform.iaas.api.auth.ai.catalog.AiCatalogLaunchAccessTest': { + 'operandSnapshotRechecksOriginalKeyWithoutRetainingCredentialRowLock', + }, + 'io.cattle.platform.iaas.api.auth.change.AiResourceChangeLockOrderTest': { + 'nativeActionReceiptPrefetchAndConcurrentGetDoNotInvertObservationWriteLocks', + 'exactSnapshotRetainsOwnerProjectKeyAndReceiptPermissionsWhileGetStillObservesAndApplyRequiresConfirmation', + }, + 'io.cattle.platform.iaas.api.auth.upgrade.AiServiceUpgradePlanLockOrderTest': { + 'acceptedUpgradeNativeActionPrefetchAndConcurrentGetKeepApplicationBeforeRowLock', + 'runningUpgradeNativeActionPrefetchAndConcurrentGetKeepApplicationBeforeRowLock', + 'onlyExactNativeUpgradeActionMarkerUsesReadSnapshotAndOrdinaryGetStillObserves', + 'upgradeActionSnapshotRetainsOwnerProjectRoleVisibilityAndBoundKeyChecks', + 'upgradeActionReloadsSnapshotRoleAndKeyAuthorityAfterOperandLookupAndClaimsOnlyOnce', + 'concurrentNativeUpgradeAppliesShareOneDurableClaimAndDispatch', + }, + 'io.cattle.platform.iaas.api.auth.ai.audit.AiAuditQueryServiceTest': { + 'realParserAndSchemaCannotEraseOriginalUnknownFieldsBeforeCanonicalError', + 'originalQueryParametersAreClosedButValidTypedQueriesKeepTheirCanonicalSuccess', + 'originalFieldCaptureIsOnlyTheAuditCollectionOwnerAndPrecedesTheRealSchemaValidator', + }, + 'io.cattle.platform.iaas.api.auth.upgrade.AiServiceUpgradePlanServiceTest': { + 'realPreviewPipelineRejectsUnknownAndCoercedStrategyWithoutPlanOrDispatch', + 'originalPreviewGuardLeavesOrdinaryUpgradeAndPlanActionsUntouchedAndRunsBeforeNativeValidation', + 'omittedAndLegacyStrategiesKeepTheOriginalEffectiveDispatchDefaults', + 'newCustomAndDefaultPlansFenceLegacySnapshotBeforeClaim', + 'legacySnapshotWithoutStrategyOrFenceStillConfirmsAndAppliesDefaults', + 'strategyAndSnapshotFenceDriftCannotClaimOrDispatch', + 'reloadedFencedStrategyRemainsImmutableAndDispatchesOnlyOnce', + 'approvedStrategyIsDetachedReviewedAndDispatchedOnceWithoutActionOverrides', + 'strategyUsesNativeMinimaAndExactJsonIntegerBoundaryWithoutClamping', + 'invalidOptionalStrategyRejectsBeforePlanOrDispatchAndNativePreflightIsRequired', + }, + 'io.cattle.platform.iaas.api.auth.upgrade.AiServiceUpgradeStrategyPreflightTest': { + 'nativeRetainIpStartFirstInvariantRejectsBeforeAnyPlanOrResourceMutation', + 'realStartFirstPortCapacityRejectsAtPreviewConfirmationAndBeforeClaim', + 'nativeVolumePreflightReceivesApprovedStartFirstAndNativeBatchNormalization', + }, 'io.cattle.platform.iaas.api.auth.ai.catalog.AiCatalogLaunchPrivateWireTest': { 'nativeCryptoHttpRoundTripOpensExactlyOneGoBase64JsonLayer', 'nativeCryptoEnvelopeRejectsEveryPlanActorProjectAndOriginalKeyRebinding', diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 160bcc1134..f020b4c7d2 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -1,5 +1,51 @@ # Compatibility Contract +## Engine 0.183.367 source candidate + +The existing image-upgrade plan accepts three optional strategy fields and +returns a closed effective `upgradeStrategy`. Omitted fields and old persisted +image-only plans keep `startFirst=false`, `batchSize=1`, `intervalMillis=2000`. +Batch size retains the native minimum 1 and interval the native minimum 100ms. +The JSON contract accepts exact integers through 9007199254740991; this is a +client representation boundary, not a new native product maximum. The native +preflight keeps its existing int-range batch normalization, while dispatch +receives the approved Long values. Full/legacy keys, role/Key revision checks, +port/volume rules and durable operation claims remain unchanged. No new mutation +operation or global grant is introduced. Publication and runtime qualification +remain pending; see the [candidate note](docs/releases/orchestration-engine-0.183.367.md). + +New plans bind the approved strategy into their private snapshot. An older +Engine rejects these plans as stale before claiming an operation instead of +applying its own defaults. Genuine legacy image-only plans remain compatible. +After a downgrade, pending newer plans require a fresh preview and approval. + +Typed Audit queries now preserve a closed-field rejection through normal schema +sanitization. The existing owner rejects unknown fields with422 and records its +canonical ERROR outcome without starting the private query runner. Other API +schema filtering and current actor/Key authorization remain unchanged. + +Catalog-launch, resource-change and upgrade action operands now use authorized, +non-mutating read snapshots only for the framework's exact internal action marker. +The native API write transaction previously retained a prefetch row lock after +releasing its application lock, allowing a concurrent receipt GET to invert the +next action's lock order. Ordinary GETs still refresh receipts; actions still +reload under their application lock and repeat owner, role, Key, snapshot and +durable-claim checks. No lock timeout, retry, database schema or grant changes. + +The lock-order tests declare managed Spring Context and AspectJ dependencies in +test scope so they can load the shipped transaction advice and pointcut. Runtime +dependency coordinates are unchanged. + +Frozen-package downloads retry transport failures only at the original signed +snapshot URL, at most four attempts. Hash or size failures stop that origin +immediately. Package selection, TLS verification and SHA256 checks are unchanged. + +Both Engine build recipes install Ubuntu's official PCRE2 security update +`10.46-1ubuntu0.1` for the already installed `libpcre2-8-0` and `libpcre2-posix3`. +The two archives are SHA256-pinned and their package, version, architecture and +installed versions are checked. Other package locks and snapshot dates remain +unchanged. See [USN-8974-1](https://ubuntu.com/security/notices/USN-8974-1). + ## Engine 0.183.366 release The [published release](https://github.com/PastureStack/orchestration-engine/releases/tag/v0.183.366) diff --git a/Dockerfile b/Dockerfile index 8e64834993..6c4b823b68 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,6 +31,8 @@ ADD --checksum=sha256:2c418a61d7165d0cde01d9cb05ae025cc41fc75ef6d3f095996bd045ba ADD --checksum=sha256:8db913e1fa5a694ca6ba7b828bd18c5204e6853ac07fe7ed9c6e1f94e3444d8e https://security.ubuntu.com/ubuntu/pool/main/c/curl/libcurl3t64-gnutls_8.18.0-1ubuntu2.8_amd64.deb /tmp/libcurl3t64-gnutls.deb ADD --checksum=sha256:7c0b37592f83a629cc22f0c98effa176da9edbd6d77b3f38184a9d0ddc5e0266 https://security.ubuntu.com/ubuntu/pool/main/c/curl/libcurl4t64_8.18.0-1ubuntu2.8_amd64.deb /tmp/libcurl4t64.deb ADD --checksum=sha256:bedeef1cf3268f90338c411cdb0a45b224d0660fd2240f310f380316c8244606 https://security.ubuntu.com/ubuntu/pool/main/b/brotli/libbrotli1_1.2.0-3ubuntu0.1_amd64.deb /tmp/libbrotli1.deb +ADD --checksum=sha256:44ebbeb6493eacffbc460ca71446959ba86a00b2e23515e4cfb1a5cc317d6672 https://security.ubuntu.com/ubuntu/pool/main/p/pcre2/libpcre2-8-0_10.46-1ubuntu0.1_amd64.deb /tmp/libpcre2-8-0.deb +ADD --checksum=sha256:44605ea665673a359927d014f4a027c50fe6472af8bc956bd1b1c43ddc07a1cd https://security.ubuntu.com/ubuntu/pool/main/p/pcre2/libpcre2-posix3_10.46-1ubuntu0.1_amd64.deb /tmp/libpcre2-posix3.deb ENV DEBIAN_FRONTEND=noninteractive ARG TEMURIN_JDK25_URL="https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.5%2B8/OpenJDK25U-jdk_x64_linux_hotspot_25.0.5_8.tar.gz" @@ -109,7 +111,17 @@ RUN set -eux; \ test "$(dpkg-deb -f /tmp/libbrotli1.deb Version)" = 1.2.0-3ubuntu0.1; \ test "$(dpkg-deb -f /tmp/libbrotli1.deb Architecture)" = amd64; \ dpkg -i /tmp/libbrotli1.deb; \ + for package in libpcre2-8-0 libpcre2-posix3; do \ + test "$(dpkg-query -W -f='${db:Status-Status}' "${package}")" = installed; \ + test "$(dpkg-deb -f "/tmp/${package}.deb" Package)" = "${package}"; \ + test "$(dpkg-deb -f "/tmp/${package}.deb" Version)" = 10.46-1ubuntu0.1; \ + test "$(dpkg-deb -f "/tmp/${package}.deb" Architecture)" = amd64; \ + done; \ + dpkg -i /tmp/libpcre2-8-0.deb /tmp/libpcre2-posix3.deb; \ sh /usr/local/bin/install-ubuntu-security-updates /licenses/ubuntu-security-packages.lock /etc/ssl/certs/ca-certificates.crt; \ + for package in libpcre2-8-0 libpcre2-posix3; do \ + test "$(dpkg-query -W -f='${Version}' "${package}")" = 10.46-1ubuntu0.1; \ + done; \ for package in curl libcurl3t64-gnutls libcurl4t64; do \ test "$(dpkg-query -W -f='${Version}' "${package}")" = "${UBUNTU_APT_CURL_VERSION}"; \ done; \ diff --git a/Dockerfile.dapper b/Dockerfile.dapper index 5b4265214e..601c8e3a32 100644 --- a/Dockerfile.dapper +++ b/Dockerfile.dapper @@ -32,6 +32,8 @@ ADD --checksum=sha256:2c418a61d7165d0cde01d9cb05ae025cc41fc75ef6d3f095996bd045ba ADD --checksum=sha256:8db913e1fa5a694ca6ba7b828bd18c5204e6853ac07fe7ed9c6e1f94e3444d8e https://security.ubuntu.com/ubuntu/pool/main/c/curl/libcurl3t64-gnutls_8.18.0-1ubuntu2.8_amd64.deb /tmp/libcurl3t64-gnutls.deb ADD --checksum=sha256:7c0b37592f83a629cc22f0c98effa176da9edbd6d77b3f38184a9d0ddc5e0266 https://security.ubuntu.com/ubuntu/pool/main/c/curl/libcurl4t64_8.18.0-1ubuntu2.8_amd64.deb /tmp/libcurl4t64.deb ADD --checksum=sha256:bedeef1cf3268f90338c411cdb0a45b224d0660fd2240f310f380316c8244606 https://security.ubuntu.com/ubuntu/pool/main/b/brotli/libbrotli1_1.2.0-3ubuntu0.1_amd64.deb /tmp/libbrotli1.deb +ADD --checksum=sha256:44ebbeb6493eacffbc460ca71446959ba86a00b2e23515e4cfb1a5cc317d6672 https://security.ubuntu.com/ubuntu/pool/main/p/pcre2/libpcre2-8-0_10.46-1ubuntu0.1_amd64.deb /tmp/libpcre2-8-0.deb +ADD --checksum=sha256:44605ea665673a359927d014f4a027c50fe6472af8bc956bd1b1c43ddc07a1cd https://security.ubuntu.com/ubuntu/pool/main/p/pcre2/libpcre2-posix3_10.46-1ubuntu0.1_amd64.deb /tmp/libpcre2-posix3.deb ENV DEBIAN_FRONTEND=noninteractive ENV LANG=C.UTF-8 LC_ALL=C.UTF-8 @@ -127,7 +129,17 @@ RUN set -eux; \ test "$(dpkg-deb -f /tmp/libbrotli1.deb Version)" = 1.2.0-3ubuntu0.1; \ test "$(dpkg-deb -f /tmp/libbrotli1.deb Architecture)" = amd64; \ dpkg -i /tmp/libbrotli1.deb; \ + for package in libpcre2-8-0 libpcre2-posix3; do \ + test "$(dpkg-query -W -f='${db:Status-Status}' "${package}")" = installed; \ + test "$(dpkg-deb -f "/tmp/${package}.deb" Package)" = "${package}"; \ + test "$(dpkg-deb -f "/tmp/${package}.deb" Version)" = 10.46-1ubuntu0.1; \ + test "$(dpkg-deb -f "/tmp/${package}.deb" Architecture)" = amd64; \ + done; \ + dpkg -i /tmp/libpcre2-8-0.deb /tmp/libpcre2-posix3.deb; \ sh /usr/local/bin/install-ubuntu-security-updates /licenses/ubuntu-security-packages.lock /etc/ssl/certs/ca-certificates.crt; \ + for package in libpcre2-8-0 libpcre2-posix3; do \ + test "$(dpkg-query -W -f='${Version}' "${package}")" = 10.46-1ubuntu0.1; \ + done; \ for package in curl libcurl3t64-gnutls libcurl4t64; do \ test "$(dpkg-query -W -f='${Version}' "${package}")" = "${UBUNTU_APT_CURL_VERSION}"; \ done; \ diff --git a/README.md b/README.md index f0fa819c5b..39baa2e3f1 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,24 @@ 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.366 +## Development candidate: Engine 0.183.367 + +Image-upgrade previews accept optional `startFirst`, `batchSize` and +`intervalMillis`. The immutable plan exposes the three effective values in +`upgradeStrategy`, with the existing defaults `false`, `1` and `2000`. +Preview, human confirmation and apply repeat the existing native port/volume +preflight using that approved strategy. Native role/Key checks, snapshot staleness +and exactly-once dispatch remain in effect. See the +[candidate contract](docs/releases/orchestration-engine-0.183.367.md). +This source candidate has not been published or accepted in runtime QA. +Catalog-launch, resource-change and upgrade action lookups use authorized read +snapshots to preserve application/row lock order during concurrent receipt reads. + +Typed Audit queries retain their closed-input check before generic schema +sanitization. Unknown caller fields are rejected with a canonical error outcome +instead of being discarded into an accepted query. + +## Latest published release: Engine 0.183.366 [Engine 0.183.366](https://github.com/PastureStack/orchestration-engine/releases/tag/v0.183.366) is published with 12 original assets verified by anonymous SHA256 readback. Use the @@ -177,7 +194,7 @@ Before publishing an Engine artifact, run the complete package gate: ```sh bash scripts/check-cattle-jdk25-full-package -ENGINE_VERSION=0.183.366 bash scripts/build --release +ENGINE_VERSION=0.183.367 bash scripts/build --release bash scripts/check-release-artifact dist/artifacts/cattle.jar ``` @@ -214,9 +231,11 @@ the credential identity and policy revision; that same Key must apply them. Using a browser's shared Cookie alone does not establish strict credential isolation. Ordinary UI/API authorization remains unchanged. -* `POST /v2-beta/projects/{projectId}/aiServiceUpgradePlans` with `serviceId` and - `image` creates a five-minute actor/project-bound preview and, for Key callers, - a credential/revision binding. It does not upgrade a service. +* `POST /v2-beta/projects/{projectId}/aiServiceUpgradePlans` with `serviceId`, + `image` and optional `startFirst` / `batchSize` / `intervalMillis` creates a + five-minute actor/project-bound preview and, for Key callers, a credential/revision + binding. The effective strategy is immutable and shown for human review. + It does not upgrade a service. * The confirmation UI separately calls `POST .../{planId}?action=confirm` with an empty body. The same owner's human session confirms through the console; API Keys cannot confirm, and a bound Key's current scope, expiry and revision diff --git a/code/framework/api-pub-sub-jetty/pom.xml b/code/framework/api-pub-sub-jetty/pom.xml index 3191ed3745..ed2e39df2d 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.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/api-pub-sub/pom.xml b/code/framework/api-pub-sub/pom.xml index e5bfcd0085..afb9eaceea 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.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/api/pom.xml b/code/framework/api/pom.xml index c1521b1a8b..3bdb69e3aa 100644 --- a/code/framework/api/pom.xml +++ b/code/framework/api/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/archaius/pom.xml b/code/framework/archaius/pom.xml index f46934b45f..80ceb00292 100644 --- a/code/framework/archaius/pom.xml +++ b/code/framework/archaius/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-meta-parent - 0.183.366 + 0.183.367 ../../meta-parent/pom.xml diff --git a/code/framework/async/pom.xml b/code/framework/async/pom.xml index 8eafe80a66..915b3a3b5b 100644 --- a/code/framework/async/pom.xml +++ b/code/framework/async/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/auditing/pom.xml b/code/framework/auditing/pom.xml index 3fdcb2c9a6..349031c9a4 100644 --- a/code/framework/auditing/pom.xml +++ b/code/framework/auditing/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/db-loader/pom.xml b/code/framework/db-loader/pom.xml index 936117cee1..c325ecf947 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.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/deferred/pom.xml b/code/framework/deferred/pom.xml index 45d50246d1..4f8bb2fd92 100644 --- a/code/framework/deferred/pom.xml +++ b/code/framework/deferred/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/encryption/pom.xml b/code/framework/encryption/pom.xml index ac21aae936..9598f3b26b 100644 --- a/code/framework/encryption/pom.xml +++ b/code/framework/encryption/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/engine/pom.xml b/code/framework/engine/pom.xml index 0aba006241..de0ab2bead 100644 --- a/code/framework/engine/pom.xml +++ b/code/framework/engine/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/eventing/pom.xml b/code/framework/eventing/pom.xml index 0a2bb1c035..a69ca8de48 100644 --- a/code/framework/eventing/pom.xml +++ b/code/framework/eventing/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/events/pom.xml b/code/framework/events/pom.xml index c28c45845a..943495e100 100644 --- a/code/framework/events/pom.xml +++ b/code/framework/events/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/extension-spring/pom.xml b/code/framework/extension-spring/pom.xml index 9e59e72f3a..9a8bba6ecf 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.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/extension/pom.xml b/code/framework/extension/pom.xml index 19a6b394f7..9dcf90b218 100644 --- a/code/framework/extension/pom.xml +++ b/code/framework/extension/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/java-server/pom.xml b/code/framework/java-server/pom.xml index cb7e0c7a4f..f777d20bb7 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.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/jmx/pom.xml b/code/framework/jmx/pom.xml index bcba2408e4..a6137215c1 100644 --- a/code/framework/jmx/pom.xml +++ b/code/framework/jmx/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/jooq/pom.xml b/code/framework/jooq/pom.xml index 55619d0f1a..bac1e06f35 100644 --- a/code/framework/jooq/pom.xml +++ b/code/framework/jooq/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/json/pom.xml b/code/framework/json/pom.xml index c7eb19e829..859c1086cf 100644 --- a/code/framework/json/pom.xml +++ b/code/framework/json/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/launcher/pom.xml b/code/framework/launcher/pom.xml index 3d321de0fa..d1db55bac0 100644 --- a/code/framework/launcher/pom.xml +++ b/code/framework/launcher/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/lock/pom.xml b/code/framework/lock/pom.xml index fd9822fc88..4c24099f31 100644 --- a/code/framework/lock/pom.xml +++ b/code/framework/lock/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/logback/pom.xml b/code/framework/logback/pom.xml index ac27e218ab..685b0f318c 100644 --- a/code/framework/logback/pom.xml +++ b/code/framework/logback/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-meta-parent - 0.183.366 + 0.183.367 ../../meta-parent/pom.xml diff --git a/code/framework/managed-context/pom.xml b/code/framework/managed-context/pom.xml index e35b4d5036..eb409344fe 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.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/metrics/pom.xml b/code/framework/metrics/pom.xml index 8af58d28f3..87cbff81bb 100644 --- a/code/framework/metrics/pom.xml +++ b/code/framework/metrics/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/module/pom.xml b/code/framework/module/pom.xml index 9242eb2bad..52542c681e 100644 --- a/code/framework/module/pom.xml +++ b/code/framework/module/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/object/pom.xml b/code/framework/object/pom.xml index bce652d977..458bd95d92 100644 --- a/code/framework/object/pom.xml +++ b/code/framework/object/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/pool/pom.xml b/code/framework/pool/pom.xml index caf6095d42..b1e5c57fa9 100644 --- a/code/framework/pool/pom.xml +++ b/code/framework/pool/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/resource-monitor/pom.xml b/code/framework/resource-monitor/pom.xml index 0fbe28ead9..5c040c1607 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.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/schema/pom.xml b/code/framework/schema/pom.xml index b2cfe6c0b5..259986998b 100644 --- a/code/framework/schema/pom.xml +++ b/code/framework/schema/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/server/pom.xml b/code/framework/server/pom.xml index 8114bedd91..6b7d159562 100644 --- a/code/framework/server/pom.xml +++ b/code/framework/server/pom.xml @@ -4,7 +4,7 @@ cattle-meta-parent io.cattle - 0.183.366 + 0.183.367 ../../meta-parent/pom.xml diff --git a/code/framework/spring/pom.xml b/code/framework/spring/pom.xml index 9b193bf375..7eceeee309 100644 --- a/code/framework/spring/pom.xml +++ b/code/framework/spring/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/system-task/pom.xml b/code/framework/system-task/pom.xml index b57bdf467d..cbc0f4b280 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.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/token/pom.xml b/code/framework/token/pom.xml index 64d42d2b7f..60a3ca76c5 100644 --- a/code/framework/token/pom.xml +++ b/code/framework/token/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/framework/utils/pom.xml b/code/framework/utils/pom.xml index e5159c176d..caed6301f5 100644 --- a/code/framework/utils/pom.xml +++ b/code/framework/utils/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/iaas/agent-instance/pom.xml b/code/iaas/agent-instance/pom.xml index d52dc7605b..e6b196ed1e 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.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/iaas/agent-server/pom.xml b/code/iaas/agent-server/pom.xml index 28dbd1d760..928fdaf637 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.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/iaas/agent/pom.xml b/code/iaas/agent/pom.xml index 00edfbbcdd..ec08f69f4c 100644 --- a/code/iaas/agent/pom.xml +++ b/code/iaas/agent/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/iaas/allocator/pom.xml b/code/iaas/allocator/pom.xml index ff4066e06b..b57761cde4 100644 --- a/code/iaas/allocator/pom.xml +++ b/code/iaas/allocator/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/iaas/api-logic/pom.xml b/code/iaas/api-logic/pom.xml index 2352f21955..80f543a390 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.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/iaas/archaius-management/pom.xml b/code/iaas/archaius-management/pom.xml index 0de183a1e2..36d59accb1 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.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/iaas/auth-logic/pom.xml b/code/iaas/auth-logic/pom.xml index 298472a2b1..d5942b2a12 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.366 + 0.183.367 ../../parent/pom.xml @@ -18,6 +18,16 @@ spring-jdbc test + + org.springframework + spring-context + test + + + org.aspectj + aspectjweaver + test + io.cattle cattle-docker-api diff --git a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryInputHandler.java b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryInputHandler.java new file mode 100644 index 0000000000..c26fae3677 --- /dev/null +++ b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryInputHandler.java @@ -0,0 +1,16 @@ +package io.cattle.platform.iaas.api.auth.ai.audit; + +import io.github.ibuildthecloud.gdapi.request.ApiRequest; +import io.github.ibuildthecloud.gdapi.request.handler.AbstractResponseGenerator; + +/** Capture only the typed owner's closed-field violation before ordinary schema sanitization. */ +public class AiAuditQueryInputHandler extends AbstractResponseGenerator { + @Override + protected void generate(ApiRequest request) { + if (!"POST".equals(request.getMethod()) || request.getId() != null || request.getAction() != null + || request.getSchemaFactory() == null) return; + var schema = request.getSchemaFactory().getSchema(request.getType()); + if (schema == null || !"aiAuditQuery".equals(schema.getId())) return; + AiAuditQueryService.captureOriginalFields(request); + } +} diff --git a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryService.java b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryService.java index b68d8b5ee2..78240055e4 100644 --- a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryService.java +++ b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryService.java @@ -19,6 +19,7 @@ /** The Engine observes the complete Go-owner result before durably recording its canonical outcome. */ public class AiAuditQueryService { + static final String ORIGINAL_FIELDS_REJECTED = AiAuditQueryService.class.getName() + ".originalFieldsRejected"; static final Set INPUTS = Set.of("keyId", "environmentId", "from", "to", "lastMinutes", "decision", "outcome", "operation", "targetType", "requestId", "httpStatus", "limit", "offset", "eventId"); static final Set ROW_FIELDS = Set.of("type", "id", "created", "accountId", "authenticatedAsAccountId", "keyId", @@ -36,6 +37,7 @@ public Map read(ApiRequest request) { try { actor = keys.actor(request); if (actor == null || actor.getAuthenticatedAsAccountId() <= 0) throw error(403, "PermissionDenied"); + if (Boolean.TRUE.equals(request.getAttribute(ORIGINAL_FIELDS_REJECTED))) throw error(422, "AuditQueryInputInvalid"); Map query = input(request.getRequestObject()); var servlet = request.getServletContext(); if (servlet == null || servlet.getRequest() == null) throw error(403, "PermissionDenied"); @@ -88,6 +90,16 @@ public Map read(ApiRequest request) { } } + static void captureOriginalFields(ApiRequest request) { + // Sticky across any repeated handler invocation. Retain no raw body, + // field values or caller-supplied field names in request attributes. + if (Boolean.TRUE.equals(request.getAttribute(ORIGINAL_FIELDS_REJECTED))) return; + Object value = request.getRequestObject(); + boolean rejected = !(value instanceof Map fields) + || fields.keySet().stream().anyMatch(key -> !(key instanceof String) || !"type".equals(key) && !INPUTS.contains(key)); + request.setAttribute(ORIGINAL_FIELDS_REJECTED, rejected); + } + static Map input(Object value) { Map values = object(value); values.remove("type"); if (!INPUTS.containsAll(values.keySet())) throw error(422, "AuditQueryInputInvalid"); diff --git a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchAccess.java b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchAccess.java index ac58641c22..0acf2f4d76 100644 --- a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchAccess.java +++ b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchAccess.java @@ -78,6 +78,15 @@ public ApiAuthenticator.CurrentAuthorization current(long actor, long project, A /** Only persisted, already ownership-checked plans may supply the expected binding. */ public Authorization bound(Map plan, ApiRequest request) { + return bound(plan, request, true); + } + + /** Operand prefetch must not retain a credential row lock in the outer API transaction either. */ + Authorization snapshot(Map plan, ApiRequest request) { + return bound(plan, request, false); + } + + private Authorization bound(Map plan, ApiRequest request, boolean lockCredential) { long actor = positive(plan.get("actorId")), project = positive(plan.get("accountId")); var current = current(actor, project, request); if (plan.get("credentialId") == null) return new Authorization(current, null); @@ -85,10 +94,11 @@ public Authorization bound(Map plan, ApiRequest request) { if (!(plan.get("credentialRevision") instanceof Number expectedRevision) || expectedRevision.longValue() < 0 || expectedRevision.doubleValue() != expectedRevision.longValue()) throw error(403, "OwnerPermissionDenied"); long revision = expectedRevision.longValue(); - // Current locking read, not an older ordinary SELECT from an API's RR - // snapshot. Keep this credential stable until the surrounding write commits. - Credential credential = create().selectFrom(io.cattle.platform.core.model.tables.CredentialTable.CREDENTIAL) - .where(io.cattle.platform.core.model.tables.CredentialTable.CREDENTIAL.ID.eq(id)).forUpdate().fetchOne(); + // Final get/action uses a current locking read and keeps the credential stable until commit. + // Operand snapshots are rechecked by action() before any claim, without retaining a row lock here. + var query = create().selectFrom(io.cattle.platform.core.model.tables.CredentialTable.CREDENTIAL) + .where(io.cattle.platform.core.model.tables.CredentialTable.CREDENTIAL.ID.eq(id)); + Credential credential = lockCredential ? query.forUpdate().fetchOne() : query.fetchOne(); if (credential == null || credential.getRemoved() != null || !"active".equals(credential.getState()) || !Set.of(CredentialConstants.KIND_API_KEY, CredentialConstants.KIND_API_KEY_RESTRICTED).contains(credential.getKind())) throw error(403, "ApiKeyRevoked"); diff --git a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPlanResourceManager.java b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPlanResourceManager.java index ac4599d58c..1d813393c4 100644 --- a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPlanResourceManager.java +++ b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPlanResourceManager.java @@ -15,7 +15,12 @@ public class AiCatalogLaunchPlanResourceManager extends AbstractNoOpResourceMana @Override public Class[] getTypeClasses() { return new Class[] { AiCatalogStackLaunchPlan.class }; } @Override protected Object createInternal(String type, ApiRequest request) { return resource(service.preview(request)); } @Override protected Object getByIdInternal(String type, String id, ListOptions options) { - return resource(service.get(id, ApiContext.getContext().getApiRequest())); + ApiRequest request = ApiContext.getContext().getApiRequest(); + // Native action validation/prefetch runs inside the outer API write transaction. + // Its operand lookup must not retain row locks before action() acquires the application lock. + boolean actionTarget = request != null && "POST".equals(request.getMethod()) && request.getAction() != null + && options != null && options.getMutationTarget() != null && options.getMutationTarget().matches(request, type, id); + return resource(actionTarget ? service.actionTarget(id, request) : service.get(id, request)); } @Override protected Object listInternal(SchemaFactory factory, String type, Map criteria, ListOptions options) { return service.pending(ApiContext.getContext().getApiRequest()).stream().map(this::resource).toList(); diff --git a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPlanService.java b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPlanService.java index 8ea198e40a..dfb4f3f424 100644 --- a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPlanService.java +++ b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPlanService.java @@ -101,6 +101,16 @@ else if ("needs_user".equals(plan.get("status")) && expired(plan)) { }))); } + /** A non-mutating, authorized operand snapshot. action() reloads and rechecks under its lock. */ + Map actionTarget(String id, ApiRequest request) { + return audited(request, "read", id, () -> { + Map plan = owned(id, request, false); + authority(access.snapshot(plan, request), request); + audit(request, "read", plan, true, "CatalogLaunchReadAllowed"); + return output(plan); + }); + } + public List> pending(ApiRequest request) { try { human(request); Policy actor = access.actor(request); @@ -241,7 +251,11 @@ private void rebuild(ApiRequest request, Map plan, Map plan, ApiRequest request) { - var authority = access.bound(plan, request); access.catalog(authority); + return authority(access.bound(plan, request), request); + } + + private AiCatalogLaunchAccess.Authorization authority(AiCatalogLaunchAccess.Authorization authority, ApiRequest request) { + access.catalog(authority); var schema = authority.current().schemas().getSchema(PLAN); String action = request.getAction(); if (schema == null || "GET".equals(request.getMethod()) && (schema.getResourceMethods() == null || !schema.getResourceMethods().contains("GET")) @@ -253,8 +267,12 @@ private AiCatalogLaunchAccess.Authorization authority(Map plan, } private Map owned(String id, ApiRequest request) { + return owned(id, request, true); + } + + private Map owned(String id, ApiRequest request, boolean forUpdate) { if (id == null || !id.matches("aicataloglaunch-[0-9a-f-]{36}")) throw error(404, "NotFound"); - Map plan = store.loadForUpdate(id); Policy actor = access.actor(request); + Map plan = forUpdate ? store.loadForUpdate(id) : store.load(id); Policy actor = access.actor(request); if (plan == null || !Objects.equals(((Number) plan.get("actorId")).longValue(), actor.getAuthenticatedAsAccountId()) || !Objects.equals(((Number) plan.get("accountId")).longValue(), actor.getAccountId())) throw error(404, "NotFound"); ApiKeyCredentialContext key = ApiKeyCredentialContext.get(request); diff --git a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanResourceManager.java b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanResourceManager.java index 226060b8f2..caeb2c4b80 100644 --- a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanResourceManager.java +++ b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanResourceManager.java @@ -16,7 +16,11 @@ public class AiResourceChangePlanResourceManager extends AbstractNoOpResourceMan @Override public Class[] getTypeClasses() { return new Class[] { AiResourceChangePlan.class }; } @Override protected Object createInternal(String type, ApiRequest request) { return resource(service.preview(request)); } @Override protected Object getByIdInternal(String type, String id, ListOptions options) { - return resource(service.get(id, ApiContext.getContext().getApiRequest())); + ApiRequest request = ApiContext.getContext().getApiRequest(); + // An internal action operand must not persist observations in the outer API write transaction. + boolean actionTarget = request != null && "POST".equals(request.getMethod()) && request.getAction() != null + && options != null && options.getMutationTarget() != null && options.getMutationTarget().matches(request, type, id); + return resource(actionTarget ? service.actionTarget(id, request) : service.get(id, request)); } @Override protected Object listInternal(SchemaFactory factory, String type, Map criteria, ListOptions options) { return service.pending(ApiContext.getContext().getApiRequest()).stream().map(this::resource).toList(); 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 993efa9317..0057915033 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 @@ -85,6 +85,14 @@ public Map get(String id, ApiRequest request) { })); } + /** Action operands are authorized snapshots; observing here would retain a write lock before action(). */ + Map actionTarget(String id, ApiRequest request) { + return audited(request, "read", id, () -> { + Map plan = owned(store.load(validId(id)), request); + audit(request, "read", plan, true, "ResourceChangeRead"); return output(plan); + }); + } + public List> pending(ApiRequest request) { human(request); Policy actor = domain.actor(request); List> result = new ArrayList<>(); for (Map plan : store.pending(actor.getAuthenticatedAsAccountId(), clock.millis())) { diff --git a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanInputHandler.java b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanInputHandler.java new file mode 100644 index 0000000000..3b0da6442c --- /dev/null +++ b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanInputHandler.java @@ -0,0 +1,16 @@ +package io.cattle.platform.iaas.api.auth.upgrade; + +import io.github.ibuildthecloud.gdapi.request.ApiRequest; +import io.github.ibuildthecloud.gdapi.request.handler.AbstractResponseGenerator; + +/** Preserve only invalid typed preview input presence before native coercion. */ +public class AiServiceUpgradePlanInputHandler extends AbstractResponseGenerator { + @Override + protected void generate(ApiRequest request) { + if (!"POST".equals(request.getMethod()) || request.getId() != null || request.getAction() != null + || request.getSchemaFactory() == null) return; + var schema = request.getSchemaFactory().getSchema(request.getType()); + if (schema == null || !"aiServiceUpgradePlan".equals(schema.getId())) return; + AiServiceUpgradePlanService.captureOriginalPreview(request); + } +} diff --git a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanResourceManager.java b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanResourceManager.java index 9dc4e486be..6fcc2ed9e7 100644 --- a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanResourceManager.java +++ b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanResourceManager.java @@ -21,7 +21,12 @@ public class AiServiceUpgradePlanResourceManager extends AbstractNoOpResourceMan return service.pending(ApiContext.getContext().getApiRequest()).stream().map(this::resource).toList(); } @Override protected Object getByIdInternal(String type, String id, ListOptions options) { - return resource(service.get(id, ApiContext.getContext().getApiRequest())); + ApiRequest request = ApiContext.getContext().getApiRequest(); + // Native action prefetch joins the outer write transaction. Observing here + // would retain a row lock before action() acquires the application lock. + boolean actionTarget = request != null && "POST".equals(request.getMethod()) && request.getAction() != null + && options != null && options.getMutationTarget() != null && options.getMutationTarget().matches(request, type, id); + return resource(actionTarget ? service.actionTarget(id, request) : service.get(id, request)); } @Override protected Object resourceActionInternal(Object obj, ApiRequest request) { Map result = service.action(request.getId(), request); diff --git a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanService.java b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanService.java index 54fbc4642c..dc936fbcd5 100644 --- a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanService.java +++ b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanService.java @@ -7,6 +7,7 @@ import io.cattle.platform.api.utils.ApiUtils; import io.cattle.platform.archaius.util.ArchaiusUtil; import io.cattle.platform.core.constants.ServiceConstants; +import io.cattle.platform.core.addon.InServiceUpgradeStrategy; import io.cattle.platform.core.model.Account; import io.cattle.platform.core.model.Instance; import io.cattle.platform.core.model.Service; @@ -28,6 +29,7 @@ import io.cattle.platform.object.util.DataAccessor; import io.cattle.platform.storage.api.filter.ExternalTemplateInstanceFilter; import io.cattle.platform.storage.service.StorageService; +import io.cattle.platform.servicediscovery.api.service.ServiceUpgradeValidation; import io.cattle.platform.util.type.CollectionUtils; import io.github.ibuildthecloud.gdapi.exception.ClientVisibleException; import io.github.ibuildthecloud.gdapi.context.ApiContext; @@ -39,6 +41,7 @@ import io.github.ibuildthecloud.gdapi.util.RequestUtils; import jakarta.inject.Inject; import java.io.IOException; +import java.math.BigDecimal; import java.time.Clock; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -56,6 +59,10 @@ public class AiServiceUpgradePlanService extends AbstractJooqDao { private static final Logger log = LoggerFactory.getLogger(AiServiceUpgradePlanService.class); public static final String PLAN_ATTRIBUTE = "ai.service.upgrade.plan"; public static final String SNAPSHOT_ATTRIBUTE = "ai.service.upgrade.snapshot"; + static final String ORIGINAL_PREVIEW_REJECTED = AiServiceUpgradePlanService.class.getName() + ".originalPreviewRejected"; + private static final Set PREVIEW_INPUTS = Set.of("serviceId", "image", "type", "startFirst", "batchSize", "intervalMillis"); + // JSON/JavaScript's exact integer boundary, not a native product maximum. + private static final long MAX_SAFE_INTEGER = 9007199254740991L; @Inject AiServiceUpgradePlanStore store; @Inject ObjectManager objects; @Inject JsonMapper json; @@ -64,20 +71,23 @@ public class AiServiceUpgradePlanService extends AbstractJooqDao { @Inject StorageService storage; @Inject List auditSinks; @Inject AiKeyAuthorization keyAuthorization; + @Inject ServiceUpgradeValidation upgradeValidation; Clock clock = Clock.systemUTC(); public Map preview(ApiRequest request) { return audited(request, "preview", null, () -> { requireEnabled(); Policy actor = actor(request); + if (Boolean.TRUE.equals(request.getAttribute(ORIGINAL_PREVIEW_REJECTED))) throw error(422, "UpgradeInputInvalid"); return locked("preview-" + actor.getAuthenticatedAsAccountId(), () -> { store.pruneExpiredPreviews(clock.millis()); if (store.pendingCount(actor.getAuthenticatedAsAccountId()) >= 20) throw error(429, "UpgradePendingPlanLimit"); - Map input = input(request, Set.of("serviceId", "image", "type")); + Map input = input(request, PREVIEW_INPUTS); String serviceId = text(input.get("serviceId")); String image = text(input.get("image")); if (serviceId == null || image == null || image.length() > 512 || image.matches(".*\\s.*") || image.contains("://") || image.equals("docker:")) throw error(422, "UpgradeInputInvalid"); + Map strategy = strategyValues(input); Service target = target(serviceId, request, true); requireReady(target); Map snapshot = snapshot(target); @@ -101,10 +111,13 @@ public Map preview(ApiRequest request) { plan.put("serviceId", formattedServiceId(target)); plan.put("internalServiceId", target.getId()); plan.put("serviceName", target.getName()); plan.put("currentImage", primary.get("imageUuid")); plan.put("displayPath", displayPath(target)); + snapshot.put("approvedUpgradeStrategy", copy(strategy)); plan.put("requestedImage", requested); plan.put("snapshot", snapshot); + plan.put("upgradeStrategy", strategy); plan.put("expiresAt", clock.instant().plusSeconds(300).toString()); plan.put("confirmationState", "pending"); plan.put("status", "preview"); plan.put("verifiedResult", false); plan.put("messageCode", "UpgradeNeedsConfirmation"); + preflight(plan, target); audit(request, "preview", id, target.getId(), true, "UpgradePreviewCreated"); store.insert(plan); return output(plan); @@ -121,6 +134,15 @@ public Map get(String id, ApiRequest request) { })); } + /** Authorized operand only; action() reloads and rechecks under its application lock. */ + Map actionTarget(String id, ApiRequest request) { + return audited(request, "read", id, () -> { + Map plan = owned(id, request, false); + audit(request, "read", id, number(plan.get("internalServiceId")), true, "UpgradeRead"); + return output(plan); + }); + } + /** Human inbox only; no Key can enumerate another plan by sharing its owner. */ public List> pending(ApiRequest request) { if (ApiKeyCredentialContext.get(request) != null) throw error(403, "UpgradeHumanSessionRequired"); @@ -189,15 +211,17 @@ public Map action(String id, ApiRequest request) { if (!clock.instant().isBefore(java.time.Instant.parse(text(plan.get("expiresAt"))))) throw error(409, "UpgradePlanExpired"); if ("cancelled".equals(plan.get("confirmationState"))) throw error(409, "UpgradeCancelled"); requireReady(target); - if (!snapshot(target).equals(plan.get("snapshot"))) throw error(409, "UpgradePlanStale"); + if (!expectedSnapshot(target, plan).equals(plan.get("snapshot"))) throw error(409, "UpgradePlanStale"); if ("confirm".equals(action)) { if (plan.containsKey("credentialId")) keyAuthorization.authorizeBound(plan, target, "upgrade", request); + preflight(plan, target); audit(request, action, id, target.getId(), true, "UpgradeConfirmed"); plan.put("confirmationState", "approved"); plan.put("messageCode", "UpgradeReady"); store.save(plan); return output(plan); } if (!"approved".equals(plan.get("confirmationState"))) throw error(409, "UpgradeNeedsConfirmation"); if (plan.containsKey("credentialId")) keyAuthorization.authorizeBound(plan, target, "upgrade", request); + preflight(plan, target); audit(request, action, id, target.getId(), true, "UpgradeAccepted"); plan.put("status", "dispatching"); plan.put("operationId", id); plan.put("acceptedAt", clock.millis()); @@ -244,13 +268,9 @@ protected void recordDispatchFailure(Map plan, RuntimeException } protected void dispatch(Map plan, Service target, ApiRequest original) { - Map primary = map(map(plan.get("snapshot")).get("launchConfig")); - primary.put("imageUuid", plan.get("requestedImage")); // Only primary is submitted; the existing filter copies secondary configs and resolves // their dependency versions without dropping env/ports/volumes/hardware settings. - Map strategy = new LinkedHashMap<>(); - strategy.put("launchConfig", primary); strategy.put("startFirst", false); - strategy.put("batchSize", 1); strategy.put("intervalMillis", 2000); + Map strategy = nativeStrategy(plan); ApiRequest action = new ApiRequest(original); action.setType(target.getKind()); action.setId(target.getId().toString()); action.setMethod("POST"); action.setAction("upgrade"); @@ -262,6 +282,65 @@ protected void dispatch(Map plan, Service target, ApiRequest ori if (result == null) throw error(409, "UpgradeDispatchUncertain"); } + private Map nativeStrategy(Map plan) { + Map primary = map(map(plan.get("snapshot")).get("launchConfig")); + primary.put("imageUuid", plan.get("requestedImage")); + Map strategy = effectiveStrategy(plan); + strategy.put("launchConfig", primary); + return strategy; + } + + private void preflight(Map plan, Service target) { + if (upgradeValidation == null) throw error(503, "UpgradePreflightUnavailable"); + InServiceUpgradeStrategy strategy = json.convertValue(nativeStrategy(plan), InServiceUpgradeStrategy.class); + Object secondaries = copy(map(plan.get("snapshot")).get("secondaryLaunchConfigs")); + if (secondaries instanceof List list) strategy.setSecondaryLaunchConfigs(new ArrayList<>(list)); + upgradeValidation.validate(target, strategy); + } + + private static Map strategyValues(Map input) { + Object start = input.get("startFirst"); + if (input.containsKey("startFirst") && !(start instanceof Boolean)) throw error(422, "UpgradeInputInvalid"); + Map strategy = new LinkedHashMap<>(); + strategy.put("startFirst", input.containsKey("startFirst") ? start : false); + strategy.put("batchSize", strategyInteger(input, "batchSize", 1, 1)); + strategy.put("intervalMillis", strategyInteger(input, "intervalMillis", 2000, 100)); + return strategy; + } + + static void captureOriginalPreview(ApiRequest request) { + // Retain only a sticky rejection bit, never original keys or values. + // The existing strategy parser owns the three new fields' exact types + // and bounds; serviceId/image keep their ordinary native validation. + if (Boolean.TRUE.equals(request.getAttribute(ORIGINAL_PREVIEW_REJECTED))) return; + Object value = request.getRequestObject(); + boolean rejected = !(value instanceof Map fields) + || fields.keySet().stream().anyMatch(key -> !(key instanceof String) || !PREVIEW_INPUTS.contains(key)); + if (!rejected) { + try { strategyValues((Map) value); } + catch (ClientVisibleException invalid) { rejected = true; } + } + request.setAttribute(ORIGINAL_PREVIEW_REJECTED, rejected); + } + + private static long strategyInteger(Map input, String field, long defaultValue, long minimum) { + if (!input.containsKey(field)) return defaultValue; + if (!(input.get(field) instanceof Number numeric)) throw error(422, "UpgradeInputInvalid"); + try { + long value = new BigDecimal(numeric.toString()).longValueExact(); + if (value >= minimum && value <= MAX_SAFE_INTEGER) return value; + } catch (NumberFormatException | ArithmeticException invalid) { /* No coercion or rounding. */ } + throw error(422, "UpgradeInputInvalid"); + } + + private static Map effectiveStrategy(Map plan) { + // Previously persisted image-only plans retain their original dispatch semantics. + if (!plan.containsKey("upgradeStrategy")) return strategyValues(Map.of()); + if (!(plan.get("upgradeStrategy") instanceof Map strategy) + || !strategy.keySet().equals(Set.of("startFirst", "batchSize", "intervalMillis"))) throw error(422, "UpgradeInputInvalid"); + return strategyValues(strategy); + } + protected Map owned(String id, ApiRequest request, boolean write) { Policy actor = actor(request); if (id == null || !id.matches("aiplan-[0-9a-f-]{36}")) throw error(404, "NotFound"); @@ -332,6 +411,14 @@ public Map snapshot(Service target) { return map(snapshot); } + private Map expectedSnapshot(Service target, Map plan) { + Map snapshot = snapshot(target); + // Older engines compare the classic snapshot and reject this extra approved operand. + // Genuine image-only plans retain their classic snapshot and dispatch defaults. + if (plan.containsKey("upgradeStrategy")) snapshot.put("approvedUpgradeStrategy", copy(effectiveStrategy(plan))); + return snapshot; + } + protected long processBaseline(long serviceId) { Long id = create().select(org.jooq.impl.DSL.max(PROCESS_INSTANCE.ID)).from(PROCESS_INSTANCE) .where(PROCESS_INSTANCE.RESOURCE_TYPE.eq("service").and(PROCESS_INSTANCE.RESOURCE_ID.eq(Long.toString(serviceId)))) @@ -487,7 +574,8 @@ private T locked(String id, Supplier action) { private Map audited(ApiRequest request, String action, String id, Supplier> operation) { try { return operation.get(); } catch (ClientVisibleException denied) { - audit(request, action, id, null, false, denied.getCode()); throw denied; + String reason = denied.getApiError() == null ? denied.getCode() : denied.getApiError().getCode(); + audit(request, action, id, null, false, reason); throw denied; } } protected void audit(ApiRequest request, String action, String id, Long target, boolean allowed, String reason) { @@ -521,6 +609,7 @@ protected Map output(Map plan) { if (plan.containsKey(key)) result.put(key, plan.get(key)); } if ("dispatching".equals(result.get("status"))) result.put("status", "unknown"); + result.put("upgradeStrategy", effectiveStrategy(plan)); return result; } protected String formattedServiceId(Service target) { diff --git a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryServiceTest.java b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryServiceTest.java index 4521d505d8..5f0876f25d 100644 --- a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryServiceTest.java +++ b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryServiceTest.java @@ -13,10 +13,15 @@ import io.github.ibuildthecloud.gdapi.context.ApiContext; import io.github.ibuildthecloud.gdapi.exception.ClientVisibleException; import io.github.ibuildthecloud.gdapi.factory.SchemaFactory; +import io.github.ibuildthecloud.gdapi.model.impl.SchemaImpl; +import io.github.ibuildthecloud.gdapi.request.handler.BodyParserRequestHandler; +import io.github.ibuildthecloud.gdapi.validation.ValidationHandler; import io.github.ibuildthecloud.gdapi.request.ApiRequest; import io.github.ibuildthecloud.gdapi.server.model.ApiServletContext; import jakarta.servlet.http.HttpServletRequest; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -27,6 +32,52 @@ public class AiAuditQueryServiceTest { @After public void clean() { ApiContext.remove(); } + @Test public void realParserAndSchemaCannotEraseOriginalUnknownFieldsBeforeCanonicalError() throws Exception { + for (String field : List.of("url", "method", "actorId", "authorization", "credentialId", "sourcePage", "data", "total")) { + Fixture f = new Fixture(); var ran = new java.util.concurrent.atomic.AtomicBoolean(); + f.use(input -> { ran.set(true); return f.frame(200, f.result()); }); + ApiRequest request = f.parsed("{\"lastMinutes\":10,\"limit\":1,\"" + field + "\":\"PRIVATE-IGNORED-INPUT\"}", Map.of()); + assertFalse(((Map) request.getRequestObject()).containsKey(field)); // Real generic sanitization still operates. + assertEquals(Boolean.TRUE, request.getAttribute(AiAuditQueryService.ORIGINAL_FIELDS_REJECTED)); + new AiAuditQueryInputHandler().handle(request); // Sanitized re-entry cannot erase the rejection. + var manager = new AiAuditQueryResourceManager(); manager.service = f.service; + var rejected = assertThrows(ClientVisibleException.class, () -> manager.createInternal("aiAuditQuery", request)); + assertEquals(422, rejected.getStatus()); assertEquals("AuditQueryInputInvalid", rejected.getCode()); + assertFalse(ran.get()); assertTrue(ApiAuditPayload.omitted(request)); + verify(f.service.audit).recordAuditQueryOutcome(eq(request), eq(f.actor), startsWith("aiaudit-"), + eq("ERROR"), eq("AuditQueryInputInvalid"), eq(0)); + } + } + + @Test public void originalQueryParametersAreClosedButValidTypedQueriesKeepTheirCanonicalSuccess() throws Exception { + Fixture f = new Fixture(); ApiRequest invalid = f.parsed("{\"lastMinutes\":10,\"limit\":1}", Map.of("url", new String[] {"PRIVATE"})); + assertEquals("AuditQueryInputInvalid", assertThrows(ClientVisibleException.class, () -> f.service.read(invalid)).getCode()); + Fixture valid = new Fixture(); ApiRequest request = valid.parsed("{\"type\":\"aiAuditQuery\",\"lastMinutes\":10,\"limit\":1}", Map.of()); + assertEquals(Boolean.FALSE, request.getAttribute(AiAuditQueryService.ORIGINAL_FIELDS_REJECTED)); + assertEquals("auditLog", valid.service.read(request).get("resourceType")); + verify(valid.service.audit).recordAuditQueryOutcome(eq(request), eq(valid.actor), anyString(), eq("ALLOW"), eq("AuditQueryCompleted"), eq(1)); + } + + @Test public void originalFieldCaptureIsOnlyTheAuditCollectionOwnerAndPrecedesTheRealSchemaValidator() throws Exception { + Fixture f = new Fixture(); var handler = new AiAuditQueryInputHandler(); + f.request.setMethod("POST"); f.request.setType("service"); f.request.setRequestObject(Map.of("url", "PRIVATE")); + handler.handle(f.request); + assertNull(f.request.getAttribute(AiAuditQueryService.ORIGINAL_FIELDS_REJECTED)); + assertEquals(Map.of("url", "PRIVATE"), f.request.getRequestObject()); + Path root = sourceRoot(); + String order = Files.readString(root.resolve("code/packaging/app-config/src/main/resources/META-INF/cattle/iaas-api/defaults.properties")); + assertTrue(order.indexOf("BodyParserRequestHandler,") < order.indexOf("AiAuditQueryInputHandler,")); + assertTrue(order.indexOf("AiAuditQueryInputHandler,") < order.indexOf("ValidationHandler,")); + String beans = Files.readString(root.resolve("code/packaging/app-config/src/main/java/io/cattle/platform/app/IaasApiConfig.java")); + assertTrue(beans.contains("return EMUtils.add(em, ApiRequestHandler.class, new AiAuditQueryInputHandler());")); + } + + private static Path sourceRoot() { + Path path = Path.of("").toAbsolutePath(); + while (path != null && !Files.isRegularFile(path.resolve("resources/content/schema/base/aiAuditQuery.json"))) path = path.getParent(); + assertNotNull("native source schema required", path); return path; + } + @Test public void interruptedOwnerCancelsAndRecordsErrorWithoutPartialResult() { Fixture f = new Fixture(); f.use(input -> { throw new InterruptedException("PRIVATE"); }); try { @@ -131,6 +182,21 @@ static final class Fixture { use(input -> frame(200, result())); } void use(AiAuditQueryRunner.Executor executor) { service.runner = new AiAuditQueryRunner(executor); service.runner.json = json; } + ApiRequest parsed(String body, Map params) throws Exception { + var mapper = new io.github.ibuildthecloud.gdapi.json.JacksonMapper(); mapper.init(); + SchemaImpl schema = mapper.readValue(Files.readAllBytes(sourceRoot().resolve("resources/content/schema/base/aiAuditQuery.json")), SchemaImpl.class); + schema.setId("aiAuditQuery"); + SchemaFactory schemas = mock(SchemaFactory.class); when(schemas.getSchema("aiAuditQuery")).thenReturn(schema); + ApiRequest parsed = new ApiRequest(null, schemas) { + @Override public java.io.InputStream getInputStream() { return new java.io.ByteArrayInputStream(body.getBytes(java.nio.charset.StandardCharsets.UTF_8)); } + }; + parsed.setMethod("POST"); parsed.setType("aiAuditQuery"); parsed.setRequestParams(params); + parsed.setRequestServletContext(request.getServletContext()); + var parser = new BodyParserRequestHandler(); parser.setJsonMarshaller(mapper); parser.init(); parser.handle(parsed); + new AiAuditQueryInputHandler().handle(parsed); + var validation = new ValidationHandler(); validation.init(); validation.generate(parsed); + return parsed; + } byte[] frame(int status, Map body) throws IOException { return json.writeValueAsBytes(Map.of("status", status, "body", body)); } Map result() { Map result = new LinkedHashMap<>(Map.of("resourceType", "auditLog", "total", 1, diff --git a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchAccessTest.java b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchAccessTest.java index 1290bab5f2..12f3eb2d86 100644 --- a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchAccessTest.java +++ b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchAccessTest.java @@ -86,6 +86,25 @@ public class AiCatalogLaunchAccessTest { verify(f.access.objects, never()).loadResource(Credential.class, 7L); } + @Test public void operandSnapshotRechecksOriginalKeyWithoutRetainingCredentialRowLock() { + Fixture f = new Fixture(); CredentialRecord current = credential(); ApiKeyPolicyCodec codec = new ApiKeyPolicyCodec(); + current.setData(codec.store(current, policy(Set.of("read", "create", "update"), false), 3)); + var rows = DSL.using(SQLDialect.MARIADB); + f.access.setConfiguration(DSL.using(new MockConnection(context -> { + assertFalse(context.sql().toLowerCase().contains("for update")); assertArrayEquals(new Object[] { 7L }, context.bindings()); + var result = rows.newResult(CREDENTIAL); result.add(current); return new MockResult[] { new MockResult(1, result) }; + }), SQLDialect.MARIADB).configuration()); + Map plan = Map.of("actorId", 42L, "accountId", 5L, + "credentialId", 7L, "credentialRevision", 3L, "credentialKind", "apiKeyRestricted"); + assertEquals(3, f.access.snapshot(plan, new ApiRequest(null, f.schemas)).key().revision()); + current.setData(codec.store(current, policy(Set.of("read", "create", "update"), false), 4)); + assertEquals("ApiKeyPolicyChanged", assertThrows(ClientVisibleException.class, + () -> f.access.snapshot(plan, new ApiRequest(null, f.schemas))).getCode()); + current.setState("inactive"); + assertEquals("ApiKeyRevoked", assertThrows(ClientVisibleException.class, + () -> f.access.snapshot(plan, new ApiRequest(null, f.schemas))).getCode()); + } + private static CredentialRecord credential() { CredentialRecord key = new CredentialRecord(); key.setId(7L); key.setAccountId(42L); key.setKind("apiKeyRestricted"); key.setState("active"); return key; diff --git a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchLockOrderTest.java b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchLockOrderTest.java new file mode 100644 index 0000000000..f23d89d11a --- /dev/null +++ b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchLockOrderTest.java @@ -0,0 +1,188 @@ +package io.cattle.platform.iaas.api.auth.ai.catalog; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import io.cattle.platform.api.auth.Policy; +import io.cattle.platform.iaas.api.auth.apikey.ApiKeyCredentialContext; +import io.cattle.platform.json.JacksonJsonMapper; +import io.cattle.platform.lock.impl.LockManagerImpl; +import io.cattle.platform.lock.provider.impl.InMemoryLockProvider; +import io.github.ibuildthecloud.gdapi.context.ApiContext; +import io.github.ibuildthecloud.gdapi.exception.ClientVisibleException; +import io.github.ibuildthecloud.gdapi.id.IdFormatter; +import io.github.ibuildthecloud.gdapi.model.ListOptions; +import io.github.ibuildthecloud.gdapi.model.impl.SchemaImpl; +import io.github.ibuildthecloud.gdapi.request.ApiRequest; +import io.github.ibuildthecloud.gdapi.request.handler.AbstractApiRequestHandler; +import io.github.ibuildthecloud.gdapi.request.handler.write.DefaultReadWriteApiDelegate; +import io.github.ibuildthecloud.gdapi.request.handler.write.ReadWriteApiDelegate; +import io.github.ibuildthecloud.gdapi.url.NullUrlBuilder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.h2.jdbcx.JdbcDataSource; +import org.jooq.SQLDialect; +import org.jooq.impl.DataSourceConnectionProvider; +import org.jooq.impl.DefaultConfiguration; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; + +public class AiCatalogLaunchLockOrderTest { + private Object priorWait; + @Before public void nativeLockWait() { + var config = com.netflix.config.ConfigurationManager.getConfigInstance(); + priorWait = config.getProperty("default.lock.wait.millis"); config.setProperty("default.lock.wait.millis", 10000L); + } + @After public void cleanup() { + ApiContext.remove(); var config = com.netflix.config.ConfigurationManager.getConfigInstance(); + if (priorWait == null) config.clearProperty("default.lock.wait.millis"); else config.setProperty("default.lock.wait.millis", priorWait); + } + + @Test public void nativeActionPrefetchAndConcurrentNativeGetCompleteWithApplicationBeforeRowLock() throws Exception { + var f = new AiCatalogLaunchWorkflowTest.Fixture(false); f.preview(); + String id = (String) f.store.plan.get("id"); IdFormatter formatter = ApiContext.getContext().getIdFormatter(); + f.request.setId(id); f.request.setAction("confirm"); f.request.setRequestObject(Map.of()); f.request.setUrlBuilder(new NullUrlBuilder()); + var data = new JdbcDataSource(); + data.setURL("jdbc:h2:mem:catalog-order-" + UUID.randomUUID() + ";DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1"); + try (var connection = data.getConnection()) { + connection.createStatement().execute("CREATE TABLE ai_catalog_stack_launch_plan(id VARCHAR(64) PRIMARY KEY,state VARCHAR(32)," + + "document CLOB,actor_id BIGINT,account_id BIGINT,stack_id BIGINT UNIQUE,expires_at BIGINT)"); + } + var transactions = new DataSourceTransactionManager(data); + CountDownLatch prefetched = new CountDownLatch(1), refreshAtRow = new CountDownLatch(1); + AtomicReference postThread = new AtomicReference<>(), refreshThread = new AtomicReference<>(); + AtomicInteger prefetchRowLocks = new AtomicInteger(), actionCalls = new AtomicInteger(); + var store = new AiCatalogLaunchPlanStore() { + @Override public Map loadForUpdate(String key) { + if (Thread.currentThread() == refreshThread.get()) refreshAtRow.countDown(); + if (Thread.currentThread() == postThread.get() && actionCalls.get() == 0) prefetchRowLocks.incrementAndGet(); + return super.loadForUpdate(key); + } + }; + store.json = new JacksonJsonMapper(); store.transactions = transactions; + store.setConfiguration(new DefaultConfiguration() + .set(new DataSourceConnectionProvider(new TransactionAwareDataSourceProxy(data))).set(SQLDialect.H2)); + store.insert(f.store.plan); f.service.store = store; + LockManagerImpl locks = new LockManagerImpl(); locks.setLockProvider(new InMemoryLockProvider()); f.service.locks = locks; + var manager = new AiCatalogLaunchPlanResourceManager() { + @Override protected Object getByIdInternal(String type, String key, ListOptions options) { + Object value = super.getByIdInternal(type, key, options); + if (Thread.currentThread() == postThread.get()) { prefetched.countDown(); await(refreshAtRow); } + return value; + } + @Override protected Object resourceActionInternal(Object target, ApiRequest request) { + actionCalls.incrementAndGet(); return super.resourceActionInternal(target, request); + } + }; + manager.service = f.service; + var target = new DefaultReadWriteApiDelegate(); + target.setHandlers(List.of(new AbstractApiRequestHandler() { + @Override public void handle(ApiRequest request) { manager.resourceAction(AiCatalogLaunchInput.PLAN, request); } + })); + try (var spring = advised(target, transactions)) { + var delegate = spring.getBean("writeDelegate", ReadWriteApiDelegate.class); + var workers = Executors.newFixedThreadPool(2); + try { + var post = workers.submit(() -> { + postThread.set(Thread.currentThread()); context(f.request, f.actor, formatter); + try { delegate.write(f.request); return true; } finally { ApiContext.remove(); } + }); + var refresh = workers.submit(() -> { + await(prefetched); refreshThread.set(Thread.currentThread()); + var read = new ApiRequest(f.request); read.setMethod("GET"); read.setAction(null); context(read, f.actor, formatter); + try { return manager.getById(AiCatalogLaunchInput.PLAN, id, new ListOptions()) != null; } + finally { ApiContext.remove(); } + }); + assertTrue(refresh.get(5, TimeUnit.SECONDS)); assertTrue(post.get(5, TimeUnit.SECONDS)); + assertEquals(0, prefetchRowLocks.get()); assertEquals(1, actionCalls.get()); + assertEquals("approved", store.load(id).get("confirmationState")); + } finally { workers.shutdownNow(); } + } + } + + @Test public void onlyExactInternalActionOperandUsesSnapshotAndOrdinaryGetStillChecksSource() throws Exception { + var f = new AiCatalogLaunchWorkflowTest.Fixture(false); f.preview(); String id = (String) f.store.plan.get("id"); + f.request.setId(id); f.request.setAction("confirm"); f.request.setUrlBuilder(new NullUrlBuilder()); + var manager = new AiCatalogLaunchPlanResourceManager(); manager.service = spy(f.service); + manager.getById(AiCatalogLaunchInput.PLAN, id, ListOptions.forResourceActionTarget(f.request, AiCatalogLaunchInput.PLAN, id)); + verify(manager.service).actionTarget(id, f.request); verify(manager.service, never()).get(id, f.request); + for (ListOptions options : List.of(new ListOptions(), + ListOptions.forResourceActionTarget(new ApiRequest(f.request), AiCatalogLaunchInput.PLAN, id), + ListOptions.forResourceActionTarget(f.request, "stack", id), + ListOptions.forResourceActionTarget(f.request, AiCatalogLaunchInput.PLAN, "other"))) + manager.getById(AiCatalogLaunchInput.PLAN, id, options); + verify(manager.service, times(4)).get(id, f.request); + f.digest = "b".repeat(64); f.request.setMethod("GET"); f.request.setAction(null); + manager.getById(AiCatalogLaunchInput.PLAN, id, new ListOptions()); + assertEquals("failed", f.store.plan.get("status")); assertEquals("CatalogLaunchSourceChanged", f.store.plan.get("reason")); + } + + @Test public void snapshotRetainsOwnerProjectRoleKeyBindingAndDoesNotAuthorizeUnconfirmedApply() throws Exception { + var f = new AiCatalogLaunchWorkflowTest.Fixture(false); f.preview(); String id = (String) f.store.plan.get("id"); + f.request.setId(id); f.request.setAction("apply"); f.request.setRequestObject(Map.of()); + var safe = f.service.actionTarget(id, f.request); + assertFalse(f.json.writeValueAsString(safe).contains("privateEnvelope")); + assertEquals("CatalogLaunchNeedsConfirmation", assertThrows(ClientVisibleException.class, + () -> f.service.action(id, f.request)).getCode()); assertEquals(0, f.store.claims); + when(f.actor.getAuthenticatedAsAccountId()).thenReturn(43L); + assertEquals(404, assertThrows(ClientVisibleException.class, () -> f.service.actionTarget(id, f.request)).getStatus()); + when(f.actor.getAuthenticatedAsAccountId()).thenReturn(42L); when(f.actor.getAccountId()).thenReturn(6L); + assertEquals(404, assertThrows(ClientVisibleException.class, () -> f.service.actionTarget(id, f.request)).getStatus()); + when(f.actor.getAccountId()).thenReturn(5L); + ApiKeyCredentialContext.attach(f.request, new ApiKeyCredentialContext(7, 42, "apiKey", 1, null)); + assertEquals("OwnerPermissionDenied", assertThrows(ClientVisibleException.class, + () -> f.service.actionTarget(id, f.request)).getCode()); + var role = new AiCatalogLaunchWorkflowTest.Fixture(false); role.preview(); role.request.setAction("confirm"); + ((SchemaImpl) role.schemas.getSchema(AiCatalogLaunchInput.PLAN)).setResourceActions(Map.of()); + assertEquals("OwnerPermissionDenied", assertThrows(ClientVisibleException.class, + () -> role.service.actionTarget((String) role.store.plan.get("id"), role.request)).getCode()); + } + + private static void await(CountDownLatch latch) { + try { assertTrue("Catalog lock ordering barrier", latch.await(5, TimeUnit.SECONDS)); } + catch (InterruptedException interrupted) { Thread.currentThread().interrupt(); throw new AssertionError(interrupted); } + } + private static void context(ApiRequest request, Policy actor, IdFormatter formatter) { + ApiContext.newContext().setApiRequest(request); ApiContext.getContext().setPolicy(actor); + ApiContext.getContext().setIdFormatter(formatter); + } + private static GenericApplicationContext advised(ReadWriteApiDelegate target, DataSourceTransactionManager transactions) throws Exception { + Path config = null; + for (Path at = Path.of("").toAbsolutePath(); at != null; at = at.getParent()) { + Path candidate = at.resolve("code/packaging/app-config/src/main/resources/META-INF/cattle/legacy/spring-legacy-context.xml"); + if (Files.isRegularFile(candidate)) { config = candidate; break; } + } + assertNotNull("Shipped API transaction advice", config); + String shipped = Files.readString(config); + int start = shipped.indexOf("", start) + "".length()); + start = shipped.indexOf("", start) + 2); + String xml = "" + + advice + "" + advisor + ""; + var spring = new GenericApplicationContext(); + spring.registerBean("CoreTransactionManager", DataSourceTransactionManager.class, () -> transactions); + spring.registerBean("writeDelegate", ReadWriteApiDelegate.class, () -> target); + new XmlBeanDefinitionReader(spring).loadBeanDefinitions(new ByteArrayResource(xml.getBytes(StandardCharsets.UTF_8))); + spring.refresh(); return spring; + } +} diff --git a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchWorkflowTest.java b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchWorkflowTest.java index 7be16aa167..42c271067c 100644 --- a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchWorkflowTest.java +++ b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchWorkflowTest.java @@ -141,17 +141,18 @@ public class AiCatalogLaunchWorkflowTest { assertNull(f.store.plan); assertEquals(0, f.store.claims); verifyNoInteractions(f.manager); } - private static final class Store extends AiCatalogLaunchPlanStore { + static final class Store extends AiCatalogLaunchPlanStore { Map plan; int claims; @Override public T transaction(Supplier action) { return action.get(); } @Override public void insert(Map value) { plan = value; } @Override public Map loadForUpdate(String id) { return plan != null && id.equals(plan.get("id")) ? plan : null; } + @Override public Map load(String id) { return loadForUpdate(id); } @Override public void save(Map value) { plan = value; } @Override public boolean claim(Map value) { if (claims != 0) return false; claims++; return true; } @Override public int pendingCount(long actor, long now) { return plan == null ? 0 : 1; } } - private static final class Fixture { + static final class Fixture { final JacksonJsonMapper json = new JacksonJsonMapper(); final Policy actor = mock(Policy.class); final SchemaFactory schemas = mock(SchemaFactory.class); final SchemaImpl serviceSchema = new SchemaImpl(); final ResourceManager manager = mock(ResourceManager.class); final ApiRequest request; final Store store = new Store(); @@ -188,6 +189,7 @@ private static final class Fixture { service.access = new AiCatalogLaunchAccess() { @Override public Policy actor(ApiRequest request) { return actor; } @Override public Authorization bound(Map plan, ApiRequest request) { return authority; } + @Override Authorization snapshot(Map plan, ApiRequest request) { return authority; } }; service.access.idFormatter = formatter; service.access.keyAccess = mock(ApiKeyAuthorizationService.class); service.access.validation = new ValidationHandler(); service.access.validation.setSupportedMethods(Set.of("GET", "POST", "PUT", "DELETE")); diff --git a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeLockOrderTest.java b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeLockOrderTest.java new file mode 100644 index 0000000000..54cd9c5371 --- /dev/null +++ b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeLockOrderTest.java @@ -0,0 +1,196 @@ +package io.cattle.platform.iaas.api.auth.change; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import io.cattle.platform.api.auth.Policy; +import io.cattle.platform.iaas.api.auth.apikey.ApiKeyCredentialContext; +import io.cattle.platform.json.JacksonJsonMapper; +import io.cattle.platform.lock.impl.LockManagerImpl; +import io.cattle.platform.lock.provider.impl.InMemoryLockProvider; +import io.github.ibuildthecloud.gdapi.context.ApiContext; +import io.github.ibuildthecloud.gdapi.exception.ClientVisibleException; +import io.github.ibuildthecloud.gdapi.id.IdFormatter; +import io.github.ibuildthecloud.gdapi.model.ListOptions; +import io.github.ibuildthecloud.gdapi.request.ApiRequest; +import io.github.ibuildthecloud.gdapi.request.handler.AbstractApiRequestHandler; +import io.github.ibuildthecloud.gdapi.request.handler.write.DefaultReadWriteApiDelegate; +import io.github.ibuildthecloud.gdapi.request.handler.write.ReadWriteApiDelegate; +import io.github.ibuildthecloud.gdapi.url.NullUrlBuilder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.h2.jdbcx.JdbcDataSource; +import org.jooq.SQLDialect; +import org.jooq.impl.DataSourceConnectionProvider; +import org.jooq.impl.DefaultConfiguration; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; + +public class AiResourceChangeLockOrderTest { + private Object priorWait; + @Before public void nativeLockWait() { + var config = com.netflix.config.ConfigurationManager.getConfigInstance(); + priorWait = config.getProperty("default.lock.wait.millis"); config.setProperty("default.lock.wait.millis", 10000L); + } + @After public void cleanup() { + ApiContext.remove(); var config = com.netflix.config.ConfigurationManager.getConfigInstance(); + if (priorWait == null) config.clearProperty("default.lock.wait.millis"); else config.setProperty("default.lock.wait.millis", priorWait); + } + + @Test public void nativeActionReceiptPrefetchAndConcurrentGetDoNotInvertObservationWriteLocks() throws Exception { + var f = new AiResourceChangePlanServiceTest.Fixture(); f.preview(false); + Map plan = f.documents.get(f.id); + plan.putAll(Map.of("status", "running", "confirmationState", "approved", "operationId", f.id, + "dispatchAcknowledged", true, "acceptedAt", f.service.clock.millis())); + var request = f.request("apply", false); request.setUrlBuilder(new NullUrlBuilder()); + IdFormatter formatter = ApiContext.getContext().getIdFormatter(); + var data = new JdbcDataSource(); + data.setURL("jdbc:h2:mem:change-order-" + UUID.randomUUID() + + ";DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1;LOCK_TIMEOUT=20000"); + try (var connection = data.getConnection()) { + connection.createStatement().execute("CREATE TABLE ai_resource_change_plan(id VARCHAR(64) PRIMARY KEY,state VARCHAR(32)," + + "document CLOB,actor_id BIGINT,expires_at BIGINT,execution_binding CLOB,failure_code VARCHAR(128))"); + } + var transactions = new DataSourceTransactionManager(data); + CountDownLatch prefetched = new CountDownLatch(1), refreshAtWrite = new CountDownLatch(1), actionAttempt = new CountDownLatch(1); + AtomicReference postThread = new AtomicReference<>(), refreshThread = new AtomicReference<>(); + AtomicInteger prefetchWrites = new AtomicInteger(), actionCalls = new AtomicInteger(); + var store = new AiResourceChangePlanStore() { + @Override public void save(Map value) { + if (Thread.currentThread() == refreshThread.get()) refreshAtWrite.countDown(); + if (Thread.currentThread() == postThread.get() && actionCalls.get() == 0) prefetchWrites.incrementAndGet(); + super.save(value); + } + }; + store.json = new JacksonJsonMapper(); + store.setConfiguration(new DefaultConfiguration() + .set(new DataSourceConnectionProvider(new TransactionAwareDataSourceProxy(data))).set(SQLDialect.H2)); + store.insert(plan); store.save(plan); f.service.store = store; + LockManagerImpl locks = new LockManagerImpl(); locks.setLockProvider(new InMemoryLockProvider()); f.service.locks = locks; + var manager = new AiResourceChangePlanResourceManager() { + @Override protected Object getByIdInternal(String type, String key, ListOptions options) { + Object value = super.getByIdInternal(type, key, options); + if (Thread.currentThread() == postThread.get()) { prefetched.countDown(); await(refreshAtWrite); } + return value; + } + @Override protected Object resourceActionInternal(Object target, ApiRequest action) { + actionCalls.incrementAndGet(); actionAttempt.countDown(); return super.resourceActionInternal(target, action); + } + }; + manager.service = f.service; + var target = new DefaultReadWriteApiDelegate(); + target.setHandlers(List.of(new AbstractApiRequestHandler() { + @Override public void handle(ApiRequest action) { manager.resourceAction(AiResourceChangePlanResourceManager.TYPE, action); } + })); + try (var spring = advised(target, transactions)) { + var delegate = spring.getBean("writeDelegate", ReadWriteApiDelegate.class); + var workers = Executors.newFixedThreadPool(2); + try { + var post = workers.submit(() -> { + postThread.set(Thread.currentThread()); context(request, f.policy, formatter); + try { delegate.write(request); return true; } finally { ApiContext.remove(); } + }); + var refresh = workers.submit(() -> { + await(prefetched); refreshThread.set(Thread.currentThread()); + var read = new ApiRequest(request); read.setMethod("GET"); read.setAction(null); context(read, f.policy, formatter); + try { return manager.getById(AiResourceChangePlanResourceManager.TYPE, f.id, new ListOptions()) != null; } + finally { ApiContext.remove(); } + }); + await(actionAttempt); + try { assertTrue(refresh.get(3, TimeUnit.SECONDS)); assertTrue(post.get(3, TimeUnit.SECONDS)); } + catch (TimeoutException blocked) { + boolean postAtApplicationLock = java.util.Arrays.stream(postThread.get().getStackTrace()) + .anyMatch(frame -> frame.getClassName().endsWith("StandardLock") && frame.getMethodName().equals("lock")); + boolean getAtDatabaseRow = java.util.Arrays.stream(refreshThread.get().getStackTrace()) + .anyMatch(frame -> frame.getClassName().startsWith("org.h2.mvstore.tx.Transaction")); + throw new AssertionError("OFFLINE_NATIVE_CHANGE_LOCK_INVERSION prefetchWrites=" + prefetchWrites.get() + + " postAtApplicationLock=" + postAtApplicationLock + " getAtDatabaseRow=" + getAtDatabaseRow, blocked); + } + assertEquals(0, prefetchWrites.get()); assertEquals(1, actionCalls.get()); + assertEquals("running", store.load(f.id).get("status")); assertEquals(0, f.claims); assertEquals(0, f.dispatched); + } finally { workers.shutdownNow(); } + } + } + + @Test public void exactSnapshotRetainsOwnerProjectKeyAndReceiptPermissionsWhileGetStillObservesAndApplyRequiresConfirmation() throws Exception { + var f = new AiResourceChangePlanServiceTest.Fixture(); f.preview(false); + f.documents.get(f.id).putAll(Map.of("status", "accepted", "operationId", f.id, + "dispatchAcknowledged", true, "acceptedAt", f.service.clock.millis())); + var request = f.request("apply", false); request.setUrlBuilder(new NullUrlBuilder()); ApiContext.getContext().setApiRequest(request); + var manager = new AiResourceChangePlanResourceManager(); manager.service = spy(f.service); + ListOptions exact = ListOptions.forResourceActionTarget(request, AiResourceChangePlanResourceManager.TYPE, f.id); + manager.getById(AiResourceChangePlanResourceManager.TYPE, f.id, exact); + assertEquals("accepted", f.documents.get(f.id).get("status")); + verify(manager.service).actionTarget(f.id, request); verify(manager.service, never()).get(f.id, request); + manager.getById(AiResourceChangePlanResourceManager.TYPE, f.id, + ListOptions.forResourceActionTarget(new ApiRequest(request), AiResourceChangePlanResourceManager.TYPE, f.id)); + verify(manager.service).get(f.id, request); assertEquals("running", f.documents.get(f.id).get("status")); + when(f.policy.getAuthenticatedAsAccountId()).thenReturn(43L); + assertEquals(404, assertThrows(ClientVisibleException.class, () -> f.service.actionTarget(f.id, request)).getStatus()); + when(f.policy.getAuthenticatedAsAccountId()).thenReturn(42L); when(f.policy.getAccountId()).thenReturn(88L); + assertEquals(404, assertThrows(ClientVisibleException.class, () -> f.service.actionTarget(f.id, request)).getStatus()); + when(f.policy.getAccountId()).thenReturn(77L); + when(f.domain.receiptTarget(any(), any(), anyMap())).thenThrow(new ClientVisibleException(403, "PermissionDenied")); + assertEquals("PermissionDenied", assertThrows(ClientVisibleException.class, + () -> f.service.actionTarget(f.id, request)).getCode()); + var key = new AiResourceChangePlanServiceTest.Fixture(); key.preview(true); + var wrongKey = key.request("apply", true); + ApiKeyCredentialContext.attach(wrongKey, new ApiKeyCredentialContext(8, 42, "apiKeyRestricted", 3, null)); + assertEquals(404, assertThrows(ClientVisibleException.class, () -> key.service.actionTarget(key.id, wrongKey)).getStatus()); + ApiKeyCredentialContext.attach(wrongKey, new ApiKeyCredentialContext(7, 42, "apiKeyRestricted", 4, null)); + assertEquals(404, assertThrows(ClientVisibleException.class, () -> key.service.actionTarget(key.id, wrongKey)).getStatus()); + var pending = new AiResourceChangePlanServiceTest.Fixture(); pending.preview(false); + var apply = pending.request("apply", false); pending.service.actionTarget(pending.id, apply); + assertEquals("ResourceChangeNeedsConfirmation", assertThrows(ClientVisibleException.class, + () -> pending.service.action(pending.id, apply)).getCode()); assertEquals(0, pending.claims); assertEquals(0, pending.dispatched); + } + + private static void await(CountDownLatch latch) { + try { assertTrue("Resource-change lock ordering barrier", latch.await(5, TimeUnit.SECONDS)); } + catch (InterruptedException interrupted) { Thread.currentThread().interrupt(); throw new AssertionError(interrupted); } + } + private static void context(ApiRequest request, Policy actor, IdFormatter formatter) { + ApiContext.newContext().setApiRequest(request); ApiContext.getContext().setPolicy(actor); + ApiContext.getContext().setIdFormatter(formatter); + } + private static GenericApplicationContext advised(ReadWriteApiDelegate target, DataSourceTransactionManager transactions) throws Exception { + Path config = null; + for (Path at = Path.of("").toAbsolutePath(); at != null; at = at.getParent()) { + Path candidate = at.resolve("code/packaging/app-config/src/main/resources/META-INF/cattle/legacy/spring-legacy-context.xml"); + if (Files.isRegularFile(candidate)) { config = candidate; break; } + } + assertNotNull("Shipped API transaction advice", config); + String shipped = Files.readString(config); + int start = shipped.indexOf("", start) + "".length()); + start = shipped.indexOf("", start) + 2); + String xml = "" + + advice + "" + advisor + ""; + var spring = new GenericApplicationContext(); + spring.registerBean("CoreTransactionManager", DataSourceTransactionManager.class, () -> transactions); + spring.registerBean("writeDelegate", ReadWriteApiDelegate.class, () -> target); + new XmlBeanDefinitionReader(spring).loadBeanDefinitions(new ByteArrayResource(xml.getBytes(StandardCharsets.UTF_8))); + spring.refresh(); return spring; + } +} diff --git a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanLockOrderTest.java b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanLockOrderTest.java new file mode 100644 index 0000000000..3229482bd4 --- /dev/null +++ b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanLockOrderTest.java @@ -0,0 +1,304 @@ +package io.cattle.platform.iaas.api.auth.upgrade; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import io.cattle.platform.api.auth.Policy; +import io.cattle.platform.iaas.api.auth.apikey.ApiKeyCredentialContext; +import io.cattle.platform.json.JacksonJsonMapper; +import io.cattle.platform.lock.impl.LockManagerImpl; +import io.cattle.platform.lock.provider.impl.InMemoryLockProvider; +import io.github.ibuildthecloud.gdapi.context.ApiContext; +import io.github.ibuildthecloud.gdapi.exception.ClientVisibleException; +import io.github.ibuildthecloud.gdapi.id.IdFormatter; +import io.github.ibuildthecloud.gdapi.id.IdentityFormatter; +import io.github.ibuildthecloud.gdapi.model.ListOptions; +import io.github.ibuildthecloud.gdapi.request.ApiRequest; +import io.github.ibuildthecloud.gdapi.request.handler.AbstractApiRequestHandler; +import io.github.ibuildthecloud.gdapi.request.handler.write.DefaultReadWriteApiDelegate; +import io.github.ibuildthecloud.gdapi.request.handler.write.ReadWriteApiDelegate; +import io.github.ibuildthecloud.gdapi.url.NullUrlBuilder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.h2.jdbcx.JdbcDataSource; +import org.jooq.SQLDialect; +import org.jooq.impl.DataSourceConnectionProvider; +import org.jooq.impl.DefaultConfiguration; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +public class AiServiceUpgradePlanLockOrderTest { + private Object priorWait; + @Before public void nativeLockWait() { + ApiContext.newContext().setIdFormatter(new IdentityFormatter()); + var config = com.netflix.config.ConfigurationManager.getConfigInstance(); + priorWait = config.getProperty("default.lock.wait.millis"); config.setProperty("default.lock.wait.millis", 10000L); + } + @After public void cleanup() { + ApiContext.remove(); var config = com.netflix.config.ConfigurationManager.getConfigInstance(); + if (priorWait == null) config.clearProperty("default.lock.wait.millis"); else config.setProperty("default.lock.wait.millis", priorWait); + } + + @Test public void acceptedUpgradeNativeActionPrefetchAndConcurrentGetKeepApplicationBeforeRowLock() throws Exception { + race("accepted"); + } + + @Test public void runningUpgradeNativeActionPrefetchAndConcurrentGetKeepApplicationBeforeRowLock() throws Exception { + race("running"); + } + + private void race(String initialState) throws Exception { + var f = new AiServiceUpgradePlanServiceTest.Fixture(); f.preview(); f.action("confirm"); f.action("apply"); + assertEquals(1, f.dispatched); assertEquals("accepted", f.document().get("status")); + f.document().put("status", initialState); + f.observed.add(Map.of("name", "service.upgrade", "ended", false)); + var request = f.request("apply", Map.of()); request.setId(f.id); request.setType(AiServiceUpgradePlanResourceManager.TYPE); + request.setMethod("POST"); request.setUrlBuilder(new NullUrlBuilder()); + IdFormatter formatter = new IdentityFormatter(); + var data = new JdbcDataSource(); + data.setURL("jdbc:h2:mem:upgrade-order-" + UUID.randomUUID() + ";DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1;LOCK_TIMEOUT=10000"); + try (var connection = data.getConnection()) { + connection.createStatement().execute("CREATE TABLE ai_service_upgrade_plan(id VARCHAR(64) PRIMARY KEY,state VARCHAR(32)," + + "document CLOB,actor_id BIGINT,expires_at BIGINT)"); + } + var transactions = new DataSourceTransactionManager(data); + CountDownLatch prefetched = new CountDownLatch(1), refreshAtSave = new CountDownLatch(1); + AtomicReference postThread = new AtomicReference<>(), refreshThread = new AtomicReference<>(); + AtomicInteger prefetchWrites = new AtomicInteger(), actionCalls = new AtomicInteger(); + var store = new AiServiceUpgradePlanStore() { + @Override public void save(Map plan) { + if (Thread.currentThread() == refreshThread.get()) refreshAtSave.countDown(); + if (Thread.currentThread() == postThread.get() && actionCalls.get() == 0) prefetchWrites.incrementAndGet(); + super.save(plan); + } + }; + store.json = new JacksonJsonMapper(); + store.setConfiguration(new DefaultConfiguration() + .set(new DataSourceConnectionProvider(new TransactionAwareDataSourceProxy(data))).set(SQLDialect.H2)); + store.insert(f.document()); f.service.store = store; + var locks = new LockManagerImpl(); locks.setLockProvider(new InMemoryLockProvider()); f.service.locks = locks; + var manager = new AiServiceUpgradePlanResourceManager() { + @Override protected Object getByIdInternal(String type, String id, ListOptions options) { + Object value = super.getByIdInternal(type, id, options); + if (Thread.currentThread() == postThread.get()) { + assertTrue("Native action prefetch joins shipped outer write transaction", TransactionSynchronizationManager.isActualTransactionActive()); + prefetched.countDown(); await(refreshAtSave); + } + return value; + } + @Override protected Object resourceActionInternal(Object target, ApiRequest action) { + actionCalls.incrementAndGet(); return super.resourceActionInternal(target, action); + } + }; + manager.service = f.service; + var target = new DefaultReadWriteApiDelegate(); + target.setHandlers(List.of(new AbstractApiRequestHandler() { + @Override public void handle(ApiRequest action) { manager.resourceAction(AiServiceUpgradePlanResourceManager.TYPE, action); } + })); + try (var spring = advised(target, transactions)) { + var delegate = spring.getBean("writeDelegate", ReadWriteApiDelegate.class); + var workers = Executors.newFixedThreadPool(2); + try { + var post = workers.submit(() -> { + postThread.set(Thread.currentThread()); context(request, f.policy, formatter); + try { delegate.write(request); return true; } finally { ApiContext.remove(); } + }); + var refresh = workers.submit(() -> { + await(prefetched); refreshThread.set(Thread.currentThread()); + var read = new ApiRequest(request); read.setMethod("GET"); read.setAction(null); context(read, f.policy, formatter); + try { return manager.getById(AiServiceUpgradePlanResourceManager.TYPE, f.id, new ListOptions()) != null; } + finally { ApiContext.remove(); } + }); + try { + assertTrue(refresh.get(5, TimeUnit.SECONDS)); assertTrue(post.get(5, TimeUnit.SECONDS)); + } catch (TimeoutException deadlock) { + System.out.println("UPGRADE_LOCK_ORDER_BARRIER prefetchWrites=" + prefetchWrites.get() + " actionCalls=" + actionCalls.get()); + for (Thread thread : List.of(postThread.get(), refreshThread.get())) { + System.out.println("UPGRADE_LOCK_ORDER_THREAD " + (thread == postThread.get() ? "POST" : "GET") + " " + thread.getState()); + for (StackTraceElement frame : thread.getStackTrace()) + if (frame.getClassName().startsWith("io.cattle.platform.lock") || frame.getClassName().startsWith("org.h2")) + System.out.println(frame); + } + throw deadlock; + } + assertEquals(0, prefetchWrites.get()); assertEquals(1, actionCalls.get()); + assertEquals("running", store.load(f.id).get("status")); assertEquals(1, f.dispatched); + } finally { workers.shutdownNow(); assertTrue(workers.awaitTermination(15, TimeUnit.SECONDS)); } + } + } + + @Test public void onlyExactNativeUpgradeActionMarkerUsesReadSnapshotAndOrdinaryGetStillObserves() { + var f = new AiServiceUpgradePlanServiceTest.Fixture(); f.preview(); f.action("confirm"); f.action("apply"); + f.observed.add(Map.of("name", "service.upgrade", "ended", false)); + var request = f.request("apply", Map.of()); request.setId(f.id); request.setType(AiServiceUpgradePlanResourceManager.TYPE); + request.setMethod("POST"); request.setUrlBuilder(new NullUrlBuilder()); context(request, f.policy, new IdentityFormatter()); + var manager = new AiServiceUpgradePlanResourceManager(); manager.service = spy(f.service); + manager.getById(AiServiceUpgradePlanResourceManager.TYPE, f.id, + ListOptions.forResourceActionTarget(request, AiServiceUpgradePlanResourceManager.TYPE, f.id)); + verify(manager.service).actionTarget(f.id, request); verify(manager.service, never()).get(f.id, request); + assertEquals("accepted", f.document().get("status")); + for (ListOptions options : List.of(new ListOptions(), + ListOptions.forResourceActionTarget(new ApiRequest(request), AiServiceUpgradePlanResourceManager.TYPE, f.id), + ListOptions.forResourceActionTarget(request, "service", f.id), + ListOptions.forResourceActionTarget(request, AiServiceUpgradePlanResourceManager.TYPE, "other"))) + manager.getById(AiServiceUpgradePlanResourceManager.TYPE, f.id, options); + assertEquals("running", f.document().get("status")); + var beforeChangedAction = ListOptions.forResourceActionTarget(request, AiServiceUpgradePlanResourceManager.TYPE, f.id); + request.setAction("cancel"); manager.getById(AiServiceUpgradePlanResourceManager.TYPE, f.id, beforeChangedAction); + var beforeChangedMethod = ListOptions.forResourceActionTarget(request, AiServiceUpgradePlanResourceManager.TYPE, f.id); + request.setMethod("GET"); request.setAction(null); + // Body/query values cannot forge the internal ListOptions marker. + request.setRequestObject(Map.of("mutationTarget", true)); request.setRequestParams(Map.of("mutationTarget", new String[] {"true"})); + manager.getById(AiServiceUpgradePlanResourceManager.TYPE, f.id, beforeChangedMethod); + manager.getById(AiServiceUpgradePlanResourceManager.TYPE, f.id, new ListOptions(request)); + verify(manager.service, times(7)).get(f.id, request); + f.observed.clear(); f.observed.add(Map.of("name", "service.upgrade", "ended", true, "result", "FAILURE")); + manager.getById(AiServiceUpgradePlanResourceManager.TYPE, f.id, new ListOptions()); + assertEquals("failed", f.document().get("status")); assertEquals("UpgradeProcessFailed", f.document().get("messageCode")); + } + + @Test public void upgradeActionSnapshotRetainsOwnerProjectRoleVisibilityAndBoundKeyChecks() { + var f = new AiServiceUpgradePlanServiceTest.Fixture(); f.preview(); + var request = f.request("apply", Map.of()); + var snapshot = f.service.actionTarget(f.id, request); + assertFalse(snapshot.containsKey("snapshot")); assertFalse(snapshot.containsKey("credentialId")); + assertFalse(snapshot.containsKey("body")); assertFalse(snapshot.containsKey("internalServiceId")); + assertEquals("UpgradeNeedsConfirmation", assertThrows(ClientVisibleException.class, + () -> f.service.action(f.id, request)).getCode()); assertEquals(0, f.dispatched); + when(f.policy.getAuthenticatedAsAccountId()).thenReturn(43L); + assertEquals(404, assertThrows(ClientVisibleException.class, () -> f.service.actionTarget(f.id, request)).getStatus()); + when(f.policy.getAuthenticatedAsAccountId()).thenReturn(42L); when(f.policy.getAccountId()).thenReturn(78L); + assertEquals(404, assertThrows(ClientVisibleException.class, () -> f.service.actionTarget(f.id, request)).getStatus()); + when(f.policy.getAccountId()).thenReturn(77L); when(f.schema.getResourceMethods()).thenReturn(List.of()); + assertEquals("PermissionDenied", assertThrows(ClientVisibleException.class, () -> f.service.actionTarget(f.id, request)).getCode()); + when(f.schema.getResourceMethods()).thenReturn(List.of("GET")); when(f.manager.getById(anyString(), anyString(), any())).thenReturn(null); + assertEquals(404, assertThrows(ClientVisibleException.class, () -> f.service.actionTarget(f.id, request)).getStatus()); + when(f.manager.getById(anyString(), anyString(), any())).thenReturn(f.target); + ApiKeyCredentialContext.attach(request, new ApiKeyCredentialContext(8, 42, "apiKey", 1, null)); + assertEquals(404, assertThrows(ClientVisibleException.class, () -> f.service.actionTarget(f.id, request)).getStatus()); + f.document().put("credentialId", 8L); f.document().put("credentialRevision", 1L); f.document().put("credentialKind", "apiKey"); + assertEquals("preview", f.service.actionTarget(f.id, request).get("status")); + verify(f.service.keyAuthorization).authorizeService(request, f.target, "read"); + ApiKeyCredentialContext.attach(request, new ApiKeyCredentialContext(8, 42, "apiKey", 2, null)); + assertEquals(404, assertThrows(ClientVisibleException.class, () -> f.service.actionTarget(f.id, request)).getStatus()); + } + + @Test public void upgradeActionReloadsSnapshotRoleAndKeyAuthorityAfterOperandLookupAndClaimsOnlyOnce() { + var changed = new AiServiceUpgradePlanServiceTest.Fixture(); changed.preview(); + var request = changed.request("confirm", Map.of()); changed.service.actionTarget(changed.id, request); + changed.target.setName("changed-after-operand"); + assertEquals("UpgradePlanStale", assertThrows(ClientVisibleException.class, + () -> changed.service.action(changed.id, request)).getCode()); assertEquals(0, changed.dispatched); + var role = new AiServiceUpgradePlanServiceTest.Fixture(); role.preview(); + role.service.actionTarget(role.id, role.request("confirm", Map.of())); when(role.schema.getResourceActions()).thenReturn(Map.of()); + assertEquals("PermissionDenied", assertThrows(ClientVisibleException.class, () -> role.action("confirm")).getCode()); + var key = new AiServiceUpgradePlanServiceTest.Fixture(); + key.key = new ApiKeyCredentialContext(8, 42, "apiKey", 1, null); key.preview(); + key.key = null; key.action("confirm"); key.key = new ApiKeyCredentialContext(8, 42, "apiKey", 1, null); + var apply = key.request("apply", Map.of()); key.service.actionTarget(key.id, apply); + doThrow(new ClientVisibleException(403, "ApiKeyPolicyChanged")).when(key.service.keyAuthorization) + .authorizeBound(anyMap(), eq(key.target), eq("upgrade"), eq(apply)); + assertEquals("ApiKeyPolicyChanged", assertThrows(ClientVisibleException.class, () -> key.service.action(key.id, apply)).getCode()); + assertEquals(0, key.dispatched); + var once = new AiServiceUpgradePlanServiceTest.Fixture(); once.preview(); once.action("confirm"); + once.service.actionTarget(once.id, once.request("apply", Map.of())); once.action("apply"); + once.service.actionTarget(once.id, once.request("apply", Map.of())); once.action("apply"); assertEquals(1, once.dispatched); + } + + @Test public void concurrentNativeUpgradeAppliesShareOneDurableClaimAndDispatch() throws Exception { + var f = new AiServiceUpgradePlanServiceTest.Fixture(); f.preview(); f.action("confirm"); + var request = f.request("apply", Map.of()); request.setId(f.id); request.setType(AiServiceUpgradePlanResourceManager.TYPE); + request.setMethod("POST"); request.setUrlBuilder(new NullUrlBuilder()); + var data = new JdbcDataSource(); + data.setURL("jdbc:h2:mem:upgrade-claim-" + UUID.randomUUID() + ";DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1;LOCK_TIMEOUT=10000"); + try (var connection = data.getConnection()) { + connection.createStatement().execute("CREATE TABLE ai_service_upgrade_plan(id VARCHAR(64) PRIMARY KEY,state VARCHAR(32)," + + "document CLOB,actor_id BIGINT,expires_at BIGINT)"); + } + var transactions = new DataSourceTransactionManager(data); AtomicInteger claims = new AtomicInteger(); + var store = new AiServiceUpgradePlanStore() { + @Override public boolean claim(Map plan) { + boolean claimed = super.claim(plan); if (claimed) claims.incrementAndGet(); return claimed; + } + }; + store.json = new JacksonJsonMapper(); + store.setConfiguration(new DefaultConfiguration() + .set(new DataSourceConnectionProvider(new TransactionAwareDataSourceProxy(data))).set(SQLDialect.H2)); + store.insert(f.document()); f.service.store = store; + var locks = new LockManagerImpl(); locks.setLockProvider(new InMemoryLockProvider()); f.service.locks = locks; + CountDownLatch bothPrefetched = new CountDownLatch(2); + var manager = new AiServiceUpgradePlanResourceManager() { + @Override protected Object getByIdInternal(String type, String id, ListOptions options) { + Object value = super.getByIdInternal(type, id, options); bothPrefetched.countDown(); await(bothPrefetched); return value; + } + }; + manager.service = f.service; var target = new DefaultReadWriteApiDelegate(); + target.setHandlers(List.of(new AbstractApiRequestHandler() { + @Override public void handle(ApiRequest action) { manager.resourceAction(AiServiceUpgradePlanResourceManager.TYPE, action); } + })); + try (var spring = advised(target, transactions)) { + var delegate = spring.getBean("writeDelegate", ReadWriteApiDelegate.class); var workers = Executors.newFixedThreadPool(2); + try { + java.util.concurrent.Callable apply = () -> { + var ownRequest = new ApiRequest(request); context(ownRequest, f.policy, new IdentityFormatter()); + try { delegate.write(ownRequest); return true; } finally { ApiContext.remove(); } + }; + var first = workers.submit(apply); var second = workers.submit(apply); + assertTrue(first.get(5, TimeUnit.SECONDS)); assertTrue(second.get(5, TimeUnit.SECONDS)); + assertEquals(1, claims.get()); assertEquals(1, f.dispatched); + assertEquals("accepted", store.load(f.id).get("status")); + } finally { workers.shutdownNow(); assertTrue(workers.awaitTermination(15, TimeUnit.SECONDS)); } + } + } + + private static void await(CountDownLatch latch) { + try { assertTrue("Upgrade lock ordering barrier", latch.await(5, TimeUnit.SECONDS)); } + catch (InterruptedException interrupted) { Thread.currentThread().interrupt(); throw new AssertionError(interrupted); } + } + private static void context(ApiRequest request, Policy actor, IdFormatter formatter) { + ApiContext.newContext().setApiRequest(request); ApiContext.getContext().setPolicy(actor); + ApiContext.getContext().setIdFormatter(formatter); + } + private static GenericApplicationContext advised(ReadWriteApiDelegate target, DataSourceTransactionManager transactions) throws Exception { + Path config = null; + for (Path at = Path.of("").toAbsolutePath(); at != null; at = at.getParent()) { + Path candidate = at.resolve("code/packaging/app-config/src/main/resources/META-INF/cattle/legacy/spring-legacy-context.xml"); + if (Files.isRegularFile(candidate)) { config = candidate; break; } + } + assertNotNull("Shipped API transaction advice", config); + String shipped = Files.readString(config); + int start = shipped.indexOf("", start) + "".length()); + start = shipped.indexOf("", start) + 2); + String xml = "" + + advice + "" + advisor + ""; + var spring = new GenericApplicationContext(); + spring.registerBean("CoreTransactionManager", DataSourceTransactionManager.class, () -> transactions); + spring.registerBean("writeDelegate", ReadWriteApiDelegate.class, () -> target); + new XmlBeanDefinitionReader(spring).loadBeanDefinitions(new ByteArrayResource(xml.getBytes(StandardCharsets.UTF_8))); + spring.refresh(); return spring; + } +} diff --git a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanServiceTest.java b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanServiceTest.java index 475d3ddf16..51c9f8cd98 100644 --- a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanServiceTest.java +++ b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanServiceTest.java @@ -23,6 +23,7 @@ import io.cattle.platform.lock.LockManager; import io.cattle.platform.object.ObjectManager; import io.cattle.platform.object.util.DataAccessor; +import io.cattle.platform.servicediscovery.api.service.ServiceUpgradeValidation; import io.github.ibuildthecloud.gdapi.exception.ClientVisibleException; import io.github.ibuildthecloud.gdapi.context.ApiContext; import io.github.ibuildthecloud.gdapi.id.IdentityFormatter; @@ -43,9 +44,215 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.nio.file.Files; +import java.nio.file.Path; import org.junit.Test; public class AiServiceUpgradePlanServiceTest { + @Test public void realPreviewPipelineRejectsUnknownAndCoercedStrategyWithoutPlanOrDispatch() throws Exception { + for (String extra : List.of("\"url\":\"https://invalid.invalid/\"", "\"launchConfig\":{}", "\"upgradeStrategy\":{}", + "\"startFirst\":\"true\"", "\"batchSize\":\"2\"", "\"intervalMillis\":\"1000\"", "\"batchSize\":2.5", + "\"intervalMillis\":100.5")) { + Fixture f = new Fixture(); + ApiRequest request = f.parsedPreview("{\"serviceId\":\"10\",\"image\":\"app:2\"," + extra + "}", Map.of()); + assertEquals(Boolean.TRUE, request.getAttribute(AiServiceUpgradePlanService.ORIGINAL_PREVIEW_REJECTED)); + new AiServiceUpgradePlanInputHandler().handle(request); // Sanitized re-entry cannot clear it. + var denied = assertThrows(ClientVisibleException.class, () -> f.service.preview(request)); + assertEquals(422, denied.getStatus()); assertEquals("UpgradeInputInvalid", denied.getCode()); + assertTrue(f.documents.isEmpty()); assertEquals(0, f.dispatched); + assertEquals(List.of("UpgradeInputInvalid"), f.deniedReasons); + verifyNoInteractions(f.service.upgradeValidation); + } + Fixture query = new Fixture(); + ApiRequest unknownQuery = query.parsedPreview("{\"serviceId\":\"10\",\"image\":\"app:2\"}", Map.of("url", new String[]{"DUMMY"})); + assertEquals("UpgradeInputInvalid", rejected(() -> query.service.preview(unknownQuery))); assertTrue(query.documents.isEmpty()); + for (String extra : List.of("", ",\"startFirst\":true,\"batchSize\":2,\"intervalMillis\":1000", ",\"batchSize\":9007199254740991")) { + Fixture valid = new Fixture(); + ApiRequest request = valid.parsedPreview("{\"serviceId\":\"10\",\"image\":\"app:2\"" + extra + "}", Map.of()); + assertEquals(Boolean.FALSE, request.getAttribute(AiServiceUpgradePlanService.ORIGINAL_PREVIEW_REJECTED)); + assertEquals("preview", valid.service.preview(request).get("status")); + assertEquals(1, valid.documents.size()); assertEquals(0, valid.dispatched); + } + ApiContext.remove(); + } + + @Test public void originalPreviewGuardLeavesOrdinaryUpgradeAndPlanActionsUntouchedAndRunsBeforeNativeValidation() throws Exception { + Fixture f = new Fixture(); var handler = new AiServiceUpgradePlanInputHandler(); + Map ordinary = Map.of("inServiceStrategy", Map.of("startFirst", "true", "batchSize", "2")); + ApiRequest nativeRequest = f.request(ordinary); nativeRequest.setMethod("POST"); nativeRequest.setType("service"); + nativeRequest.setId("10"); nativeRequest.setAction("upgrade"); handler.handle(nativeRequest); + assertNull(nativeRequest.getAttribute(AiServiceUpgradePlanService.ORIGINAL_PREVIEW_REJECTED)); + assertEquals(ordinary, nativeRequest.getRequestObject()); + for (String action : List.of("confirm","apply","cancel")) { + ApiRequest request = f.request(action, Map.of("startFirst", "true")); request.setMethod("POST"); request.setType("aiServiceUpgradePlan"); request.setId("aiplan-dummy"); + handler.handle(request); assertNull(request.getAttribute(AiServiceUpgradePlanService.ORIGINAL_PREVIEW_REJECTED)); + assertEquals(Map.of("startFirst", "true"), request.getRequestObject()); + } + String order = Files.readString(sourceRoot().resolve("code/packaging/app-config/src/main/resources/META-INF/cattle/iaas-api/defaults.properties")); + assertTrue(order.indexOf("BodyParserRequestHandler,") < order.indexOf("AiServiceUpgradePlanInputHandler,")); + assertTrue(order.indexOf("AiServiceUpgradePlanInputHandler,") < order.indexOf("ValidationHandler,")); + String beans = Files.readString(sourceRoot().resolve("code/packaging/app-config/src/main/java/io/cattle/platform/app/IaasApiConfig.java")); + assertTrue(beans.contains("return EMUtils.add(em, ApiRequestHandler.class, new AiServiceUpgradePlanInputHandler());")); + } + + private static Path sourceRoot() { + Path root = Path.of("").toAbsolutePath(); + while (root != null && !Files.isRegularFile(root.resolve("resources/content/schema/base/aiServiceUpgradePlan.json"))) root = root.getParent(); + assertNotNull(root); return root; + } + @Test public void omittedAndLegacyStrategiesKeepTheOriginalEffectiveDispatchDefaults() { + Fixture f = new Fixture(); + Map preview = f.preview(); + assertEquals(Map.of("startFirst", false, "batchSize", 1L, "intervalMillis", 2000L), preview.get("upgradeStrategy")); + f.document().remove("upgradeStrategy"); // A durable image-only plan written before367. + ((Map) f.document().get("snapshot")).remove("approvedUpgradeStrategy"); + assertEquals(preview.get("upgradeStrategy"), f.service.get(f.id, f.request(Map.of())).get("upgradeStrategy")); + f.action("confirm"); f.action("apply"); + Map strategy = (Map) ((Map) f.dispatchedRequest.getRequestObject()).get("inServiceStrategy"); + assertEquals(false, strategy.get("startFirst")); assertEquals(1L, strategy.get("batchSize")); + assertEquals(2000L, strategy.get("intervalMillis")); + } + + @Test public void newCustomAndDefaultPlansFenceLegacySnapshotBeforeClaim() { + for (Map input : List.of(Map.of("serviceId", "10", "image", "app:2"), + Map.of("serviceId", "10", "image", "app:2", "startFirst", true, "batchSize", 3, "intervalMillis", 1500))) { + Fixture f = new Fixture(); Map preview = f.preview(input); + f.service.store = spy(f.service.store); + Map persisted = f.service.map(f.document().get("snapshot")); + Map classic = f.service.snapshot(f.target); + assertFalse(classic.containsKey("approvedUpgradeStrategy")); + assertEquals(f.service.map(preview.get("upgradeStrategy")), persisted.get("approvedUpgradeStrategy")); + // This is the exact full-map comparison used in366 before claim or dispatch. + assertNotEquals(classic, persisted); + persisted.remove("approvedUpgradeStrategy"); assertEquals(classic, persisted); + assertEquals("approved", f.action("confirm").get("confirmationState")); + verify(f.service.store, never()).claim(anyMap()); assertEquals(0, f.dispatched); + } + } + + @Test public void legacySnapshotWithoutStrategyOrFenceStillConfirmsAndAppliesDefaults() { + Fixture f = new Fixture(); f.preview(Map.of("serviceId", "10", "image", "app:2", "startFirst", true, + "batchSize", 3, "intervalMillis", 1500)); + f.document().remove("upgradeStrategy"); + ((Map) f.document().get("snapshot")).remove("approvedUpgradeStrategy"); + assertEquals(f.service.snapshot(f.target), f.document().get("snapshot")); + f.service.store = spy(f.service.store); + Map defaults = Map.of("startFirst", false, "batchSize", 1L, "intervalMillis", 2000L); + assertEquals(defaults, f.action("confirm").get("upgradeStrategy")); + assertEquals(defaults, f.action("apply").get("upgradeStrategy")); + Map actual = (Map) ((Map) f.dispatchedRequest.getRequestObject()).get("inServiceStrategy"); + defaults.forEach((key, value) -> assertEquals(value, actual.get(key))); + verify(f.service.store, times(1)).claim(anyMap()); assertEquals(1, f.dispatched); + } + + @Test public void strategyAndSnapshotFenceDriftCannotClaimOrDispatch() { + for (String action : List.of("confirm", "apply")) { + for (String drift : List.of("strategy", "fence", "missing-fence", "missing-strategy")) { + Fixture f = new Fixture(); f.preview(Map.of("serviceId", "10", "image", "app:2", "startFirst", true, + "batchSize", 3, "intervalMillis", 1500)); + if ("apply".equals(action)) f.action("confirm"); + f.service.store = spy(f.service.store); + Map snapshot = (Map) f.document().get("snapshot"); + if ("strategy".equals(drift)) ((Map) f.document().get("upgradeStrategy")).put("batchSize", 4L); + if ("fence".equals(drift)) ((Map) snapshot.get("approvedUpgradeStrategy")).put("batchSize", 4L); + if ("missing-fence".equals(drift)) snapshot.remove("approvedUpgradeStrategy"); // A pre-fix367 plan must be previewed again. + if ("missing-strategy".equals(drift)) f.document().remove("upgradeStrategy"); + var denied = assertThrows(ClientVisibleException.class, () -> f.action(action)); + assertEquals(drift, 409, denied.getStatus()); assertEquals("UpgradePlanStale", denied.getCode()); + verify(f.service.store, never()).claim(anyMap()); assertEquals(0, f.dispatched); + assertEquals("preview", f.document().get("status")); assertNull(f.document().get("operationId")); + } + } + } + + @Test public void reloadedFencedStrategyRemainsImmutableAndDispatchesOnlyOnce() throws Exception { + try (var connection = java.sql.DriverManager.getConnection("jdbc:h2:mem:upgrade-strategy-fence;DATABASE_TO_LOWER=TRUE")) { + connection.createStatement().execute("CREATE TABLE ai_service_upgrade_plan(id VARCHAR(64) PRIMARY KEY, state VARCHAR(32)," + + " document CLOB, actor_id BIGINT, expires_at BIGINT)"); + Fixture original = new Fixture(); original.service.store = durableStore(connection); + Map expected = Map.of("startFirst", true, "batchSize", 3L, "intervalMillis", 1500L); + original.preview(Map.of("serviceId", "10", "image", "app:2", "startFirst", true, "batchSize", 3, "intervalMillis", 1500)); + original.action("confirm"); + Fixture restarted = new Fixture(); restarted.id = original.id; restarted.service.store = spy(durableStore(connection)); + Map restored = restarted.service.store.load(restarted.id); + Map persistedExpected = Map.of("startFirst", true, "batchSize", 3, "intervalMillis", 1500); + assertEquals(persistedExpected, restored.get("upgradeStrategy")); + assertEquals(persistedExpected, ((Map) restored.get("snapshot")).get("approvedUpgradeStrategy")); + ((Map) restored.get("upgradeStrategy")).put("batchSize", 99L); + assertEquals(persistedExpected, restarted.service.store.load(restarted.id).get("upgradeStrategy")); + assertEquals("accepted", restarted.action("apply").get("status")); + Fixture receipt = new Fixture(); receipt.id = original.id; receipt.service.store = spy(durableStore(connection)); + assertEquals(expected, receipt.action("apply").get("upgradeStrategy")); + original.action("apply"); restarted.action("apply"); + Map actual = (Map) ((Map) restarted.dispatchedRequest.getRequestObject()).get("inServiceStrategy"); + expected.forEach((key, value) -> assertEquals(value, actual.get(key))); + assertEquals(0, original.dispatched); assertEquals(1, restarted.dispatched); assertEquals(0, receipt.dispatched); + verify(restarted.service.store, times(1)).claim(anyMap()); verify(receipt.service.store, never()).claim(anyMap()); + } + } + + private static AiServiceUpgradePlanStore durableStore(java.sql.Connection connection) { + AiServiceUpgradePlanStore store = new AiServiceUpgradePlanStore(); store.json = new JacksonJsonMapper(); + store.setConfiguration(new org.jooq.impl.DefaultConfiguration().set(connection).set(org.jooq.SQLDialect.H2)); + return store; + } + + @Test public void approvedStrategyIsDetachedReviewedAndDispatchedOnceWithoutActionOverrides() { + Fixture f = new Fixture(); + Map input = new LinkedHashMap<>(Map.of("serviceId", "10", "image", "app:2", + "startFirst", true, "batchSize", 3, "intervalMillis", 1500)); + Map preview = f.preview(input); + Map expected = Map.of("startFirst", true, "batchSize", 3L, "intervalMillis", 1500L); + assertEquals(expected, preview.get("upgradeStrategy")); + input.put("batchSize", 99); + ((Map) preview.get("upgradeStrategy")).put("batchSize", 88); + assertEquals(expected, f.service.pending(f.request(Map.of())).get(0).get("upgradeStrategy")); + for (String action : List.of("confirm", "apply")) { + for (String field : List.of("startFirst", "batchSize", "intervalMillis", "upgradeStrategy")) { + assertEquals("UpgradeInputInvalid", rejected(() -> f.service.action(f.id, f.request(action, Map.of(field, true))))); + } + } + assertEquals(expected, f.action("confirm").get("upgradeStrategy")); + assertEquals(expected, f.action("apply").get("upgradeStrategy")); + f.action("apply"); assertEquals(1, f.dispatched); + Map actual = (Map) ((Map) f.dispatchedRequest.getRequestObject()).get("inServiceStrategy"); + expected.forEach((key, value) -> assertEquals(value, actual.get(key))); + verify(f.service.upgradeValidation, times(3)).validate(eq(f.target), any()); + } + + @Test public void strategyUsesNativeMinimaAndExactJsonIntegerBoundaryWithoutClamping() { + Fixture f = new Fixture(); + Map result = f.preview(Map.of("serviceId", "10", "image", "app:2", "startFirst", false, + "batchSize", 9007199254740991L, "intervalMillis", 100.0)); + assertEquals(Map.of("startFirst", false, "batchSize", 9007199254740991L, "intervalMillis", 100L), result.get("upgradeStrategy")); + f.action("confirm"); f.action("apply"); + Map strategy = (Map) ((Map) f.dispatchedRequest.getRequestObject()).get("inServiceStrategy"); + assertEquals(9007199254740991L, strategy.get("batchSize")); assertEquals(100L, strategy.get("intervalMillis")); + } + + @Test public void invalidOptionalStrategyRejectsBeforePlanOrDispatchAndNativePreflightIsRequired() { + for (String field : List.of("batchSize", "intervalMillis")) { + for (Object value : java.util.Arrays.asList(null, "2", true, 0, -1, 1.5, Double.NaN, Double.POSITIVE_INFINITY, + 9007199254740992L, Long.MAX_VALUE)) { + Fixture f = new Fixture(); var input = new LinkedHashMap(Map.of("serviceId", "10", "image", "app:2")); + input.put(field, value); + assertEquals(field + "=" + value, "UpgradeInputInvalid", rejected(() -> f.preview(input))); + assertTrue(f.documents.isEmpty()); assertEquals(0, f.dispatched); verifyNoInteractions(f.service.upgradeValidation); + } + } + for (Object value : java.util.Arrays.asList(null, "true", 1, Map.of())) { + Fixture f = new Fixture(); var input = new LinkedHashMap(Map.of("serviceId", "10", "image", "app:2")); + input.put("startFirst", value); assertEquals("UpgradeInputInvalid", rejected(() -> f.preview(input))); + assertTrue(f.documents.isEmpty()); + } + Fixture interval = new Fixture(); + assertEquals("UpgradeInputInvalid", rejected(() -> interval.preview(Map.of("serviceId", "10", "image", "app:2", "intervalMillis", 99)))); + Fixture raw = new Fixture(); + assertEquals("UpgradeInputInvalid", rejected(() -> raw.preview(Map.of("serviceId", "10", "image", "app:2", "inServiceStrategy", Map.of())))); + Fixture missing = new Fixture(); missing.service.upgradeValidation = null; + assertEquals("UpgradePreflightUnavailable", rejected(missing::preview)); assertTrue(missing.documents.isEmpty()); + } + @Test public void snapshotMapKeepsNestedValuesDetachedAndNullProducesMutableEmptyMap() { Fixture f = new Fixture(); Map environment = new LinkedHashMap<>(Map.of("TEXT", "\u4fdd\u7559", "EMPTY", "")); @@ -376,6 +583,7 @@ static class Fixture { final AccountRecord project = new AccountRecord(); final StackRecord stack = new StackRecord(); final List> observed = new ArrayList<>(); + final List deniedReasons = new ArrayList<>(); final Map> documents = new HashMap<>(); final AiServiceUpgradePlanService service; String id; int dispatched; int denied; boolean ready = true; boolean failDispatch; @@ -410,9 +618,10 @@ static class Fixture { super.dispatch(plan, target, request); } @Override protected void audit(ApiRequest request, String action, String id, Long target, boolean allowed, String reason) { if - (!allowed) denied++; } + (!allowed) { denied++; deniedReasons.add(reason); } } }; service.keyAuthorization = mock(AiKeyAuthorization.class); + service.upgradeValidation = mock(ServiceUpgradeValidation.class); service.json = new JacksonJsonMapper(); service.clock = Clock.fixed(Instant.parse("2026-10-29T00:00:00Z"), ZoneOffset.UTC); service.objects = mock(ObjectManager.class); when(service.objects.loadResource(eq(Service.class), anyString())).thenReturn(target); when(service.objects.loadResource(Account.class, 77L)).thenReturn(project); @@ -460,9 +669,25 @@ ApiRequest parsedAction(String action, String body) throws Exception { new ValidationHandler().generate(request); return request; } + ApiRequest parsedPreview(String body, Map params) throws Exception { + var mapper = new io.github.ibuildthecloud.gdapi.json.JacksonMapper(); mapper.init(); + var nativeSchema = mapper.readValue(Files.readAllBytes(sourceRoot().resolve("resources/content/schema/base/aiServiceUpgradePlan.json")), + io.github.ibuildthecloud.gdapi.model.impl.SchemaImpl.class); + nativeSchema.setId("aiServiceUpgradePlan"); when(schemas.getSchema("aiServiceUpgradePlan")).thenReturn(nativeSchema); + ApiContext.newContext().setIdFormatter(new IdentityFormatter()); + ApiRequest request = new ApiRequest(null,schemas) { + @Override public java.io.InputStream getInputStream() { return new java.io.ByteArrayInputStream(body.getBytes(java.nio.charset.StandardCharsets.UTF_8)); } + }; + request.setMethod("POST"); request.setType("aiServiceUpgradePlan"); request.setRequestParams(params); + var parser = new BodyParserRequestHandler(); parser.setJsonMarshaller(mapper); parser.init(); parser.handle(request); + new AiServiceUpgradePlanInputHandler().handle(request); + var validation = new ValidationHandler(); validation.init(); validation.generate(request); + return request; + } ApiRequest request(String action, Map body) { ApiRequest r = request(body); r.setAction(action); return r; } - Map preview() { Map result = service.preview(request(Map.of("serviceId", target.getId().toString(), - "image", "app:2"))); id = (String) result.get("id"); return result; } + Map preview() { return preview(Map.of("serviceId", target.getId().toString(), "image", "app:2")); } + Map preview(Map input) { Map result = service.preview(request(input)); + id = (String) result.get("id"); return result; } Map action(String name) { return service.action(id, request(name, Map.of())); } Map document() { return documents.get(id); } } diff --git a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanStoreTest.java b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanStoreTest.java index 0892045a02..0586c303d6 100644 --- a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanStoreTest.java +++ b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanStoreTest.java @@ -30,7 +30,8 @@ public class AiServiceUpgradePlanStoreTest { connection.createStatement().execute("CREATE TABLE ai_service_upgrade_plan(id VARCHAR(64) PRIMARY KEY, state VARCHAR(32), document CLOB, actor_id BIGINT, expires_at BIGINT)"); AiServiceUpgradePlanStore first = store(connection); Map plan = new LinkedHashMap<>(Map.of("id", "aiplan-11111111-1111-1111-1111-111111111111", "status", "preview", "actorId", 42, "expiresAt", "2026-10-29T00:05:00Z", - "snapshot", Map.of("environment", Map.of("PRIVATE", "retained")))); + "snapshot", Map.of("environment", Map.of("PRIVATE", "retained")), + "upgradeStrategy", Map.of("startFirst", true, "batchSize", 9007199254740991L, "intervalMillis", 100))); first.insert(plan); assertEquals(1, first.pendingCount(42)); AiServiceUpgradePlanStore restarted = store(connection); @@ -39,6 +40,7 @@ public class AiServiceUpgradePlanStoreTest { plan.put("status", "dispatching"); staleCopy.put("status", "dispatching"); assertTrue(restarted.claim(plan)); assertFalse(first.claim(staleCopy)); assertEquals("dispatching", first.load((String) plan.get("id")).get("status")); + assertEquals(plan.get("upgradeStrategy"), first.load((String) plan.get("id")).get("upgradeStrategy")); Map expired = new LinkedHashMap<>(plan); expired.put("id", "aiplan-22222222-2222-2222-2222-222222222222"); expired.put("status", "preview"); restarted.insert(expired); assertEquals(1, restarted.pendingCount(42)); diff --git a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradeStrategyPreflightTest.java b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradeStrategyPreflightTest.java new file mode 100644 index 0000000000..a92e05d9a1 --- /dev/null +++ b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradeStrategyPreflightTest.java @@ -0,0 +1,114 @@ +package io.cattle.platform.iaas.api.auth.upgrade; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import io.cattle.platform.core.addon.VolumePreflightInput; +import io.cattle.platform.core.model.tables.records.HostRecord; +import io.cattle.platform.iaas.api.port.PortPreflightDao; +import io.cattle.platform.iaas.api.port.PortPreflightService; +import io.cattle.platform.iaas.api.volume.VolumePreflightService; +import io.cattle.platform.object.util.DataAccessor; +import io.cattle.platform.servicediscovery.api.filter.ServiceUpgradeValidationFilter; +import io.github.ibuildthecloud.gdapi.exception.ClientVisibleException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.junit.Test; + +/** Actual native port inventory/parser and filter, with no dispatcher, socket or daemon. */ +public class AiServiceUpgradeStrategyPreflightTest { + @Test public void nativeRetainIpStartFirstInvariantRejectsBeforeAnyPlanOrResourceMutation() throws Exception { + Fixture f = new Fixture(); DataAccessor.setField(f.target, "retainIp", true); + var before = f.service.snapshot(f.target); + var denied = assertThrows(ClientVisibleException.class, () -> f.preview(true, 1)); + assertEquals(422, denied.getApiError().getStatus()); assertEquals("InvalidOption", denied.getApiError().getCode()); + assertTrue(f.documents.isEmpty()); assertEquals(0, f.dispatched); + assertEquals(List.of("InvalidOption"), f.deniedReasons); + assertEquals(before, f.service.snapshot(f.target)); assertTrue(f.volumes.isEmpty()); + verify(f.inventory, never()).getEligibleHosts(anyLong()); verify(f.inventory, never()).getPortOwners(anyLong()); + // Native retain-IP is compatible with the legacy stop-first strategy. + f.preview(false, 1); f.action("confirm"); f.action("apply"); assertEquals(1, f.dispatched); + } + + @Test public void realStartFirstPortCapacityRejectsAtPreviewConfirmationAndBeforeClaim() throws Exception { + Fixture f = new Fixture(); + var preview = assertThrows(ClientVisibleException.class, () -> f.preview(true, 2)); + assertEquals(422, preview.getApiError().getStatus()); assertEquals("PortConflict", preview.getApiError().getCode()); + assertTrue(f.documents.isEmpty()); assertEquals(0, f.dispatched); + + // One free host can fit batch1, but cannot fit the approved batch2. + f.preview(false, 2); f.action("cancel"); + f.preview(true, 1); + f.hosts(false); + var confirm = assertThrows(ClientVisibleException.class, () -> f.action("confirm")); + assertEquals(422, confirm.getApiError().getStatus()); assertEquals("PortConflict", confirm.getApiError().getCode()); + assertEquals("pending", f.document().get("confirmationState")); + f.hosts(true); f.action("confirm"); f.hosts(false); + var apply = assertThrows(ClientVisibleException.class, () -> f.action("apply")); + assertEquals(422, apply.getApiError().getStatus()); assertEquals("PortConflict", apply.getApiError().getCode()); + assertEquals("preview", f.document().get("status")); assertNull(f.document().get("operationId")); + assertEquals(0, f.dispatched); + assertEquals(List.of("PortConflict", "PortConflict", "PortConflict"), f.deniedReasons); + f.hosts(true); f.action("apply"); + assertEquals(1, f.dispatched); + assertEquals(List.of(false, true, true, true), f.volumes.stream().map(VolumePreflightInput::getStartFirst).toList()); + assertEquals(List.of(2, 1, 1, 1), f.volumes.stream().map(VolumePreflightInput::getBatchSize).toList()); + } + + @Test public void nativeVolumePreflightReceivesApprovedStartFirstAndNativeBatchNormalization() throws Exception { + Fixture f = new Fixture(); + DataAccessor.fieldMap(f.target, "launchConfig").put("ports", List.of()); + f.preview(true, 9007199254740991L); f.action("confirm"); f.action("apply"); + assertEquals(3, f.volumes.size()); + for (VolumePreflightInput input : f.volumes) { + assertEquals(Boolean.TRUE, input.getStartFirst()); assertEquals(Integer.valueOf(Integer.MAX_VALUE), input.getBatchSize()); + assertEquals(List.of("named:/data"), input.getDataVolumes()); + assertEquals(Long.valueOf(10), input.getServiceId()); assertEquals(Long.valueOf(3), input.getStackId()); + } + Map dispatched = (Map) ((Map) f.dispatchedRequest.getRequestObject()).get("inServiceStrategy"); + assertEquals(9007199254740991L, dispatched.get("batchSize")); + assertEquals(9007199254740991L, ((Number) ((Map) f.document().get("upgradeStrategy")).get("batchSize")).longValue()); + } + + private static class Fixture extends AiServiceUpgradePlanServiceTest.Fixture { + final PortPreflightDao inventory = mock(PortPreflightDao.class); + final List volumes = new ArrayList<>(); + Fixture() throws Exception { + DataAccessor.setField(target, "scale", 2); + DataAccessor.fieldMap(target, "launchConfig").put("networkMode", "bridge"); + when(service.objects.loadResource(io.cattle.platform.core.model.Service.class, 10L)).thenReturn(target); + hosts(true); + doReturn(List.of(owner(1), owner(2))).when(inventory).getPortOwners(77L); + var port = new PortPreflightService(); inject(port, "dao", inventory); inject(port, "objectManager", service.objects); + var volume = mock(VolumePreflightService.class); + doAnswer(call -> { volumes.add(call.getArgument(1)); return null; }).when(volume).assertAvailable(eq(project), any()); + var nativeFilter = new ServiceUpgradeValidationFilter(); inject(nativeFilter, "objectManager", service.objects); + inject(nativeFilter, "portPreflightService", port); inject(nativeFilter, "volumePreflightService", volume); + service.upgradeValidation = nativeFilter; + } + void hosts(boolean freeHost) { + var values = new ArrayList(); + for (long id = 1; id <= (freeHost ? 3 : 2); id++) { + var host = new HostRecord(); host.setId(id); host.setAccountId(77L); values.add(host); + } + when(inventory.getEligibleHosts(77L)).thenReturn(values); + } + Map preview(boolean startFirst, long batchSize) { + return preview(Map.of("serviceId", "10", "image", "app:2", "startFirst", startFirst, "batchSize", batchSize, "intervalMillis", 100)); + } + // PortOwner is intentionally package-private in the native inventory. + private static Object owner(long host) throws Exception { + Class type = Class.forName("io.cattle.platform.iaas.api.port.PortOwner"); + var constructor = type.getDeclaredConstructor(); constructor.setAccessible(true); Object owner = constructor.newInstance(); + for (var entry : Map.of("portId", 100L + host, "hostId", host, "serviceId", 10L, "instanceId", 200L + host, + "state", "running", "bindAddress", "0.0.0.0", "publicPort", 8080, "privatePort", 80, "protocol", "tcp").entrySet()) { + var field = type.getDeclaredField(entry.getKey()); field.setAccessible(true); field.set(owner, entry.getValue()); + } + return owner; + } + private static void inject(Object target, String field, Object value) throws Exception { + var property = target.getClass().getDeclaredField(field); property.setAccessible(true); property.set(target, value); + } + } +} diff --git a/code/iaas/bootstrap/pom.xml b/code/iaas/bootstrap/pom.xml index b71bb9724f..d5d1f69030 100644 --- a/code/iaas/bootstrap/pom.xml +++ b/code/iaas/bootstrap/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/iaas/config-item/api/pom.xml b/code/iaas/config-item/api/pom.xml index 9ee50b1f0e..80625775e7 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.366 + 0.183.367 ../../../parent/pom.xml diff --git a/code/iaas/config-item/common/pom.xml b/code/iaas/config-item/common/pom.xml index d56f3f6094..551e12de08 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.366 + 0.183.367 ../../../parent/pom.xml diff --git a/code/iaas/config-item/server/pom.xml b/code/iaas/config-item/server/pom.xml index 1cb2b0894c..6ace27b8e4 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.366 + 0.183.367 ../../../parent/pom.xml diff --git a/code/iaas/engine-jooq/pom.xml b/code/iaas/engine-jooq/pom.xml index e34f3c5d55..a0343bf350 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.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/iaas/events/pom.xml b/code/iaas/events/pom.xml index 527187149b..c6d0d579d9 100644 --- a/code/iaas/events/pom.xml +++ b/code/iaas/events/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/iaas/external-handler/pom.xml b/code/iaas/external-handler/pom.xml index f40f7551b2..4629fdf7b7 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.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/iaas/ha/pom.xml b/code/iaas/ha/pom.xml index 9fd6c4fcbc..1e23881a7e 100644 --- a/code/iaas/ha/pom.xml +++ b/code/iaas/ha/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/iaas/healthcheck/pom.xml b/code/iaas/healthcheck/pom.xml index 184e59e237..7abe247661 100644 --- a/code/iaas/healthcheck/pom.xml +++ b/code/iaas/healthcheck/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/iaas/labels/pom.xml b/code/iaas/labels/pom.xml index 6a6d72feda..2d9c224a64 100644 --- a/code/iaas/labels/pom.xml +++ b/code/iaas/labels/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/iaas/logic-common/pom.xml b/code/iaas/logic-common/pom.xml index 6c987e79bd..48f1a0a8e5 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.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/iaas/logic/pom.xml b/code/iaas/logic/pom.xml index 7274f69796..e297052aca 100644 --- a/code/iaas/logic/pom.xml +++ b/code/iaas/logic/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/iaas/metadata/pom.xml b/code/iaas/metadata/pom.xml index db3df2f448..e6497647dd 100644 --- a/code/iaas/metadata/pom.xml +++ b/code/iaas/metadata/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/iaas/model/pom.xml b/code/iaas/model/pom.xml index 4f10cfe668..33ce6eb7aa 100644 --- a/code/iaas/model/pom.xml +++ b/code/iaas/model/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/iaas/resource-pool/pom.xml b/code/iaas/resource-pool/pom.xml index 1a63af9992..294e6cef22 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.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/iaas/service-discovery/api/pom.xml b/code/iaas/service-discovery/api/pom.xml index 3d75f86672..1b52485d03 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.366 + 0.183.367 ../../../parent/pom.xml diff --git a/code/iaas/service-discovery/api/src/main/java/io/cattle/platform/servicediscovery/api/filter/ServiceUpgradeValidationFilter.java b/code/iaas/service-discovery/api/src/main/java/io/cattle/platform/servicediscovery/api/filter/ServiceUpgradeValidationFilter.java index 1e5010ff39..883bc262b6 100644 --- a/code/iaas/service-discovery/api/src/main/java/io/cattle/platform/servicediscovery/api/filter/ServiceUpgradeValidationFilter.java +++ b/code/iaas/service-discovery/api/src/main/java/io/cattle/platform/servicediscovery/api/filter/ServiceUpgradeValidationFilter.java @@ -19,6 +19,7 @@ import io.cattle.platform.object.util.DataAccessor; import io.cattle.platform.process.common.util.ProcessUtils; import io.cattle.platform.servicediscovery.api.util.ServiceDiscoveryUtil; +import io.cattle.platform.servicediscovery.api.service.ServiceUpgradeValidation; import io.cattle.platform.storage.api.filter.ExternalTemplateInstanceFilter; import io.cattle.platform.storage.service.StorageService; import io.cattle.platform.util.type.CollectionUtils; @@ -40,7 +41,7 @@ import org.apache.commons.lang3.tuple.Pair; @Named -public class ServiceUpgradeValidationFilter extends AbstractDefaultResourceManagerFilter { +public class ServiceUpgradeValidationFilter extends AbstractDefaultResourceManagerFilter implements ServiceUpgradeValidation { @Inject ObjectManager objectManager; @@ -107,9 +108,7 @@ protected void processInServiceUpgradeStrategy(ApiRequest request, Service servi } } - assertUpgradePortsAvailable(service, inServiceStrategy); - - assertUpgradeVolumesAvailable(service, inServiceStrategy); + validate(service, inServiceStrategy); Object launchConfig = DataAccessor.field(service, ServiceConstants.FIELD_LAUNCH_CONFIG, Object.class); @@ -144,6 +143,13 @@ protected void processInServiceUpgradeStrategy(ApiRequest request, Service servi } } + @Override + public void validate(Service service, InServiceUpgradeStrategy strategy) { + assertUpgradeStrategyAllowed(service, strategy); + assertUpgradePortsAvailable(service, strategy); + assertUpgradeVolumesAvailable(service, strategy); + } + protected void assertUpgradePortsAvailable(Service service, InServiceUpgradeStrategy strategy) { Integer scale = DataAccessor.fieldInteger(service, ServiceConstants.FIELD_SCALE); PortPreflightInput input = PortPreflightInputs.fromService(service, strategy.getLaunchConfig(), @@ -212,7 +218,7 @@ static Map launchConfigMap(Object launchConfigObj) { return CollectionUtils.toMap(launchConfigObj); } - protected InServiceUpgradeStrategy finalizeUpgradeStrategy(Service service, InServiceUpgradeStrategy strategy) { + protected void assertUpgradeStrategyAllowed(Service service, InServiceUpgradeStrategy strategy) { if (strategy.getLaunchConfig() == null && strategy.getSecondaryLaunchConfigs() == null) { ValidationErrorCodes.throwValidationError(ValidationErrorCodes.INVALID_OPTION, "LaunchConfig/secondaryLaunchConfigs need to be specified for inService strategy"); @@ -231,6 +237,12 @@ protected InServiceUpgradeStrategy finalizeUpgradeStrategy(Service service, InSe ValidationErrorCodes.throwValidationError(ValidationErrorCodes.INVALID_OPTION, "LaunchConfig is required for load balancer service"); } + } + } + + protected InServiceUpgradeStrategy finalizeUpgradeStrategy(Service service, InServiceUpgradeStrategy strategy) { + assertUpgradeStrategyAllowed(service, strategy); + if (service.getKind().equalsIgnoreCase(ServiceConstants.KIND_LOAD_BALANCER_SERVICE)) { ServiceDiscoveryUtil.injectBalancerLabelsAndHealthcheck(launchConfigMap(strategy.getLaunchConfig())); } diff --git a/code/iaas/service-discovery/api/src/main/java/io/cattle/platform/servicediscovery/api/service/ServiceUpgradeValidation.java b/code/iaas/service-discovery/api/src/main/java/io/cattle/platform/servicediscovery/api/service/ServiceUpgradeValidation.java new file mode 100644 index 0000000000..36a580b652 --- /dev/null +++ b/code/iaas/service-discovery/api/src/main/java/io/cattle/platform/servicediscovery/api/service/ServiceUpgradeValidation.java @@ -0,0 +1,9 @@ +package io.cattle.platform.servicediscovery.api.service; + +import io.cattle.platform.core.addon.InServiceUpgradeStrategy; +import io.cattle.platform.core.model.Service; + +/** Read-only native strategy invariants and port/volume preflight for an authorized upgrade target. */ +public interface ServiceUpgradeValidation { + void validate(Service service, InServiceUpgradeStrategy strategy); +} diff --git a/code/iaas/service-discovery/server/pom.xml b/code/iaas/service-discovery/server/pom.xml index 74646202fa..63b4533e9f 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.366 + 0.183.367 ../../../parent/pom.xml diff --git a/code/iaas/ssh-common/pom.xml b/code/iaas/ssh-common/pom.xml index d1747be0cc..dc76fc891c 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.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/iaas/storage-service/pom.xml b/code/iaas/storage-service/pom.xml index 42da5524ea..ce0698b36c 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.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/iaas/task-jooq/pom.xml b/code/iaas/task-jooq/pom.xml index 0986ad7465..032973137f 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.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/implementation/activity-log/pom.xml b/code/implementation/activity-log/pom.xml index f87fc08300..d14944ab7e 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.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/implementation/agent-instance-impl/pom.xml b/code/implementation/agent-instance-impl/pom.xml index a8ea291f23..6840c0d920 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.366 + 0.183.367 ../../parent/pom.xml cattle-agent-instance-impl diff --git a/code/implementation/docker/api/pom.xml b/code/implementation/docker/api/pom.xml index 1a1b5fd886..c8801611e3 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.366 + 0.183.367 ../../../parent/pom.xml diff --git a/code/implementation/docker/common/pom.xml b/code/implementation/docker/common/pom.xml index 00beb670ad..d21a38515b 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.366 + 0.183.367 ../../../parent/pom.xml diff --git a/code/implementation/docker/compute/pom.xml b/code/implementation/docker/compute/pom.xml index 5b184843b3..c1546b04df 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.366 + 0.183.367 ../../../parent/pom.xml diff --git a/code/implementation/docker/machine/pom.xml b/code/implementation/docker/machine/pom.xml index 1a6ffbfce9..758fd4b6b3 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.366 + 0.183.367 ../../../parent/pom.xml diff --git a/code/implementation/docker/storage/pom.xml b/code/implementation/docker/storage/pom.xml index 84d675a4c3..54116297d4 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.366 + 0.183.367 ../../../parent/pom.xml @@ -21,12 +21,12 @@ io.cattle cattle-docker-common - 0.183.366 + 0.183.367 io.cattle cattle-iaas-allocator - 0.183.366 + 0.183.367 diff --git a/code/implementation/extension-api/pom.xml b/code/implementation/extension-api/pom.xml index 75a95aa50b..7c36259a50 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.366 + 0.183.367 ../../parent/pom.xml cattle-extension-api diff --git a/code/implementation/hazelcast/common/pom.xml b/code/implementation/hazelcast/common/pom.xml index 259d0ea9af..6d1175d2c9 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.366 + 0.183.367 ../../../parent/pom.xml diff --git a/code/implementation/hazelcast/eventing/pom.xml b/code/implementation/hazelcast/eventing/pom.xml index a0da48f1d6..1ae1d4a2c1 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.366 + 0.183.367 ../../../parent/pom.xml diff --git a/code/implementation/hazelcast/lock/pom.xml b/code/implementation/hazelcast/lock/pom.xml index 1e8c1e169d..593baf8fbb 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.366 + 0.183.367 ../../../parent/pom.xml diff --git a/code/implementation/host-api/pom.xml b/code/implementation/host-api/pom.xml index ac68db6433..e79478434c 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.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/implementation/host-stats/pom.xml b/code/implementation/host-stats/pom.xml index 2b490e0441..e57d2ca913 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.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/implementation/register/pom.xml b/code/implementation/register/pom.xml index 3bc63b5967..281e029a58 100644 --- a/code/implementation/register/pom.xml +++ b/code/implementation/register/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/implementation/sample-setup/pom.xml b/code/implementation/sample-setup/pom.xml index bd5b4b28df..0fcb20a13c 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.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/implementation/settings-api/pom.xml b/code/implementation/settings-api/pom.xml index 1285c31c1b..2f6f355249 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.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/implementation/simulator/agent-connection/pom.xml b/code/implementation/simulator/agent-connection/pom.xml index 9e1818bdb3..9b5c61ac8f 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.366 + 0.183.367 ../../../parent/pom.xml diff --git a/code/implementation/simulator/storage/pom.xml b/code/implementation/simulator/storage/pom.xml index a1d3ab02c8..99d00c8ba6 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.366 + 0.183.367 ../../../parent/pom.xml diff --git a/code/implementation/system-stack/pom.xml b/code/implementation/system-stack/pom.xml index 8267914723..854847618e 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.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/implementation/vm/pom.xml b/code/implementation/vm/pom.xml index 2c911fe7c6..c18f1b08e2 100644 --- a/code/implementation/vm/pom.xml +++ b/code/implementation/vm/pom.xml @@ -3,7 +3,7 @@ cattle-parent io.cattle - 0.183.366 + 0.183.367 ../../parent/pom.xml cattle-vm diff --git a/code/meta-parent/pom.xml b/code/meta-parent/pom.xml index 7efd519e8f..2cd1dd150b 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.366 + 0.183.367 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 4b2a15d504..fec3f952a0 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.366 + 0.183.367 ../../parent/pom.xml jar diff --git a/code/packaging/app-config/src/main/java/io/cattle/platform/app/IaasApiConfig.java b/code/packaging/app-config/src/main/java/io/cattle/platform/app/IaasApiConfig.java index 8441751949..880f58cfc8 100644 --- a/code/packaging/app-config/src/main/java/io/cattle/platform/app/IaasApiConfig.java +++ b/code/packaging/app-config/src/main/java/io/cattle/platform/app/IaasApiConfig.java @@ -67,6 +67,8 @@ import io.cattle.platform.iaas.api.auth.ai.audit.AiAuditQueryService; import io.cattle.platform.iaas.api.auth.ai.audit.AiAuditQueryRunner; import io.cattle.platform.iaas.api.auth.ai.audit.AiAuditQueryResourceManager; +import io.cattle.platform.iaas.api.auth.ai.audit.AiAuditQueryInputHandler; +import io.cattle.platform.iaas.api.auth.upgrade.AiServiceUpgradePlanInputHandler; import io.cattle.platform.iaas.api.auth.ai.view.AiAuditSourcePageInput; import io.cattle.platform.iaas.api.auth.ai.catalog.AiCatalogMetadataView; import io.cattle.platform.iaas.api.auth.ai.catalog.AiCatalogLaunchAccess; @@ -648,6 +650,16 @@ ApiKeyPolicyInputHandler ApiKeyPolicyInputHandler(ExtensionManagerImpl em) { return EMUtils.add(em, ApiRequestHandler.class, new ApiKeyPolicyInputHandler()); } + @Bean + AiAuditQueryInputHandler AiAuditQueryInputHandler(ExtensionManagerImpl em) { + return EMUtils.add(em, ApiRequestHandler.class, new AiAuditQueryInputHandler()); + } + + @Bean + AiServiceUpgradePlanInputHandler AiServiceUpgradePlanInputHandler(ExtensionManagerImpl em) { + return EMUtils.add(em, ApiRequestHandler.class, new AiServiceUpgradePlanInputHandler()); + } + @Bean ValidationHandler ValidationHandler(ExtensionManagerImpl em, ResourceManagerReferenceValidator validator) { ValidationHandler handler = EMUtils.add(em, ApiRequestHandler.class, new ValidationHandler()); diff --git a/code/packaging/app-config/src/main/resources/META-INF/cattle/iaas-api/defaults.properties b/code/packaging/app-config/src/main/resources/META-INF/cattle/iaas-api/defaults.properties index 06a0b39166..a905be73aa 100644 --- a/code/packaging/app-config/src/main/resources/META-INF/cattle/iaas-api/defaults.properties +++ b/code/packaging/app-config/src/main/resources/META-INF/cattle/iaas-api/defaults.properties @@ -24,6 +24,8 @@ api.request.handler.list=\ VersionHandler,\ SchemasHandler,\ ApiKeyPolicyInputHandler,\ + AiAuditQueryInputHandler,\ + AiServiceUpgradePlanInputHandler,\ ValidationHandler,\ TransformationHandler, \ ResourceManagerReferenceValidator,\ diff --git a/code/packaging/app/pom.xml b/code/packaging/app/pom.xml index 96bf47b679..77c55bb429 100644 --- a/code/packaging/app/pom.xml +++ b/code/packaging/app/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.366 + 0.183.367 ../../parent/pom.xml war diff --git a/code/packaging/bundle/pom.xml b/code/packaging/bundle/pom.xml index b64e459aa2..0ef6eba566 100644 --- a/code/packaging/bundle/pom.xml +++ b/code/packaging/bundle/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/packaging/dev/pom.xml b/code/packaging/dev/pom.xml index cfc8924f36..64b40f1f5e 100644 --- a/code/packaging/dev/pom.xml +++ b/code/packaging/dev/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/packaging/meta/pom.xml b/code/packaging/meta/pom.xml index c35840114a..b260286b0e 100644 --- a/code/packaging/meta/pom.xml +++ b/code/packaging/meta/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.366 + 0.183.367 ../../parent/pom.xml diff --git a/code/parent/pom.xml b/code/parent/pom.xml index e8d3f4f92d..0f4950ec4c 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.366 + 0.183.367 pom diff --git a/docs/releases/orchestration-engine-0.183.367.md b/docs/releases/orchestration-engine-0.183.367.md new file mode 100644 index 0000000000..c64f4cc750 --- /dev/null +++ b/docs/releases/orchestration-engine-0.183.367.md @@ -0,0 +1,72 @@ +# Engine 0.183.367 source candidate + +This source candidate extends the existing human-reviewed Service image upgrade. +It is not a published release or a runtime acceptance result. + +The build environments also consume Ubuntu's official PCRE2 update +`10.46-1ubuntu0.1` for `libpcre2-8-0` and `libpcre2-posix3`, covering the eight +issues in [USN-8974-1](https://ubuntu.com/security/notices/USN-8974-1). +Both recipes verify archive SHA256, package identity, architecture and installed +version. Other package locks and snapshot dates are retained. + +Typed Audit queries also reject original unknown fields, including caller URL, +method and actor pointers, before they can disappear during ordinary schema +sanitization. A scoped request handler retains only a rejection boolean; the +existing typed owner returns422 and records canonical ERROR with no query-runner +execution. It does not retain the raw payload or change generic API filtering. + +`POST aiServiceUpgradePlans` retains `serviceId` and `image` and adds optional +top-level `startFirst` (boolean), `batchSize` (integer, minimum 1) and +`intervalMillis` (integer, minimum 100ms). Defaults remain false, 1 and 2000. +Integers must be exactly representable in JSON clients, at most 9007199254740991; +the native Long fields have no additional product maximum. Raw strategies, +launch configurations and client-supplied approval remain outside this input. +The exact typed preview handler checks the original field set and these three +strategy values before ordinary schema coercion. It retains only a sticky +rejection boolean; unknown fields, numeric strings and fractional integers cannot +silently disappear or become different approved values. The existing preview +owner records its original422 denial before any plan or dispatch. Ordinary native +Service upgrades and plan confirm/apply actions retain their existing behavior. + +The server persists and returns a closed `upgradeStrategy` containing all three +effective values. Human review, confirm, apply and operation reads refer to the +same immutable plan. Confirm/apply cannot override those values. Old persisted +plans without this field retain their original defaults. + +New plans also bind the complete strategy into a private snapshot operand. Older +Engines compare their classic snapshot and reject the additional operand with +`UpgradePlanStale` before claiming or dispatching. Newer Engines continue to accept +genuine legacy image-only plans. A pending plan created by a newer Engine must be +previewed and approved again after a downgrade; it cannot silently use older defaults. + +Preview, confirmation and approved apply invoke the same native upgrade filter's +read-only strategy invariants and port/volume preflight with the chosen start-first +and batch values. In particular, retain-IP Services reject start-first before any +plan is inserted or operation claimed, with the original native validation error. +The native preflight's existing batch normalization remains unchanged. Final +dispatch still uses the authorized native manager and the full preserved +primary/sidekick configuration; no new mutation grant, role bypass or Key +revision exception is added. A rejected preflight cannot claim or dispatch an +operation. An accepted or uncertain operation is not dispatched again. + +Catalog-launch, resource-change and upgrade actions also correct a shared lock +ordering fault in native action operand lookup. Lookup runs inside the API write +transaction: Catalog's locking plan read, or resource-change/upgrade receipt +observation writes, could retain a database row lock after releasing the plan's +application lock. A concurrent receipt GET could then hold the application lock +while waiting for that row, leaving the action waiting for the application lock. + +Only the exact framework-owned action marker selects an authorized, non-mutating +operand snapshot. Public GETs retain their normal source checks and receipt +observation. The action reloads its plan under the existing application lock and +repeats current owner, role, Key revision, source/snapshot and durable claim checks +before dispatch. Catalog operand snapshots also avoid retaining credential row +locks; final action authorization keeps its existing locking Key recheck. No +timeout, retry, removed lock, database change or new mutation grant is introduced. +Offline barrier tests use the native action path, shipped API transaction advice, +real application locks and JDBC row locking. They do not establish live UI polling +causality or matching Server/native runtime acceptance. + +Local focused tests, official component CI/artifacts, publication and matching +Server/native role/UI/weak-model acceptance are separate qualification stages. +No runtime or complete frontend coverage is claimed by this source change. diff --git a/pom.xml b/pom.xml index 1d90edc768..bd60776612 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ io.cattle cattle-parent - 0.183.366 + 0.183.367 code/parent/pom.xml cattle diff --git a/resources/content/schema/admin/admin-auth.json b/resources/content/schema/admin/admin-auth.json index 2d2dd8d6c0..dabbbc7377 100644 --- a/resources/content/schema/admin/admin-auth.json +++ b/resources/content/schema/admin/admin-auth.json @@ -115,6 +115,10 @@ "aiServiceUpgradePlan": "cr", "aiServiceUpgradePlan.serviceId": "cr", "aiServiceUpgradePlan.image": "cr", + "aiServiceUpgradePlan.startFirst" : "cr", + "aiServiceUpgradePlan.batchSize" : "cr", + "aiServiceUpgradePlan.intervalMillis" : "cr", + "aiServiceUpgradePlan.upgradeStrategy" : "r", "aiServiceUpgradePlan.planRef": "r", "aiServiceUpgradePlan.accountId": "r", "aiServiceUpgradePlan.serviceName": "r", diff --git a/resources/content/schema/base/aiServiceUpgradePlan.json b/resources/content/schema/base/aiServiceUpgradePlan.json index f5d36447f4..5e419f5447 100644 --- a/resources/content/schema/base/aiServiceUpgradePlan.json +++ b/resources/content/schema/base/aiServiceUpgradePlan.json @@ -10,12 +10,16 @@ "resourceFields": { "serviceId": { "type": "reference[service]", "create": true, "required": true }, "image": { "type": "string", "create": true, "required": true, "minLength": 1, "maxLength": 512 }, + "startFirst": { "type": "boolean", "create": true, "update": false, "nullable": false }, + "batchSize": { "type": "int", "create": true, "update": false, "nullable": false, "min": 1, "max": 9007199254740991 }, + "intervalMillis": { "type": "int", "create": true, "update": false, "nullable": false, "min": 100, "max": 9007199254740991 }, "planRef": { "type": "string", "create": false, "update": false }, "accountId": { "type": "reference[account]", "create": false, "update": false }, "serviceName": { "type": "string", "create": false, "update": false }, "displayPath": { "type": "string", "create": false, "update": false }, "currentImage": { "type": "string", "create": false, "update": false }, "requestedImage": { "type": "string", "create": false, "update": false }, + "upgradeStrategy": { "type": "json", "create": false, "update": false }, "expiresAt": { "type": "date", "create": false, "update": false }, "confirmationState": { "type": "string", "create": false, "update": false }, "status": { "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 ba690ea3fa..017b55df02 100644 --- a/resources/content/schema/project/project-auth.json +++ b/resources/content/schema/project/project-auth.json @@ -115,6 +115,10 @@ "aiServiceUpgradePlan": "cr", "aiServiceUpgradePlan.serviceId": "cr", "aiServiceUpgradePlan.image": "cr", + "aiServiceUpgradePlan.startFirst" : "cr", + "aiServiceUpgradePlan.batchSize" : "cr", + "aiServiceUpgradePlan.intervalMillis" : "cr", + "aiServiceUpgradePlan.upgradeStrategy" : "r", "aiServiceUpgradePlan.planRef": "r", "aiServiceUpgradePlan.accountId": "r", "aiServiceUpgradePlan.serviceName": "r", diff --git a/resources/content/schema/user/user-auth.json b/resources/content/schema/user/user-auth.json index 82f2cc83de..b6c78e9188 100644 --- a/resources/content/schema/user/user-auth.json +++ b/resources/content/schema/user/user-auth.json @@ -115,6 +115,10 @@ "aiServiceUpgradePlan": "cr", "aiServiceUpgradePlan.serviceId": "cr", "aiServiceUpgradePlan.image": "cr", + "aiServiceUpgradePlan.startFirst" : "cr", + "aiServiceUpgradePlan.batchSize" : "cr", + "aiServiceUpgradePlan.intervalMillis" : "cr", + "aiServiceUpgradePlan.upgradeStrategy" : "r", "aiServiceUpgradePlan.planRef": "r", "aiServiceUpgradePlan.accountId": "r", "aiServiceUpgradePlan.serviceName": "r", diff --git a/resources/pom.xml b/resources/pom.xml index 49a41b9f26..fa4dbb44ec 100644 --- a/resources/pom.xml +++ b/resources/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.366 + 0.183.367 ../code/parent/pom.xml diff --git a/scripts/api-key-operation-contract.json b/scripts/api-key-operation-contract.json index 23c96a856e..01fd86340f 100644 --- a/scripts/api-key-operation-contract.json +++ b/scripts/api-key-operation-contract.json @@ -9,12 +9,13 @@ "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": 441, - "sha256": "7ce17c8b05ccfba961414808148d8638bf54a9ad3bbc7de00facfcba4b26ac39", + "count": 447, + "sha256": "66a5ce2ee5f4277993c03de0f72a08dd828ec65e4c447430248c1b093583ab59", "anchors": [ "AbstractSchemaFactory.java", "AiAliasServiceChange.java", "AiAuditQuery.java", + "AiAuditQueryInputHandler.java", "AiAuditQueryResourceManager.java", "AiAuditQueryRunner.java", "AiAuditQueryService.java", @@ -58,6 +59,8 @@ "AiResourceViewResourceManager.java", "AiResourceViewService.java", "AiServiceCreateChange.java", + "AiServiceUpgradePlanInputHandler.java", + "AiServiceUpgradePlanService.java", "ApiAuditPayload.java", "ApiAuthenticator.java", "ApiCookieAuthentication.java", @@ -83,6 +86,7 @@ "FileSchemaFactory.java", "GenericWhitelistedProxy.java", "IaasApiConfig.java", + "InServiceUpgradeStrategy.java", "NotWritablePostProcessor.java", "PortBindingAddress.java", "PortPreflightInputs.java", @@ -97,9 +101,14 @@ "ServiceCreate.java", "ServiceCreateValidation.java", "ServiceDiscoveryServiceImpl.java", + "ServiceUpgradeStrategy.java", + "ServiceUpgradeValidation.java", + "ServiceUpgradeValidationFilter.java", "ServiceValidationFilter.java", "TokenAuthLookup.java", - "TransformInspect.java" + "TransformInspect.java", + "VolumePreflightInputs.java", + "VolumePreflightService.java" ], "markers": [ "@Type", @@ -242,98 +251,168 @@ "tupleCount": 1396, "tupleSha256": "8f1c0fb378663b0b3d272efba2f211cb210eb6f17cb8035aefa33b56feeaf36a", "extensionReview": { - "version": "unreleased-366-service-create-native-ports-healthcheck-and-catalog-projection", + "version": "unreleased-367-service-upgrade-strategy-audit-preview-native-action-lock-order-and-rollback-fence", "status": "SOURCE_ONLY_NOT_RUNTIME_ACCEPTANCE", - "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" - }, + "scope": "Complete published366/current367 production schema-handler-process unions were independently re-extracted. All1396 HTTP tuples, operation semantics and admission dispositions remain exact. Existing Service upgrade plans gain three bounded optional strategy inputs and a closed read-only effective strategy, with repeated native port/volume preflight and unchanged current role/Key/revision/claim checks. The typed preview checks its original closed field set and original three strategy values before native coercion, retaining only a sticky rejection boolean with unchanged serviceId/image and action semantics. The typed Audit owner preserves a boolean closed-input rejection before ordinary schema sanitization and records its existing canonical ERROR. No generic schema filtering, new public operation or global mutation grant. Native Catalog-launch, resource-change and upgrade action operands use only exact framework request markers and authorized read snapshots. No prefetch observation writes or retained plan/credential row locks precede the action application lock. Ordinary GET observation and final action owner, role, Key revision, source/snapshot, CAS and one-claim checks remain; no timeout, retry, schema or grant expansion. New upgrade plans bind the complete strategy in their private target snapshot so older engines reject them before claim; genuine legacy image-only plans keep classic snapshots and original defaults.", "previous": { - "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", "", @@ -341,15 +420,15 @@ "read" ], [ - "aiServiceUpgradePlan", + "aiResourceChangePlan", "collection", "POST", "", "", - "upgrade" + "update" ], [ - "aiServiceUpgradePlan", + "aiResourceChangePlan", "resource", "GET", "", @@ -357,37 +436,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": [ @@ -395,161 +571,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" - }, - { - "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" + "before": "b31333ffc5de2ec8fbee9d38daacd83b91578525d1fe1b61a73abac6355aa009", + "after": "fcd8d3724c1a37b2b77cab3e60ee3e973b349462833c1bf0bcc8f099d6f3567c" }, { - "path": "resources/content/schema/base/aiResourceChangePlan.json", + "path": "resources/content/schema/base/processPool.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" } ] }, @@ -564,18 +603,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" } ] }, @@ -585,19 +620,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" } ] }, @@ -611,315 +946,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" - }, - { - "path": "resources/content/schema/base/aiCatalogMetadataView.json", - "before": null, - "after": "5581897314b1cfe9135783fb8846db50ff6f9d62289b5ab35e60ab7ca8b36515" + "before": "6ea49e1130a2b9218e6c060038c73c89be5b9e05b039d3a0b81475e892d21504", + "after": "27630fdb4d5cf7a1dcd7e693b84d0b31513d627a64a5d2365119075aca6d4f85" }, { - "path": "resources/content/schema/base/aiCatalogStackLaunchInputs.json", + "path": "resources/content/schema/base/aiAuditQuery.json", "before": null, - "after": "05aa9a7b459a82efcf0331b3d6c8262e1b5c879643aef6384c67e3a12980bf4c" + "after": "c12e694997a08c3e9849413f45b96d7d9cf2471289d35be54a60903d22e609eb" }, { - "path": "resources/content/schema/base/aiCatalogStackLaunchOptions.json", + "path": "resources/content/schema/base/aiAuditSourcePageInput.json", "before": null, - "after": "30137b75ac50b6b04adec688ca56359fd7d49843f8342c423a906b998da5c12a" + "after": "2aca5cba9323daae33a7171ac5f3bec2591cbdbffbdc32cf263ac8398a75e444" }, { - "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" } ] }, @@ -937,225 +1186,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" - }, - { - "path": "code/framework/engine/src/main/java/io/cattle/platform/engine/process/ProcessAuthorization.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" - }, - { - "path": "code/framework/java-server/src/main/java/io/github/ibuildthecloud/gdapi/request/resource/impl/FilteredResourceManager.java", - "before": "a140040f0be95fd7872eb7f7e78f5824329725a109d7bc0c79d960dbc0709e77", - "after": "d2c6a28809df4912b2b22ce22e9f4e48304b91c993144867d05fa5d65f07ae76" + "before": "a10a5d8e7ca754601ea2c5a876c318069b6639c073e616d283ae71694bc95179", + "after": "cb6bb0a8ed3dda48bedd3ae89ed78a490f644c1040c5aa16463e1e701250988e" }, { - "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/context/EngineContext.java", "before": null, - "after": "c42a2b1aea75d4daae5671c702e3242c9cd1e7d32e7a5280fc3a0f6415ca829a" + "after": "ba91b4da42d1f2ea5a2b9709818c2a8f4b967c2c730d5681b53d9dbe94ce8968" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQuery.java", + "path": "code/framework/engine/src/main/java/io/cattle/platform/engine/process/impl/DefaultProcessInstanceImpl.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" } ] }, @@ -1176,57 +1260,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" } ] }, @@ -1247,25 +1299,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" } ] }, @@ -1286,15 +1328,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" } ] }, @@ -1315,25 +1367,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" } ] }, @@ -1354,31 +1412,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" } ] }, @@ -1399,17 +1441,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" }, "schemas": [ "resources/content/schema/base/aiServiceDescriptionUpdate.json", @@ -1427,153 +1604,223 @@ ], "addedTuples": [], "changedTuples": [], - "authorizationRationale": { - "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/api-logic/src/main/java/io/cattle/platform/iaas/api/port/PortPreflightInputs.java", + "path": "code/iaas/api-logic/src/main/java/io/cattle/platform/iaas/api/volume/VolumePreflightInputs.java", "before": null, - "after": "237060330f0ae0c092b11018159b6bf9fa214e1e2564d57f6b139dee34c38e13", - "baselineSourceSha256": "237060330f0ae0c092b11018159b6bf9fa214e1e2564d57f6b139dee34c38e13", + "after": "dffdce7b133cf1bc16b410c32f365a6fee5ce807e79802a2b3d52a98da1dfc02", "newlyBound": true, - "sourceBytesChanged": false + "baselineSourceSha256": "dffdce7b133cf1bc16b410c32f365a6fee5ce807e79802a2b3d52a98da1dfc02" }, { - "path": "code/iaas/api-logic/src/main/java/io/cattle/platform/iaas/api/port/PortPreflightService.java", + "path": "code/iaas/api-logic/src/main/java/io/cattle/platform/iaas/api/volume/VolumePreflightService.java", "before": null, - "after": "6a64ad664eee2f5817e6157b5564f72ac3381cb7f0881e2ef73bab6b80c3a110", - "baselineSourceSha256": "6a64ad664eee2f5817e6157b5564f72ac3381cb7f0881e2ef73bab6b80c3a110", + "after": "4f2a93e0e00abb68277d09351a0a85c7fa201bd8e1d07e2bbbd8bcaaa5ce18e2", "newlyBound": true, - "sourceBytesChanged": false + "baselineSourceSha256": "4f2a93e0e00abb68277d09351a0a85c7fa201bd8e1d07e2bbbd8bcaaa5ce18e2" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchMaterializer.java", - "before": "00945aae0ccd8091f6835168bd0d0ff7615be2f00cc86c27f48218520d680f50", - "after": "26aa70893a8182f191f9b4d02d0f68a04ec4da6017b83e1d4726a8327ee93781", - "baselineSourceSha256": "00945aae0ccd8091f6835168bd0d0ff7615be2f00cc86c27f48218520d680f50", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryInputHandler.java", + "before": null, + "after": "46abd6c96d543f84b3884d1dafd4f41db9f6111e0edf9f770a8289f5604390a1", + "newlyBound": true, + "baselineSourceSha256": null + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryService.java", + "before": "4067cc8685399b2b2bfb9e7b5eaddd10b906f63fccf286ef7e118622e4e80ea6", + "after": "01f316fb38aefd1e43f81773dab71576ff53ef37c4b8a9e844ae6d2556df3aca", "newlyBound": false, - "sourceBytesChanged": true + "baselineSourceSha256": "4067cc8685399b2b2bfb9e7b5eaddd10b906f63fccf286ef7e118622e4e80ea6" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDescriptor.java", - "before": "542f4d8b53b7360a4b72570466f63f027469d5bf2f0e3ce66ea3d6b6ceb6f417", - "after": "c60650a74476af5faadd8cc76bcab4d073fd294dfac28d8dc176c07daeb7d4dc", - "baselineSourceSha256": "542f4d8b53b7360a4b72570466f63f027469d5bf2f0e3ce66ea3d6b6ceb6f417", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchAccess.java", + "before": "639ceb2151b6668af77ff2f71ead2cd7699d76d45c17521e46a90af624286b52", + "after": "5f0e1de8995318226e0c8ae48acbd3a6e07b025e7fa068daa1bc5945e25c6d49", "newlyBound": false, - "sourceBytesChanged": true + "baselineSourceSha256": "639ceb2151b6668af77ff2f71ead2cd7699d76d45c17521e46a90af624286b52" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDomain.java", - "before": "141fa3773edceec3c7f489d10964ebc4db4c8ff4271976f6ee58ac0f342e96a8", - "after": "fc2245d42e31f3426fef91d68bbe8039f67f50b0a47b9e60cd003c83d1776ad0", - "baselineSourceSha256": "141fa3773edceec3c7f489d10964ebc4db4c8ff4271976f6ee58ac0f342e96a8", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPlanResourceManager.java", + "before": "ef70bb6e029fdb446358ad81e9f791f14e892c28bda754b8ce479077fb85ff98", + "after": "b81b7e746d7557bd740b164123bd7584cf399b8b5a2bd57e258535392fe67b3d", "newlyBound": false, - "sourceBytesChanged": true + "baselineSourceSha256": "ef70bb6e029fdb446358ad81e9f791f14e892c28bda754b8ce479077fb85ff98" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanService.java", - "before": "bfeb2e89f4a3049ec70292e14e29b2a74b4302f8756e80954edb2fdbe6497f0d", - "after": "1d52b18a6b3efc5f86b30e52cb1ab57bb27f788e28703a4e7864d473dd7aa071", - "baselineSourceSha256": "bfeb2e89f4a3049ec70292e14e29b2a74b4302f8756e80954edb2fdbe6497f0d", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPlanService.java", + "before": "77b8f8b2b4ade110064ba0a874db04c1d5adbfe9df68655b8760c395270a05e3", + "after": "119c0347b3e6b984a76e1250fb2d2fee79337b77b13ed9dd87d888518efa818a", "newlyBound": false, - "sourceBytesChanged": true + "baselineSourceSha256": "77b8f8b2b4ade110064ba0a874db04c1d5adbfe9df68655b8760c395270a05e3" }, { - "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/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanResourceManager.java", + "before": "37da81bdccdb528c1d7f1b18217d3896cabd0ad601e6a3809fd50913b6613050", + "after": "17ba569ba4f5265d7d3a8e50cc3ce2d21d220232c354e169be511b74ce212b17", + "newlyBound": false, + "baselineSourceSha256": "37da81bdccdb528c1d7f1b18217d3896cabd0ad601e6a3809fd50913b6613050" }, { - "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/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanService.java", + "before": "1d52b18a6b3efc5f86b30e52cb1ab57bb27f788e28703a4e7864d473dd7aa071", + "after": "1b920af86f0294b1c1e1ee44f4a9b7dc9bf7eac9e269298a1007c8ff235e6d45", + "newlyBound": false, + "baselineSourceSha256": "1d52b18a6b3efc5f86b30e52cb1ab57bb27f788e28703a4e7864d473dd7aa071" }, { - "path": "code/iaas/model/src/main/java/io/cattle/platform/core/util/PortSpec.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanInputHandler.java", "before": null, - "after": "66d88b6af51001ce9a49d0b47d9031c2fb0c076ff60b75b4f6b30473da64bd9e", - "baselineSourceSha256": "19c50738b291e64f425a05e58d781389cba151933d9741e9ab7f0b5a11a0d734", + "after": "a38c9fece7444a5fd5385b48f9b9ca2997e8218c82c91096473115cc87ee69aa", "newlyBound": true, - "sourceBytesChanged": true + "baselineSourceSha256": null }, { - "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/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanResourceManager.java", + "before": "51ac797f47ba68e52d360c6c53bb3aa143e33a637286deca9636f5d55559f1a8", + "after": "30cb1cdae0766c0f0a6c7f3f4c27187ca0bd565002bda4e311368e420e88d9a7", + "newlyBound": false, + "baselineSourceSha256": "51ac797f47ba68e52d360c6c53bb3aa143e33a637286deca9636f5d55559f1a8" }, { - "path": "code/iaas/service-discovery/api/src/main/java/io/cattle/platform/servicediscovery/api/service/ServiceCreateValidation.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanService.java", "before": null, - "after": "663e4a276a68636e4d5284cfb7b4fd80301d733f4a410775e4f6d7ae34e9a143", - "baselineSourceSha256": null, + "after": "7398ac81e4287ee2e950d2b86b8ce55fd574d8af40831044553514e729d8204d", "newlyBound": true, - "sourceBytesChanged": true + "baselineSourceSha256": "12782760bbd8e600c06876006e39ff9e7f3de818ffb8e39a5e294f9484579a8a" }, { - "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/api/src/main/java/io/cattle/platform/servicediscovery/api/filter/ServiceUpgradeValidationFilter.java", + "before": "27cea9ff5e7efc030de24d0095669fa5812a7931d64bcdbf8dfefb8888d7db36", + "after": "1adcf76557b3392161a46393d795b67a0ccc781a49d544fa8672aafe49475a63", + "newlyBound": false, + "baselineSourceSha256": "27cea9ff5e7efc030de24d0095669fa5812a7931d64bcdbf8dfefb8888d7db36" }, { - "path": "code/iaas/service-discovery/server/src/main/java/io/cattle/platform/servicediscovery/service/impl/ServiceDiscoveryServiceImpl.java", + "path": "code/iaas/service-discovery/api/src/main/java/io/cattle/platform/servicediscovery/api/service/ServiceUpgradeValidation.java", "before": null, - "after": "0300fb72cd8f5a1a06c3d26509a9d74c79c644d1aab0c1ce16fa46357a8b1039", - "baselineSourceSha256": "0300fb72cd8f5a1a06c3d26509a9d74c79c644d1aab0c1ce16fa46357a8b1039", + "after": "ffcf91e17f0aaffe821acf297d1e125768a5332c062c1bf5c435114035255cf1", "newlyBound": true, - "sourceBytesChanged": false + "baselineSourceSha256": null + }, + { + "path": "code/packaging/app-config/src/main/java/io/cattle/platform/app/IaasApiConfig.java", + "before": "59b633a3f6614f811b5f14b0eeeeb1f99683d319a2c28bea2093f48a33a53130", + "after": "fa64b48420abe59930c464283a773d2dfcccbaeea152fdf1a997d8dcae184e9a", + "newlyBound": false, + "baselineSourceSha256": "59b633a3f6614f811b5f14b0eeeeb1f99683d319a2c28bea2093f48a33a53130" }, { "path": "resources/content/schema/admin/admin-auth.json", - "before": "27630fdb4d5cf7a1dcd7e693b84d0b31513d627a64a5d2365119075aca6d4f85", - "after": "f713ba64c53fb498b2cbe7f96b5c794ed35d093a74a78cafcffe3f19b59ef5df", - "baselineSourceSha256": "27630fdb4d5cf7a1dcd7e693b84d0b31513d627a64a5d2365119075aca6d4f85", + "before": "f713ba64c53fb498b2cbe7f96b5c794ed35d093a74a78cafcffe3f19b59ef5df", + "after": "0282ac1900b37648214fc48dce394e00c1b22cc27340e08612d5e5e81ca66f64", "newlyBound": false, - "sourceBytesChanged": true + "baselineSourceSha256": "f713ba64c53fb498b2cbe7f96b5c794ed35d093a74a78cafcffe3f19b59ef5df" }, { - "path": "resources/content/schema/base/aiResourceChangePlan.json", - "before": "7acb469f2ffbd0454e7e4e071aa672d3c444140583f41798a6d3cb02a632ba5e", - "after": "113f44df62f09f822b7f97f4cfdcab96de2f0f70995c821632f801c20134110c", - "baselineSourceSha256": "7acb469f2ffbd0454e7e4e071aa672d3c444140583f41798a6d3cb02a632ba5e", + "path": "resources/content/schema/base/aiServiceUpgradePlan.json", + "before": "025cd4a0b044cda9f2500a79a99b605c867f95e8f69d46997ed843b221d67cb6", + "after": "5285ad2caab0e05e28fed27299e5ad114e84c15820bba9a9cfbfc45a163b62d0", "newlyBound": false, - "sourceBytesChanged": true + "baselineSourceSha256": "025cd4a0b044cda9f2500a79a99b605c867f95e8f69d46997ed843b221d67cb6" }, { "path": "resources/content/schema/project/project-auth.json", - "before": "c752d35a5b2b2bdc65af5914d89071948e5b10e41772736ade7f89dfd2554a59", - "after": "ce1ebde019bfd57d6ddd5c2b77d119a76e1df38bf887371e765478c3d96388cf", - "baselineSourceSha256": "c752d35a5b2b2bdc65af5914d89071948e5b10e41772736ade7f89dfd2554a59", + "before": "ce1ebde019bfd57d6ddd5c2b77d119a76e1df38bf887371e765478c3d96388cf", + "after": "8ba8e18dca27a9cc65208c49a5ab72f3ff3d4d4353ae6b8d6891a2f913a76f09", "newlyBound": false, - "sourceBytesChanged": true + "baselineSourceSha256": "ce1ebde019bfd57d6ddd5c2b77d119a76e1df38bf887371e765478c3d96388cf" }, { "path": "resources/content/schema/user/user-auth.json", - "before": "0f96a7e195647b967d4f83cab74d6a0fc5c442a896baa54384937af765bc2d9e", - "after": "66a82fa448806fb826ecd7a11bc9dc6cc9598e7a221d63191113457fd3376091", - "baselineSourceSha256": "0f96a7e195647b967d4f83cab74d6a0fc5c442a896baa54384937af765bc2d9e", + "before": "66a82fa448806fb826ecd7a11bc9dc6cc9598e7a221d63191113457fd3376091", + "after": "4f54ea9c4b8b2d7d7e849e17a73f33d7262f2519e8263e28842ee7eaec414835", "newlyBound": false, - "sourceBytesChanged": true + "baselineSourceSha256": "66a82fa448806fb826ecd7a11bc9dc6cc9598e7a221d63191113457fd3376091" } ], - "reviewManifest": "scripts/api-key-operation-review-0.183.366.json" + "roleFieldDelta": [ + { + "path": "resources/content/schema/admin/admin-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.startFirst", + "before": null, + "after": "cr" + }, + { + "path": "resources/content/schema/admin/admin-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.batchSize", + "before": null, + "after": "cr" + }, + { + "path": "resources/content/schema/admin/admin-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.intervalMillis", + "before": null, + "after": "cr" + }, + { + "path": "resources/content/schema/admin/admin-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.upgradeStrategy", + "before": null, + "after": "r" + }, + { + "path": "resources/content/schema/project/project-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.startFirst", + "before": null, + "after": "cr" + }, + { + "path": "resources/content/schema/project/project-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.batchSize", + "before": null, + "after": "cr" + }, + { + "path": "resources/content/schema/project/project-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.intervalMillis", + "before": null, + "after": "cr" + }, + { + "path": "resources/content/schema/project/project-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.upgradeStrategy", + "before": null, + "after": "r" + }, + { + "path": "resources/content/schema/user/user-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.startFirst", + "before": null, + "after": "cr" + }, + { + "path": "resources/content/schema/user/user-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.batchSize", + "before": null, + "after": "cr" + }, + { + "path": "resources/content/schema/user/user-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.intervalMillis", + "before": null, + "after": "cr" + }, + { + "path": "resources/content/schema/user/user-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.upgradeStrategy", + "before": null, + "after": "r" + } + ], + "authorizationRationale": { + "operation": "1396 exact tuples and each semantic/registered/unsupported/human-only disposition unchanged.", + "strategy": "Only existing upgrade-plan roles accept three optional scalar fields; dispatch still uses the original authorized native manager. No target or scope grant is added.", + "preflight": "Existing native filter implements the shared read-only interface. Native strategy invariants, including retain-IP/start-first, and the same port/volume rules run at preview/confirmation/apply; native batch normalization and original validation errors remain unchanged, including their canonical DENY reason.", + "audit": "Exact typed audit collection only; a sticky boolean retains the original closed-field rejection, and the existing owner audits ERROR without invoking its runner. No raw payload is retained.", + "acceptance": "Source extraction does not establish artifact, role matrix, HTTP/UI or asynchronous runtime acceptance.", + "nativeActionOperandLookup": "Native Catalog-launch, resource-change and upgrade action operands use only exact framework request markers and authorized read snapshots. No prefetch observation writes or retained plan/credential row locks precede the action application lock. Ordinary GET observation and final action owner, role, Key revision, source/snapshot, CAS and one-claim checks remain; no timeout, retry, schema or grant expansion. New upgrade plans bind the complete strategy in their private target snapshot so older engines reject them before claim; genuine legacy image-only plans keep classic snapshots and original defaults." + } }, "tuples": [ ["account","collection","GET","","","read"], diff --git a/scripts/api-key-operation-inventory.json b/scripts/api-key-operation-inventory.json index 3e3cb48d9e..98966dd7b7 100644 --- a/scripts/api-key-operation-inventory.json +++ b/scripts/api-key-operation-inventory.json @@ -220,20 +220,22 @@ "defaultProcesses": "ebf41ab873c349656c307511712674e0db434a9c4986e08d03ec35d92bdcd6a7" }, "currentSourceReview": { - "version": "unreleased-366-service-create-native-ports-healthcheck-and-catalog-projection", + "version": "unreleased-367-service-upgrade-strategy-audit-preview-native-action-lock-order-and-rollback-fence", "status": "SOURCE_REEXTRACTED_NOT_SIGNED_NOT_RUNTIME_ACCEPTANCE", - "baseCommit": "259ef59e8110fab71488d5dbd8c9affda4279217", - "sourceCandidate": "9efe87becb752287072e2257c949680fed75282d", + "baseCommit": "c8cd1f8218d29e034c2569af1e0e8530c305ce50", + "sourceCandidate": "WORKTREE_NOT_COMMITTED", + "producerCommit": "NOT_ASSIGNED_DURING_SOURCE_REVIEW", "contract": "scripts/api-key-operation-contract.json", - "contractSha256": "8af458ac13a30c611fde008992297ad77b6fb40e237d615b7397e74e4302c820", + "contractSha256": "35b2b2447c0f07205b806ff7304aacb4a90ecface2558701dbde8725a56c50c8", "sourceInputs": { "algorithm": "c03-source-inputs-v1", - "count": 441, - "sha256": "7ce17c8b05ccfba961414808148d8638bf54a9ad3bbc7de00facfcba4b26ac39", + "count": 447, + "sha256": "66a5ce2ee5f4277993c03de0f72a08dd828ec65e4c447430248c1b093583ab59", "anchors": [ "AbstractSchemaFactory.java", "AiAliasServiceChange.java", "AiAuditQuery.java", + "AiAuditQueryInputHandler.java", "AiAuditQueryResourceManager.java", "AiAuditQueryRunner.java", "AiAuditQueryService.java", @@ -277,6 +279,8 @@ "AiResourceViewResourceManager.java", "AiResourceViewService.java", "AiServiceCreateChange.java", + "AiServiceUpgradePlanInputHandler.java", + "AiServiceUpgradePlanService.java", "ApiAuditPayload.java", "ApiAuthenticator.java", "ApiCookieAuthentication.java", @@ -302,6 +306,7 @@ "FileSchemaFactory.java", "GenericWhitelistedProxy.java", "IaasApiConfig.java", + "InServiceUpgradeStrategy.java", "NotWritablePostProcessor.java", "PortBindingAddress.java", "PortPreflightInputs.java", @@ -316,9 +321,14 @@ "ServiceCreate.java", "ServiceCreateValidation.java", "ServiceDiscoveryServiceImpl.java", + "ServiceUpgradeStrategy.java", + "ServiceUpgradeValidation.java", + "ServiceUpgradeValidationFilter.java", "ServiceValidationFilter.java", "TokenAuthLookup.java", - "TransformInspect.java" + "TransformInspect.java", + "VolumePreflightInputs.java", + "VolumePreflightService.java" ], "markers": [ "@Type", @@ -336,68 +346,92 @@ "addedTuples": [], "removedTuples": [], "changedTuples": [], - "roleProjectionDelta": [ + "roleFieldDelta": [ { "path": "resources/content/schema/admin/admin-auth.json", - "pointer": "/authorize/aiResourceChangePlan.executionResult", + "pointer": "/authorize/aiServiceUpgradePlan.startFirst", + "before": null, + "after": "cr" + }, + { + "path": "resources/content/schema/admin/admin-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.batchSize", + "before": null, + "after": "cr" + }, + { + "path": "resources/content/schema/admin/admin-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.intervalMillis", + "before": null, + "after": "cr" + }, + { + "path": "resources/content/schema/admin/admin-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.upgradeStrategy", "before": null, "after": "r" }, { "path": "resources/content/schema/project/project-auth.json", - "pointer": "/authorize/aiResourceChangePlan.executionResult", + "pointer": "/authorize/aiServiceUpgradePlan.startFirst", + "before": null, + "after": "cr" + }, + { + "path": "resources/content/schema/project/project-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.batchSize", + "before": null, + "after": "cr" + }, + { + "path": "resources/content/schema/project/project-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.intervalMillis", + "before": null, + "after": "cr" + }, + { + "path": "resources/content/schema/project/project-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.upgradeStrategy", "before": null, "after": "r" }, { "path": "resources/content/schema/user/user-auth.json", - "pointer": "/authorize/aiResourceChangePlan.executionResult", + "pointer": "/authorize/aiServiceUpgradePlan.startFirst", + "before": null, + "after": "cr" + }, + { + "path": "resources/content/schema/user/user-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.batchSize", + "before": null, + "after": "cr" + }, + { + "path": "resources/content/schema/user/user-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.intervalMillis", + "before": null, + "after": "cr" + }, + { + "path": "resources/content/schema/user/user-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.upgradeStrategy", "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_C03_REVIEW", "previousReview": { - "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", @@ -444,6 +478,7 @@ "AiResourceChangeProcessObserver.java", "AiResourceViewResourceManager.java", "AiResourceViewService.java", + "AiServiceCreateChange.java", "ApiAuditPayload.java", "ApiAuthenticator.java", "ApiCookieAuthentication.java", @@ -470,12 +505,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" ], @@ -495,18 +538,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", @@ -605,17 +698,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", @@ -716,15 +809,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", @@ -791,6 +884,7 @@ "EngineContext.java", "EventBasedProcessHandler.java", "EventDiagnostics.java", + "ExternalDeploymentUnitInstance.java", "ExternalDynamicExtensionHandler.java", "FileSchemaFactory.java", "GenericWhitelistedProxy.java", @@ -824,15 +918,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", @@ -932,14 +1026,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", @@ -1037,14 +1132,139 @@ "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" }, - "reviewManifest": "scripts/api-key-operation-review-0.183.366.json", - "reviewManifestSha256": "44e30398885ac2eee8af7aef8bcc576c2b3d90b347e30cef6ecc96469934f0c7" + "reviewManifest": "scripts/api-key-operation-review-0.183.367.json", + "reviewManifestSha256": "d4ecd4a95d4d541b5042f410aafcf64342c2d8900a899e93fedb7b640b395669", + "nativeActionOperandLockOrder": { + "owners": [ + "aiCatalogStackLaunchPlan", + "aiResourceChangePlan", + "aiServiceUpgradePlan" + ], + "rootCause": "Outer native API write transaction retained a prefetch row lock after application-lock release; concurrent receipt GET inverted lock order.", + "fix": "Native Catalog-launch, resource-change and upgrade action operands use only exact framework request markers and authorized read snapshots. No prefetch observation writes or retained plan/credential row locks precede the action application lock. Ordinary GET observation and final action owner, role, Key revision, source/snapshot, CAS and one-claim checks remain; no timeout, retry, schema or grant expansion. New upgrade plans bind the complete strategy in their private target snapshot so older engines reject them before claim; genuine legacy image-only plans keep classic snapshots and original defaults.", + "lockTimeoutChanges": 0, + "retryChanges": 0, + "removedLocks": 0, + "databaseChanges": 0, + "uiPollingCausalityClaimed": false, + "runtimeAcceptanceClaimed": false + } } } diff --git a/scripts/api-key-operation-review-0.183.367.json b/scripts/api-key-operation-review-0.183.367.json new file mode 100644 index 0000000000..fa2aa4ce51 --- /dev/null +++ b/scripts/api-key-operation-review-0.183.367.json @@ -0,0 +1,312 @@ +{ + "schemaVersion": 1, + "version": "0.183.367", + "scope": "SOURCE_ONLY_NOT_RUNTIME_ACCEPTANCE", + "reviewedBaseCommit": "c8cd1f8218d29e034c2569af1e0e8530c305ce50", + "sourceInputs": { + "algorithm": "c03-source-inputs-v1", + "baseline": { + "count": 441, + "sha256": "7ce17c8b05ccfba961414808148d8638bf54a9ad3bbc7de00facfcba4b26ac39" + }, + "reviewed": { + "count": 447, + "sha256": "66a5ce2ee5f4277993c03de0f72a08dd828ec65e4c447430248c1b093583ab59" + }, + "addedAnchors": [ + "AiAuditQueryInputHandler.java", + "AiServiceUpgradePlanInputHandler.java", + "AiServiceUpgradePlanService.java", + "InServiceUpgradeStrategy.java", + "ServiceUpgradeStrategy.java", + "ServiceUpgradeValidation.java", + "ServiceUpgradeValidationFilter.java", + "VolumePreflightInputs.java", + "VolumePreflightService.java" + ] + }, + "tupleReview": { + "baseline": 1396, + "candidate": 1396, + "added": 0, + "removed": 0, + "semanticChanges": 0, + "dispositionChanges": 0, + "sha256": "8f1c0fb378663b0b3d272efba2f211cb210eb6f17cb8035aefa33b56feeaf36a" + }, + "delta": [ + { + "path": "code/iaas/api-logic/src/main/java/io/cattle/platform/iaas/api/volume/VolumePreflightInputs.java", + "before": null, + "after": "dffdce7b133cf1bc16b410c32f365a6fee5ce807e79802a2b3d52a98da1dfc02", + "newlyBound": true, + "baselineSourceSha256": "dffdce7b133cf1bc16b410c32f365a6fee5ce807e79802a2b3d52a98da1dfc02" + }, + { + "path": "code/iaas/api-logic/src/main/java/io/cattle/platform/iaas/api/volume/VolumePreflightService.java", + "before": null, + "after": "4f2a93e0e00abb68277d09351a0a85c7fa201bd8e1d07e2bbbd8bcaaa5ce18e2", + "newlyBound": true, + "baselineSourceSha256": "4f2a93e0e00abb68277d09351a0a85c7fa201bd8e1d07e2bbbd8bcaaa5ce18e2" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryInputHandler.java", + "before": null, + "after": "46abd6c96d543f84b3884d1dafd4f41db9f6111e0edf9f770a8289f5604390a1", + "newlyBound": true, + "baselineSourceSha256": null + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryService.java", + "before": "4067cc8685399b2b2bfb9e7b5eaddd10b906f63fccf286ef7e118622e4e80ea6", + "after": "01f316fb38aefd1e43f81773dab71576ff53ef37c4b8a9e844ae6d2556df3aca", + "newlyBound": false, + "baselineSourceSha256": "4067cc8685399b2b2bfb9e7b5eaddd10b906f63fccf286ef7e118622e4e80ea6" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchAccess.java", + "before": "639ceb2151b6668af77ff2f71ead2cd7699d76d45c17521e46a90af624286b52", + "after": "5f0e1de8995318226e0c8ae48acbd3a6e07b025e7fa068daa1bc5945e25c6d49", + "newlyBound": false, + "baselineSourceSha256": "639ceb2151b6668af77ff2f71ead2cd7699d76d45c17521e46a90af624286b52" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPlanResourceManager.java", + "before": "ef70bb6e029fdb446358ad81e9f791f14e892c28bda754b8ce479077fb85ff98", + "after": "b81b7e746d7557bd740b164123bd7584cf399b8b5a2bd57e258535392fe67b3d", + "newlyBound": false, + "baselineSourceSha256": "ef70bb6e029fdb446358ad81e9f791f14e892c28bda754b8ce479077fb85ff98" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPlanService.java", + "before": "77b8f8b2b4ade110064ba0a874db04c1d5adbfe9df68655b8760c395270a05e3", + "after": "119c0347b3e6b984a76e1250fb2d2fee79337b77b13ed9dd87d888518efa818a", + "newlyBound": false, + "baselineSourceSha256": "77b8f8b2b4ade110064ba0a874db04c1d5adbfe9df68655b8760c395270a05e3" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanResourceManager.java", + "before": "37da81bdccdb528c1d7f1b18217d3896cabd0ad601e6a3809fd50913b6613050", + "after": "17ba569ba4f5265d7d3a8e50cc3ce2d21d220232c354e169be511b74ce212b17", + "newlyBound": false, + "baselineSourceSha256": "37da81bdccdb528c1d7f1b18217d3896cabd0ad601e6a3809fd50913b6613050" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanService.java", + "before": "1d52b18a6b3efc5f86b30e52cb1ab57bb27f788e28703a4e7864d473dd7aa071", + "after": "1b920af86f0294b1c1e1ee44f4a9b7dc9bf7eac9e269298a1007c8ff235e6d45", + "newlyBound": false, + "baselineSourceSha256": "1d52b18a6b3efc5f86b30e52cb1ab57bb27f788e28703a4e7864d473dd7aa071" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanInputHandler.java", + "before": null, + "after": "a38c9fece7444a5fd5385b48f9b9ca2997e8218c82c91096473115cc87ee69aa", + "newlyBound": true, + "baselineSourceSha256": null + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanResourceManager.java", + "before": "51ac797f47ba68e52d360c6c53bb3aa143e33a637286deca9636f5d55559f1a8", + "after": "30cb1cdae0766c0f0a6c7f3f4c27187ca0bd565002bda4e311368e420e88d9a7", + "newlyBound": false, + "baselineSourceSha256": "51ac797f47ba68e52d360c6c53bb3aa143e33a637286deca9636f5d55559f1a8" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/upgrade/AiServiceUpgradePlanService.java", + "before": null, + "after": "7398ac81e4287ee2e950d2b86b8ce55fd574d8af40831044553514e729d8204d", + "newlyBound": true, + "baselineSourceSha256": "12782760bbd8e600c06876006e39ff9e7f3de818ffb8e39a5e294f9484579a8a" + }, + { + "path": "code/iaas/service-discovery/api/src/main/java/io/cattle/platform/servicediscovery/api/filter/ServiceUpgradeValidationFilter.java", + "before": "27cea9ff5e7efc030de24d0095669fa5812a7931d64bcdbf8dfefb8888d7db36", + "after": "1adcf76557b3392161a46393d795b67a0ccc781a49d544fa8672aafe49475a63", + "newlyBound": false, + "baselineSourceSha256": "27cea9ff5e7efc030de24d0095669fa5812a7931d64bcdbf8dfefb8888d7db36" + }, + { + "path": "code/iaas/service-discovery/api/src/main/java/io/cattle/platform/servicediscovery/api/service/ServiceUpgradeValidation.java", + "before": null, + "after": "ffcf91e17f0aaffe821acf297d1e125768a5332c062c1bf5c435114035255cf1", + "newlyBound": true, + "baselineSourceSha256": null + }, + { + "path": "code/packaging/app-config/src/main/java/io/cattle/platform/app/IaasApiConfig.java", + "before": "59b633a3f6614f811b5f14b0eeeeb1f99683d319a2c28bea2093f48a33a53130", + "after": "fa64b48420abe59930c464283a773d2dfcccbaeea152fdf1a997d8dcae184e9a", + "newlyBound": false, + "baselineSourceSha256": "59b633a3f6614f811b5f14b0eeeeb1f99683d319a2c28bea2093f48a33a53130" + }, + { + "path": "resources/content/schema/admin/admin-auth.json", + "before": "f713ba64c53fb498b2cbe7f96b5c794ed35d093a74a78cafcffe3f19b59ef5df", + "after": "0282ac1900b37648214fc48dce394e00c1b22cc27340e08612d5e5e81ca66f64", + "newlyBound": false, + "baselineSourceSha256": "f713ba64c53fb498b2cbe7f96b5c794ed35d093a74a78cafcffe3f19b59ef5df" + }, + { + "path": "resources/content/schema/base/aiServiceUpgradePlan.json", + "before": "025cd4a0b044cda9f2500a79a99b605c867f95e8f69d46997ed843b221d67cb6", + "after": "5285ad2caab0e05e28fed27299e5ad114e84c15820bba9a9cfbfc45a163b62d0", + "newlyBound": false, + "baselineSourceSha256": "025cd4a0b044cda9f2500a79a99b605c867f95e8f69d46997ed843b221d67cb6" + }, + { + "path": "resources/content/schema/project/project-auth.json", + "before": "ce1ebde019bfd57d6ddd5c2b77d119a76e1df38bf887371e765478c3d96388cf", + "after": "8ba8e18dca27a9cc65208c49a5ab72f3ff3d4d4353ae6b8d6891a2f913a76f09", + "newlyBound": false, + "baselineSourceSha256": "ce1ebde019bfd57d6ddd5c2b77d119a76e1df38bf887371e765478c3d96388cf" + }, + { + "path": "resources/content/schema/user/user-auth.json", + "before": "66a82fa448806fb826ecd7a11bc9dc6cc9598e7a221d63191113457fd3376091", + "after": "4f54ea9c4b8b2d7d7e849e17a73f33d7262f2519e8263e28842ee7eaec414835", + "newlyBound": false, + "baselineSourceSha256": "66a82fa448806fb826ecd7a11bc9dc6cc9598e7a221d63191113457fd3376091" + } + ], + "schemaFieldDelta": { + "startFirst": { + "type": "boolean", + "create": true, + "update": false, + "nullable": false + }, + "batchSize": { + "type": "int", + "create": true, + "update": false, + "nullable": false, + "min": 1, + "max": 9007199254740991 + }, + "intervalMillis": { + "type": "int", + "create": true, + "update": false, + "nullable": false, + "min": 100, + "max": 9007199254740991 + }, + "upgradeStrategy": { + "type": "json", + "create": false, + "update": false + } + }, + "roleFieldDelta": [ + { + "path": "resources/content/schema/admin/admin-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.startFirst", + "before": null, + "after": "cr" + }, + { + "path": "resources/content/schema/admin/admin-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.batchSize", + "before": null, + "after": "cr" + }, + { + "path": "resources/content/schema/admin/admin-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.intervalMillis", + "before": null, + "after": "cr" + }, + { + "path": "resources/content/schema/admin/admin-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.upgradeStrategy", + "before": null, + "after": "r" + }, + { + "path": "resources/content/schema/project/project-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.startFirst", + "before": null, + "after": "cr" + }, + { + "path": "resources/content/schema/project/project-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.batchSize", + "before": null, + "after": "cr" + }, + { + "path": "resources/content/schema/project/project-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.intervalMillis", + "before": null, + "after": "cr" + }, + { + "path": "resources/content/schema/project/project-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.upgradeStrategy", + "before": null, + "after": "r" + }, + { + "path": "resources/content/schema/user/user-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.startFirst", + "before": null, + "after": "cr" + }, + { + "path": "resources/content/schema/user/user-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.batchSize", + "before": null, + "after": "cr" + }, + { + "path": "resources/content/schema/user/user-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.intervalMillis", + "before": null, + "after": "cr" + }, + { + "path": "resources/content/schema/user/user-auth.json", + "pointer": "/authorize/aiServiceUpgradePlan.upgradeStrategy", + "before": null, + "after": "r" + } + ], + "authorizationBoundary": { + "existingRolePermissionsChanged": 0, + "newUpgradePlanOptionalInputEntries": 9, + "newReadOnlyRoleProjectionEntries": 3, + "newHttpMutationOperationGrants": 0, + "newGlobalMutationGrants": 0 + }, + "frozenV1SchemaFilesUnchanged": 16, + "extractor": { + "constructionSha256": "078ffe48881469fe5eb3cb43a15fa41b480fe70e11bd5f63102ded6b35a6d076", + "parserSha256": "7cc8e70d9f3ea391eb74fef2490f4407aa47833c585b6729c62b3d9e5829ac2b", + "extractorSha256": "112d2ff723d88acbe7bb45ff48c9c9018feb93b08031a3e86f867030871477fc" + }, + "historicalProducerAndReviewChainsPreserved": true, + "artifactQualificationClaimed": false, + "runtimeAcceptanceClaimed": false, + "typedClosedInputHandlerOrder": { + "path": "code/packaging/app-config/src/main/resources/META-INF/cattle/iaas-api/defaults.properties", + "beforeSha256": "516e7d8652077c783bb6709c40078bba34637169753b5d0c922050ab67099773", + "afterSha256": "59598ecd0975fec7d74ec532f3455fab9dbe1512221c410b86dd815b1a458d8f", + "change": "AiAuditQueryInputHandler and preview-only AiServiceUpgradePlanInputHandler before ValidationHandler; other handler order unchanged." + }, + "nativeActionOperandLockOrder": { + "owners": [ + "aiCatalogStackLaunchPlan", + "aiResourceChangePlan", + "aiServiceUpgradePlan" + ], + "rootCause": "Outer native API write transaction retained a prefetch row lock after application-lock release; concurrent receipt GET inverted lock order.", + "fix": "Native Catalog-launch, resource-change and upgrade action operands use only exact framework request markers and authorized read snapshots. No prefetch observation writes or retained plan/credential row locks precede the action application lock. Ordinary GET observation and final action owner, role, Key revision, source/snapshot, CAS and one-claim checks remain; no timeout, retry, schema or grant expansion. New upgrade plans bind the complete strategy in their private target snapshot so older engines reject them before claim; genuine legacy image-only plans keep classic snapshots and original defaults.", + "lockTimeoutChanges": 0, + "retryChanges": 0, + "removedLocks": 0, + "databaseChanges": 0, + "uiPollingCausalityClaimed": false, + "runtimeAcceptanceClaimed": false + } +} diff --git a/scripts/build b/scripts/build index 33a1d0023e..9e539cd660 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.366} +ENGINE_VERSION=${ENGINE_VERSION:-0.183.367} case "$SOURCE_REVISION" in ''|*[!0-9a-f]*) diff --git a/scripts/check-cattle-jdk25-standalone b/scripts/check-cattle-jdk25-standalone index 189a10602f..851f070e1d 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.366}} +expected_version=${EXPECTED_ENGINE_VERSION:-${ENGINE_VERSION:-0.183.367}} 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 c67747cf4e..b92d6eaa26 100755 --- a/scripts/check-pasturestack-source +++ b/scripts/check-pasturestack-source @@ -51,15 +51,26 @@ 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.366' +require_line code/meta-parent/pom.xml ' 0.183.367' require_line code/meta-parent/pom.xml ' 33.7.2-jre' require_line ubuntu-security-packages.lock 'libbrotli1=1.2.0-3ubuntu0.1' +require_line ubuntu-security-packages.lock 'libpcre2-8-0=10.46-1ubuntu0.1' +require_line ubuntu-security-packages.lock 'libpcre2-posix3=10.46-1ubuntu0.1' for recipe in Dockerfile Dockerfile.dapper; do require_line "$recipe" 'ADD --checksum=sha256:bedeef1cf3268f90338c411cdb0a45b224d0660fd2240f310f380316c8244606 https://security.ubuntu.com/ubuntu/pool/main/b/brotli/libbrotli1_1.2.0-3ubuntu0.1_amd64.deb /tmp/libbrotli1.deb' require_line "$recipe" 'ADD --checksum=sha256:b530942257fb935f8b2cfaa5f8eb5bd59c555fd8e8d01b8ce98912e077ea606c https://repo.maven.apache.org/maven2/com/google/guava/guava/33.7.2-jre/guava-33.7.2-jre.jar /tmp/guava-33.7.2-jre.jar' require_line "$recipe" ' test "$(dpkg-query -W -f='"'"'${db:Status-Status}'"'"' libbrotli1)" = installed; \' require_line "$recipe" ' test "$(dpkg-deb -f /tmp/libbrotli1.deb Version)" = 1.2.0-3ubuntu0.1; \' require_line "$recipe" ' dpkg -i /tmp/libbrotli1.deb; \' + require_line "$recipe" 'ADD --checksum=sha256:44ebbeb6493eacffbc460ca71446959ba86a00b2e23515e4cfb1a5cc317d6672 https://security.ubuntu.com/ubuntu/pool/main/p/pcre2/libpcre2-8-0_10.46-1ubuntu0.1_amd64.deb /tmp/libpcre2-8-0.deb' + require_line "$recipe" 'ADD --checksum=sha256:44605ea665673a359927d014f4a027c50fe6472af8bc956bd1b1c43ddc07a1cd https://security.ubuntu.com/ubuntu/pool/main/p/pcre2/libpcre2-posix3_10.46-1ubuntu0.1_amd64.deb /tmp/libpcre2-posix3.deb' + require_line "$recipe" ' for package in libpcre2-8-0 libpcre2-posix3; do \' + require_line "$recipe" ' test "$(dpkg-query -W -f='"'"'${db:Status-Status}'"'"' "${package}")" = installed; \' + require_line "$recipe" ' test "$(dpkg-deb -f "/tmp/${package}.deb" Package)" = "${package}"; \' + require_line "$recipe" ' test "$(dpkg-deb -f "/tmp/${package}.deb" Version)" = 10.46-1ubuntu0.1; \' + require_line "$recipe" ' test "$(dpkg-deb -f "/tmp/${package}.deb" Architecture)" = amd64; \' + require_line "$recipe" ' dpkg -i /tmp/libpcre2-8-0.deb /tmp/libpcre2-posix3.deb; \' + require_line "$recipe" ' test "$(dpkg-query -W -f='"'"'${Version}'"'"' "${package}")" = 10.46-1ubuntu0.1; \' require_line "$recipe" ' install -m 0644 /tmp/guava-33.7.2-jre.jar ${MAVEN_HOME}/lib/guava-33.7.2-jre.jar && \' require_line "$recipe" ' rm ${MAVEN_HOME}/lib/guava-33.6.0-jre.jar /tmp/guava-33.7.2-jre.jar && \' done @@ -475,4 +486,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.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' +printf 'PASTURESTACK_SOURCE_GATE_OK version=0.183.367 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 04dd7631cd..f77471c7d7 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.366} +expected_version=${EXPECTED_ENGINE_VERSION:-0.183.367} test -f "$artifact" artifact=$(realpath "$artifact") diff --git a/scripts/install-snapshot-packages b/scripts/install-snapshot-packages index e2e286254c..2b35bf672d 100644 --- a/scripts/install-snapshot-packages +++ b/scripts/install-snapshot-packages @@ -106,9 +106,29 @@ verified() download() { rm -f -- "$work/payload/package.deb" "$work/payload/package.deb.FAILED" - /usr/lib/apt/apt-helper -o Acquire::Retries=1 \ + download_status=0 + /usr/lib/apt/apt-helper -o Acquire::Retries=0 \ -o Acquire::https::Timeout=15 -o Acquire::http::Timeout=15 \ - download-file "$1" "$work/payload/package.deb" "SHA256:$hash" && verified "$work/payload/package.deb" + download-file "$1" "$work/payload/package.deb" "SHA256:$hash" || download_status=$? + # APT renames a hash-mismatched payload to .FAILED. Neither this nor our + # own size/hash rejection is a transport failure eligible for retry. + [ ! -e "$work/payload/package.deb.FAILED" ] || return 2 + [ "$download_status" -eq 0 ] || return 1 + verified "$work/payload/package.deb" || return 2 +} + +download_snapshot() +{ + attempt=1 + while :; do + result=0 + download "$1" || result=$? + [ "$result" -ne 0 ] || return 0 + [ "$result" -eq 1 ] || return "$result" + [ "$attempt" -lt 4 ] || return 1 + sleep "$attempt" + attempt=$((attempt + 1)) + done } tab=$(printf '\t') @@ -116,7 +136,7 @@ while IFS="$tab" read -r uri pool filename size hash; do destination=$archives/$filename if verified "$destination"; then continue; fi [ ! -d "$destination" ] || fail invalid_cache_destination - if download "https://archive.ubuntu.com/ubuntu/$pool" || download "$uri"; then + if download "https://archive.ubuntu.com/ubuntu/$pool" || download_snapshot "$uri"; then cache_copy=$(mktemp "$archives/.snapshot-package.XXXXXX") cp -- "$work/payload/package.deb" "$cache_copy" chmod 0644 "$cache_copy" diff --git a/scripts/test-install-snapshot-packages.py b/scripts/test-install-snapshot-packages.py index 0512ce2a6e..51427d5fa7 100644 --- a/scripts/test-install-snapshot-packages.py +++ b/scripts/test-install-snapshot-packages.py @@ -53,6 +53,8 @@ def setUp(self): self.bad.write_bytes(BAD) self.apt_log = self.home / 'apt.log' self.helper_log = self.home / 'helper.log' + self.sleep_log = self.home / 'sleep.log' + self.snapshot_count = self.home / 'snapshot-count' self.installed = self.home / 'installed' self.chown_log = self.home / 'chown.log' self.args = ['install', '-y', '--no-install-recommends', 'unzip=6.0-29ubuntu1'] @@ -70,6 +72,12 @@ def setUp(self): set -eu [ "$#" = 2 ] && [ "$1" = _apt ] && [ "${2##*/}" = payload ] || exit 95 printf '%s\n' "$2" > "$TEST_CHOWN_LOG" +''') + self.make_script('sleep', r'''#!/bin/sh +set -eu +[ "$#" = 1 ] || exit 90 +case "$1" in 1|2|3) ;; *) exit 90 ;; esac +printf '%s\n' "$1" >> "$TEST_SLEEP_LOG" ''') if os.name == 'nt': # The bundled Windows sh omits sha256sum. Keep this test-only @@ -101,6 +109,15 @@ def setUp(self): case "$mode" in fail) exit 22 ;; bad) cp "$TEST_BAD_PAYLOAD" "$3" ;; + failed) cp "$TEST_BAD_PAYLOAD" "$3.FAILED"; exit 100 ;; + failed-ok) cp "$TEST_BAD_PAYLOAD" "$3.FAILED"; cp "$TEST_GOOD_PAYLOAD" "$3" ;; + transient) + count=0 + [ ! -f "$TEST_SNAPSHOT_COUNT" ] || count=$(cat "$TEST_SNAPSHOT_COUNT") + count=$((count + 1)) + printf '%s\n' "$count" > "$TEST_SNAPSHOT_COUNT" + [ "$count" -gt 2 ] || exit 22 + cp "$TEST_GOOD_PAYLOAD" "$3" ;; ok) cp "$TEST_GOOD_PAYLOAD" "$3" ;; *) exit 94 ;; esac @@ -138,7 +155,8 @@ def shell_path(path): return value def run_helper(self, *, archive='ok', snapshot='ok', plan_status=0, args=None): - for path in (self.apt_log, self.helper_log, self.installed, self.chown_log): + for path in (self.apt_log, self.helper_log, self.sleep_log, + self.snapshot_count, self.installed, self.chown_log): path.unlink(missing_ok=True) paths = [self.shell_path(self.bin), self.shell_path(Path(SH).resolve().parent)] paths.extend(['/usr/bin', '/bin'] if os.name == 'nt' else [os.environ['PATH']]) @@ -146,6 +164,7 @@ def run_helper(self, *, archive='ok', snapshot='ok', plan_status=0, args=None): TEST_APT_LOG=self.apt_log.as_posix(), TEST_HELPER_LOG=self.helper_log.as_posix(), TEST_PLAN=self.plan.as_posix(), TEST_INSTALLED=self.installed.as_posix(), TEST_CHOWN_LOG=self.chown_log.as_posix(), + TEST_SLEEP_LOG=self.sleep_log.as_posix(), TEST_SNAPSHOT_COUNT=self.snapshot_count.as_posix(), TEST_GOOD_PAYLOAD=self.good.as_posix(), TEST_BAD_PAYLOAD=self.bad.as_posix(), TEST_ARCHIVE_MODE=archive, TEST_SNAPSHOT_MODE=snapshot, TEST_PLAN_STATUS=str(plan_status)) return subprocess.run([SH, self.script.as_posix(), *(args or self.args)], env=env, @@ -157,6 +176,9 @@ def apt_calls(self): def downloads(self): return self.helper_log.read_text().splitlines() if self.helper_log.exists() else [] + def sleeps(self): + return self.sleep_log.read_text().splitlines() if self.sleep_log.exists() else [] + def assert_rejected(self, result): self.assertNotEqual(0, result.returncode, result.stdout + result.stderr) self.assertFalse(self.installed.exists()) @@ -165,7 +187,7 @@ def test_posix_syntax_and_fixed_production_transport(self): result = subprocess.run([SH, '-n', str(ROOT / 'scripts/install-snapshot-packages')], capture_output=True, text=True, timeout=10) self.assertEqual(0, result.returncode, result.stderr) - self.assertIn('/usr/lib/apt/apt-helper -o Acquire::Retries=1', SOURCE) + self.assertIn('/usr/lib/apt/apt-helper -o Acquire::Retries=0', SOURCE) self.assertIn('Acquire::https::Timeout=15', SOURCE) self.assertNotIn('APT_HELPER', SOURCE) self.assertNotIn('python', SOURCE) @@ -189,6 +211,7 @@ def test_official_archive_first_and_exact_no_download_args(self): result = self.run_helper() self.assertEqual(0, result.returncode, result.stderr) self.assertEqual([f'https://archive.ubuntu.com/ubuntu/{POOL}|SHA256:{HASH}'], self.downloads()) + self.assertEqual([], self.sleeps()) self.assertEqual(GOOD, (self.cache / POOL.rsplit('/', 1)[1]).read_bytes()) calls = self.apt_calls() self.assertEqual(2, len(calls)) @@ -209,12 +232,14 @@ def test_bad_archive_hash_falls_back_and_never_uses_corrupt_payload(self): result = self.run_helper(archive='bad') self.assertEqual(0, result.returncode, result.stderr) self.assertEqual(2, len(self.downloads())) + self.assertEqual([], self.sleeps()) self.assertEqual(GOOD, (self.cache / POOL.rsplit('/', 1)[1]).read_bytes()) def test_bad_hash_on_both_origins_cannot_install(self): result = self.run_helper(archive='bad', snapshot='bad') self.assert_rejected(result) self.assertEqual(2, len(self.downloads())) + self.assertEqual([], self.sleeps()) self.assertEqual(1, len(self.apt_calls())) self.assertFalse(list(self.cache.glob('*.deb'))) @@ -223,11 +248,45 @@ def test_correct_hash_but_wrong_declared_size_cannot_install(self): result = self.run_helper() self.assert_rejected(result) self.assertEqual(2, len(self.downloads())) + self.assertEqual([], self.sleeps()) def test_both_transport_failures_cannot_install(self): result = self.run_helper(archive='fail', snapshot='fail') self.assert_rejected(result) - self.assertEqual(2, len(self.downloads())) + self.assertEqual([f'https://archive.ubuntu.com/ubuntu/{POOL}|SHA256:{HASH}'] + + [f'{SNAPSHOT}{POOL}|SHA256:{HASH}'] * 4, self.downloads()) + self.assertEqual(['1', '2', '3'], self.sleeps()) + self.assertEqual(1, len(self.apt_calls())) + self.assertFalse(list(self.cache.glob('*.deb'))) + + def test_snapshot_transient_transport_retries_original_url_then_succeeds(self): + self.write_plan(self.uri_line(uri=SECURITY + POOL)) + result = self.run_helper(archive='fail', snapshot='transient') + self.assertEqual(0, result.returncode, result.stderr) + self.assertEqual([f'https://archive.ubuntu.com/ubuntu/{POOL}|SHA256:{HASH}'] + + [f'{SECURITY}{POOL}|SHA256:{HASH}'] * 3, self.downloads()) + self.assertEqual(['1', '2'], self.sleeps()) + self.assertEqual(GOOD, (self.cache / POOL.rsplit('/', 1)[1]).read_bytes()) + self.assertIn('--no-download', self.apt_calls()[1]) + + def test_apt_failed_integrity_marker_stops_origin_even_with_nonzero_or_success_exit(self): + for mode in ('failed', 'failed-ok'): + with self.subTest(mode=mode): + result = self.run_helper(archive='fail', snapshot=mode) + self.assert_rejected(result) + self.assertEqual([f'https://archive.ubuntu.com/ubuntu/{POOL}|SHA256:{HASH}', + f'{SNAPSHOT}{POOL}|SHA256:{HASH}'], self.downloads()) + self.assertEqual([], self.sleeps()) + self.assertEqual(1, len(self.apt_calls())) + self.assertFalse(list(self.cache.glob('*.deb'))) + + def test_archive_apt_failed_integrity_marker_can_recover_from_original_snapshot(self): + result = self.run_helper(archive='failed') + self.assertEqual(0, result.returncode, result.stderr) + self.assertEqual([f'https://archive.ubuntu.com/ubuntu/{POOL}|SHA256:{HASH}', + f'{SNAPSHOT}{POOL}|SHA256:{HASH}'], self.downloads()) + self.assertEqual([], self.sleeps()) + self.assertEqual(GOOD, (self.cache / POOL.rsplit('/', 1)[1]).read_bytes()) def test_illegal_sources_and_paths_reject_before_any_download(self): invalid = ( @@ -249,6 +308,7 @@ def test_illegal_sources_and_paths_reject_before_any_download(self): result = self.run_helper() self.assert_rejected(result) self.assertEqual([], self.downloads()) + self.assertEqual([], self.sleeps()) def test_malformed_plan_hash_filename_or_size_rejects_before_download(self): invalid = ( diff --git a/ubuntu-security-packages.lock b/ubuntu-security-packages.lock index 39225969c0..dd0491af77 100644 --- a/ubuntu-security-packages.lock +++ b/ubuntu-security-packages.lock @@ -19,6 +19,9 @@ libexpat1=2.7.4-1ubuntu0.2 libcurl4t64=8.18.0-1ubuntu2.8 libgcrypt20=1.12.0-2ubuntu1.1 libmount1=2.41.3-3ubuntu2.2 +# PCRE2 is supplied by the pinned official USN-8974-1 packages (2026-11-03). +libpcre2-8-0=10.46-1ubuntu0.1 +libpcre2-posix3=10.46-1ubuntu0.1 libperl5.40=5.40.1-7ubuntu0.3 libsmartcols1=2.41.3-3ubuntu2.2 libssh2-1t64=1.11.1-1ubuntu0.26.04.4