Source commit: ea887c8f56cd53985a1179b13e5f1b85c485f584 Public tree identity: sha256:b96a97aacdbc8fd4fc2ea20d0e1450280d46db3f24aabe1475e5fbe20e05f255
1803 lines
56 KiB
Rust
1803 lines
56 KiB
Rust
use std::collections::BTreeMap;
|
|
|
|
use clusterflux_core::{
|
|
AgentId, AgentSignedRequest, ArtifactId, Authorization, Capability, CredentialKind,
|
|
DataPlaneObject, DataPlaneScope, Digest, DirectBulkTransferPlan, DownloadLink,
|
|
EnvironmentRequirements, LaunchAttemptId, LimitKind, NodeCapabilities, NodeDescriptor,
|
|
NodeEndpoint, NodeId, NodeSignedRequest, PanelEventKind, PanelState, Placement, ProcessId,
|
|
ProjectId, ResourceLimits, SourcePreparation, SourceProviderKind, TaskBoundaryHandle,
|
|
TaskBoundaryValue, TaskDefinitionId, TaskInstanceId, TaskJoinResult, TaskSpec, TenantId,
|
|
UserId, VfsPath,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
mod responses;
|
|
pub use responses::*;
|
|
|
|
use crate::{AgentPublicKeyRecord, ProjectRecord};
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct TaskReplacementBundle {
|
|
pub bundle_digest: Digest,
|
|
pub wasm_module_base64: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub source_snapshot: Option<Digest>,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum TaskFailureResolution {
|
|
AcceptFailure,
|
|
Cancel,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
|
|
pub enum CoordinatorRequest {
|
|
Ping,
|
|
Authenticated {
|
|
session_secret: String,
|
|
request: AuthenticatedCoordinatorRequest,
|
|
},
|
|
AuthStatus {
|
|
tenant: String,
|
|
project: String,
|
|
actor_user: String,
|
|
},
|
|
AdminStatus {
|
|
tenant: String,
|
|
actor_user: String,
|
|
admin_proof: Digest,
|
|
admin_nonce: String,
|
|
issued_at_epoch_seconds: u64,
|
|
},
|
|
SuspendTenant {
|
|
tenant: String,
|
|
actor_user: String,
|
|
target_tenant: String,
|
|
admin_proof: Digest,
|
|
admin_nonce: String,
|
|
issued_at_epoch_seconds: u64,
|
|
},
|
|
CreateProject {
|
|
tenant: String,
|
|
actor_user: String,
|
|
project: String,
|
|
name: String,
|
|
},
|
|
SelectProject {
|
|
tenant: String,
|
|
actor_user: String,
|
|
project: String,
|
|
},
|
|
ListProjects {
|
|
tenant: String,
|
|
actor_user: String,
|
|
},
|
|
RegisterAgentPublicKey {
|
|
tenant: String,
|
|
project: String,
|
|
user: String,
|
|
agent: String,
|
|
public_key: String,
|
|
},
|
|
ListAgentPublicKeys {
|
|
tenant: String,
|
|
project: String,
|
|
user: String,
|
|
},
|
|
RotateAgentPublicKey {
|
|
tenant: String,
|
|
project: String,
|
|
user: String,
|
|
agent: String,
|
|
public_key: String,
|
|
},
|
|
RevokeAgentPublicKey {
|
|
tenant: String,
|
|
project: String,
|
|
user: String,
|
|
agent: String,
|
|
},
|
|
AttachNode {
|
|
tenant: String,
|
|
project: String,
|
|
node: String,
|
|
public_key: String,
|
|
},
|
|
CreateNodeEnrollmentGrant {
|
|
tenant: String,
|
|
project: String,
|
|
actor_user: String,
|
|
#[serde(default = "default_node_enrollment_ttl_seconds")]
|
|
ttl_seconds: u64,
|
|
},
|
|
ExchangeNodeEnrollmentGrant {
|
|
tenant: String,
|
|
project: String,
|
|
node: String,
|
|
public_key: String,
|
|
enrollment_grant: String,
|
|
},
|
|
NodeHeartbeat {
|
|
tenant: String,
|
|
project: String,
|
|
node: String,
|
|
#[serde(default)]
|
|
node_signature: Option<NodeSignedRequest>,
|
|
},
|
|
SignedNode {
|
|
node: String,
|
|
node_signature: NodeSignedRequest,
|
|
request: Box<CoordinatorRequest>,
|
|
},
|
|
ReportNodeCapabilities {
|
|
tenant: String,
|
|
project: String,
|
|
node: String,
|
|
capabilities: NodeCapabilities,
|
|
cached_environment_digests: Vec<Digest>,
|
|
#[serde(default)]
|
|
dependency_cache_digests: Vec<Digest>,
|
|
source_snapshots: Vec<Digest>,
|
|
artifact_locations: Vec<String>,
|
|
direct_connectivity: bool,
|
|
online: bool,
|
|
},
|
|
ListNodeDescriptors {
|
|
tenant: String,
|
|
project: String,
|
|
actor_user: String,
|
|
},
|
|
RevokeNodeCredential {
|
|
tenant: String,
|
|
project: String,
|
|
actor_user: String,
|
|
node: String,
|
|
},
|
|
ScheduleTask {
|
|
tenant: String,
|
|
project: String,
|
|
environment: Option<EnvironmentRequirements>,
|
|
environment_digest: Option<Digest>,
|
|
required_capabilities: Vec<Capability>,
|
|
#[serde(default)]
|
|
dependency_cache: Option<Digest>,
|
|
source_snapshot: Option<Digest>,
|
|
required_artifacts: Vec<String>,
|
|
prefer_node: Option<String>,
|
|
},
|
|
LaunchTask {
|
|
tenant: String,
|
|
project: String,
|
|
#[serde(default)]
|
|
actor_user: Option<String>,
|
|
#[serde(default)]
|
|
actor_agent: Option<String>,
|
|
#[serde(default)]
|
|
agent_public_key_fingerprint: Option<Digest>,
|
|
#[serde(default)]
|
|
agent_signature: Option<AgentSignedRequest>,
|
|
task_spec: TaskSpec,
|
|
#[serde(default)]
|
|
wait_for_node: bool,
|
|
artifact_path: String,
|
|
wasm_module_base64: String,
|
|
},
|
|
LaunchChildTask {
|
|
tenant: String,
|
|
project: String,
|
|
process: String,
|
|
node: String,
|
|
parent_task: String,
|
|
task_spec: TaskSpec,
|
|
#[serde(default)]
|
|
wait_for_node: bool,
|
|
artifact_path: String,
|
|
wasm_module_base64: String,
|
|
},
|
|
JoinChildTask {
|
|
tenant: String,
|
|
project: String,
|
|
process: String,
|
|
node: String,
|
|
parent_task: String,
|
|
task: String,
|
|
},
|
|
PollTaskAssignment {
|
|
tenant: String,
|
|
project: String,
|
|
node: String,
|
|
},
|
|
PollArtifactTransfer {
|
|
tenant: String,
|
|
project: String,
|
|
node: String,
|
|
},
|
|
UploadArtifactTransferChunk {
|
|
tenant: String,
|
|
project: String,
|
|
node: String,
|
|
transfer_id: String,
|
|
artifact: String,
|
|
offset: u64,
|
|
content_base64: String,
|
|
chunk_digest: Digest,
|
|
eof: bool,
|
|
},
|
|
FailArtifactTransfer {
|
|
tenant: String,
|
|
project: String,
|
|
node: String,
|
|
transfer_id: String,
|
|
artifact: String,
|
|
message: String,
|
|
},
|
|
RequestRendezvous {
|
|
scope: DataPlaneScope,
|
|
source: NodeEndpoint,
|
|
destination: NodeEndpoint,
|
|
direct_connectivity: bool,
|
|
failure_reason: String,
|
|
},
|
|
RequestSourcePreparation {
|
|
tenant: String,
|
|
project: String,
|
|
provider: SourceProviderKind,
|
|
},
|
|
CompleteSourcePreparation {
|
|
tenant: String,
|
|
project: String,
|
|
node: String,
|
|
provider: SourceProviderKind,
|
|
source_snapshot: Digest,
|
|
},
|
|
StartProcess {
|
|
tenant: String,
|
|
project: String,
|
|
#[serde(default)]
|
|
actor_user: Option<String>,
|
|
#[serde(default)]
|
|
actor_agent: Option<String>,
|
|
#[serde(default)]
|
|
agent_public_key_fingerprint: Option<Digest>,
|
|
#[serde(default)]
|
|
agent_signature: Option<AgentSignedRequest>,
|
|
process: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
launch_attempt: Option<String>,
|
|
#[serde(default)]
|
|
restart: bool,
|
|
},
|
|
ReconnectNode {
|
|
tenant: String,
|
|
project: String,
|
|
node: String,
|
|
process: String,
|
|
epoch: u64,
|
|
},
|
|
CancelTask {
|
|
tenant: String,
|
|
project: String,
|
|
process: String,
|
|
node: String,
|
|
task: String,
|
|
},
|
|
CancelProcess {
|
|
tenant: String,
|
|
project: String,
|
|
actor_user: String,
|
|
process: String,
|
|
},
|
|
AbortProcess {
|
|
tenant: String,
|
|
project: String,
|
|
actor_user: String,
|
|
process: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
launch_attempt: Option<String>,
|
|
},
|
|
ListProcesses {
|
|
tenant: String,
|
|
project: String,
|
|
actor_user: String,
|
|
},
|
|
QuotaStatus {
|
|
tenant: String,
|
|
project: String,
|
|
actor_user: String,
|
|
},
|
|
PollTaskControl {
|
|
tenant: String,
|
|
project: String,
|
|
process: String,
|
|
node: String,
|
|
task: String,
|
|
},
|
|
RestartTask {
|
|
tenant: String,
|
|
project: String,
|
|
actor_user: String,
|
|
process: String,
|
|
task: String,
|
|
#[serde(default)]
|
|
replacement_bundle: Option<TaskReplacementBundle>,
|
|
},
|
|
ResolveTaskFailure {
|
|
tenant: String,
|
|
project: String,
|
|
actor_user: String,
|
|
process: String,
|
|
task: String,
|
|
resolution: TaskFailureResolution,
|
|
},
|
|
DebugAttach {
|
|
tenant: String,
|
|
project: String,
|
|
actor_user: String,
|
|
process: String,
|
|
},
|
|
SetDebugBreakpoints {
|
|
tenant: String,
|
|
project: String,
|
|
actor_user: String,
|
|
process: String,
|
|
#[serde(default)]
|
|
revision: u64,
|
|
probe_symbols: Vec<String>,
|
|
},
|
|
InspectDebugBreakpoints {
|
|
tenant: String,
|
|
project: String,
|
|
actor_user: String,
|
|
process: String,
|
|
},
|
|
CreateDebugEpoch {
|
|
tenant: String,
|
|
project: String,
|
|
actor_user: String,
|
|
process: String,
|
|
stopped_task: String,
|
|
reason: String,
|
|
},
|
|
ResumeDebugEpoch {
|
|
tenant: String,
|
|
project: String,
|
|
actor_user: String,
|
|
process: String,
|
|
epoch: u64,
|
|
},
|
|
InspectDebugEpoch {
|
|
tenant: String,
|
|
project: String,
|
|
actor_user: String,
|
|
process: String,
|
|
epoch: u64,
|
|
},
|
|
PollDebugCommand {
|
|
tenant: String,
|
|
project: String,
|
|
process: String,
|
|
node: String,
|
|
task: String,
|
|
},
|
|
ReportDebugState {
|
|
tenant: String,
|
|
project: String,
|
|
process: String,
|
|
node: String,
|
|
task: String,
|
|
epoch: u64,
|
|
state: DebugAcknowledgementState,
|
|
#[serde(default)]
|
|
stack_frames: Vec<String>,
|
|
#[serde(default)]
|
|
local_values: Vec<(String, String)>,
|
|
#[serde(default)]
|
|
task_args: Vec<(String, String)>,
|
|
#[serde(default)]
|
|
handles: Vec<(String, String)>,
|
|
#[serde(default)]
|
|
command_status: Option<String>,
|
|
#[serde(default)]
|
|
recent_output: Vec<String>,
|
|
#[serde(default)]
|
|
message: Option<String>,
|
|
},
|
|
ReportDebugProbeHit {
|
|
tenant: String,
|
|
project: String,
|
|
process: String,
|
|
node: String,
|
|
task: String,
|
|
probe_symbol: String,
|
|
},
|
|
ReportTaskLog {
|
|
tenant: String,
|
|
project: String,
|
|
process: String,
|
|
node: String,
|
|
task: String,
|
|
stdout_bytes: u64,
|
|
stderr_bytes: u64,
|
|
#[serde(default)]
|
|
stdout_tail: String,
|
|
#[serde(default)]
|
|
stderr_tail: String,
|
|
stdout_truncated: bool,
|
|
stderr_truncated: bool,
|
|
backpressured: bool,
|
|
},
|
|
ReportVfsMetadata {
|
|
tenant: String,
|
|
project: String,
|
|
process: String,
|
|
node: String,
|
|
task: String,
|
|
artifact_path: Option<String>,
|
|
artifact_digest: Option<Digest>,
|
|
artifact_size_bytes: Option<u64>,
|
|
large_bytes_uploaded: bool,
|
|
},
|
|
TaskCompleted {
|
|
tenant: String,
|
|
project: String,
|
|
process: String,
|
|
node: String,
|
|
task: String,
|
|
#[serde(default)]
|
|
terminal_state: Option<TaskTerminalState>,
|
|
status_code: Option<i32>,
|
|
stdout_bytes: u64,
|
|
stderr_bytes: u64,
|
|
#[serde(default)]
|
|
stdout_tail: String,
|
|
#[serde(default)]
|
|
stderr_tail: String,
|
|
#[serde(default)]
|
|
stdout_truncated: bool,
|
|
#[serde(default)]
|
|
stderr_truncated: bool,
|
|
artifact_path: Option<String>,
|
|
artifact_digest: Option<Digest>,
|
|
artifact_size_bytes: Option<u64>,
|
|
#[serde(default)]
|
|
result: Option<TaskBoundaryValue>,
|
|
},
|
|
ListTaskEvents {
|
|
tenant: String,
|
|
project: String,
|
|
actor_user: String,
|
|
#[serde(default)]
|
|
process: Option<String>,
|
|
},
|
|
ListTaskSnapshots {
|
|
tenant: String,
|
|
project: String,
|
|
actor_user: String,
|
|
process: String,
|
|
},
|
|
JoinTask {
|
|
tenant: String,
|
|
project: String,
|
|
actor_user: String,
|
|
process: String,
|
|
task: String,
|
|
},
|
|
RenderOperatorPanel {
|
|
tenant: String,
|
|
project: String,
|
|
process: String,
|
|
actor_user: String,
|
|
max_download_bytes: u64,
|
|
stopped: bool,
|
|
},
|
|
SubmitPanelEvent {
|
|
tenant: String,
|
|
project: String,
|
|
process: String,
|
|
widget_id: String,
|
|
kind: PanelEventKind,
|
|
max_events: u64,
|
|
},
|
|
CreateArtifactDownloadLink {
|
|
tenant: String,
|
|
project: String,
|
|
actor_user: String,
|
|
artifact: String,
|
|
max_bytes: u64,
|
|
#[serde(default = "default_download_ttl_seconds")]
|
|
ttl_seconds: u64,
|
|
},
|
|
OpenArtifactDownloadStream {
|
|
tenant: String,
|
|
project: String,
|
|
actor_user: String,
|
|
artifact: String,
|
|
max_bytes: u64,
|
|
token_digest: Digest,
|
|
chunk_bytes: u64,
|
|
},
|
|
RevokeArtifactDownloadLink {
|
|
tenant: String,
|
|
project: String,
|
|
actor_user: String,
|
|
artifact: String,
|
|
token_digest: Digest,
|
|
},
|
|
ExportArtifactToNode {
|
|
tenant: String,
|
|
project: String,
|
|
actor_user: String,
|
|
artifact: String,
|
|
receiver_node: String,
|
|
direct_connectivity: bool,
|
|
failure_reason: String,
|
|
},
|
|
}
|
|
|
|
impl CoordinatorRequest {
|
|
pub fn validate_external_identifiers(&self) -> Result<(), String> {
|
|
validate_coordinator_request(self, "request")
|
|
}
|
|
|
|
pub fn operation(&self) -> Result<String, String> {
|
|
serde_json::to_value(self)
|
|
.map_err(|err| format!("failed to encode coordinator request operation: {err}"))
|
|
.map(|value| clusterflux_core::coordinator_payload_operation(&value))
|
|
}
|
|
}
|
|
|
|
fn validate_coordinator_request(request: &CoordinatorRequest, path: &str) -> Result<(), String> {
|
|
match request {
|
|
CoordinatorRequest::Ping => Ok(()),
|
|
CoordinatorRequest::Authenticated {
|
|
session_secret,
|
|
request,
|
|
} => {
|
|
validate_external_token(session_secret, &format!("{path}.session_secret"), 512)?;
|
|
validate_authenticated_request(request, &format!("{path}.request"))
|
|
}
|
|
CoordinatorRequest::AuthStatus {
|
|
tenant,
|
|
project,
|
|
actor_user,
|
|
}
|
|
| CoordinatorRequest::CreateProject {
|
|
tenant,
|
|
project,
|
|
actor_user,
|
|
..
|
|
}
|
|
| CoordinatorRequest::SelectProject {
|
|
tenant,
|
|
project,
|
|
actor_user,
|
|
} => {
|
|
validate_tenant(tenant, &format!("{path}.tenant"))?;
|
|
validate_project(project, &format!("{path}.project"))?;
|
|
validate_user(actor_user, &format!("{path}.actor_user"))
|
|
}
|
|
CoordinatorRequest::ListProjects { tenant, actor_user } => {
|
|
validate_tenant(tenant, &format!("{path}.tenant"))?;
|
|
validate_user(actor_user, &format!("{path}.actor_user"))
|
|
}
|
|
CoordinatorRequest::AdminStatus {
|
|
tenant,
|
|
actor_user,
|
|
admin_nonce,
|
|
..
|
|
} => {
|
|
validate_tenant(tenant, &format!("{path}.tenant"))?;
|
|
validate_user(actor_user, &format!("{path}.actor_user"))?;
|
|
validate_external_token(admin_nonce, &format!("{path}.admin_nonce"), 256)
|
|
}
|
|
CoordinatorRequest::SuspendTenant {
|
|
tenant,
|
|
actor_user,
|
|
target_tenant,
|
|
admin_nonce,
|
|
..
|
|
} => {
|
|
validate_tenant(tenant, &format!("{path}.tenant"))?;
|
|
validate_user(actor_user, &format!("{path}.actor_user"))?;
|
|
validate_tenant(target_tenant, &format!("{path}.target_tenant"))?;
|
|
validate_external_token(admin_nonce, &format!("{path}.admin_nonce"), 256)
|
|
}
|
|
CoordinatorRequest::RegisterAgentPublicKey {
|
|
tenant,
|
|
project,
|
|
user,
|
|
agent,
|
|
public_key,
|
|
}
|
|
| CoordinatorRequest::RotateAgentPublicKey {
|
|
tenant,
|
|
project,
|
|
user,
|
|
agent,
|
|
public_key,
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_user(user, &format!("{path}.user"))?;
|
|
validate_agent(agent, &format!("{path}.agent"))?;
|
|
validate_external_token(public_key, &format!("{path}.public_key"), 1024)
|
|
}
|
|
CoordinatorRequest::ListAgentPublicKeys {
|
|
tenant,
|
|
project,
|
|
user,
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_user(user, &format!("{path}.user"))
|
|
}
|
|
CoordinatorRequest::RevokeAgentPublicKey {
|
|
tenant,
|
|
project,
|
|
user,
|
|
agent,
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_user(user, &format!("{path}.user"))?;
|
|
validate_agent(agent, &format!("{path}.agent"))
|
|
}
|
|
CoordinatorRequest::AttachNode {
|
|
tenant,
|
|
project,
|
|
node,
|
|
public_key,
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_node(node, &format!("{path}.node"))?;
|
|
validate_external_token(public_key, &format!("{path}.public_key"), 1024)
|
|
}
|
|
CoordinatorRequest::CreateNodeEnrollmentGrant {
|
|
tenant,
|
|
project,
|
|
actor_user,
|
|
..
|
|
}
|
|
| CoordinatorRequest::ListNodeDescriptors {
|
|
tenant,
|
|
project,
|
|
actor_user,
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_user(actor_user, &format!("{path}.actor_user"))
|
|
}
|
|
CoordinatorRequest::ExchangeNodeEnrollmentGrant {
|
|
tenant,
|
|
project,
|
|
node,
|
|
public_key,
|
|
enrollment_grant,
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_node(node, &format!("{path}.node"))?;
|
|
validate_external_token(public_key, &format!("{path}.public_key"), 1024)?;
|
|
validate_external_token(enrollment_grant, &format!("{path}.enrollment_grant"), 512)
|
|
}
|
|
CoordinatorRequest::NodeHeartbeat {
|
|
tenant,
|
|
project,
|
|
node,
|
|
node_signature,
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_node(node, &format!("{path}.node"))?;
|
|
if let Some(signature) = node_signature {
|
|
validate_node_signature(signature, &format!("{path}.node_signature"))?;
|
|
}
|
|
Ok(())
|
|
}
|
|
CoordinatorRequest::SignedNode {
|
|
node,
|
|
node_signature,
|
|
request,
|
|
} => {
|
|
validate_node(node, &format!("{path}.node"))?;
|
|
validate_node_signature(node_signature, &format!("{path}.node_signature"))?;
|
|
validate_coordinator_request(request, &format!("{path}.request"))
|
|
}
|
|
CoordinatorRequest::ReportNodeCapabilities {
|
|
tenant,
|
|
project,
|
|
node,
|
|
artifact_locations,
|
|
..
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_node(node, &format!("{path}.node"))?;
|
|
validate_artifact_array(artifact_locations, &format!("{path}.artifact_locations"))
|
|
}
|
|
CoordinatorRequest::RevokeNodeCredential {
|
|
tenant,
|
|
project,
|
|
actor_user,
|
|
node,
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_user(actor_user, &format!("{path}.actor_user"))?;
|
|
validate_node(node, &format!("{path}.node"))
|
|
}
|
|
CoordinatorRequest::ScheduleTask {
|
|
tenant,
|
|
project,
|
|
required_artifacts,
|
|
prefer_node,
|
|
..
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_artifact_array(required_artifacts, &format!("{path}.required_artifacts"))?;
|
|
validate_optional_node(prefer_node.as_deref(), &format!("{path}.prefer_node"))
|
|
}
|
|
CoordinatorRequest::LaunchTask {
|
|
tenant,
|
|
project,
|
|
actor_user,
|
|
actor_agent,
|
|
agent_signature,
|
|
task_spec,
|
|
..
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_optional_user(actor_user.as_deref(), &format!("{path}.actor_user"))?;
|
|
validate_optional_agent(actor_agent.as_deref(), &format!("{path}.actor_agent"))?;
|
|
if let Some(signature) = agent_signature {
|
|
validate_agent_signature(signature, &format!("{path}.agent_signature"))?;
|
|
}
|
|
validate_task_spec(task_spec, &format!("{path}.task_spec"))
|
|
}
|
|
CoordinatorRequest::LaunchChildTask {
|
|
tenant,
|
|
project,
|
|
process,
|
|
node,
|
|
parent_task,
|
|
task_spec,
|
|
..
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_process(process, &format!("{path}.process"))?;
|
|
validate_node(node, &format!("{path}.node"))?;
|
|
validate_task_instance(parent_task, &format!("{path}.parent_task"))?;
|
|
validate_task_spec(task_spec, &format!("{path}.task_spec"))
|
|
}
|
|
CoordinatorRequest::JoinChildTask {
|
|
tenant,
|
|
project,
|
|
process,
|
|
node,
|
|
parent_task,
|
|
task,
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_process(process, &format!("{path}.process"))?;
|
|
validate_node(node, &format!("{path}.node"))?;
|
|
validate_task_instance(parent_task, &format!("{path}.parent_task"))?;
|
|
validate_task_instance(task, &format!("{path}.task"))
|
|
}
|
|
CoordinatorRequest::PollTaskAssignment {
|
|
tenant,
|
|
project,
|
|
node,
|
|
}
|
|
| CoordinatorRequest::PollArtifactTransfer {
|
|
tenant,
|
|
project,
|
|
node,
|
|
}
|
|
| CoordinatorRequest::CompleteSourcePreparation {
|
|
tenant,
|
|
project,
|
|
node,
|
|
..
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_node(node, &format!("{path}.node"))
|
|
}
|
|
CoordinatorRequest::UploadArtifactTransferChunk {
|
|
tenant,
|
|
project,
|
|
node,
|
|
transfer_id,
|
|
artifact,
|
|
..
|
|
}
|
|
| CoordinatorRequest::FailArtifactTransfer {
|
|
tenant,
|
|
project,
|
|
node,
|
|
transfer_id,
|
|
artifact,
|
|
..
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_node(node, &format!("{path}.node"))?;
|
|
validate_external_token(transfer_id, &format!("{path}.transfer_id"), 256)?;
|
|
validate_artifact(artifact, &format!("{path}.artifact"))
|
|
}
|
|
CoordinatorRequest::RequestRendezvous {
|
|
scope,
|
|
source,
|
|
destination,
|
|
..
|
|
} => {
|
|
validate_data_plane_scope(scope, &format!("{path}.scope"))?;
|
|
validate_node_endpoint(source, &format!("{path}.source"))?;
|
|
validate_node_endpoint(destination, &format!("{path}.destination"))
|
|
}
|
|
CoordinatorRequest::RequestSourcePreparation {
|
|
tenant, project, ..
|
|
} => validate_tenant_project(tenant, project, path),
|
|
CoordinatorRequest::StartProcess {
|
|
tenant,
|
|
project,
|
|
actor_user,
|
|
actor_agent,
|
|
agent_signature,
|
|
process,
|
|
launch_attempt,
|
|
..
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_optional_user(actor_user.as_deref(), &format!("{path}.actor_user"))?;
|
|
validate_optional_agent(actor_agent.as_deref(), &format!("{path}.actor_agent"))?;
|
|
if let Some(signature) = agent_signature {
|
|
validate_agent_signature(signature, &format!("{path}.agent_signature"))?;
|
|
}
|
|
validate_process(process, &format!("{path}.process"))?;
|
|
validate_optional_launch_attempt(
|
|
launch_attempt.as_deref(),
|
|
&format!("{path}.launch_attempt"),
|
|
)
|
|
}
|
|
CoordinatorRequest::ReconnectNode {
|
|
tenant,
|
|
project,
|
|
node,
|
|
process,
|
|
..
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_node(node, &format!("{path}.node"))?;
|
|
validate_process(process, &format!("{path}.process"))
|
|
}
|
|
CoordinatorRequest::CancelTask {
|
|
tenant,
|
|
project,
|
|
process,
|
|
node,
|
|
task,
|
|
}
|
|
| CoordinatorRequest::PollTaskControl {
|
|
tenant,
|
|
project,
|
|
process,
|
|
node,
|
|
task,
|
|
}
|
|
| CoordinatorRequest::PollDebugCommand {
|
|
tenant,
|
|
project,
|
|
process,
|
|
node,
|
|
task,
|
|
}
|
|
| CoordinatorRequest::ReportDebugProbeHit {
|
|
tenant,
|
|
project,
|
|
process,
|
|
node,
|
|
task,
|
|
..
|
|
}
|
|
| CoordinatorRequest::ReportTaskLog {
|
|
tenant,
|
|
project,
|
|
process,
|
|
node,
|
|
task,
|
|
..
|
|
}
|
|
| CoordinatorRequest::ReportVfsMetadata {
|
|
tenant,
|
|
project,
|
|
process,
|
|
node,
|
|
task,
|
|
..
|
|
}
|
|
| CoordinatorRequest::TaskCompleted {
|
|
tenant,
|
|
project,
|
|
process,
|
|
node,
|
|
task,
|
|
..
|
|
}
|
|
| CoordinatorRequest::ReportDebugState {
|
|
tenant,
|
|
project,
|
|
process,
|
|
node,
|
|
task,
|
|
..
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_process(process, &format!("{path}.process"))?;
|
|
validate_node(node, &format!("{path}.node"))?;
|
|
validate_task_instance(task, &format!("{path}.task"))
|
|
}
|
|
CoordinatorRequest::CancelProcess {
|
|
tenant,
|
|
project,
|
|
actor_user,
|
|
process,
|
|
}
|
|
| CoordinatorRequest::DebugAttach {
|
|
tenant,
|
|
project,
|
|
actor_user,
|
|
process,
|
|
}
|
|
| CoordinatorRequest::SetDebugBreakpoints {
|
|
tenant,
|
|
project,
|
|
actor_user,
|
|
process,
|
|
..
|
|
}
|
|
| CoordinatorRequest::InspectDebugBreakpoints {
|
|
tenant,
|
|
project,
|
|
actor_user,
|
|
process,
|
|
}
|
|
| CoordinatorRequest::ResumeDebugEpoch {
|
|
tenant,
|
|
project,
|
|
actor_user,
|
|
process,
|
|
..
|
|
}
|
|
| CoordinatorRequest::InspectDebugEpoch {
|
|
tenant,
|
|
project,
|
|
actor_user,
|
|
process,
|
|
..
|
|
}
|
|
| CoordinatorRequest::ListTaskSnapshots {
|
|
tenant,
|
|
project,
|
|
actor_user,
|
|
process,
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_user(actor_user, &format!("{path}.actor_user"))?;
|
|
validate_process(process, &format!("{path}.process"))
|
|
}
|
|
CoordinatorRequest::AbortProcess {
|
|
tenant,
|
|
project,
|
|
actor_user,
|
|
process,
|
|
launch_attempt,
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_user(actor_user, &format!("{path}.actor_user"))?;
|
|
validate_process(process, &format!("{path}.process"))?;
|
|
validate_optional_launch_attempt(
|
|
launch_attempt.as_deref(),
|
|
&format!("{path}.launch_attempt"),
|
|
)
|
|
}
|
|
CoordinatorRequest::ListProcesses {
|
|
tenant,
|
|
project,
|
|
actor_user,
|
|
}
|
|
| CoordinatorRequest::QuotaStatus {
|
|
tenant,
|
|
project,
|
|
actor_user,
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_user(actor_user, &format!("{path}.actor_user"))
|
|
}
|
|
CoordinatorRequest::RestartTask {
|
|
tenant,
|
|
project,
|
|
actor_user,
|
|
process,
|
|
task,
|
|
..
|
|
}
|
|
| CoordinatorRequest::ResolveTaskFailure {
|
|
tenant,
|
|
project,
|
|
actor_user,
|
|
process,
|
|
task,
|
|
..
|
|
}
|
|
| CoordinatorRequest::JoinTask {
|
|
tenant,
|
|
project,
|
|
actor_user,
|
|
process,
|
|
task,
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_user(actor_user, &format!("{path}.actor_user"))?;
|
|
validate_process(process, &format!("{path}.process"))?;
|
|
validate_task_instance(task, &format!("{path}.task"))
|
|
}
|
|
CoordinatorRequest::CreateDebugEpoch {
|
|
tenant,
|
|
project,
|
|
actor_user,
|
|
process,
|
|
stopped_task,
|
|
..
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_user(actor_user, &format!("{path}.actor_user"))?;
|
|
validate_process(process, &format!("{path}.process"))?;
|
|
validate_task_instance(stopped_task, &format!("{path}.stopped_task"))
|
|
}
|
|
CoordinatorRequest::ListTaskEvents {
|
|
tenant,
|
|
project,
|
|
actor_user,
|
|
process,
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_user(actor_user, &format!("{path}.actor_user"))?;
|
|
validate_optional_process(process.as_deref(), &format!("{path}.process"))
|
|
}
|
|
CoordinatorRequest::RenderOperatorPanel {
|
|
tenant,
|
|
project,
|
|
process,
|
|
actor_user,
|
|
..
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_process(process, &format!("{path}.process"))?;
|
|
validate_user(actor_user, &format!("{path}.actor_user"))
|
|
}
|
|
CoordinatorRequest::SubmitPanelEvent {
|
|
tenant,
|
|
project,
|
|
process,
|
|
widget_id,
|
|
..
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_process(process, &format!("{path}.process"))?;
|
|
validate_external_token(widget_id, &format!("{path}.widget_id"), 256)
|
|
}
|
|
CoordinatorRequest::CreateArtifactDownloadLink {
|
|
tenant,
|
|
project,
|
|
actor_user,
|
|
artifact,
|
|
..
|
|
}
|
|
| CoordinatorRequest::OpenArtifactDownloadStream {
|
|
tenant,
|
|
project,
|
|
actor_user,
|
|
artifact,
|
|
..
|
|
}
|
|
| CoordinatorRequest::RevokeArtifactDownloadLink {
|
|
tenant,
|
|
project,
|
|
actor_user,
|
|
artifact,
|
|
..
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_user(actor_user, &format!("{path}.actor_user"))?;
|
|
validate_artifact(artifact, &format!("{path}.artifact"))
|
|
}
|
|
CoordinatorRequest::ExportArtifactToNode {
|
|
tenant,
|
|
project,
|
|
actor_user,
|
|
artifact,
|
|
receiver_node,
|
|
..
|
|
} => {
|
|
validate_tenant_project(tenant, project, path)?;
|
|
validate_user(actor_user, &format!("{path}.actor_user"))?;
|
|
validate_artifact(artifact, &format!("{path}.artifact"))?;
|
|
validate_node(receiver_node, &format!("{path}.receiver_node"))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn validate_authenticated_request(
|
|
request: &AuthenticatedCoordinatorRequest,
|
|
path: &str,
|
|
) -> Result<(), String> {
|
|
match request {
|
|
AuthenticatedCoordinatorRequest::AuthStatus
|
|
| AuthenticatedCoordinatorRequest::RevokeCliSession
|
|
| AuthenticatedCoordinatorRequest::ListProjects
|
|
| AuthenticatedCoordinatorRequest::CreateNodeEnrollmentGrant { .. }
|
|
| AuthenticatedCoordinatorRequest::ListNodeDescriptors
|
|
| AuthenticatedCoordinatorRequest::ListProcesses
|
|
| AuthenticatedCoordinatorRequest::QuotaStatus => Ok(()),
|
|
AuthenticatedCoordinatorRequest::CreateProject { project, .. }
|
|
| AuthenticatedCoordinatorRequest::SelectProject { project } => {
|
|
validate_project(project, &format!("{path}.project"))
|
|
}
|
|
AuthenticatedCoordinatorRequest::RegisterAgentPublicKey {
|
|
agent, public_key, ..
|
|
}
|
|
| AuthenticatedCoordinatorRequest::RotateAgentPublicKey {
|
|
agent, public_key, ..
|
|
} => {
|
|
validate_agent(agent, &format!("{path}.agent"))?;
|
|
validate_external_token(public_key, &format!("{path}.public_key"), 1024)
|
|
}
|
|
AuthenticatedCoordinatorRequest::ListAgentPublicKeys => Ok(()),
|
|
AuthenticatedCoordinatorRequest::RevokeAgentPublicKey { agent } => {
|
|
validate_agent(agent, &format!("{path}.agent"))
|
|
}
|
|
AuthenticatedCoordinatorRequest::RevokeNodeCredential { node } => {
|
|
validate_node(node, &format!("{path}.node"))
|
|
}
|
|
AuthenticatedCoordinatorRequest::StartProcess {
|
|
process,
|
|
launch_attempt,
|
|
..
|
|
}
|
|
| AuthenticatedCoordinatorRequest::AbortProcess {
|
|
process,
|
|
launch_attempt,
|
|
} => {
|
|
validate_process(process, &format!("{path}.process"))?;
|
|
validate_optional_launch_attempt(
|
|
launch_attempt.as_deref(),
|
|
&format!("{path}.launch_attempt"),
|
|
)
|
|
}
|
|
AuthenticatedCoordinatorRequest::ScheduleTask {
|
|
required_artifacts,
|
|
prefer_node,
|
|
..
|
|
} => {
|
|
validate_artifact_array(required_artifacts, &format!("{path}.required_artifacts"))?;
|
|
validate_optional_node(prefer_node.as_deref(), &format!("{path}.prefer_node"))
|
|
}
|
|
AuthenticatedCoordinatorRequest::LaunchTask { task_spec, .. } => {
|
|
validate_task_spec(task_spec, &format!("{path}.task_spec"))
|
|
}
|
|
AuthenticatedCoordinatorRequest::CancelProcess { process }
|
|
| AuthenticatedCoordinatorRequest::DebugAttach { process }
|
|
| AuthenticatedCoordinatorRequest::SetDebugBreakpoints { process, .. }
|
|
| AuthenticatedCoordinatorRequest::InspectDebugBreakpoints { process }
|
|
| AuthenticatedCoordinatorRequest::ResumeDebugEpoch { process, .. }
|
|
| AuthenticatedCoordinatorRequest::InspectDebugEpoch { process, .. }
|
|
| AuthenticatedCoordinatorRequest::ListTaskSnapshots { process } => {
|
|
validate_process(process, &format!("{path}.process"))
|
|
}
|
|
AuthenticatedCoordinatorRequest::RestartTask { process, task, .. }
|
|
| AuthenticatedCoordinatorRequest::ResolveTaskFailure { process, task, .. }
|
|
| AuthenticatedCoordinatorRequest::JoinTask { process, task } => {
|
|
validate_process(process, &format!("{path}.process"))?;
|
|
validate_task_instance(task, &format!("{path}.task"))
|
|
}
|
|
AuthenticatedCoordinatorRequest::CreateDebugEpoch {
|
|
process,
|
|
stopped_task,
|
|
..
|
|
} => {
|
|
validate_process(process, &format!("{path}.process"))?;
|
|
validate_task_instance(stopped_task, &format!("{path}.stopped_task"))
|
|
}
|
|
AuthenticatedCoordinatorRequest::ListTaskEvents { process } => {
|
|
validate_optional_process(process.as_deref(), &format!("{path}.process"))
|
|
}
|
|
AuthenticatedCoordinatorRequest::CreateArtifactDownloadLink { artifact, .. }
|
|
| AuthenticatedCoordinatorRequest::OpenArtifactDownloadStream { artifact, .. }
|
|
| AuthenticatedCoordinatorRequest::RevokeArtifactDownloadLink { artifact, .. } => {
|
|
validate_artifact(artifact, &format!("{path}.artifact"))
|
|
}
|
|
AuthenticatedCoordinatorRequest::ExportArtifactToNode {
|
|
artifact,
|
|
receiver_node,
|
|
..
|
|
} => {
|
|
validate_artifact(artifact, &format!("{path}.artifact"))?;
|
|
validate_node(receiver_node, &format!("{path}.receiver_node"))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn validate_task_spec(task_spec: &TaskSpec, path: &str) -> Result<(), String> {
|
|
validate_existing_id(
|
|
&task_spec.tenant,
|
|
&format!("{path}.tenant"),
|
|
TenantId::try_new,
|
|
)?;
|
|
validate_existing_id(
|
|
&task_spec.project,
|
|
&format!("{path}.project"),
|
|
ProjectId::try_new,
|
|
)?;
|
|
validate_existing_id(
|
|
&task_spec.process,
|
|
&format!("{path}.process"),
|
|
ProcessId::try_new,
|
|
)?;
|
|
validate_existing_id(
|
|
&task_spec.task_definition,
|
|
&format!("{path}.task_definition"),
|
|
TaskDefinitionId::try_new,
|
|
)?;
|
|
validate_existing_id(
|
|
&task_spec.task_instance,
|
|
&format!("{path}.task_instance"),
|
|
TaskInstanceId::try_new,
|
|
)?;
|
|
if let Some(environment_id) = &task_spec.environment_id {
|
|
validate_external_token(environment_id, &format!("{path}.environment_id"), 128)?;
|
|
}
|
|
for (index, artifact) in task_spec.required_artifacts.iter().enumerate() {
|
|
validate_existing_id(
|
|
artifact,
|
|
&format!("{path}.required_artifacts[{index}]"),
|
|
ArtifactId::try_new,
|
|
)?;
|
|
}
|
|
for (argument_index, argument) in task_spec.args.iter().enumerate() {
|
|
validate_task_boundary_value(argument, &format!("{path}.args[{argument_index}]"))?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_task_boundary_value(value: &TaskBoundaryValue, path: &str) -> Result<(), String> {
|
|
match value {
|
|
TaskBoundaryValue::Artifact(artifact) => validate_existing_id(
|
|
&artifact.id,
|
|
&format!("{path}.artifact.id"),
|
|
ArtifactId::try_new,
|
|
),
|
|
TaskBoundaryValue::Structured(structured) => {
|
|
for (index, handle) in structured.handles.iter().enumerate() {
|
|
if let TaskBoundaryHandle::Artifact(artifact) = handle {
|
|
validate_existing_id(
|
|
&artifact.id,
|
|
&format!("{path}.handles[{index}].artifact.id"),
|
|
ArtifactId::try_new,
|
|
)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
TaskBoundaryValue::SmallJson(_)
|
|
| TaskBoundaryValue::SourceSnapshot(_)
|
|
| TaskBoundaryValue::Blob(_)
|
|
| TaskBoundaryValue::VfsManifest(_) => Ok(()),
|
|
}
|
|
}
|
|
|
|
fn validate_data_plane_scope(scope: &DataPlaneScope, path: &str) -> Result<(), String> {
|
|
validate_existing_id(&scope.tenant, &format!("{path}.tenant"), TenantId::try_new)?;
|
|
validate_existing_id(
|
|
&scope.project,
|
|
&format!("{path}.project"),
|
|
ProjectId::try_new,
|
|
)?;
|
|
validate_existing_id(
|
|
&scope.process,
|
|
&format!("{path}.process"),
|
|
ProcessId::try_new,
|
|
)?;
|
|
if let DataPlaneObject::Artifact(artifact) = &scope.object {
|
|
validate_existing_id(
|
|
artifact,
|
|
&format!("{path}.object.artifact"),
|
|
ArtifactId::try_new,
|
|
)?;
|
|
}
|
|
validate_external_token(
|
|
&scope.authorization_subject,
|
|
&format!("{path}.authorization_subject"),
|
|
512,
|
|
)
|
|
}
|
|
|
|
fn validate_node_endpoint(endpoint: &NodeEndpoint, path: &str) -> Result<(), String> {
|
|
validate_existing_id(&endpoint.node, &format!("{path}.node"), NodeId::try_new)?;
|
|
validate_external_token(
|
|
&endpoint.advertised_addr,
|
|
&format!("{path}.advertised_addr"),
|
|
512,
|
|
)
|
|
}
|
|
|
|
fn validate_node_signature(signature: &NodeSignedRequest, path: &str) -> Result<(), String> {
|
|
validate_external_token(&signature.nonce, &format!("{path}.nonce"), 256)?;
|
|
validate_external_token(&signature.signature, &format!("{path}.signature"), 512)
|
|
}
|
|
|
|
fn validate_agent_signature(signature: &AgentSignedRequest, path: &str) -> Result<(), String> {
|
|
validate_external_token(&signature.nonce, &format!("{path}.nonce"), 256)?;
|
|
validate_external_token(&signature.signature, &format!("{path}.signature"), 512)
|
|
}
|
|
|
|
fn validate_tenant_project(tenant: &str, project: &str, path: &str) -> Result<(), String> {
|
|
validate_tenant(tenant, &format!("{path}.tenant"))?;
|
|
validate_project(project, &format!("{path}.project"))
|
|
}
|
|
|
|
fn validate_artifact_array(values: &[String], path: &str) -> Result<(), String> {
|
|
for (index, value) in values.iter().enumerate() {
|
|
validate_artifact(value, &format!("{path}[{index}]"))?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_optional_user(value: Option<&str>, path: &str) -> Result<(), String> {
|
|
value.map_or(Ok(()), |value| validate_user(value, path))
|
|
}
|
|
|
|
fn validate_optional_agent(value: Option<&str>, path: &str) -> Result<(), String> {
|
|
value.map_or(Ok(()), |value| validate_agent(value, path))
|
|
}
|
|
|
|
fn validate_optional_node(value: Option<&str>, path: &str) -> Result<(), String> {
|
|
value.map_or(Ok(()), |value| validate_node(value, path))
|
|
}
|
|
|
|
fn validate_optional_process(value: Option<&str>, path: &str) -> Result<(), String> {
|
|
value.map_or(Ok(()), |value| validate_process(value, path))
|
|
}
|
|
|
|
fn validate_optional_launch_attempt(value: Option<&str>, path: &str) -> Result<(), String> {
|
|
value.map_or(Ok(()), |value| validate_launch_attempt(value, path))
|
|
}
|
|
|
|
fn validate_tenant(value: &str, path: &str) -> Result<(), String> {
|
|
validate_external_id(value, path, TenantId::try_new)
|
|
}
|
|
|
|
fn validate_project(value: &str, path: &str) -> Result<(), String> {
|
|
validate_external_id(value, path, ProjectId::try_new)
|
|
}
|
|
|
|
fn validate_user(value: &str, path: &str) -> Result<(), String> {
|
|
validate_external_id(value, path, UserId::try_new)
|
|
}
|
|
|
|
fn validate_agent(value: &str, path: &str) -> Result<(), String> {
|
|
validate_external_id(value, path, AgentId::try_new)
|
|
}
|
|
|
|
fn validate_node(value: &str, path: &str) -> Result<(), String> {
|
|
validate_external_id(value, path, NodeId::try_new)
|
|
}
|
|
|
|
fn validate_process(value: &str, path: &str) -> Result<(), String> {
|
|
validate_external_id(value, path, ProcessId::try_new)
|
|
}
|
|
|
|
fn validate_task_instance(value: &str, path: &str) -> Result<(), String> {
|
|
validate_external_id(value, path, TaskInstanceId::try_new)
|
|
}
|
|
|
|
fn validate_artifact(value: &str, path: &str) -> Result<(), String> {
|
|
validate_external_id(value, path, ArtifactId::try_new)
|
|
}
|
|
|
|
fn validate_launch_attempt(value: &str, path: &str) -> Result<(), String> {
|
|
validate_external_id(value, path, LaunchAttemptId::try_new)
|
|
}
|
|
|
|
fn validate_external_id<T, E>(
|
|
value: &str,
|
|
path: &str,
|
|
parser: impl FnOnce(String) -> Result<T, E>,
|
|
) -> Result<(), String>
|
|
where
|
|
E: std::fmt::Display,
|
|
{
|
|
parser(value.to_owned())
|
|
.map(|_| ())
|
|
.map_err(|error| format!("malformed external identifier {path}: {error}"))
|
|
}
|
|
|
|
fn validate_existing_id<T, E>(
|
|
value: &T,
|
|
path: &str,
|
|
parser: impl FnOnce(String) -> Result<T, E>,
|
|
) -> Result<(), String>
|
|
where
|
|
T: std::fmt::Display,
|
|
E: std::fmt::Display,
|
|
{
|
|
validate_external_id(&value.to_string(), path, parser)
|
|
}
|
|
|
|
fn validate_external_token(value: &str, path: &str, max_bytes: usize) -> Result<(), String> {
|
|
clusterflux_core::validate_opaque_token(value, max_bytes)
|
|
.map_err(|error| format!("malformed external token {path}: {error}"))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod external_identifier_tests {
|
|
use super::*;
|
|
|
|
fn valid_task_spec() -> TaskSpec {
|
|
TaskSpec {
|
|
tenant: TenantId::from("tenant"),
|
|
project: ProjectId::from("project"),
|
|
process: ProcessId::from("process"),
|
|
task_definition: TaskDefinitionId::from("compile"),
|
|
task_instance: TaskInstanceId::from("compile-1"),
|
|
dispatch: clusterflux_core::TaskDispatch::CoordinatorNodeWasm {
|
|
export: Some("compile".to_owned()),
|
|
abi: clusterflux_core::WasmExportAbi::TaskV1,
|
|
},
|
|
environment_id: Some("linux-rootless".to_owned()),
|
|
environment: None,
|
|
environment_digest: None,
|
|
required_capabilities: Default::default(),
|
|
dependency_cache: None,
|
|
source_snapshot: None,
|
|
required_artifacts: vec![ArtifactId::from("input-artifact")],
|
|
args: Vec::new(),
|
|
vfs_epoch: 1,
|
|
failure_policy: Default::default(),
|
|
bundle_digest: None,
|
|
}
|
|
}
|
|
|
|
fn validation_error(value: serde_json::Value) -> String {
|
|
match serde_json::from_value::<CoordinatorRequest>(value) {
|
|
Ok(request) => request
|
|
.validate_external_identifiers()
|
|
.expect_err("request should contain one malformed external identifier"),
|
|
Err(error) => error.to_string(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn nested_authenticated_identifiers_are_validated() {
|
|
let request = CoordinatorRequest::Authenticated {
|
|
session_secret: "cli_session_valid".to_owned(),
|
|
request: AuthenticatedCoordinatorRequest::AbortProcess {
|
|
process: "bad process".to_owned(),
|
|
launch_attempt: Some("attempt".to_owned()),
|
|
},
|
|
};
|
|
let error = request.validate_external_identifiers().unwrap_err();
|
|
assert!(error.contains("request.request.process"));
|
|
}
|
|
|
|
#[test]
|
|
fn opaque_secrets_are_bounded_as_tokens_instead_of_object_ids() {
|
|
let request = CoordinatorRequest::Authenticated {
|
|
session_secret: "opaque secret/+==".to_owned(),
|
|
request: AuthenticatedCoordinatorRequest::AuthStatus,
|
|
};
|
|
request.validate_external_identifiers().unwrap();
|
|
|
|
let mut request = request;
|
|
let CoordinatorRequest::Authenticated { session_secret, .. } = &mut request else {
|
|
unreachable!()
|
|
};
|
|
*session_secret = "bad\0secret".to_owned();
|
|
let error = request.validate_external_identifiers().unwrap_err();
|
|
assert!(error.contains("malformed external token request.session_secret"));
|
|
}
|
|
|
|
#[test]
|
|
fn real_protocol_variants_reject_exactly_one_malformed_nested_identifier() {
|
|
let launch = CoordinatorRequest::LaunchTask {
|
|
tenant: "tenant".to_owned(),
|
|
project: "project".to_owned(),
|
|
actor_user: Some("user".to_owned()),
|
|
actor_agent: None,
|
|
agent_public_key_fingerprint: None,
|
|
agent_signature: None,
|
|
task_spec: valid_task_spec(),
|
|
wait_for_node: false,
|
|
artifact_path: "/vfs/artifacts/output".to_owned(),
|
|
wasm_module_base64: "AGFzbQEAAAA=".to_owned(),
|
|
};
|
|
|
|
let mut malformed_definition = serde_json::to_value(&launch).unwrap();
|
|
malformed_definition["task_spec"]["task_definition"] =
|
|
serde_json::Value::String("bad task definition!".to_owned());
|
|
let error = validation_error(malformed_definition);
|
|
assert!(error.contains("TaskDefinitionId is invalid"));
|
|
|
|
let mut malformed_artifact = serde_json::to_value(&launch).unwrap();
|
|
malformed_artifact["task_spec"]["required_artifacts"][0] =
|
|
serde_json::Value::String("bad artifact!".to_owned());
|
|
let error = validation_error(malformed_artifact);
|
|
assert!(error.contains("ArtifactId is invalid"));
|
|
|
|
let rendezvous = CoordinatorRequest::RequestRendezvous {
|
|
scope: DataPlaneScope {
|
|
tenant: TenantId::from("tenant"),
|
|
project: ProjectId::from("project"),
|
|
process: ProcessId::from("process"),
|
|
object: DataPlaneObject::Artifact(ArtifactId::from("artifact")),
|
|
authorization_subject: "node-a-to-node-b".to_owned(),
|
|
},
|
|
source: NodeEndpoint {
|
|
node: NodeId::from("node-a"),
|
|
advertised_addr: "node-a.invalid:4433".to_owned(),
|
|
public_key_fingerprint: Digest::sha256("node-a"),
|
|
},
|
|
destination: NodeEndpoint {
|
|
node: NodeId::from("node-b"),
|
|
advertised_addr: "node-b.invalid:4433".to_owned(),
|
|
public_key_fingerprint: Digest::sha256("node-b"),
|
|
},
|
|
direct_connectivity: true,
|
|
failure_reason: String::new(),
|
|
};
|
|
let mut malformed_endpoint = serde_json::to_value(rendezvous).unwrap();
|
|
malformed_endpoint["destination"]["node"] =
|
|
serde_json::Value::String("bad node!".to_owned());
|
|
let error = validation_error(malformed_endpoint);
|
|
assert!(error.contains("NodeId is invalid"));
|
|
}
|
|
|
|
#[test]
|
|
fn signed_and_authenticated_real_variants_validate_scoped_ids_and_tokens() {
|
|
let cases = [
|
|
CoordinatorRequest::Authenticated {
|
|
session_secret: "session-secret".to_owned(),
|
|
request: AuthenticatedCoordinatorRequest::AbortProcess {
|
|
process: "bad process!".to_owned(),
|
|
launch_attempt: Some("attempt".to_owned()),
|
|
},
|
|
},
|
|
CoordinatorRequest::StartProcess {
|
|
tenant: "tenant".to_owned(),
|
|
project: "project".to_owned(),
|
|
actor_user: None,
|
|
actor_agent: Some("bad agent!".to_owned()),
|
|
agent_public_key_fingerprint: Some(Digest::sha256("agent")),
|
|
agent_signature: Some(AgentSignedRequest {
|
|
nonce: "agent-nonce".to_owned(),
|
|
issued_at_epoch_seconds: 1,
|
|
signature: "ed25519:syntactically-bounded".to_owned(),
|
|
}),
|
|
process: "process".to_owned(),
|
|
launch_attempt: Some("attempt".to_owned()),
|
|
restart: false,
|
|
},
|
|
CoordinatorRequest::NodeHeartbeat {
|
|
tenant: "tenant".to_owned(),
|
|
project: "project".to_owned(),
|
|
node: "bad node!".to_owned(),
|
|
node_signature: Some(NodeSignedRequest {
|
|
nonce: "node-nonce".to_owned(),
|
|
issued_at_epoch_seconds: 1,
|
|
signature: "ed25519:syntactically-bounded".to_owned(),
|
|
}),
|
|
},
|
|
CoordinatorRequest::SignedNode {
|
|
node: "node".to_owned(),
|
|
node_signature: NodeSignedRequest {
|
|
nonce: "node-nonce".to_owned(),
|
|
issued_at_epoch_seconds: 1,
|
|
signature: "ed25519:syntactically-bounded".to_owned(),
|
|
},
|
|
request: Box::new(CoordinatorRequest::PollTaskAssignment {
|
|
tenant: "tenant".to_owned(),
|
|
project: "bad project!".to_owned(),
|
|
node: "node".to_owned(),
|
|
}),
|
|
},
|
|
CoordinatorRequest::AbortProcess {
|
|
tenant: "tenant".to_owned(),
|
|
project: "project".to_owned(),
|
|
actor_user: "user".to_owned(),
|
|
process: "process".to_owned(),
|
|
launch_attempt: Some("bad attempt!".to_owned()),
|
|
},
|
|
];
|
|
|
|
for request in cases {
|
|
let error = request.validate_external_identifiers().unwrap_err();
|
|
assert!(
|
|
error.contains("malformed external identifier"),
|
|
"unexpected validation error for {request:?}: {error}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn signed_request_nonces_are_validated_as_opaque_tokens() {
|
|
let agent_request = CoordinatorRequest::StartProcess {
|
|
tenant: "tenant".to_owned(),
|
|
project: "project".to_owned(),
|
|
actor_user: None,
|
|
actor_agent: Some("agent".to_owned()),
|
|
agent_public_key_fingerprint: Some(Digest::sha256("agent")),
|
|
agent_signature: Some(AgentSignedRequest {
|
|
nonce: String::new(),
|
|
issued_at_epoch_seconds: 1,
|
|
signature: "ed25519:syntactically-bounded".to_owned(),
|
|
}),
|
|
process: "process".to_owned(),
|
|
launch_attempt: Some("attempt".to_owned()),
|
|
restart: false,
|
|
};
|
|
let error = agent_request.validate_external_identifiers().unwrap_err();
|
|
assert!(error.contains("malformed external token request.agent_signature.nonce"));
|
|
|
|
let node_request = CoordinatorRequest::NodeHeartbeat {
|
|
tenant: "tenant".to_owned(),
|
|
project: "project".to_owned(),
|
|
node: "node".to_owned(),
|
|
node_signature: Some(NodeSignedRequest {
|
|
nonce: String::new(),
|
|
issued_at_epoch_seconds: 1,
|
|
signature: "ed25519:syntactically-bounded".to_owned(),
|
|
}),
|
|
};
|
|
let error = node_request.validate_external_identifiers().unwrap_err();
|
|
assert!(error.contains("malformed external token request.node_signature.nonce"));
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
|
|
pub enum AuthenticatedCoordinatorRequest {
|
|
AuthStatus,
|
|
RevokeCliSession,
|
|
CreateProject {
|
|
project: String,
|
|
name: String,
|
|
},
|
|
SelectProject {
|
|
project: String,
|
|
},
|
|
ListProjects,
|
|
RegisterAgentPublicKey {
|
|
agent: String,
|
|
public_key: String,
|
|
},
|
|
ListAgentPublicKeys,
|
|
RotateAgentPublicKey {
|
|
agent: String,
|
|
public_key: String,
|
|
},
|
|
RevokeAgentPublicKey {
|
|
agent: String,
|
|
},
|
|
CreateNodeEnrollmentGrant {
|
|
#[serde(default = "default_node_enrollment_ttl_seconds")]
|
|
ttl_seconds: u64,
|
|
},
|
|
ListNodeDescriptors,
|
|
RevokeNodeCredential {
|
|
node: String,
|
|
},
|
|
StartProcess {
|
|
process: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
launch_attempt: Option<String>,
|
|
#[serde(default)]
|
|
restart: bool,
|
|
},
|
|
ScheduleTask {
|
|
environment: Option<EnvironmentRequirements>,
|
|
environment_digest: Option<Digest>,
|
|
required_capabilities: Vec<Capability>,
|
|
#[serde(default)]
|
|
dependency_cache: Option<Digest>,
|
|
source_snapshot: Option<Digest>,
|
|
required_artifacts: Vec<String>,
|
|
prefer_node: Option<String>,
|
|
},
|
|
LaunchTask {
|
|
task_spec: Box<TaskSpec>,
|
|
#[serde(default)]
|
|
wait_for_node: bool,
|
|
artifact_path: String,
|
|
wasm_module_base64: String,
|
|
},
|
|
CancelProcess {
|
|
process: String,
|
|
},
|
|
AbortProcess {
|
|
process: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
launch_attempt: Option<String>,
|
|
},
|
|
ListProcesses,
|
|
QuotaStatus,
|
|
RestartTask {
|
|
process: String,
|
|
task: String,
|
|
#[serde(default)]
|
|
replacement_bundle: Option<TaskReplacementBundle>,
|
|
},
|
|
ResolveTaskFailure {
|
|
process: String,
|
|
task: String,
|
|
resolution: TaskFailureResolution,
|
|
},
|
|
DebugAttach {
|
|
process: String,
|
|
},
|
|
SetDebugBreakpoints {
|
|
process: String,
|
|
#[serde(default)]
|
|
revision: u64,
|
|
probe_symbols: Vec<String>,
|
|
},
|
|
InspectDebugBreakpoints {
|
|
process: String,
|
|
},
|
|
CreateDebugEpoch {
|
|
process: String,
|
|
stopped_task: String,
|
|
reason: String,
|
|
},
|
|
ResumeDebugEpoch {
|
|
process: String,
|
|
epoch: u64,
|
|
},
|
|
InspectDebugEpoch {
|
|
process: String,
|
|
epoch: u64,
|
|
},
|
|
ListTaskEvents {
|
|
#[serde(default)]
|
|
process: Option<String>,
|
|
},
|
|
ListTaskSnapshots {
|
|
process: String,
|
|
},
|
|
JoinTask {
|
|
process: String,
|
|
task: String,
|
|
},
|
|
CreateArtifactDownloadLink {
|
|
artifact: String,
|
|
max_bytes: u64,
|
|
#[serde(default = "default_download_ttl_seconds")]
|
|
ttl_seconds: u64,
|
|
},
|
|
OpenArtifactDownloadStream {
|
|
artifact: String,
|
|
max_bytes: u64,
|
|
token_digest: Digest,
|
|
chunk_bytes: u64,
|
|
},
|
|
RevokeArtifactDownloadLink {
|
|
artifact: String,
|
|
token_digest: Digest,
|
|
},
|
|
ExportArtifactToNode {
|
|
artifact: String,
|
|
receiver_node: String,
|
|
direct_connectivity: bool,
|
|
failure_reason: String,
|
|
},
|
|
}
|
|
|
|
fn default_download_ttl_seconds() -> u64 {
|
|
900
|
|
}
|
|
|
|
fn default_node_enrollment_ttl_seconds() -> u64 {
|
|
900
|
|
}
|