1use std::ops::Deref;
9
10use axum::RequestPartsExt;
11use axum::extract::{FromRequestParts, OptionalFromRequestParts};
12use axum::http::StatusCode;
13use axum::http::request::Parts;
14use axum_extra::extract::PrivateCookieJar;
15use kellnr_common::account_name::UserName;
16use kellnr_common::plain_token::PlainToken;
17use kellnr_db::SessionInfo;
18use kellnr_db::error::DbError;
19use kellnr_settings::constants::COOKIE_SESSION_ID;
20use tracing::error;
21
22use crate::authz;
23use crate::error::ApiError;
24use crate::state::AppStateData;
25use crate::token::Token;
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum Source {
30 Token(Option<PlainToken>),
32 Session,
34}
35
36#[derive(Debug, Clone)]
38pub struct Identity {
39 pub name: UserName,
40 pub is_admin: bool,
41 pub is_read_only: bool,
42 pub source: Source,
43}
44
45impl From<Token> for Identity {
46 fn from(token: Token) -> Self {
47 Self {
48 name: token.user,
49 is_admin: token.is_admin,
50 is_read_only: token.is_read_only,
51 source: Source::Token(token.value),
52 }
53 }
54}
55
56impl From<SessionInfo> for Identity {
57 fn from(session: SessionInfo) -> Self {
58 Self {
59 name: session.name,
60 is_admin: session.is_admin,
61 is_read_only: session.is_read_only,
62 source: Source::Session,
63 }
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum Accepts {
70 Token,
71 Session,
72 TokenOrSession,
73}
74
75pub(crate) async fn identify(
79 parts: &mut Parts,
80 state: &AppStateData,
81 accepts: Accepts,
82) -> Result<Identity, StatusCode> {
83 match accepts {
84 Accepts::Token => from_token(parts, state).await,
85 Accepts::Session => from_session(parts, state).await,
86 Accepts::TokenOrSession => match from_token(parts, state).await {
87 Ok(identity) => Ok(identity),
88 Err(_) => from_session(parts, state).await,
89 },
90 }
91}
92
93async fn from_token(parts: &Parts, state: &AppStateData) -> Result<Identity, StatusCode> {
94 Token::from_header(&parts.headers, state)
95 .await
96 .map(Identity::from)
97}
98
99async fn from_session(parts: &mut Parts, state: &AppStateData) -> Result<Identity, StatusCode> {
100 let jar: PrivateCookieJar = parts
101 .extract_with_state(state)
102 .await
103 .map_err(|_| StatusCode::UNAUTHORIZED)?;
104 let cookie = jar.get(COOKIE_SESSION_ID).ok_or(StatusCode::UNAUTHORIZED)?;
105 validate_session(state, cookie.value()).await
106}
107
108async fn validate_session(
111 state: &AppStateData,
112 session_token: &str,
113) -> Result<Identity, StatusCode> {
114 state
115 .db
116 .validate_session(&PlainToken::from(session_token))
117 .await
118 .map(Identity::from)
119 .map_err(|e| match e {
120 DbError::SessionNotFound => StatusCode::UNAUTHORIZED,
121 e => {
122 error!("Failed to validate session: {e}");
123 StatusCode::INTERNAL_SERVER_ERROR
124 }
125 })
126}
127
128fn no_credentials(status: StatusCode) -> bool {
130 status == StatusCode::UNAUTHORIZED
131}
132
133macro_rules! identity_extractor {
134 ($(#[$doc:meta])* $name:ident) => {
135 $(#[$doc])*
136 #[derive(Debug, Clone)]
137 pub struct $name(pub Identity);
138
139 impl Deref for $name {
140 type Target = Identity;
141
142 fn deref(&self) -> &Identity {
143 &self.0
144 }
145 }
146 };
147}
148
149identity_extractor!(
150 TokenUser
154);
155
156identity_extractor!(
157 SessionUser
160);
161
162identity_extractor!(
163 AnyUser
166);
167
168identity_extractor!(
169 AdminUser
171);
172
173impl FromRequestParts<AppStateData> for TokenUser {
174 type Rejection = ApiError;
175
176 async fn from_request_parts(
177 parts: &mut Parts,
178 state: &AppStateData,
179 ) -> Result<Self, Self::Rejection> {
180 Ok(Self(identify(parts, state, Accepts::Token).await?))
181 }
182}
183
184impl OptionalFromRequestParts<AppStateData> for TokenUser {
185 type Rejection = ApiError;
186
187 async fn from_request_parts(
188 parts: &mut Parts,
189 state: &AppStateData,
190 ) -> Result<Option<Self>, Self::Rejection> {
191 match identify(parts, state, Accepts::Token).await {
192 Ok(identity) => Ok(Some(Self(identity))),
193 Err(status) if no_credentials(status) => Ok(None),
194 Err(status) => Err(status.into()),
195 }
196 }
197}
198
199impl FromRequestParts<AppStateData> for SessionUser {
200 type Rejection = ApiError;
201
202 async fn from_request_parts(
203 parts: &mut Parts,
204 state: &AppStateData,
205 ) -> Result<Self, Self::Rejection> {
206 Ok(Self(identify(parts, state, Accepts::Session).await?))
207 }
208}
209
210impl OptionalFromRequestParts<AppStateData> for SessionUser {
211 type Rejection = ApiError;
212
213 async fn from_request_parts(
214 parts: &mut Parts,
215 state: &AppStateData,
216 ) -> Result<Option<Self>, Self::Rejection> {
217 let jar: PrivateCookieJar = parts
218 .extract_with_state(state)
219 .await
220 .map_err(|_| StatusCode::UNAUTHORIZED)?;
221 match jar.get(COOKIE_SESSION_ID) {
222 Some(cookie) => Ok(Some(Self(validate_session(state, cookie.value()).await?))),
223 None => Ok(None),
224 }
225 }
226}
227
228impl FromRequestParts<AppStateData> for AnyUser {
229 type Rejection = ApiError;
230
231 async fn from_request_parts(
232 parts: &mut Parts,
233 state: &AppStateData,
234 ) -> Result<Self, Self::Rejection> {
235 Ok(Self(identify(parts, state, Accepts::TokenOrSession).await?))
236 }
237}
238
239impl OptionalFromRequestParts<AppStateData> for AnyUser {
240 type Rejection = ApiError;
241
242 async fn from_request_parts(
243 parts: &mut Parts,
244 state: &AppStateData,
245 ) -> Result<Option<Self>, Self::Rejection> {
246 Ok(identify(parts, state, Accepts::TokenOrSession)
247 .await
248 .ok()
249 .map(Self))
250 }
251}
252
253impl FromRequestParts<AppStateData> for AdminUser {
254 type Rejection = ApiError;
255
256 async fn from_request_parts(
257 parts: &mut Parts,
258 state: &AppStateData,
259 ) -> Result<Self, Self::Rejection> {
260 let identity = identify(parts, state, Accepts::Session).await?;
261 authz::require_admin(&identity)?;
262 Ok(Self(identity))
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use axum::Router;
269 use axum::body::Body;
270 use axum::http::{Request, header};
271 use axum::routing::get;
272 use kellnr_db::UserFlags;
273 use kellnr_db::test_utils::test_drop_table;
274 use kellnr_test_db::TestDb;
275 use tower::ServiceExt;
276
277 use super::*;
278 use crate::test_support::{encode_cookies, session_cookie, sqlite_test_state, test_state};
279
280 type Result<T = (), E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
281
282 async fn admin_endpoint(_user: AdminUser) {}
283
284 async fn session_endpoint(_user: SessionUser) {}
285
286 async fn optional_session_endpoint(user: Option<SessionUser>) -> String {
287 user.map(|u| u.name.to_string()).unwrap_or_default()
288 }
289
290 async fn any_endpoint(user: AnyUser) -> String {
291 format!("{:?}", user.source)
292 }
293
294 async fn optional_any_endpoint(user: Option<AnyUser>) -> String {
295 user.map(|u| u.name.to_string()).unwrap_or_default()
296 }
297
298 async fn optional_token_endpoint(user: Option<TokenUser>) -> String {
299 user.map(|u| u.name.to_string()).unwrap_or_default()
300 }
301
302 fn app(state: AppStateData) -> Router {
303 Router::new()
304 .route("/admin", get(admin_endpoint))
305 .route("/session", get(session_endpoint))
306 .route("/optional_session", get(optional_session_endpoint))
307 .route("/any", get(any_endpoint))
308 .route("/optional_any", get(optional_any_endpoint))
309 .route("/optional_token", get(optional_token_endpoint))
310 .with_state(state)
311 }
312
313 async fn db_app() -> (Router, TestDb) {
315 let (state, db) = sqlite_test_state().await;
316 (app(state), db)
317 }
318
319 fn unknown_session() -> String {
321 encode_cookies([(COOKIE_SESSION_ID, "1234")])
322 }
323
324 async fn user_session(db: &TestDb) -> String {
325 session_cookie(db, "user", UserFlags::default()).await
326 }
327
328 async fn send(
329 app: Router,
330 uri: &str,
331 headers: &[(header::HeaderName, String)],
332 ) -> Result<(StatusCode, String)> {
333 let mut request = Request::get(uri);
334 for (name, value) in headers {
335 request = request.header(name, value);
336 }
337 let response = app.oneshot(request.body(Body::empty())?).await?;
338 let status = response.status();
339 let body = axum::body::to_bytes(response.into_body(), usize::MAX).await?;
340 Ok((status, String::from_utf8(body.to_vec())?))
341 }
342
343 #[tokio::test]
344 async fn admin_session_is_admin_user() -> Result {
345 let (app, db) = db_app().await;
346 let cookie = session_cookie(&db, "admin", UserFlags::default()).await;
347 let (status, _) = send(app, "/admin", &[(header::COOKIE, cookie)]).await?;
348 assert_eq!(status, StatusCode::OK);
349 Ok(())
350 }
351
352 #[tokio::test]
353 async fn normal_session_is_no_admin_user() -> Result {
354 let (app, db) = db_app().await;
355 let cookie = user_session(&db).await;
356 let (status, _) = send(app, "/admin", &[(header::COOKIE, cookie)]).await?;
357 assert_eq!(status, StatusCode::FORBIDDEN);
358 Ok(())
359 }
360
361 #[tokio::test]
362 async fn admin_user_needs_a_session() -> Result {
363 let (status, _) = send(app(test_state()), "/admin", &[]).await?;
364 assert_eq!(status, StatusCode::UNAUTHORIZED);
365 Ok(())
366 }
367
368 #[tokio::test]
369 async fn admin_user_with_unknown_session() -> Result {
370 let (app, _db) = db_app().await;
371 let (status, _) = send(app, "/admin", &[(header::COOKIE, unknown_session())]).await?;
372 assert_eq!(status, StatusCode::UNAUTHORIZED);
373 Ok(())
374 }
375
376 #[tokio::test]
377 async fn session_user_with_session() -> Result {
378 let (app, db) = db_app().await;
379 let cookie = user_session(&db).await;
380 let (status, _) = send(app, "/session", &[(header::COOKIE, cookie)]).await?;
381 assert_eq!(status, StatusCode::OK);
382 Ok(())
383 }
384
385 #[tokio::test]
386 async fn session_user_without_session() -> Result {
387 let (status, _) = send(app(test_state()), "/session", &[]).await?;
388 assert_eq!(status, StatusCode::UNAUTHORIZED);
389 Ok(())
390 }
391
392 #[tokio::test]
393 async fn session_user_ignores_tokens() -> Result {
394 let (app, _db) = db_app().await;
395 let (status, _) = send(app, "/session", &[(header::AUTHORIZATION, "token".into())]).await?;
396 assert_eq!(status, StatusCode::UNAUTHORIZED);
397 Ok(())
398 }
399
400 #[tokio::test]
402 async fn session_user_with_database_error() -> Result {
403 let (app, db) = db_app().await;
404 let cookie = user_session(&db).await;
405 test_drop_table(&db, "session").await?;
406 let (status, _) = send(app, "/session", &[(header::COOKIE, cookie)]).await?;
407 assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
408 Ok(())
409 }
410
411 #[tokio::test]
412 async fn session_user_with_unknown_session() -> Result {
413 let (app, _db) = db_app().await;
414 let (status, _) = send(app, "/session", &[(header::COOKIE, unknown_session())]).await?;
415 assert_eq!(status, StatusCode::UNAUTHORIZED);
416 Ok(())
417 }
418
419 #[tokio::test]
420 async fn optional_session_user_without_session_is_anonymous() -> Result {
421 let (status, body) = send(app(test_state()), "/optional_session", &[]).await?;
422 assert_eq!((status, body.as_str()), (StatusCode::OK, ""));
423 Ok(())
424 }
425
426 #[tokio::test]
427 async fn optional_session_user_with_unknown_session_is_rejected() -> Result {
428 let (app, _db) = db_app().await;
429 let (status, _) = send(
430 app,
431 "/optional_session",
432 &[(header::COOKIE, unknown_session())],
433 )
434 .await?;
435 assert_eq!(status, StatusCode::UNAUTHORIZED);
436 Ok(())
437 }
438
439 #[tokio::test]
440 async fn any_user_prefers_the_token() -> Result {
441 let (app, db) = db_app().await;
442 let cookie = user_session(&db).await;
443 let (status, body) = send(
444 app,
445 "/any",
446 &[
447 (header::AUTHORIZATION, "token".into()),
448 (header::COOKIE, cookie),
449 ],
450 )
451 .await?;
452 assert_eq!(status, StatusCode::OK);
453 assert!(body.starts_with("Token("), "{body}");
454 Ok(())
455 }
456
457 #[tokio::test]
458 async fn any_user_falls_back_to_the_session_if_the_token_fails() -> Result {
459 let (app, db) = db_app().await;
460 let cookie = user_session(&db).await;
461 let (status, body) = send(
462 app,
463 "/any",
464 &[
465 (header::AUTHORIZATION, "invalid".into()),
466 (header::COOKIE, cookie),
467 ],
468 )
469 .await?;
470 assert_eq!((status, body.as_str()), (StatusCode::OK, "Session"));
471 Ok(())
472 }
473
474 #[tokio::test]
475 async fn any_user_without_login() -> Result {
476 let (status, _) = send(app(test_state()), "/any", &[]).await?;
477 assert_eq!(status, StatusCode::UNAUTHORIZED);
478 Ok(())
479 }
480
481 #[tokio::test]
482 async fn optional_any_user_with_invalid_token_is_anonymous() -> Result {
483 let (app, _db) = db_app().await;
484 let (status, body) = send(
485 app,
486 "/optional_any",
487 &[(header::AUTHORIZATION, "invalid".into())],
488 )
489 .await?;
490 assert_eq!((status, body.as_str()), (StatusCode::OK, ""));
491 Ok(())
492 }
493
494 #[tokio::test]
495 async fn optional_token_user_without_token_is_anonymous() -> Result {
496 let (status, body) = send(app(test_state()), "/optional_token", &[]).await?;
497 assert_eq!((status, body.as_str()), (StatusCode::OK, ""));
498 Ok(())
499 }
500
501 #[tokio::test]
502 async fn optional_token_user_with_invalid_token_is_rejected() -> Result {
503 let (app, _db) = db_app().await;
504 let (status, _) = send(
505 app,
506 "/optional_token",
507 &[(header::AUTHORIZATION, "invalid".into())],
508 )
509 .await?;
510 assert_eq!(status, StatusCode::FORBIDDEN);
511 Ok(())
512 }
513
514 #[tokio::test]
515 async fn optional_token_user_with_token() -> Result {
516 let (app, _db) = db_app().await;
517 let (status, body) = send(
518 app,
519 "/optional_token",
520 &[(header::AUTHORIZATION, "token".into())],
521 )
522 .await?;
523 assert_eq!((status, body.as_str()), (StatusCode::OK, "admin"));
524 Ok(())
525 }
526}