ironflow_api/routes/secrets/
list.rs1use axum::extract::{Query, State};
4use axum::response::IntoResponse;
5use serde::Deserialize;
6
7use ironflow_auth::extractor::Authenticated;
8
9use crate::entities::SecretResponse;
10use crate::error::ApiError;
11use crate::response::ok_paged;
12use crate::state::AppState;
13
14#[cfg_attr(feature = "openapi", derive(utoipa::IntoParams))]
16#[derive(Debug, Deserialize)]
17pub struct ListSecretsQuery {
18 pub prefix: Option<String>,
20 pub page: Option<u32>,
22 pub per_page: Option<u32>,
24}
25
26#[cfg_attr(
30 feature = "openapi",
31 utoipa::path(
32 get,
33 path = "/api/v1/secrets",
34 tags = ["secrets"],
35 params(ListSecretsQuery),
36 responses(
37 (status = 200, description = "Secrets listed", body = Vec<SecretResponse>),
38 (status = 401, description = "Unauthorized"),
39 (status = 403, description = "Forbidden")
40 ),
41 security(("Bearer" = []))
42 )
43)]
44pub async fn list_secrets(
45 auth: Authenticated,
46 State(state): State<AppState>,
47 Query(query): Query<ListSecretsQuery>,
48) -> Result<impl IntoResponse, ApiError> {
49 if !auth.is_admin() {
50 return Err(ApiError::Forbidden);
51 }
52
53 let prefix = query.prefix.as_deref().unwrap_or("");
54 let page = query.page.unwrap_or(1).max(1);
55 let per_page = query.per_page.unwrap_or(50).clamp(1, 100);
56
57 let result = state.store.list_secrets(prefix, page, per_page).await?;
58
59 let data: Vec<SecretResponse> = result.items.into_iter().map(SecretResponse::from).collect();
60
61 Ok(ok_paged(data, result.page, result.per_page, result.total))
62}
63
64#[cfg(test)]
65mod tests {
66 use axum::Router;
67 use axum::body::Body;
68 use axum::http::{Request, StatusCode};
69 use axum::routing::get;
70 use http_body_util::BodyExt;
71 use ironflow_auth::jwt::JwtConfig;
72 use ironflow_core::providers::claude::ClaudeCodeProvider;
73 use ironflow_engine::context::WorkflowContext;
74 use ironflow_engine::engine::Engine;
75 use ironflow_engine::handler::{HandlerFuture, WorkflowHandler};
76 use ironflow_engine::notify::Event;
77 use ironflow_store::memory::InMemoryStore;
78 use ironflow_store::store::Store;
79 use serde_json::Value as JsonValue;
80 use std::sync::Arc;
81 use tokio::sync::broadcast;
82 use tower::ServiceExt;
83
84 use super::*;
85 use crate::routes::test_helpers::create_user_auth_header;
86
87 struct TestWorkflow;
88
89 impl WorkflowHandler for TestWorkflow {
90 fn name(&self) -> &str {
91 "test-workflow"
92 }
93
94 fn execute<'a>(&'a self, _ctx: &'a mut WorkflowContext) -> HandlerFuture<'a> {
95 Box::pin(async move { Ok(()) })
96 }
97 }
98
99 fn test_jwt_config() -> Arc<JwtConfig> {
100 Arc::new(JwtConfig {
101 secret: "test-secret".to_string(),
102 access_token_ttl_secs: 900,
103 refresh_token_ttl_secs: 604800,
104 cookie_domain: None,
105 cookie_secure: false,
106 })
107 }
108
109 fn test_state() -> AppState {
110 let store: Arc<dyn Store> = Arc::new(InMemoryStore::new());
111 let provider = Arc::new(ClaudeCodeProvider::new());
112 let mut engine = Engine::new(store.clone(), provider);
113 engine.register(TestWorkflow).unwrap();
114 let (event_sender, _) = broadcast::channel::<Event>(1);
115 AppState::new(
116 store,
117 Arc::new(engine),
118 test_jwt_config(),
119 "test-worker-token".to_string(),
120 event_sender,
121 )
122 }
123
124 #[tokio::test]
125 async fn list_secrets_admin_only() {
126 let state = test_state();
127 let auth_header = create_user_auth_header(&state, "user", false).await;
128
129 let app = Router::new()
130 .route("/", get(list_secrets))
131 .with_state(state);
132
133 let req = Request::builder()
134 .uri("/")
135 .method("GET")
136 .header("authorization", auth_header)
137 .body(Body::empty())
138 .unwrap();
139
140 let resp = app.oneshot(req).await.unwrap();
141 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
142 }
143
144 #[tokio::test]
145 async fn list_secrets_admin_succeeds() {
146 let state = test_state();
147 let auth_header = create_user_auth_header(&state, "admin", true).await;
148
149 let app = Router::new()
150 .route("/", get(list_secrets))
151 .with_state(state);
152
153 let req = Request::builder()
154 .uri("/")
155 .method("GET")
156 .header("authorization", auth_header)
157 .body(Body::empty())
158 .unwrap();
159
160 let resp = app.oneshot(req).await.unwrap();
161 assert_eq!(resp.status(), StatusCode::OK);
162
163 let body = resp.into_body().collect().await.unwrap().to_bytes();
164 let json_val: JsonValue = serde_json::from_slice(&body).unwrap();
165 assert!(json_val["data"].is_array());
166 assert!(json_val["meta"].is_object());
167 }
168
169 #[tokio::test]
170 async fn list_secrets_includes_pagination() {
171 let state = test_state();
172 let auth_header = create_user_auth_header(&state, "admin", true).await;
173
174 let app = Router::new()
175 .route("/", get(list_secrets))
176 .with_state(state);
177
178 let req = Request::builder()
179 .uri("/?page=1&per_page=10")
180 .method("GET")
181 .header("authorization", auth_header)
182 .body(Body::empty())
183 .unwrap();
184
185 let resp = app.oneshot(req).await.unwrap();
186 assert_eq!(resp.status(), StatusCode::OK);
187
188 let body = resp.into_body().collect().await.unwrap().to_bytes();
189 let json_val: JsonValue = serde_json::from_slice(&body).unwrap();
190 assert_eq!(json_val["meta"]["page"], 1);
191 assert_eq!(json_val["meta"]["per_page"], 10);
192 }
193
194 #[tokio::test]
195 async fn list_secrets_clamps_per_page_max() {
196 let state = test_state();
197 let auth_header = create_user_auth_header(&state, "admin", true).await;
198
199 let app = Router::new()
200 .route("/", get(list_secrets))
201 .with_state(state);
202
203 let req = Request::builder()
204 .uri("/?per_page=500")
205 .method("GET")
206 .header("authorization", auth_header)
207 .body(Body::empty())
208 .unwrap();
209
210 let resp = app.oneshot(req).await.unwrap();
211 assert_eq!(resp.status(), StatusCode::OK);
212
213 let body = resp.into_body().collect().await.unwrap().to_bytes();
214 let json_val: JsonValue = serde_json::from_slice(&body).unwrap();
215 assert_eq!(json_val["meta"]["per_page"], 100);
216 }
217}