Audit and meter public debug operations
This commit is contained in:
parent
0b14889c79
commit
28779a2866
5 changed files with 211 additions and 10 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"kind": "disasmer-filtered-public-tree",
|
"kind": "disasmer-filtered-public-tree",
|
||||||
"source_commit": "e50394e956649c00db26cca4307bc244988ccecd",
|
"source_commit": "ed1fbc0c464627dc5cd56d5b01343bd084e04c95",
|
||||||
"release_name": "dryrun-e50394e95664",
|
"release_name": "dryrun-ed1fbc0c4646",
|
||||||
"filtered_out": [
|
"filtered_out": [
|
||||||
"private/**",
|
"private/**",
|
||||||
"experiments/**",
|
"experiments/**",
|
||||||
|
|
|
||||||
|
|
@ -1525,6 +1525,16 @@ fn debug_attach_report_with_dap(args: DebugAttachArgs, dap: String) -> Result<Va
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_else(|| json!(false)),
|
.unwrap_or_else(|| json!(false)),
|
||||||
"authorization": authorization,
|
"authorization": authorization,
|
||||||
|
"audit_event": response.get("audit_event").cloned().unwrap_or(Value::Null),
|
||||||
|
"charged_debug_read_bytes": response
|
||||||
|
.get("charged_debug_read_bytes")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| json!(0)),
|
||||||
|
"used_debug_read_bytes": response
|
||||||
|
.get("used_debug_read_bytes")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| json!(0)),
|
||||||
|
"debug_reads_quota_limited": true,
|
||||||
"private_website_required": false,
|
"private_website_required": false,
|
||||||
"coordinator_session_requests": session.requests(),
|
"coordinator_session_requests": session.requests(),
|
||||||
}));
|
}));
|
||||||
|
|
@ -1537,6 +1547,7 @@ fn debug_attach_report_with_dap(args: DebugAttachArgs, dap: String) -> Result<Va
|
||||||
"project": args.scope.project,
|
"project": args.scope.project,
|
||||||
"dap": dap,
|
"dap": dap,
|
||||||
"authorized": "unknown_without_coordinator",
|
"authorized": "unknown_without_coordinator",
|
||||||
|
"debug_reads_quota_limited": "unknown_without_coordinator",
|
||||||
"private_website_required": false,
|
"private_website_required": false,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
@ -2688,6 +2699,16 @@ fn task_restart_request_summary(response: &Value, requires_confirmation: bool) -
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_else(|| json!(true)),
|
.unwrap_or_else(|| json!(true)),
|
||||||
"message": response.get("message").cloned().unwrap_or(Value::Null),
|
"message": response.get("message").cloned().unwrap_or(Value::Null),
|
||||||
|
"audit_event": response.get("audit_event").cloned().unwrap_or(Value::Null),
|
||||||
|
"charged_debug_read_bytes": response
|
||||||
|
.get("charged_debug_read_bytes")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| json!(0)),
|
||||||
|
"used_debug_read_bytes": response
|
||||||
|
.get("used_debug_read_bytes")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| json!(0)),
|
||||||
|
"debug_reads_quota_limited": true,
|
||||||
"website_required": false,
|
"website_required": false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -4744,7 +4765,7 @@ mod tests {
|
||||||
assert!(line.contains(r#""process":"vp""#));
|
assert!(line.contains(r#""process":"vp""#));
|
||||||
stream
|
stream
|
||||||
.write_all(
|
.write_all(
|
||||||
br#"{"type":"debug_attach","process":"vp","actor":"user","authorization":{"allowed":true,"reason":"debug attach authorized for project"}}"#,
|
br#"{"type":"debug_attach","process":"vp","actor":"user","authorization":{"allowed":true,"reason":"debug attach authorized for project"},"audit_event":{"tenant":"tenant","project":"project","process":"vp","task":null,"actor":"user","operation":"debug_attach","allowed":true,"reason":"debug attach authorized for project","charged_debug_read_bytes":1024,"used_debug_read_bytes":1024},"charged_debug_read_bytes":1024,"used_debug_read_bytes":1024}"#,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
stream.write_all(b"\n").unwrap();
|
stream.write_all(b"\n").unwrap();
|
||||||
|
|
@ -4773,6 +4794,10 @@ mod tests {
|
||||||
report["authorization"]["reason"],
|
report["authorization"]["reason"],
|
||||||
"debug attach authorized for project"
|
"debug attach authorized for project"
|
||||||
);
|
);
|
||||||
|
assert_eq!(report["audit_event"]["operation"], "debug_attach");
|
||||||
|
assert_eq!(report["charged_debug_read_bytes"], 1024);
|
||||||
|
assert_eq!(report["used_debug_read_bytes"], 1024);
|
||||||
|
assert_eq!(report["debug_reads_quota_limited"], true);
|
||||||
assert_eq!(report["private_website_required"], false);
|
assert_eq!(report["private_website_required"], false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5309,7 +5334,7 @@ mod tests {
|
||||||
assert!(line.contains(r#""task":"compile-linux""#));
|
assert!(line.contains(r#""task":"compile-linux""#));
|
||||||
stream
|
stream
|
||||||
.write_all(
|
.write_all(
|
||||||
br#"{"type":"task_restart","process":"vp","task":"compile-linux","actor":"user","accepted":false,"clean_boundary_available":false,"active_task":true,"completed_event_observed":false,"requires_whole_process_restart":true,"message":"selected task is still active; clean task restart requires a captured checkpoint boundary"}"#,
|
br#"{"type":"task_restart","process":"vp","task":"compile-linux","actor":"user","accepted":false,"clean_boundary_available":false,"active_task":true,"completed_event_observed":false,"requires_whole_process_restart":true,"message":"selected task is still active; clean task restart requires a captured checkpoint boundary","audit_event":{"tenant":"tenant","project":"project","process":"vp","task":"compile-linux","actor":"user","operation":"restart_task","allowed":true,"reason":"selected task is still active; clean task restart requires a captured checkpoint boundary","charged_debug_read_bytes":1024,"used_debug_read_bytes":1024},"charged_debug_read_bytes":1024,"used_debug_read_bytes":1024}"#,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
stream.write_all(b"\n").unwrap();
|
stream.write_all(b"\n").unwrap();
|
||||||
|
|
@ -5342,6 +5367,13 @@ mod tests {
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
assert_eq!(report["restart_request"]["active_task"], true);
|
assert_eq!(report["restart_request"]["active_task"], true);
|
||||||
|
assert_eq!(
|
||||||
|
report["restart_request"]["audit_event"]["operation"],
|
||||||
|
"restart_task"
|
||||||
|
);
|
||||||
|
assert_eq!(report["restart_request"]["charged_debug_read_bytes"], 1024);
|
||||||
|
assert_eq!(report["restart_request"]["used_debug_read_bytes"], 1024);
|
||||||
|
assert_eq!(report["restart_request"]["debug_reads_quota_limited"], true);
|
||||||
assert_eq!(report["restart_request"]["website_required"], false);
|
assert_eq!(report["restart_request"]["website_required"], false);
|
||||||
assert_eq!(report["coordinator_session_requests"], 1);
|
assert_eq!(report["coordinator_session_requests"], 1);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ pub use postgres_store::{
|
||||||
};
|
};
|
||||||
pub use service::{
|
pub use service::{
|
||||||
CoordinatorRequest, CoordinatorResponse, CoordinatorService, CoordinatorServiceError,
|
CoordinatorRequest, CoordinatorResponse, CoordinatorService, CoordinatorServiceError,
|
||||||
SourcePreparationDisposition, SourcePreparationStatus, TaskAssignment, TaskCancellationTarget,
|
DebugAuditEvent, SourcePreparationDisposition, SourcePreparationStatus, TaskAssignment,
|
||||||
TaskCompletionEvent, TaskTerminalState,
|
TaskCancellationTarget, TaskCompletionEvent, TaskTerminalState,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_TASK_LOG_TAIL_BYTES: usize = 256 * 1024;
|
const MAX_TASK_LOG_TAIL_BYTES: usize = 256 * 1024;
|
||||||
|
const DEBUG_CONTROL_READ_BYTES: u64 = 1024;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(tag = "type", rename_all = "snake_case")]
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
|
|
@ -411,6 +412,20 @@ pub struct TaskCompletionEvent {
|
||||||
pub artifact_size_bytes: Option<u64>,
|
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)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct TaskAssignment {
|
pub struct TaskAssignment {
|
||||||
pub tenant: TenantId,
|
pub tenant: TenantId,
|
||||||
|
|
@ -575,6 +590,9 @@ pub enum CoordinatorResponse {
|
||||||
completed_event_observed: bool,
|
completed_event_observed: bool,
|
||||||
requires_whole_process_restart: bool,
|
requires_whole_process_restart: bool,
|
||||||
message: String,
|
message: String,
|
||||||
|
audit_event: DebugAuditEvent,
|
||||||
|
charged_debug_read_bytes: u64,
|
||||||
|
used_debug_read_bytes: u64,
|
||||||
},
|
},
|
||||||
DebugCommand {
|
DebugCommand {
|
||||||
process: ProcessId,
|
process: ProcessId,
|
||||||
|
|
@ -585,6 +603,9 @@ pub enum CoordinatorResponse {
|
||||||
process: ProcessId,
|
process: ProcessId,
|
||||||
actor: UserId,
|
actor: UserId,
|
||||||
authorization: Authorization,
|
authorization: Authorization,
|
||||||
|
audit_event: DebugAuditEvent,
|
||||||
|
charged_debug_read_bytes: u64,
|
||||||
|
used_debug_read_bytes: u64,
|
||||||
},
|
},
|
||||||
TaskLogRecorded {
|
TaskLogRecorded {
|
||||||
process: ProcessId,
|
process: ProcessId,
|
||||||
|
|
@ -670,6 +691,7 @@ pub struct CoordinatorService {
|
||||||
node_descriptors: BTreeMap<NodeId, NodeDescriptor>,
|
node_descriptors: BTreeMap<NodeId, NodeDescriptor>,
|
||||||
enrollment_grants: BTreeMap<EnrollmentGrantKey, disasmer_core::EnrollmentGrant>,
|
enrollment_grants: BTreeMap<EnrollmentGrantKey, disasmer_core::EnrollmentGrant>,
|
||||||
task_events: Vec<TaskCompletionEvent>,
|
task_events: Vec<TaskCompletionEvent>,
|
||||||
|
debug_audit_events: Vec<DebugAuditEvent>,
|
||||||
task_assignments: BTreeMap<TaskAssignmentKey, VecDeque<TaskAssignment>>,
|
task_assignments: BTreeMap<TaskAssignmentKey, VecDeque<TaskAssignment>>,
|
||||||
active_tasks: BTreeSet<TaskControlKey>,
|
active_tasks: BTreeSet<TaskControlKey>,
|
||||||
task_cancellations: BTreeSet<TaskControlKey>,
|
task_cancellations: BTreeSet<TaskControlKey>,
|
||||||
|
|
@ -683,6 +705,8 @@ pub struct CoordinatorService {
|
||||||
rendezvous_meter: ResourceMeter,
|
rendezvous_meter: ResourceMeter,
|
||||||
download_limits: ResourceLimits,
|
download_limits: ResourceLimits,
|
||||||
download_meter: ResourceMeter,
|
download_meter: ResourceMeter,
|
||||||
|
debug_limits: ResourceLimits,
|
||||||
|
debug_meter: ResourceMeter,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CoordinatorService {
|
impl CoordinatorService {
|
||||||
|
|
@ -695,6 +719,7 @@ impl CoordinatorService {
|
||||||
node_descriptors: BTreeMap::new(),
|
node_descriptors: BTreeMap::new(),
|
||||||
enrollment_grants: BTreeMap::new(),
|
enrollment_grants: BTreeMap::new(),
|
||||||
task_events: Vec::new(),
|
task_events: Vec::new(),
|
||||||
|
debug_audit_events: Vec::new(),
|
||||||
task_assignments: BTreeMap::new(),
|
task_assignments: BTreeMap::new(),
|
||||||
active_tasks: BTreeSet::new(),
|
active_tasks: BTreeSet::new(),
|
||||||
task_cancellations: BTreeSet::new(),
|
task_cancellations: BTreeSet::new(),
|
||||||
|
|
@ -708,6 +733,8 @@ impl CoordinatorService {
|
||||||
rendezvous_meter: ResourceMeter::default(),
|
rendezvous_meter: ResourceMeter::default(),
|
||||||
download_limits: ResourceLimits::community_tier_defaults(),
|
download_limits: ResourceLimits::community_tier_defaults(),
|
||||||
download_meter: ResourceMeter::default(),
|
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);
|
let authorization = self.coordinator.authorize_debug_attach(&context, &process);
|
||||||
if !authorization.allowed {
|
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!(
|
return Err(CoordinatorError::Unauthorized(format!(
|
||||||
"task restart denied: {}",
|
"task restart denied: {}",
|
||||||
authorization.reason
|
authorization.reason
|
||||||
|
|
@ -1643,6 +1680,16 @@ impl CoordinatorService {
|
||||||
} else {
|
} else {
|
||||||
"selected task is not known in the active process; restart the whole virtual process or inspect task list"
|
"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 {
|
Ok(CoordinatorResponse::TaskRestart {
|
||||||
process,
|
process,
|
||||||
task,
|
task,
|
||||||
|
|
@ -1653,6 +1700,9 @@ impl CoordinatorService {
|
||||||
completed_event_observed,
|
completed_event_observed,
|
||||||
requires_whole_process_restart: true,
|
requires_whole_process_restart: true,
|
||||||
message: message.to_owned(),
|
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 {
|
CoordinatorRequest::DebugAttach {
|
||||||
|
|
@ -1670,13 +1720,29 @@ impl CoordinatorService {
|
||||||
project: project.clone(),
|
project: project.clone(),
|
||||||
actor: Actor::User(actor.clone()),
|
actor: Actor::User(actor.clone()),
|
||||||
};
|
};
|
||||||
self.coordinator
|
self.coordinator.upsert_user(
|
||||||
.upsert_user(tenant, actor.clone(), CredentialKind::BrowserSession);
|
tenant.clone(),
|
||||||
|
actor.clone(),
|
||||||
|
CredentialKind::BrowserSession,
|
||||||
|
);
|
||||||
let authorization = self.coordinator.authorize_debug_attach(&context, &process);
|
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 {
|
Ok(CoordinatorResponse::DebugAttach {
|
||||||
process,
|
process,
|
||||||
actor,
|
actor,
|
||||||
authorization,
|
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 {
|
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(
|
fn export_endpoint(
|
||||||
&self,
|
&self,
|
||||||
node: &NodeId,
|
node: &NodeId,
|
||||||
|
|
@ -3200,6 +3303,9 @@ mod tests {
|
||||||
process,
|
process,
|
||||||
actor,
|
actor,
|
||||||
authorization,
|
authorization,
|
||||||
|
audit_event,
|
||||||
|
charged_debug_read_bytes,
|
||||||
|
used_debug_read_bytes,
|
||||||
} = service
|
} = service
|
||||||
.handle_request(CoordinatorRequest::DebugAttach {
|
.handle_request(CoordinatorRequest::DebugAttach {
|
||||||
tenant: "tenant".to_owned(),
|
tenant: "tenant".to_owned(),
|
||||||
|
|
@ -3214,8 +3320,19 @@ mod tests {
|
||||||
assert_eq!(process, ProcessId::from("process"));
|
assert_eq!(process, ProcessId::from("process"));
|
||||||
assert_eq!(actor, UserId::from("user"));
|
assert_eq!(actor, UserId::from("user"));
|
||||||
assert!(authorization.allowed);
|
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 {
|
.handle_request(CoordinatorRequest::DebugAttach {
|
||||||
tenant: "tenant".to_owned(),
|
tenant: "tenant".to_owned(),
|
||||||
project: "project".to_owned(),
|
project: "project".to_owned(),
|
||||||
|
|
@ -3228,8 +3345,18 @@ mod tests {
|
||||||
};
|
};
|
||||||
assert!(!authorization.allowed);
|
assert!(!authorization.allowed);
|
||||||
assert!(authorization.reason.contains("explicit project permission"));
|
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 {
|
.handle_request(CoordinatorRequest::DebugAttach {
|
||||||
tenant: "other-tenant".to_owned(),
|
tenant: "other-tenant".to_owned(),
|
||||||
project: "project".to_owned(),
|
project: "project".to_owned(),
|
||||||
|
|
@ -3242,6 +3369,10 @@ mod tests {
|
||||||
};
|
};
|
||||||
assert!(!authorization.allowed);
|
assert!(!authorization.allowed);
|
||||||
assert!(authorization.reason.contains("tenant or project"));
|
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]
|
#[test]
|
||||||
|
|
@ -3282,6 +3413,9 @@ mod tests {
|
||||||
completed_event_observed,
|
completed_event_observed,
|
||||||
requires_whole_process_restart,
|
requires_whole_process_restart,
|
||||||
message,
|
message,
|
||||||
|
audit_event,
|
||||||
|
charged_debug_read_bytes,
|
||||||
|
used_debug_read_bytes,
|
||||||
..
|
..
|
||||||
} = service
|
} = service
|
||||||
.handle_request(CoordinatorRequest::RestartTask {
|
.handle_request(CoordinatorRequest::RestartTask {
|
||||||
|
|
@ -3301,6 +3435,11 @@ mod tests {
|
||||||
assert!(!completed_event_observed);
|
assert!(!completed_event_observed);
|
||||||
assert!(requires_whole_process_restart);
|
assert!(requires_whole_process_restart);
|
||||||
assert!(message.contains("not known"));
|
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
|
service
|
||||||
.handle_request(CoordinatorRequest::AttachNode {
|
.handle_request(CoordinatorRequest::AttachNode {
|
||||||
|
|
@ -3349,6 +3488,8 @@ mod tests {
|
||||||
active_task,
|
active_task,
|
||||||
completed_event_observed,
|
completed_event_observed,
|
||||||
message,
|
message,
|
||||||
|
audit_event,
|
||||||
|
used_debug_read_bytes,
|
||||||
..
|
..
|
||||||
} = service
|
} = service
|
||||||
.handle_request(CoordinatorRequest::RestartTask {
|
.handle_request(CoordinatorRequest::RestartTask {
|
||||||
|
|
@ -3365,6 +3506,11 @@ mod tests {
|
||||||
assert!(active_task);
|
assert!(active_task);
|
||||||
assert!(!completed_event_observed);
|
assert!(!completed_event_observed);
|
||||||
assert!(message.contains("still active"));
|
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
|
service
|
||||||
.handle_request(CoordinatorRequest::TaskCompleted {
|
.handle_request(CoordinatorRequest::TaskCompleted {
|
||||||
|
|
@ -3391,6 +3537,8 @@ mod tests {
|
||||||
active_task,
|
active_task,
|
||||||
completed_event_observed,
|
completed_event_observed,
|
||||||
message,
|
message,
|
||||||
|
audit_event,
|
||||||
|
used_debug_read_bytes,
|
||||||
..
|
..
|
||||||
} = service
|
} = service
|
||||||
.handle_request(CoordinatorRequest::RestartTask {
|
.handle_request(CoordinatorRequest::RestartTask {
|
||||||
|
|
@ -3407,6 +3555,12 @@ mod tests {
|
||||||
assert!(!active_task);
|
assert!(!active_task);
|
||||||
assert!(completed_event_observed);
|
assert!(completed_event_observed);
|
||||||
assert!(message.contains("terminal event metadata"));
|
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]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,21 @@ expect(
|
||||||
"coordinator task restart boundary coverage",
|
"coordinator task restart boundary coverage",
|
||||||
/fn service_reports_task_restart_boundary_through_public_api\(\)/
|
/fn service_reports_task_restart_boundary_through_public_api\(\)/
|
||||||
);
|
);
|
||||||
|
expect(
|
||||||
|
coordinator,
|
||||||
|
"public debug operation audit event",
|
||||||
|
/pub struct DebugAuditEvent[\s\S]*charged_debug_read_bytes[\s\S]*used_debug_read_bytes/
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
coordinator,
|
||||||
|
"public debug operation metering",
|
||||||
|
/record_debug_audit_event\([\s\S]*LimitKind::DebugReadBytes[\s\S]*DEBUG_CONTROL_READ_BYTES/
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
cli,
|
||||||
|
"CLI surfaces debug audit and quota fields",
|
||||||
|
/debug_reads_quota_limited[\s\S]*charged_debug_read_bytes[\s\S]*used_debug_read_bytes/
|
||||||
|
);
|
||||||
|
|
||||||
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/],
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue