1use std::collections::HashMap;
6use std::sync::Arc;
7
8use axum::extract::State;
9use axum::http::StatusCode;
10use axum::routing::post;
11use axum::{Extension, Json, Router};
12use chrono::Duration;
13use serde::{Deserialize, Serialize};
14
15use crate::error::AppError;
16use crate::middleware::AuthContext;
17use crate::state::AppState;
18use zvault_core::policy::Capability;
19use zvault_core::token::CreateTokenParams;
20
21pub fn router() -> Router<Arc<AppState>> {
23 Router::new()
24 .route("/create", post(create_token))
25 .route("/lookup", post(lookup_token))
26 .route("/lookup-self", post(lookup_self))
27 .route("/renew", post(renew_token))
28 .route("/renew-self", post(renew_self))
29 .route("/revoke", post(revoke_token))
30 .route("/revoke-self", post(revoke_self))
31}
32
33#[derive(Debug, Deserialize)]
36pub struct CreateTokenRequest {
37 pub policies: Option<Vec<String>>,
38 pub ttl: Option<String>,
39 pub display_name: Option<String>,
40 pub renewable: Option<bool>,
41 pub metadata: Option<HashMap<String, String>>,
42}
43
44#[derive(Debug, Serialize)]
45pub struct TokenResponse {
46 pub client_token: String,
47 pub policies: Vec<String>,
48 pub renewable: bool,
49 pub lease_duration: Option<i64>,
50}
51
52#[derive(Debug, Serialize)]
53pub struct TokenLookupResponse {
54 pub token_hash: String,
55 pub policies: Vec<String>,
56 pub display_name: String,
57 pub renewable: bool,
58 pub created_at: String,
59 pub expires_at: Option<String>,
60}
61
62#[derive(Debug, Deserialize)]
63pub struct TokenLookupRequest {
64 pub token: String,
65}
66
67#[derive(Debug, Deserialize)]
68pub struct TokenRenewRequest {
69 pub token: Option<String>,
70 pub increment: Option<String>,
71}
72
73#[derive(Debug, Deserialize)]
74pub struct TokenRevokeRequest {
75 pub token: String,
76}
77
78async fn create_token(
82 State(state): State<Arc<AppState>>,
83 Extension(auth): Extension<AuthContext>,
84 Json(body): Json<CreateTokenRequest>,
85) -> Result<(StatusCode, Json<TokenResponse>), AppError> {
86 state
87 .policy_store
88 .check(&auth.policies, "auth/token/create", &Capability::Sudo)
89 .await?;
90
91 let ttl = body.ttl.as_deref().map(parse_duration).transpose()?;
92 let policies = body.policies.unwrap_or_else(|| vec!["default".to_owned()]);
93
94 let token = state
95 .token_store
96 .create(CreateTokenParams {
97 policies: policies.clone(),
98 ttl,
99 max_ttl: None,
100 renewable: body.renewable.unwrap_or(true),
101 parent_hash: Some(auth.token_hash),
102 metadata: body.metadata.unwrap_or_default(),
103 display_name: body.display_name.unwrap_or_else(|| "token".to_owned()),
104 })
105 .await?;
106
107 let lease_duration = ttl.map(|d| d.num_seconds());
108
109 Ok((
110 StatusCode::OK,
111 Json(TokenResponse {
112 client_token: token,
113 policies,
114 renewable: body.renewable.unwrap_or(true),
115 lease_duration,
116 }),
117 ))
118}
119
120async fn lookup_token(
122 State(state): State<Arc<AppState>>,
123 Extension(auth): Extension<AuthContext>,
124 Json(body): Json<TokenLookupRequest>,
125) -> Result<Json<TokenLookupResponse>, AppError> {
126 state
127 .policy_store
128 .check(&auth.policies, "auth/token/lookup", &Capability::Sudo)
129 .await?;
130
131 let entry = state.token_store.lookup(&body.token).await?;
132
133 Ok(Json(TokenLookupResponse {
134 token_hash: entry.token_hash,
135 policies: entry.policies,
136 display_name: entry.display_name,
137 renewable: entry.renewable,
138 created_at: entry.created_at.to_rfc3339(),
139 expires_at: entry.expires_at.map(|t| t.to_rfc3339()),
140 }))
141}
142
143async fn lookup_self(
145 State(state): State<Arc<AppState>>,
146 Extension(auth): Extension<AuthContext>,
147) -> Result<Json<TokenLookupResponse>, AppError> {
148 state
149 .policy_store
150 .check(&auth.policies, "auth/token/lookup-self", &Capability::Read)
151 .await?;
152
153 Ok(Json(TokenLookupResponse {
157 token_hash: auth.token_hash,
158 policies: auth.policies,
159 display_name: auth.display_name,
160 renewable: false, created_at: String::new(),
162 expires_at: None,
163 }))
164}
165
166async fn renew_token(
168 State(state): State<Arc<AppState>>,
169 Extension(auth): Extension<AuthContext>,
170 Json(body): Json<TokenRenewRequest>,
171) -> Result<Json<TokenLookupResponse>, AppError> {
172 state
173 .policy_store
174 .check(&auth.policies, "auth/token/renew", &Capability::Sudo)
175 .await?;
176
177 let token = body
178 .token
179 .ok_or_else(|| AppError::BadRequest("missing 'token' field".to_owned()))?;
180
181 let increment = body
182 .increment
183 .as_deref()
184 .map(parse_duration)
185 .transpose()?
186 .unwrap_or_else(|| Duration::hours(1));
187
188 let entry = state.token_store.renew(&token, increment).await?;
189
190 Ok(Json(TokenLookupResponse {
191 token_hash: entry.token_hash,
192 policies: entry.policies,
193 display_name: entry.display_name,
194 renewable: entry.renewable,
195 created_at: entry.created_at.to_rfc3339(),
196 expires_at: entry.expires_at.map(|t| t.to_rfc3339()),
197 }))
198}
199
200async fn renew_self(
202 State(state): State<Arc<AppState>>,
203 Extension(auth): Extension<AuthContext>,
204 Json(body): Json<TokenRenewRequest>,
205) -> Result<Json<serde_json::Value>, AppError> {
206 state
207 .policy_store
208 .check(
209 &auth.policies,
210 "auth/token/renew-self",
211 &Capability::Update,
212 )
213 .await?;
214
215 let token = body
218 .token
219 .ok_or_else(|| AppError::BadRequest("missing 'token' field for renew-self".to_owned()))?;
220
221 let increment = body
222 .increment
223 .as_deref()
224 .map(parse_duration)
225 .transpose()?
226 .unwrap_or_else(|| Duration::hours(1));
227
228 let entry = state.token_store.renew(&token, increment).await?;
229
230 Ok(Json(serde_json::json!({
231 "token_hash": entry.token_hash,
232 "policies": entry.policies,
233 "renewable": entry.renewable,
234 "expires_at": entry.expires_at.map(|t| t.to_rfc3339()),
235 })))
236}
237
238async fn revoke_token(
240 State(state): State<Arc<AppState>>,
241 Extension(auth): Extension<AuthContext>,
242 Json(body): Json<TokenRevokeRequest>,
243) -> Result<StatusCode, AppError> {
244 state
245 .policy_store
246 .check(&auth.policies, "auth/token/revoke", &Capability::Sudo)
247 .await?;
248
249 state.token_store.revoke(&body.token).await?;
250
251 Ok(StatusCode::NO_CONTENT)
252}
253
254async fn revoke_self(
256 State(state): State<Arc<AppState>>,
257 Extension(auth): Extension<AuthContext>,
258 Json(body): Json<TokenRevokeRequest>,
259) -> Result<StatusCode, AppError> {
260 let _ = &auth;
262
263 state.token_store.revoke(&body.token).await?;
264
265 Ok(StatusCode::NO_CONTENT)
266}
267
268fn parse_duration(s: &str) -> Result<Duration, AppError> {
276 let s = s.trim();
277 if s.is_empty() {
278 return Err(AppError::BadRequest("empty duration string".to_owned()));
279 }
280
281 if let Ok(secs) = s.parse::<i64>() {
283 return Ok(Duration::seconds(secs));
284 }
285
286 let (num_str, unit) = s.split_at(s.len().saturating_sub(1));
287 let num: i64 = num_str
288 .parse()
289 .map_err(|_| AppError::BadRequest(format!("invalid duration: {s}")))?;
290
291 match unit {
292 "s" => Ok(Duration::seconds(num)),
293 "m" => Ok(Duration::minutes(num)),
294 "h" => Ok(Duration::hours(num)),
295 "d" => Ok(Duration::days(num)),
296 _ => Err(AppError::BadRequest(format!(
297 "unknown duration unit '{unit}', expected s/m/h/d"
298 ))),
299 }
300}