Implement public debug attach authorization
This commit is contained in:
parent
005b47e4c0
commit
f586b48893
5 changed files with 201 additions and 4 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"kind": "disasmer-filtered-public-tree",
|
"kind": "disasmer-filtered-public-tree",
|
||||||
"source_commit": "00a47f2c6f213d43fcef11fdf7926320241da30d",
|
"source_commit": "c7543c680764ad3efa8d96e8c427fddcba2d3a4a",
|
||||||
"release_name": "dryrun-00a47f2c6f21",
|
"release_name": "dryrun-c7543c680764",
|
||||||
"filtered_out": [
|
"filtered_out": [
|
||||||
"private/**",
|
"private/**",
|
||||||
"experiments/**",
|
"experiments/**",
|
||||||
|
|
|
||||||
|
|
@ -1441,13 +1441,52 @@ fn exec_dap(args: DapArgs) -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn debug_attach_report(args: DebugAttachArgs) -> Result<Value> {
|
fn debug_attach_report(args: DebugAttachArgs) -> Result<Value> {
|
||||||
|
let dap = dap_binary_path()?.display().to_string();
|
||||||
|
debug_attach_report_with_dap(args, dap)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn debug_attach_report_with_dap(args: DebugAttachArgs, dap: String) -> Result<Value> {
|
||||||
|
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 process = args.process.clone();
|
||||||
|
let mut session = JsonLineSession::connect(coordinator)?;
|
||||||
|
let response = session.request(json!({
|
||||||
|
"type": "debug_attach",
|
||||||
|
"tenant": tenant,
|
||||||
|
"project": project,
|
||||||
|
"actor_user": user,
|
||||||
|
"process": process,
|
||||||
|
}))?;
|
||||||
|
let authorization = response.get("authorization").cloned().unwrap_or_else(
|
||||||
|
|| json!({"allowed": false, "reason": "missing authorization response"}),
|
||||||
|
);
|
||||||
|
return Ok(json!({
|
||||||
|
"command": "debug attach",
|
||||||
|
"process": process,
|
||||||
|
"coordinator": coordinator,
|
||||||
|
"tenant": tenant,
|
||||||
|
"project": project,
|
||||||
|
"user": user,
|
||||||
|
"dap": dap,
|
||||||
|
"authorized": authorization
|
||||||
|
.get("allowed")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| json!(false)),
|
||||||
|
"authorization": authorization,
|
||||||
|
"private_website_required": false,
|
||||||
|
"coordinator_session_requests": session.requests(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
Ok(json!({
|
Ok(json!({
|
||||||
"command": "debug attach",
|
"command": "debug attach",
|
||||||
"process": args.process,
|
"process": args.process,
|
||||||
"coordinator": args.scope.coordinator,
|
"coordinator": args.scope.coordinator,
|
||||||
"tenant": args.scope.tenant,
|
"tenant": args.scope.tenant,
|
||||||
"project": args.scope.project,
|
"project": args.scope.project,
|
||||||
"dap": dap_binary_path()?.display().to_string(),
|
"dap": dap,
|
||||||
|
"authorized": "unknown_without_coordinator",
|
||||||
"private_website_required": false,
|
"private_website_required": false,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
@ -4588,6 +4627,54 @@ mod tests {
|
||||||
assert_eq!(suspended["private_website_required"], false);
|
assert_eq!(suspended["private_website_required"], false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn debug_attach_reports_public_authorization() {
|
||||||
|
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":"debug_attach""#));
|
||||||
|
assert!(line.contains(r#""tenant":"tenant""#));
|
||||||
|
assert!(line.contains(r#""project":"project""#));
|
||||||
|
assert!(line.contains(r#""actor_user":"user""#));
|
||||||
|
assert!(line.contains(r#""process":"vp""#));
|
||||||
|
stream
|
||||||
|
.write_all(
|
||||||
|
br#"{"type":"debug_attach","process":"vp","actor":"user","authorization":{"allowed":true,"reason":"debug attach authorized for project"}}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
stream.write_all(b"\n").unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
let report = debug_attach_report_with_dap(
|
||||||
|
DebugAttachArgs {
|
||||||
|
scope: CliScopeArgs {
|
||||||
|
coordinator: Some(addr),
|
||||||
|
tenant: "tenant".to_owned(),
|
||||||
|
project: "project".to_owned(),
|
||||||
|
user: "user".to_owned(),
|
||||||
|
json: false,
|
||||||
|
},
|
||||||
|
process: "vp".to_owned(),
|
||||||
|
},
|
||||||
|
"/tmp/disasmer-debug-dap-test".to_owned(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
server.join().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(report["command"], "debug attach");
|
||||||
|
assert_eq!(report["process"], "vp");
|
||||||
|
assert_eq!(report["authorized"], true);
|
||||||
|
assert_eq!(
|
||||||
|
report["authorization"]["reason"],
|
||||||
|
"debug attach authorized for project"
|
||||||
|
);
|
||||||
|
assert_eq!(report["private_website_required"], false);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn human_report_is_text_not_json() {
|
fn human_report_is_text_not_json() {
|
||||||
let report = json!({
|
let report = json!({
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use std::net::{SocketAddr, TcpListener, TcpStream};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use disasmer_core::{
|
use disasmer_core::{
|
||||||
Actor, AgentId, ArtifactId, ArtifactRegistry, Capability, CapabilityReportError,
|
Actor, AgentId, ArtifactId, ArtifactRegistry, Authorization, Capability, CapabilityReportError,
|
||||||
CredentialKind, DataPlaneObject, DataPlaneScope, DefaultScheduler, Digest,
|
CredentialKind, DataPlaneObject, DataPlaneScope, DefaultScheduler, Digest,
|
||||||
DirectBulkTransferPlan, DownloadLink, DownloadPolicy, EnvironmentRequirements, LimitError,
|
DirectBulkTransferPlan, DownloadLink, DownloadPolicy, EnvironmentRequirements, LimitError,
|
||||||
LimitKind, NativeQuicTransport, NodeCapabilities, NodeDescriptor, NodeEndpoint, NodeId,
|
LimitKind, NativeQuicTransport, NodeCapabilities, NodeDescriptor, NodeEndpoint, NodeId,
|
||||||
|
|
@ -221,6 +221,12 @@ pub enum CoordinatorRequest {
|
||||||
node: String,
|
node: String,
|
||||||
task: String,
|
task: String,
|
||||||
},
|
},
|
||||||
|
DebugAttach {
|
||||||
|
tenant: String,
|
||||||
|
project: String,
|
||||||
|
actor_user: String,
|
||||||
|
process: String,
|
||||||
|
},
|
||||||
PollDebugCommand {
|
PollDebugCommand {
|
||||||
tenant: String,
|
tenant: String,
|
||||||
project: String,
|
project: String,
|
||||||
|
|
@ -557,6 +563,11 @@ pub enum CoordinatorResponse {
|
||||||
task: TaskId,
|
task: TaskId,
|
||||||
command: Option<String>,
|
command: Option<String>,
|
||||||
},
|
},
|
||||||
|
DebugAttach {
|
||||||
|
process: ProcessId,
|
||||||
|
actor: UserId,
|
||||||
|
authorization: Authorization,
|
||||||
|
},
|
||||||
TaskLogRecorded {
|
TaskLogRecorded {
|
||||||
process: ProcessId,
|
process: ProcessId,
|
||||||
task: TaskId,
|
task: TaskId,
|
||||||
|
|
@ -1560,6 +1571,30 @@ impl CoordinatorService {
|
||||||
cancel_requested,
|
cancel_requested,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
CoordinatorRequest::DebugAttach {
|
||||||
|
tenant,
|
||||||
|
project,
|
||||||
|
actor_user,
|
||||||
|
process,
|
||||||
|
} => {
|
||||||
|
let tenant = TenantId::new(tenant);
|
||||||
|
let project = ProjectId::new(project);
|
||||||
|
let actor = UserId::new(actor_user);
|
||||||
|
let process = ProcessId::new(process);
|
||||||
|
let context = disasmer_core::AuthContext {
|
||||||
|
tenant: tenant.clone(),
|
||||||
|
project: project.clone(),
|
||||||
|
actor: Actor::User(actor.clone()),
|
||||||
|
};
|
||||||
|
self.coordinator
|
||||||
|
.upsert_user(tenant, actor.clone(), CredentialKind::BrowserSession);
|
||||||
|
let authorization = self.coordinator.authorize_debug_attach(&context, &process);
|
||||||
|
Ok(CoordinatorResponse::DebugAttach {
|
||||||
|
process,
|
||||||
|
actor,
|
||||||
|
authorization,
|
||||||
|
})
|
||||||
|
}
|
||||||
CoordinatorRequest::PollDebugCommand {
|
CoordinatorRequest::PollDebugCommand {
|
||||||
tenant,
|
tenant,
|
||||||
project,
|
project,
|
||||||
|
|
@ -3057,6 +3092,74 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn service_authorizes_debug_attach_through_public_api() {
|
||||||
|
let mut service = CoordinatorService::new(7);
|
||||||
|
service
|
||||||
|
.handle_request(CoordinatorRequest::CreateProject {
|
||||||
|
tenant: "tenant".to_owned(),
|
||||||
|
actor_user: "user".to_owned(),
|
||||||
|
project: "project".to_owned(),
|
||||||
|
name: "Demo".to_owned(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
service
|
||||||
|
.handle_request(CoordinatorRequest::StartProcess {
|
||||||
|
tenant: "tenant".to_owned(),
|
||||||
|
project: "project".to_owned(),
|
||||||
|
process: "process".to_owned(),
|
||||||
|
restart: false,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let CoordinatorResponse::DebugAttach {
|
||||||
|
process,
|
||||||
|
actor,
|
||||||
|
authorization,
|
||||||
|
} = service
|
||||||
|
.handle_request(CoordinatorRequest::DebugAttach {
|
||||||
|
tenant: "tenant".to_owned(),
|
||||||
|
project: "project".to_owned(),
|
||||||
|
actor_user: "user".to_owned(),
|
||||||
|
process: "process".to_owned(),
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
else {
|
||||||
|
panic!("expected debug attach authorization");
|
||||||
|
};
|
||||||
|
assert_eq!(process, ProcessId::from("process"));
|
||||||
|
assert_eq!(actor, UserId::from("user"));
|
||||||
|
assert!(authorization.allowed);
|
||||||
|
|
||||||
|
let CoordinatorResponse::DebugAttach { authorization, .. } = service
|
||||||
|
.handle_request(CoordinatorRequest::DebugAttach {
|
||||||
|
tenant: "tenant".to_owned(),
|
||||||
|
project: "project".to_owned(),
|
||||||
|
actor_user: "other-user".to_owned(),
|
||||||
|
process: "process".to_owned(),
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
else {
|
||||||
|
panic!("expected denied debug attach authorization");
|
||||||
|
};
|
||||||
|
assert!(!authorization.allowed);
|
||||||
|
assert!(authorization.reason.contains("explicit project permission"));
|
||||||
|
|
||||||
|
let CoordinatorResponse::DebugAttach { authorization, .. } = service
|
||||||
|
.handle_request(CoordinatorRequest::DebugAttach {
|
||||||
|
tenant: "other-tenant".to_owned(),
|
||||||
|
project: "project".to_owned(),
|
||||||
|
actor_user: "user".to_owned(),
|
||||||
|
process: "process".to_owned(),
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
else {
|
||||||
|
panic!("expected cross-tenant debug attach denial");
|
||||||
|
};
|
||||||
|
assert!(!authorization.allowed);
|
||||||
|
assert!(authorization.reason.contains("tenant or project"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn service_cancels_whole_process_and_blocks_new_task_launches() {
|
fn service_cancels_whole_process_and_blocks_new_task_launches() {
|
||||||
let mut service = CoordinatorService::new(7);
|
let mut service = CoordinatorService::new(7);
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,7 @@ for (const [name, pattern] of [
|
||||||
["CLI key lifecycle coverage", /fn key_lifecycle_reports_project_scoped_agent_credentials\(\)/],
|
["CLI key lifecycle coverage", /fn key_lifecycle_reports_project_scoped_agent_credentials\(\)/],
|
||||||
["CLI node revoke coverage", /fn node_revoke_reports_scoped_credential_revocation\(\)/],
|
["CLI node revoke coverage", /fn node_revoke_reports_scoped_credential_revocation\(\)/],
|
||||||
["CLI admin public API coverage", /fn admin_status_and_suspend_use_public_coordinator_api\(\)/],
|
["CLI admin public API coverage", /fn admin_status_and_suspend_use_public_coordinator_api\(\)/],
|
||||||
|
["CLI debug attach coverage", /fn debug_attach_reports_public_authorization\(\)/],
|
||||||
["doctor unchecked reachability coverage", /fn doctor_reports_unchecked_coordinator_reachability_without_config\(\)/],
|
["doctor unchecked reachability coverage", /fn doctor_reports_unchecked_coordinator_reachability_without_config\(\)/],
|
||||||
["doctor ping reachability coverage", /fn doctor_pings_configured_coordinator\(\)/],
|
["doctor ping reachability coverage", /fn doctor_pings_configured_coordinator\(\)/],
|
||||||
["project local config coverage", /fn project_init_select_and_status_use_local_project_config\(\)/],
|
["project local config coverage", /fn project_init_select_and_status_use_local_project_config\(\)/],
|
||||||
|
|
@ -149,6 +150,11 @@ expect(
|
||||||
"coordinator public admin suspension coverage",
|
"coordinator public admin suspension coverage",
|
||||||
/fn service_reports_and_enforces_public_admin_tenant_suspension\(\)/
|
/fn service_reports_and_enforces_public_admin_tenant_suspension\(\)/
|
||||||
);
|
);
|
||||||
|
expect(
|
||||||
|
coordinator,
|
||||||
|
"coordinator debug attach coverage",
|
||||||
|
/fn service_authorizes_debug_attach_through_public_api\(\)/
|
||||||
|
);
|
||||||
|
|
||||||
for (const [name, pattern] of [
|
for (const [name, pattern] of [
|
||||||
["agent --json flag", /struct AgentEnrollArgs[\s\S]*#\[arg\(long\)\]\s*json: bool/],
|
["agent --json flag", /struct AgentEnrollArgs[\s\S]*#\[arg\(long\)\]\s*json: bool/],
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ for (const variant of [
|
||||||
"CancelTask",
|
"CancelTask",
|
||||||
"CancelProcess",
|
"CancelProcess",
|
||||||
"PollTaskControl",
|
"PollTaskControl",
|
||||||
|
"DebugAttach",
|
||||||
"TaskCompleted",
|
"TaskCompleted",
|
||||||
]) {
|
]) {
|
||||||
assertNoUserSessionCredential(
|
assertNoUserSessionCredential(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue