diff --git a/.github/workflows/security-release-gate.yml b/.github/workflows/security-release-gate.yml
index 606dd4de42..36b31c0396 100644
--- a/.github/workflows/security-release-gate.yml
+++ b/.github/workflows/security-release-gate.yml
@@ -267,6 +267,9 @@ jobs:
'nativeSqlExcludesEveryForeignRemovedAndNonContainerRelationshipWithoutWrites',
},
'io.cattle.platform.iaas.api.auth.change.AiHostMetadataChangeTest': {
+ 'nativeDockerHostFullKeyUsesModelForVisiblePutAndReceipt',
+ 'nativeDockerHostReadKeyCanReadButRealWriteAuthorizationStillDenies',
+ 'hostSchemaRejectsWrongNativeModelEvenWhenItsKindClaimsHost',
'onlyMetadataIntentAndFieldsAreAccepted',
'nativePutUsesRealRevisionGuardAndRestoresContext',
'nativeMethodFieldAndCrossProjectCeilingsRejectBeforeMutation',
@@ -304,6 +307,12 @@ jobs:
'emptyHandlersAndLegacyListOverrideRemainValidWithoutEagerDependencyCycles',
'nativeCreateBindsExactlyOnceAndNestedFailureRestoresThreadLocalEnvelope',
},
+ 'io.cattle.platform.iaas.api.auth.ai.catalog.AiCatalogLaunchDelegationTest': {
+ 'nativeVersionRootSchemaHandshakeIsRepeatableOwnerMetadataWithoutChildWrites',
+ 'versionRootAuditsVerifiedDenialsWithoutAttributingForgedProofToPlanOrKey',
+ 'versionRootRechecksEventProofOriginalOwnerSourceAndFrozenStackBeforeMetadata',
+ 'versionRootRejectsUnknownPathsMethodsQueriesAndContradictoryRouteShapesWithSafe403',
+ },
}
suites = []
observed = {}
diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md
index 30f2d8a593..a377be8345 100644
--- a/COMPATIBILITY.md
+++ b/COMPATIBILITY.md
@@ -1,5 +1,30 @@
# Compatibility Contract
+## Engine 0.183.371 source candidate
+
+The typed Host change owner uses the native Host model for visibility and
+receipt checks. A Host can have driver kind `docker` while its API type remains
+`host`; another native model cannot gain Host access by claiming that kind.
+Service and Container kind checks, native read/write schema and Key ceilings,
+approval, locking, durable claim and audit remain unchanged. No lifecycle or
+permission grant is added.
+
+Three new focused Host cases cover Full-Key visibility/PUT/receipt, read-only
+Key read versus write403, and a rejected wrong model. Four Catalog cases cover
+the real API root/schema handshake, closed route shapes, fresh owner/source
+checks and safe denial audit. Valid root/schema reads are repeatable metadata;
+original delegated writes retain their event and replay boundaries.
+
+Both builders install the same already-pinned OpenSSL security packages before
+the base upgrade. The temporary security source is removed and normal indexes
+are reloaded before that base plan; its original source phase and final exact
+OpenSSL readback are restored afterwards. The twelve already-pinned final
+Python/Curl/CA/Brotli/PCRE packages enter through the normal local-deb APT solver
+before the base plan. Only the three old bootstrap Python/Curl/CA version pins
+are aligned with those existing final versions. Final package versions, security
+lock, source URLs, SHA pins and snapshot dates remain unchanged. Official CI,
+artifact publication and designated runtime acceptance remain pending.
+
## Engine 0.183.370 published
Host metadata is limited to name/description through the existing native PUT
diff --git a/Dockerfile b/Dockerfile
index fb77146e79..ff11d510ab 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -20,6 +20,7 @@ COPY ubuntu-security-packages.lock /licenses/ubuntu-security-packages.lock
COPY scripts/install-ubuntu-security-updates.sh /usr/local/bin/install-ubuntu-security-updates
COPY scripts/install-python-security-tools.sh /usr/local/bin/install-python-security-tools
COPY tools/pip-security-overlay /usr/local/lib/pip-security-overlay
+COPY --chmod=0755 scripts/install-snapshot-packages /usr/local/bin/install-snapshot-packages
ADD --checksum=sha256:fac6f0728cf9490a7a698e6b343e4140283e92355b060193c04f5770a666df8a https://security.ubuntu.com/ubuntu/pool/main/p/python3.14/libpython3.14-minimal_3.14.4-1ubuntu0.2_amd64.deb /tmp/libpython3.14-minimal.deb
ADD --checksum=sha256:3c267ef0f902f763c3615cbf28dd6fe91d2e4f603075d7f03db99f8d4eb66f46 https://security.ubuntu.com/ubuntu/pool/main/p/python3.14/libpython3.14-stdlib_3.14.4-1ubuntu0.2_amd64.deb /tmp/libpython3.14-stdlib.deb
ADD --checksum=sha256:0a7ea8a52834f9b3de0f11758924f80f7a6ebcbb4b02a55ba4f457503569f55f https://security.ubuntu.com/ubuntu/pool/main/p/python3.14/python3.14-minimal_3.14.4-1ubuntu0.2_amd64.deb /tmp/python3.14-minimal.deb
@@ -79,9 +80,49 @@ RUN set -eux; \
printf 'Types: deb\nURIs: https://snapshot.ubuntu.com/ubuntu/%s\nSuites: resolute resolute-updates resolute-backports resolute-security\nComponents: main universe restricted multiverse\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\nSnapshot: no\n' \
"${UBUNTU_APT_SNAPSHOT}" > /etc/apt/sources.list.d/pasturestack-snapshot.sources; \
printf 'Acquire::Retries "5";\nAcquire::http::Timeout "30";\nAcquire::https::Timeout "30";\nAcquire::http::Pipeline-Depth "0";\nAcquire::https::CaInfo "/etc/ssl/certs/ca-certificates.crt";\nAcquire::https::Verify-Peer "true";\nAcquire::https::Verify-Host "true";\nAcquire::AllowInsecureRepositories "false";\nAPT::Get::AllowUnauthenticated "false";\n' > /etc/apt/apt.conf.d/80pasturestack-retries; \
+ printf 'Types: deb\nURIs: https://snapshot.ubuntu.com/ubuntu/%s\nSuites: resolute-security\nComponents: main\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\nSnapshot: no\n' \
+ "${UBUNTU_APT_SECURITY_SNAPSHOT}" > /etc/apt/sources.list.d/pasturestack-security.sources; \
+ apt-get update; \
+ set --; \
+ for package in libexpat1 libsqlite3-0 libssh2-1t64; do \
+ test "$(grep -c "^${package}=" /licenses/ubuntu-security-packages.lock)" -eq 1; \
+ entry=$(grep "^${package}=" /licenses/ubuntu-security-packages.lock); \
+ set -- "$@" "$entry"; \
+ done; \
+ install-snapshot-packages install -y --no-install-recommends \
+ libssl3t64="${UBUNTU_APT_OPENSSL_VERSION}" \
+ openssl="${UBUNTU_APT_OPENSSL_VERSION}" \
+ openssl-provider-legacy="${UBUNTU_APT_OPENSSL_VERSION}" "$@"; \
+ for entry in "$@"; do \
+ test "$(dpkg-query -W -f='${Version}' "${entry%%=*}")" = "${entry#*=}"; \
+ done; \
+ for package in libssl3t64 openssl openssl-provider-legacy; do \
+ test "$(dpkg-query -W -f='${Version}' "${package}")" = "${UBUNTU_APT_OPENSSL_VERSION}"; \
+ done; \
+ rm -f /etc/apt/sources.list.d/pasturestack-security.sources; \
apt-get update; \
- apt-get upgrade -y; \
- apt-get install -y --no-install-recommends \
+ set --; \
+ for package in libpython3.14-minimal libpython3.14-stdlib python3.14-minimal python3.14 python3.14-venv curl libcurl3t64-gnutls libcurl4t64 ca-certificates libbrotli1 libpcre2-8-0 libpcre2-posix3; do \
+ architecture=amd64; \
+ case "$package" in \
+ libpython3.14-minimal|libpython3.14-stdlib|python3.14-minimal|python3.14|python3.14-venv) version="${UBUNTU_APT_PYTHON3_14_VERSION}";; \
+ curl|libcurl3t64-gnutls|libcurl4t64) version="${UBUNTU_APT_CURL_VERSION}";; \
+ ca-certificates) version="${UBUNTU_APT_CA_CERTIFICATES_VERSION}"; architecture=all;; \
+ libbrotli1) version=1.2.0-3ubuntu0.1;; \
+ libpcre2-8-0|libpcre2-posix3) version=10.46-1ubuntu0.1;; \
+ *) exit 1;; \
+ esac; \
+ test "$(dpkg-deb -f "/tmp/${package}.deb" Package)" = "$package"; \
+ test "$(dpkg-deb -f "/tmp/${package}.deb" Version)" = "$version"; \
+ test "$(dpkg-deb -f "/tmp/${package}.deb" Architecture)" = "$architecture"; \
+ set -- "$@" "/tmp/${package}.deb"; \
+ done; \
+ install-snapshot-packages install -y --no-install-recommends "$@"; \
+ for package in libpython3.14-minimal libpython3.14-stdlib python3.14-minimal python3.14 python3.14-venv curl libcurl3t64-gnutls libcurl4t64 ca-certificates libbrotli1 libpcre2-8-0 libpcre2-posix3; do \
+ test "$(dpkg-query -W -f='${Version}' "$package")" = "$(dpkg-deb -f "/tmp/${package}.deb" Version)"; \
+ done; \
+ install-snapshot-packages upgrade -y; \
+ install-snapshot-packages install -y --no-install-recommends \
bash="${UBUNTU_APT_BASH_VERSION}" \
ca-certificates="${UBUNTU_APT_CA_CERTIFICATES_VERSION}" \
curl="${UBUNTU_APT_CURL_BOOTSTRAP_VERSION}" \
@@ -104,10 +145,6 @@ RUN set -eux; \
printf 'Types: deb\nURIs: https://snapshot.ubuntu.com/ubuntu/%s\nSuites: resolute-security\nComponents: main\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\nSnapshot: no\n' \
"${UBUNTU_APT_SECURITY_SNAPSHOT}" > /etc/apt/sources.list.d/pasturestack-security.sources; \
apt-get update; \
- apt-get install -y --no-install-recommends \
- libssl3t64="${UBUNTU_APT_OPENSSL_VERSION}" \
- openssl="${UBUNTU_APT_OPENSSL_VERSION}" \
- openssl-provider-legacy="${UBUNTU_APT_OPENSSL_VERSION}"; \
for package in libssl3t64 openssl openssl-provider-legacy; do \
test "$(dpkg-query -W -f='${Version}' "${package}")" = "${UBUNTU_APT_OPENSSL_VERSION}"; \
done; \
diff --git a/Dockerfile.dapper b/Dockerfile.dapper
index 4c361adb8d..79a49a440a 100644
--- a/Dockerfile.dapper
+++ b/Dockerfile.dapper
@@ -97,7 +97,47 @@ RUN set -eux; \
printf 'Types: deb\nURIs: https://snapshot.ubuntu.com/ubuntu/%s\nSuites: resolute resolute-updates resolute-backports resolute-security\nComponents: main universe\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\nSnapshot: no\n' \
"${UBUNTU_APT_SNAPSHOT}" > /etc/apt/sources.list.d/pasturestack-snapshot.sources; \
printf 'Acquire::Retries "5";\nAcquire::http::Timeout "30";\nAcquire::https::Timeout "30";\nAcquire::http::Pipeline-Depth "0";\nAcquire::https::CaInfo "/etc/ssl/certs/ca-certificates.crt";\nAcquire::https::Verify-Peer "true";\nAcquire::https::Verify-Host "true";\nAcquire::AllowInsecureRepositories "false";\nAPT::Get::AllowUnauthenticated "false";\n' > /etc/apt/apt.conf.d/80pasturestack-retries; \
+ printf 'Types: deb\nURIs: https://snapshot.ubuntu.com/ubuntu/%s\nSuites: resolute-security\nComponents: main\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\nSnapshot: no\n' \
+ "${UBUNTU_APT_SECURITY_SNAPSHOT}" > /etc/apt/sources.list.d/pasturestack-security.sources; \
+ apt-get update; \
+ set --; \
+ for package in libexpat1 libsqlite3-0 libssh2-1t64; do \
+ test "$(grep -c "^${package}=" /licenses/ubuntu-security-packages.lock)" -eq 1; \
+ entry=$(grep "^${package}=" /licenses/ubuntu-security-packages.lock); \
+ set -- "$@" "$entry"; \
+ done; \
+ install-snapshot-packages install -y --no-install-recommends \
+ libssl3t64="${UBUNTU_APT_OPENSSL_VERSION}" \
+ openssl="${UBUNTU_APT_OPENSSL_VERSION}" \
+ openssl-provider-legacy="${UBUNTU_APT_OPENSSL_VERSION}" "$@"; \
+ for entry in "$@"; do \
+ test "$(dpkg-query -W -f='${Version}' "${entry%%=*}")" = "${entry#*=}"; \
+ done; \
+ for package in libssl3t64 openssl openssl-provider-legacy; do \
+ test "$(dpkg-query -W -f='${Version}' "${package}")" = "${UBUNTU_APT_OPENSSL_VERSION}"; \
+ done; \
+ rm -f /etc/apt/sources.list.d/pasturestack-security.sources; \
apt-get update; \
+ set --; \
+ for package in libpython3.14-minimal libpython3.14-stdlib python3.14-minimal python3.14 python3.14-venv curl libcurl3t64-gnutls libcurl4t64 ca-certificates libbrotli1 libpcre2-8-0 libpcre2-posix3; do \
+ architecture=amd64; \
+ case "$package" in \
+ libpython3.14-minimal|libpython3.14-stdlib|python3.14-minimal|python3.14|python3.14-venv) version="${UBUNTU_APT_PYTHON3_14_VERSION}";; \
+ curl|libcurl3t64-gnutls|libcurl4t64) version="${UBUNTU_APT_CURL_VERSION}";; \
+ ca-certificates) version="${UBUNTU_APT_CA_CERTIFICATES_VERSION}"; architecture=all;; \
+ libbrotli1) version=1.2.0-3ubuntu0.1;; \
+ libpcre2-8-0|libpcre2-posix3) version=10.46-1ubuntu0.1;; \
+ *) exit 1;; \
+ esac; \
+ test "$(dpkg-deb -f "/tmp/${package}.deb" Package)" = "$package"; \
+ test "$(dpkg-deb -f "/tmp/${package}.deb" Version)" = "$version"; \
+ test "$(dpkg-deb -f "/tmp/${package}.deb" Architecture)" = "$architecture"; \
+ set -- "$@" "/tmp/${package}.deb"; \
+ done; \
+ install-snapshot-packages install -y --no-install-recommends "$@"; \
+ for package in libpython3.14-minimal libpython3.14-stdlib python3.14-minimal python3.14 python3.14-venv curl libcurl3t64-gnutls libcurl4t64 ca-certificates libbrotli1 libpcre2-8-0 libpcre2-posix3; do \
+ test "$(dpkg-query -W -f='${Version}' "$package")" = "$(dpkg-deb -f "/tmp/${package}.deb" Version)"; \
+ done; \
install-snapshot-packages upgrade -y; \
install-snapshot-packages install -y --no-install-recommends \
bash="${UBUNTU_APT_BASH_VERSION}" \
@@ -122,10 +162,6 @@ RUN set -eux; \
printf 'Types: deb\nURIs: https://snapshot.ubuntu.com/ubuntu/%s\nSuites: resolute-security\nComponents: main\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\nSnapshot: no\n' \
"${UBUNTU_APT_SECURITY_SNAPSHOT}" > /etc/apt/sources.list.d/pasturestack-security.sources; \
apt-get update; \
- install-snapshot-packages install -y --no-install-recommends \
- libssl3t64="${UBUNTU_APT_OPENSSL_VERSION}" \
- openssl="${UBUNTU_APT_OPENSSL_VERSION}" \
- openssl-provider-legacy="${UBUNTU_APT_OPENSSL_VERSION}"; \
for package in libssl3t64 openssl openssl-provider-legacy; do \
test "$(dpkg-query -W -f='${Version}' "${package}")" = "${UBUNTU_APT_OPENSSL_VERSION}"; \
done; \
diff --git a/README.md b/README.md
index 6c09fc1c48..57cf56d198 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,19 @@ 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.
+## Source candidate: Engine 0.183.371
+
+Typed Host metadata plans identify the native Host model independently of its
+driver kind, including `docker`. This corrects Host preview and receipt reads
+that could return 404 in 0.183.370. Native role/Key checks, human approval,
+locking and exact-once dispatch remain unchanged. Qualification, publication
+and native acceptance of this candidate are pending. The internal Catalog
+launcher also gains its authenticated API root/schema metadata handshake;
+resource grants and one-time writes remain bounded by the original delegation.
+The builders install the existing pinned final security packages before the
+frozen base plan, retaining the same final versions and source phases. See the
+[candidate note](docs/releases/orchestration-engine-0.183.371.md).
+
## Latest published release: Engine 0.183.370
[Engine 0.183.370](https://github.com/PastureStack/orchestration-engine/releases/tag/v0.183.370)
diff --git a/code/framework/api-pub-sub-jetty/pom.xml b/code/framework/api-pub-sub-jetty/pom.xml
index ca5a0d5a69..12c737806a 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.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/api-pub-sub/pom.xml b/code/framework/api-pub-sub/pom.xml
index e97f965522..7cd38434fd 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.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/api/pom.xml b/code/framework/api/pom.xml
index de7f78f7db..2ad250b951 100644
--- a/code/framework/api/pom.xml
+++ b/code/framework/api/pom.xml
@@ -4,7 +4,7 @@
cattle-parent
io.cattle
- 0.183.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/archaius/pom.xml b/code/framework/archaius/pom.xml
index 69a015e5c3..eb44a73e08 100644
--- a/code/framework/archaius/pom.xml
+++ b/code/framework/archaius/pom.xml
@@ -4,7 +4,7 @@
io.cattle
cattle-meta-parent
- 0.183.370
+ 0.183.371
../../meta-parent/pom.xml
diff --git a/code/framework/async/pom.xml b/code/framework/async/pom.xml
index 16be4ad8d2..115146baeb 100644
--- a/code/framework/async/pom.xml
+++ b/code/framework/async/pom.xml
@@ -4,7 +4,7 @@
io.cattle
cattle-parent
- 0.183.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/auditing/pom.xml b/code/framework/auditing/pom.xml
index d28991f238..aa658c0ca5 100644
--- a/code/framework/auditing/pom.xml
+++ b/code/framework/auditing/pom.xml
@@ -4,7 +4,7 @@
cattle-parent
io.cattle
- 0.183.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/auditing/src/test/java/io/cattle/platform/iaas/api/auditing/ApiKeyAuditTest.java b/code/framework/auditing/src/test/java/io/cattle/platform/iaas/api/auditing/ApiKeyAuditTest.java
index 4dc71c8dc6..ac9af67f44 100644
--- a/code/framework/auditing/src/test/java/io/cattle/platform/iaas/api/auditing/ApiKeyAuditTest.java
+++ b/code/framework/auditing/src/test/java/io/cattle/platform/iaas/api/auditing/ApiKeyAuditTest.java
@@ -145,6 +145,7 @@ public void governanceErrorsUseTheSamePublicKeyIdentityBeforeAndAfterValidation(
service.idFormatter = typedFormatter();
ApiRequest request = new ApiRequest(null, null);
request.setType(type); request.setId(id); request.setMethod("PUT"); request.setResponseCode(status);
+ request.setAttribute("apiKey.audit.requestId", "00001c99-0000-4000-8000-000000000000");
request.setRequestObject(Map.of("id", "1c99", "accountId", "1a99", "secretValue", "private-secret"));
ApiContext.newContext().setPolicy(policy());
ApiContext.getContext().setIdFormatter(service.idFormatter);
@@ -157,7 +158,10 @@ public void governanceErrorsUseTheSamePublicKeyIdentityBeforeAndAfterValidation(
assertEquals("1a2", event.get("actor")); assertEquals("apiKey", event.get("targetType"));
assertEquals("update", event.get("operation")); assertEquals(status, event.get("httpStatus"));
assertEquals(status == 401 || status == 403 ? "DENIED" : "FAILED", event.get("outcome"));
- assertFalse(delivered.toString().contains("private")); assertFalse(delivered.toString().contains("1c99"));
+ assertEquals("00001c99-0000-4000-8000-000000000000", event.get("requestId"));
+ assertFalse(event.containsValue("1c99"));
+ assertFalse(event.containsKey("requestObject")); assertFalse(event.containsKey("responseObject"));
+ assertFalse(event.containsKey("secretValue")); assertFalse(event.toString().contains("private-secret"));
}
}
}
diff --git a/code/framework/db-loader/pom.xml b/code/framework/db-loader/pom.xml
index 3cb1bac916..aafe0604e8 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.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/deferred/pom.xml b/code/framework/deferred/pom.xml
index e774b792fe..fa49d2de04 100644
--- a/code/framework/deferred/pom.xml
+++ b/code/framework/deferred/pom.xml
@@ -4,7 +4,7 @@
io.cattle
cattle-parent
- 0.183.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/encryption/pom.xml b/code/framework/encryption/pom.xml
index 85ab9bd9a9..a7dfdbe2e8 100644
--- a/code/framework/encryption/pom.xml
+++ b/code/framework/encryption/pom.xml
@@ -4,7 +4,7 @@
io.cattle
cattle-parent
- 0.183.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/engine/pom.xml b/code/framework/engine/pom.xml
index 018b5c480d..736a2130fa 100644
--- a/code/framework/engine/pom.xml
+++ b/code/framework/engine/pom.xml
@@ -4,7 +4,7 @@
io.cattle
cattle-parent
- 0.183.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/eventing/pom.xml b/code/framework/eventing/pom.xml
index 6cc25af202..f9674ea542 100644
--- a/code/framework/eventing/pom.xml
+++ b/code/framework/eventing/pom.xml
@@ -4,7 +4,7 @@
io.cattle
cattle-parent
- 0.183.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/events/pom.xml b/code/framework/events/pom.xml
index 6611570858..3dd958529c 100644
--- a/code/framework/events/pom.xml
+++ b/code/framework/events/pom.xml
@@ -4,7 +4,7 @@
io.cattle
cattle-parent
- 0.183.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/extension-spring/pom.xml b/code/framework/extension-spring/pom.xml
index 64965df28e..1576cf2383 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.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/extension/pom.xml b/code/framework/extension/pom.xml
index fc55059518..3a68107f6d 100644
--- a/code/framework/extension/pom.xml
+++ b/code/framework/extension/pom.xml
@@ -4,7 +4,7 @@
cattle-parent
io.cattle
- 0.183.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/java-server/pom.xml b/code/framework/java-server/pom.xml
index 1868e1655b..1506387aa0 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.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/jmx/pom.xml b/code/framework/jmx/pom.xml
index 90493aa6b7..2a2f2207e0 100644
--- a/code/framework/jmx/pom.xml
+++ b/code/framework/jmx/pom.xml
@@ -4,7 +4,7 @@
io.cattle
cattle-parent
- 0.183.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/jooq/pom.xml b/code/framework/jooq/pom.xml
index 4d22214a24..f513d9d1a5 100644
--- a/code/framework/jooq/pom.xml
+++ b/code/framework/jooq/pom.xml
@@ -4,7 +4,7 @@
io.cattle
cattle-parent
- 0.183.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/json/pom.xml b/code/framework/json/pom.xml
index ac79a49611..ebede60ad6 100644
--- a/code/framework/json/pom.xml
+++ b/code/framework/json/pom.xml
@@ -4,7 +4,7 @@
io.cattle
cattle-parent
- 0.183.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/launcher/pom.xml b/code/framework/launcher/pom.xml
index 8c0694f9db..e158c8fb20 100644
--- a/code/framework/launcher/pom.xml
+++ b/code/framework/launcher/pom.xml
@@ -4,7 +4,7 @@
io.cattle
cattle-parent
- 0.183.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/lock/pom.xml b/code/framework/lock/pom.xml
index b2500aa5d4..b71a48cc3c 100644
--- a/code/framework/lock/pom.xml
+++ b/code/framework/lock/pom.xml
@@ -4,7 +4,7 @@
io.cattle
cattle-parent
- 0.183.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/logback/pom.xml b/code/framework/logback/pom.xml
index f21f68f7f0..3c10540049 100644
--- a/code/framework/logback/pom.xml
+++ b/code/framework/logback/pom.xml
@@ -4,7 +4,7 @@
io.cattle
cattle-meta-parent
- 0.183.370
+ 0.183.371
../../meta-parent/pom.xml
diff --git a/code/framework/managed-context/pom.xml b/code/framework/managed-context/pom.xml
index 845af59bd4..3db20e4ca4 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.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/metrics/pom.xml b/code/framework/metrics/pom.xml
index 3ca8e59fee..ffb386932e 100644
--- a/code/framework/metrics/pom.xml
+++ b/code/framework/metrics/pom.xml
@@ -4,7 +4,7 @@
io.cattle
cattle-parent
- 0.183.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/module/pom.xml b/code/framework/module/pom.xml
index 027bb42925..cf1a9d5fd7 100644
--- a/code/framework/module/pom.xml
+++ b/code/framework/module/pom.xml
@@ -4,7 +4,7 @@
io.cattle
cattle-parent
- 0.183.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/object/pom.xml b/code/framework/object/pom.xml
index 6ffd58cbd6..1f91d3f943 100644
--- a/code/framework/object/pom.xml
+++ b/code/framework/object/pom.xml
@@ -4,7 +4,7 @@
io.cattle
cattle-parent
- 0.183.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/pool/pom.xml b/code/framework/pool/pom.xml
index 8a56ad90ae..e12c4ea688 100644
--- a/code/framework/pool/pom.xml
+++ b/code/framework/pool/pom.xml
@@ -4,7 +4,7 @@
io.cattle
cattle-parent
- 0.183.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/resource-monitor/pom.xml b/code/framework/resource-monitor/pom.xml
index e1251af43e..994a551d20 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.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/schema/pom.xml b/code/framework/schema/pom.xml
index 764f3fbba5..f092c8cea0 100644
--- a/code/framework/schema/pom.xml
+++ b/code/framework/schema/pom.xml
@@ -4,7 +4,7 @@
cattle-parent
io.cattle
- 0.183.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/server/pom.xml b/code/framework/server/pom.xml
index 83948d75a9..02957bdf74 100644
--- a/code/framework/server/pom.xml
+++ b/code/framework/server/pom.xml
@@ -4,7 +4,7 @@
cattle-meta-parent
io.cattle
- 0.183.370
+ 0.183.371
../../meta-parent/pom.xml
diff --git a/code/framework/spring/pom.xml b/code/framework/spring/pom.xml
index 1721c6b2cd..83d2959f84 100644
--- a/code/framework/spring/pom.xml
+++ b/code/framework/spring/pom.xml
@@ -4,7 +4,7 @@
io.cattle
cattle-parent
- 0.183.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/system-task/pom.xml b/code/framework/system-task/pom.xml
index d95516883f..70356d9ce2 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.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/token/pom.xml b/code/framework/token/pom.xml
index b6f496036d..fd7d3f14d7 100644
--- a/code/framework/token/pom.xml
+++ b/code/framework/token/pom.xml
@@ -4,7 +4,7 @@
io.cattle
cattle-parent
- 0.183.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/framework/utils/pom.xml b/code/framework/utils/pom.xml
index c2aedff318..05da0b40db 100644
--- a/code/framework/utils/pom.xml
+++ b/code/framework/utils/pom.xml
@@ -4,7 +4,7 @@
io.cattle
cattle-parent
- 0.183.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/iaas/agent-instance/pom.xml b/code/iaas/agent-instance/pom.xml
index 8c360e8430..b93fcb8bf2 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.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/iaas/agent-server/pom.xml b/code/iaas/agent-server/pom.xml
index 22e1b768cb..5683195985 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.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/iaas/agent/pom.xml b/code/iaas/agent/pom.xml
index 90b1147525..a03839be8e 100644
--- a/code/iaas/agent/pom.xml
+++ b/code/iaas/agent/pom.xml
@@ -4,7 +4,7 @@
io.cattle
cattle-parent
- 0.183.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/iaas/allocator/pom.xml b/code/iaas/allocator/pom.xml
index d678223c6a..695a3df19e 100644
--- a/code/iaas/allocator/pom.xml
+++ b/code/iaas/allocator/pom.xml
@@ -4,7 +4,7 @@
io.cattle
cattle-parent
- 0.183.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/iaas/api-logic/pom.xml b/code/iaas/api-logic/pom.xml
index 48c15b90c3..95451062da 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.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/iaas/archaius-management/pom.xml b/code/iaas/archaius-management/pom.xml
index 0cd7129c8a..fd716d9292 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.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/iaas/auth-logic/pom.xml b/code/iaas/auth-logic/pom.xml
index 5995405f27..0051e99455 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.370
+ 0.183.371
../../parent/pom.xml
diff --git a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchAccess.java b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchAccess.java
index 0acf2f4d76..8e8def9da6 100644
--- a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchAccess.java
+++ b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchAccess.java
@@ -54,7 +54,7 @@ public static boolean trustedDomain(ApiRequest request) {
if (!sameId && permit.id() != null && request.getId() != null && request.getId().matches("[1-9][0-9]*")
&& ApiContext.getContext() != null && ApiContext.getContext().getIdFormatter() != null)
sameId = request.getId().equals(ApiContext.getContext().getIdFormatter().parseId(permit.id()));
- return permit.type().equals(request.getType()) && permit.method().equals(request.getMethod())
+ return Objects.equals(permit.type(), request.getType()) && permit.method().equals(request.getMethod())
&& sameId && Objects.equals(permit.action(), request.getAction());
}
diff --git a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchDelegation.java b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchDelegation.java
index eacabfbcc2..cc52a4c0f2 100644
--- a/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchDelegation.java
+++ b/code/iaas/auth-logic/src/main/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchDelegation.java
@@ -15,6 +15,7 @@
import io.cattle.platform.object.util.ObjectUtils;
import io.cattle.platform.process.common.handler.ProcessEventDecorator;
import io.github.ibuildthecloud.gdapi.model.Resource;
+import io.github.ibuildthecloud.gdapi.exception.ClientVisibleException;
import io.github.ibuildthecloud.gdapi.request.ApiRequest;
import io.github.ibuildthecloud.gdapi.request.handler.ApiRequestCompletionSink;
import jakarta.inject.Inject;
@@ -32,6 +33,7 @@
/** One event-local, exact-graph capability for the authenticated internal Compose launcher. */
public class AiCatalogLaunchDelegation implements ProcessEventDecorator, ApiRequestCompletionSink,
io.cattle.platform.api.auth.CatalogLaunchTransformAuthorization {
+ private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(AiCatalogLaunchDelegation.class);
public static final String HEADER = "X-PastureStack-Catalog-Launch-Delegation";
private static final String VERIFIED = AiCatalogLaunchDelegation.class.getName();
private static final String CLAIM = VERIFIED + ".claim";
@@ -97,23 +99,37 @@ public Admission admit(ApiRequest request, Account authenticated) {
if (launcher(authenticated) && targetsActiveExecution(request)) throw error(403, "CatalogLaunchDelegationRequired");
return null;
}
- if (!launcher(authenticated) || !proof.matches("cld1\\.[0-9a-f-]{36}\\.[A-Za-z0-9_-]{43}")) throw error(403, "CatalogLaunchDelegationDenied");
- String planId = "aicataloglaunch-" + proof.substring(5, 41);
- return store.independent(() -> {
- Map plan = store.loadForUpdate(planId);
- if (plan == null) throw error(403, "CatalogLaunchDelegationDenied");
- if (!(plan.get("delegationBindingDigest") instanceof String bindingDigest) || !bindingDigest.matches("[0-9a-f]{64}")
- || !constant(hash(proof + ":" + bindingDigest), Objects.toString(plan.get("delegationProofHash"), "")))
+ try {
+ if (!launcher(authenticated) || !proof.matches("cld1\\.[0-9a-f-]{36}\\.[A-Za-z0-9_-]{43}"))
throw error(403, "CatalogLaunchDelegationDenied");
- // The proof is authentic before any live-policy rejection. Only now
- // may generic audit omit this private request/response payload.
- ApiAuditPayload.verifiedCatalogLaunch(request);
+ String planId = "aicataloglaunch-" + proof.substring(5, 41);
+ return store.independent(() -> {
+ Map plan = store.loadForUpdate(planId);
+ if (plan == null) throw error(403, "CatalogLaunchDelegationDenied");
+ if (!(plan.get("delegationBindingDigest") instanceof String bindingDigest) || !bindingDigest.matches("[0-9a-f]{64}")
+ || !constant(hash(proof + ":" + bindingDigest), Objects.toString(plan.get("delegationProofHash"), "")))
+ throw error(403, "CatalogLaunchDelegationDenied");
+ // The proof is authentic before any live-policy rejection. Only now
+ // may generic audit omit this private request/response payload.
+ ApiAuditPayload.verifiedCatalogLaunch(request);
+ return admitVerified(request, plan, bindingDigest);
+ });
+ } catch (ClientVisibleException denied) {
+ // Unverified proofs have no trusted plan/Key attribution. Never log the proof, body or original error.
+ LOG.warn("Catalog launch delegation rejected: phase=admission status={} reason={}", denied.getStatus(), safeReason(denied));
+ throw denied;
+ }
+ }
+
+ private Admission admitVerified(ApiRequest request, Map plan, String bindingDigest) {
+ AiCatalogLaunchAccess.Authorization authority = null;
+ try {
Map values = privateData.open(plan);
if (!bindingDigest.equals(binding(plan, values))) throw error(403, "CatalogLaunchDelegationDenied");
requireActive(plan);
if (!(plan.get("delegationExpiresAt") instanceof Number expiry) || clock.millis() >= expiry.longValue())
throw error(403, "CatalogLaunchDelegationExpired");
- var authority = access.bound(plan, request); access.catalog(authority);
+ authority = access.bound(plan, request); access.catalog(authority);
frozenStack(plan, values);
Object project = request.getAttribute(io.cattle.platform.core.constants.ProjectConstants.PROJECT_HEADER);
String projectHeader = request.getServletContext().getRequest().getHeader(io.cattle.platform.core.constants.ProjectConstants.PROJECT_HEADER);
@@ -134,12 +150,30 @@ public Admission admit(ApiRequest request, Account authenticated) {
if (verified.operation() != null) claim(plan, verified, request);
Account actor = access.objects.loadResource(Account.class, authority.current().policy().getAuthenticatedAsAccountId());
return new Admission(authority.current(), actor);
- });
+ } catch (ClientVisibleException denied) {
+ var current = authority;
+ if (current != null && current.key() != null) ApiKeyCredentialContext.attach(request, current.key());
+ // A verified proof identifies this plan, but a rejected live owner must never be replaced by the launcher Policy.
+ AiCatalogLaunchAccess.inContext(request, current == null ? null : current.current().policy(), () -> {
+ plans.audit(request, "delegate", plan, false, safeReason(denied)); return null;
+ });
+ throw denied;
+ }
+ }
+
+ private static String safeReason(ClientVisibleException denied) {
+ String code = denied.getCode();
+ return code != null && code.matches("[A-Za-z][A-Za-z0-9_]{0,63}") ? code : "CatalogLaunchDelegationDenied";
}
Verified match(ApiRequest request, Map plan, Map values) {
if (request.getLink() != null) throw error(403, "CatalogLaunchDelegationDenied");
String type = request.getType(), method = request.getMethod(), id = request.getId(), action = request.getAction();
+ if (type == null) {
+ if ("GET".equals(method) && id == null && action == null && parameters(request).isEmpty() && versionRoot(request))
+ return new Verified((String) plan.get("id"), null, null, null, true);
+ throw error(403, "CatalogLaunchDelegationDenied");
+ }
Map graph = map(values.get("graph")), created = AiCatalogLaunchMaterializer.nullableMap(plan.get("createdGraph"));
String stackId = wire("stack", ((Number) plan.get("internalStackId")).longValue());
if ("GET".equals(method) && action == null) {
@@ -193,6 +227,17 @@ && map(values.get("authorizedReferenceIds")).containsKey(params.get("name")))
throw error(403, "CatalogLaunchDelegationDenied");
}
+ private static boolean versionRoot(ApiRequest request) {
+ String version = request.getVersion();
+ if (!("v1".equals(version) || "v2-beta".equals(version)) || request.getServletContext() == null) return false;
+ var http = request.getServletContext().getRequest();
+ String path = http.getServletPath(), root = "/" + version;
+ return "GET".equals(http.getMethod()) && (root.equals(path) || (root + "/").equals(path))
+ && Objects.equals(http.getRequestURI(), Objects.toString(http.getContextPath(), "") + path)
+ && http.getQueryString() == null && (request.getRequestObject() == null
+ || request.getRequestObject() instanceof Map, ?> body && body.isEmpty());
+ }
+
void claim(Map plan, Verified verified, ApiRequest request) {
Map ledger = AiCatalogLaunchMaterializer.nullableMap(plan.get("ledger"));
String digest = graphs.digest(body(request));
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 68613d3065..529c66e959 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
@@ -112,12 +112,17 @@ private Object read(ApiRequest original, AiResourceChangeDescriptor descriptor,
request.setAttribute(ALIAS_REFERENCE, new AliasReference(type, publicId, Long.toString(internalId(type, publicId))));
return authorized(original, request, () -> {
Object row = manager(type).getById(type, request.getId(), new ListOptions(request));
- if (row == null || ObjectUtils.getRemoved(row) != null || !Objects.equals(ObjectUtils.getKind(row), type)
+ if (row == null || ObjectUtils.getRemoved(row) != null || !matchesResourceType(type, row)
|| !Objects.equals(ObjectUtils.getAccountId(row), ApiUtils.getPolicy().getAccountId())) throw error(404, "NotFound");
return row;
});
}
+ private static boolean matchesResourceType(String type, Object target) {
+ // A Host's kind identifies its native driver (for example docker), not the API resource type.
+ return "host".equals(type) ? target instanceof Host : Objects.equals(ObjectUtils.getKind(target), type);
+ }
+
public Map prepare(ApiRequest original, AiResourceChangeDescriptor descriptor,
String publicId, Map parameters) {
return prepare(original, descriptor, publicId, parameters, null);
@@ -406,7 +411,7 @@ public Object receiptTarget(ApiRequest request, AiResourceChangeDescriptor descr
Schema schema = current.schemas().getSchema(descriptor.resourceType());
if (schema == null || schema.getResourceMethods() == null || !schema.getResourceMethods().contains("GET")) throw error(403, "PermissionDenied");
if (target == null || !Objects.equals(ObjectUtils.getAccountId(target), actor.getAccountId())
- || !Objects.equals(ObjectUtils.getKind(target), descriptor.resourceType()) || current.policy().authorizeObject(target) == null)
+ || !matchesResourceType(descriptor.resourceType(), target) || current.policy().authorizeObject(target) == null)
throw error(403, "PermissionDenied");
if (descriptor.createsResource() && "service".equals(descriptor.resourceType())
&& (number(ObjectUtils.getId(target)) != number(plan.get("internalResourceId"))
diff --git a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchDelegationTest.java b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchDelegationTest.java
index f79d9cbe0a..d4a180798c 100644
--- a/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchDelegationTest.java
+++ b/code/iaas/auth-logic/src/test/java/io/cattle/platform/iaas/api/auth/ai/catalog/AiCatalogLaunchDelegationTest.java
@@ -31,6 +31,7 @@
import io.github.ibuildthecloud.gdapi.factory.SchemaFactory;
import io.github.ibuildthecloud.gdapi.model.impl.SchemaImpl;
import io.github.ibuildthecloud.gdapi.request.ApiRequest;
+import io.github.ibuildthecloud.gdapi.request.parser.DefaultApiRequestParser;
import io.github.ibuildthecloud.gdapi.server.model.ApiServletContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@@ -59,6 +60,126 @@ public class AiCatalogLaunchDelegationTest {
if (previous == null) ArchaiusUtil.getConfiguration().clearProperty("api.ai.catalog.launch.enabled");
else ArchaiusUtil.getConfiguration().setProperty("api.ai.catalog.launch.enabled", previous); }
+ @Test public void nativeVersionRootSchemaHandshakeIsRepeatableOwnerMetadataWithoutChildWrites() throws Exception {
+ for (String version : List.of("v1", "v2-beta")) {
+ Fixture f = new Fixture();
+ for (String suffix : List.of("", "/schemas", "/", "/schemas")) {
+ ApiRequest request = f.route("/" + version + suffix, "GET", null, Map.of(), f.proof);
+ assertEquals(version, request.getVersion()); assertNull(request.getId()); assertNull(request.getLink()); assertNull(request.getAction());
+ if (suffix.endsWith("schemas")) assertEquals("schema", request.getType()); else assertNull(request.getType());
+ f.delegation.access.authenticator.handle(request);
+ assertTrue(AiCatalogLaunchDelegation.bootstrap(request));
+ assertEquals(42L, ((Policy) ApiContext.getContext().getPolicy()).getAuthenticatedAsAccountId());
+ assertEquals(5L, ((Policy) ApiContext.getContext().getPolicy()).getAccountId());
+ assertTrue(AiCatalogLaunchAccess.trustedDomain(request));
+ request.setUrlBuilder(mock(io.github.ibuildthecloud.gdapi.url.UrlBuilder.class));
+ if (suffix.endsWith("schemas")) new io.github.ibuildthecloud.gdapi.request.handler.SchemasHandler().handle(request);
+ else new io.github.ibuildthecloud.gdapi.request.handler.VersionHandler().handle(request);
+ assertNotNull(request.getResponseObject());
+ var locator = mock(io.github.ibuildthecloud.gdapi.request.resource.ResourceManagerLocator.class);
+ var resources = new io.github.ibuildthecloud.gdapi.request.handler.ResourceManagerRequestHandler();
+ resources.setResourceManagerLocator(locator); resources.handle(request); verifyNoInteractions(locator);
+ assertFalse(new JacksonJsonMapper().writeValueAsString(request.getResponseObject()).contains(f.proof));
+ f.delegation.complete(request, null); assertEquals(200, request.getResponseCode());
+ }
+ ApiRequest target = f.route("/" + version + "/stacks/1st889", "GET", null, Map.of(), f.proof);
+ f.delegation.access.authenticator.handle(target);
+ assertFalse(AiCatalogLaunchDelegation.bootstrap(target));
+ assertEquals(0, f.store.writesAfterIssue); assertFalse(f.plan.containsKey("ledger")); assertFalse(f.plan.containsKey("createdGraph"));
+ verify(f.delegation.plans, times(5)).audit(any(), eq("delegate"), same(f.plan), eq(true), eq("CatalogLaunchDelegationAllowed"));
+ verifyNoInteractions(f.transformer);
+ }
+ }
+
+ @Test public void versionRootRejectsUnknownPathsMethodsQueriesAndContradictoryRouteShapesWithSafe403() throws Exception {
+ for (String mutation : List.of("empty", "slash", "unknown", "unsupported", "post", "query", "action", "link", "id", "uri",
+ "null-unknown", "version", "body", "params")) {
+ Fixture f = new Fixture();
+ String path = switch (mutation) {
+ case "empty" -> ""; case "slash" -> "/"; case "unknown", "null-unknown" -> "/v2-beta/unknown";
+ case "unsupported" -> "/v9"; default -> "/v2-beta";
+ };
+ ApiRequest request = f.route(path, "post".equals(mutation) ? "POST" : "GET",
+ "query".equals(mutation) ? "name=private-synthetic" : null,
+ "query".equals(mutation) ? Map.of("name", new String[] { "private-synthetic" }) : Map.of(), f.proof);
+ if ("action".equals(mutation)) request.setAction("activate");
+ if ("link".equals(mutation)) request.setLink("services");
+ if ("id".equals(mutation)) request.setId("1st889");
+ if ("uri".equals(mutation)) when(request.getServletContext().getRequest().getRequestURI()).thenReturn("/v2-beta/unknown");
+ if ("null-unknown".equals(mutation)) request.setType(null);
+ if ("version".equals(mutation)) request.setVersion("v1");
+ if ("body".equals(mutation)) request.setRequestObject(Map.of("name", "private-synthetic"));
+ if ("params".equals(mutation)) request.setRequestParams(Map.of("name", "private-synthetic"));
+ var denied = assertThrows(mutation, ClientVisibleException.class, () -> f.delegation.access.authenticator.handle(request));
+ assertEquals(mutation, 403, denied.getStatus()); assertEquals("CatalogLaunchDelegationDenied", denied.getCode());
+ assertFalse(AiCatalogLaunchDelegation.bootstrap(request)); assertEquals(0, f.store.writesAfterIssue);
+ verifyNoInteractions(f.transformer);
+ }
+ }
+
+ @Test public void versionRootRechecksEventProofOriginalOwnerSourceAndFrozenStackBeforeMetadata() throws Exception {
+ for (String mutation : List.of("forged", "event", "expired", "closed", "project", "owner", "revision", "revoked",
+ "credential-expiry", "policy", "source", "stack")) {
+ Fixture f = new Fixture(); String proof = f.proof;
+ if ("forged".equals(mutation)) proof = proof.substring(0, proof.length() - 1) + (proof.endsWith("A") ? "B" : "A");
+ if ("event".equals(mutation)) f.plan.put("delegationEventId", UUID.randomUUID().toString());
+ if ("expired".equals(mutation)) f.plan.put("delegationExpiresAt", 0L);
+ if ("closed".equals(mutation)) f.plan.put("delegationClosed", true);
+ if ("owner".equals(mutation)) f.denial = "OwnerPermissionDenied";
+ if ("revision".equals(mutation)) f.denial = "ApiKeyPolicyChanged";
+ if ("revoked".equals(mutation)) f.denial = "ApiKeyRevoked";
+ if ("credential-expiry".equals(mutation)) f.denial = "ApiKeyExpired";
+ if ("policy".equals(mutation)) f.denial = "KeyPolicyDenied";
+ if ("source".equals(mutation)) f.delegation.gateway = new AiCatalogLaunchGateway() {
+ @Override public Map material(String p, String t, String v) {
+ return Map.of("digest", "b".repeat(64), "questionSchema", List.of());
+ }
+ };
+ if ("stack".equals(mutation)) f.stack.setName("changed");
+ ApiRequest request = f.route("/v2-beta", "GET", null, Map.of(), proof);
+ if ("project".equals(mutation)) request.setAttribute(io.cattle.platform.core.constants.ProjectConstants.PROJECT_HEADER, "1a88");
+ var denied = assertThrows(mutation, ClientVisibleException.class, () -> f.delegation.access.authenticator.handle(request));
+ String expected = switch (mutation) {
+ case "expired" -> "CatalogLaunchDelegationExpired"; case "owner" -> "OwnerPermissionDenied";
+ case "revision" -> "ApiKeyPolicyChanged"; case "source", "stack" -> "CatalogLaunchSourceChanged";
+ case "revoked" -> "ApiKeyRevoked"; case "credential-expiry" -> "ApiKeyExpired"; case "policy" -> "KeyPolicyDenied";
+ default -> "CatalogLaunchDelegationDenied";
+ };
+ assertEquals(mutation, Set.of("source", "stack").contains(mutation) ? 409 : 403, denied.getStatus());
+ assertEquals(mutation, expected, denied.getCode());
+ assertFalse(AiCatalogLaunchDelegation.bootstrap(request)); assertEquals(0, f.store.writesAfterIssue);
+ verifyNoInteractions(f.transformer);
+ }
+ }
+
+ @Test public void versionRootAuditsVerifiedDenialsWithoutAttributingForgedProofToPlanOrKey() throws Exception {
+ for (String mutation : List.of("query", "expired", "owner", "forged")) {
+ Fixture f = new Fixture(); String proof = f.proof;
+ if ("expired".equals(mutation)) f.plan.put("delegationExpiresAt", 0L);
+ if ("owner".equals(mutation)) f.denial = "OwnerPermissionDenied";
+ if ("forged".equals(mutation)) proof = proof.substring(0, proof.length() - 1) + (proof.endsWith("A") ? "B" : "A");
+ ApiRequest request = f.route("/v2-beta", "GET", "query".equals(mutation) ? "synthetic-private=secret" : null,
+ "query".equals(mutation) ? Map.of("synthetic-private", new String[] { "secret" }) : Map.of(), proof);
+ ClientVisibleException denied = assertThrows(ClientVisibleException.class, () -> f.delegation.access.authenticator.handle(request));
+ assertEquals(403, denied.getStatus());
+ assertEquals("expired".equals(mutation) ? "CatalogLaunchDelegationExpired"
+ : "owner".equals(mutation) ? "OwnerPermissionDenied" : "CatalogLaunchDelegationDenied", denied.getCode());
+ if ("forged".equals(mutation)) {
+ assertTrue(f.catalogDecisions.isEmpty()); assertNull(ApiKeyCredentialContext.get(request));
+ } else {
+ assertEquals(1, f.catalogDecisions.size()); Map decision = f.catalogDecisions.getFirst();
+ assertEquals(false, decision.get("allowed")); assertEquals(denied.getCode(), decision.get("reason"));
+ assertEquals(f.plan.get("id"), decision.get("planId"));
+ assertEquals("query".equals(mutation) ? 42L : null, decision.get("actor"));
+ assertEquals("query".equals(mutation) ? 7L : null, decision.get("key"));
+ assertFalse(decision.toString().contains(proof)); assertFalse(decision.toString().contains("synthetic-private"));
+ assertFalse(decision.toString().contains("secret"));
+ }
+ assertNull(ApiContext.getContext().getPolicy()); assertSame(request, ApiContext.getContext().getApiRequest());
+ assertEquals(0, f.store.writesAfterIssue); verifyNoInteractions(f.transformer);
+ }
+ }
+
@Test public void earlyTransformUsesRealAuthenticationAndFrozenInspectInsteadOfLauncherFullPolicy() throws Exception {
Fixture f = new Fixture(); ApiRequest request = f.transform(f.proof, f.inspect);
assertTrue(f.handler.handle(request));
@@ -231,11 +352,14 @@ final class Fixture {
final TransformInspect handler = new TransformInspect(); final DockerTransformer transformer = mock(DockerTransformer.class);
final Map inspect = Map.of("Name", "/app", "Config", Map.of("Image", "approved"));
final Map plan = new LinkedHashMap<>(), values = new LinkedHashMap<>();
+ final List