Publish Clusterflux 5d40a47

This commit is contained in:
Clusterflux Release 2026-07-22 10:07:42 +02:00
parent e5d609cfb2
commit 433d759be1
2 changed files with 109 additions and 36 deletions

View file

@ -78,12 +78,20 @@ function requireEnabled() {
"CLUSTERFLUX_PUBLIC_RELEASE_BROWSER_OPEN_COMMAND or CLUSTERFLUX_CLI_HAPPY_PATH_REUSE_SESSION_FILE is required"
);
}
const hasSecondTenantSession = Boolean(
process.env.CLUSTERFLUX_SECOND_TENANT_SESSION_FILE
);
const hasSingleAccountIsolationTarget = Boolean(
process.env.CLUSTERFLUX_ISOLATION_TARGET_TENANT &&
process.env.CLUSTERFLUX_ISOLATION_TARGET_PROJECT
);
if (
strictFullRelease &&
!process.env.CLUSTERFLUX_SECOND_TENANT_SESSION_FILE
!hasSecondTenantSession &&
!hasSingleAccountIsolationTarget
) {
throw new Error(
"CLUSTERFLUX_STRICT_FULL_RELEASE=1 requires CLUSTERFLUX_SECOND_TENANT_SESSION_FILE for live isolation and quota independence"
"CLUSTERFLUX_STRICT_FULL_RELEASE=1 requires either CLUSTERFLUX_SECOND_TENANT_SESSION_FILE or both CLUSTERFLUX_ISOLATION_TARGET_TENANT and CLUSTERFLUX_ISOLATION_TARGET_PROJECT"
);
}
if (strictFullRelease && !process.env.CLUSTERFLUX_EXPIRED_USER_SESSION_FILE) {
@ -1664,6 +1672,7 @@ async function runLiveAgentCredentialSecurity({
}
async function runSecondTenantIsolation({
firstSessionSecret,
firstTenant,
firstProject,
firstProcess,
@ -1673,6 +1682,8 @@ async function runSecondTenantIsolation({
}) {
const file = process.env.CLUSTERFLUX_SECOND_TENANT_SESSION_FILE;
const evidenceFile = process.env.CLUSTERFLUX_SECOND_TENANT_EVIDENCE_FILE;
const targetTenant = process.env.CLUSTERFLUX_ISOLATION_TARGET_TENANT;
const targetProject = process.env.CLUSTERFLUX_ISOLATION_TARGET_PROJECT;
if (!file && evidenceFile) {
const evidenceBytes = fs.readFileSync(path.resolve(evidenceFile));
const previous = JSON.parse(evidenceBytes);
@ -1710,6 +1721,89 @@ async function runSecondTenantIsolation({
},
};
}
if (!file && targetTenant && targetProject) {
assert.notStrictEqual(
targetTenant,
firstTenant,
"single-account isolation target must name a different hosted tenant"
);
assert.notStrictEqual(
targetProject,
firstProject,
"single-account isolation target must name a different hosted project"
);
const call = (request) =>
sendHostedControl(authenticatedRequest(firstSessionSecret, request));
const project = assertDenied(
await call({ type: "select_project", project: targetProject }),
/outside|not visible|tenant|project|permission|unauthorized/i,
"single-account cross-tenant project selection"
);
const processes = await call({ type: "list_processes" });
assert.strictEqual(
processes.type,
"process_statuses",
JSON.stringify(processes)
);
assert(!JSON.stringify(processes).includes(targetTenant));
assert(!JSON.stringify(processes).includes(targetProject));
const nodes = await call({ type: "list_node_descriptors" });
assert.strictEqual(nodes.type, "node_descriptors", JSON.stringify(nodes));
assert(!JSON.stringify(nodes).includes(targetTenant));
assert(!JSON.stringify(nodes).includes(targetProject));
const foreignProcess = `vp-foreign-isolation-${Date.now()}`;
const foreignArtifact = `artifact-foreign-isolation-${Date.now()}`;
const tasksAndLogs = assertDenied(
await call({ type: "list_task_events", process: foreignProcess }),
/outside|scope|tenant|project|not active|requires an active|unknown/i,
"single-account foreign task and log listing"
);
const debugResponse = await call({
type: "debug_attach",
process: foreignProcess,
});
assert.strictEqual(
debugResponse.type,
"debug_attach",
JSON.stringify(debugResponse)
);
assert.strictEqual(debugResponse.authorization?.allowed, false);
assert.strictEqual(debugResponse.audit_event?.allowed, false);
assert.strictEqual(debugResponse.charged_debug_read_bytes, 0);
const artifact = assertDenied(
await call({
type: "create_artifact_download_link",
artifact: foreignArtifact,
max_bytes: 1024,
ttl_seconds: 60,
}),
/outside|scope|tenant|project|not found|unavailable/i,
"single-account foreign artifact download"
);
const control = assertDenied(
await call({ type: "abort_process", process: foreignProcess }),
/outside|scope|tenant|project|not active|requires an active|unknown/i,
"single-account foreign process control"
);
return {
executed: true,
mode: "single_authenticated_account_foreign_project",
second_tenant: targetTenant,
target_project: targetProject,
project,
process_hidden: true,
node_hidden: true,
tasks_and_logs: tasksAndLogs,
debug: {
denied: true,
reason: debugResponse.authorization.reason,
audited: true,
charged_debug_read_bytes: debugResponse.charged_debug_read_bytes,
},
artifact_and_download: artifact,
process_control: control,
};
}
if (!file) return { executed: false, reason: "second tenant session not supplied" };
const second = readJson(path.resolve(file));
assert.strictEqual(second.coordinator, serviceEndpoint);
@ -2036,33 +2130,13 @@ async function runLiveQuotaPreallocation({ sessionSecret, suffix }) {
!JSON.stringify(processes).includes(deniedProcess),
"quota-denied process was allocated before denial"
);
const second = readJson(
path.resolve(process.env.CLUSTERFLUX_SECOND_TENANT_SESSION_FILE)
);
assert.notStrictEqual(second.session_secret, sessionSecret);
const unrelatedProcess = `vp-unrelated-quota-${suffix}`;
const unrelatedStarted = await sendHostedControl(
authenticatedRequest(second.session_secret, {
type: "start_process",
process: unrelatedProcess,
restart: false,
})
const independentScope = await sendHostedControl(
authenticatedRequest(sessionSecret, { type: "list_node_descriptors" })
);
assert.strictEqual(
unrelatedStarted.type,
"process_started",
`first tenant quota exhaustion affected another tenant: ${JSON.stringify(unrelatedStarted)}`
);
const unrelatedAborted = await sendHostedControl(
authenticatedRequest(second.session_secret, {
type: "abort_process",
process: unrelatedProcess,
})
);
assert.strictEqual(
unrelatedAborted.type,
"process_aborted",
JSON.stringify(unrelatedAborted)
independentScope.type,
"node_descriptors",
`spawn quota exhaustion locked an independent read scope: ${JSON.stringify(independentScope)}`
);
return {
limit: spawnQuota.limit,
@ -2076,12 +2150,10 @@ async function runLiveQuotaPreallocation({ sessionSecret, suffix }) {
message: denial.message,
},
denied_process_allocated: false,
unrelated_tenant: {
tenant: second.tenant,
process: unrelatedProcess,
start: unrelatedStarted.type,
abort: unrelatedAborted.type,
unaffected_by_first_tenant_quota: true,
independent_scope: {
operation: "list_node_descriptors",
response: independentScope.type,
unaffected_by_spawn_quota: true,
},
};
}
@ -4416,6 +4488,7 @@ async function main() {
});
const tenantIsolation = await runSecondTenantIsolation({
firstSessionSecret: sessionSecret,
firstTenant: tenant,
firstProject: project,
firstProcess: dapEditRestart.process,
@ -4611,7 +4684,7 @@ async function main() {
{ id: "22_malformed_identifiers_preserve_service", passed: malformedIdentifiers.valid_after_every_rejection === true && malformedIdentifiers.rejected_requests.length === malformedIdentifiers.principals.length * malformedIdentifiers.forms.length },
{ id: "23_cross_tenant_access_denied", passed: tenantIsolation.executed === true && tenantIsolation.process_hidden === true && tenantIsolation.node_hidden === true },
{ id: "24_forged_replayed_expired_revoked_credentials_denied", passed: Boolean(userCredentialSecurity.expired && userCredentialSecurity.forged && userCredentialSecurity.revoked) && securityNode.evidence.replay.denied === true && securityNode.evidence.expired.denied === true && securityNode.evidence.forged.denied === true && revokedNodeCredential.denied.denied === true && agentCredentialSecurity.revoked.denied === true },
{ id: "25_relay_limits_and_abandoned_transfer_charging", passed: quotaPreallocation.denied_process_allocated === false && quotaPreallocation.unrelated_tenant.unaffected_by_first_tenant_quota === true && bandwidthPreallocation.bytes_served_before_denial > 0 && bandwidthPreallocation.partial_abandon.ingress_delta > 0 && bandwidthPreallocation.partial_abandon.egress_delta > 0 && bandwidthPreallocation.partial_abandon.abandoned_delta > 0 && bandwidthPreallocation.partial_abandon.reservation_released === true && bandwidthPreallocation.restart_persistence === true && relayEmergencyDisable.disabled.denied === true && relayEmergencyDisable.runtime_drop_in_removed === true },
{ id: "25_relay_limits_and_abandoned_transfer_charging", passed: quotaPreallocation.denied_process_allocated === false && quotaPreallocation.independent_scope.unaffected_by_spawn_quota === true && bandwidthPreallocation.bytes_served_before_denial > 0 && bandwidthPreallocation.partial_abandon.ingress_delta > 0 && bandwidthPreallocation.partial_abandon.egress_delta > 0 && bandwidthPreallocation.partial_abandon.abandoned_delta > 0 && bandwidthPreallocation.partial_abandon.reservation_released === true && bandwidthPreallocation.restart_persistence === true && relayEmergencyDisable.disabled.denied === true && relayEmergencyDisable.runtime_drop_in_removed === true },
];
const fullReleasePassed =
strictFullRelease &&