339 lines
11 KiB
Rust
339 lines
11 KiB
Rust
use chrono::{Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use thiserror::Error;
|
|
use uuid::Uuid;
|
|
use log::{error};
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
use wasm_bindgen::prelude::*;
|
|
|
|
/// Signature for a job - contains the signatory's public key and their signature
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct JobSignature {
|
|
/// Public key of the signatory (hex-encoded secp256k1 public key)
|
|
pub public_key: String,
|
|
/// Signature (hex-encoded secp256k1 signature)
|
|
pub signature: String,
|
|
}
|
|
|
|
/// Job status enumeration
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub enum JobStatus {
|
|
Created,
|
|
Dispatched,
|
|
WaitingForPrerequisites,
|
|
Started,
|
|
Error,
|
|
Stopping,
|
|
Finished,
|
|
}
|
|
|
|
/// Job result response for RPC calls
|
|
#[derive(Debug, Serialize, Clone)]
|
|
#[serde(untagged)]
|
|
pub enum JobResult {
|
|
Success { success: String },
|
|
Error { error: String },
|
|
}
|
|
|
|
impl JobStatus {
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self {
|
|
JobStatus::Created => "created",
|
|
JobStatus::Dispatched => "dispatched",
|
|
JobStatus::WaitingForPrerequisites => "waiting_for_prerequisites",
|
|
JobStatus::Started => "started",
|
|
JobStatus::Error => "error",
|
|
JobStatus::Stopping => "stopping",
|
|
JobStatus::Finished => "finished",
|
|
}
|
|
}
|
|
|
|
pub fn from_str(s: &str) -> Option<Self> {
|
|
match s {
|
|
"created" => Some(JobStatus::Created),
|
|
"dispatched" => Some(JobStatus::Dispatched),
|
|
"waiting_for_prerequisites" => Some(JobStatus::WaitingForPrerequisites),
|
|
"started" => Some(JobStatus::Started),
|
|
"error" => Some(JobStatus::Error),
|
|
"stopping" => Some(JobStatus::Stopping),
|
|
"finished" => Some(JobStatus::Finished),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Job structure representing a unit of work to be executed
|
|
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter_with_clone))]
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Job {
|
|
pub id: String,
|
|
pub caller_id: String,
|
|
pub context_id: String,
|
|
pub payload: String,
|
|
pub runner: String, // name of the runner to execute this job
|
|
pub executor: String, // name of the executor the runner will use to execute this job
|
|
pub timeout: u64, // timeout in seconds
|
|
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))]
|
|
pub env_vars: HashMap<String, String>, // environment variables for script execution
|
|
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))]
|
|
pub created_at: chrono::DateTime<chrono::Utc>,
|
|
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))]
|
|
pub updated_at: chrono::DateTime<chrono::Utc>,
|
|
|
|
/// Signatures from authorized signatories (public keys are included in each signature)
|
|
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))]
|
|
pub signatures: Vec<JobSignature>,
|
|
}
|
|
|
|
/// Error types for job operations
|
|
#[derive(Error, Debug)]
|
|
pub enum JobError {
|
|
#[error("Serialization error: {0}")]
|
|
Serialization(#[from] serde_json::Error),
|
|
#[error("Job not found: {0}")]
|
|
NotFound(String),
|
|
#[error("Invalid data: {0}")]
|
|
InvalidData(String),
|
|
#[error("Validation error: {0}")]
|
|
Validation(String),
|
|
#[error("Signature verification failed: {0}")]
|
|
SignatureVerification(String),
|
|
}
|
|
|
|
impl Job {
|
|
/// Create a new job with the given parameters
|
|
pub fn new(
|
|
caller_id: String,
|
|
context_id: String,
|
|
payload: String,
|
|
runner: String,
|
|
executor: String,
|
|
) -> Self {
|
|
let now = Utc::now();
|
|
Self {
|
|
id: Uuid::new_v4().to_string(),
|
|
caller_id,
|
|
context_id,
|
|
payload,
|
|
runner,
|
|
executor,
|
|
timeout: 300, // 5 minutes default
|
|
env_vars: HashMap::new(),
|
|
created_at: now,
|
|
updated_at: now,
|
|
signatures: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Get the canonical representation of the job for signing
|
|
/// This creates a deterministic string representation that can be hashed and signed
|
|
/// Note: Signatures are excluded from the canonical representation
|
|
pub fn canonical_representation(&self) -> String {
|
|
// Create a deterministic representation excluding signatures
|
|
// Sort env_vars keys for deterministic ordering
|
|
let mut env_vars_sorted: Vec<_> = self.env_vars.iter().collect();
|
|
env_vars_sorted.sort_by_key(|&(k, _)| k);
|
|
|
|
format!(
|
|
"{}:{}:{}:{}:{}:{}:{}:{:?}",
|
|
self.id,
|
|
self.caller_id,
|
|
self.context_id,
|
|
self.payload,
|
|
self.runner,
|
|
self.executor,
|
|
self.timeout,
|
|
env_vars_sorted
|
|
)
|
|
}
|
|
|
|
/// Get list of signatory public keys from signatures
|
|
pub fn signatories(&self) -> Vec<String> {
|
|
self.signatures.iter()
|
|
.map(|sig| sig.public_key.clone())
|
|
.collect()
|
|
}
|
|
|
|
/// Verify that all signatures are valid
|
|
/// Returns Ok(()) if verification passes, Err otherwise
|
|
/// Empty signatures list is allowed - loop simply won't execute
|
|
pub fn verify_signatures(&self) -> Result<(), JobError> {
|
|
use secp256k1::{Message, PublicKey, Secp256k1, ecdsa::Signature};
|
|
use sha2::{Sha256, Digest};
|
|
|
|
// Get the canonical representation and hash it
|
|
let canonical = self.canonical_representation();
|
|
let mut hasher = Sha256::new();
|
|
hasher.update(canonical.as_bytes());
|
|
let hash = hasher.finalize();
|
|
|
|
let secp = Secp256k1::verification_only();
|
|
let message = Message::from_digest_slice(&hash)
|
|
.map_err(|e| JobError::SignatureVerification(format!("Invalid message: {}", e)))?;
|
|
|
|
// Verify each signature (if any)
|
|
for sig_data in &self.signatures {
|
|
// Decode public key
|
|
let pubkey_bytes = hex::decode(&sig_data.public_key)
|
|
.map_err(|e| JobError::SignatureVerification(format!("Invalid public key hex: {}", e)))?;
|
|
let pubkey = PublicKey::from_slice(&pubkey_bytes)
|
|
.map_err(|e| JobError::SignatureVerification(format!("Invalid public key: {}", e)))?;
|
|
|
|
// Decode signature
|
|
let sig_bytes = hex::decode(&sig_data.signature)
|
|
.map_err(|e| JobError::SignatureVerification(format!("Invalid signature hex: {}", e)))?;
|
|
let signature = Signature::from_compact(&sig_bytes)
|
|
.map_err(|e| JobError::SignatureVerification(format!("Invalid signature: {}", e)))?;
|
|
|
|
// Verify signature
|
|
secp.verify_ecdsa(&message, &signature, &pubkey)
|
|
.map_err(|e| JobError::SignatureVerification(format!("Signature verification failed: {}", e)))?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Builder for constructing job execution requests.
|
|
pub struct JobBuilder {
|
|
caller_id: String,
|
|
context_id: String,
|
|
payload: String,
|
|
runner: String,
|
|
executor: String,
|
|
timeout: u64, // timeout in seconds
|
|
env_vars: HashMap<String, String>,
|
|
signatures: Vec<JobSignature>,
|
|
}
|
|
|
|
impl JobBuilder {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
caller_id: "".to_string(),
|
|
context_id: "".to_string(),
|
|
payload: "".to_string(),
|
|
runner: "".to_string(),
|
|
executor: "".to_string(),
|
|
timeout: 300, // 5 minutes default
|
|
env_vars: HashMap::new(),
|
|
signatures: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Set the caller ID for this job
|
|
pub fn caller_id(mut self, caller_id: &str) -> Self {
|
|
self.caller_id = caller_id.to_string();
|
|
self
|
|
}
|
|
|
|
/// Set the context ID for this job
|
|
pub fn context_id(mut self, context_id: &str) -> Self {
|
|
self.context_id = context_id.to_string();
|
|
self
|
|
}
|
|
|
|
/// Set the payload (script content) for this job
|
|
pub fn payload(mut self, payload: &str) -> Self {
|
|
self.payload = payload.to_string();
|
|
self
|
|
}
|
|
|
|
/// Set the runner name for this job
|
|
pub fn runner(mut self, runner: &str) -> Self {
|
|
self.runner = runner.to_string();
|
|
self
|
|
}
|
|
|
|
/// Set the executor for this job
|
|
pub fn executor(mut self, executor: &str) -> Self {
|
|
self.executor = executor.to_string();
|
|
self
|
|
}
|
|
|
|
/// Set the timeout for job execution (in seconds)
|
|
pub fn timeout(mut self, timeout: u64) -> Self {
|
|
self.timeout = timeout;
|
|
self
|
|
}
|
|
|
|
/// Set a single environment variable
|
|
pub fn env_var(mut self, key: &str, value: &str) -> Self {
|
|
self.env_vars.insert(key.to_string(), value.to_string());
|
|
self
|
|
}
|
|
|
|
/// Set multiple environment variables from a HashMap
|
|
pub fn env_vars(mut self, env_vars: HashMap<String, String>) -> Self {
|
|
self.env_vars = env_vars;
|
|
self
|
|
}
|
|
|
|
/// Clear all environment variables
|
|
pub fn clear_env_vars(mut self) -> Self {
|
|
self.env_vars.clear();
|
|
self
|
|
}
|
|
|
|
/// Add a signature (public key and signature)
|
|
pub fn signature(mut self, public_key: &str, signature: &str) -> Self {
|
|
self.signatures.push(JobSignature {
|
|
public_key: public_key.to_string(),
|
|
signature: signature.to_string(),
|
|
});
|
|
self
|
|
}
|
|
|
|
/// Set multiple signatures
|
|
pub fn signatures(mut self, signatures: Vec<JobSignature>) -> Self {
|
|
self.signatures = signatures;
|
|
self
|
|
}
|
|
|
|
/// Clear all signatures
|
|
pub fn clear_signatures(mut self) -> Self {
|
|
self.signatures.clear();
|
|
self
|
|
}
|
|
|
|
/// Build the job
|
|
pub fn build(self) -> Result<Job, JobError> {
|
|
if self.caller_id.is_empty() {
|
|
return Err(JobError::InvalidData("caller_id is required".to_string()));
|
|
}
|
|
if self.context_id.is_empty() {
|
|
return Err(JobError::InvalidData("context_id is required".to_string()));
|
|
}
|
|
if self.payload.is_empty() {
|
|
return Err(JobError::InvalidData("payload is required".to_string()));
|
|
}
|
|
if self.runner.is_empty() {
|
|
return Err(JobError::InvalidData("runner is required".to_string()));
|
|
}
|
|
if self.executor.is_empty() {
|
|
return Err(JobError::InvalidData("executor is required".to_string()));
|
|
}
|
|
|
|
let mut job = Job::new(
|
|
self.caller_id,
|
|
self.context_id,
|
|
self.payload,
|
|
self.runner,
|
|
self.executor,
|
|
);
|
|
|
|
job.timeout = self.timeout;
|
|
job.env_vars = self.env_vars;
|
|
job.signatures = self.signatures;
|
|
|
|
Ok(job)
|
|
}
|
|
}
|
|
|
|
impl Default for JobBuilder {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|