Publish Clusterflux 5d40a47
This commit is contained in:
parent
e5d609cfb2
commit
433d759be1
2 changed files with 109 additions and 36 deletions
|
|
@ -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 &&
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue