Skip to main content

ironflow_api/routes/secrets/
list.rs

1//! `GET /api/v1/secrets` -- List secrets (admin only, never exposes values).
2
3use axum::extract::{Query, State};
4use axum::response::IntoResponse;
5use serde::Deserialize;
6
7use ironflow_auth::extractor::Authenticated;
8use ironflow_store::entities::ApiKeyScope;
9
10use crate::authorization::AuthorizeExt;
11use crate::entities::SecretResponse;
12use crate::error::ApiError;
13use crate::response::ok_paged;
14use crate::state::AppState;
15
16/// Query parameters for listing secrets.
17#[cfg_attr(feature = "openapi", derive(utoipa::IntoParams))]
18#[derive(Debug, Deserialize)]
19pub struct ListSecretsQuery {
20    /// Filter by key prefix (e.g. `workflows/inbox/`).
21    pub prefix: Option<String>,
22    /// Page number (1-based, default 1).
23    pub page: Option<u32>,
24    /// Items per page (default 50, max 100).
25    pub per_page: Option<u32>,
26}
27
28/// List secret metadata. Admin only.
29///
30/// Returns key, id, and timestamps. Never returns encrypted or decrypted values.
31#[cfg_attr(
32    feature = "openapi",
33    utoipa::path(
34        get,
35        path = "/api/v1/secrets",
36        tags = ["secrets"],
37        params(ListSecretsQuery),
38        responses(
39            (status = 200, description = "Secrets listed", body = Vec<SecretResponse>),
40            (status = 401, description = "Unauthorized"),
41            (status = 403, description = "Forbidden")
42        ),
43        security(("Bearer" = []))
44    )
45)]
46pub async fn list_secrets(
47    auth: Authenticated,
48    State(state): State<AppState>,
49    Query(query): Query<ListSecretsQuery>,
50) -> Result<impl IntoResponse, ApiError> {
51    auth.require(ApiKeyScope::Admin, true)?;
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}