Skip to main content

tryaudex_core/
chain.rs

1use serde::{Deserialize, Serialize};
2
3/// A single step in a multi-step session chain.
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct ChainStep {
6    /// IAM actions allowed for this step (e.g. "s3:GetObject")
7    pub allow: String,
8    /// Optional resource restriction
9    pub resource: Option<String>,
10    /// Optional named profile
11    pub profile: Option<String>,
12    /// TTL for this step's credentials (defaults to parent TTL)
13    pub ttl: Option<String>,
14    /// Command to run in this step
15    pub command: Vec<String>,
16}
17
18/// Result of executing a single chain step.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct StepResult {
21    pub step_index: usize,
22    pub allow: String,
23    pub exit_code: i32,
24    pub stdout: String,
25    pub stderr: String,
26}
27
28/// Full result of a chain execution.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ChainResult {
31    pub steps: Vec<StepResult>,
32    pub total_steps: usize,
33    pub completed_steps: usize,
34    pub success: bool,
35}
36
37impl ChainResult {
38    pub fn new(total: usize) -> Self {
39        Self {
40            steps: Vec::new(),
41            total_steps: total,
42            completed_steps: 0,
43            success: true,
44        }
45    }
46
47    pub fn add_step(&mut self, result: StepResult) {
48        if result.exit_code != 0 {
49            self.success = false;
50        }
51        self.completed_steps += 1;
52        self.steps.push(result);
53    }
54}
55
56/// Parse a step specification string.
57/// Format: "allow:command" or just "allow" (command comes from CLI args).
58pub fn parse_step_spec(spec: &str) -> ChainStep {
59    ChainStep {
60        allow: spec.to_string(),
61        resource: None,
62        profile: None,
63        ttl: None,
64        command: Vec::new(),
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn test_parse_step_spec() {
74        let step = parse_step_spec("s3:GetObject,s3:ListBucket");
75        assert_eq!(step.allow, "s3:GetObject,s3:ListBucket");
76        assert!(step.resource.is_none());
77        assert!(step.command.is_empty());
78    }
79
80    #[test]
81    fn test_chain_result_success() {
82        let mut result = ChainResult::new(2);
83        result.add_step(StepResult {
84            step_index: 0,
85            allow: "s3:GetObject".to_string(),
86            exit_code: 0,
87            stdout: "ok".to_string(),
88            stderr: String::new(),
89        });
90        result.add_step(StepResult {
91            step_index: 1,
92            allow: "lambda:InvokeFunction".to_string(),
93            exit_code: 0,
94            stdout: "ok".to_string(),
95            stderr: String::new(),
96        });
97        assert!(result.success);
98        assert_eq!(result.completed_steps, 2);
99    }
100
101    #[test]
102    fn test_chain_result_failure_stops() {
103        let mut result = ChainResult::new(3);
104        result.add_step(StepResult {
105            step_index: 0,
106            allow: "s3:GetObject".to_string(),
107            exit_code: 0,
108            stdout: "ok".to_string(),
109            stderr: String::new(),
110        });
111        result.add_step(StepResult {
112            step_index: 1,
113            allow: "lambda:InvokeFunction".to_string(),
114            exit_code: 1,
115            stdout: String::new(),
116            stderr: "access denied".to_string(),
117        });
118        assert!(!result.success);
119        assert_eq!(result.completed_steps, 2);
120    }
121
122    #[test]
123    fn test_chain_result_serializable() {
124        let result = ChainResult::new(1);
125        let json = serde_json::to_string(&result).unwrap();
126        assert!(json.contains("\"total_steps\":1"));
127    }
128}