Skip to main content

ironflow_cli/commands/
audit_log.rs

1//! Audit log subcommands: list.
2
3use anyhow::Result;
4use chrono::{DateTime, Utc};
5use clap::{Args, Subcommand};
6use ironflow_sdk::IronflowClient;
7use ironflow_sdk::client::ListAuditLogsFilter;
8use ironflow_sdk::types::EventKind;
9use uuid::Uuid;
10
11use crate::commands::parse_enum;
12use crate::output;
13
14/// Arguments for the `audit-log` command group.
15#[derive(Debug, Args)]
16pub struct AuditLogArgs {
17    /// Audit log subcommand.
18    #[command(subcommand)]
19    pub command: AuditLogCommands,
20}
21
22/// Available audit log subcommands.
23#[derive(Debug, Subcommand)]
24pub enum AuditLogCommands {
25    /// List audit log entries with optional filters.
26    List {
27        /// Only entries attached to this run.
28        #[arg(long = "run", value_name = "UUID")]
29        run: Option<Uuid>,
30        /// Only entries of this event type (e.g. `run_created`).
31        #[arg(long = "type", value_name = "KIND", value_parser = parse_event_kind)]
32        event_type: Option<EventKind>,
33        /// Only entries recorded at or after this instant (RFC 3339).
34        #[arg(long)]
35        from: Option<DateTime<Utc>>,
36        /// Only entries recorded at or before this instant (RFC 3339).
37        #[arg(long)]
38        to: Option<DateTime<Utc>>,
39        /// Page number (1-based).
40        #[arg(long)]
41        page: Option<u32>,
42        /// Items per page.
43        #[arg(long)]
44        per_page: Option<u32>,
45    },
46}
47
48/// Every event kind the API records, in the order the enum declares them.
49const ALL_EVENT_KINDS: [EventKind; 21] = [
50    EventKind::RunCreated,
51    EventKind::RunStatusChanged,
52    EventKind::RunFailed,
53    EventKind::RunBudgetExceeded,
54    EventKind::StepCompleted,
55    EventKind::StepFailed,
56    EventKind::ApprovalRequested,
57    EventKind::ApprovalGranted,
58    EventKind::ApprovalRejected,
59    EventKind::ApprovalEscalated,
60    EventKind::LogLine,
61    EventKind::UserSignedIn,
62    EventKind::UserSignedUp,
63    EventKind::UserSignedOut,
64    EventKind::ProviderAccountUpdated,
65    EventKind::SignalAwaited,
66    EventKind::SignalReceived,
67    EventKind::ScheduleOccurrencesMissed,
68    EventKind::WorkflowPaused,
69    EventKind::WorkflowResumed,
70    EventKind::AgentRoleCostCapUpdated,
71];
72
73/// Parse a `--type` value, listing the accepted values on failure.
74///
75/// # Errors
76///
77/// Returns the list of accepted event kinds when `raw` is not one of them.
78fn parse_event_kind(raw: &str) -> Result<EventKind, String> {
79    parse_enum(raw, &ALL_EVENT_KINDS, "event type")
80}
81
82/// Execute an audit log subcommand.
83///
84/// # Errors
85///
86/// Returns an error on API failure, including 403 for non-admin callers.
87pub async fn execute(client: &IronflowClient, args: &AuditLogArgs, json_mode: bool) -> Result<()> {
88    match &args.command {
89        AuditLogCommands::List {
90            run,
91            event_type,
92            from,
93            to,
94            page,
95            per_page,
96        } => {
97            let event_type = event_type.as_ref().map(ToString::to_string);
98            let filter = ListAuditLogsFilter {
99                run_id: *run,
100                event_type: event_type.as_deref(),
101                from: *from,
102                to: *to,
103                page: *page,
104                per_page: *per_page,
105            };
106
107            let response = client.list_audit_logs_filtered(&filter).await?;
108            output::print_output(json_mode, &response, || {
109                output::audit_logs_table(&response.data)
110            })?;
111        }
112    }
113    Ok(())
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn parse_event_kind_accepts_every_declared_kind() {
122        for kind in ALL_EVENT_KINDS {
123            let raw = kind.to_string();
124            assert_eq!(parse_event_kind(&raw).unwrap(), kind);
125        }
126    }
127
128    #[test]
129    fn parse_event_kind_rejects_an_unknown_value_and_lists_the_valid_ones() {
130        let err = parse_event_kind("run_exploded").unwrap_err();
131        assert!(err.contains("unknown event type 'run_exploded'"), "{err}");
132        assert!(err.contains("run_created"), "{err}");
133    }
134}