Skip to main content

ironflow_cli/commands/
api_key.rs

1//! API key subcommands: list, create, scopes, delete.
2
3use std::io::Write as _;
4
5use anyhow::{Context, Result};
6use chrono::{DateTime, Utc};
7use clap::{Args, Subcommand};
8use ironflow_sdk::IronflowClient;
9use ironflow_sdk::types::{ApiKeyScope, CreateApiKeyRequest};
10use uuid::Uuid;
11
12use crate::commands::parse_enum;
13use crate::confirm::confirm;
14use crate::output;
15
16/// Arguments for the `api-key` command group.
17#[derive(Debug, Args)]
18pub struct ApiKeyArgs {
19    /// API key subcommand.
20    #[command(subcommand)]
21    pub command: ApiKeyCommands,
22}
23
24/// Available API key subcommands.
25#[derive(Debug, Subcommand)]
26pub enum ApiKeyCommands {
27    /// List API keys. The raw key is never listed, only its prefix.
28    List,
29    /// Create an API key. The raw key is printed once and never again.
30    Create {
31        /// Human-readable name for the key.
32        name: String,
33        /// Scope to grant. Repeat for several scopes. Run `api-key scopes`
34        /// to list the accepted values.
35        #[arg(long = "scope", value_name = "SCOPE", required = true, value_parser = parse_scope)]
36        scopes: Vec<ApiKeyScope>,
37        /// Expiration date (RFC 3339, e.g. `2026-12-31T23:59:59Z`).
38        #[arg(long)]
39        expires_at: Option<DateTime<Utc>>,
40        /// Per-key rate limit override (requests per minute). 0 disables
41        /// rate limiting for this key.
42        #[arg(long)]
43        rate_limit_override: Option<u32>,
44    },
45    /// List the scopes an API key can be granted.
46    Scopes,
47    /// Delete an API key.
48    Delete {
49        /// API key UUID.
50        id: Uuid,
51        /// Skip the interactive confirmation.
52        #[arg(long)]
53        yes: bool,
54    },
55}
56
57/// Every scope the API accepts, in the order the enum declares them.
58const ALL_SCOPES: [ApiKeyScope; 9] = [
59    ApiKeyScope::WorkflowsRead,
60    ApiKeyScope::RunsRead,
61    ApiKeyScope::RunsWrite,
62    ApiKeyScope::RunsManage,
63    ApiKeyScope::StatsRead,
64    ApiKeyScope::AccountsRead,
65    ApiKeyScope::AccountsManage,
66    ApiKeyScope::SignalsSend,
67    ApiKeyScope::Admin,
68];
69
70/// Parse a `--scope` value, listing the accepted values on failure.
71///
72/// # Errors
73///
74/// Returns the list of accepted scopes when `raw` is not one of them.
75fn parse_scope(raw: &str) -> Result<ApiKeyScope, String> {
76    parse_enum(raw, &ALL_SCOPES, "scope")
77}
78
79/// Execute an API key subcommand.
80///
81/// # Errors
82///
83/// Returns an error on API failure or when a destructive command is not
84/// confirmed.
85pub async fn execute(client: &IronflowClient, args: &ApiKeyArgs, json_mode: bool) -> Result<()> {
86    match &args.command {
87        ApiKeyCommands::List => {
88            let response = client.list_api_keys().await?;
89            output::print_output(json_mode, &response, || {
90                output::api_keys_table(&response.data)
91            })?;
92        }
93        ApiKeyCommands::Create {
94            name,
95            scopes,
96            expires_at,
97            rate_limit_override,
98        } => {
99            let mut builder = CreateApiKeyRequest::builder()
100                .name(name.clone())
101                .scopes(scopes.clone())
102                .expires_at(*expires_at);
103            if let Some(val) = rate_limit_override {
104                builder = builder.rate_limit_override(*val as i32);
105            }
106            let request: CreateApiKeyRequest = builder
107                .try_into()
108                .context("failed to build CreateApiKeyRequest")?;
109
110            let response = client.create_api_key(&request).await?;
111
112            if !json_mode {
113                let mut stderr = std::io::stderr();
114                writeln!(stderr, "This is the only time the key is shown.")?;
115            }
116
117            output::print_output(json_mode, &response, || {
118                output::created_api_key_table(&response.data)
119            })?;
120        }
121        ApiKeyCommands::Scopes => {
122            let response = client.available_scopes().await?;
123            output::print_output(json_mode, &response, || {
124                output::scopes_table(&response.data)
125            })?;
126        }
127        ApiKeyCommands::Delete { id, yes } => {
128            confirm(&format!("Delete API key '{id}'?"), *yes)?;
129            client.delete_api_key(*id).await?;
130            output::report_deletion(json_mode, "api-key", id.to_string())?;
131        }
132    }
133    Ok(())
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn parse_scope_accepts_every_declared_scope() {
142        for scope in ALL_SCOPES {
143            let raw = scope.to_string();
144            assert_eq!(parse_scope(&raw).unwrap(), scope);
145        }
146    }
147
148    #[test]
149    fn parse_scope_rejects_an_unknown_value_and_lists_the_valid_ones() {
150        let err = parse_scope("root").unwrap_err();
151        assert!(err.contains("unknown scope 'root'"), "{err}");
152        assert!(err.contains("runs_read"), "{err}");
153        assert!(err.contains("admin"), "{err}");
154    }
155
156    #[test]
157    fn parse_scope_is_case_sensitive() {
158        assert!(parse_scope("ADMIN").is_err());
159    }
160}