From efa6e4acf2bb023411ee7ab213c4a84d346e2662 Mon Sep 17 00:00:00 2001 From: chen21019 <19357113+chen21019@users.noreply.github.com> Date: Thu, 5 Nov 2026 10:32:42 +0800 Subject: [PATCH] Add Host metadata and readable workloads with Catalog launch binding --- .github/workflows/security-release-gate.yml | 70 +- COMPATIBILITY.md | 33 + Dockerfile | 20 +- Dockerfile.dapper | 20 +- README.md | 15 +- code/framework/api-pub-sub-jetty/pom.xml | 2 +- code/framework/api-pub-sub/pom.xml | 2 +- code/framework/api/pom.xml | 2 +- code/framework/archaius/pom.xml | 2 +- code/framework/async/pom.xml | 2 +- code/framework/auditing/pom.xml | 2 +- .../iaas/api/auditing/AuditServiceImpl.java | 3 +- .../auditing/AiServiceUpgradeAuditTest.java | 9 +- code/framework/db-loader/pom.xml | 2 +- code/framework/deferred/pom.xml | 2 +- code/framework/encryption/pom.xml | 2 +- code/framework/engine/pom.xml | 2 +- code/framework/eventing/pom.xml | 2 +- code/framework/events/pom.xml | 2 +- code/framework/extension-spring/pom.xml | 2 +- code/framework/extension/pom.xml | 2 +- code/framework/java-server/pom.xml | 2 +- code/framework/jmx/pom.xml | 2 +- code/framework/jooq/pom.xml | 2 +- code/framework/json/pom.xml | 2 +- code/framework/launcher/pom.xml | 2 +- code/framework/lock/pom.xml | 2 +- code/framework/logback/pom.xml | 2 +- code/framework/managed-context/pom.xml | 2 +- code/framework/metrics/pom.xml | 2 +- code/framework/module/pom.xml | 2 +- code/framework/object/pom.xml | 2 +- .../object/impl/AbstractObjectManager.java | 15 +- code/framework/pool/pom.xml | 2 +- code/framework/resource-monitor/pom.xml | 2 +- code/framework/schema/pom.xml | 2 +- code/framework/server/pom.xml | 2 +- code/framework/spring/pom.xml | 2 +- code/framework/system-task/pom.xml | 2 +- code/framework/token/pom.xml | 2 +- code/framework/utils/pom.xml | 2 +- code/iaas/agent-instance/pom.xml | 2 +- code/iaas/agent-server/pom.xml | 2 +- code/iaas/agent/pom.xml | 2 +- code/iaas/allocator/pom.xml | 2 +- code/iaas/api-logic/pom.xml | 2 +- code/iaas/archaius-management/pom.xml | 2 +- code/iaas/auth-logic/pom.xml | 2 +- .../auth/ai/view/AiResourceViewService.java | 109 +- .../api/auth/change/AiHostMetadataChange.java | 28 + .../change/AiResourceChangeDescriptor.java | 1 + .../auth/change/AiResourceChangeDomain.java | 9 +- .../core/dao/impl/HostWorkloadDaoTest.java | 67 + .../core/dao/impl/UsageServiceDaoTest.java | 87 + .../auth/ai/view/AiHostWorkloadViewTest.java | 205 ++ .../ai/view/AiUsageServiceStackViewTest.java | 112 + .../auth/change/AiHostMetadataChangeTest.java | 201 ++ code/iaas/bootstrap/pom.xml | 2 +- code/iaas/config-item/api/pom.xml | 2 +- code/iaas/config-item/common/pom.xml | 2 +- code/iaas/config-item/server/pom.xml | 2 +- code/iaas/engine-jooq/pom.xml | 2 +- code/iaas/events/pom.xml | 2 +- code/iaas/external-handler/pom.xml | 2 +- code/iaas/ha/pom.xml | 2 +- code/iaas/healthcheck/pom.xml | 2 +- code/iaas/labels/pom.xml | 2 +- code/iaas/logic-common/pom.xml | 2 +- code/iaas/logic/pom.xml | 2 +- code/iaas/metadata/pom.xml | 2 +- code/iaas/model/pom.xml | 2 +- .../io/cattle/platform/core/dao/HostDao.java | 5 + .../cattle/platform/core/dao/ServiceDao.java | 5 + .../platform/core/dao/impl/HostDaoImpl.java | 27 + .../core/dao/impl/ServiceDaoImpl.java | 40 +- code/iaas/resource-pool/pom.xml | 2 +- code/iaas/service-discovery/api/pom.xml | 2 +- code/iaas/service-discovery/server/pom.xml | 2 +- code/iaas/ssh-common/pom.xml | 2 +- code/iaas/storage-service/pom.xml | 2 +- code/iaas/task-jooq/pom.xml | 2 +- code/implementation/activity-log/pom.xml | 2 +- .../agent-instance-impl/pom.xml | 2 +- code/implementation/docker/api/pom.xml | 2 +- code/implementation/docker/common/pom.xml | 2 +- code/implementation/docker/compute/pom.xml | 2 +- code/implementation/docker/machine/pom.xml | 2 +- code/implementation/docker/storage/pom.xml | 6 +- code/implementation/extension-api/pom.xml | 2 +- code/implementation/hazelcast/common/pom.xml | 2 +- .../implementation/hazelcast/eventing/pom.xml | 2 +- code/implementation/hazelcast/lock/pom.xml | 2 +- code/implementation/host-api/pom.xml | 2 +- code/implementation/host-stats/pom.xml | 2 +- code/implementation/register/pom.xml | 2 +- code/implementation/sample-setup/pom.xml | 2 +- code/implementation/settings-api/pom.xml | 2 +- .../simulator/agent-connection/pom.xml | 2 +- code/implementation/simulator/storage/pom.xml | 2 +- code/implementation/system-stack/pom.xml | 2 +- code/implementation/vm/pom.xml | 2 +- code/meta-parent/pom.xml | 2 +- code/packaging/app-config/pom.xml | 2 +- .../platform/app/SystemServicesConfig.java | 4 +- ...atalogLaunchObjectLifecycleWiringTest.java | 202 ++ .../TypedResourceAuditContractTest.java | 94 + code/packaging/app/pom.xml | 2 +- code/packaging/bundle/pom.xml | 2 +- code/packaging/dev/pom.xml | 2 +- code/packaging/meta/pom.xml | 2 +- code/parent/pom.xml | 2 +- .../orchestration-engine-0.183.370.md | 44 + pom.xml | 2 +- .../schema/base/aiResourceChangePlan.json | 2 +- resources/pom.xml | 2 +- scripts/api-key-operation-contract.json | 2244 +++++++++-------- scripts/api-key-operation-inventory.json | 509 ++-- .../api-key-operation-review-0.183.370.json | 119 + scripts/build | 2 +- scripts/check-apt-fixed-vex.py | 197 ++ scripts/check-cattle-jdk25-standalone | 2 +- scripts/check-pasturestack-source | 15 +- scripts/check-release-artifact | 2 +- scripts/test-apt-fixed-vex.py | 161 ++ scripts/test-apt-security-bootstrap.py | 122 + scripts/test-maven-distribution-download.py | 194 ++ security/ubuntu-apt-fixed.openvex.json | 24 + security/ubuntu-apt-fixed.sha256 | 19 + ubuntu-security-packages.lock | 3 + 129 files changed, 3823 insertions(+), 1399 deletions(-) create mode 100644 code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiHostMetadataChange.java create mode 100644 code/iaas/auth-logic/src/test/java/io/cattle/platform/core/dao/impl/HostWorkloadDaoTest.java create mode 100644 code/iaas/auth-logic/src/test/java/io/cattle/platform/core/dao/impl/UsageServiceDaoTest.java create mode 100644 code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/view/AiHostWorkloadViewTest.java create mode 100644 code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/view/AiUsageServiceStackViewTest.java create mode 100644 code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiHostMetadataChangeTest.java create mode 100644 code/packaging/app-config/src/test/java/io/cattle/platform/app/AiCatalogLaunchObjectLifecycleWiringTest.java create mode 100644 code/packaging/app-config/src/test/java/io/cattle/platform/iaas/api/auditing/TypedResourceAuditContractTest.java create mode 100644 docs/releases/orchestration-engine-0.183.370.md create mode 100644 scripts/api-key-operation-review-0.183.370.json create mode 100644 scripts/check-apt-fixed-vex.py create mode 100644 scripts/test-apt-fixed-vex.py create mode 100644 scripts/test-apt-security-bootstrap.py create mode 100644 scripts/test-maven-distribution-download.py create mode 100644 security/ubuntu-apt-fixed.openvex.json create mode 100644 security/ubuntu-apt-fixed.sha256 diff --git a/.github/workflows/security-release-gate.yml b/.github/workflows/security-release-gate.yml index 3595f61a4d..606dd4de42 100644 --- a/.github/workflows/security-release-gate.yml +++ b/.github/workflows/security-release-gate.yml @@ -266,6 +266,44 @@ jobs: 'nativeSqlBoundsRelationshipCandidatesBeforeMaterializationIncludingStoppedContainers', 'nativeSqlExcludesEveryForeignRemovedAndNonContainerRelationshipWithoutWrites', }, + 'io.cattle.platform.iaas.api.auth.change.AiHostMetadataChangeTest': { + 'onlyMetadataIntentAndFieldsAreAccepted', + 'nativePutUsesRealRevisionGuardAndRestoresContext', + 'nativeMethodFieldAndCrossProjectCeilingsRejectBeforeMutation', + 'durableSnapshotAndSummaryExcludeMaterialAndObserveExactMetadata', + 'boundPlanRequiresHumanApprovalAndExactOnceObservedCompletion', + 'casAndLiveWriteRevocationNeverClaimOrDispatch', + 'actualHostTableLockRequiresTransactionAndReloadsTheMetadataSnapshot', + }, + 'io.cattle.platform.iaas.api.auditing.TypedResourceAuditContractTest': { + 'everyRegisteredTypedDomainHasRealClosedMetadataAuditAllowAndDeny', + }, + 'io.cattle.platform.core.dao.impl.HostWorkloadDaoTest': { + 'boundsNativeHostMappingsBeforeMaterializationAndKeepsStoppedContainers', + 'bindsExactHostAndAllTenantOwnersAndRechecksMovedOrRemovedMappings', + }, + 'io.cattle.platform.core.dao.impl.UsageServiceDaoTest': { + 'nativeSemUsesTwoDistinctServiceSentinelAndBindsAllOwnersIncludingLegacyNullStack', + 'finalNativeQueryRequiresCapturedUniqueSemServiceStackAndOptionalExactHostMapping', + 'latePersistedStackAssignmentInvalidatesCapturedNullSemFallbackForHostAndVolume', + }, + 'io.cattle.platform.iaas.api.auth.ai.view.AiHostWorkloadViewTest': { + 'sameNameRunningAndStoppedChildrenUseFreshContainerAndStackOwnersWithoutDtoAugmentation', + 'boundUnknownSnapshotsExcludeMovedForeignRemovedAndUnreadableChildrenOrStackNames', + 'realScopedKeyNativePredicateHidesChildrenAndTheirAuditIdentitiesButDirectTargetStillDenies', + 'parentRbacAndKeyRevocationFailClosedBeforeRelationshipOutputAndRestoreContext', + 'cookieFullAndLegacyHostMetadataMatchesActualNativeWrappingAndHostsFilter', + }, + 'io.cattle.platform.iaas.api.auth.ai.view.AiUsageServiceStackViewTest': { + 'realNativeNullInstanceStackAndNoStackFieldUsesReadableSemServiceStackForHostAndVolume', + 'hiddenTransformedNonNullDirectOrMultipleSemRelationsNeverBypassReadableSchemaOrLeakNames', + 'exactCapturedSemAndStackRaceDropsFormerNameWhileChangedKeyAbortsTheWholeRead', + }, + 'io.cattle.platform.app.AiCatalogLaunchObjectLifecycleWiringTest': { + 'actualSpringFactoryLazilyBindsNativeStackBeforeCreateProcessScheduling', + 'emptyHandlersAndLegacyListOverrideRemainValidWithoutEagerDependencyCycles', + 'nativeCreateBindsExactlyOnceAndNestedFailureRestoresThreadLocalEnvelope', + }, } suites = [] observed = {} @@ -393,8 +431,38 @@ jobs: "$TRIVY_IMAGE" image \ --offline-scan --scanners vuln --list-all-pkgs --format json --output /work/evidence/dapper-image-vulnerabilities.raw.json \ "$DAPPER_IMAGE" - python3 tools/pip-security-overlay/filter-pip-vendor-findings.py \ + cp security/ubuntu-apt-fixed.openvex.json evidence/dapper-apt-fixed.openvex.json + cp security/ubuntu-apt-fixed.sha256 evidence/dapper-apt-official-elf.sha256 + docker run --rm --network none --entrypoint sha256sum "$DAPPER_IMAGE" \ + /usr/bin/apt /usr/bin/apt-cache /usr/bin/apt-cdrom /usr/bin/apt-config \ + /usr/bin/apt-get /usr/bin/apt-mark /usr/lib/apt/apt-extracttemplates /usr/lib/apt/apt-helper \ + /usr/lib/apt/methods/cdrom /usr/lib/apt/methods/copy /usr/lib/apt/methods/file /usr/lib/apt/methods/gpgv \ + /usr/lib/apt/methods/http /usr/lib/apt/methods/mirror /usr/lib/apt/methods/rred /usr/lib/apt/methods/store \ + /usr/lib/apt/solvers/dump /usr/lib/x86_64-linux-gnu/libapt-pkg.so.7.0.0 /usr/lib/x86_64-linux-gnu/libapt-private.so.0.0.0 > evidence/dapper-apt-installed-elf.sha256 + python3 scripts/check-apt-fixed-vex.py \ + --dpkg evidence/dapper-dpkg.tsv --inspect evidence/dapper-image-inspect.json \ --raw evidence/dapper-image-vulnerabilities.raw.json \ + --installed evidence/dapper-apt-installed-elf.sha256 \ + --vex evidence/dapper-apt-fixed.openvex.json --inputs-only + docker run --rm --network none \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$PWD:/work" -w /work \ + -v "$trivy_cache:/root/.cache/trivy" \ + "$TRIVY_IMAGE" image \ + --offline-scan --scanners vuln --list-all-pkgs \ + --skip-db-update --skip-java-db-update --skip-vex-repo-update \ + --vex /work/evidence/dapper-apt-fixed.openvex.json \ + --format json --output /work/evidence/dapper-image-vulnerabilities.apt-fixed.json \ + "$DAPPER_IMAGE" + python3 scripts/check-apt-fixed-vex.py \ + --dpkg evidence/dapper-dpkg.tsv --inspect evidence/dapper-image-inspect.json \ + --raw evidence/dapper-image-vulnerabilities.raw.json \ + --installed evidence/dapper-apt-installed-elf.sha256 \ + --vex evidence/dapper-apt-fixed.openvex.json \ + --native evidence/dapper-image-vulnerabilities.apt-fixed.json \ + --proof evidence/dapper-apt-fixed-vex-proof.json + python3 tools/pip-security-overlay/filter-pip-vendor-findings.py \ + --raw evidence/dapper-image-vulnerabilities.apt-fixed.json \ --proof evidence/dapper-pip-vendor-proof.json \ --output evidence/dapper-image-vulnerabilities.json \ --vex evidence/dapper-pip-vendor.vex.json \ diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 12614dbc1e..6239d605db 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -1,5 +1,38 @@ # Compatibility Contract +## Engine 0.183.370 candidate — not published + +Host metadata is limited to name/description through the existing native PUT +owner and human approval. Bounded Host workload references and the Volume +serviceIds-to-Stack fallback independently recheck current object, role and Key +visibility; ambiguous or moved relations remain unknown. The native Catalog +Stack lifecycle binding now runs before CREATE scheduling through lazy, +memoized generic-handler composition. Ordinary creates and output filters stay +unchanged. No Host lifecycle, labels, new tool or permission grant is added. + +The release registry preserves all 142 prior identities and adds 24 exact methods +(166 total). C03 binds 458 real source inputs and retains all 1396 tuples with +zero HTTP semantic, admission or native-role grant delta. The only typed plan +schema change adds Host to the existing resourceType enum. Existing sixteen +frozen v1 serialized schemas are byte-identical. Necessary full CI, artifact +qualification, matching Web 1.6.209 / Bridge 0.2.11, a new Server assembly and +native acceptance remain pending. See the [candidate note](docs/releases/orchestration-engine-0.183.370.md). + +Both builders download the same SHA512-pinned Maven 3.9.16 bytes from Apache's +official CDN, with the original Apache archive as a bounded transport fallback. +The existing TLS, checksum and installed-library checks remain; checksum failure +is terminal and does not select another origin. +The same pinned Guava JAR now uses HTTP-status-aware bounded curl downloads +instead of remote ADD, followed by the unchanged SHA256 check. +Both builders install the two exact Ubuntu USN-8996-1 APT 3.2.1ubuntu0.1 +packages before the first APT operation. Official package SHA256, metadata and +installed-version checks remain mandatory; release vulnerability gates are unchanged. +A narrow native OpenVEX fixed statement covers only these two exact Ubuntu +amd64 package PURLs while the scanner vendor data lags the official fix. CI +verifies all nineteen installed package-owned ELF hashes and the native package +pair on the same image, preserves the original raw report, and rejects any +unrelated finding removal. The existing pip overlay and release thresholds remain. + ## Engine 0.183.369 published Volume detail gains a read-only projection of at most fifty currently readable diff --git a/Dockerfile b/Dockerfile index 6c4b823b68..fb77146e79 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,6 +34,10 @@ ADD --checksum=sha256:bedeef1cf3268f90338c411cdb0a45b224d0660fd2240f310f380316c8 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 +# USN-8996-1: replace vulnerable APT before its first package operation. +ADD --checksum=sha256:0de729b8fe0e1c67c04e8e8d5bd6e0420dbd47f75b93f0b71c8e740edc2d5ffc https://security.ubuntu.com/ubuntu/pool/main/a/apt/apt_3.2.1ubuntu0.1_amd64.deb /tmp/apt.deb +ADD --checksum=sha256:94e5d41303265b66af0f8c70ade18bfb77260ad286edee6c8045faddef82b94c https://security.ubuntu.com/ubuntu/pool/main/a/apt/libapt-pkg7.0_3.2.1ubuntu0.1_amd64.deb /tmp/libapt-pkg7.0.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" ARG TEMURIN_JDK25_SHA256=68982529869257d0ee6b824ec5aa270ff8b118bb438670da0b97ebdab178d7fb @@ -52,6 +56,16 @@ LABEL org.opencontainers.image.source="https://github.com/PastureStack/orchestra org.opencontainers.image.licenses="Apache-2.0" RUN set -eux; \ + for package in apt libapt-pkg7.0; 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)" = 3.2.1ubuntu0.1; \ + test "$(dpkg-deb -f "/tmp/${package}.deb" Architecture)" = amd64; \ + done; \ + dpkg -i /tmp/libapt-pkg7.0.deb /tmp/apt.deb; \ + for package in apt libapt-pkg7.0; do \ + test "$(dpkg-query -W -f='${Version}' "${package}")" = 3.2.1ubuntu0.1; \ + done; \ . /licenses/ubuntu-apt.lock; \ rm -f /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; \ rm -rf /tmp/ca-bootstrap; \ @@ -134,7 +148,8 @@ RUN set -eux; \ rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/bin/pebble COPY --from=docker-cli-build /go/src/github.com/docker/cli/build/docker-linux-amd64 /usr/bin/docker -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 +RUN curl -fsSL --retry 5 --retry-all-errors --retry-delay 2 --connect-timeout 10 --max-time 300 -o /tmp/guava-33.7.2-jre.jar "https://repo.maven.apache.org/maven2/com/google/guava/guava/33.7.2-jre/guava-33.7.2-jre.jar" && \ + echo 'b530942257fb935f8b2cfaa5f8eb5bd59c555fd8e8d01b8ce98912e077ea606c /tmp/guava-33.7.2-jre.jar' | sha256sum -c - RUN mkdir -p ${JAVA_HOME} /usr/lib/jvm && \ curl -fsSL --retry 5 --retry-all-errors --retry-delay 2 --connect-timeout 10 --max-time 600 -o /tmp/temurin-jdk25.tar.gz "${TEMURIN_JDK25_URL}" && \ @@ -142,7 +157,8 @@ RUN mkdir -p ${JAVA_HOME} /usr/lib/jvm && \ tar -xzf /tmp/temurin-jdk25.tar.gz -C ${JAVA_HOME} --strip-components=1 && \ rm -f /tmp/temurin-jdk25.tar.gz && \ ln -sfn ${JAVA_HOME} /usr/lib/jvm/temurin-25-amd64 && \ - curl -fsSL --retry 5 --retry-all-errors --retry-delay 2 --connect-timeout 10 --max-time 300 -o /tmp/apache-maven.tar.gz "https://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz" && \ + (curl -fsSL --retry 5 --retry-all-errors --retry-delay 2 --connect-timeout 10 --max-time 300 -o /tmp/apache-maven.tar.gz "https://dlcdn.apache.org/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz" || \ + curl -fsSL --retry 5 --retry-all-errors --retry-delay 2 --connect-timeout 10 --max-time 300 -o /tmp/apache-maven.tar.gz "https://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz") && \ echo "${MAVEN_SHA512} /tmp/apache-maven.tar.gz" | sha512sum -c - && \ mkdir -p ${MAVEN_HOME} && \ tar -xzf /tmp/apache-maven.tar.gz -C ${MAVEN_HOME} --strip-components=1 && \ diff --git a/Dockerfile.dapper b/Dockerfile.dapper index 601c8e3a32..4c361adb8d 100644 --- a/Dockerfile.dapper +++ b/Dockerfile.dapper @@ -35,6 +35,10 @@ ADD --checksum=sha256:bedeef1cf3268f90338c411cdb0a45b224d0660fd2240f310f380316c8 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 +# USN-8996-1: replace vulnerable APT before its first package operation. +ADD --checksum=sha256:0de729b8fe0e1c67c04e8e8d5bd6e0420dbd47f75b93f0b71c8e740edc2d5ffc https://security.ubuntu.com/ubuntu/pool/main/a/apt/apt_3.2.1ubuntu0.1_amd64.deb /tmp/apt.deb +ADD --checksum=sha256:94e5d41303265b66af0f8c70ade18bfb77260ad286edee6c8045faddef82b94c https://security.ubuntu.com/ubuntu/pool/main/a/apt/libapt-pkg7.0_3.2.1ubuntu0.1_amd64.deb /tmp/libapt-pkg7.0.deb + ENV DEBIAN_FRONTEND=noninteractive ENV LANG=C.UTF-8 LC_ALL=C.UTF-8 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" @@ -70,6 +74,16 @@ ENV PGUSER=cattle ENV PGDATABASE=cattle RUN set -eux; \ + for package in apt libapt-pkg7.0; 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)" = 3.2.1ubuntu0.1; \ + test "$(dpkg-deb -f "/tmp/${package}.deb" Architecture)" = amd64; \ + done; \ + dpkg -i /tmp/libapt-pkg7.0.deb /tmp/apt.deb; \ + for package in apt libapt-pkg7.0; do \ + test "$(dpkg-query -W -f='${Version}' "${package}")" = 3.2.1ubuntu0.1; \ + done; \ . /licenses/ubuntu-apt.lock; \ rm -f /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; \ rm -rf /tmp/ca-bootstrap; \ @@ -152,7 +166,8 @@ RUN set -eux; \ rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/bin/pebble COPY --from=docker-cli-build /go/src/github.com/docker/cli/build/docker-linux-amd64 /usr/bin/docker -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 +RUN curl -fsSL --retry 5 --retry-all-errors --retry-delay 2 --connect-timeout 10 --max-time 300 -o /tmp/guava-33.7.2-jre.jar "https://repo.maven.apache.org/maven2/com/google/guava/guava/33.7.2-jre/guava-33.7.2-jre.jar" && \ + echo 'b530942257fb935f8b2cfaa5f8eb5bd59c555fd8e8d01b8ce98912e077ea606c /tmp/guava-33.7.2-jre.jar' | sha256sum -c - RUN mkdir -p ${JAVA_HOME} /usr/lib/jvm && \ curl -fsSL --retry 5 --retry-all-errors --retry-delay 2 --connect-timeout 10 --max-time 600 -o /tmp/temurin-jdk25.tar.gz "${TEMURIN_JDK25_URL}" && \ @@ -160,7 +175,8 @@ RUN mkdir -p ${JAVA_HOME} /usr/lib/jvm && \ tar -xzf /tmp/temurin-jdk25.tar.gz -C ${JAVA_HOME} --strip-components=1 && \ rm -f /tmp/temurin-jdk25.tar.gz && \ ln -sfn ${JAVA_HOME} /usr/lib/jvm/temurin-25-amd64 && \ - curl -fsSL --retry 5 --retry-all-errors --retry-delay 2 --connect-timeout 10 --max-time 300 -o /tmp/apache-maven.tar.gz "https://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz" && \ + (curl -fsSL --retry 5 --retry-all-errors --retry-delay 2 --connect-timeout 10 --max-time 300 -o /tmp/apache-maven.tar.gz "https://dlcdn.apache.org/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz" || \ + curl -fsSL --retry 5 --retry-all-errors --retry-delay 2 --connect-timeout 10 --max-time 300 -o /tmp/apache-maven.tar.gz "https://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz") && \ echo "${MAVEN_SHA512} /tmp/apache-maven.tar.gz" | sha512sum -c - && \ mkdir -p ${MAVEN_HOME} && \ tar -xzf /tmp/apache-maven.tar.gz -C ${MAVEN_HOME} --strip-components=1 && \ diff --git a/README.md b/README.md index 3f76b8af92..a40ad69720 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,12 @@ 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. +## Next candidate: Engine 0.183.370 + +Host name/description review, bounded readable workload references and the +native Catalog launch binding fix are in [source/CI preparation](docs/releases/orchestration-engine-0.183.370.md). +Publication and native acceptance remain pending; use the published release below. + ## Latest published release: Engine 0.183.369 [Engine 0.183.369](https://github.com/PastureStack/orchestration-engine/releases/tag/v0.183.369) @@ -30,10 +36,9 @@ Original CI, source/artifact qualification, immutable release verification and public checksums are recorded in the [published release note](docs/releases/orchestration-engine-0.183.369.md). Existing lower security findings remain disclosed; publication is not a claim -of zero vulnerabilities. Server 1.6.567 still includes Engine 0.183.368. -Matching Web 1.6.208 / bridge0.2.10, a new Server assembly and designated native -Volume/reference/role/Key/layout acceptance remain separate. Full frontend -coverage is in progress. +of zero vulnerabilities. Server 1.6.568 includes Engine 0.183.369 with Web +1.6.208 / Bridge 0.2.10. Component publication does not complete all native +business acceptance or the 243 frontend groups. Four independent AI opt-ins remain default-off. The [release note](docs/releases/orchestration-engine-0.183.368.md) mirrors the immutable @@ -179,7 +184,7 @@ Before publishing an Engine artifact, run the complete package gate: ```sh bash scripts/check-cattle-jdk25-full-package -ENGINE_VERSION=0.183.369 bash scripts/build --release +ENGINE_VERSION=0.183.370 bash scripts/build --release bash scripts/check-release-artifact dist/artifacts/cattle.jar ``` diff --git a/code/framework/api-pub-sub-jetty/pom.xml b/code/framework/api-pub-sub-jetty/pom.xml index ef32e38c60..ca5a0d5a69 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.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/api-pub-sub/pom.xml b/code/framework/api-pub-sub/pom.xml index 882100e841..e97f965522 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.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/api/pom.xml b/code/framework/api/pom.xml index 42bb78aff3..de7f78f7db 100644 --- a/code/framework/api/pom.xml +++ b/code/framework/api/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/archaius/pom.xml b/code/framework/archaius/pom.xml index 6292c7f365..69a015e5c3 100644 --- a/code/framework/archaius/pom.xml +++ b/code/framework/archaius/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-meta-parent - 0.183.369 + 0.183.370 ../../meta-parent/pom.xml diff --git a/code/framework/async/pom.xml b/code/framework/async/pom.xml index 6154f76eb3..16be4ad8d2 100644 --- a/code/framework/async/pom.xml +++ b/code/framework/async/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/auditing/pom.xml b/code/framework/auditing/pom.xml index a35a6c3406..d28991f238 100644 --- a/code/framework/auditing/pom.xml +++ b/code/framework/auditing/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/auditing/src/main/java/io/cattle/platform/iaas/api/auditing/AuditServiceImpl.java b/code/framework/auditing/src/main/java/io/cattle/platform/iaas/api/auditing/AuditServiceImpl.java index 1b214d53ad..8baf734b46 100644 --- a/code/framework/auditing/src/main/java/io/cattle/platform/iaas/api/auditing/AuditServiceImpl.java +++ b/code/framework/auditing/src/main/java/io/cattle/platform/iaas/api/auditing/AuditServiceImpl.java @@ -374,7 +374,8 @@ private void recordServiceDecision(ApiRequest request, Policy policy, String act @Override public void recordResourceChangeDecision(ApiRequest request, Policy policy, String action, String receiptId, String resourceType, Long resourceId, boolean allowed, String reason) { - if (!Set.of("stack", "service", "container", "externalService", "dnsService", "certificate", "aiResourceChangePlan").contains(resourceType)) + if (!Set.of("stack", "service", "container", "externalService", "dnsService", "certificate", "host", "aiResourceChangePlan") + .contains(resourceType)) throw new IllegalArgumentException("Invalid resource-change audit target"); recordTypedResourceDecision(request, policy, action, receiptId, "aichange", resourceType, resourceId, allowed, reason, "api.aiResourceChangePlan." + action, "ResourceChangeDenied"); diff --git a/code/framework/auditing/src/test/java/io/cattle/platform/iaas/api/auditing/AiServiceUpgradeAuditTest.java b/code/framework/auditing/src/test/java/io/cattle/platform/iaas/api/auditing/AiServiceUpgradeAuditTest.java index a95dc4e529..fb457757dc 100644 --- a/code/framework/auditing/src/test/java/io/cattle/platform/iaas/api/auditing/AiServiceUpgradeAuditTest.java +++ b/code/framework/auditing/src/test/java/io/cattle/platform/iaas/api/auditing/AiServiceUpgradeAuditTest.java @@ -109,11 +109,12 @@ public class AiServiceUpgradeAuditTest { AuditServiceImpl service = service(delivered, envelopes); ApiRequest request = new ApiRequest(null, null); request.setType("aiResourceChangePlan"); request.setMethod("POST"); request.setRequestObject(Map.of("hostname", "PRIVATE-HOST", "externalIpAddresses", List.of("PRIVATE-IP"), - "certificate", "PRIVATE-PEM", "key", "PRIVATE-KEY", "targetServices", List.of("PRIVATE-TARGET"))); + "certificate", "PRIVATE-PEM", "key", "PRIVATE-KEY", "targetServices", List.of("PRIVATE-TARGET"), + "name", "PRIVATE-NAME", "description", "PRIVATE-DESCRIPTION")); request.setAttribute("apiKey.audit.keyId", "1c8"); request.setAttribute("apiKey.audit.policyRevision", 3L); request.setAttribute("apiKey.audit.verifiedPrincipalAccountId", 42L); String receipt = "aichange-11111111-1111-1111-1111-111111111111"; - for (String type : List.of("externalService", "dnsService", "certificate")) { + for (String type : List.of("externalService", "dnsService", "certificate", "host")) { for (boolean allowed : List.of(true, false)) { service.recordResourceChangeDecision(request, policy(), "preview", receipt, type, 10L, allowed, allowed ? "ResourceChangePreviewCreated" : "KeyScopeDenied"); @@ -128,12 +129,12 @@ public class AiServiceUpgradeAuditTest { assertFalse(data.toString().contains("PRIVATE")); assertFalse(data.containsKey("targetServices")); } } - assertEquals(6, delivered.size()); + assertEquals(8, delivered.size()); for (String unsupported : List.of("unregistered", "ExternalService", "secret", "certificate/key")) { assertThrows(IllegalArgumentException.class, () -> service.recordResourceChangeDecision(request, policy(), "preview", receipt, unsupported, 10L, true, "ResourceChangePreviewCreated")); } - assertEquals(6, delivered.size()); + assertEquals(8, delivered.size()); } @Test public void bothAllowAndDenyUseDurableMetadataOnlyAudit() throws Exception { diff --git a/code/framework/db-loader/pom.xml b/code/framework/db-loader/pom.xml index 418945bb62..3cb1bac916 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.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/deferred/pom.xml b/code/framework/deferred/pom.xml index f431676b79..e774b792fe 100644 --- a/code/framework/deferred/pom.xml +++ b/code/framework/deferred/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/encryption/pom.xml b/code/framework/encryption/pom.xml index 1083826ad1..85ab9bd9a9 100644 --- a/code/framework/encryption/pom.xml +++ b/code/framework/encryption/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/engine/pom.xml b/code/framework/engine/pom.xml index cd716e1534..018b5c480d 100644 --- a/code/framework/engine/pom.xml +++ b/code/framework/engine/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/eventing/pom.xml b/code/framework/eventing/pom.xml index 0ebbcf801c..6cc25af202 100644 --- a/code/framework/eventing/pom.xml +++ b/code/framework/eventing/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/events/pom.xml b/code/framework/events/pom.xml index 740483596a..6611570858 100644 --- a/code/framework/events/pom.xml +++ b/code/framework/events/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/extension-spring/pom.xml b/code/framework/extension-spring/pom.xml index fc390c500a..64965df28e 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.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/extension/pom.xml b/code/framework/extension/pom.xml index af2460c68e..fc55059518 100644 --- a/code/framework/extension/pom.xml +++ b/code/framework/extension/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/java-server/pom.xml b/code/framework/java-server/pom.xml index 34fdb69cbf..1868e1655b 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.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/jmx/pom.xml b/code/framework/jmx/pom.xml index 1845ba2261..90493aa6b7 100644 --- a/code/framework/jmx/pom.xml +++ b/code/framework/jmx/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/jooq/pom.xml b/code/framework/jooq/pom.xml index eb18fb5776..4d22214a24 100644 --- a/code/framework/jooq/pom.xml +++ b/code/framework/jooq/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/json/pom.xml b/code/framework/json/pom.xml index 0ceb07876b..ac79a49611 100644 --- a/code/framework/json/pom.xml +++ b/code/framework/json/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/launcher/pom.xml b/code/framework/launcher/pom.xml index 55dc54b965..8c0694f9db 100644 --- a/code/framework/launcher/pom.xml +++ b/code/framework/launcher/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/lock/pom.xml b/code/framework/lock/pom.xml index db0e5f3ab7..b2500aa5d4 100644 --- a/code/framework/lock/pom.xml +++ b/code/framework/lock/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/logback/pom.xml b/code/framework/logback/pom.xml index 7b43645b15..f21f68f7f0 100644 --- a/code/framework/logback/pom.xml +++ b/code/framework/logback/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-meta-parent - 0.183.369 + 0.183.370 ../../meta-parent/pom.xml diff --git a/code/framework/managed-context/pom.xml b/code/framework/managed-context/pom.xml index 0d71500f99..845af59bd4 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.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/metrics/pom.xml b/code/framework/metrics/pom.xml index e12bccccb7..3ca8e59fee 100644 --- a/code/framework/metrics/pom.xml +++ b/code/framework/metrics/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/module/pom.xml b/code/framework/module/pom.xml index fad83c50d0..027bb42925 100644 --- a/code/framework/module/pom.xml +++ b/code/framework/module/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/object/pom.xml b/code/framework/object/pom.xml index fe56c00d63..6ffd58cbd6 100644 --- a/code/framework/object/pom.xml +++ b/code/framework/object/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/object/src/main/java/io/cattle/platform/object/impl/AbstractObjectManager.java b/code/framework/object/src/main/java/io/cattle/platform/object/impl/AbstractObjectManager.java index ffd11ab96c..ad3a863b25 100644 --- a/code/framework/object/src/main/java/io/cattle/platform/object/impl/AbstractObjectManager.java +++ b/code/framework/object/src/main/java/io/cattle/platform/object/impl/AbstractObjectManager.java @@ -26,6 +26,7 @@ public abstract class AbstractObjectManager implements ObjectManager { SchemaFactory schemaFactory; List postInitHandlers; List lifeCycleHandlers; + private java.util.function.Supplier> lifeCycleHandlerSupplier; ObjectMetaDataManager metaDataManager; @Override @@ -82,11 +83,12 @@ protected T construct(Class clz, Map properties) { } protected T callLifeCycleHandlers(LifeCycleEvent event, T instance, Class clz, Map properties) { - if (lifeCycleHandlers == null) { + List handlers = getLifeCycleHandlers(); + if (handlers == null) { return instance; } - for (ObjectLifeCycleHandler handler : lifeCycleHandlers) { + for (ObjectLifeCycleHandler handler : handlers) { instance = handler.onEvent(event, instance, clz, properties); } return instance; @@ -240,11 +242,18 @@ public void setPostInitHandlers(List postInitHan } public List getLifeCycleHandlers() { - return lifeCycleHandlers; + return lifeCycleHandlerSupplier == null ? lifeCycleHandlers : lifeCycleHandlerSupplier.get(); } + /** An explicit legacy list (including null) replaces any lazy supplier. */ public void setLifeCycleHandlers(List lifeCycleHandlers) { this.lifeCycleHandlers = lifeCycleHandlers; + this.lifeCycleHandlerSupplier = null; + } + + /** Replaces the legacy list on use, after dependent beans can finish construction. */ + public void setLifeCycleHandlerSupplier(java.util.function.Supplier> supplier) { + this.lifeCycleHandlerSupplier = java.util.Objects.requireNonNull(supplier); } public ObjectMetaDataManager getMetaDataManager() { diff --git a/code/framework/pool/pom.xml b/code/framework/pool/pom.xml index 4f444e77c1..8a56ad90ae 100644 --- a/code/framework/pool/pom.xml +++ b/code/framework/pool/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/resource-monitor/pom.xml b/code/framework/resource-monitor/pom.xml index 15cd336924..e1251af43e 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.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/schema/pom.xml b/code/framework/schema/pom.xml index 94044d2a1d..764f3fbba5 100644 --- a/code/framework/schema/pom.xml +++ b/code/framework/schema/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/server/pom.xml b/code/framework/server/pom.xml index bf9b5a1de8..83948d75a9 100644 --- a/code/framework/server/pom.xml +++ b/code/framework/server/pom.xml @@ -4,7 +4,7 @@ cattle-meta-parent io.cattle - 0.183.369 + 0.183.370 ../../meta-parent/pom.xml diff --git a/code/framework/spring/pom.xml b/code/framework/spring/pom.xml index a55aac8a8e..1721c6b2cd 100644 --- a/code/framework/spring/pom.xml +++ b/code/framework/spring/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/system-task/pom.xml b/code/framework/system-task/pom.xml index 478a735931..d95516883f 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.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/token/pom.xml b/code/framework/token/pom.xml index 18df61d5e3..b6f496036d 100644 --- a/code/framework/token/pom.xml +++ b/code/framework/token/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/framework/utils/pom.xml b/code/framework/utils/pom.xml index 58ea1e8635..c2aedff318 100644 --- a/code/framework/utils/pom.xml +++ b/code/framework/utils/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/iaas/agent-instance/pom.xml b/code/iaas/agent-instance/pom.xml index e2bc11401b..8c360e8430 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.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/iaas/agent-server/pom.xml b/code/iaas/agent-server/pom.xml index bc333c81ab..22e1b768cb 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.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/iaas/agent/pom.xml b/code/iaas/agent/pom.xml index d68113ba2e..90b1147525 100644 --- a/code/iaas/agent/pom.xml +++ b/code/iaas/agent/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/iaas/allocator/pom.xml b/code/iaas/allocator/pom.xml index e3e32d0e57..d678223c6a 100644 --- a/code/iaas/allocator/pom.xml +++ b/code/iaas/allocator/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/iaas/api-logic/pom.xml b/code/iaas/api-logic/pom.xml index 9b7c54189c..48c15b90c3 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.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/iaas/archaius-management/pom.xml b/code/iaas/archaius-management/pom.xml index 996d640041..0cd7129c8a 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.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/iaas/auth-logic/pom.xml b/code/iaas/auth-logic/pom.xml index 189d0951ef..5995405f27 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.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/view/AiResourceViewService.java b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/view/AiResourceViewService.java index 2d3f85be53..759587cd9b 100644 --- a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/view/AiResourceViewService.java +++ b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/view/AiResourceViewService.java @@ -8,6 +8,11 @@ import io.cattle.platform.iaas.api.auth.apikey.ApiKeyAiBridgeGuard; import io.cattle.platform.core.model.Account; import io.cattle.platform.core.dao.VolumeDao; +import io.cattle.platform.core.dao.HostDao; +import io.cattle.platform.core.dao.ServiceDao; +import io.cattle.platform.core.model.Host; +import io.cattle.platform.core.model.InstanceHostMap; +import io.cattle.platform.core.model.Service; import io.cattle.platform.core.model.Mount; import io.cattle.platform.core.model.Volume; import io.cattle.platform.core.model.Instance; @@ -43,6 +48,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.HexFormat; +import org.apache.commons.lang3.StringUtils; /** * Metadata is projected on the server before it reaches a dedicated AI Key. @@ -71,7 +77,8 @@ public class AiResourceViewService { Map.entry("dnsService", List.of("stackId", "archived")), Map.entry("externalService", List.of("stackId", "archived")), Map.entry("container", List.of("imageUuid", "memory", "cpuShares", "startCount", "networkMode")), Map.entry("virtualMachine", List.of("imageUuid", "memory", "cpuShares")), - Map.entry("host", List.of("hostname", "agentIpAddress", "dockerVersion", "memTotal", "cpuCount")), + Map.entry("host", List.of("hostname", "agentIpAddress", "dockerVersion", "memTotal", "cpuCount", + "agentState", "memory", "milliCpu", "localStorageMb")), Map.entry("volume", List.of("driver", "accessMode", "size")), Map.entry("certificate", List.of("cn", "expiresAt", "issuedAt")), Map.entry("registry", List.of()), Map.entry("registryCredential", List.of()), @@ -91,6 +98,10 @@ public class AiResourceViewService { @Inject ObjectManager objects; @Inject io.cattle.platform.iaas.api.auth.change.AiResourceChangeDomain resourceChanges; @Inject VolumeDao volumes; + @Inject HostDao hosts; + @Inject ServiceDao services; + + private record AuthorizedTarget(Resource resource, boolean serviceIdsReadable, boolean missingPersistedStack, String instanceUuid) { } public Map read(ApiRequest request) { Map input = input(request); @@ -181,7 +192,7 @@ protected Map fetch(ApiRequest domain, Map input } List> visible = new ArrayList<>(); for (Object row : rows) { - Resource resource = "volume".equals(type) && domain.getId() != null + Resource resource = Set.of("volume", "host").contains(type) && domain.getId() != null ? coreDetail(row, domain) : manager.convertResponse(row, domain); if (resource == null) throw error(503, "ResourceViewUnavailable"); String id = "processPool".equals(type) ? poolId(resource.getFields()) : resource.getId(); @@ -193,6 +204,7 @@ protected Map fetch(ApiRequest domain, Map input if (resource.getFields().containsKey("removed")) safe.put("removed", resource.getFields().get("removed")); volumeUsage(domain, id, safe); } + if ("host".equals(type) && domain.getId() != null) hostWorkloads(domain, id, safe); if ("dnsService".equals(type)) safe.putAll(resourceChanges.aliasViewTargets(domain, id, domain.getId() != null)); if ("processPool".equals(type) && (!(safe.get("name") instanceof String name) || name.isBlank())) safe.put("name", "Process pool " + id.substring(5, 17)); @@ -291,7 +303,8 @@ private void volumeUsage(ApiRequest domain, String id, Map safe) for (Mount mount : mounts.subList(0, Math.min(50, mounts.size()))) { if (mount == null || !Long.valueOf(volumeId).equals(mount.getVolumeId()) || mount.getInstanceId() == null || mount.getRemoved() != null) continue; - Resource target = usageTarget(domain, "container", mount.getInstanceId().toString()); + AuthorizedTarget child = usageTarget(domain, "container", mount.getInstanceId().toString()); + Resource target = child == null ? null : child.resource(); if (target == null || !"container".equals(target.getType()) || !(target.getFields().get("name") instanceof String) || !(target.getFields().get("state") instanceof String)) continue; String path = mount.getPath(); @@ -303,20 +316,67 @@ private void volumeUsage(ApiRequest domain, String id, Map safe) scalar(reference, target.getFields(), "name"); scalar(reference, target.getFields(), "state"); reference.put("mountPath", path); reference.put("readOnly", "ro".equals(mount.getPermissions())); reference.put("stackResolution", "unknown"); - // The native container owns its persisted Stack reference. Never - // infer names from labels or a cross-scope relationship SQL join. - Object stackId = target.getFields().get("stackId"); - if (stackId instanceof String || stackId instanceof Number) { - Resource stack = usageTarget(domain, "stack", stackId.toString()); - if (stack != null && stack.getFields().get("name") instanceof String name && !name.isBlank() && name.length() <= 512) { - reference.put("stackName", name); reference.put("stackResolution", "resolved"); - } - } + usageStack(domain, child, reference, null, null); references.add(reference); } } - private Resource usageTarget(ApiRequest domain, String type, String id) { + private void hostWorkloads(ApiRequest domain, String id, Map safe) { + safe.put("workloadReferencesStatus", "unknown"); + List> references = new ArrayList<>(); + safe.put("workloadReferences", references); + if (hosts == null) return; + long hostId = Long.parseLong(rawId(id).toString()), account = ApiUtils.getPolicy().getAccountId(); + if (hostId <= 0 || !sameId(domain.getId(), id)) throw error(503, "ResourceViewUnavailable"); + List candidates = hosts.getWorkloadCandidates(account, hostId); + if (candidates == null || candidates.size() > 51) throw error(503, "ResourceViewUnavailable"); + Set seen = new LinkedHashSet<>(); + for (InstanceHostMap candidate : candidates.subList(0, Math.min(50, candidates.size()))) { + if (candidate == null || candidate.getId() == null || candidate.getId() <= 0 + || !Long.valueOf(hostId).equals(candidate.getHostId()) || candidate.getInstanceId() == null + || candidate.getInstanceId() <= 0 || candidate.getRemoved() != null || !seen.add(candidate.getInstanceId())) continue; + AuthorizedTarget child = usageTarget(domain, "container", candidate.getInstanceId().toString()); + Resource target = child == null ? null : child.resource(); + if (target == null || !(target.getFields().get("name") instanceof String) + || !(target.getFields().get("state") instanceof String)) continue; + Map reference = new LinkedHashMap<>(); + reference.put("containerId", target.getId()); + scalar(reference, target.getFields(), "name"); scalar(reference, target.getFields(), "state"); + reference.put("stackResolution", "unknown"); + boolean resolved = usageStack(domain, child, reference, hostId, candidate.getId()); + if (resolved || hosts.isWorkloadCurrent(account, hostId, candidate.getId(), candidate.getInstanceId(), null)) references.add(reference); + } + } + + private boolean usageStack(ApiRequest domain, AuthorizedTarget child, Map reference, Long hostId, Long hostMappingId) { + Resource target = child.resource(); Object stackId = target.getFields().get("stackId"); + Long serviceId = null; long account = ApiUtils.getPolicy().getAccountId(); + long instanceId = Long.parseLong(rawId(target.getId()).toString()); + if (stackId == null) { + // Native service containers commonly have no persisted Stack. Use + // only the same visible serviceIds relationship as the normal UI. + if (!child.missingPersistedStack() || !child.serviceIdsReadable() || services == null + || child.instanceUuid() == null || !child.instanceUuid().matches("[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}")) + return false; + List ids = services.getUsageServiceIds(account, instanceId, child.instanceUuid()); + if (ids == null || ids.size() > 2) throw error(503, "ResourceViewUnavailable"); + if (ids.size() != 1 || ids.getFirst() == null || ids.getFirst() <= 0) return false; + serviceId = ids.getFirst(); AuthorizedTarget service = usageTarget(domain, "service", serviceId.toString()); + if (service == null) return false; + stackId = service.resource().getFields().get("stackId"); + } + if (!(stackId instanceof String || stackId instanceof Number)) return false; + long capturedStack = Long.parseLong(rawId(stackId.toString()).toString()); + AuthorizedTarget stack = usageTarget(domain, "stack", stackId.toString()); + if (stack == null || !(stack.resource().getFields().get("name") instanceof String name) || name.isBlank() || name.length() > 512) + return false; + if (serviceId != null) { + if (!services.isUsageServiceCurrent(account, instanceId, child.instanceUuid(), serviceId, capturedStack, hostId, hostMappingId)) return false; + } else if (hostId != null && !hosts.isWorkloadCurrent(account, hostId, hostMappingId, instanceId, capturedStack)) return false; + reference.put("stackName", name); reference.put("stackResolution", "resolved"); return true; + } + + private AuthorizedTarget usageTarget(ApiRequest domain, String type, String id) { ApiRequest child = new ApiRequest(domain); child.setRequestServletContext(domain.getServletContext()); child.setType(type); child.setId(rawId(id).toString()); child.setMethod("GET"); @@ -343,10 +403,15 @@ private Resource usageTarget(ApiRequest domain, String type, String id) { if (key != null && key.restricted() && !manager.supportsScopedCollectionQuery(keyAccess)) return null; Object row = manager.getById(type, child.getId(), new ListOptions(child)); if (row == null) return null; + if ("service".equals(type) && row instanceof Service service && !"service".equals(service.getKind())) return null; Resource resource = coreDetail(row, child); if (resource == null || resource.getId() == null || !sameId(resource.getId(), id) || resource.getFields().get("removed") != null) return null; - return resource; + var relation = schema.getResourceFields() == null ? null : schema.getResourceFields().get("serviceIds"); + boolean readable = relation != null && relation.isIncludeInList() && !relation.isReadOnCreateOnly() + && (relation.getTransform() == null || relation.getTransform().isBlank()); + return new AuthorizedTarget(resource, "container".equals(type) && readable, + row instanceof Instance instance && instance.getStackId() == null, row instanceof Instance instance ? instance.getUuid() : null); } catch (ClientVisibleException denied) { if (!Set.of("KeyScopeDenied", "KeyPolicyDenied", "OwnerPermissionDenied", "NotFound").contains(denied.getCode())) throw denied; return null; @@ -360,10 +425,14 @@ private Resource coreDetail(Object row, ApiRequest request) { Long id, account; java.util.Date removed; if ("volume".equals(request.getType()) && row instanceof Volume volume) { id = volume.getId(); account = volume.getAccountId(); removed = volume.getRemoved(); + } else if ("host".equals(request.getType()) && row instanceof Host host) { + id = host.getId(); account = host.getAccountId(); removed = host.getRemoved(); } else if ("container".equals(request.getType()) && row instanceof Instance instance && "container".equals(instance.getKind())) { id = instance.getId(); account = instance.getAccountId(); removed = instance.getRemoved(); } else if ("stack".equals(request.getType()) && row instanceof Stack stack) { id = stack.getId(); account = stack.getAccountId(); removed = stack.getRemoved(); + } else if ("service".equals(request.getType()) && row instanceof Service service && "service".equals(service.getKind())) { + id = service.getId(); account = service.getAccountId(); removed = service.getRemoved(); } else throw error(503, "ResourceViewUnavailable"); if (id == null || !sameId(id, request.getId()) || account == null || account != ApiUtils.getPolicy().getAccountId() || removed != null) throw error(404, "NotFound"); @@ -372,7 +441,10 @@ private Resource coreDetail(Object row, ApiRequest request) { throw error(403, "OwnerPermissionDenied"); List names = "volume".equals(request.getType()) ? List.of("name", "description", "state", "kind", "created", "removed", "driver", "accessMode", "size", "sizeMb", "isHostPath", "isNative") - : "container".equals(request.getType()) ? List.of("name", "state", "stackId") : List.of("name"); + : "host".equals(request.getType()) ? List.of("name", "description", "state", "kind", "created", "hostname", + "agentIpAddress", "dockerVersion", "memTotal", "cpuCount", "agentState", "memory", "milliCpu", "localStorageMb") + : "container".equals(request.getType()) ? List.of("name", "state", "stackId") + : "service".equals(request.getType()) ? List.of("stackId") : List.of("name"); // Reuse the exact role-schema Field accessors and native wrapping rules // (including aliases and reference formatting), // with a bounded field subset. No full DTO/output augmentation runs. @@ -396,8 +468,15 @@ private Resource coreDetail(Object row, ApiRequest request) { } Resource wrapped = new WrappedResource(ApiContext.getContext().getIdFormatter(), request.getSchemaFactory(), projection, row, additional, Set.of(), "GET", false); + // Preserve the ordinary HostsFilter state semantics without its unbounded + // instanceIds/physical-host augmentation or bypassing a hidden state field. + if (!"v1".equals(request.getVersion()) && row instanceof Host host && wrapped.getFields().containsKey("state") + && "active".equals(host.getState()) && StringUtils.isNotBlank(host.getAgentState())) + wrapped.getFields().put("state", host.getAgentState()); if (row instanceof Instance instance && (!sameId(wrapped.getFields().get("stackId"), instance.getStackId()))) wrapped.getFields().remove("stackId"); + if (row instanceof Service service && !sameId(wrapped.getFields().get("stackId"), service.getStackId())) + wrapped.getFields().remove("stackId"); return wrapped; } diff --git a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiHostMetadataChange.java b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiHostMetadataChange.java new file mode 100644 index 0000000000..a04c2aede3 --- /dev/null +++ b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiHostMetadataChange.java @@ -0,0 +1,28 @@ +package io.cattle.platform.iaas.api.auth.change; + +import io.cattle.platform.core.model.Host; +import io.cattle.platform.object.util.DataAccessor; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +/** Name/description only: agent configuration, credentials and host info never enter the plan. */ +public final class AiHostMetadataChange { + private AiHostMetadataChange() { } + + public static Map snapshot(Host host) { + Map result = new LinkedHashMap<>(); + result.put("id", host.getId()); result.put("uuid", host.getUuid()); + result.put("accountId", host.getAccountId()); result.put("kind", host.getKind()); + result.put("name", host.getName()); result.put("description", host.getDescription()); + result.put("state", host.getState()); result.put("removed", host.getRemoved()); + result.put("hostname", DataAccessor.fieldString(host, "hostname")); + return result; + } + + public static String displayName(Host host) { + String name = host.getName(), hostname = DataAccessor.fieldString(host, "hostname"); + if (name == null || name.isBlank()) return Objects.toString(hostname, ""); + return hostname == null || hostname.isBlank() || name.equals(hostname) ? name : hostname + " (" + name + ")"; + } +} diff --git a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDescriptor.java b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDescriptor.java index 9d29330f5b..7975d7da1a 100644 --- a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDescriptor.java +++ b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDescriptor.java @@ -51,6 +51,7 @@ private static Map descriptors() { : "update_targets".equals(operation) ? Set.of("serviceIds") : Set.of()); } add(result, "certificate", "update_metadata", "PUT", null, "update", Set.of("name", "description")); + add(result, "host", "update_metadata", "PUT", null, "update", Set.of("name", "description")); return Map.copyOf(result); } diff --git a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDomain.java b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDomain.java index 1e16881763..68613d3065 100644 --- a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDomain.java +++ b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDomain.java @@ -1,6 +1,7 @@ package io.cattle.platform.iaas.api.auth.change; import static io.cattle.platform.core.model.tables.InstanceTable.INSTANCE; +import static io.cattle.platform.core.model.tables.HostTable.HOST; import static io.cattle.platform.core.model.tables.CertificateTable.CERTIFICATE; import static io.cattle.platform.core.model.tables.ServiceTable.SERVICE; import static io.cattle.platform.core.model.tables.StackTable.STACK; @@ -9,6 +10,7 @@ import io.cattle.platform.api.utils.ApiUtils; import io.cattle.platform.core.model.Account; import io.cattle.platform.core.model.Certificate; +import io.cattle.platform.core.model.Host; import io.cattle.platform.core.model.Instance; import io.cattle.platform.core.model.InstanceHostMap; import io.cattle.platform.core.model.InstanceLink; @@ -254,7 +256,7 @@ private T authorized(ApiRequest original, ApiRequest request, Supplier op Schema schema = current.schemas().getSchema(request.getType()); if (schema == null) throw error(403, "PermissionDenied"); if (request.getAttribute(DOMAIN) instanceof AiResourceChangeDescriptor descriptor - && Set.of("dnsService", "certificate").contains(descriptor.resourceType())) { + && Set.of("dnsService", "certificate", "host").contains(descriptor.resourceType())) { if (request.getAction() != null) { var actions = request.getId() == null ? schema.getCollectionActions() : schema.getResourceActions(); if (actions == null || !actions.containsKey(request.getAction())) throw error(403, "PermissionDenied"); @@ -301,11 +303,13 @@ public Object lock(String type, long id) { case "service", "externalService", "dnsService" -> create().selectFrom(SERVICE).where(SERVICE.ID.eq(id)).forUpdate().fetchOne(); case "container" -> create().selectFrom(INSTANCE).where(INSTANCE.ID.eq(id)).forUpdate().fetchOne(); case "certificate" -> create().selectFrom(CERTIFICATE).where(CERTIFICATE.ID.eq(id)).forUpdate().fetchOne(); + case "host" -> create().selectFrom(HOST).where(HOST.ID.eq(id)).forUpdate().fetchOne(); default -> throw error(422, "ResourceChangeUnsupported"); }; } public Map snapshot(Object target) { + if (target instanceof Host host) return copyMap(AiHostMetadataChange.snapshot(host)); Map result = new LinkedHashMap<>(); if (target != null) for (String field : List.of("id", "accountId", "kind", "name", "description", "state", "removed", "stackId", "startCount", "data")) { @@ -383,7 +387,8 @@ public String targetPath(ApiRequest request, Object target, Object parent, Map refs = refs(f.read()); assertEquals(2, refs.size()); + assertEquals("Blue", ((Map) refs.get(0)).get("stackName")); + assertEquals("Green", ((Map) refs.get(1)).get("stackName")); + assertEquals("stopped", ((Map) refs.get(1)).get("state")); + verify(f.service.keyAuthorization, times(5)).actor(any()); + verify(f.manager, never()).convertResponse(any(Object.class), any()); + assertFalse(refs.toString().contains("PRIVATE")); assertSame(f.originalContext, ApiContext.getContext().getApiRequest()); + } + + @Test public void boundUnknownSnapshotsExcludeMovedForeignRemovedAndUnreadableChildrenOrStackNames() { + Fixture f = new Fixture(); + for (int id = 11; id <= 15; id++) f.candidates.add(mapping(id, id)); + f.resources.put("container:11", resource("11", "container", "stopped", Map.of("stackId", "31"))); + f.resources.put("container:12", resource("12", "container", "running", Map.of("accountId", "88", "name", "PRIVATE_FOREIGN"))); + f.resources.put("container:13", resource("13", "container", "removed", Map.of("removed", "yesterday", "name", "PRIVATE_REMOVED"))); + f.resources.put("container:14", resource("14", "container", "running", Map.of("name", "PRIVATE_DENIED"))); + f.resources.put("container:15", resource("15", "container", "running", Map.of("name", "PRIVATE_MOVED"))); + f.nativeScope = (type, id) -> !"14".equals(id); when(f.hosts.isWorkloadCurrent(77, 7, 15, 15, null)).thenReturn(false); + Map result = f.read(); assertEquals(1, refs(result).size()); assertFalse(result.toString().contains("PRIVATE")); + assertEquals("unknown", ((Map) refs(result).getFirst()).get("stackResolution")); + assertEquals("unknown", result.get("workloadReferencesStatus")); assertFalse(result.containsKey("hiddenCount")); + f.resources.put("stack:31", resource("31", "stack", "active", Map.of("name", "PRIVATE_OLD_STACK"))); + when(f.hosts.isWorkloadCurrent(77, 7, 11, 11, 31L)).thenReturn(false); + Map moved = f.read(); assertEquals(1, refs(moved).size()); + assertEquals("unknown", ((Map) refs(moved).getFirst()).get("stackResolution")); + assertFalse(moved.toString().contains("PRIVATE_OLD_STACK")); + + f.candidates.clear(); + for (int id = 1; id <= 51; id++) { f.candidates.add(mapping(id, 100 + id)); + f.resources.put("container:" + (100 + id), resource("" + (100 + id), "container", "stopped", Map.of())); } + assertEquals(50, refs(f.read()).size()); verify(f.manager, never()).getById(eq("container"), eq("151"), any()); + f.candidates.clear(); assertTrue(refs(f.read()).isEmpty()); + f.service.read(f.request(Map.of("resourceType", "host"))); verify(f.hosts, times(4)).getWorkloadCandidates(77, 7); + } + + @Test public void realScopedKeyNativePredicateHidesChildrenAndTheirAuditIdentitiesButDirectTargetStillDenies() throws Exception { + Fixture f = new Fixture(); f.candidates.add(mapping(1, 11)); f.candidates.add(mapping(2, 12)); + f.resources.put("container:11", resource("11", "container", "stopped", Map.of())); + f.resources.put("container:12", resource("12", "container", "running", Map.of("name", "PRIVATE_DENIED"))); + ApiKeyAuthorizationService access = new ApiKeyAuthorizationService(); ApiKeyTargetResolver targets = mock(ApiKeyTargetResolver.class); + ApiKeyAuditSink sink = mock(ApiKeyAuditSink.class); List decisions = new ArrayList<>(); + doAnswer(call -> { ApiRequest r = call.getArgument(0); assertEquals("GET", r.getMethod()); + decisions.add(r.getType() + ":" + r.getId() + ":" + r.getAttribute("apiKey.audit.decision")); return null; + }).when(sink).recordDecision(any(), any()); + field(access, "targets", targets); field(access, "auditSinks", List.of(sink)); + when(targets.resolve(any(), any())).thenAnswer(call -> { ApiRequest r = call.getArgument(0); + return List.of(ApiKeyPolicyEvaluator.Target.projectResource(r.getType(), r.getId(), "77")); }); + when(targets.stackLifecycleTargets(any(), any())).thenReturn(List.of()); when(targets.references(any(), any())).thenReturn(List.of()); + when(targets.parseScopeId(anyString(), anyString())).thenAnswer(call -> Long.valueOf(call.getArgument(1))); + f.service.keyAccess = access; + ApiKeyPolicy policy = new ApiKeyPolicy(ApiKeyPolicy.Mode.CUSTOM, ApiKeyPolicy.Effect.DENY, null, List.of( + new ApiKeyPolicy.Rule("host", ApiKeyPolicy.Effect.ALLOW, ApiKeyPolicy.Scope.resource("host", "7"), Set.of("read")), + new ApiKeyPolicy.Rule("container", ApiKeyPolicy.Effect.ALLOW, ApiKeyPolicy.Scope.resource("container", "11"), Set.of("read")))); + ApiKeyCredentialContext key = new ApiKeyCredentialContext(5, 42, "apiKey", 3, policy); + try (var c = java.sql.DriverManager.getConnection("jdbc:h2:mem:" + java.util.UUID.randomUUID())) { + var sql = org.jooq.impl.DSL.using(c, org.jooq.SQLDialect.H2, + new org.jooq.conf.Settings().withRenderQuotedNames(org.jooq.conf.RenderQuotedNames.NEVER)); + sql.execute("create schema cattle"); + sql.execute("create table cattle.instance(id bigint,account_id bigint,environment_id bigint,service_id bigint)"); + sql.execute("create table cattle.service(id bigint,account_id bigint,environment_id bigint,removed timestamp)"); + sql.execute("create table cattle.service_expose_map(id bigint,account_id bigint,instance_id bigint,service_id bigint," + + "managed boolean,upgrade boolean,removed timestamp)"); + sql.execute("insert into cattle.instance values(11,77,null,null),(12,77,null,null)"); + f.nativeScope = (type, id) -> { if (!"container".equals(type)) return true; + var table = io.cattle.platform.core.model.tables.InstanceTable.INSTANCE; + var condition = access.constrain(ApiContext.getContext().getApiRequest(), f.schemas, "instance", table); + return sql.fetchExists(sql.selectOne().from(table).where(table.ID.eq(Long.valueOf(id))).and(condition)); }; + ApiRequest request = f.request(Map.of("resourceType", "host", "resourceId", "7")); ApiKeyCredentialContext.attach(request, key); + Map result = (Map) ((List) f.service.read(request).get("data")).getFirst(); + assertEquals(1, refs(result).size()); assertFalse(result.toString().contains("PRIVATE")); + assertEquals(List.of("host:7:ALLOW"), decisions); + ApiRequest direct = new ApiRequest(null, f.schemas); direct.setType("container"); direct.setId("12"); direct.setMethod("GET"); + ApiKeyCredentialContext.attach(direct, key); assertThrows(ClientVisibleException.class, () -> access.authorize(direct, f.live)); + assertEquals(List.of("host:7:ALLOW", "container:12:DENY"), decisions); + } + } + + @Test public void parentRbacAndKeyRevocationFailClosedBeforeRelationshipOutputAndRestoreContext() { + Fixture denied = new Fixture(); when(denied.schema.getResourceMethods()).thenReturn(List.of()); + assertThrows(ClientVisibleException.class, denied::read); verifyNoInteractions(denied.hosts); + Fixture f = new Fixture(); f.candidates.add(mapping(1, 11)); f.candidates.add(mapping(2, 12)); + f.resources.put("container:11", resource("11", "container", "stopped", Map.of())); + when(f.service.keyAuthorization.actor(any())).thenReturn(f.owner, f.owner).thenThrow(new ClientVisibleException(403, "ApiKeyPolicyChanged")); + assertEquals("ApiKeyPolicyChanged", assertThrows(ClientVisibleException.class, f::read).getCode()); + verify(f.manager, never()).getById(eq("container"), eq("12"), any()); + assertSame(f.originalContext, ApiContext.getContext().getApiRequest()); assertSame(f.owner, ApiContext.getContext().getPolicy()); + Fixture unattested = new Fixture(); unattested.candidates.add(mapping(1, 11)); + when(unattested.manager.supportsScopedCollectionQuery(any())).thenReturn(false); + ApiRequest request = unattested.request(Map.of("resourceType", "host", "resourceId", "7")); + ApiKeyCredentialContext.attach(request, new ApiKeyCredentialContext(5, 42, "apiKey", 3, + new ApiKeyPolicy(ApiKeyPolicy.Mode.CUSTOM, ApiKeyPolicy.Effect.ALLOW, null, List.of()))); + assertTrue(refs((Map) ((List) unattested.service.read(request).get("data")).getFirst()).isEmpty()); + verify(unattested.manager, never()).getById(eq("container"), anyString(), any()); + } + + @Test public void cookieFullAndLegacyHostMetadataMatchesActualNativeWrappingAndHostsFilter() throws Exception { + for (int mode = 0; mode < 3; mode++) { + Fixture f = new Fixture(); HostRecord host = (HostRecord) f.nativeRows.get("host:7"); + host.setAgentState("disconnected"); host.setMemory(4096L); host.setMilliCpu(2000L); host.setLocalStorageMb(8192L); + host.setData(Map.of("fields", Map.of("hostname", "native.example", "agentIpAddress", "192.0.2.1", "dockerVersion", "29.0", + "memTotal", 4096L, "cpuCount", 2L, "instanceIds", List.of("PRIVATE"), "agentToken", "PRIVATE"))); + for (String name : List.of("hostname", "agentIpAddress", "dockerVersion", "memTotal", "cpuCount", + "agentState", "memory", "milliCpu", "localStorageMb")) { + var capability = mock(io.github.ibuildthecloud.gdapi.model.Field.class); + when(capability.isIncludeInList()).thenReturn(true); + when(capability.getTypeEnum()).thenReturn(io.github.ibuildthecloud.gdapi.model.FieldType.STRING); + when(capability.getValue(any())).thenAnswer(call -> { + if (io.cattle.platform.object.util.DataUtils.getFields(host).containsKey(name)) + return io.cattle.platform.object.util.DataUtils.getFields(host).get(name); + return host.getClass().getMethod("get" + Character.toUpperCase(name.charAt(0)) + name.substring(1)).invoke(host); + }); f.fields.put(name, capability); + } + var fullSchema = new io.github.ibuildthecloud.gdapi.model.impl.SchemaImpl(); fullSchema.setId("host"); fullSchema.setResourceFields(f.fields); + var nativeResource = new io.github.ibuildthecloud.gdapi.model.impl.WrappedResource(ApiContext.getContext().getIdFormatter(), f.schemas, + fullSchema, host, new LinkedHashMap<>(io.cattle.platform.object.util.DataUtils.getFields(host)), Set.of(), "GET", false); + var filter = new io.cattle.platform.iaas.api.filter.hosts.HostsFilter(); field(filter, "hostDao", f.hosts); + when(f.hosts.getInstancesPerHost(anyList(), any())).thenReturn(Map.of(7L, List.of("PRIVATE_INSTANCE_ID"))); + when(f.hosts.getPhysicalHostsForHosts(anyList())).thenReturn(new java.util.HashMap<>()); + ApiRequest nativeRequest = f.request(Map.of()); nativeRequest.setResponseObject(host); + Resource filtered = filter.filter(nativeRequest, host, nativeResource); + Map expected = AiResourceViewService.project("host", filtered.getId(), filtered.getFields()); + ApiRequest request = f.request(Map.of("resourceType", "host", "resourceId", "7")); + if (mode != 0) ApiKeyCredentialContext.attach(request, new ApiKeyCredentialContext(5, 42, "apiKey", 3, + mode == 1 ? new ApiKeyPolicy(ApiKeyPolicy.Mode.FULL, ApiKeyPolicy.Effect.ALLOW, null, List.of()) : null)); + Map actual = new LinkedHashMap<>((Map) ((List) f.service.read(request).get("data")).getFirst()); + actual.remove("workloadReferences"); actual.remove("workloadReferencesStatus"); assertEquals(expected, actual); + assertEquals("disconnected", actual.get("state")); assertEquals(4096L, actual.get("memory")); + assertFalse(actual.toString().contains("PRIVATE")); verify(f.manager, never()).convertResponse(any(Object.class), any()); + var v1Resource = new io.github.ibuildthecloud.gdapi.model.impl.WrappedResource(ApiContext.getContext().getIdFormatter(), f.schemas, + fullSchema, host, new LinkedHashMap<>(io.cattle.platform.object.util.DataUtils.getFields(host)), Set.of(), "GET", false); + nativeRequest.setVersion("v1"); + Map v1Expected = AiResourceViewService.project("host", v1Resource.getId(), + filter.filter(nativeRequest, host, v1Resource).getFields()); + request.setVersion("v1"); + actual = new LinkedHashMap<>((Map) ((List) f.service.read(request).get("data")).getFirst()); + actual.remove("workloadReferences"); actual.remove("workloadReferencesStatus"); assertEquals(v1Expected, actual); + assertEquals("active", actual.get("state")); + var hostname = f.fields.get("hostname"); clearInvocations(hostname); when(hostname.getTransform()).thenReturn("sensitive-transform"); + Map row = (Map) ((List) f.service.read(request).get("data")).getFirst(); assertFalse(row.containsKey("hostname")); + verify(hostname, never()).getValue(any()); + } + } + + private static void field(Object owner, String name, Object value) throws Exception { + var field = owner.getClass().getDeclaredField(name); field.setAccessible(true); field.set(owner, value); + } + private static List refs(Map row) { return (List) row.get("workloadReferences"); } + private static InstanceHostMap mapping(long id, long instance) { InstanceHostMapRecord m = new InstanceHostMapRecord(); + m.setId(id); m.setHostId(7L); m.setInstanceId(instance); return m; } + private static Resource resource(String id, String type, String state, Map extra) { + Map fields = new LinkedHashMap<>(Map.of("accountId", "77", "name", "same", "state", state, "launchConfig", "PRIVATE")); + fields.putAll(extra); return new ResourceImpl(id, type, fields); + } + static class Fixture extends AiVolumeUsageViewTest.Fixture { + final HostDao hosts = mock(HostDao.class); + final List candidates = new ArrayList<>(); + Fixture() { + service.hosts = hosts; when(hosts.getWorkloadCandidates(77, 7)).thenAnswer(call -> candidates); + when(hosts.isWorkloadCurrent(anyLong(), anyLong(), anyLong(), anyLong(), any())).thenReturn(true); + HostRecord host = new HostRecord(); host.setId(7L); host.setAccountId(77L); host.setName("named-host"); host.setState("active"); + nativeRows.put("host:7", host); + } + @Override Map read() { + return (Map) ((List) service.read(request(Map.of("resourceType", "host", "resourceId", "7"))).get("data")).getFirst(); + } + } +} diff --git a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/view/AiUsageServiceStackViewTest.java b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/view/AiUsageServiceStackViewTest.java new file mode 100644 index 0000000000..05e5d65abc --- /dev/null +++ b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/view/AiUsageServiceStackViewTest.java @@ -0,0 +1,112 @@ +package io.cattle.platform.iaas.api.auth.ai.view; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import io.cattle.platform.core.dao.ServiceDao; +import io.cattle.platform.core.model.tables.records.InstanceRecord; +import io.cattle.platform.core.model.tables.records.MountRecord; +import io.cattle.platform.core.model.tables.records.InstanceHostMapRecord; +import io.cattle.platform.core.model.tables.records.ServiceRecord; +import io.cattle.platform.iaas.api.auth.apikey.ApiKeyCredentialContext; +import io.cattle.platform.iaas.api.auth.apikey.ApiKeyPolicy; +import io.github.ibuildthecloud.gdapi.context.ApiContext; +import io.github.ibuildthecloud.gdapi.exception.ClientVisibleException; +import io.github.ibuildthecloud.gdapi.model.Field; +import io.github.ibuildthecloud.gdapi.model.Schema; +import io.github.ibuildthecloud.gdapi.model.impl.ResourceImpl; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.After; +import org.junit.Test; + +public class AiUsageServiceStackViewTest { + private static final String UUID = "11111111-1111-1111-1111-111111111111"; + @After public void cleanup() { ApiContext.remove(); } + + @Test public void realNativeNullInstanceStackAndNoStackFieldUsesReadableSemServiceStackForHostAndVolume() { + for (String type : List.of("host", "volume")) { + Fixture f = new Fixture(type); Map row = f.read(); + assertEquals("Blue", ((Map) f.refs(row).getFirst()).get("stackName")); + assertEquals("resolved", ((Map) f.refs(row).getFirst()).get("stackResolution")); + assertFalse(f.containerFields.containsKey("stackId")); assertNull(f.instance.getStackId()); + verify(f.manager).getById(eq("service"), eq("51"), any()); verify(f.manager).getById(eq("stack"), eq("31"), any()); + verify(f.services).isUsageServiceCurrent(77, 11, UUID, 51, 31, "host".equals(type) ? 7L : null, "host".equals(type) ? 1L : null); + verify(f.manager, never()).convertResponse(any(Object.class), any()); + assertFalse(row.toString().contains("PRIVATE")); assertFalse(row.toString().contains("serviceIds")); + } + } + + @Test public void hiddenTransformedNonNullDirectOrMultipleSemRelationsNeverBypassReadableSchemaOrLeakNames() { + for (String scenario : List.of("hiddenRelation", "transformed", "createOnly", "hiddenDirect", "multiple", "hiddenService", + "hiddenStack", "hiddenServiceStack", "otherKind", "unattested")) { + Fixture f = new Fixture("volume"); + if ("hiddenRelation".equals(scenario)) f.containerFields.remove("serviceIds"); + if ("transformed".equals(scenario)) when(f.relation.getTransform()).thenReturn("private-transform"); + if ("createOnly".equals(scenario)) when(f.relation.isReadOnCreateOnly()).thenReturn(true); + if ("hiddenDirect".equals(scenario)) f.instance.setStackId(99L); + if ("multiple".equals(scenario)) when(f.services.getUsageServiceIds(77, 11, UUID)).thenReturn(List.of(51L, 52L)); + if ("hiddenService".equals(scenario)) f.nativeScope = (type, id) -> !"service".equals(type); + if ("hiddenStack".equals(scenario)) f.nativeScope = (type, id) -> !"stack".equals(type); + if ("hiddenServiceStack".equals(scenario)) f.fields.remove("stackId"); + if ("otherKind".equals(scenario)) f.serviceRow.setKind("loadBalancerService"); + if ("unattested".equals(scenario)) { + when(f.manager.supportsScopedCollectionQuery(any())).thenAnswer(call -> !"service".equals(ApiContext.getContext().getApiRequest().getType())); + f.key = new ApiKeyCredentialContext(5, 42, "apiKey", 3, + new ApiKeyPolicy(ApiKeyPolicy.Mode.CUSTOM, ApiKeyPolicy.Effect.ALLOW, null, List.of())); + } + Map row = f.read(); assertEquals(1, f.refs(row).size()); + Map ref = (Map) f.refs(row).getFirst(); assertEquals("unknown", ref.get("stackResolution")); + assertFalse(ref.containsKey("stackName")); assertFalse(ref.containsKey("serviceId")); assertFalse(ref.containsKey("hiddenCount")); + if (List.of("hiddenRelation", "transformed", "createOnly", "hiddenDirect").contains(scenario)) verifyNoInteractions(f.services); + if ("multiple".equals(scenario) || "unattested".equals(scenario)) verify(f.manager, never()).getById(eq("service"), anyString(), any()); + } + } + + @Test public void exactCapturedSemAndStackRaceDropsFormerNameWhileChangedKeyAbortsTheWholeRead() { + for (String type : List.of("host", "volume")) { + Fixture f = new Fixture(type); when(f.services.isUsageServiceCurrent(anyLong(), anyLong(), anyString(), anyLong(), anyLong(), any(), any())) + .thenReturn(false); + Map row = f.read(); assertEquals(1, f.refs(row).size()); + assertEquals("unknown", ((Map) f.refs(row).getFirst()).get("stackResolution")); assertFalse(row.toString().contains("Blue")); + verify(f.services).isUsageServiceCurrent(77, 11, UUID, 51, 31, "host".equals(type) ? 7L : null, "host".equals(type) ? 1L : null); + } + Fixture f = new Fixture("volume"); + when(f.service.keyAuthorization.actor(any())).thenReturn(f.owner, f.owner).thenThrow(new ClientVisibleException(403, "ApiKeyPolicyChanged")); + assertEquals("ApiKeyPolicyChanged", assertThrows(ClientVisibleException.class, f::read).getCode()); + assertSame(f.originalContext, ApiContext.getContext().getApiRequest()); assertSame(f.owner, ApiContext.getContext().getPolicy()); + } + + static class Fixture extends AiHostWorkloadViewTest.Fixture { + final ServiceDao services = mock(ServiceDao.class); final String type; + final Map containerFields = new LinkedHashMap<>(); final Field relation = mock(Field.class); + final InstanceRecord instance = new InstanceRecord(); final ServiceRecord serviceRow = new ServiceRecord(); + ApiKeyCredentialContext key; + Fixture(String type) { + this.type = type; service.services = services; + when(services.getUsageServiceIds(77, 11, UUID)).thenReturn(List.of(51L)); + when(services.isUsageServiceCurrent(anyLong(), anyLong(), anyString(), anyLong(), anyLong(), any(), any())).thenReturn(true); + instance.setId(11L); instance.setUuid(UUID); instance.setAccountId(77L); instance.setKind("container"); + instance.setName("same"); instance.setState("stopped"); nativeRows.put("container:11", instance); + serviceRow.setId(51L); serviceRow.setAccountId(77L); serviceRow.setKind("service"); serviceRow.setStackId(31L); + nativeRows.put("service:51", serviceRow); resources.put("stack:31", new ResourceImpl("31", "stack", + Map.of("accountId", "77", "name", "Blue", "state", "active"))); + containerFields.putAll(fields); containerFields.remove("stackId"); containerFields.put("serviceIds", relation); + when(relation.isIncludeInList()).thenReturn(true); + Schema container = mock(Schema.class); when(container.getId()).thenReturn("container"); + when(container.getResourceFields()).thenReturn(containerFields); when(container.getResourceMethods()).thenReturn(List.of("GET")); + when(schemas.getSchema("container")).thenReturn(container); + MountRecord mount = new MountRecord(); mount.setId(1L); mount.setVolumeId(7L); mount.setInstanceId(11L); mount.setPath("/data"); + mount.setPermissions("rw"); mounts.add(mount); + InstanceHostMapRecord placement = new InstanceHostMapRecord(); placement.setId(1L); placement.setHostId(7L); + placement.setInstanceId(11L); candidates.add(placement); + } + @Override Map read() { + var request = request(Map.of("resourceType", type, "resourceId", "7")); if (key != null) ApiKeyCredentialContext.attach(request, key); + return (Map) ((List) service.read(request).get("data")).getFirst(); + } + List refs(Map row) { return (List) row.get("host".equals(type) ? "workloadReferences" : "usageReferences"); } + } +} diff --git a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiHostMetadataChangeTest.java b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiHostMetadataChangeTest.java new file mode 100644 index 0000000000..5df0eaa2b2 --- /dev/null +++ b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/change/AiHostMetadataChangeTest.java @@ -0,0 +1,201 @@ +package io.cattle.platform.iaas.api.auth.change; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static io.cattle.platform.core.model.tables.HostTable.HOST; + +import io.cattle.platform.archaius.util.ArchaiusUtil; +import io.cattle.platform.core.model.tables.records.HostRecord; +import io.cattle.platform.iaas.api.auth.apikey.ApiKeyAiBridgeGuard; +import io.cattle.platform.iaas.api.auth.apikey.ApiKeyAuthorizationService; +import io.cattle.platform.api.auth.Policy; +import io.cattle.platform.json.JacksonJsonMapper; +import io.cattle.platform.object.util.DataAccessor; +import io.github.ibuildthecloud.gdapi.context.ApiContext; +import io.github.ibuildthecloud.gdapi.exception.ClientVisibleException; +import io.github.ibuildthecloud.gdapi.model.impl.FieldImpl; +import io.github.ibuildthecloud.gdapi.request.ApiRequest; +import io.github.ibuildthecloud.gdapi.request.resource.ResourceManager; +import io.github.ibuildthecloud.gdapi.request.resource.ResourceManagerLocator; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.sql.DriverManager; +import org.jooq.SQLDialect; +import org.jooq.impl.DSL; +import org.jooq.impl.DefaultConfiguration; +import org.junit.After; +import org.junit.Test; + +/** New host subset through the existing domain and durable plan owners. */ +public class AiHostMetadataChangeTest { + @After public void cleanup() { ApiContext.remove(); } + + @Test public void onlyMetadataIntentAndFieldsAreAccepted() { + var descriptor = descriptor(); + assertEquals("PUT", descriptor.method()); assertNull(descriptor.action()); assertEquals("update", descriptor.keyOperation()); + Map metadata = new LinkedHashMap<>(); metadata.put("name", "new-name"); metadata.put("description", null); + assertEquals(metadata, descriptor.body(descriptor.parameters(metadata))); + for (String field : List.of("hostname", "labels", "info", "credentials", "data", "accountId", "uuid", "state", "memory", "milliCpu")) + assertEquals("ResourceChangeInputInvalid", assertThrows(ClientVisibleException.class, + () -> descriptor.parameters(Map.of(field, "PRIVATE-CANARY"))).getCode()); + for (String operation : List.of("create", "remove", "start", "stop", "restore", "update_targets")) + assertThrows(ClientVisibleException.class, () -> AiResourceChangeDescriptor.require("host", operation)); + assertThrows(ClientVisibleException.class, () -> descriptor.parameters(Map.of())); + assertThrows(ClientVisibleException.class, () -> descriptor.parameters(Map.of("name", " "))); + assertThrows(ClientVisibleException.class, () -> descriptor.parameters(Map.of("description", "x".repeat(1025)))); + } + + @Test public void nativePutUsesRealRevisionGuardAndRestoresContext() throws Exception { + var f = fixture(); var manager = f.domain.managers.getResourceManagerByType("host"); + var target = host(); Map body = Map.of("name", "safe-name"); + when(manager.update(eq("host"), eq("1h7"), any())).thenAnswer(call -> { + ApiRequest actual = call.getArgument(2); assertEquals(body, actual.getRequestObject()); + assertEquals("PUT", actual.getMethod()); assertNull(actual.getAction()); + assertTrue(AiResourceChangeDomain.trustedDomain(actual)); + assertSame(actual, ApiContext.getContext().getApiRequest()); return target; + }); + Object previous = ArchaiusUtil.getConfiguration().getProperty("api.ai.bridge.enabled"); + try { + ArchaiusUtil.getConfiguration().setProperty("api.ai.bridge.enabled", true); + assertEquals(body, f.domain.prepare(f.request, descriptor(), "1h7", body)); + assertSame(target, f.domain.dispatch(f.request, descriptor(), "1h7", body, "own-plan")); + assertSame(f.request, ApiContext.getContext().getApiRequest()); assertSame(f.policy, ApiContext.getContext().getPolicy()); + when(f.http.getHeader(ApiKeyAiBridgeGuard.EXPECTED_REVISION_HEADER)).thenReturn("2"); + assertEquals("ApiKeyPolicyChanged", assertThrows(ClientVisibleException.class, + () -> f.domain.dispatch(f.request, descriptor(), "1h7", body, "own-plan")).getCode()); + verify(manager, times(1)).update(eq("host"), eq("1h7"), any()); + assertSame(f.request, ApiContext.getContext().getApiRequest()); + } finally { + if (previous == null) ArchaiusUtil.getConfiguration().clearProperty("api.ai.bridge.enabled"); + else ArchaiusUtil.getConfiguration().setProperty("api.ai.bridge.enabled", previous); + } + } + + @Test public void nativeMethodFieldAndCrossProjectCeilingsRejectBeforeMutation() throws Exception { + Object previous = ArchaiusUtil.getConfiguration().getProperty("api.ai.bridge.enabled"); + try { + ArchaiusUtil.getConfiguration().setProperty("api.ai.bridge.enabled", true); + var f = fixture(); var manager = f.domain.managers.getResourceManagerByType("host"); + when(f.schema.getResourceMethods()).thenReturn(List.of("GET")); + assertEquals("PermissionDenied", assertThrows(ClientVisibleException.class, + () -> f.domain.prepare(f.request, descriptor(), "1h7", Map.of("name", "safe"))).getCode()); + when(f.schema.getResourceMethods()).thenReturn(List.of("GET", "PUT")); f.name.setUpdate(false); + assertEquals("PermissionDenied", assertThrows(ClientVisibleException.class, + () -> f.domain.prepare(f.request, descriptor(), "1h7", Map.of("name", "safe"))).getCode()); + verify(f.validation, never()).generate(any()); f.name.setUpdate(true); + var row = host(); when(manager.getById(eq("host"), eq("1h7"), any())).thenReturn(row); + row.setAccountId(88L); + assertEquals("NotFound", assertThrows(ClientVisibleException.class, + () -> f.domain.visible(f.request, descriptor(), "1h7")).getCode()); + row.setAccountId(77L); row.setKind("service"); + assertEquals("NotFound", assertThrows(ClientVisibleException.class, + () -> f.domain.visible(f.request, descriptor(), "1h7")).getCode()); + row.setKind("host"); assertSame(row, f.domain.visible(f.request, descriptor(), "1h7")); + f.domain.keyAccess = new ApiKeyAuthorizationService() { + @Override public void authorize(ApiRequest request, Policy policy) { + throw new ClientVisibleException(403, "KeyScopeDenied"); + } + }; + assertEquals("KeyScopeDenied", assertThrows(ClientVisibleException.class, + () -> f.domain.prepare(f.request, descriptor(), "1h7", Map.of("name", "safe"))).getCode()); + verify(manager, never()).update(anyString(), anyString(), any()); + + } finally { + if (previous == null) ArchaiusUtil.getConfiguration().clearProperty("api.ai.bridge.enabled"); + else ArchaiusUtil.getConfiguration().setProperty("api.ai.bridge.enabled", previous); + } + } + + @Test public void durableSnapshotAndSummaryExcludeMaterialAndObserveExactMetadata() { + var f = fixture(); var row = host(); + for (String field : List.of("credentials", "secretKey", "agentToken")) DataAccessor.fields(row).withKey(field).set("PRIVATE-CANARY"); + var snapshot = f.domain.snapshot(row); + assertFalse(snapshot.containsKey("data")); assertFalse(snapshot.toString().contains("PRIVATE-CANARY")); + assertEquals("host-uuid", snapshot.get("uuid")); assertEquals("node.example.test", snapshot.get("hostname")); + assertEquals("node.example.test (before)", AiHostMetadataChange.displayName(row)); + assertFalse(f.domain.complete(descriptor(), row, Map.of("name", "after"), snapshot)); + row.setName("after"); assertTrue(f.domain.complete(descriptor(), row, Map.of("name", "after"), snapshot)); + row.setRemoved(new Date()); assertFalse(f.domain.complete(descriptor(), row, Map.of("name", "after"), snapshot)); + assertFalse(f.domain.changes(descriptor(), row, Map.of("name", "after")).toString().contains("PRIVATE-CANARY")); + } + + @Test public void boundPlanRequiresHumanApprovalAndExactOnceObservedCompletion() { + var f = preview(); assertEquals(0, f.dispatched); + denied("AiKeyConfirmationForbidden", () -> f.service.action(f.id, f.request("confirm", true))); + denied("ResourceChangeNeedsConfirmation", () -> f.apply(true)); + f.confirm(); denied("ResourceChangeBoundKeyRequired", () -> f.apply(false)); + assertEquals("accepted", f.apply(true).get("status")); assertEquals(1, f.dispatched); assertEquals(1, f.claims); + assertEquals("running", f.service.get(f.id, f.request(null, true)).get("status")); + f.complete = true; var observed = f.service.get(f.id, f.request(null, true)); + assertEquals("succeeded", observed.get("status")); assertEquals(true, observed.get("verifiedResult")); + f.apply(true); assertEquals(1, f.dispatched); assertEquals(1, f.claims); + assertFalse(observed.containsKey("snapshot")); assertFalse(f.audits.toString().contains("new private description")); + } + + @Test public void casAndLiveWriteRevocationNeverClaimOrDispatch() { + var f = preview(); var row = (HostRecord) f.domain.visible(f.request(null, true), descriptor(), "1h7"); row.setName("changed-by-ui"); + denied("ResourceChangePlanStale", f::confirm); assertEquals(0, f.claims); + row.setName("before"); f.writeAllowed = false; + denied("PermissionDenied", f::confirm); assertEquals(0, f.dispatched); + f.writeAllowed = true; f.confirm(); f.writeAllowed = false; + denied("PermissionDenied", () -> f.apply(true)); assertEquals(0, f.claims); assertEquals(0, f.dispatched); + } + + @Test public void actualHostTableLockRequiresTransactionAndReloadsTheMetadataSnapshot() throws Exception { + try (var connection = DriverManager.getConnection("jdbc:h2:mem:host-metadata;DATABASE_TO_LOWER=TRUE")) { + var configuration = new DefaultConfiguration().set(connection).set(SQLDialect.H2); + var sql = DSL.using(configuration); sql.createSchemaIfNotExists("cattle").execute(); + sql.createTable(HOST).columns(HOST.fields()).execute(); + sql.insertInto(HOST).set(HOST.ID, 7L).set(HOST.ACCOUNT_ID, 77L).set(HOST.UUID, "host-uuid") + .set(HOST.KIND, "host").set(HOST.NAME, "before") + .set(HOST.STATE, "active").set(HOST.IS_PUBLIC, false).set(HOST.DATA, Map.of()).execute(); + var domain = new AiResourceChangeDomain(); domain.json = new JacksonJsonMapper(); domain.setConfiguration(configuration); + assertEquals("ResourceChangeTransactionRequired", assertThrows(ClientVisibleException.class, + () -> domain.lock("host", 7L)).getCode()); + connection.setAutoCommit(false); + var before = domain.snapshot(domain.lock("host", 7L)); + sql.update(HOST).set(HOST.NAME, "changed-by-ui").where(HOST.ID.eq(7L)).execute(); + var after = domain.snapshot(domain.lock("host", 7L)); + assertEquals("before", before.get("name")); assertEquals("changed-by-ui", after.get("name")); + assertNotEquals(before, after); assertFalse(after.containsKey("data")); connection.rollback(); + } + } + + private static AiResourceChangeDescriptor descriptor() { return AiResourceChangeDescriptor.require("host", "update_metadata"); } + + private static HostRecord host() { + var row = new HostRecord(); row.setId(7L); row.setAccountId(77L); row.setUuid("host-uuid"); + row.setKind("host"); row.setName("before"); DataAccessor.fields(row).withKey("hostname").set("node.example.test"); + row.setState("active"); return row; + } + + private static AiResourceChangeDomainTest.Fixture fixture() { + var f = new AiResourceChangeDomainTest.Fixture(); when(f.schemas.getSchema("host")).thenReturn(f.schema); + when(f.schema.getResourceMethods()).thenReturn(List.of("GET", "PUT")); + FieldImpl description = new FieldImpl(); description.setUpdate(true); + when(f.schema.getResourceFields()).thenReturn(Map.of("name", f.name, "description", description)); + f.domain.managers = mock(ResourceManagerLocator.class); + when(f.domain.managers.getResourceManagerByType("host")).thenReturn(mock(ResourceManager.class)); return f; + } + + private static AiResourceChangePlanServiceTest.Fixture preview() { + var f = new AiResourceChangePlanServiceTest.Fixture(); var row = host(); + var snapshotOwner = new AiResourceChangeDomain(); snapshotOwner.json = f.json; + when(f.domain.visible(any(), any(), anyString())).thenReturn(row); + when(f.domain.lock(anyString(), anyLong())).thenReturn(row); + when(f.domain.receiptTarget(any(), any(), anyMap())).thenReturn(row); + when(f.domain.snapshot(any())).thenAnswer(call -> snapshotOwner.snapshot(call.getArgument(0))); + doAnswer(call -> { + f.dispatched++; row.setDescription("new private description"); return row; + }).when(f.domain).dispatch(any(), any(), nullable(String.class), anyMap(), anyString()); + ApiRequest request = f.request(null, true); request.setId(null); + request.setRequestObject(Map.of("resourceType", "host", "operation", "update_metadata", "resourceId", "1h7", + "parameters", Map.of("description", "new private description"))); + var plan = f.service.preview(request); f.id = (String) plan.get("id"); assertEquals("pending", plan.get("confirmationState")); return f; + } + + private static void denied(String code, Runnable action) { assertEquals(code, assertThrows(ClientVisibleException.class, action::run).getCode()); } +} diff --git a/code/iaas/bootstrap/pom.xml b/code/iaas/bootstrap/pom.xml index 307e59ccc9..f0fca3432b 100644 --- a/code/iaas/bootstrap/pom.xml +++ b/code/iaas/bootstrap/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/iaas/config-item/api/pom.xml b/code/iaas/config-item/api/pom.xml index 85744f2c96..e892aaa4ea 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.369 + 0.183.370 ../../../parent/pom.xml diff --git a/code/iaas/config-item/common/pom.xml b/code/iaas/config-item/common/pom.xml index 3b8874e7a3..bdd8b9e7b0 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.369 + 0.183.370 ../../../parent/pom.xml diff --git a/code/iaas/config-item/server/pom.xml b/code/iaas/config-item/server/pom.xml index 36bb69f51c..a066b00c72 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.369 + 0.183.370 ../../../parent/pom.xml diff --git a/code/iaas/engine-jooq/pom.xml b/code/iaas/engine-jooq/pom.xml index 2272a61107..b7930ec407 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.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/iaas/events/pom.xml b/code/iaas/events/pom.xml index 9cdf1b28cf..cfb262d1cb 100644 --- a/code/iaas/events/pom.xml +++ b/code/iaas/events/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/iaas/external-handler/pom.xml b/code/iaas/external-handler/pom.xml index ffb0514177..a2549f7485 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.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/iaas/ha/pom.xml b/code/iaas/ha/pom.xml index 04e2d117ed..721c00d20b 100644 --- a/code/iaas/ha/pom.xml +++ b/code/iaas/ha/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/iaas/healthcheck/pom.xml b/code/iaas/healthcheck/pom.xml index 526c28c22c..1a4e53b690 100644 --- a/code/iaas/healthcheck/pom.xml +++ b/code/iaas/healthcheck/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/iaas/labels/pom.xml b/code/iaas/labels/pom.xml index 4ecb48131c..bdd65bc54c 100644 --- a/code/iaas/labels/pom.xml +++ b/code/iaas/labels/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/iaas/logic-common/pom.xml b/code/iaas/logic-common/pom.xml index aec2f15ca2..3b2e17d3d5 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.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/iaas/logic/pom.xml b/code/iaas/logic/pom.xml index 658497dc0c..8a2c854e47 100644 --- a/code/iaas/logic/pom.xml +++ b/code/iaas/logic/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/iaas/metadata/pom.xml b/code/iaas/metadata/pom.xml index 24e25c6e30..711d08006f 100644 --- a/code/iaas/metadata/pom.xml +++ b/code/iaas/metadata/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/iaas/model/pom.xml b/code/iaas/model/pom.xml index 4f476f4873..b1e117fadc 100644 --- a/code/iaas/model/pom.xml +++ b/code/iaas/model/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/iaas/model/src/main/java/io/cattle/platform/core/dao/HostDao.java b/code/iaas/model/src/main/java/io/cattle/platform/core/dao/HostDao.java index f3bd0940ef..3dc0d63485 100644 --- a/code/iaas/model/src/main/java/io/cattle/platform/core/dao/HostDao.java +++ b/code/iaas/model/src/main/java/io/cattle/platform/core/dao/HostDao.java @@ -3,6 +3,7 @@ import io.cattle.platform.archaius.util.ArchaiusUtil; import io.cattle.platform.archaius.util.ConfigProperty; import io.cattle.platform.core.model.Host; +import io.cattle.platform.core.model.InstanceHostMap; import io.cattle.platform.core.model.IpAddress; import io.cattle.platform.core.model.PhysicalHost; import io.github.ibuildthecloud.gdapi.id.IdFormatter; @@ -25,6 +26,10 @@ public interface HostDao { Map> getInstancesPerHost(List hosts, IdFormatter idFormatter); + List getWorkloadCandidates(long accountId, long hostId); + + boolean isWorkloadCurrent(long accountId, long hostId, long mappingId, long instanceId, Long stackId); + PhysicalHost createMachineForHost(Host host, String driver); Map getPhysicalHostsForHosts(List hosts); diff --git a/code/iaas/model/src/main/java/io/cattle/platform/core/dao/ServiceDao.java b/code/iaas/model/src/main/java/io/cattle/platform/core/dao/ServiceDao.java index e152ec011e..d2fb849890 100644 --- a/code/iaas/model/src/main/java/io/cattle/platform/core/dao/ServiceDao.java +++ b/code/iaas/model/src/main/java/io/cattle/platform/core/dao/ServiceDao.java @@ -23,6 +23,11 @@ public interface ServiceDao { Map> getServicesForInstances(List ids, IdFormatter idFormatter); + List getUsageServiceIds(long accountId, long instanceId, String instanceUuid); + + boolean isUsageServiceCurrent(long accountId, long instanceId, String instanceUuid, long serviceId, long stackId, + Long hostId, Long hostMappingId); + Map> getInstances(List ids, IdFormatter idFormatter); Map> getServiceLinks(List ids); diff --git a/code/iaas/model/src/main/java/io/cattle/platform/core/dao/impl/HostDaoImpl.java b/code/iaas/model/src/main/java/io/cattle/platform/core/dao/impl/HostDaoImpl.java index 9bd3d85502..0e3fa73c31 100644 --- a/code/iaas/model/src/main/java/io/cattle/platform/core/dao/impl/HostDaoImpl.java +++ b/code/iaas/model/src/main/java/io/cattle/platform/core/dao/impl/HostDaoImpl.java @@ -17,9 +17,11 @@ import io.cattle.platform.core.dao.GenericResourceDao; import io.cattle.platform.core.dao.HostDao; import io.cattle.platform.core.model.Host; +import io.cattle.platform.core.model.InstanceHostMap; import io.cattle.platform.core.model.IpAddress; import io.cattle.platform.core.model.PhysicalHost; import io.cattle.platform.core.model.tables.records.HostRecord; +import io.cattle.platform.core.model.tables.records.InstanceHostMapRecord; import io.cattle.platform.core.model.tables.records.IpAddressRecord; import io.cattle.platform.core.model.tables.records.PhysicalHostRecord; import io.cattle.platform.db.jooq.dao.impl.AbstractJooqDao; @@ -133,6 +135,31 @@ public Map> getInstancesPerHost(List hosts, final IdFor return result; } + @Override + public List getWorkloadCandidates(long accountId, long hostId) { + return workloadQuery(accountId, hostId).orderBy(INSTANCE_HOST_MAP.ID.asc()).limit(51) + .fetchInto(InstanceHostMapRecord.class); + } + + @Override + public boolean isWorkloadCurrent(long accountId, long hostId, long mappingId, long instanceId, Long stackId) { + return create().fetchExists(workloadQuery(accountId, hostId) + .and(INSTANCE_HOST_MAP.ID.eq(mappingId)).and(INSTANCE_HOST_MAP.INSTANCE_ID.eq(instanceId)) + .and(stackId == null ? DSL.trueCondition() : INSTANCE.STACK_ID.eq(stackId))); + } + + private org.jooq.SelectConditionStep workloadQuery(long accountId, long hostId) { + // Match native Host instances (including stopped containers), but bind + // both resource owners and bound candidates before any resource projection. + return create().select(INSTANCE_HOST_MAP.ID, INSTANCE_HOST_MAP.HOST_ID, INSTANCE_HOST_MAP.INSTANCE_ID) + .from(INSTANCE_HOST_MAP).join(INSTANCE).on(INSTANCE.ID.eq(INSTANCE_HOST_MAP.INSTANCE_ID)) + .join(HOST).on(HOST.ID.eq(INSTANCE_HOST_MAP.HOST_ID)) + .where(INSTANCE_HOST_MAP.HOST_ID.eq(hostId)).and(HOST.ACCOUNT_ID.eq(accountId)) + .and(INSTANCE.ACCOUNT_ID.eq(accountId)) + .and(INSTANCE.KIND.eq("container")).and(HOST.REMOVED.isNull()) + .and(INSTANCE_HOST_MAP.REMOVED.isNull()).and(INSTANCE.REMOVED.isNull()); + } + @Override public PhysicalHost createMachineForHost(final Host host, String driver) { String uuid = UUID.randomUUID().toString(); diff --git a/code/iaas/model/src/main/java/io/cattle/platform/core/dao/impl/ServiceDaoImpl.java b/code/iaas/model/src/main/java/io/cattle/platform/core/dao/impl/ServiceDaoImpl.java index 1629ceca3b..e74838d54c 100644 --- a/code/iaas/model/src/main/java/io/cattle/platform/core/dao/impl/ServiceDaoImpl.java +++ b/code/iaas/model/src/main/java/io/cattle/platform/core/dao/impl/ServiceDaoImpl.java @@ -5,6 +5,7 @@ import static io.cattle.platform.core.model.tables.HealthcheckInstanceTable.*; import static io.cattle.platform.core.model.tables.HostTable.*; import static io.cattle.platform.core.model.tables.InstanceTable.*; +import static io.cattle.platform.core.model.tables.InstanceHostMapTable.*; import static io.cattle.platform.core.model.tables.ServiceConsumeMapTable.*; import static io.cattle.platform.core.model.tables.ServiceExposeMapTable.*; import static io.cattle.platform.core.model.tables.ServiceIndexTable.*; @@ -45,8 +46,10 @@ import jakarta.inject.Named; import org.jooq.Record; +import org.jooq.Record1; import org.jooq.Record2; import org.jooq.Record3; +import org.jooq.impl.DSL; @Named public class ServiceDaoImpl extends AbstractJooqDao implements ServiceDao { @@ -120,6 +123,40 @@ public Map> getServicesForInstances(List ids, final IdF return result; } + @Override + public List getUsageServiceIds(long accountId, long instanceId, String instanceUuid) { + return usageServices(accountId, instanceId, instanceUuid).orderBy(SERVICE_EXPOSE_MAP.SERVICE_ID.asc()) + .limit(2).fetch(SERVICE_EXPOSE_MAP.SERVICE_ID); + } + + private org.jooq.SelectConditionStep> usageServices(long accountId, long instanceId, String instanceUuid) { + // The same native SEM relation as serviceIds, including stopped/inactive + // resources. Two distinct Services mean unknown, not a hidden count. + return create().selectDistinct(SERVICE_EXPOSE_MAP.SERVICE_ID).from(SERVICE_EXPOSE_MAP) + .join(SERVICE).on(SERVICE.ID.eq(SERVICE_EXPOSE_MAP.SERVICE_ID)) + .join(INSTANCE).on(INSTANCE.ID.eq(SERVICE_EXPOSE_MAP.INSTANCE_ID)) + .where(INSTANCE.ID.eq(instanceId)).and(INSTANCE.UUID.eq(instanceUuid)) + .and(INSTANCE.ACCOUNT_ID.eq(accountId)).and(INSTANCE.KIND.eq("container")).and(INSTANCE.REMOVED.isNull()) + .and(SERVICE_EXPOSE_MAP.ACCOUNT_ID.eq(accountId)).and(SERVICE_EXPOSE_MAP.REMOVED.isNull()) + .and(SERVICE.ACCOUNT_ID.eq(accountId)).and(SERVICE.REMOVED.isNull()); + } + + @Override + public boolean isUsageServiceCurrent(long accountId, long instanceId, String instanceUuid, long serviceId, long stackId, + Long hostId, Long hostMappingId) { + if ((hostId == null) != (hostMappingId == null)) return false; + var current = usageServices(accountId, instanceId, instanceUuid).and(INSTANCE.STACK_ID.isNull()) + .and(SERVICE.ID.eq(serviceId)).and(SERVICE.STACK_ID.eq(stackId)) + .andExists(DSL.selectOne().from(STACK).where(STACK.ID.eq(stackId)) + .and(STACK.ACCOUNT_ID.eq(accountId)).and(STACK.REMOVED.isNull())); + current.and(DSL.field(DSL.selectCount().from(usageServices(accountId, instanceId, instanceUuid).limit(2).asTable("bindings"))).eq(1)); + if (hostId != null) current.andExists(DSL.selectOne().from(INSTANCE_HOST_MAP).join(HOST).on(HOST.ID.eq(INSTANCE_HOST_MAP.HOST_ID)) + .where(INSTANCE_HOST_MAP.ID.eq(hostMappingId)).and(INSTANCE_HOST_MAP.INSTANCE_ID.eq(instanceId)) + .and(HOST.ID.eq(hostId)).and(HOST.ACCOUNT_ID.eq(accountId)).and(HOST.REMOVED.isNull()) + .and(INSTANCE_HOST_MAP.REMOVED.isNull())); + return create().fetchExists(current); + } + @Override public Map> getInstances(List ids, final IdFormatter idFormatter) { final Map> result = new HashMap<>(); @@ -163,7 +200,8 @@ public Map> getServiceLinks(List ids) { result.put(serviceId, links); } - String consumedServiceName = (record.getValue(SERVICE_CONSUME_MAP.CONSUMED_SERVICE)) != null ? record.getValue(SERVICE_CONSUME_MAP.CONSUMED_SERVICE) : record.getValue(SERVICE.NAME); + String consumedServiceName = (record.getValue(SERVICE_CONSUME_MAP.CONSUMED_SERVICE)) != null + ? record.getValue(SERVICE_CONSUME_MAP.CONSUMED_SERVICE) : record.getValue(SERVICE.NAME); links.add(new ServiceLink( record.getValue(SERVICE_CONSUME_MAP.NAME), diff --git a/code/iaas/resource-pool/pom.xml b/code/iaas/resource-pool/pom.xml index f727aabbfb..65cbba222c 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.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/iaas/service-discovery/api/pom.xml b/code/iaas/service-discovery/api/pom.xml index bb86f76794..f0abacb89c 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.369 + 0.183.370 ../../../parent/pom.xml diff --git a/code/iaas/service-discovery/server/pom.xml b/code/iaas/service-discovery/server/pom.xml index e29f77edf7..6ffc7208d9 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.369 + 0.183.370 ../../../parent/pom.xml diff --git a/code/iaas/ssh-common/pom.xml b/code/iaas/ssh-common/pom.xml index 66bdb0a2fd..d421b56e99 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.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/iaas/storage-service/pom.xml b/code/iaas/storage-service/pom.xml index 6cc7411b30..792421c3c2 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.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/iaas/task-jooq/pom.xml b/code/iaas/task-jooq/pom.xml index 8a52f7810a..1dbeb7beb8 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.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/implementation/activity-log/pom.xml b/code/implementation/activity-log/pom.xml index eb26651792..1bd309f293 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.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/implementation/agent-instance-impl/pom.xml b/code/implementation/agent-instance-impl/pom.xml index adf076be13..14e8ec8b44 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.369 + 0.183.370 ../../parent/pom.xml cattle-agent-instance-impl diff --git a/code/implementation/docker/api/pom.xml b/code/implementation/docker/api/pom.xml index ebe8739007..9bcf52374c 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.369 + 0.183.370 ../../../parent/pom.xml diff --git a/code/implementation/docker/common/pom.xml b/code/implementation/docker/common/pom.xml index 98216c8b4e..13b1b466d5 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.369 + 0.183.370 ../../../parent/pom.xml diff --git a/code/implementation/docker/compute/pom.xml b/code/implementation/docker/compute/pom.xml index 64365d9d3f..96200c1fd7 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.369 + 0.183.370 ../../../parent/pom.xml diff --git a/code/implementation/docker/machine/pom.xml b/code/implementation/docker/machine/pom.xml index 5660a0aade..39370f6627 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.369 + 0.183.370 ../../../parent/pom.xml diff --git a/code/implementation/docker/storage/pom.xml b/code/implementation/docker/storage/pom.xml index 375cd8e3e3..a99aafa8c7 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.369 + 0.183.370 ../../../parent/pom.xml @@ -21,12 +21,12 @@ io.cattle cattle-docker-common - 0.183.369 + 0.183.370 io.cattle cattle-iaas-allocator - 0.183.369 + 0.183.370 diff --git a/code/implementation/extension-api/pom.xml b/code/implementation/extension-api/pom.xml index b63929f0da..71b64504bb 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.369 + 0.183.370 ../../parent/pom.xml cattle-extension-api diff --git a/code/implementation/hazelcast/common/pom.xml b/code/implementation/hazelcast/common/pom.xml index b78fd309b5..91e8c3ebbd 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.369 + 0.183.370 ../../../parent/pom.xml diff --git a/code/implementation/hazelcast/eventing/pom.xml b/code/implementation/hazelcast/eventing/pom.xml index bec314c33c..0cf46c743e 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.369 + 0.183.370 ../../../parent/pom.xml diff --git a/code/implementation/hazelcast/lock/pom.xml b/code/implementation/hazelcast/lock/pom.xml index 70cae8321e..b3f0d155f2 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.369 + 0.183.370 ../../../parent/pom.xml diff --git a/code/implementation/host-api/pom.xml b/code/implementation/host-api/pom.xml index e571dcb596..f9e5e1fec4 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.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/implementation/host-stats/pom.xml b/code/implementation/host-stats/pom.xml index 77b2127eb5..660dfef481 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.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/implementation/register/pom.xml b/code/implementation/register/pom.xml index 81a5cc3a24..3320f2477e 100644 --- a/code/implementation/register/pom.xml +++ b/code/implementation/register/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/implementation/sample-setup/pom.xml b/code/implementation/sample-setup/pom.xml index 0f30f955bd..fe2a079792 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.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/implementation/settings-api/pom.xml b/code/implementation/settings-api/pom.xml index 659e206e3b..d9fc5e4ba3 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.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/implementation/simulator/agent-connection/pom.xml b/code/implementation/simulator/agent-connection/pom.xml index b532d9de07..8f015eb9e7 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.369 + 0.183.370 ../../../parent/pom.xml diff --git a/code/implementation/simulator/storage/pom.xml b/code/implementation/simulator/storage/pom.xml index 0a9cefeade..8b7541304e 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.369 + 0.183.370 ../../../parent/pom.xml diff --git a/code/implementation/system-stack/pom.xml b/code/implementation/system-stack/pom.xml index 68aedb692f..9a4ce37680 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.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/implementation/vm/pom.xml b/code/implementation/vm/pom.xml index 5bf7d79dad..c5a0b10947 100644 --- a/code/implementation/vm/pom.xml +++ b/code/implementation/vm/pom.xml @@ -3,7 +3,7 @@ cattle-parent io.cattle - 0.183.369 + 0.183.370 ../../parent/pom.xml cattle-vm diff --git a/code/meta-parent/pom.xml b/code/meta-parent/pom.xml index 20450ce20c..bc83349a15 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.369 + 0.183.370 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 419a913ce2..75bb5eb853 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.369 + 0.183.370 ../../parent/pom.xml jar diff --git a/code/packaging/app-config/src/main/java/io/cattle/platform/app/SystemServicesConfig.java b/code/packaging/app-config/src/main/java/io/cattle/platform/app/SystemServicesConfig.java index 529535dc83..b99f7301b5 100644 --- a/code/packaging/app-config/src/main/java/io/cattle/platform/app/SystemServicesConfig.java +++ b/code/packaging/app-config/src/main/java/io/cattle/platform/app/SystemServicesConfig.java @@ -437,10 +437,12 @@ DefaultObjectMetaDataManager DefaultObjectMetaDataManager(ExtensionManagerImpl e @Bean DefaultObjectManager defaultObjectManager(@Qualifier("LockingJooqConfiguration") io.cattle.platform.db.jooq.config.Configuration config, - @Qualifier("CoreSchemaFactory") SchemaFactory schemaFactory) { + @Qualifier("CoreSchemaFactory") SchemaFactory schemaFactory, + org.springframework.beans.factory.ObjectProvider handlers) { DefaultObjectManager om = new DefaultObjectManager(); om.setLockingConfiguration(config); om.setSchemaFactory(schemaFactory); + om.setLifeCycleHandlerSupplier(org.springframework.util.function.SingletonSupplier.of(() -> handlers.orderedStream().toList())); return om; } diff --git a/code/packaging/app-config/src/test/java/io/cattle/platform/app/AiCatalogLaunchObjectLifecycleWiringTest.java b/code/packaging/app-config/src/test/java/io/cattle/platform/app/AiCatalogLaunchObjectLifecycleWiringTest.java new file mode 100644 index 0000000000..155cf0249a --- /dev/null +++ b/code/packaging/app-config/src/test/java/io/cattle/platform/app/AiCatalogLaunchObjectLifecycleWiringTest.java @@ -0,0 +1,202 @@ +package io.cattle.platform.app; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; + +import io.cattle.platform.api.resource.AbstractObjectResourceManager; +import io.cattle.platform.core.model.Stack; +import io.cattle.platform.core.model.tables.StackTable; +import io.cattle.platform.core.model.tables.records.StackRecord; +import io.cattle.platform.iaas.api.auth.ai.catalog.AiCatalogLaunchBinding; +import io.cattle.platform.iaas.api.auth.ai.catalog.AiCatalogLaunchPlanStore; +import io.cattle.platform.json.JacksonJsonMapper; +import io.cattle.platform.object.impl.DefaultObjectManager; +import io.cattle.platform.object.impl.TransactionDelegate; +import io.cattle.platform.object.lifecycle.ObjectLifeCycleHandler; +import io.cattle.platform.object.meta.ObjectMetaDataManager; +import io.cattle.platform.object.postinit.ObjectPostInstantiationHandler; +import io.cattle.platform.object.process.StandardProcess; +import io.github.ibuildthecloud.gdapi.factory.SchemaFactory; +import io.github.ibuildthecloud.gdapi.request.ApiRequest; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; +import org.h2.jdbcx.JdbcDataSource; +import org.jooq.SQLDialect; +import org.jooq.impl.DSL; +import org.junit.Test; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; + +public class AiCatalogLaunchObjectLifecycleWiringTest { + @Test public void actualSpringFactoryLazilyBindsNativeStackBeforeCreateProcessScheduling() throws Exception { + try (Fixture f = new Fixture(true)) { + assertEquals(0, f.handlerConstructions.get()); + DefaultObjectManager objects = f.context.getBean(DefaultObjectManager.class); + Map plan = f.plan("catalog-one"); + AiCatalogLaunchBinding binding = f.context.getBean(AiCatalogLaunchBinding.class); + NativeManager manager = new NativeManager(objects, f.store(), plan); + Stack created = envelope(binding, plan, () -> manager.createStack("catalog-one")); + assertEquals(1, f.handlerConstructions.get()); + assertSame("Stable singleton handlers resolve once", objects.getLifeCycleHandlers(), objects.getLifeCycleHandlers()); + assertEquals(created.getId(), plan.get("internalStackId")); + assertEquals(created.getId().longValue(), ((Number) f.store().forStack(created.getId()).get("internalStackId")).longValue()); + assertEquals(1, f.store().saves); + assertEquals(List.of("bound-before-schedule"), manager.scheduled); + manager.expected = null; + manager.createStack("ordinary-after-catalog"); + assertEquals("Ordinary creates do not write a Catalog plan", 1, f.store().saves); + } + } + + @Test public void emptyHandlersAndLegacyListOverrideRemainValidWithoutEagerDependencyCycles() throws Exception { + try (Fixture f = new Fixture(false)) { + DefaultObjectManager objects = f.context.getBean(DefaultObjectManager.class); + assertTrue(objects.getLifeCycleHandlers().isEmpty()); + NativeManager manager = new NativeManager(objects, null, null); + manager.createStack("zero-handlers"); + AtomicInteger events = new AtomicInteger(); + ObjectLifeCycleHandler handler = new ObjectLifeCycleHandler() { + @Override public T onEvent(LifeCycleEvent event, T instance, Class type, Map properties) { + events.incrementAndGet(); return instance; + } + }; + objects.setLifeCycleHandlers(List.of(handler)); + manager.createStack("legacy-list"); + assertEquals(1, events.get()); + objects.setLifeCycleHandlerSupplier(List::of); + manager.createStack("supplier-overrides-list"); + assertEquals(1, events.get()); + objects.setLifeCycleHandlers(null); + manager.createStack("legacy-null-overrides-supplier"); + assertNull(objects.getLifeCycleHandlers()); + } + } + + @Test public void nativeCreateBindsExactlyOnceAndNestedFailureRestoresThreadLocalEnvelope() throws Exception { + try (Fixture f = new Fixture(true)) { + DefaultObjectManager objects = f.context.getBean(DefaultObjectManager.class); + AiCatalogLaunchBinding binding = f.context.getBean(AiCatalogLaunchBinding.class); + Map outer = f.plan("outer"); + NativeManager manager = new NativeManager(objects, f.store(), outer); + envelope(binding, outer, () -> { + assertThrows(IllegalStateException.class, () -> envelope(binding, f.plan("inner"), () -> { + throw new IllegalStateException("Synthetic failure before native create"); + })); + return manager.createStack("outer"); + }); + assertEquals(1, f.store().saves); + assertThrows(io.github.ibuildthecloud.gdapi.exception.ClientVisibleException.class, + () -> envelope(binding, outer, () -> manager.createStack("outer"))); + assertEquals("A bound plan cannot schedule another native create", 1, manager.scheduled.size()); + assertEquals(1, f.store().saves); + manager.expected = null; + manager.createStack("ordinary-after-exception"); + assertEquals("ThreadLocal cleared after success and exception", 1, f.store().saves); + } + } + + private static Stack envelope(AiCatalogLaunchBinding binding, Map plan, Supplier action) { + try { + var method = AiCatalogLaunchBinding.class.getDeclaredMethod("create", Map.class, Supplier.class); + method.setAccessible(true); + return Stack.class.cast(method.invoke(binding, plan, action)); + } catch (InvocationTargetException failure) { + if (failure.getCause() instanceof RuntimeException runtime) throw runtime; + throw new AssertionError(failure.getCause()); + } catch (ReflectiveOperationException failure) { throw new AssertionError(failure); } + } + + static class CountingStore extends AiCatalogLaunchPlanStore { + int saves; + @Override public void save(Map plan) { super.save(plan); saves++; } + } + + static class NativeManager extends AbstractObjectResourceManager { + final CountingStore store; + final List scheduled = new ArrayList<>(); + Map expected; + NativeManager(DefaultObjectManager objects, CountingStore store, Map expected) { + setObjectManager(objects); this.store = store; this.expected = expected; + } + Stack createStack(String name) { return (Stack) createAndScheduleObject(StackRecord.class, Map.of("name", name)); } + @Override protected void scheduleProcess(StandardProcess process, Object resource, Map data) { + assertEquals(StandardProcess.CREATE, process); + if (expected != null) { + Stack stack = (Stack) resource; + assertEquals(stack.getId(), expected.get("internalStackId")); + assertNotNull("The real plan SQL row must bind before scheduling", store.forStack(stack.getId())); + scheduled.add("bound-before-schedule"); + } else scheduled.add("ordinary"); + } + @Override protected Object removeFromStore(String type, String id, Object obj, ApiRequest request) { + throw new UnsupportedOperationException(); + } + @Override protected Object listInternal(SchemaFactory schemas, String type, Map criteria, + io.github.ibuildthecloud.gdapi.model.ListOptions options) { + throw new UnsupportedOperationException(); + } + @Override public String[] getTypes() { return new String[] { "stack" }; } + @Override public Class[] getTypeClasses() { return new Class[] { Stack.class }; } + } + + static class Fixture implements AutoCloseable { + final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + final AtomicInteger handlerConstructions = new AtomicInteger(); + final io.cattle.platform.db.jooq.config.Configuration configuration = new io.cattle.platform.db.jooq.config.Configuration(); + Fixture(boolean withBinding) throws Exception { + JdbcDataSource source = new JdbcDataSource(); + source.setURL("jdbc:h2:mem:lifecycle-" + UUID.randomUUID() + ";DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE"); + configuration.set(source); configuration.set(SQLDialect.H2); + configuration.settings().setRenderSchema(false); + DSL.using(configuration).createTable(StackTable.STACK).columns(StackTable.STACK.fields()).execute(); + DSL.using(configuration).execute("create table ai_catalog_stack_launch_plan " + + "(id varchar primary key, state varchar, document clob, stack_id bigint)"); + context.getBeanFactory().registerSingleton("configuration", configuration); + context.getBeanFactory().registerSingleton("CoreTransactionManager", new DataSourceTransactionManager(source)); + context.getBeanFactory().registerSingleton("metadata", mock(ObjectMetaDataManager.class)); + context.getBeanFactory().registerSingleton("transactionDelegate", mock(TransactionDelegate.class)); + context.registerBean(JacksonJsonMapper.class); + context.registerBean(ObjectPostInstantiationHandler.class, () -> new ObjectPostInstantiationHandler() { + @Override public T postProcess(T instance, Class type, Map properties) { + Stack row = (Stack) instance; + row.setAccountId(5L); row.setKind("stack"); row.setName((String) properties.get("name")); + row.setUuid(UUID.randomUUID().toString()); row.setState("creating"); row.setSystem(false); + return instance; + } + }); + context.registerBean(DefaultObjectManager.class, () -> { + ObjectProvider handlers = context.getBeanProvider(ObjectLifeCycleHandler.class); + return new SystemServicesConfig().defaultObjectManager(configuration, mock(SchemaFactory.class), handlers); + }); + if (withBinding) { + context.registerBean(CountingStore.class, () -> { + assertNotNull("Handler dependency may depend on the already constructed ObjectManager", + context.getBean(DefaultObjectManager.class)); + return new CountingStore(); + }, definition -> definition.setLazyInit(true)); + context.registerBean(AiCatalogLaunchBinding.class, () -> { + handlerConstructions.incrementAndGet(); + return new IaasApiConfig().AiCatalogLaunchBinding(); + }, definition -> definition.setLazyInit(true)); + } + context.refresh(); + } + CountingStore store() { return context.getBean(CountingStore.class); } + Map plan(String name) { + Map plan = new LinkedHashMap<>(); + plan.put("id", "aicataloglaunch-" + UUID.randomUUID()); plan.put("status", "dispatching"); + plan.put("name", name); plan.put("accountId", 5L); + DSL.using(configuration).execute("insert into ai_catalog_stack_launch_plan(id,state,document) values (?, ?, ?)", + plan.get("id"), "dispatching", "{}"); + return plan; + } + @Override public void close() { context.close(); } + } +} diff --git a/code/packaging/app-config/src/test/java/io/cattle/platform/iaas/api/auditing/TypedResourceAuditContractTest.java b/code/packaging/app-config/src/test/java/io/cattle/platform/iaas/api/auditing/TypedResourceAuditContractTest.java new file mode 100644 index 0000000000..e213c63c31 --- /dev/null +++ b/code/packaging/app-config/src/test/java/io/cattle/platform/iaas/api/auditing/TypedResourceAuditContractTest.java @@ -0,0 +1,94 @@ +package io.cattle.platform.iaas.api.auditing; + +import static org.junit.Assert.*; + +import io.cattle.platform.api.auth.Policy; +import io.cattle.platform.api.formatter.DefaultIdFormatter; +import io.cattle.platform.iaas.api.auditing.dao.AuditLogDao; +import io.cattle.platform.iaas.api.auth.change.AiResourceChangeDescriptor; +import io.github.ibuildthecloud.gdapi.factory.SchemaFactory; +import io.github.ibuildthecloud.gdapi.json.JacksonMapper; +import io.github.ibuildthecloud.gdapi.request.ApiRequest; +import java.lang.reflect.Proxy; +import java.nio.file.Files; +import java.nio.file.attribute.AclEntry; +import java.nio.file.attribute.AclEntryFlag; +import java.nio.file.attribute.AclEntryPermission; +import java.nio.file.attribute.AclEntryType; +import java.nio.file.attribute.AclFileAttributeView; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/** Composition contract uses the existing app-config dependency closure; no runtime module dependency is added. */ +public class TypedResourceAuditContractTest { + @Rule public TemporaryFolder temporary = new TemporaryFolder(); + + @Test public void everyRegisteredTypedDomainHasRealClosedMetadataAuditAllowAndDeny() throws Exception { + var registry = AiResourceChangeDescriptor.class.getDeclaredField("DESCRIPTORS"); + registry.setAccessible(true); Object value = registry.get(null); + assertTrue(value instanceof Map); + Set types = new TreeSet<>(); + for (Object entry : ((Map) value).values()) { + assertTrue(entry instanceof AiResourceChangeDescriptor); + types.add(((AiResourceChangeDescriptor) entry).resourceType()); + } + assertTrue(types.contains("host")); + List> delivered = new ArrayList<>(); AuditServiceImpl owner = owner(delivered); + ApiRequest request = new ApiRequest(null, null); request.setType("aiResourceChangePlan"); request.setMethod("POST"); + request.setRequestObject(Map.of("name", "PRIVATE-NAME", "description", "PRIVATE-DESCRIPTION", + "data", Map.of("credentials", "PRIVATE-CREDENTIAL"))); + request.setAttribute("apiKey.audit.keyId", "1c8"); request.setAttribute("apiKey.audit.policyRevision", 3L); + request.setAttribute("apiKey.audit.verifiedPrincipalAccountId", 42L); + Policy actor = (Policy) Proxy.newProxyInstance(Policy.class.getClassLoader(), new Class[] {Policy.class}, + (proxy, method, args) -> { + if (method.getName().equals("getAccountId")) return 77L; + if (method.getName().equals("getAuthenticatedAsAccountId")) return 42L; + return null; + }); + String receipt = "aichange-11111111-1111-1111-1111-111111111111"; + for (String type : types) for (boolean allowed : List.of(true, false)) { + owner.recordResourceChangeDecision(request, actor, "preview", receipt, type, 10L, allowed, + allowed ? "ResourceChangePreviewCreated" : "KeyScopeDenied"); + Map event = delivered.getLast(); + assertEquals(type, event.get("targetType")); assertEquals(allowed ? "ALLOW" : "DENY", event.get("decision")); + assertEquals("admission", event.get("phase")); assertEquals(receipt, event.get("requestId")); + assertEquals("1c8", event.get("keyId")); assertEquals(3L, ((Number) event.get("policyRevision")).longValue()); + assertFalse(event.toString().contains("PRIVATE")); assertFalse(event.containsKey("requestObject")); + } + assertEquals(types.size() * 2, delivered.size()); + int count = delivered.size(); + for (String unknown : List.of("plugin", "Host", "secret", "host/credentials")) { + assertThrows(IllegalArgumentException.class, () -> owner.recordResourceChangeDecision(request, actor, + "preview", receipt, unknown, 10L, true, "ResourceChangePreviewCreated")); + } + assertEquals(count, delivered.size()); + } + + private AuditServiceImpl owner(List> delivered) throws Exception { + AuditServiceImpl owner = new AuditServiceImpl(); JacksonMapper json = new JacksonMapper(); json.init(); + owner.jsonMapper = json; owner.idFormatter = new DefaultIdFormatter(); + ((DefaultIdFormatter) owner.idFormatter).setSchemaFactory((SchemaFactory) Proxy.newProxyInstance(SchemaFactory.class.getClassLoader(), + new Class[] {SchemaFactory.class}, (proxy, method, args) -> method.getName().equals("getBaseType") ? args[0] : null)); + var directory = temporary.newFolder().toPath(); + AclFileAttributeView acl = Files.getFileAttributeView(directory, AclFileAttributeView.class); + if (acl != null) { + var principal = directory.getFileSystem().getUserPrincipalLookupService().lookupPrincipalByName(System.getProperty("user.name")); + Files.setOwner(directory, principal); + acl.setAcl(List.of(AclEntry.newBuilder().setType(AclEntryType.ALLOW).setPrincipal(principal) + .setPermissions(AclEntryPermission.values()).setFlags(AclEntryFlag.DIRECTORY_INHERIT, AclEntryFlag.FILE_INHERIT).build())); + } + owner.apiKeyOutbox = new ApiKeyAuditOutbox(directory, json); + owner.auditLogDao = (AuditLogDao) Proxy.newProxyInstance(AuditLogDao.class.getClassLoader(), new Class[] {AuditLogDao.class}, + (proxy, method, args) -> { + assertEquals("createApiKeyEvent", method.getName()); delivered.add(new HashMap<>((Map) args[2])); return null; + }); + return owner; + } +} diff --git a/code/packaging/app/pom.xml b/code/packaging/app/pom.xml index 2a2a83ce97..717892c4d6 100644 --- a/code/packaging/app/pom.xml +++ b/code/packaging/app/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.369 + 0.183.370 ../../parent/pom.xml war diff --git a/code/packaging/bundle/pom.xml b/code/packaging/bundle/pom.xml index 4ab4449164..36cd201915 100644 --- a/code/packaging/bundle/pom.xml +++ b/code/packaging/bundle/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/packaging/dev/pom.xml b/code/packaging/dev/pom.xml index 522707672e..264874cb09 100644 --- a/code/packaging/dev/pom.xml +++ b/code/packaging/dev/pom.xml @@ -4,7 +4,7 @@ io.cattle cattle-parent - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/packaging/meta/pom.xml b/code/packaging/meta/pom.xml index 66d1dd9b49..d816fdcef5 100644 --- a/code/packaging/meta/pom.xml +++ b/code/packaging/meta/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.369 + 0.183.370 ../../parent/pom.xml diff --git a/code/parent/pom.xml b/code/parent/pom.xml index 9cd59d4f9b..eed20101eb 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.369 + 0.183.370 pom diff --git a/docs/releases/orchestration-engine-0.183.370.md b/docs/releases/orchestration-engine-0.183.370.md new file mode 100644 index 0000000000..c6d326c2f5 --- /dev/null +++ b/docs/releases/orchestration-engine-0.183.370.md @@ -0,0 +1,44 @@ +# Engine 0.183.370 candidate + +This source candidate is not published or runtime-qualified. Matching Web +Console 1.6.209 / Bridge 0.2.11 and a new Server assembly remain separate. + +Host metadata plans now accept only name and description through the existing +human-reviewed typed change owner. Native PUT capabilities, current role and +Key policy, revision/CAS checks and the durable single claim remain mandatory. +Host lifecycle, labels, scheduling and agent configuration are not added. + +Host detail returns at most fifty freshly readable container references from +a native query bounded to fifty-one mappings. Container and Stack visibility +are checked independently. Host and Volume parent resolution follows the native +serviceIds relation only when the persisted parent is truly absent; a bounded +unique ordinary Service and a freshly readable Stack must still match at the +final native check. Hidden, multiple or changed relations stay unknown without +disclosing identities or counts. Empty references never establish unused, +empty or safe-to-delete status. Existing read audit and deletion rules remain. + +Catalog launch now wires the existing lifecycle binding into the real native +ObjectManager. Lazy, memoized composition avoids eager dependency cycles and +binds the Stack ID before its CREATE process can be scheduled. Ordinary creates, +an empty handler set and legacy explicit-list wiring keep their contracts. +The fix does not retry uncertain mutations or retrospectively bind orphan plans. + +Twenty-four new exact required methods join the preserved 142 (166 total). +Focused source checks passed; full original Security/JDK25 and CodeQL gates, +artifact qualification and native business acceptance remain pending. C03 +re-extraction preserves all 1396 HTTP tuples and operation/grant semantics. +No zero-vulnerability or whole-243-group completion claim is made. + +Builder downloads retain Maven 3.9.16 and its exact SHA512 pin, using Apache's +official CDN with the original archive as a transport-only fallback. Checksum +failure remains terminal; this does not change runtime dependencies. +The fixed Guava URL and SHA256 also remain unchanged; bounded curl rejects +HTTP errors before the JAR checksum and installation checks. +The build environments also replace APT and libapt-pkg7.0 with the official +USN-8996-1 version 3.2.1ubuntu0.1 before package operations, addressing +CVE-2026-109518 without changing the release vulnerability threshold. +A narrow native OpenVEX fixed statement covers only these two exact Ubuntu +amd64 package PURLs while the scanner vendor data lags the official fix. CI +verifies all nineteen installed package-owned ELF hashes and the native package +pair on the same image, preserves the original raw report, and rejects any +unrelated finding removal. The existing pip overlay and release thresholds remain. diff --git a/pom.xml b/pom.xml index a1a92f8fe0..9ca5d6b22e 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ io.cattle cattle-parent - 0.183.369 + 0.183.370 code/parent/pom.xml cattle diff --git a/resources/content/schema/base/aiResourceChangePlan.json b/resources/content/schema/base/aiResourceChangePlan.json index 30318cad84..a7c7d7ea60 100644 --- a/resources/content/schema/base/aiResourceChangePlan.json +++ b/resources/content/schema/base/aiResourceChangePlan.json @@ -4,7 +4,7 @@ "resourceMethods": [ "GET" ], "resourceActions": { "confirm": {}, "apply": {}, "cancel": {} }, "resourceFields": { - "resourceType": { "type": "enum", "options": [ "stack", "service", "container", "externalService", "dnsService", "certificate" ], "create": true, "required": true }, + "resourceType": { "type": "enum", "options": [ "stack", "service", "container", "externalService", "dnsService", "certificate", "host" ], "create": true, "required": true }, "operation": { "type": "enum", "options": [ "start", "stop", "restart", "remove", "create", "update_metadata", "update_targets", "scale", "archive", "restore" ], "create": true, "required": true }, "resourceId": { "type": "string", "create": true, "maxLength": 128, "nullable": true }, "parameters": { "type": "json", "create": true, "required": true }, diff --git a/resources/pom.xml b/resources/pom.xml index 39db34a076..f54afa7159 100644 --- a/resources/pom.xml +++ b/resources/pom.xml @@ -4,7 +4,7 @@ cattle-parent io.cattle - 0.183.369 + 0.183.370 ../code/parent/pom.xml diff --git a/scripts/api-key-operation-contract.json b/scripts/api-key-operation-contract.json index 2b7078d51b..90bfa3e39d 100644 --- a/scripts/api-key-operation-contract.json +++ b/scripts/api-key-operation-contract.json @@ -9,9 +9,10 @@ "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": 449, - "sha256": "5bbfd304067c39be44f7415ed00e654e2386ef8827f618a2f13ef6ed1c7562a3", + "count": 458, + "sha256": "7082897c9cfe9d80534ac02a871e0ef4e79356f1c23a618aadd8616be89c3069", "anchors": [ + "AbstractObjectManager.java", "AbstractSchemaFactory.java", "AiAliasServiceChange.java", "AiAuditQuery.java", @@ -51,6 +52,7 @@ "AiCatalogLaunchRunner.java", "AiCatalogLaunchWorkflowTest.java", "AiExternalServiceChange.java", + "AiHostMetadataChange.java", "AiResourceChangeDescriptor.java", "AiResourceChangeDomain.java", "AiResourceChangePlanService.java", @@ -85,6 +87,8 @@ "ExternalDynamicExtensionHandler.java", "FileSchemaFactory.java", "GenericWhitelistedProxy.java", + "HostDao.java", + "HostDaoImpl.java", "IaasApiConfig.java", "InServiceUpgradeStrategy.java", "NotWritablePostProcessor.java", @@ -100,11 +104,14 @@ "ServiceConsumeMapDaoImpl.java", "ServiceCreate.java", "ServiceCreateValidation.java", + "ServiceDao.java", + "ServiceDaoImpl.java", "ServiceDiscoveryServiceImpl.java", "ServiceUpgradeStrategy.java", "ServiceUpgradeValidation.java", "ServiceUpgradeValidationFilter.java", "ServiceValidationFilter.java", + "SystemServicesConfig.java", "TokenAuthLookup.java", "TransformInspect.java", "VolumeDao.java", @@ -253,110 +260,180 @@ "tupleCount": 1396, "tupleSha256": "8f1c0fb378663b0b3d272efba2f211cb210eb6f17cb8035aefa33b56feeaf36a", "extensionReview": { - "version": "unreleased-369-volume-usage-safe-read", + "version": "unreleased-370-host-metadata-readable-workloads-and-catalog-binding", "status": "SOURCE_ONLY_NOT_RUNTIME_ACCEPTANCE", - "scope": "Volume detail adds a read-only bounded usage-reference projection through the existing typed resource view. Native SQL retrieves at most 51 relationships before materialization and exposes at most 50 authorized container targets. Current native object authorization and an exact attested Key query scope remain mandatory; unsupported scoped managers return unknown. Hidden targets create no synthetic API/audit entries. The selected Volume read retains canonical audit and direct hidden-resource requests retain DENY audit. Closed scalar fields reuse native data-field precedence and current-role schema wrapping, excluding transformed/create-only fields before access; parent names require a readable persisted Stack relationship and a fresh authorized Stack read. Completeness is always unknown; no total, hidden identity/count, host source path, write, grant or deletion-oracle change.", + "scope": "Host metadata plans accept only name and description and retain native PUT/role/Key ceilings, exact human approval, revision and one claim. Host detail and Volume usage resolve only freshly authorized container, ordinary Service and Stack relations through bounded native queries. The SEM fallback requires a genuinely NULL persisted parent at capture and final verification; multiple, hidden, removed or moved relations remain unknown without IDs or counts. Catalog lifecycle handlers are lazily composed into the native ObjectManager so the new Stack ID is durably bound before CREATE process scheduling. Ordinary creates and output filters remain unchanged. No new tool, Host lifecycle action, generic endpoint, operation grant or secret output is added.", "previous": { - "version": "unreleased-368-external-service-create-tcp-healthcheck", + "version": "unreleased-369-volume-usage-safe-read", "status": "SOURCE_ONLY_NOT_RUNTIME_ACCEPTANCE", - "scope": "External Service CREATE accepts an optional closed TCP healthCheck subset through its existing typed plan owner. Exactly the five caller fields are reviewed; port is required 1..65535 and four omitted timing fields are not filled. The positive integer timing subset matches existing typed Service validation; native metadata declares no timing minimum or defaults. HTTP controls and unknown, null, wrong-type or fractional values are rejected. The detached native body alone carries requestLine=null. Native schema/role projection is rechecked before preparation and dispatch; exact TCP readback is required for a verified receipt. No UPDATE healthCheck support, new HTTP route, operation semantics, role/Key grant, or approval bypass. Omitted healthCheck retains None and ordinary Service health checks are unchanged.", + "scope": "Volume detail adds a read-only bounded usage-reference projection through the existing typed resource view. Native SQL retrieves at most 51 relationships before materialization and exposes at most 50 authorized container targets. Current native object authorization and an exact attested Key query scope remain mandatory; unsupported scoped managers return unknown. Hidden targets create no synthetic API/audit entries. The selected Volume read retains canonical audit and direct hidden-resource requests retain DENY audit. Closed scalar fields reuse native data-field precedence and current-role schema wrapping, excluding transformed/create-only fields before access; parent names require a readable persisted Stack relationship and a fresh authorized Stack read. Completeness is always unknown; no total, hidden identity/count, host source path, write, grant or deletion-oracle change.", "previous": { - "version": "unreleased-367-service-upgrade-strategy-audit-preview-native-action-lock-order-and-rollback-fence", + "version": "unreleased-368-external-service-create-tcp-healthcheck", "status": "SOURCE_ONLY_NOT_RUNTIME_ACCEPTANCE", - "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.", + "scope": "External Service CREATE accepts an optional closed TCP healthCheck subset through its existing typed plan owner. Exactly the five caller fields are reviewed; port is required 1..65535 and four omitted timing fields are not filled. The positive integer timing subset matches existing typed Service validation; native metadata declares no timing minimum or defaults. HTTP controls and unknown, null, wrong-type or fractional values are rejected. The detached native body alone carries requestLine=null. Native schema/role projection is rechecked before preparation and dispatch; exact TCP readback is required for a verified receipt. No UPDATE healthCheck support, new HTTP route, operation semantics, role/Key grant, or approval bypass. Omitted healthCheck retains None and ordinary Service health checks are unchanged.", "previous": { - "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", "", @@ -364,15 +441,15 @@ "read" ], [ - "aiServiceUpgradePlan", + "aiResourceChangePlan", "collection", "POST", "", "", - "upgrade" + "update" ], [ - "aiServiceUpgradePlan", + "aiResourceChangePlan", "resource", "GET", "", @@ -380,37 +457,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": [ @@ -418,161 +592,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" } ] }, @@ -587,18 +624,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" } ] }, @@ -608,19 +641,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" } ] }, @@ -634,551 +967,300 @@ "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", - "collection", - "POST", - "", - "", - "read" - ], - [ - "aiCatalogStackLaunchPlan", - "collection", - "GET", - "", - "", - "read" - ], - [ - "aiCatalogStackLaunchPlan", + "aiAuditQuery", "collection", "POST", "", "", - "create" - ], - [ - "aiCatalogStackLaunchPlan", - "resource", - "GET", - "", - "", "read" - ], - [ - "aiCatalogStackLaunchPlan", - "resource", - "POST", - "apply", - "", - "create" - ], - [ - "aiCatalogStackLaunchPlan", - "resource", - "POST", - "cancel", - "", - "ai-catalog-launch:cancel" - ], - [ - "aiCatalogStackLaunchPlan", - "resource", - "POST", - "completeInputs", - "", - "ai-catalog-launch:completeInputs" - ], - [ - "aiCatalogStackLaunchPlan", - "resource", - "POST", - "confirm", - "", - "ai-catalog-launch:confirm" ] ], "changedTuples": [ - { - "before": [ - "service", + [ + [ + "dnsService", "resource", "POST", "setservicelinks", "", "action:setservicelinks" ], - "after": [ - "service", + [ + "dnsService", "resource", "POST", "setservicelinks", "", "update" - ], - "rationale": "Only the existing Service.setservicelinks owner is update. The typed launch preflights original live destination update and each delegated exact action; create does not imply update. Unknown plugin roots remain denied." - } + ] + ] ], "authorizationRationale": { - "aiCatalogStackLaunchOptions": "Read-only fixed owner projection; no create grant or secret defaults.", - "aiCatalogStackLaunchPlan": "Dedicated durable plan. Preview/confirm/apply recheck original live destination read/create/update and references; completeInputs/confirm/cancel are verified Cookie-only, including for GENERAL FULL Keys. Apply claims once and normal Stack/Compose owners execute an exact per-event graph, with per-request actor/Key/source revalidation; private answers and proof never enter public DTO or audit payload.", - "serviceSetServiceLinks": "Exact Service action classified update; role schema, original live Key target scope and explicit DENY still apply. No plugin or create-to-update grant.", - "privateOwnerChannel": "Public Catalog/raw proxy initial and redirect targets reject canonical v1-catalog/ai namespace before body/upstream; fixed typed owner loopback calls retain their independent live authorization.", - "aiCatalogMetadataView": "Fixed metadata read POST. Fresh active owner/current-project membership plus live Key PLATFORM catalogTemplate read precede a fixed loopback Catalog projection. No raw proxy/accounts grant, project ancestry inference, forwarded credentials or new JWT. Fine-grained selectors fail closed; template/version logical identities are not cache row IDs." + "aiAuditQuery": "Read-semantic metadata admission only. Original native role/schema/current Key and SQL/row filters authorize each source page. Fixed installed broker read-only CLI aggregates using private original credentials; Engine observes complete owner output and persists canonical ALLOW/DENY/error through existing ApiKeyAuditSink. No client-reported result is authoritative.", + "aiAuditSourcePages": "Exactly project/account/apiKey/auditLog with bounded typed ID/time/page input; no independent input route. Original account/project selection, current membership and each native GET apply. Restricted Keys cannot mutate/export credentials; target-Key query requires original actual Key metadata read. Every Key source page pins revision; native API direct/raw routes retain their prior purpose guard and Full/legacy behavior." }, "sourceInputChanges": [ { "path": "code/framework/api/src/main/java/io/cattle/platform/api/auth/ApiAuditPayload.java", - "before": null, - "after": "697bb6f57b26354f336e14f905bc2a87f8a41acfdf725500652100e90d4e4381" + "before": "697bb6f57b26354f336e14f905bc2a87f8a41acfdf725500652100e90d4e4381", + "after": "16122e192f490b7866d3b0af3ed4981184c938d8446ec9b7d4c892d68cf2d67d" }, { - "path": "code/framework/api/src/main/java/io/cattle/platform/api/auth/ApiCookieAuthentication.java", + "path": "code/framework/api/src/main/java/io/cattle/platform/api/auth/ApiKeyAuditSink.java", "before": null, - "after": "b49fe3b141ac1689e49a583bab05caf7a449dc2b3495b0ba391534bc5812d3c4" + "after": "c04eec56c774f68254c832cc8916b57ec000d5fce3bd07e5cafb65260a48fe9b" }, { - "path": "code/framework/api/src/main/java/io/cattle/platform/api/auth/CatalogLaunchTransformAuthorization.java", + "path": "code/framework/auditing/src/main/java/io/cattle/platform/iaas/api/auditing/AuditLogOutPutFilter.java", "before": null, - "after": "b104bb0aed1ae18dc7485aa1f7c7eddf3b6eb9176b7e7209cb2b89534600e4c6" + "after": "368c21b043bdfc149f3d5a8663cf12b6ad93f0efdec0df6b7e065d62200f4ac7" }, { "path": "code/framework/auditing/src/main/java/io/cattle/platform/iaas/api/auditing/AuditServiceImpl.java", - "before": null, - "after": "1f834204996aabfd6e6d3d5bb520d35b80547c5e77b8edea8f25d8d6010cfd4e" + "before": "1f834204996aabfd6e6d3d5bb520d35b80547c5e77b8edea8f25d8d6010cfd4e", + "after": "a10a5d8e7ca754601ea2c5a876c318069b6639c073e616d283ae71694bc95179" }, { - "path": "code/framework/eventing/src/main/java/io/cattle/platform/eventing/model/EventDiagnostics.java", + "path": "code/framework/engine/src/main/java/io/cattle/platform/engine/process/ProcessAuthorization.java", "before": null, - "after": "affeccbf8445e1d61b525df683504de7c084642a52cec8e6fb9371ca935c3034" + "after": "a717b03de1ec528f7047161679be436535d68fb416b573c3aaf5b09a2e44b520" }, { - "path": "code/iaas/api-logic/src/main/java/io/cattle/platform/iaas/api/request/handler/GenericWhitelistedProxy.java", - "before": null, - "after": "9606a7e0f76cb4c7eba033afc897d6d9e5d61045262d582ee94604169817608a" + "path": "code/framework/java-server/src/main/java/io/github/ibuildthecloud/gdapi/request/resource/ResourceManager.java", + "before": "8f5f72c17541e73cab9f59ad8d36c06593baef517628a923967bc4c18f82b999", + "after": "ef08c305584fe79e2218d360dbc2ce6f3b29b64f4123ac325c8473430bc0a1f1" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchAccess.java", - "before": null, - "after": "639ceb2151b6668af77ff2f71ead2cd7699d76d45c17521e46a90af624286b52" + "path": "code/framework/java-server/src/main/java/io/github/ibuildthecloud/gdapi/request/resource/impl/FilteredResourceManager.java", + "before": "a140040f0be95fd7872eb7f7e78f5824329725a109d7bc0c79d960dbc0709e77", + "after": "d2c6a28809df4912b2b22ce22e9f4e48304b91c993144867d05fa5d65f07ae76" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchBinding.java", + "path": "code/framework/schema/src/main/java/io/cattle/platform/schema/processor/NotWritablePostProcessor.java", "before": null, - "after": "9f75784fa67b7341439d3ffecf8bada16ef0e55a7db3bbb41d6eceed4db90e8d" + "after": "c42a2b1aea75d4daae5671c702e3242c9cd1e7d32e7a5280fc3a0f6415ca829a" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchDelegation.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQuery.java", "before": null, - "after": "e75f911701471e63f85b70df645040a4142704f8da94f3008ed2187c43b35a1d" + "after": "6ef0559fdec19c2fe76742587dd5b5c88b5ec904ee3544f70dd6937fffe1e455" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchGateway.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryResourceManager.java", "before": null, - "after": "9a12293de31831d7dda5f2e787b809351c347bf95e7b95f6798bb7e18d5ae12c" + "after": "bdeaebe2fbc3f84be1c9bc19b0bcc7d534a5d43888cc3a95ec446d300458c8c2" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchGraph.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryRunner.java", "before": null, - "after": "26618418c19bd7eef3dc7e5e4cd526072f3a614535a04b914e0559a20f12d875" + "after": "608fa5f5b412dccdf154290094ff1d67204c092cfb60b850d8d7a3124f53c812" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchInput.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryService.java", "before": null, - "after": "3db05d510e3e057ae5d10340e0e313111de0a5a7e5558a477e91395f1f4280de" + "after": "4067cc8685399b2b2bfb9e7b5eaddd10b906f63fccf286ef7e118622e4e80ea6" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchMaterializer.java", - "before": null, - "after": "00945aae0ccd8091f6835168bd0d0ff7615be2f00cc86c27f48218520d680f50" + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchResponseFilter.java", + "before": "0e35935d8e3ce19a843d3770b9a574b0c2d501612a8f6211f077834f7975bb60", + "after": "9884cc5bc272d0a872ab7b48f5365863dfc8154b0e6c291fe418bdc14541be73" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchObservation.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/view/AiAuditSourcePage.java", "before": null, - "after": "e3ecb29c1585002c61a4c3a6e14e94868c4e9de6da94e5098eb5d2f14b93d82d" + "after": "4e47ab9f1fef9108e68a0471b633d3d99f5f5069591e7dcecfe57018f09b6b78" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchOptionsResourceManager.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/view/AiAuditSourcePageInput.java", "before": null, - "after": "fb2977ff6cbd4819f5ecaa75afdcd5c3d9a3c43add3b5c2d4614379349e74552" + "after": "771bdb99ca900a268c900c957d476f3f8d96b31ce45d56583e7c572f3abe73c5" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchOptionsService.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/view/AiResourceViewResourceManager.java", "before": null, - "after": "318bb0e18d8a7292ff57a0b281d53eb5040e7c4a5fef388baea4aac4acf30d5d" + "after": "ea08a26aee7cfa32df3485dd9c4e2e7bd63ca5d2e680cdbabdfcdc5f3c63b8dd" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchOptionsStore.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/view/AiResourceViewService.java", "before": null, - "after": "b4dbbfffcb333ce11c559e10eee78ce2ae9dc1fa5d4a958c475f5a893b270c2b" + "after": "54f9aeaeecbf135e34210417f5cf7ec128a90e05b1971fc351ed4bdd76bf8585" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPlanResourceManager.java", - "before": null, - "after": "ef70bb6e029fdb446358ad81e9f791f14e892c28bda754b8ce479077fb85ff98" + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyAuthorizationService.java", + "before": "d3cac29f7b6f61be0bda38f643115d08877d59e99b9312575f064290e73ede27", + "after": "85a065246a8f40e1d9a64d984f49368d488e5d9b712029b5e3e306ca37aeab47" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPlanService.java", - "before": null, - "after": "77b8f8b2b4ade110064ba0a874db04c1d5adbfe9df68655b8760c395270a05e3" + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyOperations.java", + "before": "28847a8d6873d8e8fe1644a19813e8f8f9d1917a5c1b369dab8abcb882e6a9b6", + "after": "662bbef806ecf3b266699bb00809fe62b712c9610b36788cfb76e244de37940a" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPlanStore.java", - "before": null, - "after": "8716e8f4038bad48098f16c9009446c4e77f379cba50d3049897aa2442fd7b22" + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyProcessAuthorization.java", + "before": "65052468195ad36213f68dc585d63d1cebc13409f7a92704d53297e2efcb482e", + "after": "1bf968d28d977725fa426fe7f32b00d5bc322979c0bfe1e0501fc2c34c03100e" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchPrivate.java", - "before": null, - "after": "df91328e28fca5f15aceda4faaf57be47c8165dfc5cbf36894abd12935c8d3b2" + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyTargetResolver.java", + "before": "95e9c38861b984e995770e5ddf6d7af21c96d1bff1d8848d1ad2e3a54722efd2", + "after": "3c299d683055c0301f93b4bc8d23252215d1d9c0746d0aaafa0b52447d3eb26b" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchResponseFilter.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiAliasServiceChange.java", "before": null, - "after": "0e35935d8e3ce19a843d3770b9a574b0c2d501612a8f6211f077834f7975bb60" + "after": "fa69385c6ffee09261466e995e59474b632302225088729de3242ccaf2b2118b" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchRunner.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiExternalServiceChange.java", "before": null, - "after": "e6e530a39cc6ca2d270038f0084ed6c104f45d69ebfe7a307391e90918c0f6c9" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyAuthorizationService.java", - "before": "9ceda0f5118d38fc45fc83bd4a16eb92717a20f7837bcabb804975600c59d2c5", - "after": "d3cac29f7b6f61be0bda38f643115d08877d59e99b9312575f064290e73ede27" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyOperations.java", - "before": "494a57d48564da85f14de564a82906444fa732f1adb201fe27d8c0ca88ea65e5", - "after": "28847a8d6873d8e8fe1644a19813e8f8f9d1917a5c1b369dab8abcb882e6a9b6" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyTargetResolver.java", - "before": "645652a1352b74cf6ad4c2b1a9bbdfee1d168cef9df7c59e459d84c17d547c17", - "after": "95e9c38861b984e995770e5ddf6d7af21c96d1bff1d8848d1ad2e3a54722efd2" + "after": "d9497a9c37842c09e833d434e5f361585f721a72eff9976c5b66902181f392e5" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/impl/ApiAuthenticator.java", - "before": "7a3e331dc9e23ae1689495b6ce4066a9decb8293def66fbffa6e0e404697db12", - "after": "d412d32cf7032b4bb0dde546a9e22193fc29ecae71b5c1efcbb052bd4590945d" + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDescriptor.java", + "before": null, + "after": "2ef7555c7864a2ce91a7c28951e895da412f68849b6cc3ec04350a75b35186f8" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/integration/internal/rancher/TokenAuthLookup.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDomain.java", "before": null, - "after": "11c66db998415be5deab8a34df362dcb299a7b1a8df938ef1afb6f6602b71c91" + "after": "aafe0ea7a5f60f7e9dc926067318cb2d470ec066cc1e5986b76b2c79122570a4" }, { - "path": "code/iaas/logic-common/src/main/java/io/cattle/platform/process/common/handler/EventBasedProcessHandler.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanService.java", "before": null, - "after": "d784906d3bd0e0399dc38cd38ddc5ee38c49850e7a51ea07d2a4a158dcb068de" + "after": "d0605f3e94757996e699e4083b1cd314d6d68acf623358a92f9841bc75384fb9" }, { - "path": "code/iaas/logic-common/src/main/java/io/cattle/platform/process/common/handler/ProcessEventDecorator.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanStore.java", "before": null, - "after": "b0dc3d894c3ae0345e374cda8af588ac2e6c17c74f25501e6689a28d4ef934e5" + "after": "9862ed000b4b40a6a7643968313c250fad9b1e718a684dccbab8f35c3a29cd5f" }, { - "path": "code/iaas/service-discovery/api/src/main/java/io/cattle/platform/servicediscovery/api/filter/ArchiveValidationFilter.java", - "before": "63fbdcc3d0ac73186b181afc55d0e0aae4424a347a367724348802246cad823c", - "after": "908e6e0bf0d665d2392481f6d6e59d27c3cd89b4b35471fe587d6ed14e591890" + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeProcessObserver.java", + "before": null, + "after": "6f7898590bb64e55cb815c9b1a51f4ce00540a6268e8fa4914ef1b6b445f5a70" }, { - "path": "code/implementation/docker/api/src/main/java/io/cattle/platform/docker/api/transform/TransformInspect.java", + "path": "code/iaas/service-discovery/server/src/main/java/io/cattle/platform/servicediscovery/dao/impl/ServiceConsumeMapDaoImpl.java", "before": null, - "after": "c67974b0abbb3b7c7801b51ca95a649e95e0853fba9c1500da6b4b5977a0afb2" + "after": "a127711511a3f1271d441235f9889d63e06a5c2d457974e4a45bd04f1e7e5747" }, { "path": "code/packaging/app-config/src/main/java/io/cattle/platform/app/IaasApiConfig.java", - "before": null, - "after": "02184dc6ae781547e850c80cf51ddb4f33cbc7b2f799aba3ca1e59c8eb9cf34f" + "before": "02184dc6ae781547e850c80cf51ddb4f33cbc7b2f799aba3ca1e59c8eb9cf34f", + "after": "59b633a3f6614f811b5f14b0eeeeb1f99683d319a2c28bea2093f48a33a53130" }, { "path": "resources/content/schema/admin/admin-auth.json", - "before": "79a757118dd66055c6e7e4c03ded4b0dd8ef1965887c50a4c106e15836d4938f", - "after": "6ea49e1130a2b9218e6c060038c73c89be5b9e05b039d3a0b81475e892d21504" + "before": "6ea49e1130a2b9218e6c060038c73c89be5b9e05b039d3a0b81475e892d21504", + "after": "27630fdb4d5cf7a1dcd7e693b84d0b31513d627a64a5d2365119075aca6d4f85" }, { - "path": "resources/content/schema/base/aiCatalogMetadataView.json", + "path": "resources/content/schema/base/aiAuditQuery.json", "before": null, - "after": "5581897314b1cfe9135783fb8846db50ff6f9d62289b5ab35e60ab7ca8b36515" + "after": "c12e694997a08c3e9849413f45b96d7d9cf2471289d35be54a60903d22e609eb" }, { - "path": "resources/content/schema/base/aiCatalogStackLaunchInputs.json", + "path": "resources/content/schema/base/aiAuditSourcePageInput.json", "before": null, - "after": "05aa9a7b459a82efcf0331b3d6c8262e1b5c879643aef6384c67e3a12980bf4c" + "after": "2aca5cba9323daae33a7171ac5f3bec2591cbdbffbdc32cf263ac8398a75e444" }, { - "path": "resources/content/schema/base/aiCatalogStackLaunchOptions.json", - "before": null, - "after": "30137b75ac50b6b04adec688ca56359fd7d49843f8342c423a906b998da5c12a" + "path": "resources/content/schema/base/aiResourceChangePlan.json", + "before": "21b4acfd55e2aa64b725bacd520655daf7f707141e4226343c16a5b6fae47d8c", + "after": "90db2324422f25129edf155127da8e29303b74fe2467797d4459b1df451e9439" }, { - "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/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" } ] }, "schemas": [ "resources/content/schema/base/aiServiceDescriptionUpdate.json", - "resources/content/schema/base/aiServiceUpgradePlan.json", - "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/aiCatalogMetadataView.json", - "resources/content/schema/base/aiCatalogStackLaunchOptions.json", - "resources/content/schema/base/aiCatalogStackLaunchPlan.json", - "resources/content/schema/base/aiCatalogStackLaunchInputs.json", - "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" - ] - ] - ], - "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." - }, - "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" - }, - { - "path": "code/framework/schema/src/main/java/io/cattle/platform/schema/processor/NotWritablePostProcessor.java", - "before": null, - "after": "c42a2b1aea75d4daae5671c702e3242c9cd1e7d32e7a5280fc3a0f6415ca829a" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQuery.java", - "before": null, - "after": "6ef0559fdec19c2fe76742587dd5b5c88b5ec904ee3544f70dd6937fffe1e455" - }, - { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryResourceManager.java", - "before": null, - "after": "bdeaebe2fbc3f84be1c9bc19b0bcc7d534a5d43888cc3a95ec446d300458c8c2" - }, - { - "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" - }, - { - "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/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" - }, - { - "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/change/AiResourceChangeDomain.java", - "before": null, - "after": "aafe0ea7a5f60f7e9dc926067318cb2d470ec066cc1e5986b76b2c79122570a4" - }, + "resources/content/schema/base/aiServiceUpgradePlan.json", + "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/aiCatalogMetadataView.json", + "resources/content/schema/base/aiCatalogStackLaunchOptions.json", + "resources/content/schema/base/aiCatalogStackLaunchPlan.json", + "resources/content/schema/base/aiCatalogStackLaunchInputs.json", + "resources/content/schema/base/aiAuditQuery.json", + "resources/content/schema/base/aiAuditSourcePageInput.json" + ], + "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." + }, + "sourceInputChanges": [ { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanService.java", - "before": null, - "after": "d0605f3e94757996e699e4083b1cd314d6d68acf623358a92f9841bc75384fb9" + "path": "code/framework/auditing/src/main/java/io/cattle/platform/iaas/api/auditing/AuditServiceImpl.java", + "before": "a10a5d8e7ca754601ea2c5a876c318069b6639c073e616d283ae71694bc95179", + "after": "cb6bb0a8ed3dda48bedd3ae89ed78a490f644c1040c5aa16463e1e701250988e" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangePlanStore.java", + "path": "code/framework/engine/src/main/java/io/cattle/platform/engine/context/EngineContext.java", "before": null, - "after": "9862ed000b4b40a6a7643968313c250fad9b1e718a684dccbab8f35c3a29cd5f" + "after": "ba91b4da42d1f2ea5a2b9709818c2a8f4b967c2c730d5681b53d9dbe94ce8968" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeProcessObserver.java", + "path": "code/framework/engine/src/main/java/io/cattle/platform/engine/process/impl/DefaultProcessInstanceImpl.java", "before": null, - "after": "6f7898590bb64e55cb815c9b1a51f4ce00540a6268e8fa4914ef1b6b445f5a70" + "after": "986e14156ce42ac72033fad21289db4e3732469378cb6c9b0ae1d0a4c36376b0" }, { - "path": "code/iaas/service-discovery/server/src/main/java/io/cattle/platform/servicediscovery/dao/impl/ServiceConsumeMapDaoImpl.java", - "before": null, - "after": "a127711511a3f1271d441235f9889d63e06a5c2d457974e4a45bd04f1e7e5747" + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryRunner.java", + "before": "608fa5f5b412dccdf154290094ff1d67204c092cfb60b850d8d7a3124f53c812", + "after": "684fa84a259118a13d5a1ef20f76e05592ef867492e3fc54c1dcb94c06bb5758" }, { - "path": "code/packaging/app-config/src/main/java/io/cattle/platform/app/IaasApiConfig.java", - "before": "02184dc6ae781547e850c80cf51ddb4f33cbc7b2f799aba3ca1e59c8eb9cf34f", - "after": "59b633a3f6614f811b5f14b0eeeeb1f99683d319a2c28bea2093f48a33a53130" + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyProcessAuthorization.java", + "before": "1bf968d28d977725fa426fe7f32b00d5bc322979c0bfe1e0501fc2c34c03100e", + "after": "39ac76abfff5b7a8d95f722f72a4869443ec9c4e99cac754b2690f7ab5be382d" }, { - "path": "resources/content/schema/admin/admin-auth.json", - "before": "6ea49e1130a2b9218e6c060038c73c89be5b9e05b039d3a0b81475e892d21504", - "after": "27630fdb4d5cf7a1dcd7e693b84d0b31513d627a64a5d2365119075aca6d4f85" + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/apikey/ApiKeyTargetResolver.java", + "before": "3c299d683055c0301f93b4bc8d23252215d1d9c0746d0aaafa0b52447d3eb26b", + "after": "e755cdf32b48df5392cc6700e0d86890393828da25f957cdfc60c9e6ce157968" }, { - "path": "resources/content/schema/base/aiAuditQuery.json", - "before": null, - "after": "c12e694997a08c3e9849413f45b96d7d9cf2471289d35be54a60903d22e609eb" + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDescriptor.java", + "before": "2ef7555c7864a2ce91a7c28951e895da412f68849b6cc3ec04350a75b35186f8", + "after": "542f4d8b53b7360a4b72570466f63f027469d5bf2f0e3ce66ea3d6b6ceb6f417" }, { - "path": "resources/content/schema/base/aiAuditSourcePageInput.json", - "before": null, - "after": "2aca5cba9323daae33a7171ac5f3bec2591cbdbffbdc32cf263ac8398a75e444" + "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": "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" } ] }, @@ -1199,57 +1281,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" } ] }, @@ -1270,25 +1320,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" } ] }, @@ -1309,15 +1349,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" } ] }, @@ -1338,25 +1388,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" } ] }, @@ -1377,31 +1433,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" } ] }, @@ -1422,17 +1462,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", @@ -1450,153 +1625,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": "dffdce7b133cf1bc16b410c32f365a6fee5ce807e79802a2b3d52a98da1dfc02", + "newlyBound": true, + "baselineSourceSha256": "dffdce7b133cf1bc16b410c32f365a6fee5ce807e79802a2b3d52a98da1dfc02" + }, + { + "path": "code/iaas/api-logic/src/main/java/io/cattle/platform/iaas/api/volume/VolumePreflightService.java", "before": null, - "after": "237060330f0ae0c092b11018159b6bf9fa214e1e2564d57f6b139dee34c38e13", - "baselineSourceSha256": "237060330f0ae0c092b11018159b6bf9fa214e1e2564d57f6b139dee34c38e13", + "after": "4f2a93e0e00abb68277d09351a0a85c7fa201bd8e1d07e2bbbd8bcaaa5ce18e2", "newlyBound": true, - "sourceBytesChanged": false + "baselineSourceSha256": "4f2a93e0e00abb68277d09351a0a85c7fa201bd8e1d07e2bbbd8bcaaa5ce18e2" }, { - "path": "code/iaas/api-logic/src/main/java/io/cattle/platform/iaas/api/port/PortPreflightService.java", + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/audit/AiAuditQueryInputHandler.java", "before": null, - "after": "6a64ad664eee2f5817e6157b5564f72ac3381cb7f0881e2ef73bab6b80c3a110", - "baselineSourceSha256": "6a64ad664eee2f5817e6157b5564f72ac3381cb7f0881e2ef73bab6b80c3a110", + "after": "46abd6c96d543f84b3884d1dafd4f41db9f6111e0edf9f770a8289f5604390a1", "newlyBound": true, - "sourceBytesChanged": false + "baselineSourceSha256": null }, { - "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/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" + } + ], + "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" } ], - "reviewManifest": "scripts/api-key-operation-review-0.183.366.json" + "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." + } }, "schemas": [ "resources/content/schema/base/aiServiceDescriptionUpdate.json", @@ -1616,275 +1861,114 @@ "changedTuples": [], "sourceInputChanges": [ { - "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/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiExternalServiceChange.java", + "before": "d9497a9c37842c09e833d434e5f361585f721a72eff9976c5b66902181f392e5", + "after": "4475ebc7215960f69ebaf5c5c925a3c2f4b933975efb9205aba16f80d30eaf47" }, { - "path": "code/packaging/app-config/src/main/java/io/cattle/platform/app/IaasApiConfig.java", - "before": "59b633a3f6614f811b5f14b0eeeeb1f99683d319a2c28bea2093f48a33a53130", - "after": "fa64b48420abe59930c464283a773d2dfcccbaeea152fdf1a997d8dcae184e9a", - "newlyBound": false, - "baselineSourceSha256": "59b633a3f6614f811b5f14b0eeeeb1f99683d319a2c28bea2093f48a33a53130" + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDescriptor.java", + "before": "c60650a74476af5faadd8cc76bcab4d073fd294dfac28d8dc176c07daeb7d4dc", + "after": "8b81eaa0446c07c786e588ae8299456bc3c25dca90cf31810c29d1ca5a838e35" }, { - "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" - } - ], - "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" + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDomain.java", + "before": "fc2245d42e31f3426fef91d68bbe8039f67f50b0a47b9e60cd003c83d1776ad0", + "after": "331c7fa5eae1f60d4f33e3f3767c546d684042be6156a756ab60c627feef443a" } ], + "roleFieldDelta": [], "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." + "externalServiceCreate": "Existing parent/owner/current role/Key/revision, native schema and single durable claim boundaries remain; native cr field only, no update or global grant." } }, - "schemas": [ - "resources/content/schema/base/aiServiceDescriptionUpdate.json", - "resources/content/schema/base/aiServiceUpgradePlan.json", - "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/aiCatalogMetadataView.json", - "resources/content/schema/base/aiCatalogStackLaunchOptions.json", - "resources/content/schema/base/aiCatalogStackLaunchPlan.json", - "resources/content/schema/base/aiCatalogStackLaunchInputs.json", - "resources/content/schema/base/aiAuditQuery.json", - "resources/content/schema/base/aiAuditSourcePageInput.json" - ], - "addedTuples": [], - "changedTuples": [], "sourceInputChanges": [ { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiExternalServiceChange.java", - "before": "d9497a9c37842c09e833d434e5f361585f721a72eff9976c5b66902181f392e5", - "after": "4475ebc7215960f69ebaf5c5c925a3c2f4b933975efb9205aba16f80d30eaf47" + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/view/AiResourceViewService.java", + "before": "54f9aeaeecbf135e34210417f5cf7ec128a90e05b1971fc351ed4bdd76bf8585", + "after": "19d91fa89474c9c9604ff7a688afd9a36e6d75fd313667f16ef5550efedc050b" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDescriptor.java", - "before": "c60650a74476af5faadd8cc76bcab4d073fd294dfac28d8dc176c07daeb7d4dc", - "after": "8b81eaa0446c07c786e588ae8299456bc3c25dca90cf31810c29d1ca5a838e35" + "path": "code/iaas/model/src/main/java/io/cattle/platform/core/dao/VolumeDao.java", + "before": null, + "after": "8fbeb7ef1b63fa000d1788e2603d39de5e6e7dd83eec19df126bcb84510d3823" }, { - "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDomain.java", - "before": "fc2245d42e31f3426fef91d68bbe8039f67f50b0a47b9e60cd003c83d1776ad0", - "after": "331c7fa5eae1f60d4f33e3f3767c546d684042be6156a756ab60c627feef443a" + "path": "code/iaas/model/src/main/java/io/cattle/platform/core/dao/impl/VolumeDaoImpl.java", + "before": null, + "after": "b0a41797307b7a02d44ad5ac08f00bad3fa21fb78143825ac4a01774a8ddcfe6" } - ], - "roleFieldDelta": [], - "authorizationRationale": { - "externalServiceCreate": "Existing parent/owner/current role/Key/revision, native schema and single durable claim boundaries remain; native cr field only, no update or global grant." - } + ] }, "sourceInputChanges": [ + { + "path": "code/framework/auditing/src/main/java/io/cattle/platform/iaas/api/auditing/AuditServiceImpl.java", + "before": "cb6bb0a8ed3dda48bedd3ae89ed78a490f644c1040c5aa16463e1e701250988e", + "after": "df2b080903d9ee35ccb89c0970adbf5b6c88d46fc5a6401eecb620a984b739c4" + }, + { + "path": "code/framework/object/src/main/java/io/cattle/platform/object/impl/AbstractObjectManager.java", + "before": null, + "after": "db0328ff05701ca146e2f770d9bcb34af1d91a73a3e4d59ae432a2c749649539" + }, { "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/view/AiResourceViewService.java", - "before": "54f9aeaeecbf135e34210417f5cf7ec128a90e05b1971fc351ed4bdd76bf8585", - "after": "19d91fa89474c9c9604ff7a688afd9a36e6d75fd313667f16ef5550efedc050b" + "before": "19d91fa89474c9c9604ff7a688afd9a36e6d75fd313667f16ef5550efedc050b", + "after": "9343029c4b9022380b5b03b434d0d42167d17884b0b54dd868c2fa826ad100ea" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiHostMetadataChange.java", + "before": null, + "after": "e08978156bc77733326da1d299c97038103049d441a41f0332e3115b4ac07ce7" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDescriptor.java", + "before": "8b81eaa0446c07c786e588ae8299456bc3c25dca90cf31810c29d1ca5a838e35", + "after": "dd89c69f6f0608176ca8b6daac85f408c572932b434452abddee233b31f77f96" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDomain.java", + "before": "331c7fa5eae1f60d4f33e3f3767c546d684042be6156a756ab60c627feef443a", + "after": "2378a8875ae2a2b76e70d7c57a0af715d02f6165228bc2063139f208298669d7" + }, + { + "path": "code/iaas/model/src/main/java/io/cattle/platform/core/dao/HostDao.java", + "before": null, + "after": "eace82dc713adf92ea4d5b5f0f19db96ad978528956b6abce249b3bf436a25ad" + }, + { + "path": "code/iaas/model/src/main/java/io/cattle/platform/core/dao/ServiceDao.java", + "before": null, + "after": "92a4cd76c988cfab8f3ee3b8e96f2f2ef5fa06c2787ec46ca03cb75ec4f2a7ce" + }, + { + "path": "code/iaas/model/src/main/java/io/cattle/platform/core/dao/impl/HostDaoImpl.java", + "before": null, + "after": "ae0fff79276ce84e8ab5ec19b80b09a78f7c91740c24706e6129be0f15f0f483" }, { - "path": "code/iaas/model/src/main/java/io/cattle/platform/core/dao/VolumeDao.java", + "path": "code/iaas/model/src/main/java/io/cattle/platform/core/dao/impl/ServiceDaoImpl.java", "before": null, - "after": "8fbeb7ef1b63fa000d1788e2603d39de5e6e7dd83eec19df126bcb84510d3823" + "after": "d13a7164f03e71e7fbbedd9165667834b6f33f99607500d2d819ceecab75902b" }, { - "path": "code/iaas/model/src/main/java/io/cattle/platform/core/dao/impl/VolumeDaoImpl.java", + "path": "code/iaas/service-discovery/api/src/main/java/io/cattle/platform/servicediscovery/api/dao/ServiceDao.java", "before": null, - "after": "b0a41797307b7a02d44ad5ac08f00bad3fa21fb78143825ac4a01774a8ddcfe6" + "after": "68efa2475c42844de04d61ec821ea877cea9fdbd7da1600ab2a9dea954a66c93" + }, + { + "path": "code/iaas/service-discovery/server/src/main/java/io/cattle/platform/servicediscovery/dao/impl/ServiceDaoImpl.java", + "before": null, + "after": "05cb4a7254436ddf8583984274d7c1e1567eb6e98d33ba00d17dba24ea312c86" + }, + { + "path": "code/packaging/app-config/src/main/java/io/cattle/platform/app/SystemServicesConfig.java", + "before": null, + "after": "32dba774946a48c2d6a9ec9030fcb5b2ca324ba98cd3269478aec237984a75a8" + }, + { + "path": "resources/content/schema/base/aiResourceChangePlan.json", + "before": "113f44df62f09f822b7f97f4cfdcab96de2f0f70995c821632f801c20134110c", + "after": "b1d67ec8bd3a48b5d9768e6ac15ea92a3ed4203b1bedf512eccbcb7935192baf" } ] }, diff --git a/scripts/api-key-operation-inventory.json b/scripts/api-key-operation-inventory.json index af029e732b..39a7516674 100644 --- a/scripts/api-key-operation-inventory.json +++ b/scripts/api-key-operation-inventory.json @@ -220,18 +220,19 @@ "defaultProcesses": "ebf41ab873c349656c307511712674e0db434a9c4986e08d03ec35d92bdcd6a7" }, "currentSourceReview": { - "version": "unreleased-369-volume-usage-safe-read", + "version": "unreleased-370-host-metadata-readable-workloads-and-catalog-binding", "status": "SOURCE_REEXTRACTED_NOT_ARTIFACT_OR_RUNTIME_ACCEPTANCE", - "baseCommit": "f466d3e514cc08f1d826299826b0b5db27df5d2f", + "baseCommit": "de1372f27117900a8940783efdaa0e90edeb415b", "sourceCandidate": "WORKTREE_NOT_COMMITTED", "producerCommit": "NOT_ASSIGNED_DURING_SOURCE_REVIEW", "contract": "scripts/api-key-operation-contract.json", - "contractSha256": "99aba456727ef358339aa9456aa711e36aa9d72448cf10886a665892b1e2d048", + "contractSha256": "55ad6de95049436b43bcdc55ba910cdc09634cb7845a588980d4749fa945832b", "sourceInputs": { "algorithm": "c03-source-inputs-v1", - "count": 449, - "sha256": "5bbfd304067c39be44f7415ed00e654e2386ef8827f618a2f13ef6ed1c7562a3", + "count": 458, + "sha256": "7082897c9cfe9d80534ac02a871e0ef4e79356f1c23a618aadd8616be89c3069", "anchors": [ + "AbstractObjectManager.java", "AbstractSchemaFactory.java", "AiAliasServiceChange.java", "AiAuditQuery.java", @@ -271,6 +272,7 @@ "AiCatalogLaunchRunner.java", "AiCatalogLaunchWorkflowTest.java", "AiExternalServiceChange.java", + "AiHostMetadataChange.java", "AiResourceChangeDescriptor.java", "AiResourceChangeDomain.java", "AiResourceChangePlanService.java", @@ -305,6 +307,8 @@ "ExternalDynamicExtensionHandler.java", "FileSchemaFactory.java", "GenericWhitelistedProxy.java", + "HostDao.java", + "HostDaoImpl.java", "IaasApiConfig.java", "InServiceUpgradeStrategy.java", "NotWritablePostProcessor.java", @@ -320,11 +324,14 @@ "ServiceConsumeMapDaoImpl.java", "ServiceCreate.java", "ServiceCreateValidation.java", + "ServiceDao.java", + "ServiceDaoImpl.java", "ServiceDiscoveryServiceImpl.java", "ServiceUpgradeStrategy.java", "ServiceUpgradeValidation.java", "ServiceUpgradeValidationFilter.java", "ServiceValidationFilter.java", + "SystemServicesConfig.java", "TokenAuthLookup.java", "TransformInspect.java", "VolumeDao.java", @@ -351,18 +358,17 @@ "roleFieldDelta": [], "historicalInventoryPreserved": true, "previousReview": { - "version": "unreleased-368-external-service-create-tcp-healthcheck", + "version": "unreleased-369-volume-usage-safe-read", "status": "SOURCE_REEXTRACTED_NOT_ARTIFACT_OR_RUNTIME_ACCEPTANCE", - "baseCommit": "8678dec7dd261c2f2b68f329f0722d8edc91ac1f", - "functionalSourceCommit": "2bd0ed5abffd3413422916b1df1aeaecb86fcb6a", + "baseCommit": "f466d3e514cc08f1d826299826b0b5db27df5d2f", "sourceCandidate": "WORKTREE_NOT_COMMITTED", "producerCommit": "NOT_ASSIGNED_DURING_SOURCE_REVIEW", "contract": "scripts/api-key-operation-contract.json", - "contractSha256": "0fb84661147a50f27e51a8a9501ac52fb0d3cabac02e790d6522dfe8c556e5dd", + "contractSha256": "99aba456727ef358339aa9456aa711e36aa9d72448cf10886a665892b1e2d048", "sourceInputs": { "algorithm": "c03-source-inputs-v1", - "count": 447, - "sha256": "da122b39e8ffac069b78d60c7234ff62cbb1995028efa835a7f717f8d02dee4c", + "count": 449, + "sha256": "5bbfd304067c39be44f7415ed00e654e2386ef8827f618a2f13ef6ed1c7562a3", "anchors": [ "AbstractSchemaFactory.java", "AiAliasServiceChange.java", @@ -459,6 +465,8 @@ "ServiceValidationFilter.java", "TokenAuthLookup.java", "TransformInspect.java", + "VolumeDao.java", + "VolumeDaoImpl.java", "VolumePreflightInputs.java", "VolumePreflightService.java" ], @@ -481,17 +489,18 @@ "roleFieldDelta": [], "historicalInventoryPreserved": true, "previousReview": { - "version": "unreleased-367-service-upgrade-strategy-audit-preview-native-action-lock-order-and-rollback-fence", - "status": "SOURCE_REEXTRACTED_NOT_SIGNED_NOT_RUNTIME_ACCEPTANCE", - "baseCommit": "c8cd1f8218d29e034c2569af1e0e8530c305ce50", + "version": "unreleased-368-external-service-create-tcp-healthcheck", + "status": "SOURCE_REEXTRACTED_NOT_ARTIFACT_OR_RUNTIME_ACCEPTANCE", + "baseCommit": "8678dec7dd261c2f2b68f329f0722d8edc91ac1f", + "functionalSourceCommit": "2bd0ed5abffd3413422916b1df1aeaecb86fcb6a", "sourceCandidate": "WORKTREE_NOT_COMMITTED", "producerCommit": "NOT_ASSIGNED_DURING_SOURCE_REVIEW", "contract": "scripts/api-key-operation-contract.json", - "contractSha256": "35b2b2447c0f07205b806ff7304aacb4a90ecface2558701dbde8725a56c50c8", + "contractSha256": "0fb84661147a50f27e51a8a9501ac52fb0d3cabac02e790d6522dfe8c556e5dd", "sourceInputs": { "algorithm": "c03-source-inputs-v1", "count": 447, - "sha256": "66a5ce2ee5f4277993c03de0f72a08dd828ec65e4c447430248c1b093583ab59", + "sha256": "da122b39e8ffac069b78d60c7234ff62cbb1995028efa835a7f717f8d02dee4c", "anchors": [ "AbstractSchemaFactory.java", "AiAliasServiceChange.java", @@ -607,96 +616,25 @@ "addedTuples": [], "removedTuples": [], "changedTuples": [], - "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" - } - ], + "roleFieldDelta": [], "historicalInventoryPreserved": true, "previousReview": { - "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", @@ -740,6 +678,8 @@ "AiResourceViewResourceManager.java", "AiResourceViewService.java", "AiServiceCreateChange.java", + "AiServiceUpgradePlanInputHandler.java", + "AiServiceUpgradePlanService.java", "ApiAuditPayload.java", "ApiAuthenticator.java", "ApiCookieAuthentication.java", @@ -765,6 +705,7 @@ "FileSchemaFactory.java", "GenericWhitelistedProxy.java", "IaasApiConfig.java", + "InServiceUpgradeStrategy.java", "NotWritablePostProcessor.java", "PortBindingAddress.java", "PortPreflightInputs.java", @@ -779,9 +720,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", @@ -799,68 +745,92 @@ "addedTuples": [], "removedTuples": [], "changedTuples": [], - "roleProjectionDelta": [ + "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/aiResourceChangePlan.executionResult", + "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", @@ -907,6 +877,7 @@ "AiResourceChangeProcessObserver.java", "AiResourceViewResourceManager.java", "AiResourceViewService.java", + "AiServiceCreateChange.java", "ApiAuditPayload.java", "ApiAuthenticator.java", "ApiCookieAuthentication.java", @@ -933,12 +904,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" ], @@ -958,18 +937,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", @@ -1068,17 +1097,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", @@ -1179,15 +1208,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", @@ -1254,6 +1283,7 @@ "EngineContext.java", "EventBasedProcessHandler.java", "EventDiagnostics.java", + "ExternalDeploymentUnitInstance.java", "ExternalDynamicExtensionHandler.java", "FileSchemaFactory.java", "GenericWhitelistedProxy.java", @@ -1287,15 +1317,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", @@ -1395,14 +1425,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", @@ -1500,38 +1531,148 @@ "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 + } }, - "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 - } + "reviewManifest": "scripts/api-key-operation-review-0.183.368.json", + "reviewManifestSha256": "71c1ab8fe2080bfda7a52c9a8349754c0c67b48a9eb16b6181a42ce8a4528d15" }, - "reviewManifest": "scripts/api-key-operation-review-0.183.368.json", - "reviewManifestSha256": "71c1ab8fe2080bfda7a52c9a8349754c0c67b48a9eb16b6181a42ce8a4528d15" + "reviewManifest": "scripts/api-key-operation-review-0.183.369.json", + "reviewManifestSha256": "8adb6a02225e5806561146b83a34461eb6d640fddf5a0a8aa552d0ccf81d53c9" }, - "reviewManifest": "scripts/api-key-operation-review-0.183.369.json", - "reviewManifestSha256": "8adb6a02225e5806561146b83a34461eb6d640fddf5a0a8aa552d0ccf81d53c9" + "reviewManifest": "scripts/api-key-operation-review-0.183.370.json", + "reviewManifestSha256": "0d2859a40044d1f1085748b9be65192e9df4d20e2967a02eee1b2f967b8fa4ea" } } diff --git a/scripts/api-key-operation-review-0.183.370.json b/scripts/api-key-operation-review-0.183.370.json new file mode 100644 index 0000000000..8a67b45e6f --- /dev/null +++ b/scripts/api-key-operation-review-0.183.370.json @@ -0,0 +1,119 @@ +{ + "schemaVersion": 1, + "version": "0.183.370", + "scope": "SOURCE_ONLY_NOT_RUNTIME_ACCEPTANCE", + "reviewedBaseCommit": "de1372f27117900a8940783efdaa0e90edeb415b", + "sourceInputs": { + "algorithm": "c03-source-inputs-v1", + "baseline": { + "count": 449, + "sha256": "5bbfd304067c39be44f7415ed00e654e2386ef8827f618a2f13ef6ed1c7562a3" + }, + "reviewed": { + "count": 458, + "sha256": "7082897c9cfe9d80534ac02a871e0ef4e79356f1c23a618aadd8616be89c3069" + }, + "addedAnchors": [ + "AbstractObjectManager.java", + "AiHostMetadataChange.java", + "HostDao.java", + "HostDaoImpl.java", + "ServiceDao.java", + "ServiceDaoImpl.java", + "SystemServicesConfig.java" + ] + }, + "tupleReview": { + "baseline": 1396, + "candidate": 1396, + "added": 0, + "removed": 0, + "semanticChanges": 0, + "dispositionChanges": 0, + "sha256": "8f1c0fb378663b0b3d272efba2f211cb210eb6f17cb8035aefa33b56feeaf36a" + }, + "delta": [ + { + "path": "code/framework/auditing/src/main/java/io/cattle/platform/iaas/api/auditing/AuditServiceImpl.java", + "before": "cb6bb0a8ed3dda48bedd3ae89ed78a490f644c1040c5aa16463e1e701250988e", + "after": "df2b080903d9ee35ccb89c0970adbf5b6c88d46fc5a6401eecb620a984b739c4" + }, + { + "path": "code/framework/object/src/main/java/io/cattle/platform/object/impl/AbstractObjectManager.java", + "before": null, + "after": "db0328ff05701ca146e2f770d9bcb34af1d91a73a3e4d59ae432a2c749649539" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/view/AiResourceViewService.java", + "before": "19d91fa89474c9c9604ff7a688afd9a36e6d75fd313667f16ef5550efedc050b", + "after": "9343029c4b9022380b5b03b434d0d42167d17884b0b54dd868c2fa826ad100ea" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiHostMetadataChange.java", + "before": null, + "after": "e08978156bc77733326da1d299c97038103049d441a41f0332e3115b4ac07ce7" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDescriptor.java", + "before": "8b81eaa0446c07c786e588ae8299456bc3c25dca90cf31810c29d1ca5a838e35", + "after": "dd89c69f6f0608176ca8b6daac85f408c572932b434452abddee233b31f77f96" + }, + { + "path": "code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/change/AiResourceChangeDomain.java", + "before": "331c7fa5eae1f60d4f33e3f3767c546d684042be6156a756ab60c627feef443a", + "after": "2378a8875ae2a2b76e70d7c57a0af715d02f6165228bc2063139f208298669d7" + }, + { + "path": "code/iaas/model/src/main/java/io/cattle/platform/core/dao/HostDao.java", + "before": null, + "after": "eace82dc713adf92ea4d5b5f0f19db96ad978528956b6abce249b3bf436a25ad" + }, + { + "path": "code/iaas/model/src/main/java/io/cattle/platform/core/dao/ServiceDao.java", + "before": null, + "after": "92a4cd76c988cfab8f3ee3b8e96f2f2ef5fa06c2787ec46ca03cb75ec4f2a7ce" + }, + { + "path": "code/iaas/model/src/main/java/io/cattle/platform/core/dao/impl/HostDaoImpl.java", + "before": null, + "after": "ae0fff79276ce84e8ab5ec19b80b09a78f7c91740c24706e6129be0f15f0f483" + }, + { + "path": "code/iaas/model/src/main/java/io/cattle/platform/core/dao/impl/ServiceDaoImpl.java", + "before": null, + "after": "d13a7164f03e71e7fbbedd9165667834b6f33f99607500d2d819ceecab75902b" + }, + { + "path": "code/iaas/service-discovery/api/src/main/java/io/cattle/platform/servicediscovery/api/dao/ServiceDao.java", + "before": null, + "after": "68efa2475c42844de04d61ec821ea877cea9fdbd7da1600ab2a9dea954a66c93" + }, + { + "path": "code/iaas/service-discovery/server/src/main/java/io/cattle/platform/servicediscovery/dao/impl/ServiceDaoImpl.java", + "before": null, + "after": "05cb4a7254436ddf8583984274d7c1e1567eb6e98d33ba00d17dba24ea312c86" + }, + { + "path": "code/packaging/app-config/src/main/java/io/cattle/platform/app/SystemServicesConfig.java", + "before": null, + "after": "32dba774946a48c2d6a9ec9030fcb5b2ca324ba98cd3269478aec237984a75a8" + }, + { + "path": "resources/content/schema/base/aiResourceChangePlan.json", + "before": "113f44df62f09f822b7f97f4cfdcab96de2f0f70995c821632f801c20134110c", + "after": "b1d67ec8bd3a48b5d9768e6ac15ea92a3ed4203b1bedf512eccbcb7935192baf" + } + ], + "authorizationBoundary": "Host metadata plans accept only name and description and retain native PUT/role/Key ceilings, exact human approval, revision and one claim. Host detail and Volume usage resolve only freshly authorized container, ordinary Service and Stack relations through bounded native queries. The SEM fallback requires a genuinely NULL persisted parent at capture and final verification; multiple, hidden, removed or moved relations remain unknown without IDs or counts. Catalog lifecycle handlers are lazily composed into the native ObjectManager so the new Stack ID is durably bound before CREATE process scheduling. Ordinary creates and output filters remain unchanged. No new tool, Host lifecycle action, generic endpoint, operation grant or secret output is added.", + "frozenV1SchemaFilesUnchanged": true, + "nativeRoleGrantsUnchanged": true, + "onlyTypedPlanSchemaChange": "resourceType enum adds host; no operation/action expansion", + "extractor": { + "constructionSha256": "078ffe48881469fe5eb3cb43a15fa41b480fe70e11bd5f63102ded6b35a6d076", + "parserSha256": "7cc8e70d9f3ea391eb74fef2490f4407aa47833c585b6729c62b3d9e5829ac2b", + "extractorSha256": "112d2ff723d88acbe7bb45ff48c9c9018feb93b08031a3e86f867030871477fc" + }, + "historicalProducerAndReviewChainsPreserved": true, + "artifactQualificationClaimed": false, + "runtimeAcceptanceClaimed": false +} diff --git a/scripts/build b/scripts/build index c368694f2d..613906e566 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.369} +ENGINE_VERSION=${ENGINE_VERSION:-0.183.370} case "$SOURCE_REVISION" in ''|*[!0-9a-f]*) diff --git a/scripts/check-apt-fixed-vex.py b/scripts/check-apt-fixed-vex.py new file mode 100644 index 0000000000..5e2db643f0 --- /dev/null +++ b/scripts/check-apt-fixed-vex.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +"""Validate only the official USN-8996-1 APT pair and Trivy's native VEX result. + +This verifier never filters a report or changes the release severity policy. +""" +import argparse +from collections import Counter +import copy +import hashlib +import json +from pathlib import Path +import re + + +ROOT = Path(__file__).resolve().parent.parent +CVE = "CVE-2026-109518" +VERSION = "3.2.1ubuntu0.1" +USN = "https://ubuntu.com/security/notices/USN-8996-1" +ARCHIVES = { + "apt": "0de729b8fe0e1c67c04e8e8d5bd6e0420dbd47f75b93f0b71c8e740edc2d5ffc", + "libapt-pkg7.0": "94e5d41303265b66af0f8c70ade18bfb77260ad286edee6c8045faddef82b94c", +} +PURLS = {name: "pkg:deb/ubuntu/" + name + "@" + VERSION + "?arch=amd64&distro=ubuntu-26.04" + for name in ARCHIVES} +ELF_MANIFEST_SHA256 = "365b11be723d25f344a5f5bed1a0bb457a45b660e10ad1968edb60d6f043c5f5" + + +def require(condition, reason): + if not condition: + raise ValueError(reason) + + +def document_guard(document): + require(set(document) == {"@context", "@id", "author", "timestamp", "version", "statements"}, "closed OpenVEX document") + require(document["@context"] == "https://openvex.dev/ns/v0.2.0" + and document["@id"] == "https://github.com/PastureStack/orchestration-engine/security/openvex/USN-8996-1" + and document["author"] == "PastureStack Security" and type(document["version"]) is int + and document["version"] == 1, "exact advisory identity") + require(re.fullmatch(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z", document["timestamp"]) is not None, "UTC timestamp") + require(len(document["statements"]) == 1, "one exact advisory statement") + statement = document["statements"][0] + require(set(statement) == {"vulnerability", "products", "status", "impact_statement"}, "closed fixed statement") + require(statement["vulnerability"] == {"name": CVE} and statement["status"] == "fixed", "official fixed CVE only") + products = statement["products"] + require(len(products) == 2 and all(set(p) == {"@id"} for p in products) + and {p["@id"] for p in products} == set(PURLS.values()), "exact two version/arch/distro subjects") + impact = statement["impact_statement"] + require(all(value in impact for value in [USN, VERSION, *ARCHIVES.values()]), "official notice and archive hashes") + + +def checksum_rows(text): + result = {} + for line in text.splitlines(): + fields = line.split() + require(len(fields) == 2 and re.fullmatch(r"[0-9a-f]{64}", fields[0]) is not None, "SHA256 row") + require(fields[1] not in result, "duplicate ELF path") + result[fields[1]] = fields[0] + return result + + +def input_guard(document, manifest, dpkg, inspect, raw, installed): + document_guard(document) + require(hashlib.sha256(manifest.encode()).hexdigest() == ELF_MANIFEST_SHA256, "official archive ELF manifest") + expected = checksum_rows(manifest) + require(len(expected) == 19 and checksum_rows(installed) == expected, "all nineteen installed official ELF bytes") + require(len(inspect) == 1 and inspect[0]["Architecture"] == "amd64" and inspect[0]["Os"] == "linux", "one Linux amd64 image") + image = inspect[0]["Id"] + require(re.fullmatch(r"sha256:[0-9a-f]{64}", image) is not None, "immutable image identity") + metadata = raw["Metadata"] + require(metadata["ImageID"] == image and metadata["OS"]["Family"] == "ubuntu" + and metadata["OS"]["Name"] == "26.04", "same Ubuntu image as native readback") + lines = dpkg.splitlines() + require(lines and lines[0] == "package\tversion", "native dpkg inventory") + os_results = [r for r in raw["Results"] if r.get("Class") == "os-pkgs" and r.get("Type") == "ubuntu"] + require(len(os_results) == 1, "one Ubuntu package inventory") + for name, purl in PURLS.items(): + native = [line.split("\t") for line in lines[1:] if line.split("\t", 1)[0].split(":", 1)[0] == name] + require(len(native) == 1 and len(native[0]) == 2 and native[0][1] == VERSION, "exact installed pair " + name) + packages = [p for p in os_results[0]["Packages"] if p.get("Name") == name] + require(len(packages) == 1, "one raw package " + name) + package = packages[0] + require(package.get("ID") == name + "@" + VERSION and package.get("Version") == VERSION + and not package.get("Release") and not package.get("Epoch") and package.get("Arch") == "amd64" + and package.get("SrcName") == "apt" and package.get("SrcVersion") == VERSION + and not package.get("SrcRelease") and not package.get("SrcEpoch") + and package.get("Identifier", {}).get("PURL") == purl, "exact raw binary/source/PURL " + name) + return image + + +def covered(finding): + name = finding.get("PkgName") + return (finding.get("VulnerabilityID") == CVE and name in PURLS + and finding.get("PkgID") == name + "@" + VERSION and finding.get("InstalledVersion") == VERSION + and finding.get("PkgIdentifier", {}).get("PURL") == PURLS[name]) + + +def package_identity(result): + """Keep every package field/count; canonicalize only documented graph metadata. + + Trivy v0.74.0 vex/vex.go:78 forces SBOM regeneration. Its sbom/io/encode.go:238 + fills empty package IDs with Name@Version; dependency/id.go:37 defines UID as + a package-graph hash. Imported Python SBOM UIDs can change between scans. + Neither report is changed: original IDs/UIDs remain recorded in the proof. + """ + rows, mapping, uid_owners = [], [], {} + language = result.get("Class") == "lang-pkgs" and result.get("Type") in {"jar", "python-pkg"} + for original in result.get("Packages", []): + package = copy.deepcopy(original) + change = {} + if language and not package.get("ID"): + require(isinstance(package.get("Name"), str) and isinstance(package.get("Version"), str), "graph ID source fields") + require("ID" not in package or package["ID"] == "", "empty graph ID only") + package["ID"] = package["Name"] + "@" + package["Version"] + change.update(originalId=original.get("ID"), canonicalId=package["ID"]) + identifier = package.get("Identifier", {}) + purl = identifier.get("PURL", "") + if (result.get("Class") == "lang-pkgs" and result.get("Type") == "python-pkg" + and package.get("AnalyzedBy") == "sbom" and not package.get("FilePath") and purl.startswith("pkg:pypi/") + and identifier.get("BOMRef") == purl): + uid = identifier.get("UID") + require(isinstance(uid, str) and re.fullmatch(r"[0-9a-f]{1,16}", uid) is not None, "derived Python SBOM graph UID") + del identifier["UID"] + stable = json.dumps(package, sort_keys=True, separators=(",", ":")) + canonical = "package-content-sha256:" + hashlib.sha256(stable.encode()).hexdigest() + require(uid not in uid_owners or uid_owners[uid] == canonical, "graph UID collision across distinct packages") + uid_owners[uid] = canonical + identifier["UID"] = canonical + change.update(originalUid=uid, canonicalUid=canonical) + rows.append(json.dumps(package, sort_keys=True, separators=(",", ":"))) + if change: + mapping.append(dict(purl=purl, **change)) + return Counter(rows), sorted(mapping, key=lambda row: json.dumps(row, sort_keys=True)) + + +def package_identity_proof(raw, native): + result = [] + for old, new in zip(raw["Results"], native["Results"]): + before, old_mapping = package_identity(old) + after, new_mapping = package_identity(new) + require(before == after, "complete package content/multiplicity drift") + result.append(dict(target=old.get("Target"), type=old.get("Type"), count=sum(before.values()), + completePackageMultisetSha256=hashlib.sha256(json.dumps(sorted(before.items())).encode()).hexdigest(), + rawDerivedMetadata=old_mapping, nativeDerivedMetadata=new_mapping)) + return result + + +def output_guard(raw, native): + for key in ("ArtifactName", "ArtifactType", "Metadata"): + require(native.get(key) == raw.get(key), "same native scan identity/inventory " + key) + before, after = raw["Results"], native["Results"] + require(len(before) == len(after), "same scan target closure") + removed = 0 + for old, new in zip(before, after): + for key in ("Target", "Class", "Type"): + require(old.get(key) == new.get(key), "native scan target/package drift " + key) + expected = [v for v in old.get("Vulnerabilities", []) if not covered(v)] + removed += len(old.get("Vulnerabilities", [])) - len(expected) + require(new.get("Vulnerabilities", []) == expected, "native VEX must preserve every unrelated finding") + require(0 <= removed <= 2, "only the two exact fixed subjects may be qualified") + package_identity_proof(raw, native) + return removed + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ("dpkg", "inspect", "raw", "installed"): + parser.add_argument("--" + name, type=Path, required=True) + parser.add_argument("--vex", type=Path, default=ROOT / "security/ubuntu-apt-fixed.openvex.json") + parser.add_argument("--manifest", type=Path, default=ROOT / "security/ubuntu-apt-fixed.sha256") + mode = parser.add_mutually_exclusive_group(required=True) + mode.add_argument("--inputs-only", action="store_true") + mode.add_argument("--native", type=Path) + parser.add_argument("--proof", type=Path) + args = parser.parse_args() + document = json.loads(args.vex.read_bytes()) + raw = json.loads(args.raw.read_bytes()) + image = input_guard(document, args.manifest.read_text(), args.dpkg.read_text(), + json.loads(args.inspect.read_bytes()), raw, args.installed.read_text()) + if args.inputs_only: + require(args.proof is None, "input validation is not final qualification") + print("APT_FIXED_VEX_INPUTS_OK pair=2 official_elf_bytes=19") + return + require(args.proof is not None, "native output proof required") + removed = output_guard(raw, json.loads(args.native.read_bytes())) + inputs = {name: getattr(args, name) for name in ("dpkg", "inspect", "raw", "installed", "vex", "manifest", "native")} + proof = dict(status="PASS_EXACT_NATIVE_TRIVY_FIXED_APT_VEX", imageId=image, cve=CVE, + version=VERSION, purls=PURLS, officialArchiveSha256=ARCHIVES, installedOfficialElfCount=19, + qualifiedFixedOccurrences=removed, everyUnrelatedFindingPreserved=True, + scannerDerivedPackageIdentity=package_identity_proof(raw, json.loads(args.native.read_bytes())), + inputSha256={name: hashlib.sha256(path.read_bytes()).hexdigest() for name, path in inputs.items()}, + rawReportRewritten=False, severityPolicyChanged=False, pipOverlayOwnerChanged=False) + args.proof.write_text(json.dumps(proof, sort_keys=True, indent=2) + "\n") + print("APT_FIXED_NATIVE_VEX_OK qualified_fixed=%d unrelated_preserved=true" % removed) + + +if __name__ == "__main__": + main() diff --git a/scripts/check-cattle-jdk25-standalone b/scripts/check-cattle-jdk25-standalone index 4c3133d189..edd03ff2f2 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.369}} +expected_version=${EXPECTED_ENGINE_VERSION:-${ENGINE_VERSION:-0.183.370}} 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 de25015dc1..a5d07eb28c 100755 --- a/scripts/check-pasturestack-source +++ b/scripts/check-pasturestack-source @@ -51,14 +51,20 @@ 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.369' +require_line code/meta-parent/pom.xml ' 0.183.370' 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 'apt=3.2.1ubuntu0.1' +require_line ubuntu-security-packages.lock 'libapt-pkg7.0=3.2.1ubuntu0.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:0de729b8fe0e1c67c04e8e8d5bd6e0420dbd47f75b93f0b71c8e740edc2d5ffc https://security.ubuntu.com/ubuntu/pool/main/a/apt/apt_3.2.1ubuntu0.1_amd64.deb /tmp/apt.deb' + require_line "$recipe" 'ADD --checksum=sha256:94e5d41303265b66af0f8c70ade18bfb77260ad286edee6c8045faddef82b94c https://security.ubuntu.com/ubuntu/pool/main/a/apt/libapt-pkg7.0_3.2.1ubuntu0.1_amd64.deb /tmp/libapt-pkg7.0.deb' + require_line "$recipe" ' dpkg -i /tmp/libapt-pkg7.0.deb /tmp/apt.deb; \' 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" 'RUN curl -fsSL --retry 5 --retry-all-errors --retry-delay 2 --connect-timeout 10 --max-time 300 -o /tmp/guava-33.7.2-jre.jar "https://repo.maven.apache.org/maven2/com/google/guava/guava/33.7.2-jre/guava-33.7.2-jre.jar" && \' + require_line "$recipe" ' echo '"'"'b530942257fb935f8b2cfaa5f8eb5bd59c555fd8e8d01b8ce98912e077ea606c /tmp/guava-33.7.2-jre.jar'"'"' | sha256sum -c -' 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; \' @@ -144,6 +150,9 @@ require_text scripts/install-snapshot-packages '/usr/lib/apt/apt-helper' require_text scripts/install-snapshot-packages 'SHA256:' require_text scripts/install-snapshot-packages '--no-download' python3 -B scripts/test-install-snapshot-packages.py +python3 -B scripts/test-maven-distribution-download.py +python3 -B scripts/test-apt-security-bootstrap.py +python3 -B scripts/test-apt-fixed-vex.py require_line Makefile 'DAPPER_SOURCE ?= /usr/src/pasturestack/orchestration-engine' require_line Dockerfile.dapper 'ENV DAPPER_SOURCE=/usr/src/pasturestack/orchestration-engine' require_line scripts/entry 'git config --global --add safe.directory "$(pwd)"' @@ -486,4 +495,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.369 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.370 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 4f31c5b1cc..42e838281c 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.369} +expected_version=${EXPECTED_ENGINE_VERSION:-0.183.370} test -f "$artifact" artifact=$(realpath "$artifact") diff --git a/scripts/test-apt-fixed-vex.py b/scripts/test-apt-fixed-vex.py new file mode 100644 index 0000000000..16d1bf3180 --- /dev/null +++ b/scripts/test-apt-fixed-vex.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +"""Scoped controls for the native APT fixed-VEX evidence boundary.""" +import copy +import importlib.util +import json +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parent.parent + + +def module(name, path): + spec = importlib.util.spec_from_file_location(name, ROOT / path) + result = importlib.util.module_from_spec(spec) + spec.loader.exec_module(result) + return result + + +OWNER = module("apt_fixed_owner", "scripts/check-apt-fixed-vex.py") +POLICY = module("release_policy", ".github/scripts/enforce-runtime-vulnerabilities.py") + + +class AptFixedVexTest(unittest.TestCase): + def setUp(self): + self.document = json.loads((ROOT / "security/ubuntu-apt-fixed.openvex.json").read_bytes()) + self.manifest = (ROOT / "security/ubuntu-apt-fixed.sha256").read_text() + self.inspect = [dict(Id="sha256:" + "a" * 64, Architecture="amd64", Os="linux")] + packages = [dict(Name=n, ID=n + "@" + OWNER.VERSION, Version=OWNER.VERSION, Arch="amd64", + SrcName="apt", SrcVersion=OWNER.VERSION, Identifier=dict(PURL=p)) for n, p in OWNER.PURLS.items()] + findings = [dict(VulnerabilityID=OWNER.CVE, PkgName=n, PkgID=n + "@" + OWNER.VERSION, + InstalledVersion=OWNER.VERSION, Severity="HIGH", PkgIdentifier=dict(PURL=p)) for n, p in OWNER.PURLS.items()] + self.raw = dict(SchemaVersion=2, ArtifactName="synthetic-owned-dapper", ArtifactType="container_image", + Metadata=dict(ImageID=self.inspect[0]["Id"], OS=dict(Family="ubuntu", Name="26.04")), + Results=[dict(Target="Ubuntu 26.04", Class="os-pkgs", Type="ubuntu", Packages=packages, Vulnerabilities=findings)]) + self.dpkg = "package\tversion\n" + "".join(n + "\t" + OWNER.VERSION + "\n" for n in OWNER.PURLS) + + def inputs(self, **replace): + values = dict(document=self.document, manifest=self.manifest, dpkg=self.dpkg, + inspect=self.inspect, raw=self.raw, installed=self.manifest) + values.update(replace) + return OWNER.input_guard(**values) + + def test_exact_official_pair_and_all_elf_bytes_bind_one_image(self): + self.assertEqual(self.inspect[0]["Id"], self.inputs()) + native = copy.deepcopy(self.raw) + native["Results"][0]["Vulnerabilities"] = [] + self.assertEqual(2, OWNER.output_guard(self.raw, native)) + for count in (0, 1): + raw = copy.deepcopy(self.raw) + raw["Results"][0]["Vulnerabilities"] = raw["Results"][0]["Vulnerabilities"][:count] + self.assertEqual(count, OWNER.output_guard(raw, native)) + + def test_wrong_version_package_arch_distro_or_cve_cannot_expand_subjects(self): + for old, new in (("3.2.1ubuntu0.1", "3.2.0"), ("apt@", "other@"), ("amd64", "arm64"), + ("ubuntu-26.04", "ubuntu-24.04")): + with self.subTest(subject=new): + document = copy.deepcopy(self.document) + document["statements"][0]["products"][0]["@id"] = document["statements"][0]["products"][0]["@id"].replace(old, new) + with self.assertRaises(ValueError): self.inputs(document=document) + for change in (dict(vulnerability=dict(name="CVE-OTHER")), dict(status="not_affected"), + dict(products=[dict(**{"@id": "pkg:deb/ubuntu/apt@*"})])): + document = copy.deepcopy(self.document) + document["statements"][0].update(change) + with self.assertRaises(ValueError): self.inputs(document=document) + + def test_native_old_version_missing_pair_or_changed_elf_fails_closed(self): + for value in (self.dpkg.replace(OWNER.VERSION, "3.2.0"), "package\tversion\napt\t" + OWNER.VERSION + "\n"): + with self.assertRaises(ValueError): self.inputs(dpkg=value) + for value in (self.manifest.replace(self.manifest[:64], "0" * 64, 1), "", self.manifest + self.manifest.splitlines()[0] + "\n"): + with self.assertRaises(ValueError): self.inputs(installed=value) + raw = copy.deepcopy(self.raw) + raw["Results"][0]["Packages"][0]["Identifier"]["PURL"] = OWNER.PURLS["apt"].replace("amd64", "arm64") + with self.assertRaises(ValueError): self.inputs(raw=raw) + + def test_other_package_cve_version_arch_and_distro_stay_uncovered(self): + original = self.raw["Results"][0]["Vulnerabilities"][0] + for field, value in (("VulnerabilityID", "CVE-OTHER"), ("PkgName", "other"), ("InstalledVersion", "3.2.0"), + ("PkgID", "apt@3.2.0")): + changed = copy.deepcopy(original) + changed[field] = value + self.assertFalse(OWNER.covered(changed)) + for old, new in (("amd64", "arm64"), ("ubuntu-26.04", "ubuntu-24.04"), (OWNER.VERSION, "3.2.0")): + changed = copy.deepcopy(original) + changed["PkgIdentifier"]["PURL"] = changed["PkgIdentifier"]["PURL"].replace(old, new) + self.assertFalse(OWNER.covered(changed)) + + def test_native_vex_cannot_remove_other_high_or_change_package_inventory(self): + raw = copy.deepcopy(self.raw) + other = dict(VulnerabilityID="CVE-OTHER", PkgName="other", PkgID="other@1", InstalledVersion="1", Severity="HIGH") + raw["Results"][0]["Vulnerabilities"].append(other) + native = copy.deepcopy(raw) + native["Results"][0]["Vulnerabilities"] = [other] + self.assertEqual(2, OWNER.output_guard(raw, native)) + self.assertEqual(1, len(POLICY.violations(native, dict(repository="PastureStack/orchestration-engine", unfixed=[]), "PastureStack/orchestration-engine"))) + native["Results"][0]["Vulnerabilities"] = [] + with self.assertRaises(ValueError): OWNER.output_guard(raw, native) + native = copy.deepcopy(self.raw) + native["Results"][0]["Packages"] = [] + with self.assertRaises(ValueError): OWNER.output_guard(self.raw, native) + native = copy.deepcopy(self.raw) + native["Metadata"]["ImageID"] = "sha256:" + "b" * 64 + with self.assertRaises(ValueError): OWNER.output_guard(self.raw, native) + + def test_package_permutation_and_documented_graph_metadata_keep_all_content_and_counts(self): + raw = copy.deepcopy(self.raw) + java = dict(Target="Java", Class="lang-pkgs", Type="jar", Packages=[ + dict(Name="example:a", Version="1", FilePath="a.jar", Identifier=dict(PURL="pkg:maven/example/a@1", UID="11"), Extra=dict(value=1)), + dict(Name="example:b", Version="2", FilePath="b.jar", Identifier=dict(PURL="pkg:maven/example/b@2", UID="22"), Extra=dict(value=2))]) + purl = "pkg:pypi/example@1" + python = dict(Target="Python", Class="lang-pkgs", Type="python-pkg", Packages=[ + dict(ID="example@1", Name="example", Version="1", AnalyzedBy="sbom", Identifier=dict(PURL=purl, BOMRef=purl, UID="33"), Licenses=["MIT"])]) + raw["Results"].extend([java, python]) + native = copy.deepcopy(raw) + native["Results"][0]["Vulnerabilities"] = [] + native["Results"][1]["Packages"].reverse() + for package in native["Results"][1]["Packages"]: + package["ID"] = package["Name"] + "@" + package["Version"] + native["Results"][2]["Packages"][0]["Identifier"]["UID"] = "44" + self.assertEqual(2, OWNER.output_guard(raw, native)) + for field, value in (("Extra", dict(value=99)), ("FilePath", "other.jar"), ("Version", "9"), ("ID", "wrong-id")): + changed = copy.deepcopy(native) + changed["Results"][1]["Packages"][0][field] = value + with self.assertRaises(ValueError): OWNER.output_guard(raw, changed) + changed = copy.deepcopy(native) + changed["Results"][1]["Packages"].append(copy.deepcopy(changed["Results"][1]["Packages"][0])) + with self.assertRaises(ValueError): OWNER.output_guard(raw, changed) + changed = copy.deepcopy(native) + other = copy.deepcopy(changed["Results"][2]["Packages"][0]) + other["Name"] = "other" + other["Identifier"]["PURL"] = other["Identifier"]["BOMRef"] = "pkg:pypi/other@1" + changed["Results"][2]["Packages"].append(other) + with self.assertRaisesRegex(ValueError, "graph UID collision"): OWNER.output_guard(raw, changed) + changed = copy.deepcopy(native) + changed["Results"][0]["Packages"][0]["Identifier"]["UID"] = "55" + with self.assertRaises(ValueError): OWNER.output_guard(raw, changed) + raw["Results"][2]["Packages"][0]["AnalyzedBy"] = "python-package" + changed = copy.deepcopy(raw) + changed["Results"][0]["Vulnerabilities"] = [] + changed["Results"][2]["Packages"][0]["Identifier"]["UID"] = "44" + with self.assertRaises(ValueError): OWNER.output_guard(raw, changed) + + def test_workflow_keeps_raw_then_native_vex_then_original_pip_owner(self): + workflow = (ROOT / ".github/workflows/security-release-gate.yml").read_text() + raw = workflow.index("--output /work/evidence/dapper-image-vulnerabilities.raw.json") + inputs = workflow.index("scripts/check-apt-fixed-vex.py", raw) + native = workflow.index("--vex /work/evidence/dapper-apt-fixed.openvex.json", inputs) + proof = workflow.index("--native evidence/dapper-image-vulnerabilities.apt-fixed.json", native) + pip = workflow.index("python3 tools/pip-security-overlay/filter-pip-vendor-findings.py", proof) + self.assertLess(raw, inputs) + self.assertLess(inputs, native) + self.assertLess(native, proof) + self.assertLess(proof, pip) + self.assertIn("--raw evidence/dapper-image-vulnerabilities.apt-fixed.json", workflow[pip:]) + self.assertIn("--skip-db-update --skip-java-db-update --skip-vex-repo-update", workflow[inputs:pip]) + self.assertNotIn("--ignore-unfixed", workflow[raw:pip]) + self.assertNotIn("--ignore-status", workflow[raw:pip]) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test-apt-security-bootstrap.py b/scripts/test-apt-security-bootstrap.py new file mode 100644 index 0000000000..021b250789 --- /dev/null +++ b/scripts/test-apt-security-bootstrap.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +"""Exercise the exact APT bootstrap security boundary without network or installation.""" +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + + +REPO = Path(__file__).resolve().parent.parent +VERSION = "3.2.1ubuntu0.1" +PACKAGES = { + "apt": "0de729b8fe0e1c67c04e8e8d5bd6e0420dbd47f75b93f0b71c8e740edc2d5ffc", + "libapt-pkg7.0": "94e5d41303265b66af0f8c70ade18bfb77260ad286edee6c8045faddef82b94c", +} + + +def shell(): + found = shutil.which("sh") + if found: + return found + git = shutil.which("git") + if os.name == "nt" and git: + candidate = Path(git).resolve().parents[1] / "usr/bin/sh.exe" + if candidate.is_file(): + return str(candidate) + raise RuntimeError("A POSIX shell is required for the actual builder block test") + + +def block(name): + source = (REPO / name).read_text(encoding="utf-8") + start = source.index(" for package in apt libapt-pkg7.0; do \\\n") + end = source.index(" . /licenses/ubuntu-apt.lock; \\\n", start) + return source, source[start:end] + + +class AptSecurityBootstrapTest(unittest.TestCase): + def run_block(self, name, mode): + _, production = block(name) + with tempfile.TemporaryDirectory(prefix="engine-apt-security-") as directory: + root = Path(directory) + log = root / "calls.txt" + commands = { + "dpkg-query": r"""case "$*" in + *db:Status-Status*) + if [ "$PST_APT_MODE" = missing-installed ]; then printf 'not-installed'; else printf 'installed'; fi ;; + *) + if [ "$PST_APT_MODE" = wrong-installed ]; then printf '3.2.0'; else printf '3.2.1ubuntu0.1'; fi ;; +esac +""", + "dpkg-deb": r"""package=${2##*/}; package=${package%.deb} +case "$3" in + Package) if [ "$PST_APT_MODE" = wrong-package ]; then printf 'other'; else printf '%s' "$package"; fi ;; + Version) + if [ "$PST_APT_MODE" = wrong-version ] && [ "$package" = libapt-pkg7.0 ]; then printf '3.2.0'; else printf '3.2.1ubuntu0.1'; fi ;; + Architecture) if [ "$PST_APT_MODE" = wrong-arch ]; then printf 'arm64'; else printf 'amd64'; fi ;; + *) exit 99 ;; +esac +""", + "dpkg": r"""printf '%s\n' "$*" >> "$PST_APT_LOG" +if [ "$PST_APT_MODE" = install-failure ]; then exit 1; fi +""", + } + for command, body in commands.items(): + target = root / command + target.write_text("#!/bin/sh\nset -eu\n" + body, encoding="utf-8", newline="\n") + target.chmod(0o755) + # Commands with hyphens are portable executable names, not POSIX + # shell function names. Use the same /bin/sh path as the builders. + bin_path = root.as_posix() + if os.name == "nt": + bin_path = "/" + root.drive[0].lower() + bin_path[2:] + env = dict(os.environ, PST_APT_MODE=mode, PST_APT_LOG=log.as_posix(), PST_APT_BIN=bin_path) + script = 'set -eu\nPATH="$PST_APT_BIN"\nexport PATH\n' + production + script += '\nprintf "%s\\n" first-apt-operation >> "$PST_APT_LOG"\n' + result = subprocess.run([shell(), "-c", script], env=env, capture_output=True, timeout=10) + self.assertEqual(b"", result.stderr, "A harness/shell error must never count as a valid denial") + return result.returncode, log.read_text(encoding="utf-8").splitlines() if log.exists() else [] + + def test_only_exact_official_two_package_pins_and_existing_snapshot_are_used(self): + lock = (REPO / "ubuntu-security-packages.lock").read_text(encoding="utf-8") + for name in ("Dockerfile", "Dockerfile.dapper"): + with self.subTest(builder=name): + source, production = block(name) + for package, digest in PACKAGES.items(): + expected = "ADD --checksum=sha256:" + digest + " https://security.ubuntu.com/ubuntu/pool/main/a/apt/" + package + "_" + VERSION + "_amd64.deb /tmp/" + package + ".deb" + self.assertEqual(1, source.splitlines().count(expected)) + self.assertIn(package + "=" + VERSION + "\n", lock) + self.assertIn('Version)" = ' + VERSION, production) + self.assertIn('Architecture)" = amd64', production) + self.assertIn("${db:Status-Status}", production) + self.assertIn("${Version}", production) + self.assertLess(source.index(production), source.index(" apt-get update;")) + self.assertIn("Acquire::https::Verify-Peer", source) + self.assertNotIn("--allow-downgrades", production) + self.assertNotIn("--force", production) + + def test_verified_pair_installs_before_any_apt_operation(self): + for name in ("Dockerfile", "Dockerfile.dapper"): + with self.subTest(builder=name): + self.assertEqual((0, ["-i /tmp/libapt-pkg7.0.deb /tmp/apt.deb", "first-apt-operation"]), self.run_block(name, "valid")) + + def test_wrong_metadata_or_missing_installed_package_never_installs_or_calls_apt(self): + for name in ("Dockerfile", "Dockerfile.dapper"): + for mode in ("wrong-package", "wrong-version", "wrong-arch", "missing-installed"): + with self.subTest(builder=name, mode=mode): + code, calls = self.run_block(name, mode) + self.assertNotEqual(0, code) + self.assertEqual([], calls) + + def test_install_or_installed_version_failure_never_calls_apt(self): + for name in ("Dockerfile", "Dockerfile.dapper"): + for mode in ("install-failure", "wrong-installed"): + with self.subTest(builder=name, mode=mode): + code, calls = self.run_block(name, mode) + self.assertNotEqual(0, code) + self.assertEqual(["-i /tmp/libapt-pkg7.0.deb /tmp/apt.deb"], calls) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test-maven-distribution-download.py b/scripts/test-maven-distribution-download.py new file mode 100644 index 0000000000..42aef44048 --- /dev/null +++ b/scripts/test-maven-distribution-download.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""Exercise exact builder dependency downloads with no network or installation.""" +import hashlib +import os +from pathlib import Path +import re +import shutil +import subprocess +import sys +import tempfile +import unittest + + +REPO = Path(__file__).resolve().parent.parent +VERSION = "3.9.16" +SHA512 = "831a8591fe20c8243b1dbe7d71e3244f31d1665b0804b2e825e38cbbe5ce0cafb8338851f90780735568773e0a6cd07bbec107cda0b896b008b861075358b6f6" +PRIMARY = "https://dlcdn.apache.org/maven/maven-3/3.9.16/binaries/apache-maven-3.9.16-bin.tar.gz" +ARCHIVE = "https://archive.apache.org/dist/maven/maven-3/3.9.16/binaries/apache-maven-3.9.16-bin.tar.gz" +PAYLOAD = b"Synthetic Maven distribution fixture; never installed." + + +def shell(): + found = shutil.which("sh") + if found: + return found + git = shutil.which("git") + if os.name == "nt" and git: + candidate = Path(git).resolve().parents[1] / "usr/bin/sh.exe" + if candidate.is_file(): + return str(candidate) + raise RuntimeError("A POSIX shell is required for the actual builder block test") + + +def block(name): + source = (REPO / name).read_text(encoding="utf-8") + match = re.search(r'(?ms)^ \(curl -fsSL .*?\n echo "\$\{MAVEN_SHA512\} /tmp/apache-maven.tar.gz" \| sha512sum -c - && \\\n', source) + if not match: + raise AssertionError("Exact grouped download followed by checksum is required") + return source, match[0].rsplit(" && \\", 1)[0] + + +class MavenDistributionDownloadTest(unittest.TestCase): + def run_block(self, name, mode): + _, production = block(name) + with tempfile.TemporaryDirectory(prefix="engine-maven-download-") as directory: + root = Path(directory) + output = (root / "archive.tar.gz").as_posix() + log = root / "calls.txt" + env = dict(os.environ, PST_DOWNLOAD_MODE=mode, PST_DOWNLOAD_LOG=log.as_posix(), + PST_PYTHON=Path(sys.executable).as_posix(), + MAVEN_VERSION=VERSION, MAVEN_SHA512=hashlib.sha512(PAYLOAD).hexdigest()) + # Only the temporary output path is remapped. URL/flags/control flow + # and the checksum command are the actual production block. + script = r''' +set -eu +curl() { + previous=''; destination=''; url='' + for value in "$@"; do + if [ "$previous" = '-o' ]; then destination="$value"; fi + previous="$value"; url="$value" + done + printf '%s\n' "$url" >> "$PST_DOWNLOAD_LOG" + case "$PST_DOWNLOAD_MODE:$url" in + all-fail:*|fallback:*dlcdn.apache.org*|fallback-bad:*dlcdn.apache.org*) return 28 ;; + esac + case "$PST_DOWNLOAD_MODE" in + primary-bad|fallback-bad) printf 'corrupted fixture' > "$destination" ;; + *) printf 'Synthetic Maven distribution fixture; never installed.' > "$destination" ;; + esac +} +sha512sum() { + "$PST_PYTHON" -c 'import hashlib,pathlib,sys; expected,path=sys.stdin.readline().strip().split(" ",1); sys.exit(0 if hashlib.sha512(pathlib.Path(path).read_bytes()).hexdigest()==expected else 1)' +} +''' + production.replace("/tmp/apache-maven.tar.gz", output) + result = subprocess.run([shell(), "-c", script], env=env, capture_output=True, timeout=10) + return result.returncode, log.read_text(encoding="utf-8").splitlines() + + def test_only_official_exact_version_and_existing_sha512_are_used(self): + for name in ("Dockerfile", "Dockerfile.dapper"): + with self.subTest(builder=name): + source, command = block(name) + self.assertIn("ARG MAVEN_VERSION=" + VERSION + "\n", source) + self.assertIn("ARG MAVEN_SHA512=" + SHA512 + "\n", source) + urls = re.findall(r'"(https://[^\"]+)"', command) + self.assertEqual([PRIMARY, ARCHIVE], [url.replace("${MAVEN_VERSION}", VERSION) for url in urls]) + self.assertEqual(2, command.count("--retry 5 --retry-all-errors --retry-delay 2 --connect-timeout 10 --max-time 300")) + self.assertNotIn("--insecure", command) + self.assertTrue(command.rstrip().endswith("| sha512sum -c -")) + + def test_primary_success_and_transport_fallback_keep_bounded_origin_order(self): + for name in ("Dockerfile", "Dockerfile.dapper"): + with self.subTest(builder=name): + self.assertEqual((0, [PRIMARY]), self.run_block(name, "primary")) + self.assertEqual((0, [PRIMARY, ARCHIVE]), self.run_block(name, "fallback")) + + def test_all_transport_failures_stop_after_two_fixed_origin_commands(self): + for name in ("Dockerfile", "Dockerfile.dapper"): + with self.subTest(builder=name): + code, calls = self.run_block(name, "all-fail") + self.assertNotEqual(0, code) + self.assertEqual([PRIMARY, ARCHIVE], calls) + + def test_checksum_failure_is_terminal_and_cannot_retry_another_origin(self): + for name in ("Dockerfile", "Dockerfile.dapper"): + with self.subTest(builder=name): + code, calls = self.run_block(name, "primary-bad") + self.assertNotEqual(0, code) + self.assertEqual([PRIMARY], calls) + code, calls = self.run_block(name, "fallback-bad") + self.assertNotEqual(0, code) + self.assertEqual([PRIMARY, ARCHIVE], calls) + + + +GUAVA_URL = "https://repo.maven.apache.org/maven2/com/google/guava/guava/33.7.2-jre/guava-33.7.2-jre.jar" +GUAVA_SHA256 = "b530942257fb935f8b2cfaa5f8eb5bd59c555fd8e8d01b8ce98912e077ea606c" +GUAVA_PAYLOAD = b"Synthetic Guava fixture; never installed." + + +def guava_block(name): + source = (REPO / name).read_text(encoding="utf-8") + match = re.search(r'(?m)^RUN curl -fsSL .* /tmp/guava-33\.7\.2-jre\.jar "https://[^"\n]+" && \\\n echo \'[0-9a-f]{64} /tmp/guava-33\.7\.2-jre\.jar\' \| sha256sum -c -$', source) + if not match: + raise AssertionError("HTTP-status-aware Guava download followed by checksum is required") + return match[0].removeprefix("RUN ") + + +class GuavaDownloadTest(unittest.TestCase): + def run_block(self, name, mode): + production = guava_block(name) + with tempfile.TemporaryDirectory(prefix="engine-guava-download-") as directory: + root = Path(directory) + output = (root / "guava.jar").as_posix() + log = root / "calls.txt" + env = dict(os.environ, PST_DOWNLOAD_MODE=mode, PST_DOWNLOAD_LOG=log.as_posix(), + PST_PYTHON=Path(sys.executable).as_posix()) + script = r''' +set -eu +curl() { + previous=''; destination=''; url='' + for value in "$@"; do + if [ "$previous" = '-o' ]; then destination="$value"; fi + previous="$value"; url="$value" + done + printf '%s\n' "$url" >> "$PST_DOWNLOAD_LOG" + case "$PST_DOWNLOAD_MODE" in + http-error) return 22 ;; + transport-error) return 28 ;; + bad-bytes) printf 'corrupted fixture' > "$destination" ;; + *) printf 'Synthetic Guava fixture; never installed.' > "$destination" ;; + esac +} +sha256sum() { + printf '%s\n' 'checksum' >> "$PST_DOWNLOAD_LOG" + "$PST_PYTHON" -c 'import hashlib,pathlib,sys; expected,path=sys.stdin.readline().strip().split(" ",1); sys.exit(0 if hashlib.sha256(pathlib.Path(path).read_bytes()).hexdigest()==expected else 1)' +} +''' + production.replace("/tmp/guava-33.7.2-jre.jar", output).replace( + GUAVA_SHA256, hashlib.sha256(GUAVA_PAYLOAD).hexdigest()) + result = subprocess.run([shell(), "-c", script], env=env, capture_output=True, timeout=10) + return result.returncode, log.read_text(encoding="utf-8").splitlines() + + def test_exact_existing_url_hash_and_bounded_http_error_flags(self): + for name in ("Dockerfile", "Dockerfile.dapper"): + with self.subTest(builder=name): + command = guava_block(name) + self.assertEqual([GUAVA_URL], re.findall(r'"(https://[^\"]+)"', command)) + self.assertIn("curl -fsSL --retry 5 --retry-all-errors --retry-delay 2 --connect-timeout 10 --max-time 300", command) + self.assertIn("echo '" + GUAVA_SHA256 + " /tmp/guava-33.7.2-jre.jar' | sha256sum -c -", command) + self.assertNotIn("--insecure", command) + self.assertNotIn("||", command) + + def test_success_requires_checksum_before_installation(self): + for name in ("Dockerfile", "Dockerfile.dapper"): + with self.subTest(builder=name): + self.assertEqual((0, [GUAVA_URL, "checksum"]), self.run_block(name, "success")) + + def test_http_and_transport_errors_stop_before_checksum(self): + for name in ("Dockerfile", "Dockerfile.dapper"): + for mode in ("http-error", "transport-error"): + with self.subTest(builder=name, mode=mode): + code, calls = self.run_block(name, mode) + self.assertNotEqual(0, code) + self.assertEqual([GUAVA_URL], calls) + + def test_bad_bytes_fail_terminal_checksum_without_retry_or_install(self): + for name in ("Dockerfile", "Dockerfile.dapper"): + with self.subTest(builder=name): + code, calls = self.run_block(name, "bad-bytes") + self.assertNotEqual(0, code) + self.assertEqual([GUAVA_URL, "checksum"], calls) + + +if __name__ == "__main__": + unittest.main() diff --git a/security/ubuntu-apt-fixed.openvex.json b/security/ubuntu-apt-fixed.openvex.json new file mode 100644 index 0000000000..3ddc19d16a --- /dev/null +++ b/security/ubuntu-apt-fixed.openvex.json @@ -0,0 +1,24 @@ +{ + "@context": "https://openvex.dev/ns/v0.2.0", + "@id": "https://github.com/PastureStack/orchestration-engine/security/openvex/USN-8996-1", + "author": "PastureStack Security", + "timestamp": "2026-11-05T03:56:55Z", + "version": 1, + "statements": [ + { + "vulnerability": { + "name": "CVE-2026-109518" + }, + "products": [ + { + "@id": "pkg:deb/ubuntu/apt@3.2.1ubuntu0.1?arch=amd64&distro=ubuntu-26.04" + }, + { + "@id": "pkg:deb/ubuntu/libapt-pkg7.0@3.2.1ubuntu0.1?arch=amd64&distro=ubuntu-26.04" + } + ], + "status": "fixed", + "impact_statement": "Ubuntu USN-8996-1 (https://ubuntu.com/security/notices/USN-8996-1) fixes CVE-2026-109518 in Ubuntu 26.04 APT 3.2.1ubuntu0.1. The official amd64 apt archive SHA256 is 0de729b8fe0e1c67c04e8e8d5bd6e0420dbd47f75b93f0b71c8e740edc2d5ffc; libapt-pkg7.0 is 94e5d41303265b66af0f8c70ade18bfb77260ad286edee6c8045faddef82b94c. Both exact packages are installed before the first APT operation. CI binds the native installed pair, every package-owned ELF byte hash, raw scanner package identity and this exact PURL set to the same Dapper image. Other versions, architectures, distributions, packages and CVEs are not covered." + } + ] +} diff --git a/security/ubuntu-apt-fixed.sha256 b/security/ubuntu-apt-fixed.sha256 new file mode 100644 index 0000000000..024dd7d6a7 --- /dev/null +++ b/security/ubuntu-apt-fixed.sha256 @@ -0,0 +1,19 @@ +7f2a4f2cd0f524362347c34f134a9a2b9a189ff5965b25bb8398718105fe203d /usr/bin/apt +a9acce8175d39277cd0336399e3be93010ab5628dc5f6a6d4dd68046b90bc73a /usr/bin/apt-cache +9d65d1fca227c5025725446e5fe15fcfa95a61daf097c66bbf24dadabf2971e8 /usr/bin/apt-cdrom +b0146365ee21a14b8e5e2e5b5c3d684b7deeaa4d5185934dd55c138a0fae2513 /usr/bin/apt-config +a32eb93ec7802ccf20438ca0567a8bd92066f7fae9004511c4b07d8c5488092a /usr/bin/apt-get +3c23352b5906468bb3d2573b139dff97f2cf9c44c827dde0dd6af92d3cb6f0bb /usr/bin/apt-mark +0bba48624507b46cec7f90335e3c8ea8cf54332982035c10571f2fbbafc1aa7d /usr/lib/apt/apt-extracttemplates +d38bda32dc6b16148907a01ac966f39f3314b5358da6d9fb06a125185c31accb /usr/lib/apt/apt-helper +74eb0a049a8ed76c5d72dd59b2f4a1154c612a7eaf79209a58db080caf4b8eae /usr/lib/apt/methods/cdrom +aeb8ddfa1c62fe905fbede5af9e088990d452a48019521b3530d2e92fd893de9 /usr/lib/apt/methods/copy +8d74560ad9901b6329f5d91f6eb9e6823b79ee415229b3d1824ad4a875906960 /usr/lib/apt/methods/file +0b0e69545fe943113a1fbc9db2328e25555bbedf5a2c034df15d8e5fe8aa6318 /usr/lib/apt/methods/gpgv +5c9f5ca2c31dffbf3ed486962d91b6a37d451521e25cc1e7ad707f8009783e9e /usr/lib/apt/methods/http +e81d635fb3d10127de2e9bb2d7db07c2c8582c816d1342e254b4f40d99f45b00 /usr/lib/apt/methods/mirror +805e03be7b50fc9cec7a6848592a6ce56a37f55697aa586bd5c435d614f57607 /usr/lib/apt/methods/rred +f933b828c336e902be17c645c0877c636574c19223523ccd125c8a1d7d854423 /usr/lib/apt/methods/store +bf7e362d1ac2caa7c6a8e7051aaa1c104885fa7bbebb7e28e778487dc5eb5731 /usr/lib/apt/solvers/dump +56da91047fcfe29ce22474f44fe1d037e1f0a0961a7f0a9a624644ec4d92f1f5 /usr/lib/x86_64-linux-gnu/libapt-pkg.so.7.0.0 +5a716f413291f59c3be68381a97f959381d945689967c9ba09e1f503f1c98b37 /usr/lib/x86_64-linux-gnu/libapt-private.so.0.0.0 diff --git a/ubuntu-security-packages.lock b/ubuntu-security-packages.lock index dd0491af77..bc23e96531 100644 --- a/ubuntu-security-packages.lock +++ b/ubuntu-security-packages.lock @@ -1,5 +1,8 @@ # Official Ubuntu 26.04 security/updates snapshot: 20261023T000000Z # Existing packages only; ABI series and unrelated packages are preserved. +# APT is supplied before bootstrap by pinned USN-8996-1 packages (2026-11-04). +apt=3.2.1ubuntu0.1 +libapt-pkg7.0=3.2.1ubuntu0.1 bsdutils=1:2.41.3-3ubuntu2.2 curl=8.18.0-1ubuntu2.8 diffutils=1:3.12-1ubuntu0.1