Publish Clusterflux 5d40a47
This commit is contained in:
parent
e5d609cfb2
commit
433d759be1
2 changed files with 109 additions and 36 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"kind": "clusterflux-filtered-public-tree",
|
"kind": "clusterflux-filtered-public-tree",
|
||||||
"source_commit": "1d0b6fab342bd34e6f539adb649eaa4ed82f0cfa",
|
"source_commit": "5d40a47fb4bdeebabb181286d1fb49af81c4901b",
|
||||||
"release_name": "release-1d0b6fab342b",
|
"release_name": "release-5d40a47fb4bd",
|
||||||
"filtered_out": [
|
"filtered_out": [
|
||||||
"private/**",
|
"private/**",
|
||||||
"internal/**",
|
"internal/**",
|
||||||
|
|
|
||||||
|
|
@ -78,12 +78,20 @@ function requireEnabled() {
|
||||||
"CLUSTERFLUX_PUBLIC_RELEASE_BROWSER_OPEN_COMMAND or CLUSTERFLUX_CLI_HAPPY_PATH_REUSE_SESSION_FILE is required"
|
"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 (
|
if (
|
||||||
strictFullRelease &&
|
strictFullRelease &&
|
||||||
!process.env.CLUSTERFLUX_SECOND_TENANT_SESSION_FILE
|
!hasSecondTenantSession &&
|
||||||
|
!hasSingleAccountIsolationTarget
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
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) {
|
if (strictFullRelease && !process.env.CLUSTERFLUX_EXPIRED_USER_SESSION_FILE) {
|
||||||
|
|
@ -1664,6 +1672,7 @@ async function runLiveAgentCredentialSecurity({
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runSecondTenantIsolation({
|
async function runSecondTenantIsolation({
|
||||||
|
firstSessionSecret,
|
||||||
firstTenant,
|
firstTenant,
|
||||||
firstProject,
|
firstProject,
|
||||||
firstProcess,
|
firstProcess,
|
||||||
|
|
@ -1673,6 +1682,8 @@ async function runSecondTenantIsolation({
|
||||||
}) {
|
}) {
|
||||||
const file = process.env.CLUSTERFLUX_SECOND_TENANT_SESSION_FILE;
|
const file = process.env.CLUSTERFLUX_SECOND_TENANT_SESSION_FILE;
|
||||||
const evidenceFile = process.env.CLUSTERFLUX_SECOND_TENANT_EVIDENCE_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) {
|
if (!file && evidenceFile) {
|
||||||
const evidenceBytes = fs.readFileSync(path.resolve(evidenceFile));
|
const evidenceBytes = fs.readFileSync(path.resolve(evidenceFile));
|
||||||
const previous = JSON.parse(evidenceBytes);
|
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" };
|
if (!file) return { executed: false, reason: "second tenant session not supplied" };
|
||||||
const second = readJson(path.resolve(file));
|
const second = readJson(path.resolve(file));
|
||||||
assert.strictEqual(second.coordinator, serviceEndpoint);
|
assert.strictEqual(second.coordinator, serviceEndpoint);
|
||||||
|
|
@ -2036,33 +2130,13 @@ async function runLiveQuotaPreallocation({ sessionSecret, suffix }) {
|
||||||
!JSON.stringify(processes).includes(deniedProcess),
|
!JSON.stringify(processes).includes(deniedProcess),
|
||||||
"quota-denied process was allocated before denial"
|
"quota-denied process was allocated before denial"
|
||||||
);
|
);
|
||||||
const second = readJson(
|
const independentScope = await sendHostedControl(
|
||||||
path.resolve(process.env.CLUSTERFLUX_SECOND_TENANT_SESSION_FILE)
|
authenticatedRequest(sessionSecret, { type: "list_node_descriptors" })
|
||||||
);
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
unrelatedStarted.type,
|
independentScope.type,
|
||||||
"process_started",
|
"node_descriptors",
|
||||||
`first tenant quota exhaustion affected another tenant: ${JSON.stringify(unrelatedStarted)}`
|
`spawn quota exhaustion locked an independent read scope: ${JSON.stringify(independentScope)}`
|
||||||
);
|
|
||||||
const unrelatedAborted = await sendHostedControl(
|
|
||||||
authenticatedRequest(second.session_secret, {
|
|
||||||
type: "abort_process",
|
|
||||||
process: unrelatedProcess,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
|
||||||
unrelatedAborted.type,
|
|
||||||
"process_aborted",
|
|
||||||
JSON.stringify(unrelatedAborted)
|
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
limit: spawnQuota.limit,
|
limit: spawnQuota.limit,
|
||||||
|
|
@ -2076,12 +2150,10 @@ async function runLiveQuotaPreallocation({ sessionSecret, suffix }) {
|
||||||
message: denial.message,
|
message: denial.message,
|
||||||
},
|
},
|
||||||
denied_process_allocated: false,
|
denied_process_allocated: false,
|
||||||
unrelated_tenant: {
|
independent_scope: {
|
||||||
tenant: second.tenant,
|
operation: "list_node_descriptors",
|
||||||
process: unrelatedProcess,
|
response: independentScope.type,
|
||||||
start: unrelatedStarted.type,
|
unaffected_by_spawn_quota: true,
|
||||||
abort: unrelatedAborted.type,
|
|
||||||
unaffected_by_first_tenant_quota: true,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -4416,6 +4488,7 @@ async function main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const tenantIsolation = await runSecondTenantIsolation({
|
const tenantIsolation = await runSecondTenantIsolation({
|
||||||
|
firstSessionSecret: sessionSecret,
|
||||||
firstTenant: tenant,
|
firstTenant: tenant,
|
||||||
firstProject: project,
|
firstProject: project,
|
||||||
firstProcess: dapEditRestart.process,
|
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: "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: "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: "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 =
|
const fullReleasePassed =
|
||||||
strictFullRelease &&
|
strictFullRelease &&
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue