feat(edp): provision Foundry reader grants

This commit is contained in:
Codex
2026-07-19 15:03:48 +03:00
parent 847a08da93
commit def9a24e0d
17 changed files with 891 additions and 7 deletions
+6
View File
@@ -89,6 +89,12 @@ EXTERNAL_DATA_PLANE_MANAGED_PROVISIONER_KEY_ID=engine-edp-managed-provisioner-v1
EXTERNAL_DATA_PLANE_MANAGED_PROVISIONER_AUDIENCE=nodedc-external-data-plane.managed-provisioning.v1
EXTERNAL_DATA_PLANE_MANAGED_PROVISIONER_MAX_SKEW_SECONDS=60
EXTERNAL_DATA_PLANE_MANAGED_PROVISIONER_REPLAY_CACHE_MAX_ENTRIES=10000
EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONING_ENABLED=true
EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_SERVICE_ID=nodedc-module-foundry
EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_KEY_ID=foundry-edp-managed-provisioner-v1
EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_AUDIENCE=nodedc-external-data-plane.managed-provisioning.v1
EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_MAX_SKEW_SECONDS=60
EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_REPLAY_CACHE_MAX_ENTRIES=10000
# notification core
NOTIFICATION_PG_DB=nodedc_notifications
+24 -2
View File
@@ -182,13 +182,14 @@ force-recreates only `external-data-plane`; the already healthy
independent deploy prerequisite and are never selected by an EDP application
artifact. The reviewed Compose source pins the Timescale image, named volume
and target, internal database-only network, absence of database host ports,
healthy dependency, localhost-only EDP bind and the two read-only
healthy dependency, localhost-only EDP bind and the three read-only
provisioner/trust mounts in the reviewed Compose source. The runner does not
reinterpret version-dependent `docker compose config` JSON as a second deploy
schema. Its canonical enforcement remains the artifact/path allowlist plus
hard-coded build command, selected service set, runtime-secret preparation and
health acceptance. Post-apply acceptance also requires
`database=ready`; a first-rollout failure removes only the candidate EDP
`database=ready`; the managed Foundry slice additionally requires
`foundryReaderBindingProvisioning=digest+server-resolved-source`. A first-rollout failure removes only the candidate EDP
container without volumes and restores the source overlay. It never contains a
provider credential, provider endpoint,
collection schedule or command
@@ -218,6 +219,21 @@ symlinks and permissive modes fail closed. `plan` discloses both paths without
printing key material. The private key must not be broadened into an L2 graph,
MCP surface, artifact or shared-token boundary.
Module Foundry has a separate Ed25519 managed-provisioner identity for
target-scoped Data Product consumer grants. On a relevant `platform` or
`module-foundry` apply, the runner creates or validates the Foundry-only private
key at
`/volume1/docker/nodedc-platform/secrets/foundry-edp-managed-provisioner/private-key.pem`
as `root:root 0400` and the matching EDP trust copy at
`/volume1/docker/nodedc-platform/trust/foundry-managed-provisioner/public-key.pem`
as `root:11006 0440`. Foundry generates the opaque reader token only inside its
persistent private runtime; its signed EDP request contains only a SHA-256
digest and Data Product id. EDP resolves the unique active writer source scope
server-side and fails closed on missing or ambiguous coverage. No provider,
tenant, connection, token, private key or endpoint is admitted to the Foundry
MCP plan, application state or browser response. The Engine signing identity,
native n8n credentials and legacy issuance bearer cannot call this endpoint.
The reviewed Engine source candidate has dedicated server-derived MCP
plan/apply handling for the exact `ndcDataProductWriterApi` + Data Product
Publish tuple. It is separate from the generic HTTP safe-ref path and accepts no
@@ -340,6 +356,12 @@ never enters an artifact. The component reuses the existing internal platform
credential for Launcher handoff validation and requires that runtime
configuration before its first `apply`.
When the managed reader-grant source is present, Module Foundry acceptance also
requires `/healthz` to report the dedicated Ed25519 provisioner as configured.
The check is source-aware: if an apply rolls back to the prior source, rollback
acceptance uses that prior health contract instead of falsely requiring a
feature which the restored generation does not contain.
The Foundry ↔ Map Gateway signing key is not an application or `.env` setting.
On the first relevant `platform` or `module-foundry` apply, the root-owned
runner creates `/volume1/docker/nodedc-platform/secrets/map-gateway-admin-secret`
@@ -77,6 +77,10 @@ async function assertSourceBoundary() {
"source: /volume1/docker/nodedc-platform/trust/engine-managed-provisioner",
"target: /run/nodedc-trust/engine-managed-provisioner",
"EXTERNAL_DATA_PLANE_MANAGED_PROVISIONER_PUBLIC_KEY_FILE: /run/nodedc-trust/engine-managed-provisioner/public-key.pem",
"source: /volume1/docker/nodedc-platform/trust/foundry-managed-provisioner",
"target: /run/nodedc-trust/foundry-managed-provisioner",
"EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_PUBLIC_KEY_FILE: /run/nodedc-trust/foundry-managed-provisioner/public-key.pem",
"EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONING_ENABLED: ${EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONING_ENABLED:-true}",
"create_host_path: false",
]) {
if (!compose.includes(fragment)) throw new Error(`platform_compose_boundary_missing:${fragment}`);
@@ -97,6 +101,9 @@ async function assertSourceBoundary() {
'managedReaderBindingLifetime: config.managedProvisionerApiEnabled ? "explicit-revoke" : "disabled"',
'managedReaderBindings: "digest+idempotent-generation+explicit-revoke"',
'readerSourceScope: "writer-resolved+fail-closed-ambiguity"',
'app.post("/internal/data-plane/v1/consumer-reader-bindings/plan"',
'app.put("/internal/data-plane/v1/consumer-reader-bindings/by-key/:bindingKey"',
'foundryReaderBindingProvisioning: config.foundryProvisionerApiEnabled ? "digest+server-resolved-source" : "disabled"',
'source_connection_id as "sourceConnectionId"',
'where id = $1 and binding_key is null and active = true',
]) {
@@ -109,6 +116,8 @@ async function assertSourceBoundary() {
for (const marker of [
"managed_reader_source_scope_not_found",
"managed_reader_source_scope_ambiguous",
"managed_consumer_reader_source_scope_not_found",
"managed_consumer_reader_source_scope_ambiguous",
"external_data_plane_writer_bindings",
]) {
if (!readerSourceScope.includes(marker)) throw new Error(`reader_source_scope_boundary_missing:${marker}`);
@@ -38,6 +38,8 @@ const stage = await mkdtemp(join(tmpdir(), "nodedc-module-foundry-artifact-"));
const payload = join(stage, "payload");
const target = join(artifactDir, `nodedc-module-foundry-${patchId}.tgz`);
await assertSourceBoundary();
try {
await mkdir(payload, { recursive: true });
for (const sourceRelative of files) {
@@ -60,6 +62,31 @@ try {
await rm(stage, { recursive: true, force: true });
}
async function assertSourceBoundary() {
const compose = await readFile(resolve(foundryRoot, "infra/docker-compose.module-foundry.yml"), "utf8");
for (const fragment of [
"source: /volume1/docker/nodedc-platform/secrets/foundry-edp-managed-provisioner/private-key.pem",
"target: /run/nodedc-secrets/foundry-edp-managed-provisioner/private-key.pem",
"NODEDC_EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_PRIVATE_KEY_FILE: /run/nodedc-secrets/foundry-edp-managed-provisioner/private-key.pem",
"create_host_path: false",
]) {
if (!compose.includes(fragment)) throw new Error(`foundry_reader_grant_boundary_missing:${fragment}`);
}
const provisioner = await readFile(resolve(foundryRoot, "server/foundry-reader-grant-provisioner.mjs"), "utf8");
for (const marker of [
'"/internal/data-plane/v1/consumer-reader-bindings/plan"',
"consumer-reader-bindings/by-key/${encodeURIComponent(bindingKey)}",
"capabilityDigest: createHash(\"sha256\").update(token",
"sourceScope: \"resolved-server-side\"",
"O_NOFOLLOW",
]) {
if (!provisioner.includes(marker)) throw new Error(`foundry_reader_grant_boundary_missing:${marker}`);
}
if (/providerId|tenantId|connectionId/.test(provisioner)) {
throw new Error("foundry_reader_grant_source_scope_boundary_violation");
}
}
function canonicalTarScript() {
return [
"import gzip,io,pathlib,sys,tarfile",
@@ -88,6 +115,7 @@ async function copySafe(source, destination) {
await mkdir(destination, { recursive: true });
for (const entry of await readdir(source, { withFileTypes: true })) {
if (entry.name === "private-key.pem") throw new Error(`private_key_source_rejected:${relative(foundryRoot, join(source, entry.name))}`);
if (ignoredBasenames.has(entry.name) || entry.name.startsWith(".env") || entry.name.endsWith(".tsbuildinfo")) continue;
const childSource = join(source, entry.name);
const childDestination = join(destination, entry.name);
+198
View File
@@ -63,6 +63,10 @@ ENGINE_EDP_MANAGED_PROVISIONER_SECRET_DIR = MAP_GATEWAY_SECRET_DIR / "engine-edp
ENGINE_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE = ENGINE_EDP_MANAGED_PROVISIONER_SECRET_DIR / "private-key.pem"
ENGINE_EDP_MANAGED_PROVISIONER_TRUST_DIR = Path("/volume1/docker/nodedc-platform/trust/engine-managed-provisioner")
ENGINE_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE = ENGINE_EDP_MANAGED_PROVISIONER_TRUST_DIR / "public-key.pem"
FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR = MAP_GATEWAY_SECRET_DIR / "foundry-edp-managed-provisioner"
FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE = FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR / "private-key.pem"
FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR = Path("/volume1/docker/nodedc-platform/trust/foundry-managed-provisioner")
FOUNDRY_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE = FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR / "public-key.pem"
EXTERNAL_DATA_PLANE_READER_GRANTS_DIR = MAP_GATEWAY_SECRET_DIR / "external-data-plane-reader-grants"
FOUNDRY_BINDING_GRANTS_DIR = MAP_GATEWAY_SECRET_DIR / "foundry-binding-grants"
N8N_PRIVATE_EXTENSION_RELEASES_ROOT = Path("/volume1/docker/nodedc-platform/n8n-private-extensions")
@@ -1159,6 +1163,168 @@ def ensure_engine_edp_managed_provisioner_keypair():
if temporary_public.exists():
temporary_public.unlink()
return "created"
return "created"
def ensure_foundry_edp_managed_provisioner_keypair():
# Foundry receives its own signing identity. It must never reuse or read
# the Engine private key; EDP trusts the matching public key separately.
openssl = shutil.which("openssl")
if not openssl:
die("openssl is required to manage the Foundry EDP signing key")
try:
secret_parent_stat = MAP_GATEWAY_SECRET_DIR.lstat()
except FileNotFoundError:
MAP_GATEWAY_SECRET_DIR.mkdir(parents=True, exist_ok=False)
secret_parent_stat = MAP_GATEWAY_SECRET_DIR.lstat()
if stat.S_ISLNK(secret_parent_stat.st_mode) or not stat.S_ISDIR(secret_parent_stat.st_mode):
die("Foundry EDP signing key parent directory is unsafe")
os.chown(MAP_GATEWAY_SECRET_DIR, 0, MAP_GATEWAY_RUNTIME_GID)
MAP_GATEWAY_SECRET_DIR.chmod(0o710)
try:
private_dir_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR.lstat()
except FileNotFoundError:
FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR.mkdir(parents=False, exist_ok=False)
private_dir_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR.lstat()
if stat.S_ISLNK(private_dir_stat.st_mode) or not stat.S_ISDIR(private_dir_stat.st_mode):
die("Foundry EDP signing key directory is unsafe")
os.chown(FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR, 0, 0)
FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR.chmod(0o700)
trust_parent = FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR.parent
try:
trust_parent_stat = trust_parent.lstat()
except FileNotFoundError:
trust_parent.mkdir(parents=True, exist_ok=False)
trust_parent_stat = trust_parent.lstat()
if stat.S_ISLNK(trust_parent_stat.st_mode) or not stat.S_ISDIR(trust_parent_stat.st_mode):
die("Foundry EDP trust parent directory is unsafe")
os.chown(trust_parent, 0, EXTERNAL_DATA_PLANE_RUNTIME_GID)
trust_parent.chmod(0o710)
try:
trust_dir_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR.lstat()
except FileNotFoundError:
FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR.mkdir(parents=False, exist_ok=False)
trust_dir_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR.lstat()
if stat.S_ISLNK(trust_dir_stat.st_mode) or not stat.S_ISDIR(trust_dir_stat.st_mode):
die("Foundry EDP trust directory is unsafe")
os.chown(FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR, 0, EXTERNAL_DATA_PLANE_RUNTIME_GID)
FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR.chmod(0o550)
try:
private_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE.lstat()
except FileNotFoundError:
private_stat = None
try:
public_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE.lstat()
except FileNotFoundError:
public_stat = None
if private_stat is None and public_stat is not None:
die("Foundry EDP public key exists without its private key")
private_created = False
if private_stat is None:
temporary_private = (
FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR
/ f".private-key.{os.getpid()}.{time.time_ns()}.tmp"
)
try:
subprocess.run(
[openssl, "genpkey", "-algorithm", "ED25519", "-out", str(temporary_private)],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True,
)
temporary_stat = temporary_private.lstat()
if (stat.S_ISLNK(temporary_stat.st_mode) or not stat.S_ISREG(temporary_stat.st_mode)
or temporary_stat.st_size < 80 or temporary_stat.st_size > 8192):
die("generated Foundry EDP private key is invalid")
os.chown(temporary_private, 0, 0)
temporary_private.chmod(0o400)
with temporary_private.open("rb") as handle:
os.fsync(handle.fileno())
os.replace(temporary_private, FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE)
fsync_directory(FOUNDRY_EDP_MANAGED_PROVISIONER_SECRET_DIR)
private_created = True
except (OSError, subprocess.CalledProcessError) as error:
die(f"failed to generate Foundry EDP signing key: {type(error).__name__}")
finally:
if temporary_private.exists():
temporary_private.unlink()
private_stat = FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE.lstat()
if (stat.S_ISLNK(private_stat.st_mode) or not stat.S_ISREG(private_stat.st_mode)
or private_stat.st_uid != 0 or private_stat.st_gid != 0
or stat.S_IMODE(private_stat.st_mode) != 0o400
or private_stat.st_size < 80 or private_stat.st_size > 8192):
die("Foundry EDP private key has unsafe ownership, mode, or size")
try:
derived = subprocess.run(
[openssl, "pkey", "-in", str(FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE), "-pubout"],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
).stdout
derived_der = subprocess.run(
[
openssl,
"pkey",
"-in",
str(FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE),
"-pubout",
"-outform",
"DER",
],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
).stdout
except (OSError, subprocess.CalledProcessError) as error:
die(f"Foundry EDP private key validation failed: {type(error).__name__}")
if (not derived.startswith(b"-----BEGIN PUBLIC KEY-----\n")
or not derived.rstrip().endswith(b"-----END PUBLIC KEY-----")
or len(derived) > 8192
or len(derived_der) != 44
or not derived_der.startswith(bytes.fromhex("302a300506032b6570032100"))):
die("Foundry EDP signing key must be Ed25519")
if public_stat is not None:
if (stat.S_ISLNK(public_stat.st_mode) or not stat.S_ISREG(public_stat.st_mode)
or public_stat.st_uid != 0
or public_stat.st_gid != EXTERNAL_DATA_PLANE_RUNTIME_GID
or stat.S_IMODE(public_stat.st_mode) != 0o440
or public_stat.st_size < 80 or public_stat.st_size > 8192):
die("Foundry EDP public key has unsafe ownership, mode, or size")
if FOUNDRY_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE.read_bytes() != derived:
die("Foundry EDP public key does not match the installed private key")
return "created" if private_created else "reused"
temporary_public = (
FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR
/ f".public-key.{os.getpid()}.{time.time_ns()}.tmp"
)
descriptor = None
try:
descriptor = os.open(str(temporary_public), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o440)
os.write(descriptor, derived)
os.fsync(descriptor)
os.fchown(descriptor, 0, EXTERNAL_DATA_PLANE_RUNTIME_GID)
os.fchmod(descriptor, 0o440)
os.close(descriptor)
descriptor = None
os.replace(temporary_public, FOUNDRY_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE)
fsync_directory(FOUNDRY_EDP_MANAGED_PROVISIONER_TRUST_DIR)
finally:
if descriptor is not None:
os.close(descriptor)
if temporary_public.exists():
temporary_public.unlink()
return "created"
def ensure_engine_data_product_grant_private_state(include_reader=False):
@@ -6394,9 +6560,12 @@ def plan_artifact(artifact):
if component == "module-foundry":
print(f"runtime_grants=runner-managed:{EXTERNAL_DATA_PLANE_READER_GRANTS_DIR}")
print(f"runtime_grants=runner-managed:{FOUNDRY_BINDING_GRANTS_DIR}")
print(f"runtime_private_key=runner-managed:{FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE}")
print(f"runtime_public_trust=runner-managed:{FOUNDRY_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE}")
if touches_external_data_plane:
print(f"runtime_secret=runner-managed:{EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_FILE}")
print(f"runtime_grants=runner-managed:{EXTERNAL_DATA_PLANE_READER_GRANTS_DIR}")
print(f"runtime_public_trust=runner-managed:{FOUNDRY_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE}")
if touches_engine_credential_sink(component, entries):
print(f"runtime_identity_key_id={ENGINE_CREDENTIAL_PROVISIONER_KEY_ID}")
print(f"runtime_private_key=runner-managed:{ENGINE_CREDENTIAL_PROVISIONER_PRIVATE_KEY_FILE}")
@@ -7136,6 +7305,7 @@ def prepare_component_runtime(component, entries=None):
if component == "module-foundry":
ensure_map_gateway_admin_secret()
ensure_foundry_edp_managed_provisioner_keypair()
ensure_root_owned_grant_directory(
EXTERNAL_DATA_PLANE_READER_GRANTS_DIR,
"external data plane reader grants",
@@ -7195,6 +7365,7 @@ def prepare_component_runtime(component, entries=None):
if touches_external_data_plane:
ensure_external_data_plane_provisioner_secret()
ensure_engine_edp_managed_provisioner_keypair()
ensure_foundry_edp_managed_provisioner_keypair()
ensure_root_owned_grant_directory(
EXTERNAL_DATA_PLANE_READER_GRANTS_DIR,
"external data plane reader grants",
@@ -7292,12 +7463,37 @@ def external_data_plane_healthcheck(require_managed=True):
expected_json["managedWriterBindingLifetime"] = "explicit-revoke"
if 'managedReaderBindingLifetime: config.managedProvisionerApiEnabled ? "explicit-revoke" : "disabled"' in server_text:
expected_json["managedReaderBindingLifetime"] = "explicit-revoke"
if 'foundryReaderBindingProvisioning: config.foundryProvisionerApiEnabled ? "digest+server-resolved-source" : "disabled"' in server_text:
expected_json["foundryReaderBindingProvisioning"] = "digest+server-resolved-source"
expected_json["foundryReaderBindingLifetime"] = "explicit-revoke"
return {
"url": "http://127.0.0.1:18106/healthz",
"expected_json": expected_json,
}
def module_foundry_healthcheck():
expected_json = {
"status": "ok",
"service": "nodedc-module-foundry",
}
server_source = component_root("module-foundry") / "server/catalog-server.mjs"
try:
server_text = server_source.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
server_text = ""
if "dataProductConsumerProvisioner" in server_text:
expected_json["dataProductConsumerProvisioner"] = {
"configured": True,
"auth": "dedicated-ed25519-service-identity",
"sourceScope": "external-data-plane-resolved",
}
return {
"url": "http://172.22.0.222:9920/healthz",
"expected_json": expected_json,
}
def component_healthchecks(component, entries=None, services=None):
if is_engine_n8n_transition(component, entries):
# The transition does not restart the Engine UI/backend generation.
@@ -7311,6 +7507,8 @@ def component_healthchecks(component, entries=None, services=None):
return ("http://127.0.0.1:3001/health",)
if component == "platform" and entries is not None and is_platform_provider_catalog_only(entries):
return ()
if component == "module-foundry":
return (module_foundry_healthcheck(),)
if (
(
is_engine_data_product_publish_grant_slice(component, entries)
@@ -81,6 +81,14 @@ class ExternalDataPlaneArtifactTest(unittest.TestCase):
"source: /volume1/docker/nodedc-platform/trust/engine-managed-provisioner",
compose,
)
self.assertIn(
"source: /volume1/docker/nodedc-platform/trust/foundry-managed-provisioner",
compose,
)
self.assertIn(
"EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_PUBLIC_KEY_FILE: /run/nodedc-trust/foundry-managed-provisioner/public-key.pem",
compose,
)
self.assertNotIn("private-key.pem", compose)
self.assertNotIn(b"-----BEGIN PRIVATE KEY-----", b"\n".join(regular_payloads))
@@ -293,6 +293,48 @@ class CanonicalPlatformRegistryTest(unittest.TestCase):
keypair.assert_called_once_with()
private_state.assert_called_once_with()
def test_module_foundry_runtime_prepares_separate_edp_identity_and_grant_roots(self):
with (
mock.patch.object(RUNNER, "ensure_map_gateway_admin_secret") as map_secret,
mock.patch.object(RUNNER, "ensure_foundry_edp_managed_provisioner_keypair") as foundry_keypair,
mock.patch.object(RUNNER, "ensure_root_owned_grant_directory") as grant_directory,
):
RUNNER.prepare_component_runtime("module-foundry", ("server/catalog-server.mjs",))
map_secret.assert_called_once_with()
foundry_keypair.assert_called_once_with()
self.assertEqual(
grant_directory.call_args_list,
[
mock.call(
RUNNER.EXTERNAL_DATA_PLANE_READER_GRANTS_DIR,
"external data plane reader grants",
),
mock.call(
RUNNER.FOUNDRY_BINDING_GRANTS_DIR,
"foundry binding grants",
),
],
)
def test_edp_runtime_prepares_engine_and_foundry_public_trust_independently(self):
entries = ("platform/services/external-data-plane/src/server.mjs",)
with (
mock.patch.object(RUNNER, "ensure_external_data_plane_provisioner_secret") as legacy_secret,
mock.patch.object(RUNNER, "ensure_engine_edp_managed_provisioner_keypair") as engine_keypair,
mock.patch.object(RUNNER, "ensure_foundry_edp_managed_provisioner_keypair") as foundry_keypair,
mock.patch.object(RUNNER, "ensure_root_owned_grant_directory") as grant_directory,
):
RUNNER.prepare_component_runtime("platform", entries)
legacy_secret.assert_called_once_with()
engine_keypair.assert_called_once_with()
foundry_keypair.assert_called_once_with()
grant_directory.assert_called_once_with(
RUNNER.EXTERNAL_DATA_PLANE_READER_GRANTS_DIR,
"external data plane reader grants",
)
def test_partial_publish_path_has_no_publish_runtime_side_effects(self):
entries = ("nodedc-source/server/dataProductPublishGrant/store.js",)
self.assertTrue(RUNNER.touches_engine_data_product_publish_grant(entries))
@@ -441,6 +483,46 @@ class CanonicalPlatformRegistryTest(unittest.TestCase):
(RUNNER.external_data_plane_healthcheck(),),
)
def test_edp_healthcheck_requires_foundry_provisioner_for_the_new_source(self):
with tempfile.TemporaryDirectory(prefix="nodedc-edp-foundry-health-") as directory:
root = Path(directory)
source = root / "platform/services/external-data-plane/src/server.mjs"
source.parent.mkdir(parents=True)
source.write_text(
'foundryReaderBindingProvisioning: config.foundryProvisionerApiEnabled ? "digest+server-resolved-source" : "disabled"\n',
encoding="utf-8",
)
original_root = RUNNER.COMPONENTS["platform"]["payload_root"]
RUNNER.COMPONENTS["platform"]["payload_root"] = root
try:
expected = RUNNER.external_data_plane_healthcheck()["expected_json"]
finally:
RUNNER.COMPONENTS["platform"]["payload_root"] = original_root
self.assertEqual(
expected["foundryReaderBindingProvisioning"],
"digest+server-resolved-source",
)
self.assertEqual(expected["foundryReaderBindingLifetime"], "explicit-revoke")
def test_module_foundry_healthcheck_tracks_candidate_and_rollback_source(self):
with tempfile.TemporaryDirectory(prefix="nodedc-foundry-health-") as directory:
root = Path(directory)
source = root / "server/catalog-server.mjs"
source.parent.mkdir(parents=True)
original_root = RUNNER.COMPONENTS["module-foundry"]["payload_root"]
RUNNER.COMPONENTS["module-foundry"]["payload_root"] = root
try:
source.write_text("const dataProductConsumerProvisioner = true;\n", encoding="utf-8")
candidate = RUNNER.module_foundry_healthcheck()["expected_json"]
source.write_text("const previous = true;\n", encoding="utf-8")
rollback = RUNNER.module_foundry_healthcheck()["expected_json"]
finally:
RUNNER.COMPONENTS["module-foundry"]["payload_root"] = original_root
self.assertEqual(candidate["dataProductConsumerProvisioner"]["configured"], True)
self.assertNotIn("dataProductConsumerProvisioner", rollback)
def test_edp_runtime_never_probes_unselected_platform_services(self):
entries = ("platform/services/external-data-plane/src/server.mjs",)
services = ("external-data-plane",)
+6
View File
@@ -111,6 +111,12 @@ EXTERNAL_DATA_PLANE_MANAGED_PROVISIONER_KEY_ID=engine-edp-managed-provisioner-v1
EXTERNAL_DATA_PLANE_MANAGED_PROVISIONER_AUDIENCE=nodedc-external-data-plane.managed-provisioning.v1
EXTERNAL_DATA_PLANE_MANAGED_PROVISIONER_MAX_SKEW_SECONDS=60
EXTERNAL_DATA_PLANE_MANAGED_PROVISIONER_REPLAY_CACHE_MAX_ENTRIES=10000
EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONING_ENABLED=true
EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_SERVICE_ID=nodedc-module-foundry
EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_KEY_ID=foundry-edp-managed-provisioner-v1
EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_AUDIENCE=nodedc-external-data-plane.managed-provisioning.v1
EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_MAX_SKEW_SECONDS=60
EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_REPLAY_CACHE_MAX_ENTRIES=10000
NOTIFICATION_PG_DB=nodedc_notifications
NOTIFICATION_PG_USER=nodedc_notifications
@@ -68,6 +68,16 @@ services:
EXTERNAL_DATA_PLANE_MANAGED_PROVISIONER_AUDIENCE: ${EXTERNAL_DATA_PLANE_MANAGED_PROVISIONER_AUDIENCE:-nodedc-external-data-plane.managed-provisioning.v1}
EXTERNAL_DATA_PLANE_MANAGED_PROVISIONER_MAX_SKEW_SECONDS: ${EXTERNAL_DATA_PLANE_MANAGED_PROVISIONER_MAX_SKEW_SECONDS:-60}
EXTERNAL_DATA_PLANE_MANAGED_PROVISIONER_REPLAY_CACHE_MAX_ENTRIES: ${EXTERNAL_DATA_PLANE_MANAGED_PROVISIONER_REPLAY_CACHE_MAX_ENTRIES:-10000}
# Foundry owns a separate managed-provisioner identity. It submits only
# a consumer target digest and Data Product id; EDP resolves the active
# writer scope internally and returns no provider identity.
EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONING_ENABLED: ${EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONING_ENABLED:-true}
EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_PUBLIC_KEY_FILE: /run/nodedc-trust/foundry-managed-provisioner/public-key.pem
EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_SERVICE_ID: ${EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_SERVICE_ID:-nodedc-module-foundry}
EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_KEY_ID: ${EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_KEY_ID:-foundry-edp-managed-provisioner-v1}
EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_AUDIENCE: ${EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_AUDIENCE:-nodedc-external-data-plane.managed-provisioning.v1}
EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_MAX_SKEW_SECONDS: ${EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_MAX_SKEW_SECONDS:-60}
EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_REPLAY_CACHE_MAX_ENTRIES: ${EXTERNAL_DATA_PLANE_FOUNDRY_PROVISIONER_REPLAY_CACHE_MAX_ENTRIES:-10000}
volumes:
- type: bind
source: /volume1/docker/nodedc-platform/secrets/external-data-plane-provisioner/token
@@ -81,6 +91,12 @@ services:
read_only: true
bind:
create_host_path: false
- type: bind
source: /volume1/docker/nodedc-platform/trust/foundry-managed-provisioner
target: /run/nodedc-trust/foundry-managed-provisioner
read_only: true
bind:
create_host_path: false
expose:
- "18106"
ports: