Implement public debug attach authorization

This commit is contained in:
Michel Paulissen 2026-07-03 21:18:10 +02:00
parent 005b47e4c0
commit f586b48893
5 changed files with 201 additions and 4 deletions

View file

@ -4,7 +4,7 @@ use std::net::{SocketAddr, TcpListener, TcpStream};
use std::sync::{Arc, Mutex};
use disasmer_core::{
Actor, AgentId, ArtifactId, ArtifactRegistry, Capability, CapabilityReportError,
Actor, AgentId, ArtifactId, ArtifactRegistry, Authorization, Capability, CapabilityReportError,
CredentialKind, DataPlaneObject, DataPlaneScope, DefaultScheduler, Digest,
DirectBulkTransferPlan, DownloadLink, DownloadPolicy, EnvironmentRequirements, LimitError,
LimitKind, NativeQuicTransport, NodeCapabilities, NodeDescriptor, NodeEndpoint, NodeId,
@ -221,6 +221,12 @@ pub enum CoordinatorRequest {
node: String,
task: String,
},
DebugAttach {
tenant: String,
project: String,
actor_user: String,
process: String,
},
PollDebugCommand {
tenant: String,
project: String,
@ -557,6 +563,11 @@ pub enum CoordinatorResponse {
task: TaskId,
command: Option<String>,
},
DebugAttach {
process: ProcessId,
actor: UserId,
authorization: Authorization,
},
TaskLogRecorded {
process: ProcessId,
task: TaskId,
@ -1560,6 +1571,30 @@ impl CoordinatorService {
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 {
tenant,
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]
fn service_cancels_whole_process_and_blocks_new_task_launches() {
let mut service = CoordinatorService::new(7);