Update public node enrollment report

This commit is contained in:
Michel Paulissen 2026-07-04 10:09:40 +02:00
parent 298fbcca72
commit c4d8d94f49
3 changed files with 150 additions and 11 deletions

View file

@ -1,7 +1,7 @@
{ {
"kind": "disasmer-filtered-public-tree", "kind": "disasmer-filtered-public-tree",
"source_commit": "57e56a24e60a3990d91c199548b96b6c1f0fe962", "source_commit": "3313653a3d00fcd3fa5613e12cde2f76ae9a8ad0",
"release_name": "dryrun-57e56a24e60a", "release_name": "dryrun-3313653a3d00",
"filtered_out": [ "filtered_out": [
"private/**", "private/**",
"experiments/**", "experiments/**",

View file

@ -1335,32 +1335,98 @@ fn build_report(args: BuildArgs, cwd: PathBuf) -> Result<Value> {
fn node_enroll_report(args: NodeEnrollArgs) -> Result<Value> { fn node_enroll_report(args: NodeEnrollArgs) -> Result<Value> {
if let Some(coordinator) = &args.scope.coordinator { if let Some(coordinator) = &args.scope.coordinator {
let tenant = args.scope.tenant.clone();
let project = args.scope.project.clone();
let user = args.scope.user.clone();
let requested_grant = args.grant.clone();
let ttl_seconds = args.ttl_seconds;
let mut session = JsonLineSession::connect(coordinator)?; let mut session = JsonLineSession::connect(coordinator)?;
let response = session.request(json!({ let response = session.request(json!({
"type": "create_node_enrollment_grant", "type": "create_node_enrollment_grant",
"tenant": args.scope.tenant, "tenant": tenant,
"project": args.scope.project, "project": project,
"actor_user": args.scope.user, "actor_user": user,
"grant": args.grant, "grant": requested_grant,
"now_epoch_seconds": 0, "now_epoch_seconds": 0,
"ttl_seconds": args.ttl_seconds, "ttl_seconds": ttl_seconds,
}))?; }))?;
let enrollment_grant = node_enrollment_grant_summary(
&response,
&tenant,
&project,
&user,
&requested_grant,
ttl_seconds,
);
return Ok(json!({ return Ok(json!({
"command": "node enroll", "command": "node enroll",
"status": "created",
"coordinator": coordinator, "coordinator": coordinator,
"tenant": tenant,
"project": project,
"user": user,
"private_website_required": false,
"enrollment_grant": enrollment_grant,
"response": response, "response": response,
"coordinator_session_requests": session.requests(), "coordinator_session_requests": session.requests(),
})); }));
} }
let enrollment_grant = json!({
"grant": args.grant,
"tenant": args.scope.tenant,
"project": args.scope.project,
"user": args.scope.user,
"scope": "node:attach",
"ttl_seconds": args.ttl_seconds,
"short_lived": true,
"exchange_for_persistent_node_identity": true,
"node_credentials_separate_from_user_session": true,
});
Ok(json!({ Ok(json!({
"command": "node enroll", "command": "node enroll",
"status": "planned_without_coordinator", "status": "planned_without_coordinator",
"grant": args.grant, "private_website_required": false,
"ttl_seconds": args.ttl_seconds, "enrollment_grant": enrollment_grant,
"scope": "node:attach",
})) }))
} }
fn node_enrollment_grant_summary(
response: &Value,
tenant: &str,
project: &str,
user: &str,
requested_grant: &str,
ttl_seconds: u64,
) -> Value {
json!({
"grant": response
.get("grant")
.cloned()
.unwrap_or_else(|| json!(requested_grant)),
"tenant": response
.get("tenant")
.cloned()
.unwrap_or_else(|| json!(tenant)),
"project": response
.get("project")
.cloned()
.unwrap_or_else(|| json!(project)),
"user": user,
"scope": response
.get("scope")
.cloned()
.unwrap_or_else(|| json!("node:attach")),
"ttl_seconds": ttl_seconds,
"expires_at_epoch_seconds": response
.get("expires_at_epoch_seconds")
.cloned()
.unwrap_or(Value::Null),
"short_lived": true,
"exchange_for_persistent_node_identity": true,
"node_credentials_separate_from_user_session": true,
})
}
fn node_list_report(args: NodeListArgs) -> Result<Value> { fn node_list_report(args: NodeListArgs) -> Result<Value> {
node_descriptors_report("node list", args.scope, None) node_descriptors_report("node list", args.scope, None)
} }
@ -7534,6 +7600,65 @@ mod tests {
assert_eq!(report["diagnostics"][0]["code"], "missing_environment"); assert_eq!(report["diagnostics"][0]["code"], "missing_environment");
} }
#[test]
fn node_enroll_reports_short_lived_public_api_grant() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut line = String::new();
reader.read_line(&mut line).unwrap();
assert!(line.contains(r#""type":"create_node_enrollment_grant""#));
assert!(line.contains(r#""tenant":"tenant""#));
assert!(line.contains(r#""project":"project""#));
assert!(line.contains(r#""actor_user":"user""#));
assert!(line.contains(r#""grant":"grant-live""#));
assert!(line.contains(r#""ttl_seconds":300"#));
stream
.write_all(
br#"{"type":"node_enrollment_grant_created","tenant":"tenant","project":"project","grant":"grant-live","scope":"node:attach","expires_at_epoch_seconds":300}"#,
)
.unwrap();
stream.write_all(b"\n").unwrap();
});
let report = node_enroll_report(NodeEnrollArgs {
scope: CliScopeArgs {
coordinator: Some(addr),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
json: false,
},
grant: "grant-live".to_owned(),
ttl_seconds: 300,
})
.unwrap();
server.join().unwrap();
assert_eq!(report["command"], "node enroll");
assert_eq!(report["status"], "created");
assert_eq!(report["private_website_required"], false);
assert_eq!(report["tenant"], "tenant");
assert_eq!(report["project"], "project");
assert_eq!(report["user"], "user");
assert_eq!(report["enrollment_grant"]["grant"], "grant-live");
assert_eq!(report["enrollment_grant"]["scope"], "node:attach");
assert_eq!(report["enrollment_grant"]["ttl_seconds"], 300);
assert_eq!(report["enrollment_grant"]["expires_at_epoch_seconds"], 300);
assert_eq!(report["enrollment_grant"]["short_lived"], true);
assert_eq!(
report["enrollment_grant"]["exchange_for_persistent_node_identity"],
true
);
assert_eq!(
report["enrollment_grant"]["node_credentials_separate_from_user_session"],
true
);
assert_eq!(report["coordinator_session_requests"], 1);
}
#[test] #[test]
fn node_enroll_and_process_commands_have_safe_plan_without_coordinator() { fn node_enroll_and_process_commands_have_safe_plan_without_coordinator() {
let scope = CliScopeArgs { let scope = CliScopeArgs {
@ -7550,7 +7675,15 @@ mod tests {
}) })
.unwrap(); .unwrap();
assert_eq!(enroll["status"], "planned_without_coordinator"); assert_eq!(enroll["status"], "planned_without_coordinator");
assert_eq!(enroll["scope"], "node:attach"); assert_eq!(enroll["private_website_required"], false);
assert_eq!(enroll["enrollment_grant"]["grant"], "grant");
assert_eq!(enroll["enrollment_grant"]["scope"], "node:attach");
assert_eq!(enroll["enrollment_grant"]["ttl_seconds"], 60);
assert_eq!(enroll["enrollment_grant"]["short_lived"], true);
assert_eq!(
enroll["enrollment_grant"]["exchange_for_persistent_node_identity"],
true
);
let cancel = process_cancel_report(ProcessCancelArgs { let cancel = process_cancel_report(ProcessCancelArgs {
scope, scope,

View file

@ -199,6 +199,7 @@ for (const [name, pattern] of [
["CLI top-level logout alias coverage", /fn top_level_logout_alias_removes_only_cli_session_state\(\)/], ["CLI top-level logout alias coverage", /fn top_level_logout_alias_removes_only_cli_session_state\(\)/],
["CLI run rejection category coverage", /fn run_rejection_reports_machine_readable_error_category\(\)/], ["CLI run rejection category coverage", /fn run_rejection_reports_machine_readable_error_category\(\)/],
["node attach grant disclosure coverage", /fn node_attach_discloses_dangerous_capability_grants\(\)/], ["node attach grant disclosure coverage", /fn node_attach_discloses_dangerous_capability_grants\(\)/],
["node enroll public API grant coverage", /fn node_enroll_reports_short_lived_public_api_grant\(\)/],
["quota local status coverage", /fn quota_status_uses_project_config_and_generic_public_limits\(\)/], ["quota local status coverage", /fn quota_status_uses_project_config_and_generic_public_limits\(\)/],
["quota coordinator usage coverage", /fn quota_status_queries_public_coordinator_usage\(\)/], ["quota coordinator usage coverage", /fn quota_status_queries_public_coordinator_usage\(\)/],
["task event summary coverage", /fn process_task_log_and_artifact_reports_summarize_task_events\(\)/], ["task event summary coverage", /fn process_task_log_and_artifact_reports_summarize_task_events\(\)/],
@ -305,6 +306,11 @@ expect(
"CLI exposes node attach grant disclosures", "CLI exposes node attach grant disclosures",
/struct CapabilityGrantDisclosure[\s\S]*coordinator_policy_limited[\s\S]*fn capability_grant_disclosures/ /struct CapabilityGrantDisclosure[\s\S]*coordinator_policy_limited[\s\S]*fn capability_grant_disclosures/
); );
expect(
cli,
"CLI exposes normalized node enrollment grants",
/fn node_enroll_report[\s\S]*create_node_enrollment_grant[\s\S]*enrollment_grant[\s\S]*private_website_required[\s\S]*fn node_enrollment_grant_summary[\s\S]*short_lived[\s\S]*node_credentials_separate_from_user_session/
);
expect( expect(
cli, cli,
"CLI exposes task placement reasons", "CLI exposes task placement reasons",