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 },
41 Scopes,
43 Delete {
45 id: Uuid,
47 #[arg(long)]
49 yes: bool,
50 },
51}
52
53const ALL_SCOPES: [ApiKeyScope; 6] = [
55 ApiKeyScope::WorkflowsRead,
56 ApiKeyScope::RunsRead,
57 ApiKeyScope::RunsWrite,
58 ApiKeyScope::RunsManage,
59 ApiKeyScope::StatsRead,
60 ApiKeyScope::Admin,
61];
62
63fn parse_scope(raw: &str) -> Result<ApiKeyScope, String> {
69 parse_enum(raw, &ALL_SCOPES, "scope")
70}
71
72pub async fn execute(client: &IronflowClient, args: &ApiKeyArgs, json_mode: bool) -> Result<()> {
79 match &args.command {
80 ApiKeyCommands::List => {
81 let response = client.list_api_keys().await?;
82 output::print_output(json_mode, &response, || {
83 output::api_keys_table(&response.data)
84 })?;
85 }
86 ApiKeyCommands::Create {
87 name,
88 scopes,
89 expires_at,
90 } => {
91 let request: CreateApiKeyRequest = CreateApiKeyRequest::builder()
92 .name(name.clone())
93 .scopes(scopes.clone())
94 .expires_at(*expires_at)
95 .try_into()
96 .context("failed to build CreateApiKeyRequest")?;
97
98 let response = client.create_api_key(&request).await?;
99
100 if !json_mode {
101 let mut stderr = std::io::stderr();
102 writeln!(stderr, "This is the only time the key is shown.")?;
103 }
104
105 output::print_output(json_mode, &response, || {
106 output::created_api_key_table(&response.data)
107 })?;
108 }
109 ApiKeyCommands::Scopes => {
110 let response = client.available_scopes().await?;
111 output::print_output(json_mode, &response, || {
112 output::scopes_table(&response.data)
113 })?;
114 }
115 ApiKeyCommands::Delete { id, yes } => {
116 confirm(&format!("Delete API key '{id}'?"), *yes)?;
117 client.delete_api_key(*id).await?;
118 output::report_deletion(json_mode, "api-key", id.to_string())?;
119 }
120 }
121 Ok(())
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn parse_scope_accepts_every_declared_scope() {
130 for scope in ALL_SCOPES {
131 let raw = scope.to_string();
132 assert_eq!(parse_scope(&raw).unwrap(), scope);
133 }
134 }
135
136 #[test]
137 fn parse_scope_rejects_an_unknown_value_and_lists_the_valid_ones() {
138 let err = parse_scope("root").unwrap_err();
139 assert!(err.contains("unknown scope 'root'"), "{err}");
140 assert!(err.contains("runs_read"), "{err}");
141 assert!(err.contains("admin"), "{err}");
142 }
143
144 #[test]
145 fn parse_scope_is_case_sensitive() {
146 assert!(parse_scope("ADMIN").is_err());
147 }
148}