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; 6] = [
59    ApiKeyScope::WorkflowsRead,
60    ApiKeyScope::RunsRead,
61    ApiKeyScope::RunsWrite,
62    ApiKeyScope::RunsManage,
63    ApiKeyScope::StatsRead,
64    ApiKeyScope::Admin,
65];
66
67/// Parse a `--scope` value, listing the accepted values on failure.
68///
69/// # Errors
70///
71/// Returns the list of accepted scopes when `raw` is not one of them.
72fn parse_scope(raw: &str) -> Result<ApiKeyScope, String> {
73    parse_enum(raw, &ALL_SCOPES, "scope")
74}
75
76/// Execute an API key subcommand.
77///
78/// # Errors
79///
80/// Returns an error on API failure or when a destructive command is not
81/// confirmed.
82pub async fn execute(client: &IronflowClient, args: &ApiKeyArgs, json_mode: bool) -> Result<()> {
83    match &args.command {
84        ApiKeyCommands::List => {
85            let response = client.list_api_keys().await?;
86            output::print_output(json_mode, &response, || {
87                output::api_keys_table(&response.data)
88            })?;
89        }
90        ApiKeyCommands::Create {
91            name,
92            scopes,
93            expires_at,
94            rate_limit_override,
95        } => {
96            let mut builder = CreateApiKeyRequest::builder()
97                .name(name.clone())
98                .scopes(scopes.clone())
99                .expires_at(*expires_at);
100            if let Some(val) = rate_limit_override {
101                builder = builder.rate_limit_override(*val as i32);
102            }
103            let request: CreateApiKeyRequest = builder
104                .try_into()
105                .context("failed to build CreateApiKeyRequest")?;
106
107            let response = client.create_api_key(&request).await?;
108
109            if !json_mode {
110                let mut stderr = std::io::stderr();
111                writeln!(stderr, "This is the only time the key is shown.")?;
112            }
113
114            output::print_output(json_mode, &response, || {
115                output::created_api_key_table(&response.data)
116            })?;
117        }
118        ApiKeyCommands::Scopes => {
119            let response = client.available_scopes().await?;
120            output::print_output(json_mode, &response, || {
121                output::scopes_table(&response.data)
122            })?;
123        }
124        ApiKeyCommands::Delete { id, yes } => {
125            confirm(&format!("Delete API key '{id}'?"), *yes)?;
126            client.delete_api_key(*id).await?;
127            output::report_deletion(json_mode, "api-key", id.to_string())?;
128        }
129    }
130    Ok(())
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn parse_scope_accepts_every_declared_scope() {
139        for scope in ALL_SCOPES {
140            let raw = scope.to_string();
141            assert_eq!(parse_scope(&raw).unwrap(), scope);
142        }
143    }
144
145    #[test]
146    fn parse_scope_rejects_an_unknown_value_and_lists_the_valid_ones() {
147        let err = parse_scope("root").unwrap_err();
148        assert!(err.contains("unknown scope 'root'"), "{err}");
149        assert!(err.contains("runs_read"), "{err}");
150        assert!(err.contains("admin"), "{err}");
151    }
152
153    #[test]
154    fn parse_scope_is_case_sensitive() {
155        assert!(parse_scope("ADMIN").is_err());
156    }
157}