Files
horus/lib/models/job/lib.rs

324 lines
10 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 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,
) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4().to_string(),
caller_id,
context_id,
payload,
runner,
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.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,
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(),
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 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()));
}
let mut job = Job::new(
self.caller_id,
self.context_id,
self.payload,
self.runner,
);
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()
}
}