Public release release-f70e47af061d
Source commit: f70e47af061d8f7ba27cd769c8cd2e2f8707dc72 Public tree identity: sha256:03a43db60d295811688bedaa5ad6460df0dbde27fbf37033997f4d8100f83ab2
This commit is contained in:
commit
b23e4e5bff
210 changed files with 78295 additions and 0 deletions
222
scripts/cli-browser-login-flow-smoke.js
Normal file
222
scripts/cli-browser-login-flow-smoke.js
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const cp = require("child_process");
|
||||
const fs = require("fs");
|
||||
const net = require("net");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const tmp = path.join(repo, "target", "acceptance", "tmp", "cli-browser-login-flow");
|
||||
const project = path.join(tmp, "project");
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
fs.mkdirSync(project, { recursive: true });
|
||||
|
||||
function writeOpener() {
|
||||
const opener = path.join(tmp, "browser-opener.js");
|
||||
const trace = path.join(tmp, "browser-opener.log");
|
||||
fs.writeFileSync(
|
||||
opener,
|
||||
`#!/usr/bin/env node
|
||||
const fs = require("fs");
|
||||
const trace = ${JSON.stringify(trace)};
|
||||
const loginUrl = new URL(process.argv[2]);
|
||||
const state = loginUrl.searchParams.get("state");
|
||||
const nonce = loginUrl.searchParams.get("nonce");
|
||||
const challenge = loginUrl.searchParams.get("code_challenge");
|
||||
if (loginUrl.protocol !== "https:" || !state || !nonce || !challenge) {
|
||||
fs.appendFileSync(trace, "missing server-owned OIDC parameters\\n");
|
||||
process.exit(1);
|
||||
}
|
||||
fs.appendFileSync(trace, "server-owned browser transaction\\n");
|
||||
// Model a real browser/opener that remains alive after the CLI transaction.
|
||||
// Its descriptors must not keep the invoking CLI process open.
|
||||
setTimeout(() => process.exit(0), 5000);
|
||||
`
|
||||
);
|
||||
fs.chmodSync(opener, 0o755);
|
||||
return opener;
|
||||
}
|
||||
|
||||
function startCoordinator() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const requests = [];
|
||||
const server = net.createServer((socket) => {
|
||||
let buffered = "";
|
||||
socket.on("data", (chunk) => {
|
||||
buffered += chunk.toString("utf8");
|
||||
while (buffered.includes("\n")) {
|
||||
const newline = buffered.indexOf("\n");
|
||||
const line = buffered.slice(0, newline);
|
||||
buffered = buffered.slice(newline + 1);
|
||||
const envelope = JSON.parse(line);
|
||||
requests.push(envelope);
|
||||
assert.strictEqual(envelope.type, "coordinator_request");
|
||||
assert.strictEqual(envelope.protocol_version, 1);
|
||||
assert.strictEqual(envelope.authentication.kind, "none");
|
||||
const request = envelope.payload;
|
||||
|
||||
if (requests.length === 1) {
|
||||
assert.strictEqual(envelope.request_id, "cli-1");
|
||||
assert.strictEqual(envelope.operation, "begin_oidc_browser_login");
|
||||
assert.deepStrictEqual(request, {
|
||||
type: "begin_oidc_browser_login",
|
||||
});
|
||||
socket.write(
|
||||
`${JSON.stringify({
|
||||
type: "oidc_browser_login_started",
|
||||
transaction_id: "login-transaction",
|
||||
polling_secret: "opaque-polling-secret",
|
||||
authorization_url:
|
||||
"https://auth.michelpaulissen.com/application/o/authorize/?state=server-state&nonce=server-nonce&code_challenge=server-pkce&code_challenge_method=S256&redirect_uri=https%3A%2F%2Fclusterflux.michelpaulissen.com%2Fauth%2Fcallback",
|
||||
expires_at_epoch_seconds: 1800000000,
|
||||
})}\n`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
assert.strictEqual(envelope.request_id, "cli-2");
|
||||
assert.strictEqual(envelope.operation, "poll_oidc_browser_login");
|
||||
assert.deepStrictEqual(request, {
|
||||
type: "poll_oidc_browser_login",
|
||||
transaction_id: "login-transaction",
|
||||
polling_secret: "opaque-polling-secret",
|
||||
});
|
||||
socket.end(
|
||||
`${JSON.stringify({
|
||||
type: "oidc_browser_session",
|
||||
session: {
|
||||
tenant: "tenant-smoke",
|
||||
project: "project-smoke",
|
||||
user: "user-smoke",
|
||||
cli_session_credential_kind: "CliDeviceSession",
|
||||
cli_session_secret: "scoped-cli-session-secret",
|
||||
expires_at_epoch_seconds: 1800000000,
|
||||
provider_tokens_sent_to_nodes: false,
|
||||
},
|
||||
})}\n`
|
||||
);
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const address = server.address();
|
||||
resolve({
|
||||
url: `${address.address}:${address.port}`,
|
||||
requests,
|
||||
close: () =>
|
||||
new Promise((closeResolve) => {
|
||||
if (!server.listening) closeResolve();
|
||||
else server.close(() => closeResolve());
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function runClusterflux(args, env) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = cp.spawn(
|
||||
"cargo",
|
||||
[
|
||||
"run",
|
||||
"-q",
|
||||
"--manifest-path",
|
||||
path.join(repo, "Cargo.toml"),
|
||||
"-p",
|
||||
"clusterflux-cli",
|
||||
"--bin",
|
||||
"clusterflux",
|
||||
"--",
|
||||
...args,
|
||||
],
|
||||
{ cwd: project, env, stdio: ["ignore", "pipe", "pipe"] }
|
||||
);
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.on("data", (chunk) => (stdout += chunk.toString("utf8")));
|
||||
child.stderr.on("data", (chunk) => (stderr += chunk.toString("utf8")));
|
||||
child.once("error", reject);
|
||||
child.once("close", (code) => {
|
||||
if (code === 0) resolve(stdout);
|
||||
else reject(new Error(`clusterflux exited ${code}\n${stderr}\n${stdout}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const opener = writeOpener();
|
||||
const coordinator = await startCoordinator();
|
||||
try {
|
||||
const loginStarted = Date.now();
|
||||
const report = JSON.parse(
|
||||
await runClusterflux(
|
||||
[
|
||||
"login",
|
||||
"--browser",
|
||||
"--json",
|
||||
"--coordinator",
|
||||
coordinator.url,
|
||||
"--project-id",
|
||||
"project-smoke",
|
||||
],
|
||||
{
|
||||
...process.env,
|
||||
CLUSTERFLUX_BROWSER_OPEN_COMMAND: opener,
|
||||
CLUSTERFLUX_BROWSER_LOGIN_TIMEOUT_SECONDS: "5",
|
||||
}
|
||||
)
|
||||
);
|
||||
assert(
|
||||
Date.now() - loginStarted < 3000,
|
||||
"a long-lived browser opener must not keep CLI output pipes or login completion open"
|
||||
);
|
||||
assert.strictEqual(report.plan.coordinator, coordinator.url);
|
||||
assert.strictEqual(report.boundary.cli_contacted_coordinator, true);
|
||||
assert.strictEqual(report.boundary.scoped_cli_session_received, true);
|
||||
assert.strictEqual(report.boundary.local_cli_session_file_written, true);
|
||||
assert.strictEqual(report.boundary.provider_tokens_persisted_locally, false);
|
||||
assert.strictEqual(report.boundary.provider_tokens_exposed_to_cli, false);
|
||||
assert.strictEqual(report.boundary.provider_tokens_sent_to_nodes, false);
|
||||
assert.strictEqual(report.boundary.coordinator_session_requests, 2);
|
||||
assert.strictEqual(coordinator.requests.length, 2);
|
||||
|
||||
const sessionFile = path.join(project, ".clusterflux", "session.json");
|
||||
const sessionText = fs.readFileSync(sessionFile, "utf8");
|
||||
const session = JSON.parse(sessionText);
|
||||
assert.strictEqual(session.kind, "human");
|
||||
assert.strictEqual(session.coordinator, coordinator.url);
|
||||
assert.strictEqual(session.tenant, "tenant-smoke");
|
||||
assert.strictEqual(session.project, "project-smoke");
|
||||
assert.strictEqual(session.user, "user-smoke");
|
||||
assert.strictEqual(session.cli_session_credential_kind, "CliDeviceSession");
|
||||
assert.strictEqual(session.token_expiry_posture, "expires_at");
|
||||
assert.strictEqual(session.expires_at, "1800000000");
|
||||
assert.strictEqual(session.provider_tokens_exposed_to_cli, false);
|
||||
assert.strictEqual(session.provider_tokens_sent_to_nodes, false);
|
||||
assert.doesNotMatch(
|
||||
sessionText,
|
||||
/access_token|refresh_token|id_token|provider-secret|authorization_code|Bearer/
|
||||
);
|
||||
|
||||
const authStatus = JSON.parse(
|
||||
await runClusterflux(["auth", "status", "--json"], process.env)
|
||||
);
|
||||
assert.strictEqual(authStatus.active_coordinator, coordinator.url);
|
||||
assert.strictEqual(authStatus.principal, "user-smoke");
|
||||
assert.strictEqual(authStatus.tenant, "tenant-smoke");
|
||||
assert.strictEqual(authStatus.project, "project-smoke");
|
||||
assert.strictEqual(authStatus.session.kind, "human");
|
||||
assert.strictEqual(authStatus.session.source, "session_file");
|
||||
assert.strictEqual(authStatus.session.provider_tokens_exposed_to_cli, false);
|
||||
assert.strictEqual(authStatus.session.provider_tokens_exposed_to_nodes, false);
|
||||
} finally {
|
||||
await coordinator.close();
|
||||
}
|
||||
console.log("CLI browser login flow smoke passed");
|
||||
})().catch((error) => {
|
||||
console.error(error.stack || error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue