ironflow_cli/commands/
api_key.rs1use 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#[derive(Debug, Args)]
18pub struct ApiKeyArgs {
19 #[command(subcommand)]
21 pub command: ApiKeyCommands,
22}
23
24#[derive(Debug, Subcommand)]
26pub enum ApiKeyCommands {
27 List,
29 Create {
31 name: String,
33 #[arg(long = "scope", value_name = "SCOPE", required = true, value_parser = parse_scope)]
36 scopes: Vec<ApiKeyScope>,
37 #[arg(long)]
39 expires_at: Option<DateTime<Utc>>,
40 #[arg(long)]
43 rate_limit_override: Option<u32>,
44 },
45 Scopes,
47 Delete {
49 id: Uuid,
51 #[arg(long)]
53 yes: bool,
54 },
55}
56
57const ALL_SCOPES: [ApiKeyScope; 6] = [
59 ApiKeyScope::WorkflowsRead,
60 ApiKeyScope::RunsRead,
61 ApiKeyScope::RunsWrite,
62 ApiKeyScope::RunsManage,
63 ApiKeyScope::StatsRead,
64 ApiKeyScope::Admin,
65];
66
67fn parse_scope(raw: &str) -> Result<ApiKeyScope, String> {
73 parse_enum(raw, &ALL_SCOPES, "scope")
74}
75
76pub 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}