Audit and meter public debug operations
This commit is contained in:
parent
0b14889c79
commit
28779a2866
5 changed files with 211 additions and 10 deletions
|
|
@ -15,8 +15,8 @@ pub use postgres_store::{
|
|||
};
|
||||
pub use service::{
|
||||
CoordinatorRequest, CoordinatorResponse, CoordinatorService, CoordinatorServiceError,
|
||||
SourcePreparationDisposition, SourcePreparationStatus, TaskAssignment, TaskCancellationTarget,
|
||||
TaskCompletionEvent, TaskTerminalState,
|
||||
DebugAuditEvent, SourcePreparationDisposition, SourcePreparationStatus, TaskAssignment,
|
||||
TaskCancellationTarget, TaskCompletionEvent, TaskTerminalState,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ use crate::{
|
|||
};
|
||||
|
||||
const MAX_TASK_LOG_TAIL_BYTES: usize = 256 * 1024;
|
||||
const DEBUG_CONTROL_READ_BYTES: u64 = 1024;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
|
|
@ -411,6 +412,20 @@ pub struct TaskCompletionEvent {
|
|||
pub artifact_size_bytes: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct DebugAuditEvent {
|
||||
pub tenant: TenantId,
|
||||
pub project: ProjectId,
|
||||
pub process: ProcessId,
|
||||
pub task: Option<TaskId>,
|
||||
pub actor: UserId,
|
||||
pub operation: String,
|
||||
pub allowed: bool,
|
||||
pub reason: String,
|
||||
pub charged_debug_read_bytes: u64,
|
||||
pub used_debug_read_bytes: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct TaskAssignment {
|
||||
pub tenant: TenantId,
|
||||
|
|
@ -575,6 +590,9 @@ pub enum CoordinatorResponse {
|
|||
completed_event_observed: bool,
|
||||
requires_whole_process_restart: bool,
|
||||
message: String,
|
||||
audit_event: DebugAuditEvent,
|
||||
charged_debug_read_bytes: u64,
|
||||
used_debug_read_bytes: u64,
|
||||
},
|
||||
DebugCommand {
|
||||
process: ProcessId,
|
||||
|
|
@ -585,6 +603,9 @@ pub enum CoordinatorResponse {
|
|||
process: ProcessId,
|
||||
actor: UserId,
|
||||
authorization: Authorization,
|
||||
audit_event: DebugAuditEvent,
|
||||
charged_debug_read_bytes: u64,
|
||||
used_debug_read_bytes: u64,
|
||||
},
|
||||
TaskLogRecorded {
|
||||
process: ProcessId,
|
||||
|
|
@ -670,6 +691,7 @@ pub struct CoordinatorService {
|
|||
node_descriptors: BTreeMap<NodeId, NodeDescriptor>,
|
||||
enrollment_grants: BTreeMap<EnrollmentGrantKey, disasmer_core::EnrollmentGrant>,
|
||||
task_events: Vec<TaskCompletionEvent>,
|
||||
debug_audit_events: Vec<DebugAuditEvent>,
|
||||
task_assignments: BTreeMap<TaskAssignmentKey, VecDeque<TaskAssignment>>,
|
||||
active_tasks: BTreeSet<TaskControlKey>,
|
||||
task_cancellations: BTreeSet<TaskControlKey>,
|
||||
|
|
@ -683,6 +705,8 @@ pub struct CoordinatorService {
|
|||
rendezvous_meter: ResourceMeter,
|
||||
download_limits: ResourceLimits,
|
||||
download_meter: ResourceMeter,
|
||||
debug_limits: ResourceLimits,
|
||||
debug_meter: ResourceMeter,
|
||||
}
|
||||
|
||||
impl CoordinatorService {
|
||||
|
|
@ -695,6 +719,7 @@ impl CoordinatorService {
|
|||
node_descriptors: BTreeMap::new(),
|
||||
enrollment_grants: BTreeMap::new(),
|
||||
task_events: Vec::new(),
|
||||
debug_audit_events: Vec::new(),
|
||||
task_assignments: BTreeMap::new(),
|
||||
active_tasks: BTreeSet::new(),
|
||||
task_cancellations: BTreeSet::new(),
|
||||
|
|
@ -708,6 +733,8 @@ impl CoordinatorService {
|
|||
rendezvous_meter: ResourceMeter::default(),
|
||||
download_limits: ResourceLimits::community_tier_defaults(),
|
||||
download_meter: ResourceMeter::default(),
|
||||
debug_limits: ResourceLimits::community_tier_defaults(),
|
||||
debug_meter: ResourceMeter::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1613,6 +1640,16 @@ impl CoordinatorService {
|
|||
);
|
||||
let authorization = self.coordinator.authorize_debug_attach(&context, &process);
|
||||
if !authorization.allowed {
|
||||
let _ = self.record_debug_audit_event(
|
||||
tenant,
|
||||
project,
|
||||
process,
|
||||
Some(task),
|
||||
actor,
|
||||
"restart_task",
|
||||
false,
|
||||
authorization.reason.clone(),
|
||||
)?;
|
||||
return Err(CoordinatorError::Unauthorized(format!(
|
||||
"task restart denied: {}",
|
||||
authorization.reason
|
||||
|
|
@ -1643,6 +1680,16 @@ impl CoordinatorService {
|
|||
} else {
|
||||
"selected task is not known in the active process; restart the whole virtual process or inspect task list"
|
||||
};
|
||||
let audit_event = self.record_debug_audit_event(
|
||||
tenant,
|
||||
project,
|
||||
process.clone(),
|
||||
Some(task.clone()),
|
||||
actor.clone(),
|
||||
"restart_task",
|
||||
true,
|
||||
message,
|
||||
)?;
|
||||
Ok(CoordinatorResponse::TaskRestart {
|
||||
process,
|
||||
task,
|
||||
|
|
@ -1653,6 +1700,9 @@ impl CoordinatorService {
|
|||
completed_event_observed,
|
||||
requires_whole_process_restart: true,
|
||||
message: message.to_owned(),
|
||||
charged_debug_read_bytes: audit_event.charged_debug_read_bytes,
|
||||
used_debug_read_bytes: audit_event.used_debug_read_bytes,
|
||||
audit_event,
|
||||
})
|
||||
}
|
||||
CoordinatorRequest::DebugAttach {
|
||||
|
|
@ -1670,13 +1720,29 @@ impl CoordinatorService {
|
|||
project: project.clone(),
|
||||
actor: Actor::User(actor.clone()),
|
||||
};
|
||||
self.coordinator
|
||||
.upsert_user(tenant, actor.clone(), CredentialKind::BrowserSession);
|
||||
self.coordinator.upsert_user(
|
||||
tenant.clone(),
|
||||
actor.clone(),
|
||||
CredentialKind::BrowserSession,
|
||||
);
|
||||
let authorization = self.coordinator.authorize_debug_attach(&context, &process);
|
||||
let audit_event = self.record_debug_audit_event(
|
||||
tenant,
|
||||
project,
|
||||
process.clone(),
|
||||
None,
|
||||
actor.clone(),
|
||||
"debug_attach",
|
||||
authorization.allowed,
|
||||
authorization.reason.clone(),
|
||||
)?;
|
||||
Ok(CoordinatorResponse::DebugAttach {
|
||||
process,
|
||||
actor,
|
||||
authorization,
|
||||
charged_debug_read_bytes: audit_event.charged_debug_read_bytes,
|
||||
used_debug_read_bytes: audit_event.used_debug_read_bytes,
|
||||
audit_event,
|
||||
})
|
||||
}
|
||||
CoordinatorRequest::PollDebugCommand {
|
||||
|
|
@ -2142,6 +2208,43 @@ impl CoordinatorService {
|
|||
}
|
||||
}
|
||||
|
||||
fn record_debug_audit_event(
|
||||
&mut self,
|
||||
tenant: TenantId,
|
||||
project: ProjectId,
|
||||
process: ProcessId,
|
||||
task: Option<TaskId>,
|
||||
actor: UserId,
|
||||
operation: &'static str,
|
||||
allowed: bool,
|
||||
reason: impl Into<String>,
|
||||
) -> Result<DebugAuditEvent, CoordinatorServiceError> {
|
||||
let charged_debug_read_bytes = if allowed {
|
||||
self.debug_meter.charge(
|
||||
&self.debug_limits,
|
||||
LimitKind::DebugReadBytes,
|
||||
DEBUG_CONTROL_READ_BYTES,
|
||||
)?;
|
||||
DEBUG_CONTROL_READ_BYTES
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let event = DebugAuditEvent {
|
||||
tenant,
|
||||
project,
|
||||
process,
|
||||
task,
|
||||
actor,
|
||||
operation: operation.to_owned(),
|
||||
allowed,
|
||||
reason: reason.into(),
|
||||
charged_debug_read_bytes,
|
||||
used_debug_read_bytes: self.debug_meter.used(&LimitKind::DebugReadBytes),
|
||||
};
|
||||
self.debug_audit_events.push(event.clone());
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
fn export_endpoint(
|
||||
&self,
|
||||
node: &NodeId,
|
||||
|
|
@ -3200,6 +3303,9 @@ mod tests {
|
|||
process,
|
||||
actor,
|
||||
authorization,
|
||||
audit_event,
|
||||
charged_debug_read_bytes,
|
||||
used_debug_read_bytes,
|
||||
} = service
|
||||
.handle_request(CoordinatorRequest::DebugAttach {
|
||||
tenant: "tenant".to_owned(),
|
||||
|
|
@ -3214,8 +3320,19 @@ mod tests {
|
|||
assert_eq!(process, ProcessId::from("process"));
|
||||
assert_eq!(actor, UserId::from("user"));
|
||||
assert!(authorization.allowed);
|
||||
assert_eq!(audit_event.operation, "debug_attach");
|
||||
assert_eq!(audit_event.actor, UserId::from("user"));
|
||||
assert!(audit_event.allowed);
|
||||
assert_eq!(charged_debug_read_bytes, DEBUG_CONTROL_READ_BYTES);
|
||||
assert_eq!(used_debug_read_bytes, DEBUG_CONTROL_READ_BYTES);
|
||||
|
||||
let CoordinatorResponse::DebugAttach { authorization, .. } = service
|
||||
let CoordinatorResponse::DebugAttach {
|
||||
authorization,
|
||||
audit_event,
|
||||
charged_debug_read_bytes,
|
||||
used_debug_read_bytes,
|
||||
..
|
||||
} = service
|
||||
.handle_request(CoordinatorRequest::DebugAttach {
|
||||
tenant: "tenant".to_owned(),
|
||||
project: "project".to_owned(),
|
||||
|
|
@ -3228,8 +3345,18 @@ mod tests {
|
|||
};
|
||||
assert!(!authorization.allowed);
|
||||
assert!(authorization.reason.contains("explicit project permission"));
|
||||
assert!(!audit_event.allowed);
|
||||
assert_eq!(audit_event.charged_debug_read_bytes, 0);
|
||||
assert_eq!(charged_debug_read_bytes, 0);
|
||||
assert_eq!(used_debug_read_bytes, DEBUG_CONTROL_READ_BYTES);
|
||||
|
||||
let CoordinatorResponse::DebugAttach { authorization, .. } = service
|
||||
let CoordinatorResponse::DebugAttach {
|
||||
authorization,
|
||||
audit_event,
|
||||
charged_debug_read_bytes,
|
||||
used_debug_read_bytes,
|
||||
..
|
||||
} = service
|
||||
.handle_request(CoordinatorRequest::DebugAttach {
|
||||
tenant: "other-tenant".to_owned(),
|
||||
project: "project".to_owned(),
|
||||
|
|
@ -3242,6 +3369,10 @@ mod tests {
|
|||
};
|
||||
assert!(!authorization.allowed);
|
||||
assert!(authorization.reason.contains("tenant or project"));
|
||||
assert!(!audit_event.allowed);
|
||||
assert_eq!(charged_debug_read_bytes, 0);
|
||||
assert_eq!(used_debug_read_bytes, DEBUG_CONTROL_READ_BYTES);
|
||||
assert_eq!(service.debug_audit_events.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -3282,6 +3413,9 @@ mod tests {
|
|||
completed_event_observed,
|
||||
requires_whole_process_restart,
|
||||
message,
|
||||
audit_event,
|
||||
charged_debug_read_bytes,
|
||||
used_debug_read_bytes,
|
||||
..
|
||||
} = service
|
||||
.handle_request(CoordinatorRequest::RestartTask {
|
||||
|
|
@ -3301,6 +3435,11 @@ mod tests {
|
|||
assert!(!completed_event_observed);
|
||||
assert!(requires_whole_process_restart);
|
||||
assert!(message.contains("not known"));
|
||||
assert_eq!(audit_event.operation, "restart_task");
|
||||
assert_eq!(audit_event.task, Some(TaskId::from("task")));
|
||||
assert!(audit_event.allowed);
|
||||
assert_eq!(charged_debug_read_bytes, DEBUG_CONTROL_READ_BYTES);
|
||||
assert_eq!(used_debug_read_bytes, DEBUG_CONTROL_READ_BYTES);
|
||||
|
||||
service
|
||||
.handle_request(CoordinatorRequest::AttachNode {
|
||||
|
|
@ -3349,6 +3488,8 @@ mod tests {
|
|||
active_task,
|
||||
completed_event_observed,
|
||||
message,
|
||||
audit_event,
|
||||
used_debug_read_bytes,
|
||||
..
|
||||
} = service
|
||||
.handle_request(CoordinatorRequest::RestartTask {
|
||||
|
|
@ -3365,6 +3506,11 @@ mod tests {
|
|||
assert!(active_task);
|
||||
assert!(!completed_event_observed);
|
||||
assert!(message.contains("still active"));
|
||||
assert_eq!(
|
||||
audit_event.charged_debug_read_bytes,
|
||||
DEBUG_CONTROL_READ_BYTES
|
||||
);
|
||||
assert_eq!(used_debug_read_bytes, DEBUG_CONTROL_READ_BYTES * 2);
|
||||
|
||||
service
|
||||
.handle_request(CoordinatorRequest::TaskCompleted {
|
||||
|
|
@ -3391,6 +3537,8 @@ mod tests {
|
|||
active_task,
|
||||
completed_event_observed,
|
||||
message,
|
||||
audit_event,
|
||||
used_debug_read_bytes,
|
||||
..
|
||||
} = service
|
||||
.handle_request(CoordinatorRequest::RestartTask {
|
||||
|
|
@ -3407,6 +3555,12 @@ mod tests {
|
|||
assert!(!active_task);
|
||||
assert!(completed_event_observed);
|
||||
assert!(message.contains("terminal event metadata"));
|
||||
assert_eq!(
|
||||
audit_event.charged_debug_read_bytes,
|
||||
DEBUG_CONTROL_READ_BYTES
|
||||
);
|
||||
assert_eq!(used_debug_read_bytes, DEBUG_CONTROL_READ_BYTES * 3);
|
||||
assert_eq!(service.debug_audit_events.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue