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; 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
70fn parse_scope(raw: &str) -> Result<ApiKeyScope, String> {
76 parse_enum(raw, &ALL_SCOPES, "scope")
77}
78
79pub 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}