diff --git a/.github/workflows/publish-current-server.yml b/.github/workflows/publish-current-server.yml
index 700314f814..4ebfb40d52 100644
--- a/.github/workflows/publish-current-server.yml
+++ b/.github/workflows/publish-current-server.yml
@@ -336,8 +336,10 @@ jobs:
# Exercise the published API and real auth overlay on this fresh,
# disposable database before the image is allowed to be released.
python3 source/scripts/test-mfa-policy-api.py \
- --port 18082 --disposable |
+ --port 18082 --disposable --registration-basic |
tee "$evidence/mfa-policy-api-smoke.txt"
+ grep -E '^REGISTRATION_BASIC_API_OK checks=7 ordinary_user=true entry_only=true node_active_verified=false script_sha256=[0-9a-f]{64}$' \
+ "$evidence/mfa-policy-api-smoke.txt" >"$evidence/registration-basic-api-smoke.txt"
docker stop "$CANDIDATE_NAME" >/dev/null
mkdir -p "$rootfs"
@@ -585,6 +587,7 @@ jobs:
host-api-package-check.txt \
image-inspect.txt \
mfa-policy-api-smoke.txt \
+ registration-basic-api-smoke.txt \
published.txt \
push.log \
release-notes.md \
@@ -615,6 +618,7 @@ jobs:
"$evidence/host-api-package-check.txt" \
"$evidence/image-inspect.txt" \
"$evidence/mfa-policy-api-smoke.txt" \
+ "$evidence/registration-basic-api-smoke.txt" \
"$evidence/published.txt" \
"$evidence/push.log" \
"$evidence/release-notes.md" \
diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md
index de306769f0..bceafd664e 100644
--- a/COMPATIBILITY.md
+++ b/COMPATIBILITY.md
@@ -1,5 +1,23 @@
# Compatibility Contract
+## Server v1.6.522 prepared, not published
+
+The minimal onboarding candidate uses released Engine `v0.183.339` and Web Console
+`1.6.183`. It restores dedicated registration-token Basic authentication.
+Custom add-host requires RegistrationToken POST; built-in and Other drivers
+require Host POST, using the current project's loaded native schemas.
+Cancel / Go Back preserves the external return target across driver changes and
+API-host settings refresh; missing or self-referencing targets return to Hosts.
+Backend roles, resource authorization, other Server components and the console
+broker are unchanged.
+
+Clean merged component source and official asset checksums have been read back;
+the original component packagers produced those assets. Focused component and
+native UI validation passed, but the final Server image, its seven-check native
+registration gate and active-node lifecycle acceptance remain pending.
+Integration candidates are not this published release. See the
+[candidate note](docs/releases/server-1.6.522.md) for exact component identities.
+
## Server v1.6.521 published
The [official release](https://github.com/PastureStack/server/releases/tag/v1.6.521)
diff --git a/README.md b/README.md
index b1b22d3328..77be1e24aa 100644
--- a/README.md
+++ b/README.md
@@ -9,72 +9,68 @@ Rancher 1.6 ecosystem. It is not affiliated with Rancher Labs or SUSE.
This fork of [`rancher/rancher`](https://github.com/rancher/rancher) preserves
upstream history, authorship, licenses, and notices.
-## Current release
-
-Server `v1.6.521` packages Web Console `1.6.181`
-and Orchestration Engine `v0.183.337`. API Keys support full, custom and closed
-policies, optional expiry, and key-scoped durable audit records. Their permissions
-remain bounded by the owner's current role and environment access. Delegated
-terminal/log access requires the verified Host API audit capability and the
-compatible Linux Node Agent lifecycle.
-Custom Key Project targets share the normal API's live membership checks;
-membership never bypasses Key rules or unrelated resource authorization.
+
+
+## Current source candidate
+
+Server `v1.6.522` is a prepared source candidate, not a published image. It targets
+released Web Console `1.6.183` and Orchestration Engine `v0.183.339`. Registration
+tokens use their dedicated Basic authentication route. Add-host access follows
+the form's actual operation: Custom requires RegistrationToken POST; built-in
+and Other drivers require Host POST. Cancel / Go Back retains the return route
+across driver changes and API-host settings refresh, with Hosts as the fallback
+for a missing or self-referencing target.
+Component validation and official asset readback have passed; final Server image
+publication, its native registration gate and active-node acceptance remain pending.
+The published Server remains
+[`v1.6.521`](https://github.com/PastureStack/server/releases/tag/v1.6.521); its
+registration-token Basic route has a known authentication regression. Its
+[release note](docs/releases/server-1.6.521.md) retains the published identity. See the
+[candidate release note](docs/releases/server-1.6.522.md) for the release boundary.
The console supports OIDC, TOTP and passkeys, role-aware resource operations,
container resource/hardware settings, and movable terminal/log windows.
Hardware options require compatible node software and actual host capabilities;
-GPU device access is not exclusive GPU allocation. Audit queries and exports
-cover retained records with environment-aware access and bounded pagination.
-Explicit date bounds remain attached to every audit page, so an older interval
-does not scan newer records first. Oversized queries retain the console filters
-and offer explicit narrowing or retry. Export failures are translated on the
-page instead of downloaded as error files. An export failure keeps successful
-query results intact; scan and export limits are retained.
-The Key modal owns initial focus and yields to keyboard or pointer interaction.
-Audit export closes its own menu before locking the request, allowing consecutive
-JSON, CSV and XLSX downloads without leaving a disabled menu open.
-Key scope choices use the viewer's authorized environment list. Environment,
-stack and resource selectors close before dependent loading or verification.
-Failed Key edits use a consistent public Key ID in audit records. Per-Key audit
-also reads historical numeric-ID records within the same ownership and environment
-authorization checks; existing records are not rewritten.
-See the [published v1.6.521 release](https://github.com/PastureStack/server/releases/tag/v1.6.521)
-and [release note](docs/releases/server-1.6.521.md) for exact
-component identities and verification boundaries. A version or packaged feature
-does not establish that every resource/role/hardware combination has been tested.
+GPU device access is not exclusive GPU allocation. Audit queries and JSON, CSV
+and XLSX exports preserve environment authorization, explicit date bounds and
+bounded pagination. Query/export errors retain filters and successful query
+results. See the [Host compatibility guide](docs/hosts/README.md) before enrolling
+a host and the [API Key guide](docs/api-keys.md) for policy and audit behavior.
+A packaged feature does not establish that every resource, role or hardware
+combination has been tested.
## API Keys
Choose allow-by-default with deny exceptions, or deny-by-default with allow
exceptions, with optional expiry. Without exceptions these retain full or closed
access. Search for resource names; the editor and reviewed changes include a
-resource-by-operation policy matrix, not an extra grant of authority. The maximum access
-is always the Key owner's current account/environment RBAC; a Key never grants
-an additional role. Existing legacy Keys are not automatically narrowed on upgrade.
+resource-by-operation policy matrix, not an extra grant of authority. The maximum
+access is the Key owner's current account/environment RBAC, including live
+membership checks; a Key never grants an additional role. Existing legacy Keys
+are not automatically narrowed on upgrade.
New Key secrets are shown once at creation and are not returned by later lists or
details. Per-Key audit separates authorization, the HTTP response and any later
job/stream completion. A successful WebSocket upgrade is recorded as a successful
HTTP `101` handshake, not as proof of the stream's later completion.
Environment-Key audit rechecks live Engine access in
Engine-verified project contexts; a caller-supplied project header is not a grant.
-Open a Key's audit from its action menu; the close control follows the audit
-filters, results and details, below the audit block.
-Scope options and unselected hints use readable light/dark theme text, including
-keyboard highlighting; narrow layouts keep the matrix in its own scroll region.
-Errors use the existing API status/code and console error
-handling. See the [API Key guide](docs/api-keys.md) for policy and audit contracts.
+Delegated terminal/log access requires the verified Host API audit capability
+and a compatible Linux Node Agent lifecycle. Open a Key's audit from its action
+menu. See the [API Key guide](docs/api-keys.md) for policy and audit contracts.
## Quick start
-Public GHCR downloads do not require a registry login. Use the published numeric
-version tag and keep all three named volumes:
+Public GHCR downloads do not require a registry login. The examples below target
+the prepared numeric `v1.6.522` tag: run them only after its official release and
+image have been published and read back. This candidate is not yet available
+from GHCR. Keep all three named volumes:
```sh
docker run -d --name pasturestack-server --restart unless-stopped -p 8080:8080 \
-v pasturestack-cattle:/var/lib/cattle \
-v pasturestack-mysql:/var/lib/mysql \
-v pasturestack-mysqllog:/var/log/mysql \
- ghcr.io/pasturestack/server:v1.6.521
+ ghcr.io/pasturestack/server:v1.6.522
```
For HTTPS termination at a reverse proxy, set the exact public origin:
@@ -82,7 +78,7 @@ For HTTPS termination at a reverse proxy, set the exact public origin:
```yaml
services:
pasturestack-server:
- image: ghcr.io/pasturestack/server:v1.6.521
+ image: ghcr.io/pasturestack/server:v1.6.522
restart: unless-stopped
ports:
- "8080:8080"
diff --git a/docs/README.md b/docs/README.md
index 2e0ccfe4d2..ed16cb3964 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -7,6 +7,12 @@ runtime gates have passed.
## Release notes
+- [Server v1.6.522 candidate](releases/server-1.6.522.md) — prepared source using
+ released Engine `0.183.339` and Web Console `1.6.183`; dedicated registration
+ authentication and schema-bound add-host access/navigation. Component checks
+ passed; the final Server image, native registration gate and active-node
+ acceptance remain pending. It is not a published image.
+
- [Server v1.6.521](releases/server-1.6.521.md) — officially published with
Engine `0.183.337` and Web Console `1.6.181`; canonical API Key audit identifiers
and historical numeric-ID reads retain ownership and environment checks.
diff --git a/docs/performance/README.md b/docs/performance/README.md
index 9c36c507f9..f8988e2765 100644
--- a/docs/performance/README.md
+++ b/docs/performance/README.md
@@ -8,10 +8,14 @@ The following example is appropriate as a starting point for a dedicated
16-vCPU host with about 32 GiB of RAM. It is not a universal performance
profile; measure the real workload before increasing it further.
+The example matches the prepared `v1.6.522` source candidate: run it only after
+its official release and image have been published and read back. This candidate
+is not yet available from GHCR; see the [candidate release note](../releases/server-1.6.522.md).
+
```yaml
services:
pasturestack-server:
- image: ghcr.io/pasturestack/server:v1.6.521
+ image: ghcr.io/pasturestack/server:v1.6.522
restart: unless-stopped
ports:
- "8080:8080"
diff --git a/docs/releases/server-1.6.522.md b/docs/releases/server-1.6.522.md
new file mode 100644
index 0000000000..79372a71a7
--- /dev/null
+++ b/docs/releases/server-1.6.522.md
@@ -0,0 +1,76 @@
+# Server v1.6.522 candidate
+
+Prepared source only; no Server image or release has been published.
+The candidate uses released Engine `v0.183.339` and Web Console `1.6.183`.
+The published Server remains `v1.6.521`.
+
+## Minimal onboarding changes
+
+Engine gives the reserved `registrationToken` Basic username to its dedicated
+registration lookup before ordinary API Key authentication. A rejected native
+token does not fall through to an API Key or browser session. This does not
+broaden the register schema, API Key permissions or account RBAC.
+
+Web Console gates each add-host form by its actual submission path: Custom
+creates a RegistrationToken and requires RegistrationToken POST; built-in and
+Other driver forms create a Host and require Host POST. Machine POST alone does
+not grant either path. These checks use the current project's loaded native
+schemas; absent, readonly or stale schemas grant no onboarding access.
+
+Cancel / Go Back explicitly dispatches to the active route. The external return
+target is saved before the initial driver redirect and preserved across driver
+changes and API-host settings refresh. Missing, loading or self-referencing
+add-host targets fall back to Hosts; valid external targets and the existing
+waiting destination remain. Other Server components and the console broker are
+unchanged; this is not the broader API Key integration candidate.
+
+## Released component inputs
+
+Engine `v0.183.339` uses source
+`436a37215bc5702e516ecf1feb6753b4be5bf602`; official `cattle.jar` SHA-256 is
+`bce020a298ebb89ef3d14ba0666f040e4a732288d1b100da9859dcf957a7981d`.
+Web Console `1.6.183` uses normally merged source
+`e056a0111d20816742eff17a65dfabb5d003dae2`; official
+`web-console-1.6.183.tar.gz` SHA-256 is
+`8540758ad4521f901f53fc79473570cf98862add2addcb49271d8dbf125b4217`.
+
+These components use clean merged source and the original component packagers;
+the official versioned release assets and checksums were independently read back.
+They are not CI-produced component artifacts. The Server image is a separate
+normal publication CI output and is still pending.
+
+## Publication prerequisites
+
+The formal recipe, builder defaults and coordinate assertions must use the
+released component identities above and the verified Engine schema hashes.
+An uncommitted local build checksum or integration image is not an official
+release identity.
+
+The normal Server publication workflow still requires a clean exact source,
+source gates, formal image build and flatten comparison, isolated startup and
+restart, proxy/cache contracts, native MFA/API smoke, final merged-rootfs
+security and SBOM gates, provenance, immutable numeric publication, and release
+asset readback. Existing vulnerability decisions and review deadlines remain;
+the new version is not permission to widen or reset them.
+
+## Native acceptance boundary
+
+Eleven focused native-schema and return-history component cases and the native
+add-host UI checks passed on the component source/tree used for the released
+Web bundle. They validate the component changes only, not a final Server image.
+
+The final Server's seven-check native registration gate remains pending. It must
+verify native token creation and readiness, generated script retrieval,
+`GET /v1/register`, the restricted registration schema, ordinary API Key Basic
+authentication and invalid-token rejection even with a valid cookie.
+Successful HTTP entry alone does not prove an agent connection or active node.
+The real agent lifecycle and active-node acceptance also remain pending.
+
+## Install, upgrade and rollback
+
+After publication and readback, use the numeric image tag
+`ghcr.io/pasturestack/server:v1.6.522` shown in the README. Keep all three existing
+volumes, environment variables, networking, restart policy and security options.
+The intended changes do not add a database migration or authentication setting.
+Rollback to `v1.6.521` restores the known registration-token authentication defect;
+keep a matching database backup and follow the existing API Key rollback guide.
diff --git a/scripts/check-server-api-explorer-patch.sh b/scripts/check-server-api-explorer-patch.sh
index a6c5bd0510..2bc067e24d 100755
--- a/scripts/check-server-api-explorer-patch.sh
+++ b/scripts/check-server-api-explorer-patch.sh
@@ -323,6 +323,7 @@ check_server497_published_docs
# Install examples must follow the highest actually published numeric release;
# Install commands use numeric tags; digest identity stays in publication evidence.
+# An explicitly guarded522 source candidate may show numeric examples, not claim publication.
python3 - <<'PY'
import pathlib
import re
@@ -332,7 +333,11 @@ install_pattern = r'ghcr\.io/pasturestack/server:v[0-9]+\.[0-9]+\.[0-9]+'
image_pattern = install_pattern + r'@sha256:[0-9a-f]{64}'
reference_pattern = r'''ghcr\.io/pasturestack/server:[^\s`"'<>()\[\]{},;]+'''
current = re.search(r'(?ms)^## Current release\n(.*?)(?=^## |\Z)', readme)
-tags = re.findall(r'https://github\.com/PastureStack/server/releases/tag/(v[0-9]+\.[0-9]+\.[0-9]+)(?=[)\s])', current[1] if current else '')
+candidate = re.search(r'(?ms)^## Current source candidate\n(.*?)(?=^## |\Z)', readme)
+if bool(current) == bool(candidate):
+ raise SystemExit('SERVER_LATEST_PUBLISHED_IDENTITY_MISSING')
+context = current if current else candidate
+tags = re.findall(r'https://github\.com/PastureStack/server/releases/tag/(v[0-9]+\.[0-9]+\.[0-9]+)(?=[)\s])', context[1])
if len(tags) != 1:
raise SystemExit('SERVER_LATEST_PUBLISHED_IDENTITY_MISSING')
tag = tags[0]
@@ -342,12 +347,34 @@ if len(references) != 2 or references[0] != references[1] or not re.fullmatch(in
raise SystemExit('SERVER_LATEST_PUBLISHED_QUICK_START_MISMATCH')
image = references[0]
notes_path = 'docs/releases/server-' + tag[1:] + '.md'
-if image != 'ghcr.io/pasturestack/server:' + tag or '(' + notes_path + ')' not in current[1]:
+expected_install_tag = 'v1.6.522' if candidate else tag
+if image != 'ghcr.io/pasturestack/server:' + expected_install_tag or '(' + notes_path + ')' not in context[1]:
raise SystemExit('SERVER_LATEST_PUBLISHED_QUICK_START_MISMATCH')
performance = pathlib.Path('docs/performance/README.md').read_text(encoding='utf-8')
performance_references = re.findall(r'ghcr\.io/pasturestack/server:[^\s`"<>]+', performance)
if performance_references != [image]:
raise SystemExit('SERVER_LATEST_PUBLISHED_PERFORMANCE_MISMATCH')
+if candidate:
+ candidate_path = 'docs/releases/server-1.6.522.md'
+ candidate_notes = pathlib.Path(candidate_path).read_text(encoding='utf-8')
+ required = (
+ 'Server `v1.6.522` is a prepared source candidate, not a published image.',
+ '(' + candidate_path + ')',
+ )
+ if '' not in readme or \
+ any(marker not in context[1] for marker in required) or not all(marker in quick[1] for marker in (
+ 'run them only after its official release',
+ 'This candidate is not yet available')):
+ raise SystemExit('SERVER_SOURCE_CANDIDATE_DISCLOSURE_MISSING')
+ normalized_performance = ' '.join(performance.split())
+ if not all(marker in normalized_performance for marker in (
+ 'run it only after its official release and image have been published and read back.',
+ 'is not yet available from GHCR',
+ '(../releases/server-1.6.522.md)')):
+ raise SystemExit('SERVER_SOURCE_CANDIDATE_PERFORMANCE_DISCLOSURE_MISSING')
+ if not re.search(r'(?m)^# Server v1\.6\.522 candidate[ \t]*$', candidate_notes) or \
+ 'Prepared source only; no Server image or release has been published.' not in candidate_notes:
+ raise SystemExit('SERVER_SOURCE_CANDIDATE_NOTES_MISMATCH')
notes = pathlib.Path(notes_path).read_text(encoding='utf-8')
compat = pathlib.Path('COMPATIBILITY.md').read_text(encoding='utf-8')
published = [reference for reference in re.findall(reference_pattern, compat)
@@ -356,6 +383,11 @@ if not published:
raise SystemExit('SERVER_LATEST_PUBLISHED_IDENTITY_MISSING')
latest_tag = max((reference.split('@')[0].rsplit(':', 1)[1] for reference in published),
key=lambda value: tuple(map(int, value[1:].split('.'))))
+if candidate:
+ if tuple(map(int, expected_install_tag[1:].split('.'))) <= tuple(map(int, latest_tag[1:].split('.'))) or \
+ '## Server v1.6.522 prepared, not published' not in compat or \
+ 'ghcr.io/pasturestack/server:v1.6.522@' in readme + performance + compat + candidate_notes:
+ raise SystemExit('SERVER_SOURCE_CANDIDATE_PUBLICATION_MISMATCH')
if tag != latest_tag or not re.search(r'(?m)^# Server ' + re.escape(tag) + r'[ \t]*$', notes):
raise SystemExit('SERVER_LATEST_PUBLISHED_QUICK_START_MISMATCH')
publication_identity = None
@@ -368,7 +400,10 @@ for text in (notes, compat):
publication_identity = identities[0]
if any(reference != publication_identity for reference in identities):
raise SystemExit('SERVER_LATEST_PUBLISHED_QUICK_START_MISMATCH')
-print('SERVER_LATEST_PUBLISHED_QUICK_START_OK release=' + tag)
+if candidate:
+ print('SERVER_SOURCE_CANDIDATE_INSTALL_OK release=' + expected_install_tag + ' published=' + tag)
+else:
+ print('SERVER_LATEST_PUBLISHED_QUICK_START_OK release=' + tag)
PY
python3 scripts/test-published-install-gate.py
python3 scripts/test-current-server-packaging-contract.py
@@ -491,7 +526,7 @@ require_marker "$release_dockerfile" \
'ARG BASE_IMAGE=ghcr.io/pasturestack/server:v1.6.460@sha256:c855af8aea232dacc5bb6df68e2271d482c68b53c43ab0c108ec19118f5ab403' \
SERVER_INCREMENTAL_RELEASE_BASE_MISSING
require_marker "$release_dockerfile" \
- 'ARG SERVER_RELEASE_TAG=v1.6.519' \
+ 'ARG SERVER_RELEASE_TAG=v1.6.522' \
SERVER_INCREMENTAL_RELEASE_DEFAULT_MISSING
require_marker "$release_dockerfile" \
'org.opencontainers.image.version="${SERVER_RELEASE_TAG}"' \
@@ -532,16 +567,16 @@ require_marker "$publish_workflow" \
'bash source/server/artifacts/verify-host-api-package.sh' \
SERVER_HOST_API_PRODUCER_RELEASE_CHECK_MISSING
require_marker "$release_dockerfile" \
- 'ARG WEB_CONSOLE_RELEASE_TAG=1.6.181' \
+ 'ARG WEB_CONSOLE_RELEASE_TAG=1.6.183' \
SERVER_INCREMENTAL_WEB_CONSOLE_VERSION_MISSING
require_marker "$release_dockerfile" \
- 'ARG WEB_CONSOLE_ARTIFACT=web-console-1.6.181.tar.gz' \
+ 'ARG WEB_CONSOLE_ARTIFACT=web-console-1.6.183.tar.gz' \
SERVER_INCREMENTAL_WEB_CONSOLE_ARTIFACT_MISSING
require_marker "$release_dockerfile" \
- 'ARG WEB_CONSOLE_ARTIFACT_SHA256=9199681f2c4311f3fbe8727fcf0f6fa136c9bafe9fbe9d13878c55df442a7a03' \
+ 'ARG WEB_CONSOLE_ARTIFACT_SHA256=8540758ad4521f901f53fc79473570cf98862add2addcb49271d8dbf125b4217' \
SERVER_INCREMENTAL_WEB_CONSOLE_HASH_MISSING
require_marker "$release_dockerfile" \
- 'ARG WEB_CONSOLE_COMMIT=7f2bdad0c90a3241693c2ddd4475202ababba550' \
+ 'ARG WEB_CONSOLE_COMMIT=e056a0111d20816742eff17a65dfabb5d003dae2' \
SERVER_INCREMENTAL_WEB_CONSOLE_COMMIT_MISSING
require_marker "$release_dockerfile" \
"grep -aF 'hostsPage.permissionDenied'" \
@@ -550,16 +585,16 @@ require_marker "$release_dockerfile" \
'"hostsPage.permissionDenied":"您沒有權限在此環境中新增主機。"' \
SERVER_INCREMENTAL_WEB_CONSOLE_ZH_TW_PERMISSION_MESSAGE_MISSING
require_marker "$build_script" \
- 'web_console_commit=${WEB_CONSOLE_COMMIT:-7f2bdad0c90a3241693c2ddd4475202ababba550}' \
+ 'web_console_commit=${WEB_CONSOLE_COMMIT:-e056a0111d20816742eff17a65dfabb5d003dae2}' \
SERVER_INCREMENTAL_WEB_CONSOLE_BUILD_COMMIT_MISSING
require_marker "$build_script" \
- 'web_console_release_tag=${WEB_CONSOLE_RELEASE_TAG:-1.6.181}' \
+ 'web_console_release_tag=${WEB_CONSOLE_RELEASE_TAG:-1.6.183}' \
SERVER_INCREMENTAL_WEB_CONSOLE_BUILD_VERSION_MISSING
require_marker "$build_script" \
- 'web_console_artifact=${WEB_CONSOLE_ARTIFACT:-web-console-1.6.181.tar.gz}' \
+ 'web_console_artifact=${WEB_CONSOLE_ARTIFACT:-web-console-1.6.183.tar.gz}' \
SERVER_INCREMENTAL_WEB_CONSOLE_BUILD_ARTIFACT_MISSING
require_marker "$build_script" \
- 'web_console_artifact_sha256=${WEB_CONSOLE_ARTIFACT_SHA256:-9199681f2c4311f3fbe8727fcf0f6fa136c9bafe9fbe9d13878c55df442a7a03}' \
+ 'web_console_artifact_sha256=${WEB_CONSOLE_ARTIFACT_SHA256:-8540758ad4521f901f53fc79473570cf98862add2addcb49271d8dbf125b4217}' \
SERVER_INCREMENTAL_WEB_CONSOLE_BUILD_HASH_MISSING
require_marker "$release_dockerfile" \
'ARG CATALOG_SERVICE_VERSION=0.20.13' \
@@ -1024,21 +1059,21 @@ require_marker "$build_script" \
'--file server/Dockerfile.web-compose-release' \
SERVER_INCREMENTAL_RELEASE_BUILD_PATH_MISSING
require_marker "$build_script" \
- 'image=${IMAGE:-pasturestack-validation/server:v1.6.519}' \
+ 'image=${IMAGE:-pasturestack-validation/server:v1.6.522}' \
SERVER_INCREMENTAL_RELEASE_BUILD_VERSION_MISSING
require_marker "$build_script" \
- 'server_release_tag=${SERVER_RELEASE_TAG:-v1.6.519}' \
+ 'server_release_tag=${SERVER_RELEASE_TAG:-v1.6.522}' \
SERVER_INCREMENTAL_RELEASE_BUILD_DEFAULT_MISSING
require_marker "$build_script" \
'CATTLE_RANCHER_SERVER_VERSION="${server_release_tag}"' \
SERVER_INCREMENTAL_RELEASE_BUILD_RUNTIME_VERSION_MISSING
for release_engine_marker in \
- 'ARG ORCHESTRATION_ENGINE_RELEASE_TAG=v0.183.334' \
+ 'ARG ORCHESTRATION_ENGINE_RELEASE_TAG=v0.183.339' \
'ARG ORCHESTRATION_ENGINE_ARTIFACT=cattle.jar' \
- 'ARG ORCHESTRATION_ENGINE_ARTIFACT_SHA256=a62d8f6ddd61c850e71bbb6fd30352b3a73c7c09e605bb5c45d549074e8824e8' \
- 'ARG ORCHESTRATION_ENGINE_COMMIT=66a87ea6901296fa11ada5df58b0618a4ca96975' \
+ 'ARG ORCHESTRATION_ENGINE_ARTIFACT_SHA256=bce020a298ebb89ef3d14ba0666f040e4a732288d1b100da9859dcf957a7981d' \
+ 'ARG ORCHESTRATION_ENGINE_COMMIT=436a37215bc5702e516ecf1feb6753b4be5bf602' \
'COPY --from=release_artifacts /out/orchestration-engine.jar /tmp/orchestration-engine.jar' \
- 'ARG ORCHESTRATION_ENGINE_VERSION=0.183.334' \
+ 'ARG ORCHESTRATION_ENGINE_VERSION=0.183.339' \
'grep -Fx "Implementation-Version: ${ORCHESTRATION_ENGINE_VERSION}"' \
'cattle-resources-${ORCHESTRATION_ENGINE_VERSION}.jar' \
'cattle-app-config-${ORCHESTRATION_ENGINE_VERSION}.jar' \
@@ -1059,10 +1094,10 @@ for release_engine_marker in \
SERVER_INCREMENTAL_ENGINE_REPLACEMENT_MISSING
done
for release_engine_build_marker in \
- 'orchestration_engine_release_tag=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.334}' \
+ 'orchestration_engine_release_tag=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.339}' \
'orchestration_engine_artifact=${ORCHESTRATION_ENGINE_ARTIFACT:-cattle.jar}' \
- 'orchestration_engine_artifact_sha256=${ORCHESTRATION_ENGINE_ARTIFACT_SHA256:-a62d8f6ddd61c850e71bbb6fd30352b3a73c7c09e605bb5c45d549074e8824e8}' \
- 'orchestration_engine_commit=${ORCHESTRATION_ENGINE_COMMIT:-66a87ea6901296fa11ada5df58b0618a4ca96975}' \
+ 'orchestration_engine_artifact_sha256=${ORCHESTRATION_ENGINE_ARTIFACT_SHA256:-bce020a298ebb89ef3d14ba0666f040e4a732288d1b100da9859dcf957a7981d}' \
+ 'orchestration_engine_commit=${ORCHESTRATION_ENGINE_COMMIT:-436a37215bc5702e516ecf1feb6753b4be5bf602}' \
'CATTLE_CATTLE_VERSION="${orchestration_engine_release_tag}"' \
'cattle-resources-${ORCHESTRATION_ENGINE_VERSION}.jar' \
'test "${hazelcast_entry}" = "WEB-INF/lib/hazelcast-5.7.5.jar"'; do
@@ -1505,10 +1540,7 @@ for current_release_marker in \
SERVER_CURRENT_RELEASE_NOTES_IDENTITY_MISSING
done
for current_readme_marker in \
- '## Current release' \
'## Quick start' \
- 'Use the published numeric' \
- 'keep all three named volumes' \
'[performance settings](docs/performance/README.md)' \
'## Upgrade and rollback' \
'Back up the database and volumes, test a restore' \
@@ -1520,6 +1552,27 @@ for current_readme_marker in \
require_marker README.md "$current_readme_marker" \
SERVER_CURRENT_README_IDENTITY_MISSING
done
+if ! grep -Fqi 'keep all three named volumes' README.md; then
+ echo SERVER_CURRENT_README_VOLUME_CONTRACT_MISSING >&2
+ exit 1
+fi
+# The earlier install proof validates the actual candidate/published body and identities.
+if grep -Fxq '## Current source candidate' README.md; then
+ for candidate_readme_marker in \
+ '' \
+ 'Server `v1.6.522` is a prepared source candidate, not a published image.' \
+ 'run them only after its official release' \
+ 'This candidate is not yet available' \
+ 'ghcr.io/pasturestack/server:v1.6.522'; do
+ require_marker README.md "$candidate_readme_marker" \
+ SERVER_CURRENT_CANDIDATE_README_MISSING
+ done
+elif grep -Fxq '## Current release' README.md; then
+ require_marker README.md '## Current release' SERVER_CURRENT_PUBLISHED_README_MISSING
+else
+ echo SERVER_CURRENT_README_STATUS_MISSING >&2
+ exit 1
+fi
for historical_v481_marker in \
'# Server v1.6.481' \
'This release assembles Web Console `1.6.146`' \
@@ -2091,7 +2144,7 @@ done
jq -e '
.["@context"] == "https://openvex.dev/ns/v0.2.0"
- and .["@id"] == "https://github.com/PastureStack/server/security/openvex/v1.6.519"
+ and .["@id"] == "https://github.com/PastureStack/server/security/openvex/v1.6.522"
and (.statements | length) == 51
and ([.statements[].vulnerability.name] | length == (unique | length))
and ([.statements[] | select(.status == "fixed") | .vulnerability.name] | sort)
@@ -2117,7 +2170,7 @@ jq -r '
| @tsv
' "$runtime_vendor_pending" | LC_ALL=C sort -u >"$vendor_pending_fixture"
bash "$vendor_pending_validator" "$runtime_vendor_pending" \
- "$vendor_pending_fixture" v1.6.519 >/dev/null
+ "$vendor_pending_fixture" v1.6.522 >/dev/null
bash scripts/test-vendor-pending-findings.sh >/dev/null
rm -f "$vendor_pending_fixture"
trap - EXIT
@@ -2206,4 +2259,4 @@ for release_readback_contract in \
fi
done
-printf 'SERVER_API_EXPLORER_PATCH_OK release=v1.6.519 base=v1.6.460 engine=0.183.334 web_console=1.6.181 catalog_service=0.20.13 webhook_automation_service=0.10.4 authentication_service=0.4.43 curl=8.18.0-1ubuntu2.7 freemarker=2.3.35 artifact_scan=required vendor_pending=exact-set role_matrix=qa-required locale_layout=qa-required\n'
+printf 'SERVER_API_EXPLORER_PATCH_OK release=v1.6.522 base=v1.6.460 engine=0.183.339 web_console=1.6.183 catalog_service=0.20.13 webhook_automation_service=0.10.4 authentication_service=0.4.43 curl=8.18.0-1ubuntu2.7 freemarker=2.3.35 artifact_scan=required vendor_pending=exact-set role_matrix=qa-required locale_layout=qa-required\n'
diff --git a/scripts/test-current-server-packaging-contract.py b/scripts/test-current-server-packaging-contract.py
index 99671579d5..b26818701e 100644
--- a/scripts/test-current-server-packaging-contract.py
+++ b/scripts/test-current-server-packaging-contract.py
@@ -17,21 +17,21 @@
PUBLISHED_508 = 'Server `v1.6.508` 已正式發布,封裝 Web Console `1.6.171`。'
STALE_508_CANDIDATE = 'Server `v1.6.508` candidate packages Web Console `1.6.171`'
COMPATIBILITY_CODE = 'SERVER_CREATE_RESPONSE_ORDER_COMPATIBILITY_MISSING'
-WEB_SHA = '9199681f2c4311f3fbe8727fcf0f6fa136c9bafe9fbe9d13878c55df442a7a03'
-WEB_SOURCE = '7f2bdad0c90a3241693c2ddd4475202ababba550'
+WEB_SHA = '8540758ad4521f901f53fc79473570cf98862add2addcb49271d8dbf125b4217'
+WEB_SOURCE = 'e056a0111d20816742eff17a65dfabb5d003dae2'
CATALOG_COMMIT = 'b6b658888fce50d3ec217eb4eba0f26ab0113baf'
-ENGINE_SHA = 'a62d8f6ddd61c850e71bbb6fd30352b3a73c7c09e605bb5c45d549074e8824e8'
-ENGINE_SOURCE = '66a87ea6901296fa11ada5df58b0618a4ca96975'
+ENGINE_SHA = 'bce020a298ebb89ef3d14ba0666f040e4a732288d1b100da9859dcf957a7981d'
+ENGINE_SOURCE = '436a37215bc5702e516ecf1feb6753b4be5bf602'
OLD_COORDINATES = {
- 'v1.6.519': 'v1.6.518',
- '1.6.181': '1.6.180',
- WEB_SHA: 'a367bd6907281298a8e2bf0f3ad0444083db06062f3a1521db573cc5606d274e',
- WEB_SOURCE: '637604b38401b19d2c9ef73d5729d356bb80c8e6',
+ 'v1.6.522': 'v1.6.521',
+ '1.6.183': '1.6.181',
+ WEB_SHA: '9199681f2c4311f3fbe8727fcf0f6fa136c9bafe9fbe9d13878c55df442a7a03',
+ WEB_SOURCE: '7f2bdad0c90a3241693c2ddd4475202ababba550',
CATALOG_COMMIT: '7670ffd81d5f0b5570197fb03c7e55b46da45bf3',
}
-# Engine334 version stays pinned; its verified-frame WAR/source are current.
-# The previous518 Web180 values remain mutation controls.
-# Component coordinates do not establish Server519 artifact/runtime PASS.
+# Engine339 and Web183 use independently read-back official release identities.
+# Published521 component values remain mutation controls.
+# Component coordinates do not establish Server522 artifact/runtime PASS.
CATALOG_FIELDS = (
('VERSION', 'version', '0.20.13'),
('COMMIT', 'commit', '4c39c73a8131ba06e9ff0aaec3b95cc27e049324'),
@@ -41,25 +41,25 @@
('LICENSE_SHA256', 'license_sha256', '0d542e0c8804e39aa7f37eb00da5a762149dc682d7829451287e11b938e94594'),
)
LEGACY_ENGINE_COORDINATES = {
- '0.183.334': '0.183.333',
- ENGINE_SHA: '8c42c0982cbc2f4569fa265ad320b341551758cb4fc0bc6d79ba06d70e20d328',
- ENGINE_SOURCE: '0d94f7d879d314235e582a7f4062914a27b82709',
+ '0.183.339': '0.183.337',
+ ENGINE_SHA: '7e45189f2b57097efc7929c04ddf7767aebbe027d1b9f90023aa6711522c5c80',
+ ENGINE_SOURCE: 'ba041fe312dfe97fc1e5144af576ddcf1b573d8e',
}
EXPECTED = {
COMPATIBILITY_CODE: (COMPATIBILITY, PUBLISHED_508),
- 'SERVER_INCREMENTAL_RELEASE_DEFAULT_MISSING': (DOCKER, 'ARG SERVER_RELEASE_TAG=v1.6.519'),
+ 'SERVER_INCREMENTAL_RELEASE_DEFAULT_MISSING': (DOCKER, 'ARG SERVER_RELEASE_TAG=v1.6.522'),
'SERVER_INCREMENTAL_RELEASE_VERSION_MISSING': (DOCKER, 'org.opencontainers.image.version="${SERVER_RELEASE_TAG}"'),
'SERVER_INCREMENTAL_RELEASE_RUNTIME_VERSION_MISSING': (DOCKER, 'ENV CATTLE_RANCHER_SERVER_VERSION=${SERVER_RELEASE_TAG}'),
- 'SERVER_INCREMENTAL_WEB_CONSOLE_VERSION_MISSING': (DOCKER, 'ARG WEB_CONSOLE_RELEASE_TAG=1.6.181'),
- 'SERVER_INCREMENTAL_WEB_CONSOLE_ARTIFACT_MISSING': (DOCKER, 'ARG WEB_CONSOLE_ARTIFACT=web-console-1.6.181.tar.gz'),
+ 'SERVER_INCREMENTAL_WEB_CONSOLE_VERSION_MISSING': (DOCKER, 'ARG WEB_CONSOLE_RELEASE_TAG=1.6.183'),
+ 'SERVER_INCREMENTAL_WEB_CONSOLE_ARTIFACT_MISSING': (DOCKER, 'ARG WEB_CONSOLE_ARTIFACT=web-console-1.6.183.tar.gz'),
'SERVER_INCREMENTAL_WEB_CONSOLE_HASH_MISSING': (DOCKER, 'ARG WEB_CONSOLE_ARTIFACT_SHA256=' + WEB_SHA),
'SERVER_INCREMENTAL_WEB_CONSOLE_COMMIT_MISSING': (DOCKER, 'ARG WEB_CONSOLE_COMMIT=' + WEB_SOURCE),
'SERVER_INCREMENTAL_WEB_CONSOLE_BUILD_COMMIT_MISSING': (BUILD, 'web_console_commit=${WEB_CONSOLE_COMMIT:-' + WEB_SOURCE + '}'),
- 'SERVER_INCREMENTAL_WEB_CONSOLE_BUILD_VERSION_MISSING': (BUILD, 'web_console_release_tag=${WEB_CONSOLE_RELEASE_TAG:-1.6.181}'),
- 'SERVER_INCREMENTAL_WEB_CONSOLE_BUILD_ARTIFACT_MISSING': (BUILD, 'web_console_artifact=${WEB_CONSOLE_ARTIFACT:-web-console-1.6.181.tar.gz}'),
+ 'SERVER_INCREMENTAL_WEB_CONSOLE_BUILD_VERSION_MISSING': (BUILD, 'web_console_release_tag=${WEB_CONSOLE_RELEASE_TAG:-1.6.183}'),
+ 'SERVER_INCREMENTAL_WEB_CONSOLE_BUILD_ARTIFACT_MISSING': (BUILD, 'web_console_artifact=${WEB_CONSOLE_ARTIFACT:-web-console-1.6.183.tar.gz}'),
'SERVER_INCREMENTAL_WEB_CONSOLE_BUILD_HASH_MISSING': (BUILD, 'web_console_artifact_sha256=${WEB_CONSOLE_ARTIFACT_SHA256:-' + WEB_SHA + '}'),
- 'SERVER_INCREMENTAL_RELEASE_BUILD_VERSION_MISSING': (BUILD, 'image=${IMAGE:-pasturestack-validation/server:v1.6.519}'),
- 'SERVER_INCREMENTAL_RELEASE_BUILD_DEFAULT_MISSING': (BUILD, 'server_release_tag=${SERVER_RELEASE_TAG:-v1.6.519}'),
+ 'SERVER_INCREMENTAL_RELEASE_BUILD_VERSION_MISSING': (BUILD, 'image=${IMAGE:-pasturestack-validation/server:v1.6.522}'),
+ 'SERVER_INCREMENTAL_RELEASE_BUILD_DEFAULT_MISSING': (BUILD, 'server_release_tag=${SERVER_RELEASE_TAG:-v1.6.522}'),
'SERVER_INCREMENTAL_RELEASE_BUILD_RUNTIME_VERSION_MISSING': (BUILD, 'CATTLE_RANCHER_SERVER_VERSION="${server_release_tag}"'),
'SERVER_WEB_CONSOLE_RUNTIME_VERSION_GATE_MISSING': (BUILD, 'test "$(cat "${web_root}/VERSION.txt")" = "${WEB_CONSOLE_RELEASE_TAG}"'),
'SERVER_VOLUME_NATIVE_READ_ONLY_BUILD_GATE_MISSING': (DOCKER, 'grep -F \'"volume.isNative" : "r"\' >/dev/null'),
@@ -80,18 +80,18 @@
CREATE_RESPONSE_CODES = ['SERVER_WEB_CREATE_IDENTITY_BUILD_GATE_MISSING','SERVER_WEB_CANONICAL_RECORD_BUILD_GATE_MISSING','SERVER_WEB_CREATE_IDENTITY_RUNTIME_GATE_MISSING','SERVER_WEB_CANONICAL_RECORD_RUNTIME_GATE_MISSING']
ENGINE_EXPECTED = {
'SERVER_INCREMENTAL_ENGINE_REPLACEMENT_MISSING': (DOCKER, 'release_engine_marker', (
- 'ARG ORCHESTRATION_ENGINE_RELEASE_TAG=v0.183.334',
+ 'ARG ORCHESTRATION_ENGINE_RELEASE_TAG=v0.183.339',
'ARG ORCHESTRATION_ENGINE_ARTIFACT=cattle.jar',
'ARG ORCHESTRATION_ENGINE_ARTIFACT_SHA256=' + ENGINE_SHA,
'ARG ORCHESTRATION_ENGINE_COMMIT=' + ENGINE_SOURCE,
- 'ARG ORCHESTRATION_ENGINE_VERSION=0.183.334',
+ 'ARG ORCHESTRATION_ENGINE_VERSION=0.183.339',
'grep -Fx "Implementation-Version: ${ORCHESTRATION_ENGINE_VERSION}"',
'cattle-resources-${ORCHESTRATION_ENGINE_VERSION}.jar',
'cattle-app-config-${ORCHESTRATION_ENGINE_VERSION}.jar',
'ENV CATTLE_CATTLE_VERSION=${ORCHESTRATION_ENGINE_RELEASE_TAG}',
)),
'SERVER_INCREMENTAL_ENGINE_BUILD_COORDINATE_MISSING': (BUILD, 'release_engine_build_marker', (
- 'orchestration_engine_release_tag=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.334}',
+ 'orchestration_engine_release_tag=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.339}',
'orchestration_engine_artifact=${ORCHESTRATION_ENGINE_ARTIFACT:-cattle.jar}',
'orchestration_engine_artifact_sha256=${ORCHESTRATION_ENGINE_ARTIFACT_SHA256:-' + ENGINE_SHA + '}',
'orchestration_engine_commit=${ORCHESTRATION_ENGINE_COMMIT:-' + ENGINE_SOURCE + '}',
@@ -470,15 +470,15 @@ def verify(files, gate=GATE):
pipeline = r'(?:^|[;\n])\s*' + re.escape(marker) + r'(?=\s*(?:;|\n|$))'
if len(re.findall(pipeline, source)) != 1:
raise AssertionError('CURRENT_CATALOG_HASH_BYPASS')
- if 'and .["@id"] == "https://github.com/PastureStack/server/security/openvex/v1.6.519"' not in gate:
+ if 'and .["@id"] == "https://github.com/PastureStack/server/security/openvex/v1.6.522"' not in gate:
raise AssertionError('CURRENT_VEX_GATE_PIN_MISMATCH')
- if '"$vendor_pending_fixture" v1.6.519 >/dev/null' not in gate:
+ if '"$vendor_pending_fixture" v1.6.522 >/dev/null' not in gate:
raise AssertionError('CURRENT_VENDOR_GATE_PIN_MISMATCH')
- if json.loads(files[VEX]).get('@id') != 'https://github.com/PastureStack/server/security/openvex/v1.6.519':
+ if json.loads(files[VEX]).get('@id') != 'https://github.com/PastureStack/server/security/openvex/v1.6.522':
raise AssertionError('CURRENT_VEX_RELEASE_MISMATCH')
- if json.loads(files[VENDOR]).get('release') != 'v1.6.519':
+ if json.loads(files[VENDOR]).get('release') != 'v1.6.522':
raise AssertionError('CURRENT_VENDOR_RELEASE_MISMATCH')
- if 'SERVER_API_EXPLORER_PATCH_OK release=v1.6.519 base=v1.6.460 engine=0.183.334 web_console=1.6.181 catalog_service=0.20.13 ' not in gate:
+ if 'SERVER_API_EXPLORER_PATCH_OK release=v1.6.522 base=v1.6.460 engine=0.183.339 web_console=1.6.183 catalog_service=0.20.13 ' not in gate:
raise AssertionError('CURRENT_SUMMARY_MISMATCH')
verify_incremental_contract(files, gate)
@@ -492,18 +492,18 @@ def previous_gate(gate=GATE):
if stale(marker) != marker:
replacements["'" + marker + "' "] = "'" + stale(marker) + "' "
replacements.update({
- 'and .["@id"] == "https://github.com/PastureStack/server/security/openvex/v1.6.519"':
- 'and .["@id"] == "https://github.com/PastureStack/server/security/openvex/v1.6.518"',
- '"$vendor_pending_fixture" v1.6.519 >/dev/null':
- '"$vendor_pending_fixture" v1.6.518 >/dev/null',
- 'SERVER_API_EXPLORER_PATCH_OK release=v1.6.519 base=v1.6.460 engine=0.183.334 web_console=1.6.181 catalog_service=0.20.13 ':
- 'SERVER_API_EXPLORER_PATCH_OK release=v1.6.518 base=v1.6.460 engine=0.183.334 web_console=1.6.180 catalog_service=0.20.12 ',
+ 'and .["@id"] == "https://github.com/PastureStack/server/security/openvex/v1.6.522"':
+ 'and .["@id"] == "https://github.com/PastureStack/server/security/openvex/v1.6.521"',
+ '"$vendor_pending_fixture" v1.6.522 >/dev/null':
+ '"$vendor_pending_fixture" v1.6.521 >/dev/null',
+ 'SERVER_API_EXPLORER_PATCH_OK release=v1.6.522 base=v1.6.460 engine=0.183.339 web_console=1.6.183 catalog_service=0.20.13 ':
+ 'SERVER_API_EXPLORER_PATCH_OK release=v1.6.521 base=v1.6.460 engine=0.183.339 web_console=1.6.181 catalog_service=0.20.12 ',
})
for marker, stale_marker in replacements.items():
if gate.count(marker) != 1:
raise AssertionError('UNEXPECTED_CURRENT_MARKER_SHAPE')
gate = gate.replace(marker, stale_marker)
- # Synthetic mutation leaves current engine pins; separate tests reject333.
+ # Synthetic mutation leaves current engine pins; separate tests reject337.
return gate
@@ -1043,7 +1043,7 @@ def test_govc_fixed_dependency_and_binary_readback_use_formal_pins(self):
def test_current_readme_keeps_install_and_upgrade_contract(self):
block = GATE.split('for current_readme_marker in ', 1)[1].split('; do', 1)[0]
markers = shlex.split(block.replace('\\\n', ' '))
- for marker in ('## Current release', '## Quick start', '## Upgrade and rollback',
+ for marker in ('## Quick start', '## Upgrade and rollback',
'[upgrade guide](docs/upgrades/README.md)',
'Do not replace existing volumes with new empty ones or run `docker compose down -v`.',
'rollback may require restoring matching data', '[release notes](docs/releases)'):
@@ -1052,6 +1052,20 @@ def test_current_readme_keeps_install_and_upgrade_contract(self):
for marker in markers:
self.assertIn(marker, readme)
+ def test_current_readme_status_specific_prose_does_not_block_publication_transition(self):
+ common = GATE.split('for current_readme_marker in ', 1)[1].split('; do', 1)[0]
+ self.assertNotIn('candidate', common)
+ self.assertNotIn('v1.6.522', common)
+ self.assertIn("grep -Fqi 'keep all three named volumes' README.md", GATE)
+ status = GATE.split("if grep -Fxq '## Current source candidate' README.md; then", 1)[1]
+ candidate, published = status.split("elif grep -Fxq '## Current release' README.md; then", 1)
+ published = published.split('else\n', 1)[0]
+ self.assertIn('run them only after its official release', candidate)
+ self.assertIn('This candidate is not yet available', candidate)
+ self.assertIn('SERVER_CURRENT_PUBLISHED_README_MISSING', published)
+ self.assertNotIn('candidate', published)
+ self.assertNotIn('v1.6.522', published)
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/scripts/test-mfa-policy-api.py b/scripts/test-mfa-policy-api.py
index 9f33a43272..5d71c6fbf5 100644
--- a/scripts/test-mfa-policy-api.py
+++ b/scripts/test-mfa-policy-api.py
@@ -10,7 +10,9 @@
import hashlib
import hmac
import http.client
+import importlib.util
import json
+from pathlib import Path
import secrets
import struct
import time
@@ -21,6 +23,8 @@ def main():
parser.add_argument('--port', required=True, type=int)
parser.add_argument('--disposable', action='store_true', required=True)
parser.add_argument('--api-version', choices=('v1', 'v2-beta'), default='v2-beta')
+ parser.add_argument('--registration-basic', action='store_true',
+ help='Run native registration entry checks using this same fresh administrator session.')
args = parser.parse_args()
assert 1 <= args.port <= 65535
# Fixed loopback authority: callers cannot supply a remote URL, and the
@@ -94,6 +98,16 @@ def verify_session_bound_token(api_root, issued_token, generation, label):
call('POST', '/v1/localauthconfigs', local)
check('authenticated-administrator', call('GET', api + '/accounts').get('type') == 'collection')
+ if args.registration_basic:
+ # Share only RAM session material; do not enable local auth a second time
+ # or create another bootstrap administrator. Run before MFA enrollment.
+ spec = importlib.util.spec_from_file_location('registration_basic_smoke',
+ Path(__file__).with_name('test-registration-basic-api.py'))
+ registration = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(registration)
+ registration.run(port=args.port, administrator_jwt=token,
+ administrator_account_id=login['accountId'], disposable=args.disposable)
+
# The token resource intentionally has no public schema entry. Its
# clientSessionId contract is therefore verified below through observable
# issue/delete semantics: a mismatched generation must preserve the token,
diff --git a/scripts/test-published-install-gate.py b/scripts/test-published-install-gate.py
index d3a62ee1db..dbda06b9a5 100644
--- a/scripts/test-published-install-gate.py
+++ b/scripts/test-published-install-gate.py
@@ -20,6 +20,25 @@
'COMPATIBILITY.md': 'Server v1.6.499 preparing; no published digest.\nPublished Server v1.6.498\n' + PUBLISHED_IMAGE,
'docs/releases/server-1.6.498.md': '# Server v1.6.498\n\nOfficially published: ' + PUBLISHED_IMAGE,
}
+CANDIDATE_IMAGE = 'ghcr.io/pasturestack/server:v1.6.522'
+CANDIDATE_FILES = dict(FILES)
+intro, quick = FILES['README.md'].split('## Quick start', 1)
+CANDIDATE_FILES['README.md'] = (
+ '\n\n' +
+ intro.replace('## Current release', '## Current source candidate') +
+ 'Server `v1.6.522` is a prepared source candidate, not a published image.\n'
+ '[candidate note](docs/releases/server-1.6.522.md)\n\n## Quick start\n\n'
+ 'run them only after its official release\nThis candidate is not yet available\n' +
+ quick.replace(INSTALL_IMAGE, CANDIDATE_IMAGE))
+CANDIDATE_FILES[PERFORMANCE] = (
+ 'run it only after its official release and image have been published and read back.\n'
+ 'is not yet available from GHCR\n[candidate note](../releases/server-1.6.522.md)\n' +
+ FILES[PERFORMANCE].replace(INSTALL_IMAGE, CANDIDATE_IMAGE))
+CANDIDATE_FILES['COMPATIBILITY.md'] = (
+ '## Server v1.6.522 prepared, not published\n\n' + FILES['COMPATIBILITY.md'])
+CANDIDATE_FILES['docs/releases/server-1.6.522.md'] = (
+ '# Server v1.6.522 candidate\n\n'
+ 'Prepared source only; no Server image or release has been published.\n')
def verify(files):
@@ -35,12 +54,52 @@ def test_checked_out_install_documents_match_actual_latest_publication(self):
tags = set(re.findall(r'https://github\.com/PastureStack/server/releases/tag/(v[0-9]+\.[0-9]+\.[0-9]+)', readme))
files = {'README.md': readme, 'COMPATIBILITY.md': (REPO / 'COMPATIBILITY.md').read_text(encoding='utf-8'),
PERFORMANCE: (REPO / PERFORMANCE).read_text(encoding='utf-8')}
- for tag in tags:
- notes = REPO / ('docs/releases/server-' + tag[1:] + '.md')
+ note_paths = {'docs/releases/server-' + tag[1:] + '.md' for tag in tags}
+ note_paths.update(re.findall(r'\((docs/releases/server-[0-9.]+\.md)\)', readme))
+ for name in note_paths:
+ notes = REPO / name
if notes.is_file():
- files['docs/releases/server-' + tag[1:] + '.md'] = notes.read_text(encoding='utf-8')
+ files[name] = notes.read_text(encoding='utf-8')
verify(files)
+ def test_explicit_522_candidate_keeps_publication_evidence_and_numeric_examples(self):
+ verify(CANDIDATE_FILES)
+ files = dict(CANDIDATE_FILES)
+ files['COMPATIBILITY.md'] = files['COMPATIBILITY.md'].replace(PUBLISHED_IMAGE, '')
+ with self.assertRaisesRegex(SystemExit, 'IDENTITY_MISSING'):
+ verify(files)
+
+ def test_candidate_cannot_hide_warning_mismatch_or_fabricate_publication(self):
+ for name, old, new, error in (
+ ('README.md', '', '', 'DISCLOSURE_MISSING'),
+ ('README.md', 'run them only after its official release', '', 'DISCLOSURE_MISSING'),
+ (PERFORMANCE, 'is not yet available from GHCR', '', 'DISCLOSURE_MISSING'),
+ (PERFORMANCE, CANDIDATE_IMAGE, INSTALL_IMAGE, 'PERFORMANCE_MISMATCH'),
+ ('docs/releases/server-1.6.522.md', '# Server v1.6.522 candidate', '# Server v1.6.522', 'NOTES_MISMATCH'),
+ ('README.md', 'Server `v1.6.522` is a prepared source candidate, not a published image.',
+ 'Server `v1.6.522` is a prepared source candidate, not a published image.\n' +
+ CANDIDATE_IMAGE + '@sha256:' + 'a' * 64, 'PUBLICATION_MISMATCH'),
+ ('COMPATIBILITY.md', '## Server v1.6.522 prepared, not published',
+ '## Server v1.6.522 published\n' + CANDIDATE_IMAGE + '@sha256:' + 'a' * 64,
+ 'PUBLICATION_MISMATCH')):
+ with self.subTest(name=name, old=old):
+ files = dict(CANDIDATE_FILES)
+ files[name] = files[name].replace(old, new)
+ with self.assertRaisesRegex(SystemExit, error):
+ verify(files)
+
+ def test_actual_522_publication_needs_correct_docs_not_a_gate_code_change(self):
+ files = dict(FILES)
+ files['README.md'] = files['README.md'].replace('v1.6.498', 'v1.6.522').replace('server-1.6.498.md', 'server-1.6.522.md')
+ files[PERFORMANCE] = files[PERFORMANCE].replace('v1.6.498', 'v1.6.522')
+ image = CANDIDATE_IMAGE + '@sha256:' + 'a' * 64
+ files['COMPATIBILITY.md'] += '\n' + image
+ files['docs/releases/server-1.6.522.md'] = '# Server v1.6.522\n\nOfficially published: ' + image
+ verify(files)
+ files['docs/releases/server-1.6.522.md'] = CANDIDATE_FILES['docs/releases/server-1.6.522.md']
+ with self.assertRaisesRegex(SystemExit, 'QUICK_START_MISMATCH'):
+ verify(files)
+
def test_current_release_and_quick_start_need_no_readme_history(self):
self.assertNotRegex(FILES['README.md'], r'(?m)^## v[0-9]')
verify(FILES)
diff --git a/scripts/test-registration-basic-api-unit.py b/scripts/test-registration-basic-api-unit.py
new file mode 100644
index 0000000000..35c2672903
--- /dev/null
+++ b/scripts/test-registration-basic-api-unit.py
@@ -0,0 +1,77 @@
+"""Focused pure guard/integration checks; no Server or old smoke execution."""
+import contextlib
+import importlib.util
+import io
+from pathlib import Path
+import unittest
+from unittest.mock import Mock, patch
+
+SCRIPT = Path(__file__).with_name('test-registration-basic-api.py')
+spec = importlib.util.spec_from_file_location('registration_tested', SCRIPT)
+r = importlib.util.module_from_spec(spec); spec.loader.exec_module(r)
+
+
+class RegistrationSmokeTests(unittest.TestCase):
+ def test_creatable_string_kind_without_options_is_native_legal(self):
+ self.assertTrue(r.ordinary_kind_creatable({'create': True, 'type': 'string'}))
+ self.assertFalse(r.ordinary_kind_creatable({'create': False, 'type': 'string'}))
+ self.assertFalse(r.ordinary_kind_creatable({'create': True, 'type': 'unknown'}))
+
+ def test_enum_or_explicit_options_must_allow_user(self):
+ for field in ({'create': True, 'type': 'enum'},
+ {'create': True, 'type': 'enum', 'options': []},
+ {'create': True, 'type': 'enum', 'options': ['admin', 'service']},
+ {'create': True, 'type': 'string', 'options': ['admin']},
+ {'create': True, 'type': 'string', 'options': 'user'}):
+ with self.subTest(field=field):
+ self.assertFalse(r.ordinary_kind_creatable(field))
+ for kind in ('string', 'enum'):
+ self.assertTrue(r.ordinary_kind_creatable({'create': True, 'type': kind, 'options': ['user', 'admin']}))
+
+ def test_schema_link_is_native_and_transport_cannot_target_external_or_existing_port(self):
+ path = '/v2-beta/projects/1a8/registrationtokens'
+ schema = {'id': 'registrationToken', 'collectionMethods': ['GET', 'POST'],
+ 'links': {'collection': r.ORIGIN + path}}
+ self.assertEqual(r.collection(schema, 'registrationToken', path), path)
+ self.assertNotIn('_collection', schema)
+ for link in ('https://company.example' + path, r.ORIGIN + path + '?secret=x'):
+ with self.assertRaises(r.SmokeFailure): r.native_path(link, path)
+ for port in (8080, 18193, 18194):
+ with patch.object(r.http.client, 'HTTPConnection', side_effect=AssertionError('network')):
+ with self.assertRaisesRegex(r.SmokeFailure, 'LOOPBACK_PORT'): r.Client(port)
+ client = r.Client(r.PORT)
+ client.request = Mock(side_effect=[(404, b'{"code":"Not Found"}', (None, None)),
+ (200, b'{"id":"1a8","state":"active"}', ('1a7', '1a7'))])
+ with patch.object(r.time, 'sleep'):
+ self.assertEqual(client.active('/v2-beta/projects/1a8', 'Basic RAM', project=True)['id'], '1a8')
+ client.request = Mock(return_value=(403, b'{"code":"Forbidden"}', (None, None)))
+ with self.assertRaisesRegex(r.SmokeFailure, 'ACTIVE_HTTP_STATUS'):
+ client.active('/v2-beta/projects/1a8', 'Basic RAM', project=True)
+
+ def test_native_post_is_once_even_if_first_response_fails(self):
+ client = r.Client(r.PORT)
+ client.request = Mock(return_value=(503, b'{}', (None, None)))
+ for _ in range(2):
+ with self.assertRaises(r.SmokeFailure):
+ client.post('/v2-beta/accounts', {'kind': 'user', 'name': 'fresh'}, 'Bearer RAM')
+ self.assertEqual(client.request.call_count, 1)
+
+ def test_failure_cannot_echo_secret_and_workflow_reuses_existing_bootstrap(self):
+ secret = 'RAM-secret-marker-1234567890'
+ output = io.StringIO()
+ with patch.object(r.Client, 'request', side_effect=RuntimeError(secret)), contextlib.redirect_stdout(output):
+ with self.assertRaises(r.SmokeFailure) as failure:
+ r.run(port=r.PORT, administrator_jwt=secret, administrator_account_id='1a1', disposable=True)
+ self.assertNotIn(secret, str(failure.exception)); self.assertNotIn(secret, output.getvalue())
+ mfa = SCRIPT.with_name('test-mfa-policy-api.py').read_text()
+ self.assertLess(mfa.index("check('authenticated-administrator'"), mfa.index('registration.run('))
+ self.assertLess(mfa.index('registration.run('), mfa.index('recovery_codes = enroll()'))
+ self.assertNotIn('/localauthconfigs', SCRIPT.read_text())
+ workflow = SCRIPT.parents[1].joinpath('.github/workflows/publish-current-server.yml').read_text()
+ self.assertIn('--port 18082 --disposable --registration-basic', workflow)
+ self.assertEqual(workflow.count('python3 source/scripts/test-mfa-policy-api.py'), 1)
+ self.assertEqual(workflow.count('registration-basic-api-smoke.txt'), 3)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/scripts/test-registration-basic-api.py b/scripts/test-registration-basic-api.py
new file mode 100644
index 0000000000..2fdc8e002e
--- /dev/null
+++ b/scripts/test-registration-basic-api.py
@@ -0,0 +1,223 @@
+#!/usr/bin/env python3
+"""Focused native registration entry smoke sharing the disposable MFA bootstrap.
+
+Call run() with its already-issued administrator session, before MFA enrollment.
+No auth bootstrap, SQL, agent launch, redirects, secret files or response logs.
+The publication workflow owns the candidate container and all three volumes.
+"""
+import base64
+import hashlib
+import http.client
+import json
+import re
+import secrets
+import time
+from urllib.parse import urlsplit
+
+PORT = 18082
+ORIGIN = 'https://stack.example.test'
+ORIGINS = frozenset((ORIGIN, 'http://127.0.0.1:18082'))
+BOUND = 2 * 1024 * 1024
+
+
+class SmokeFailure(RuntimeError):
+ pass
+
+
+def require(condition, code):
+ if not condition:
+ raise SmokeFailure('REGISTRATION_BASIC_API_' + code)
+
+
+def native_path(link, expected):
+ require(isinstance(link, str) and len(link) <= 8192, 'NATIVE_LINK')
+ parsed = urlsplit(link)
+ authority = parsed.scheme + '://' + parsed.netloc
+ require((not parsed.scheme and not parsed.netloc or authority in ORIGINS)
+ and not parsed.query and not parsed.fragment and '\\' not in link
+ and '..' not in parsed.path and parsed.path == expected, 'OWNED_NATIVE_LINK')
+ return parsed.path
+
+
+def collection(schema, kind, path):
+ require(schema.get('id') == kind and 'POST' in schema.get('collectionMethods', []), 'NATIVE_POST_SCHEMA')
+ return native_path(schema.get('links', {}).get('collection'), path)
+
+
+def ordinary_kind_creatable(field):
+ if not isinstance(field, dict) or field.get('create') is not True:
+ return False
+ options = field.get('options')
+ if options is None:
+ return field.get('type') == 'string'
+ return field.get('type') in ('string', 'enum') and isinstance(options, list) and 'user' in options
+
+
+def private_absent(raw, value, private):
+ require(not any(secret.encode() in raw for secret in private if secret), 'PRIVATE_RESPONSE')
+ def walk(node):
+ if isinstance(node, str):
+ require(not any(secret in node for secret in private if secret), 'PRIVATE_RESPONSE')
+ elif isinstance(node, dict):
+ for key, item in node.items(): walk(key); walk(item)
+ elif isinstance(node, list):
+ for item in node: walk(item)
+ walk(value)
+
+
+class Client:
+ def __init__(self, port):
+ require(type(port) is int and port == PORT, 'DISPOSABLE_LOOPBACK_PORT')
+ self.port, self.private, self.used_posts = port, [], set()
+ self.post_paths = {'/v2-beta/accounts', '/v2-beta/apikeys', '/v2-beta/projects'}
+
+ def request(self, method, path, data=None, auth=None, cookie=None):
+ parsed = urlsplit(path)
+ require(method in ('GET', 'POST') and path.startswith(('/v1/', '/v2-beta/'))
+ and not parsed.scheme and not parsed.netloc and not parsed.query and not parsed.fragment
+ and '..' not in path and '\\' not in path, 'OWNED_REQUEST')
+ headers = {'Accept': 'application/json', 'Content-Type': 'application/json',
+ 'Host': urlsplit(ORIGIN).netloc, 'Origin': ORIGIN}
+ if auth: headers['Authorization'] = auth
+ if cookie: headers['Cookie'] = cookie
+ connection = http.client.HTTPConnection('127.0.0.1', port=self.port, timeout=20)
+ try:
+ connection.request(method, path, headers=headers,
+ body=None if data is None else json.dumps(data).encode())
+ response = connection.getresponse()
+ raw = response.read(BOUND + 1); require(len(raw) <= BOUND, 'RESPONSE_BOUND')
+ principal = (response.getheader('X-API-USER-ID'), response.getheader('X-API-ACCOUNT-ID'))
+ return response.status, raw, principal
+ finally:
+ connection.close()
+
+ def json(self, method, path, data=None, auth=None, cookie=None, expected=200):
+ status, raw, principal = self.request(method, path, data, auth, cookie)
+ require(status == expected, 'HTTP_STATUS')
+ value = json.loads(raw); require(isinstance(value, dict), 'JSON_OBJECT')
+ return value, raw, principal
+
+ def post(self, path, data, auth):
+ require(path in self.post_paths and path not in self.used_posts, 'NATIVE_POST_ONCE')
+ self.used_posts.add(path) # A failed request is never retried as a POST.
+ return self.json('POST', path, data, auth, expected=201)[0]
+
+ def active(self, path, auth, project=False):
+ require(not project or re.fullmatch(r'/v2-beta/projects/1a[1-9][0-9]*', path), 'PROJECT_ACTIVE_PATH')
+ deadline = time.monotonic() + 60
+ while True:
+ status, raw, _ = self.request('GET', path, auth=auth)
+ value = json.loads(raw); require(isinstance(value, dict), 'JSON_OBJECT')
+ # The ordinary creator's native membership must become visible;
+ # only this exact new-project Not Found can remain a waiting state.
+ waiting = project and status == 404 and value.get('code') == 'Not Found'
+ require(status == 200 or waiting, 'ACTIVE_HTTP_STATUS')
+ if status == 200 and value.get('state') == 'active':
+ require(not project or value.get('id') == path.rsplit('/', 1)[-1], 'PROJECT_ACTIVE_ID')
+ return value
+ require(time.monotonic() < deadline, 'NATIVE_ACTIVE_TIMEOUT')
+ time.sleep(.2)
+
+
+def run(*, port, administrator_jwt, administrator_account_id, disposable):
+ require(disposable is True, 'DISPOSABLE_REQUIRED')
+ require(isinstance(administrator_jwt, str) and len(administrator_jwt) >= 20
+ and '\n' not in administrator_jwt and '\r' not in administrator_jwt, 'SHARED_ADMIN_SESSION')
+ require(re.fullmatch(r'1a[1-9][0-9]*', administrator_account_id or ''), 'SHARED_ADMIN_ACCOUNT')
+ client = Client(port)
+ admin = 'Bearer ' + administrator_jwt
+ client.private.extend((administrator_jwt, admin))
+ try:
+ schemas = {kind: client.json('GET', '/v2-beta/schemas/' + kind, auth=admin)[0]
+ for kind in ('account', 'apiKey', 'project')}
+ account_path = collection(schemas['account'], 'account', '/v2-beta/accounts')
+ key_path = collection(schemas['apiKey'], 'apiKey', '/v2-beta/apikeys')
+ project_path = collection(schemas['project'], 'project', '/v2-beta/projects')
+ kind_field = schemas['account'].get('resourceFields', {}).get('kind', {})
+ require(ordinary_kind_creatable(kind_field)
+ and schemas['apiKey']['resourceFields']['accountId'].get('create') is True, 'ORDINARY_USER_SCHEMA')
+ name = 'registration-smoke-' + secrets.token_hex(8)
+ actor = client.post(account_path, {'kind': 'user', 'name': name}, admin)
+ actor_id = actor.get('id')
+ require(re.fullmatch(r'1a[1-9][0-9]*', actor_id or '') and actor_id != administrator_account_id
+ and actor.get('kind') == 'user' and actor.get('name') == name, 'ORDINARY_USER')
+ actor = client.active(account_path + '/' + actor_id, admin)
+ require(actor.get('kind') == 'user', 'ORDINARY_USER')
+ # Missing policy is a current native legacy/full control, not an upgrade claim.
+ key = client.post(key_path, {'type': 'apiKey', 'name': name + '-key', 'accountId': actor_id}, admin)
+ require(key.get('accountId') == actor_id and isinstance(key.get('publicValue'), str)
+ and isinstance(key.get('secretValue'), str) and len(key['secretValue']) >= 16, 'NATIVE_OWNER_KEY')
+ basic = 'Basic ' + base64.b64encode((key['publicValue'] + ':' + key['secretValue']).encode()).decode()
+ client.private.extend((key['publicValue'], key['secretValue'], basic))
+ key_id = key.get('id'); require(re.fullmatch(r'1c[1-9][0-9]*', key_id or ''), 'KEY_ID')
+ self_path = urlsplit(key.get('links', {}).get('self', '')).path
+ require(self_path in (key_path + '/' + key_id, '/v2-beta/apikeyrestricteds/' + key_id), 'KEY_SELF')
+ native_path(key['links']['self'], self_path)
+ ready = client.active(self_path, admin)
+ require(ready.get('accountId') == actor_id and type(ready.get('apiKeyPolicyRevision')) is int
+ and ready['apiKeyPolicyRevision'] == 0, 'LEGACY_KEY_OWNER')
+ own, raw, principal = client.json('GET', account_path + '/' + actor_id, auth=basic)
+ private_absent(raw, own, client.private)
+ require(own.get('kind') == 'user' and principal == (actor_id, actor_id), 'ORDINARY_BASIC_PRINCIPAL')
+ project = client.post(project_path, {'name': name + '-project'}, basic)
+ project_id = project.get('id'); require(re.fullmatch(r'1a[1-9][0-9]*', project_id or '')
+ and project_id not in (actor_id, administrator_account_id)
+ and project.get('kind') == 'project'
+ and project.get('name') == name + '-project', 'NATIVE_PROJECT')
+ client.active(project_path + '/' + project_id, basic, project=True)
+ own, raw, principal = client.json('GET', '/v1/projects/' + project_id, auth=basic)
+ private_absent(raw, own, client.private)
+ require(own.get('id') == project_id and principal == (actor_id, actor_id), 'LEGACY_BASIC_CONTROL')
+ prefix = '/v2-beta/projects/' + project_id
+ schema = client.json('GET', prefix + '/schemas/registrationToken', auth=basic)[0]
+ token_path = collection(schema, 'registrationToken', prefix + '/registrationtokens')
+ client.post_paths.add(token_path)
+ token = client.post(token_path, {'type': 'registrationToken'}, basic)
+ token_id = token.get('id'); require(re.fullmatch(r'1c[1-9][0-9]*', token_id or ''), 'TOKEN_ID')
+ token = client.active(token_path + '/' + token_id, basic)
+ secret = token.get('token')
+ require(token.get('accountId') == project_id and isinstance(secret, str)
+ and len(secret.split(':')) == 3, 'NATIVE_REGISTRATION_TOKEN')
+ url = token.get('registrationUrl'); require(isinstance(url, str), 'NATIVE_SCRIPT_URL')
+ script = urlsplit(url).path
+ require(script.startswith(prefix + '/scripts/') and len(script.rsplit('/', 1)[-1]) > 20, 'PROJECT_SCRIPT')
+ native_path(url, script)
+ registration = 'Basic ' + base64.b64encode(('registrationToken:' + secret).encode()).decode()
+ wrong = secret[:-1] + ('A' if secret[-1] != 'A' else 'B')
+ invalid = 'Basic ' + base64.b64encode(('registrationToken:' + wrong).encode()).decode()
+ cookie = 'token=' + administrator_jwt
+ client.private.extend((secret, url, script, registration, wrong, invalid, cookie))
+ status, body, _ = client.request('GET', script)
+ require(status == 200 and body.startswith(b'#!') and secret.encode() in body
+ and b'registrationToken' in body, 'GENERATED_SCRIPT')
+ script_hash = hashlib.sha256(body).hexdigest()
+ value, raw, principal = client.json('GET', '/v1/register', auth=registration)
+ private_absent(raw, value, client.private)
+ require(value.get('type') == 'collection' and value.get('resourceType') == 'register'
+ and isinstance(value.get('data'), list) and principal == (project_id, project_id), 'NATIVE_REGISTER_GET')
+ value, raw, principal = client.json('GET', '/v1/schemas/register', auth=registration)
+ private_absent(raw, value, client.private)
+ require(value.get('id') == 'register' and 'GET' in value.get('collectionMethods', [])
+ and principal == (project_id, project_id), 'RESTRICTED_REGISTER_SCHEMA')
+ value, raw, principal = client.json('GET', '/v1/accounts', auth=registration, expected=404)
+ private_absent(raw, value, client.private)
+ require(value.get('code') == 'Not Found' and principal == (project_id, project_id), 'REGISTRATION_CANNOT_ACCOUNTS')
+ value, raw, principal = client.json('GET', '/v2-beta/projects/' + project_id, cookie=cookie)
+ private_absent(raw, value, client.private)
+ require(value.get('id') == project_id and principal == (administrator_account_id, administrator_account_id), 'VALID_COOKIE_CONTROL')
+ value, raw, principal = client.json('GET', '/v1/register', auth=invalid, cookie=cookie, expected=401)
+ private_absent(raw, value, client.private)
+ require(value.get('code') == 'Unauthorized' and principal == (None, None), 'INVALID_TOKEN_NO_COOKIE_FALLTHROUGH')
+ print('REGISTRATION_BASIC_API_OK checks=7 ordinary_user=true entry_only=true node_active_verified=false script_sha256=' + script_hash,
+ flush=True)
+ return {'checks': 7, 'ordinaryUser': True, 'nodeActiveVerified': False, 'scriptSha256': script_hash}
+ except SmokeFailure:
+ raise
+ except Exception:
+ raise SmokeFailure('REGISTRATION_BASIC_API_FAILURE') from None
+ finally:
+ client.private.clear()
+
+
+if __name__ == '__main__':
+ print('PLAN_ONLY: shared disposable test-mfa-policy-api.py --registration-basic bootstrap required; no runtime executed.')
diff --git a/server/Dockerfile.web-compose-release b/server/Dockerfile.web-compose-release
index 04fa467dd0..4cb470da1d 100644
--- a/server/Dockerfile.web-compose-release
+++ b/server/Dockerfile.web-compose-release
@@ -55,9 +55,9 @@ ARG NODE_AGENT_BINARY_SHA256=ce8342e10da07c50cc414b471cadc67db4dd390e6e2dcb632b3
ARG NODE_AGENT_APPLY_SHA256=dd8cb342518a43e7468cc121db5c4e44b16731f2620b77ea0fd03c02c03766a9
ARG WEB_CONSOLE_RELEASE_BASE_URL=https://github.com/PastureStack/web-console/releases/download
-ARG WEB_CONSOLE_RELEASE_TAG=1.6.181
-ARG WEB_CONSOLE_ARTIFACT=web-console-1.6.181.tar.gz
-ARG WEB_CONSOLE_ARTIFACT_SHA256=9199681f2c4311f3fbe8727fcf0f6fa136c9bafe9fbe9d13878c55df442a7a03
+ARG WEB_CONSOLE_RELEASE_TAG=1.6.183
+ARG WEB_CONSOLE_ARTIFACT=web-console-1.6.183.tar.gz
+ARG WEB_CONSOLE_ARTIFACT_SHA256=8540758ad4521f901f53fc79473570cf98862add2addcb49271d8dbf125b4217
ARG CATALOG_SERVICE_RELEASE_BASE_URL=https://github.com/PastureStack/catalog-service/releases/download
ARG CATALOG_SERVICE_VERSION=0.20.13
ARG CATALOG_SERVICE_COMMIT=4c39c73a8131ba06e9ff0aaec3b95cc27e049324
@@ -79,9 +79,9 @@ ARG WEBHOOK_AUTOMATION_SERVICE_VERSION=0.10.4
ARG WEBHOOK_AUTOMATION_SERVICE_ARCHIVE_SHA256=49c4579829a04e758045fae02a5a9fca12bb0ba3af0d5e979cf9eb97f23a88a9
ARG WEBHOOK_AUTOMATION_SERVICE_BINARY_SHA256=98c7faea665b7eb95206b8c73a6f47d644c5d2d0eae53f274f6a15faf5205744
ARG ORCHESTRATION_ENGINE_RELEASE_BASE_URL=https://github.com/PastureStack/orchestration-engine/releases/download
-ARG ORCHESTRATION_ENGINE_RELEASE_TAG=v0.183.337
+ARG ORCHESTRATION_ENGINE_RELEASE_TAG=v0.183.339
ARG ORCHESTRATION_ENGINE_ARTIFACT=cattle.jar
-ARG ORCHESTRATION_ENGINE_ARTIFACT_SHA256=7e45189f2b57097efc7929c04ddf7767aebbe027d1b9f90023aa6711522c5c80
+ARG ORCHESTRATION_ENGINE_ARTIFACT_SHA256=bce020a298ebb89ef3d14ba0666f040e4a732288d1b100da9859dcf957a7981d
ARG COMPOSE_EXECUTOR_RELEASE_BASE_URL=https://github.com/PastureStack/compose-cli/releases/download
ARG COMPOSE_EXECUTOR_VERSION=0.14.37
ARG COMPOSE_EXECUTOR_ARCHIVE_SHA256=5ff465601930218f531885a384f231d969033e4cef794ce33d16bfe24e5c1c81
@@ -565,9 +565,9 @@ FROM ${BASE_IMAGE}
ARG SOURCE_DATE_EPOCH
ARG PASTURESTACK_SERVER_REVISION
-ARG SERVER_RELEASE_TAG=v1.6.521
-ARG ORCHESTRATION_ENGINE_RELEASE_TAG=v0.183.337
-ARG ORCHESTRATION_ENGINE_VERSION=0.183.337
+ARG SERVER_RELEASE_TAG=v1.6.522
+ARG ORCHESTRATION_ENGINE_RELEASE_TAG=v0.183.339
+ARG ORCHESTRATION_ENGINE_VERSION=0.183.339
ARG ENGINE_READONLY_SCHEMA_SHA256=7f274219e8dd9c6d750a408a6edec1448b564f4b16ba2204c425c5b45cec2233
ARG ENGINE_RESTRICTED_SCHEMA_SHA256=f854ba99260f29e324ab7446a8592996946038920ac9bcf17689d98d1ebc5e51
ARG HOST_API_VERSION=0.38.5
@@ -585,9 +585,9 @@ ARG FREETYPE_PACKAGE_VERSION=2.14.2+dfsg-1ubuntu0.2
ARG LIBPNG_PACKAGE_VERSION=1.6.57-1ubuntu0.1
ARG GLIBC_PACKAGE_VERSION=2.43-2ubuntu2.4
ARG PERL_PACKAGE_VERSION=5.40.1-7ubuntu0.3
-ARG WEB_CONSOLE_RELEASE_TAG=1.6.181
-ARG WEB_CONSOLE_ARTIFACT_SHA256=9199681f2c4311f3fbe8727fcf0f6fa136c9bafe9fbe9d13878c55df442a7a03
-ARG WEB_CONSOLE_COMMIT=7f2bdad0c90a3241693c2ddd4475202ababba550
+ARG WEB_CONSOLE_RELEASE_TAG=1.6.183
+ARG WEB_CONSOLE_ARTIFACT_SHA256=8540758ad4521f901f53fc79473570cf98862add2addcb49271d8dbf125b4217
+ARG WEB_CONSOLE_COMMIT=e056a0111d20816742eff17a65dfabb5d003dae2
ARG CATALOG_SERVICE_VERSION=0.20.13
ARG CATALOG_SERVICE_COMMIT=4c39c73a8131ba06e9ff0aaec3b95cc27e049324
ARG CATALOG_SERVICE_ARCHIVE_SHA256=29626181cb8489b016e5975ffaddc00b2a32d290b1c0066e833d27fa7a5edf83
@@ -606,8 +606,8 @@ ARG WEBHOOK_AUTOMATION_SERVICE_VERSION=0.10.4
ARG WEBHOOK_AUTOMATION_SERVICE_COMMIT=400118b893843d2a7d7c65cc70c3449d76c4a8d8
ARG WEBHOOK_AUTOMATION_SERVICE_ARCHIVE_SHA256=49c4579829a04e758045fae02a5a9fca12bb0ba3af0d5e979cf9eb97f23a88a9
ARG WEBHOOK_AUTOMATION_SERVICE_BINARY_SHA256=98c7faea665b7eb95206b8c73a6f47d644c5d2d0eae53f274f6a15faf5205744
-ARG ORCHESTRATION_ENGINE_ARTIFACT_SHA256=7e45189f2b57097efc7929c04ddf7767aebbe027d1b9f90023aa6711522c5c80
-ARG ORCHESTRATION_ENGINE_COMMIT=ba041fe312dfe97fc1e5144af576ddcf1b573d8e
+ARG ORCHESTRATION_ENGINE_ARTIFACT_SHA256=bce020a298ebb89ef3d14ba0666f040e4a732288d1b100da9859dcf957a7981d
+ARG ORCHESTRATION_ENGINE_COMMIT=436a37215bc5702e516ecf1feb6753b4be5bf602
ARG HOST_PROVISIONER_VERSION=0.39.8
ARG HOST_PROVISIONER_COMMIT=385e5b536c108f17fdfcdec84c01750a5be5ab1c
ARG HOST_PROVISIONER_ARCHIVE_SHA256=d775f36a613b1a486a5e60d6ad61fdbd1ebb4bd22422cbb6dcb70fdd7c4abf0c
diff --git a/server/build-api-explorer-patch-image.sh b/server/build-api-explorer-patch-image.sh
index 3e045369e8..5f15a37c0a 100755
--- a/server/build-api-explorer-patch-image.sh
+++ b/server/build-api-explorer-patch-image.sh
@@ -14,14 +14,14 @@ if [[ -n "$(git status --porcelain --untracked-files=normal)" ]]; then
fi
revision=${PASTURESTACK_SERVER_REVISION:-$(git rev-parse HEAD)}
-server_release_tag=${SERVER_RELEASE_TAG:-v1.6.521}
+server_release_tag=${SERVER_RELEASE_TAG:-v1.6.522}
source_date_epoch=${SOURCE_DATE_EPOCH:-$(git show -s --format=%ct HEAD)}
base_image=${BASE_IMAGE:-ghcr.io/pasturestack/server:v1.6.460@sha256:c855af8aea232dacc5bb6df68e2271d482c68b53c43ab0c108ec19118f5ab403}
orchestration_engine_release_base_url=${ORCHESTRATION_ENGINE_RELEASE_BASE_URL:-https://github.com/PastureStack/orchestration-engine/releases/download}
-orchestration_engine_release_tag=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.337}
+orchestration_engine_release_tag=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.339}
orchestration_engine_artifact=${ORCHESTRATION_ENGINE_ARTIFACT:-cattle.jar}
-orchestration_engine_artifact_sha256=${ORCHESTRATION_ENGINE_ARTIFACT_SHA256:-7e45189f2b57097efc7929c04ddf7767aebbe027d1b9f90023aa6711522c5c80}
-orchestration_engine_commit=${ORCHESTRATION_ENGINE_COMMIT:-ba041fe312dfe97fc1e5144af576ddcf1b573d8e}
+orchestration_engine_artifact_sha256=${ORCHESTRATION_ENGINE_ARTIFACT_SHA256:-bce020a298ebb89ef3d14ba0666f040e4a732288d1b100da9859dcf957a7981d}
+orchestration_engine_commit=${ORCHESTRATION_ENGINE_COMMIT:-436a37215bc5702e516ecf1feb6753b4be5bf602}
orchestration_engine_version=${orchestration_engine_release_tag#v}
engine_readonly_schema_sha256=${ENGINE_READONLY_SCHEMA_SHA256:-7f274219e8dd9c6d750a408a6edec1448b564f4b16ba2204c425c5b45cec2233}
engine_restricted_schema_sha256=${ENGINE_RESTRICTED_SCHEMA_SHA256:-f854ba99260f29e324ab7446a8592996946038920ac9bcf17689d98d1ebc5e51}
@@ -48,10 +48,10 @@ api_explorer_artifact=${API_EXPLORER_ARTIFACT:-api-explorer-1.1.18.tar.gz}
api_explorer_artifact_sha256=${API_EXPLORER_ARTIFACT_SHA256:-92b718c46163018ea40c008ac552911f0eb610647377725405f4046dcd411f2c}
api_explorer_commit=${API_EXPLORER_COMMIT:-3b1c39e8a116f58649d94233a384a0362c02b43e}
web_console_release_base_url=${WEB_CONSOLE_RELEASE_BASE_URL:-https://github.com/PastureStack/web-console/releases/download}
-web_console_release_tag=${WEB_CONSOLE_RELEASE_TAG:-1.6.181}
-web_console_artifact=${WEB_CONSOLE_ARTIFACT:-web-console-1.6.181.tar.gz}
-web_console_artifact_sha256=${WEB_CONSOLE_ARTIFACT_SHA256:-9199681f2c4311f3fbe8727fcf0f6fa136c9bafe9fbe9d13878c55df442a7a03}
-web_console_commit=${WEB_CONSOLE_COMMIT:-7f2bdad0c90a3241693c2ddd4475202ababba550}
+web_console_release_tag=${WEB_CONSOLE_RELEASE_TAG:-1.6.183}
+web_console_artifact=${WEB_CONSOLE_ARTIFACT:-web-console-1.6.183.tar.gz}
+web_console_artifact_sha256=${WEB_CONSOLE_ARTIFACT_SHA256:-8540758ad4521f901f53fc79473570cf98862add2addcb49271d8dbf125b4217}
+web_console_commit=${WEB_CONSOLE_COMMIT:-e056a0111d20816742eff17a65dfabb5d003dae2}
catalog_service_release_base_url=${CATALOG_SERVICE_RELEASE_BASE_URL:-https://github.com/PastureStack/catalog-service/releases/download}
catalog_service_version=${CATALOG_SERVICE_VERSION:-0.20.13}
catalog_service_commit=${CATALOG_SERVICE_COMMIT:-4c39c73a8131ba06e9ff0aaec3b95cc27e049324}
@@ -101,7 +101,7 @@ usage_telemetry_agent_archive_sha256=${USAGE_TELEMETRY_AGENT_ARCHIVE_SHA256:-5ce
usage_telemetry_agent_binary_sha256=${USAGE_TELEMETRY_AGENT_BINARY_SHA256:-e62a21270142181315293d7e11482288ffe8fc4d91cc8fe07dacc90b461f3709}
supported_docker_range='~v1.12.3 || ~v1.13.0 || ~v17.03.0 || ~v17.06.0 || ~v17.09.0 || ~v17.12.0 || ~v18.03.0 || ~v18.06.0 || ~v18.09.0 || ~v19.03.2 || v24.0.9 || >=v29.4.1 <=v29.7.2 || v29.8.0'
newest_docker_version=v29.8.0
-image=${IMAGE:-pasturestack-validation/server:v1.6.521}
+image=${IMAGE:-pasturestack-validation/server:v1.6.522}
build_options=()
[[ "$revision" =~ ^[0-9a-f]{40}$ ]]
diff --git a/server/build-component-candidate.sh b/server/build-component-candidate.sh
index 0698ba770d..92a70f3d01 100755
--- a/server/build-component-candidate.sh
+++ b/server/build-component-candidate.sh
@@ -14,10 +14,10 @@ for name in "${required[@]}"; do
case "$name" in *_COMMIT) [[ ${!name} =~ ^[0-9a-f]{40}$ ]] ;; *) [[ ${!name} =~ ^[0-9a-f]{64}$ ]] ;; esac
export "$name"
done
-export SERVER_RELEASE_TAG=${SERVER_RELEASE_TAG:-v1.6.521}
-export ORCHESTRATION_ENGINE_RELEASE_TAG=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.337}
+export SERVER_RELEASE_TAG=${SERVER_RELEASE_TAG:-v1.6.522}
+export ORCHESTRATION_ENGINE_RELEASE_TAG=${ORCHESTRATION_ENGINE_RELEASE_TAG:-v0.183.339}
export ORCHESTRATION_ENGINE_ARTIFACT=cattle.jar
-export WEB_CONSOLE_RELEASE_TAG=${WEB_CONSOLE_RELEASE_TAG:-1.6.181}
+export WEB_CONSOLE_RELEASE_TAG=${WEB_CONSOLE_RELEASE_TAG:-1.6.183}
export WEB_CONSOLE_ARTIFACT="web-console-${WEB_CONSOLE_RELEASE_TAG}.tar.gz"
export WEBSOCKET_PROXY_VERSION=${WEBSOCKET_PROXY_VERSION:-0.23.15}
export HOST_API_VERSION=${HOST_API_VERSION:-0.38.5}
diff --git a/server/security/openvex.json b/server/security/openvex.json
index 28391fe212..a2a56cc806 100644
--- a/server/security/openvex.json
+++ b/server/security/openvex.json
@@ -1,6 +1,6 @@
{
"@context": "https://openvex.dev/ns/v0.2.0",
- "@id": "https://github.com/PastureStack/server/security/openvex/v1.6.521",
+ "@id": "https://github.com/PastureStack/server/security/openvex/v1.6.522",
"author": "PastureStack Security",
"timestamp": "2026-10-02T22:05:00Z",
"version": 51,
diff --git a/server/security/vendor-pending.json b/server/security/vendor-pending.json
index 3008895889..9b9a78b0e8 100644
--- a/server/security/vendor-pending.json
+++ b/server/security/vendor-pending.json
@@ -1,6 +1,6 @@
{
"schemaVersion": 1,
- "release": "v1.6.521",
+ "release": "v1.6.522",
"generatedAt": "2026-10-10T00:00:00Z",
"policy": {
"allowedSeverities": [