ironflow_api/routes/api_keys/
create.rs1use axum::Json;
4use axum::extract::State;
5use axum::http::StatusCode;
6use axum::response::IntoResponse;
7use chrono::{DateTime, Utc};
8use ironflow_auth::extractor::AuthenticatedUser;
9use ironflow_auth::password;
10use ironflow_store::entities::{ApiKeyScope, NewApiKey};
11use rand::Rng;
12use serde::{Deserialize, Serialize};
13use uuid::Uuid;
14
15use crate::error::ApiError;
16use crate::response::ok;
17use crate::state::AppState;
18use ironflow_auth::extractor::{API_KEY_PREFIX, API_KEY_SUFFIX_LEN};
19
20const MAX_API_KEY_NAME_LEN: usize = 255;
23
24#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
26#[derive(Debug, Deserialize)]
27pub struct CreateApiKeyRequest {
28 pub name: String,
30 pub scopes: Vec<ApiKeyScope>,
32 pub expires_at: Option<DateTime<Utc>>,
34 pub rate_limit_override: Option<u32>,
38}
39
40#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
43#[derive(Debug, Serialize)]
44pub struct CreateApiKeyResponse {
45 pub id: Uuid,
47 pub key: String,
49 pub key_prefix: String,
51 pub name: String,
53 pub scopes: Vec<ApiKeyScope>,
55 pub expires_at: Option<DateTime<Utc>>,
57 pub rate_limit_override: Option<u32>,
59 pub created_at: DateTime<Utc>,
61}
62
63#[cfg_attr(
69 feature = "openapi",
70 utoipa::path(
71 post,
72 path = "/api/v1/api-keys",
73 tags = ["api-keys"],
74 request_body(content = CreateApiKeyRequest, description = "API key configuration"),
75 responses(
76 (status = 201, description = "API key created successfully", body = CreateApiKeyResponse),
77 (status = 400, description = "Invalid input"),
78 (status = 401, description = "Unauthorized"),
79 (status = 403, description = "Forbidden (member assigning forbidden scopes or a rate_limit_override)")
80 ),
81 security(("Bearer" = []))
82 )
83)]
84pub async fn create_api_key(
85 user: AuthenticatedUser,
86 State(state): State<AppState>,
87 Json(req): Json<CreateApiKeyRequest>,
88) -> Result<impl IntoResponse, ApiError> {
89 let name = validate_name(&req.name)?.to_string();
90
91 if req.scopes.is_empty() {
92 return Err(ApiError::BadRequest(
93 "at least one scope is required".to_string(),
94 ));
95 }
96
97 if !user.is_admin && !ApiKeyScope::all_allowed_for_member(&req.scopes) {
98 return Err(ApiError::Forbidden);
99 }
100
101 if !user.is_admin && req.rate_limit_override.is_some() {
103 return Err(ApiError::Forbidden);
104 }
105
106 if let Some(override_val) = req.rate_limit_override
107 && override_val > 10_000
108 {
109 return Err(ApiError::BadRequest(
110 "rate_limit_override must be between 0 and 10000".to_string(),
111 ));
112 }
113
114 let raw_key = generate_api_key();
115 let key_prefix = raw_key[..API_KEY_PREFIX.len() + API_KEY_SUFFIX_LEN].to_string();
116 let key_hash =
117 password::hash(&raw_key).map_err(|e| ApiError::Internal(format!("hashing: {e}")))?;
118
119 let api_key = state
120 .store
121 .create_api_key(NewApiKey {
122 user_id: user.user_id,
123 name,
124 key_hash,
125 key_prefix: key_prefix.clone(),
126 scopes: req.scopes,
127 expires_at: req.expires_at,
128 rate_limit_override: req.rate_limit_override,
129 })
130 .await
131 .map_err(ApiError::from)?;
132
133 let response = CreateApiKeyResponse {
134 id: api_key.id,
135 key: raw_key,
136 key_prefix,
137 name: api_key.name,
138 scopes: api_key.scopes,
139 expires_at: api_key.expires_at,
140 rate_limit_override: api_key.rate_limit_override,
141 created_at: api_key.created_at,
142 };
143
144 Ok((StatusCode::CREATED, ok(response)))
145}
146
147fn validate_name(name: &str) -> Result<&str, ApiError> {
152 let name = name.trim();
153 if name.is_empty() {
154 return Err(ApiError::BadRequest("name must not be empty".to_string()));
155 }
156 if name.chars().count() > MAX_API_KEY_NAME_LEN {
157 return Err(ApiError::BadRequest(format!(
158 "name must be at most {MAX_API_KEY_NAME_LEN} characters"
159 )));
160 }
161 Ok(name)
162}
163
164fn generate_api_key() -> String {
166 let mut rng = rand::rng();
167 let random_bytes: Vec<u8> = (0..32).map(|_| rng.random::<u8>()).collect();
168 let encoded = hex::encode(&random_bytes);
169 format!("{API_KEY_PREFIX}{encoded}")
170}
171
172#[cfg(test)]
173mod tests {
174 use axum::Router;
175 use axum::body::Body;
176 use axum::http::Request;
177 use axum::routing::post;
178 use http_body_util::BodyExt;
179 use serde_json::{Value as JsonValue, from_slice};
180 use tower::ServiceExt;
181
182 use super::*;
183 use crate::routes::test_helpers::{create_user_auth_header, test_state};
184
185 use std::sync::Arc;
186
187 use ironflow_auth::jwt::JwtConfig;
188 use ironflow_core::providers::claude::ClaudeCodeProvider;
189 use ironflow_engine::engine::Engine;
190 use ironflow_engine::notify::Event;
191 use ironflow_store::entities::NewUser;
192 use ironflow_store::memory::InMemoryStore;
193 use ironflow_store::store::Store;
194 use tokio::sync::broadcast;
195
196 async fn admin_state() -> (AppState, AuthenticatedUser) {
197 let store: Arc<dyn Store> = Arc::new(InMemoryStore::new());
198 let provider = Arc::new(ClaudeCodeProvider::new());
199 let engine = Arc::new(Engine::new(store.clone(), provider));
200 let jwt_config = Arc::new(JwtConfig {
201 secret: "test-secret-for-api-keys".to_string(),
202 access_token_ttl_secs: 900,
203 refresh_token_ttl_secs: 604800,
204 cookie_domain: None,
205 cookie_secure: false,
206 });
207 let (event_sender, _) = broadcast::channel::<Event>(1);
208 let state = AppState::new(
209 store.clone(),
210 engine,
211 jwt_config,
212 "test-worker-token".to_string(),
213 event_sender,
214 );
215
216 let user = store
217 .create_user(NewUser {
218 email: "admin@example.com".to_string(),
219 username: "admin".to_string(),
220 password_hash: password::hash("password123").expect("hash"),
221 is_admin: Some(true),
222 })
223 .await
224 .expect("create user");
225 let auth = AuthenticatedUser {
226 user_id: user.id,
227 username: user.username,
228 is_admin: true,
229 };
230 (state, auth)
231 }
232
233 fn request(name: String) -> CreateApiKeyRequest {
234 CreateApiKeyRequest {
235 name,
236 scopes: vec![ApiKeyScope::RunsRead],
237 expires_at: None,
238 rate_limit_override: None,
239 }
240 }
241
242 #[test]
243 fn validate_name_rejects_empty_and_whitespace() {
244 assert!(matches!(validate_name(""), Err(ApiError::BadRequest(_))));
245 assert!(matches!(validate_name(" "), Err(ApiError::BadRequest(_))));
246 }
247
248 #[test]
249 fn validate_name_accepts_exactly_255_chars() {
250 let name = "a".repeat(255);
251 assert_eq!(validate_name(&name).expect("valid"), name);
252 }
253
254 #[test]
255 fn validate_name_rejects_256_chars() {
256 let name = "a".repeat(256);
257 match validate_name(&name) {
258 Err(ApiError::BadRequest(msg)) => assert!(msg.contains("255")),
259 _ => panic!("expected BadRequest"),
260 }
261 }
262
263 #[test]
264 fn validate_name_trims_before_counting() {
265 let padded = format!(" {} ", "a".repeat(255));
266 let name = validate_name(&padded).expect("valid");
267 assert_eq!(name.len(), 255);
268 }
269
270 #[test]
271 fn validate_name_counts_chars_not_bytes() {
272 let ok = "é".repeat(255);
273 assert!(validate_name(&ok).is_ok());
274 let too_long = "é".repeat(256);
275 assert!(matches!(
276 validate_name(&too_long),
277 Err(ApiError::BadRequest(_))
278 ));
279 }
280
281 #[tokio::test]
282 async fn create_api_key_name_longer_than_255_returns_400() {
283 let (state, auth) = admin_state().await;
284 let user_id = auth.user_id;
285 let store = state.store.clone();
286
287 let result = create_api_key(auth, State(state), Json(request("a".repeat(300)))).await;
288 assert!(matches!(result, Err(ApiError::BadRequest(_))));
289
290 let keys = store
291 .list_api_keys_by_user(user_id)
292 .await
293 .expect("list keys");
294 assert!(keys.is_empty());
295 }
296
297 #[tokio::test]
298 async fn create_api_key_name_of_255_chars_returns_201() {
299 let (state, auth) = admin_state().await;
300
301 let response = create_api_key(auth, State(state), Json(request("a".repeat(255))))
302 .await
303 .expect("created")
304 .into_response();
305 assert_eq!(response.status(), StatusCode::CREATED);
306 }
307
308 #[tokio::test]
309 async fn create_api_key_stores_trimmed_name() {
310 let (state, auth) = admin_state().await;
311 let user_id = auth.user_id;
312 let store = state.store.clone();
313
314 let response = create_api_key(auth, State(state), Json(request(" key ".to_string())))
315 .await
316 .expect("created")
317 .into_response();
318 assert_eq!(response.status(), StatusCode::CREATED);
319
320 let keys = store
321 .list_api_keys_by_user(user_id)
322 .await
323 .expect("list keys");
324 assert_eq!(keys.len(), 1);
325 assert_eq!(keys[0].name, "key");
326 }
327
328 #[test]
329 fn generate_api_key_has_correct_prefix() {
330 let key = generate_api_key();
331 assert!(key.starts_with(API_KEY_PREFIX));
332 }
333
334 #[test]
335 fn generate_api_key_is_long_enough_for_prefix_extraction() {
336 let key = generate_api_key();
337 let expected_prefix_len = API_KEY_PREFIX.len() + API_KEY_SUFFIX_LEN;
338 assert!(
339 key.len() >= expected_prefix_len,
340 "key length {} is shorter than expected prefix length {}",
341 key.len(),
342 expected_prefix_len
343 );
344
345 let prefix = &key[..expected_prefix_len];
346 assert_eq!(prefix.len(), 13);
347 assert!(prefix.starts_with(API_KEY_PREFIX));
348 }
349
350 #[test]
351 fn generate_api_key_prefix_fits_varchar_16() {
352 let key = generate_api_key();
353 let prefix = &key[..API_KEY_PREFIX.len() + API_KEY_SUFFIX_LEN];
354 assert!(
355 prefix.len() <= 16,
356 "prefix length {} exceeds VARCHAR(16)",
357 prefix.len()
358 );
359 }
360
361 #[test]
362 fn generate_api_key_produces_unique_keys() {
363 let key1 = generate_api_key();
364 let key2 = generate_api_key();
365 assert_ne!(key1, key2);
366 }
367
368 async fn post_create(is_admin: bool, body: &str) -> (StatusCode, JsonValue) {
369 let state = test_state();
370 let username = if is_admin { "admin" } else { "member" };
371 let auth = create_user_auth_header(&state, username, is_admin).await;
372 let app = Router::new()
373 .route("/", post(create_api_key))
374 .with_state(state);
375 let req = Request::builder()
376 .method("POST")
377 .uri("/")
378 .header("authorization", auth)
379 .header("content-type", "application/json")
380 .body(Body::from(body.to_string()))
381 .unwrap();
382 let resp = app.oneshot(req).await.unwrap();
383 let status = resp.status();
384 let bytes = resp.into_body().collect().await.unwrap().to_bytes();
385 let json = if bytes.is_empty() {
386 JsonValue::Null
387 } else {
388 from_slice(&bytes).unwrap()
389 };
390 (status, json)
391 }
392
393 #[tokio::test]
394 async fn member_setting_rate_limit_override_is_forbidden() {
395 let body = r#"{"name":"k","scopes":["runs_read"],"rate_limit_override":0}"#;
396 let (status, _) = post_create(false, body).await;
397 assert_eq!(status, StatusCode::FORBIDDEN);
398 }
399
400 #[tokio::test]
401 async fn admin_can_set_rate_limit_override() {
402 let body = r#"{"name":"k","scopes":["runs_read"],"rate_limit_override":0}"#;
403 let (status, json) = post_create(true, body).await;
404 assert_eq!(status, StatusCode::CREATED);
405 assert_eq!(json["data"]["rate_limit_override"], 0);
406 }
407
408 #[tokio::test]
409 async fn member_without_rate_limit_override_can_create_key() {
410 let body = r#"{"name":"k","scopes":["runs_read"]}"#;
411 let (status, json) = post_create(false, body).await;
412 assert_eq!(status, StatusCode::CREATED);
413 assert!(json["data"]["rate_limit_override"].is_null());
414 }
415}