1use axum::{
4 extract::{Path, Query, State},
5 http::StatusCode,
6 Json,
7};
8use serde::{Deserialize, Serialize};
9
10use cloudillo_core::extract::{IdTag, OptionalRequestId};
11use cloudillo_types::identity_provider_adapter::{ApiKey, CreateApiKeyOptions, ListApiKeyOptions};
12use cloudillo_types::types::{
13 serialize_timestamp_iso, serialize_timestamp_iso_opt, ApiResponse, Timestamp,
14};
15
16use crate::prelude::*;
17
18fn split_id_tag_with_tenant(id_tag: &str, tenant_domain: &str) -> ClResult<(String, String)> {
20 let expected_suffix = format!(".{}", tenant_domain);
21 if id_tag.ends_with(&expected_suffix) {
22 let prefix = id_tag[..id_tag.len() - expected_suffix.len()].to_string();
23 if !prefix.is_empty() {
24 Ok((prefix, tenant_domain.to_string()))
25 } else {
26 Err(Error::ValidationError("Invalid id_tag: prefix cannot be empty".to_string()))
27 }
28 } else {
29 Err(Error::ValidationError(format!(
30 "Identity {} does not belong to this IDP domain {}",
31 id_tag, tenant_domain
32 )))
33 }
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase")]
39pub struct ApiKeyResponse {
40 pub id: i32,
41 pub id_tag: String,
42 pub key_prefix: String,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub name: Option<String>,
45 #[serde(serialize_with = "serialize_timestamp_iso")]
46 pub created_at: Timestamp,
47 #[serde(
48 skip_serializing_if = "Option::is_none",
49 serialize_with = "serialize_timestamp_iso_opt"
50 )]
51 pub last_used_at: Option<Timestamp>,
52 #[serde(
53 skip_serializing_if = "Option::is_none",
54 serialize_with = "serialize_timestamp_iso_opt"
55 )]
56 pub expires_at: Option<Timestamp>,
57}
58
59impl From<ApiKey> for ApiKeyResponse {
60 fn from(key: ApiKey) -> Self {
61 let id_tag = format!("{}.{}", key.id_tag_prefix, key.id_tag_domain);
63 Self {
64 id: key.id,
65 id_tag,
66 key_prefix: key.key_prefix.to_string(),
67 name: key.name.clone(),
68 created_at: key.created_at,
69 last_used_at: key.last_used_at,
70 expires_at: key.expires_at,
71 }
72 }
73}
74
75#[derive(Debug, Serialize)]
77#[serde(rename_all = "camelCase")]
78pub struct CreatedApiKeyResponse {
79 pub api_key: ApiKeyResponse,
80 pub plaintext_key: String,
81}
82
83#[derive(Debug, Deserialize)]
85#[serde(rename_all = "camelCase")]
86pub struct CreateApiKeyRequest {
87 pub id_tag: String,
89 pub name: Option<String>,
91 pub expires_at: Option<i64>,
93}
94
95#[derive(Debug, Deserialize, Default)]
97#[serde(rename_all = "camelCase")]
98pub struct ListApiKeysQuery {
99 pub id_tag: Option<String>,
101 pub limit: Option<u32>,
103 pub offset: Option<u32>,
105}
106
107#[axum::debug_handler]
109pub async fn create_api_key(
110 State(app): State<App>,
111 tn_id: TnId,
112 IdTag(_auth_id_tag): IdTag,
113 OptionalRequestId(req_id): OptionalRequestId,
114 Json(create_req): Json<CreateApiKeyRequest>,
115) -> ClResult<(StatusCode, Json<ApiResponse<CreatedApiKeyResponse>>)> {
116 let tenant_domain = app.auth_adapter.read_id_tag(tn_id).await?;
118
119 let expected_suffix = format!(".{}", tenant_domain);
121 if !create_req.id_tag.ends_with(&expected_suffix) {
122 return Err(Error::ValidationError(format!(
123 "Identity {} does not belong to this IDP domain {}",
124 create_req.id_tag, tenant_domain
125 )));
126 }
127
128 let id_tag_prefix =
130 create_req.id_tag[..create_req.id_tag.len() - expected_suffix.len()].to_string();
131 let id_tag_domain = tenant_domain.to_string();
132
133 info!(
134 id_tag_prefix = %id_tag_prefix,
135 id_tag_domain = %id_tag_domain,
136 name = ?create_req.name,
137 "POST /api/api-keys - Creating new API key"
138 );
139
140 let idp_adapter = app.idp_adapter.as_ref().ok_or(Error::ServiceUnavailable(
142 "Identity Provider not available on this instance".to_string(),
143 ))?;
144
145 if let Some(expires_timestamp) = create_req.expires_at {
147 let expiration = Timestamp(expires_timestamp);
148 if expiration.0 <= Timestamp::now().0 {
149 return Err(Error::ValidationError(
150 "Expiration time must be in the future".to_string(),
151 ));
152 }
153 }
154
155 let opts = CreateApiKeyOptions {
157 id_tag_prefix: &id_tag_prefix,
158 id_tag_domain: &id_tag_domain,
159 name: create_req.name.as_deref(),
160 expires_at: create_req.expires_at.map(Timestamp),
161 };
162
163 let created = idp_adapter.create_api_key(opts).await.map_err(|e| {
164 warn!("Failed to create API key: {}", e);
165 e
166 })?;
167
168 let response_data = CreatedApiKeyResponse {
169 api_key: ApiKeyResponse::from(created.api_key),
170 plaintext_key: created.plaintext_key,
171 };
172
173 let mut response = ApiResponse::new(response_data);
174 if let Some(id) = req_id {
175 response = response.with_req_id(id);
176 }
177
178 Ok((StatusCode::CREATED, Json(response)))
179}
180
181#[axum::debug_handler]
183pub async fn list_api_keys(
184 State(app): State<App>,
185 tn_id: TnId,
186 IdTag(_auth_id_tag): IdTag,
187 Query(query_params): Query<ListApiKeysQuery>,
188 OptionalRequestId(req_id): OptionalRequestId,
189) -> ClResult<(StatusCode, Json<ApiResponse<Vec<ApiKeyResponse>>>)> {
190 let id_tag = query_params
192 .id_tag
193 .as_ref()
194 .ok_or(Error::ValidationError("id_tag query parameter is required".to_string()))?;
195
196 let tenant_domain = app.auth_adapter.read_id_tag(tn_id).await?;
198
199 let (id_tag_prefix, id_tag_domain) = split_id_tag_with_tenant(id_tag, &tenant_domain)?;
201
202 info!(
203 id_tag_prefix = %id_tag_prefix,
204 id_tag_domain = %id_tag_domain,
205 "GET /api/api-keys - Listing API keys"
206 );
207
208 let idp_adapter = app.idp_adapter.as_ref().ok_or(Error::ServiceUnavailable(
210 "Identity Provider not available on this instance".to_string(),
211 ))?;
212
213 let opts = ListApiKeyOptions {
214 id_tag_prefix: Some(id_tag_prefix),
215 id_tag_domain: Some(id_tag_domain),
216 limit: query_params.limit,
217 offset: query_params.offset,
218 };
219
220 let keys = idp_adapter.list_api_keys(opts).await?;
221
222 let response_data: Vec<ApiKeyResponse> = keys.into_iter().map(ApiKeyResponse::from).collect();
223
224 let total = response_data.len();
225 let offset = query_params.offset.unwrap_or(0) as usize;
226 let limit = query_params.limit.unwrap_or(20) as usize;
227 let mut response = ApiResponse::with_pagination(response_data, offset, limit, total);
228 if let Some(id) = req_id {
229 response = response.with_req_id(id);
230 }
231
232 Ok((StatusCode::OK, Json(response)))
233}
234
235#[derive(Debug, Deserialize)]
237#[serde(rename_all = "camelCase")]
238pub struct ApiKeyIdTagQuery {
239 pub id_tag: String,
241}
242
243#[axum::debug_handler]
245pub async fn get_api_key(
246 State(app): State<App>,
247 tn_id: TnId,
248 IdTag(_auth_id_tag): IdTag,
249 Path(api_key_id): Path<i32>,
250 Query(query): Query<ApiKeyIdTagQuery>,
251 OptionalRequestId(req_id): OptionalRequestId,
252) -> ClResult<(StatusCode, Json<ApiResponse<ApiKeyResponse>>)> {
253 let tenant_domain = app.auth_adapter.read_id_tag(tn_id).await?;
255
256 let (id_tag_prefix, id_tag_domain) = split_id_tag_with_tenant(&query.id_tag, &tenant_domain)?;
258
259 info!(
260 api_key_id = %api_key_id,
261 id_tag_prefix = %id_tag_prefix,
262 id_tag_domain = %id_tag_domain,
263 "GET /api/idp/api-keys/:api_key_id - Getting API key"
264 );
265
266 let idp_adapter = app.idp_adapter.as_ref().ok_or(Error::ServiceUnavailable(
268 "Identity Provider not available on this instance".to_string(),
269 ))?;
270
271 let opts = ListApiKeyOptions {
273 id_tag_prefix: Some(id_tag_prefix),
274 id_tag_domain: Some(id_tag_domain),
275 limit: None,
276 offset: None,
277 };
278
279 let keys = idp_adapter.list_api_keys(opts).await?;
280 let key = keys.into_iter().find(|k| k.id == api_key_id).ok_or(Error::NotFound)?;
281
282 let response_data = ApiKeyResponse::from(key);
283 let mut response = ApiResponse::new(response_data);
284 if let Some(id) = req_id {
285 response = response.with_req_id(id);
286 }
287
288 Ok((StatusCode::OK, Json(response)))
289}
290
291#[axum::debug_handler]
293pub async fn delete_api_key(
294 State(app): State<App>,
295 tn_id: TnId,
296 IdTag(_auth_id_tag): IdTag,
297 Path(api_key_id): Path<i32>,
298 Query(query): Query<ApiKeyIdTagQuery>,
299 OptionalRequestId(req_id): OptionalRequestId,
300) -> ClResult<(StatusCode, Json<ApiResponse<()>>)> {
301 let tenant_domain = app.auth_adapter.read_id_tag(tn_id).await?;
303
304 let (id_tag_prefix, id_tag_domain) = split_id_tag_with_tenant(&query.id_tag, &tenant_domain)?;
306
307 info!(
308 api_key_id = %api_key_id,
309 id_tag_prefix = %id_tag_prefix,
310 id_tag_domain = %id_tag_domain,
311 "DELETE /api/idp/api-keys/:api_key_id - Deleting API key"
312 );
313
314 let idp_adapter = app.idp_adapter.as_ref().ok_or(Error::ServiceUnavailable(
316 "Identity Provider not available on this instance".to_string(),
317 ))?;
318
319 let deleted = idp_adapter
321 .delete_api_key_for_identity(api_key_id, &id_tag_prefix, &id_tag_domain)
322 .await
323 .map_err(|e| {
324 warn!("Failed to delete API key: {}", e);
325 e
326 })?;
327
328 if !deleted {
329 warn!(
330 api_key_id = %api_key_id,
331 id_tag_prefix = %id_tag_prefix,
332 id_tag_domain = %id_tag_domain,
333 "Attempted to delete non-existent or unowned API key"
334 );
335 return Err(Error::NotFound);
336 }
337
338 let mut response = ApiResponse::new(());
339 if let Some(id) = req_id {
340 response = response.with_req_id(id);
341 }
342
343 Ok((StatusCode::OK, Json(response)))
344}
345
346