1use axum::extract::{Path, State};
2use axum::http::StatusCode;
3use axum::{Extension, Json};
4use axum_extra::extract::PrivateCookieJar;
5use axum_extra::extract::cookie::Cookie;
6use kellnr_common::account_name::{UserName, validate_token_name};
7use kellnr_common::plain_token::PlainToken;
8use kellnr_common::util::generate_rand_string;
9use kellnr_db::{self, AuthToken, AuthTokenId, DbProvider, User, UserFlags};
10use kellnr_http::error::ApiError;
11use kellnr_http::identity::{AdminUser, SessionUser};
12use kellnr_http::password::{PasswordAuthError, authenticate_password};
13use kellnr_http::state::{AppState, DbState, LoginThrottleState, SettingsState, TokenCacheState};
14use kellnr_http::token;
15use kellnr_settings::Settings;
16use kellnr_settings::constants::{COOKIE_OIDC_ID_TOKEN, COOKIE_SESSION_ID, COOKIE_SESSION_USER};
17use serde::{Deserialize, Serialize};
18use tracing::warn;
19use utoipa::ToSchema;
20
21use crate::oauth2::OAuth2Ext;
22use crate::session::create_session_jar;
23
24fn ensure_local_auth_enabled(settings: &Settings) -> Result<(), ApiError> {
26 if settings.oauth2.enforced {
27 return Err(ApiError::from(StatusCode::FORBIDDEN));
28 }
29 Ok(())
30}
31
32async fn revoke_sessions(
36 db: &dyn DbProvider,
37 user_name: &UserName,
38 keep_session_token: Option<&str>,
39) -> Result<(), ApiError> {
40 match keep_session_token {
41 Some(token) => {
42 db.delete_other_user_sessions(user_name, &PlainToken::from(token))
43 .await?
44 }
45 None => db.delete_user_sessions(user_name).await?,
46 };
47 Ok(())
48}
49
50#[derive(Serialize, ToSchema)]
51pub struct NewTokenResponse {
52 name: String,
53 token: String,
54}
55
56#[utoipa::path(
58 post,
59 path = "/me/tokens",
60 tag = "users",
61 request_body = token::NewTokenReqData,
62 responses(
63 (status = 200, description = "Token created successfully", body = NewTokenResponse),
64 (status = 400, description = "Invalid token name"),
65 (status = 401, description = "Not authenticated")
66 ),
67 security(("session_cookie" = []))
68)]
69pub async fn add_token(
70 user: SessionUser,
71 State(db): DbState,
72 State(cache): TokenCacheState,
73 Json(auth_token): Json<token::NewTokenReqData>,
74) -> Result<Json<NewTokenResponse>, ApiError> {
75 validate_token_name(&auth_token.name)?;
76 let token = token::generate_token();
77 db.add_auth_token(&auth_token.name, &token, &user.name)
78 .await?;
79
80 cache.invalidate_all();
81
82 Ok(NewTokenResponse {
83 name: auth_token.name.clone(),
84 token: token.into_secret(),
85 }
86 .into())
87}
88
89#[utoipa::path(
91 get,
92 path = "/me/tokens",
93 tag = "users",
94 responses(
95 (status = 200, description = "List of auth tokens", body = Vec<AuthToken>),
96 (status = 401, description = "Not authenticated")
97 ),
98 security(("session_cookie" = []))
99)]
100pub async fn list_tokens(
101 user: SessionUser,
102 State(db): DbState,
103) -> Result<Json<Vec<AuthToken>>, ApiError> {
104 Ok(Json(db.get_auth_tokens(&user.name).await?))
105}
106
107#[utoipa::path(
109 get,
110 path = "/",
111 tag = "users",
112 responses(
113 (status = 200, description = "List of all users", body = Vec<User>),
114 (status = 403, description = "Admin access required")
115 ),
116 security(("session_cookie" = []))
117)]
118pub async fn list_users(_user: AdminUser, State(db): DbState) -> Result<Json<Vec<User>>, ApiError> {
119 Ok(Json(db.get_users().await?))
120}
121
122#[utoipa::path(
124 delete,
125 path = "/me/tokens/{id}",
126 tag = "users",
127 params(
128 ("id" = i64, Path, description = "Token ID to delete")
129 ),
130 responses(
131 (status = 200, description = "Token deleted successfully"),
132 (status = 401, description = "Not authenticated"),
133 (status = 404, description = "Token not found")
134 ),
135 security(("session_cookie" = []))
136)]
137pub async fn delete_token(
138 user: SessionUser,
139 Path(id): Path<AuthTokenId>,
140 State(db): DbState,
141 State(cache): TokenCacheState,
142) -> Result<(), ApiError> {
143 db.delete_auth_token(&user.name, id).await?;
145
146 cache.invalidate_all();
147
148 Ok(())
149}
150
151#[derive(Serialize, ToSchema)]
152pub struct ResetPwd {
153 new_pwd: String,
154 user: UserName,
155}
156
157#[utoipa::path(
159 put,
160 path = "/{name}/password",
161 tag = "users",
162 params(
163 ("name" = String, Path, description = "Username")
164 ),
165 responses(
166 (status = 200, description = "Password reset successfully", body = ResetPwd),
167 (status = 403, description = "Admin access required")
168 ),
169 security(("session_cookie" = []))
170)]
171pub async fn reset_pwd(
172 admin: AdminUser,
173 jar: PrivateCookieJar,
174 Path(name): Path<UserName>,
175 State(db): DbState,
176 State(cache): TokenCacheState,
177 State(settings): SettingsState,
178) -> Result<Json<ResetPwd>, ApiError> {
179 ensure_local_auth_enabled(&settings)?;
180
181 let new_pwd = generate_rand_string(12);
182 db.change_pwd(&name, &new_pwd).await?;
183
184 let keep_session = jar.get(COOKIE_SESSION_ID).filter(|_| admin.name == name);
186 revoke_sessions(db.as_ref(), &name, keep_session.as_ref().map(Cookie::value)).await?;
187
188 cache.invalidate_all();
190
191 Ok(ResetPwd {
193 user: name,
194 new_pwd,
195 }
196 .into())
197}
198
199#[derive(Deserialize, ToSchema)]
200pub struct ReadOnlyState {
201 pub state: bool,
202}
203
204#[utoipa::path(
206 post,
207 path = "/{name}/read-only",
208 tag = "users",
209 params(
210 ("name" = String, Path, description = "Username")
211 ),
212 request_body = ReadOnlyState,
213 responses(
214 (status = 200, description = "Read-only state changed successfully"),
215 (status = 400, description = "Cannot lock yourself"),
216 (status = 403, description = "Admin access required")
217 ),
218 security(("session_cookie" = []))
219)]
220pub async fn read_only(
221 user: AdminUser,
222 Path(name): Path<UserName>,
223 State(db): DbState,
224 State(cache): TokenCacheState,
225 Json(ro_state): Json<ReadOnlyState>,
226) -> Result<(), ApiError> {
227 if user.name == name && ro_state.state {
229 return Err(ApiError::bad_request("You cannot make yourself read-only"));
230 }
231
232 db.change_read_only_state(&name, ro_state.state).await?;
233
234 cache.invalidate_all();
235
236 Ok(())
237}
238
239#[derive(Deserialize, ToSchema)]
240pub struct AdminState {
241 pub state: bool,
242}
243
244#[utoipa::path(
246 post,
247 path = "/{name}/admin",
248 tag = "users",
249 params(
250 ("name" = String, Path, description = "Username")
251 ),
252 request_body = AdminState,
253 responses(
254 (status = 200, description = "Admin state changed successfully"),
255 (status = 400, description = "Cannot demote yourself"),
256 (status = 403, description = "Admin access required")
257 ),
258 security(("session_cookie" = []))
259)]
260pub async fn admin(
261 user: AdminUser,
262 Path(name): Path<UserName>,
263 State(db): DbState,
264 State(cache): TokenCacheState,
265 Json(admin_state): Json<AdminState>,
266) -> Result<(), ApiError> {
267 if user.name == name && !admin_state.state {
269 return Err(ApiError::bad_request(
270 "You cannot remove your own admin rights",
271 ));
272 }
273
274 db.change_admin_state(&name, admin_state.state).await?;
275
276 cache.invalidate_all();
277
278 Ok(())
279}
280
281#[utoipa::path(
283 delete,
284 path = "/{name}",
285 tag = "users",
286 params(
287 ("name" = String, Path, description = "Username to delete")
288 ),
289 responses(
290 (status = 200, description = "User deleted successfully"),
291 (status = 400, description = "Cannot delete yourself"),
292 (status = 403, description = "Admin access required")
293 ),
294 security(("session_cookie" = []))
295)]
296pub async fn delete(
297 user: AdminUser,
298 Path(name): Path<UserName>,
299 State(db): DbState,
300 State(cache): TokenCacheState,
301) -> Result<(), ApiError> {
302 if user.name == name {
304 return Err(ApiError::bad_request("You cannot delete yourself"));
305 }
306
307 db.delete_user(&name).await?;
308
309 cache.invalidate_all();
310
311 Ok(())
312}
313
314#[derive(Serialize, ToSchema)]
315pub struct LoggedInUser {
316 user: String,
317 is_admin: bool,
318 is_logged_in: bool,
319}
320
321#[derive(Deserialize, ToSchema)]
322pub struct Credentials {
323 pub user: String,
324 pub pwd: String,
325}
326
327impl Credentials {
328 pub fn validate(&self) -> Result<UserName, ApiError> {
330 let name = UserName::new(self.user.as_str())?;
331 if self.pwd.is_empty() {
332 return Err(ApiError::bad_request("The password must not be empty"));
333 }
334 Ok(name)
335 }
336}
337
338#[utoipa::path(
340 post,
341 path = "/login",
342 tag = "auth",
343 request_body = Credentials,
344 responses(
345 (status = 200, description = "Successfully logged in", body = LoggedInUser),
346 (status = 400, description = "Invalid credentials"),
347 (status = 401, description = "Authentication failed")
348 )
349)]
350pub async fn login(
351 cookies: PrivateCookieJar,
352 State(state): AppState,
353 Json(credentials): Json<Credentials>,
354) -> Result<(PrivateCookieJar, Json<LoggedInUser>), ApiError> {
355 ensure_local_auth_enabled(&state.settings)?;
356 let name = credentials.validate()?;
357
358 let user = authenticate_password(
359 state.db.as_ref(),
360 &state.login_throttle,
361 &name,
362 &credentials.pwd,
363 )
364 .await
365 .map_err(|e| match e {
366 PasswordAuthError::Throttled => ApiError::from(StatusCode::TOO_MANY_REQUESTS),
367 PasswordAuthError::Invalid => {
368 warn!("Login with wrong username or password");
369 ApiError::unauthorized("Wrong username or password")
370 }
371 })?;
372
373 let jar = create_session_jar(cookies, &state, &name).await?;
374
375 Ok((
376 jar,
377 LoggedInUser {
378 user: credentials.user.clone(),
379 is_admin: user.is_admin,
380 is_logged_in: true,
381 }
382 .into(),
383 ))
384}
385
386#[utoipa::path(
388 get,
389 path = "/state",
390 tag = "auth",
391 responses(
392 (status = 200, description = "Current login state", body = LoggedInUser)
393 )
394)]
395#[expect(clippy::unused_async)] pub async fn login_state(user: Option<SessionUser>) -> Json<LoggedInUser> {
397 match user {
398 Some(SessionUser(user)) => LoggedInUser {
399 user: user.name.into_inner(),
400 is_admin: user.is_admin,
401 is_logged_in: true,
402 },
403 None => LoggedInUser {
404 user: String::new(),
405 is_admin: false,
406 is_logged_in: false,
407 },
408 }
409 .into()
410}
411
412#[derive(Serialize, ToSchema)]
413pub struct LogoutResponse {
414 pub logout_url: Option<String>,
415}
416
417#[utoipa::path(
419 post,
420 path = "/logout",
421 tag = "auth",
422 responses(
423 (status = 200, description = "Successfully logged out", body = LogoutResponse)
424 )
425)]
426pub async fn logout(
427 mut jar: PrivateCookieJar,
428 State(state): AppState,
429 Extension(oauth2_handler): OAuth2Ext,
430) -> Result<(PrivateCookieJar, Json<LogoutResponse>), ApiError> {
431 let session_id = match jar.get(COOKIE_SESSION_ID) {
432 Some(c) => c.value().to_owned(),
433 None => return Ok((jar, Json(LogoutResponse { logout_url: None }))),
434 };
435
436 let logout_url = jar
437 .get(COOKIE_OIDC_ID_TOKEN)
438 .zip(oauth2_handler.as_ref())
439 .and_then(|(id_token, handler)| {
440 let post_logout = format!("{}/", state.settings.origin.base_url());
441 handler.end_session_url(id_token.value(), &post_logout)
442 });
443
444 jar = jar.remove(Cookie::build((COOKIE_SESSION_ID, "")).path("/"));
445 jar = jar.remove(Cookie::build((COOKIE_SESSION_USER, "")).path("/"));
446 jar = jar.remove(Cookie::build((COOKIE_OIDC_ID_TOKEN, "")).path("/"));
447
448 if let Err(e) = state
451 .db
452 .delete_session_token(&PlainToken::from(session_id))
453 .await
454 {
455 tracing::error!("Failed to delete session on logout: {e}");
456 }
457 Ok((jar, Json(LogoutResponse { logout_url })))
458}
459
460#[derive(Deserialize, ToSchema)]
461pub struct PwdChange {
462 pub old_pwd: String,
463 pub new_pwd1: String,
464 pub new_pwd2: String,
465}
466
467impl PwdChange {
468 pub fn validate(&self) -> Result<(), ApiError> {
469 if self.old_pwd.is_empty() {
470 return Err(ApiError::bad_request("The old password must not be empty"));
471 }
472 if self.new_pwd1.is_empty() || self.new_pwd2.is_empty() {
473 return Err(ApiError::bad_request("The new password must not be empty"));
474 }
475 if self.new_pwd1 != self.new_pwd2 {
476 return Err(ApiError::bad_request("The new passwords do not match"));
477 }
478 Ok(())
479 }
480}
481
482#[utoipa::path(
484 put,
485 path = "/me/password",
486 tag = "users",
487 request_body = PwdChange,
488 responses(
489 (status = 200, description = "Password changed successfully"),
490 (status = 400, description = "Invalid password or validation failed"),
491 (status = 401, description = "Not authenticated")
492 ),
493 security(("session_cookie" = []))
494)]
495pub async fn change_pwd(
496 user: SessionUser,
497 jar: PrivateCookieJar,
498 State(db): DbState,
499 State(cache): TokenCacheState,
500 State(throttle): LoginThrottleState,
501 State(settings): SettingsState,
502 Json(pwd_change): Json<PwdChange>,
503) -> Result<(), ApiError> {
504 ensure_local_auth_enabled(&settings)?;
505 pwd_change.validate()?;
506
507 let user = authenticate_password(db.as_ref(), &throttle, &user.name, &pwd_change.old_pwd)
508 .await
509 .map_err(|e| match e {
510 PasswordAuthError::Throttled => ApiError::from(StatusCode::TOO_MANY_REQUESTS),
511 PasswordAuthError::Invalid => ApiError::bad_request("The old password is wrong"),
512 })?;
513
514 db.change_pwd(&user.name, &pwd_change.new_pwd1).await?;
515 let current_session = jar.get(COOKIE_SESSION_ID);
516 revoke_sessions(
517 db.as_ref(),
518 &user.name,
519 current_session.as_ref().map(Cookie::value),
520 )
521 .await?;
522
523 cache.invalidate_all();
525 Ok(())
526}
527
528#[derive(Deserialize, ToSchema)]
529pub struct NewUser {
530 pub pwd1: String,
531 pub pwd2: String,
532 pub name: String,
533 #[serde(default)] pub is_admin: bool,
535 #[serde(default)] pub is_read_only: bool,
537}
538
539impl NewUser {
540 pub fn validate(&self) -> Result<UserName, ApiError> {
542 let name = UserName::for_new_user(self.name.as_str())?;
543 if self.pwd1.is_empty() || self.pwd2.is_empty() {
544 return Err(ApiError::bad_request("The password must not be empty"));
545 }
546 if self.pwd1 != self.pwd2 {
547 return Err(ApiError::bad_request("The passwords do not match"));
548 }
549 Ok(name)
550 }
551}
552
553#[utoipa::path(
555 post,
556 path = "/",
557 tag = "users",
558 request_body = NewUser,
559 responses(
560 (status = 200, description = "User created successfully"),
561 (status = 400, description = "Validation failed"),
562 (status = 403, description = "Admin access required")
563 ),
564 security(("session_cookie" = []))
565)]
566pub async fn add(
567 _user: AdminUser,
568 State(db): DbState,
569 State(cache): TokenCacheState,
570 State(settings): SettingsState,
571 Json(new_user): Json<NewUser>,
572) -> Result<(), ApiError> {
573 ensure_local_auth_enabled(&settings)?;
574 let name = new_user.validate()?;
575
576 db.add_user(
577 &name,
578 &new_user.pwd1,
579 UserFlags {
580 admin: new_user.is_admin,
581 read_only: new_user.is_read_only,
582 },
583 )
584 .await?;
585
586 cache.invalidate_all();
587
588 Ok(())
589}
590
591#[cfg(test)]
592mod tests {
593 use std::sync::Arc;
594 use std::time::Duration;
595
596 use axum::Router;
597 use axum::body::Body;
598 use axum::http::Response;
599 use axum::response::IntoResponse;
600 use axum::routing::{post, put};
601 use hyper::{Method, Request, header};
602 use kellnr_db::test_utils::test_drop_table;
603 use kellnr_http::login_throttle::LoginThrottle;
604 use kellnr_http::state::AppStateData;
605 use kellnr_http::test_support::{encode_cookies, session_cookie, sqlite_test_state};
606 use kellnr_http::token_cache::{CachedTokenData, TokenCacheManager};
607 use kellnr_settings::constants::COOKIE_SESSION_ID;
608 use kellnr_test_db::TestDb;
609 use tower::ServiceExt;
610
611 use super::*;
612
613 fn app(state: AppStateData) -> Router {
615 Router::new()
616 .route("/login", post(login))
617 .route("/logout", post(logout))
618 .route("/", post(add))
619 .route("/{name}", axum::routing::delete(delete))
620 .route("/{name}/password", put(reset_pwd))
621 .route("/{name}/admin", post(admin))
622 .route("/{name}/read-only", post(read_only))
623 .route("/me/password", put(change_pwd))
624 .route("/me/tokens", post(add_token))
625 .route("/me/tokens/{id}", axum::routing::delete(delete_token))
626 .layer(Extension::<Option<Arc<kellnr_oauth2::OAuth2Handler>>>(None))
627 .with_state(state)
628 }
629
630 fn sso_enforced(mut state: AppStateData) -> AppStateData {
631 let mut settings = (*state.settings).clone();
632 settings.oauth2.enabled = true;
633 settings.oauth2.enforced = true;
634 state.settings = Arc::new(settings);
635 state
636 }
637
638 fn token_cache() -> Arc<TokenCacheManager> {
639 Arc::new(TokenCacheManager::new(true, Duration::from_secs(60), 100))
640 }
641
642 async fn token_cache_with(key: &str, user: &str) -> Arc<TokenCacheManager> {
644 let cache = token_cache();
645 cache
646 .insert(
647 key.to_string(),
648 CachedTokenData {
649 user: UserName::new(user).unwrap(),
650 is_admin: false,
651 is_read_only: false,
652 },
653 cache.generation(),
654 )
655 .await;
656 cache
657 }
658
659 async fn send(
660 state: AppStateData,
661 method: Method,
662 uri: &str,
663 cookie: &str,
664 json: Option<&str>,
665 ) -> Response<Body> {
666 let request = Request::builder()
667 .method(method)
668 .uri(uri)
669 .header(header::COOKIE, cookie)
670 .header(header::CONTENT_TYPE, "application/json");
671 let body = json.map_or_else(Body::empty, |json| Body::from(json.to_string()));
672 app(state)
673 .oneshot(request.body(body).unwrap())
674 .await
675 .unwrap()
676 }
677
678 async fn add_user(db: &TestDb, name: &str, flags: UserFlags) {
679 db.add_user(&UserName::new(name).unwrap(), "pwd", flags)
680 .await
681 .unwrap();
682 }
683
684 async fn has_password(db: &TestDb, name: &str, pwd: &str) -> bool {
685 db.authenticate_user(&UserName::new(name).unwrap(), pwd)
686 .await
687 .is_ok()
688 }
689
690 async fn user(db: &TestDb, name: &str) -> User {
691 db.get_user(&UserName::new(name).unwrap()).await.unwrap()
692 }
693
694 async fn has_session(db: &TestDb, token: &str) -> bool {
695 db.validate_session(&PlainToken::from(token)).await.is_ok()
696 }
697
698 async fn add_session(db: &TestDb, name: &str, token: &str) {
699 db.add_session_token(&UserName::new(name).unwrap(), &PlainToken::from(token))
700 .await
701 .unwrap();
702 }
703
704 #[tokio::test]
711 async fn test_login_forbidden_when_sso_enforced() {
712 let (state, _db) = sqlite_test_state().await;
713
714 let response = send(
715 sso_enforced(state),
716 Method::POST,
717 "/login",
718 "",
719 Some(r#"{"user":"admin","pwd":"123"}"#),
720 )
721 .await;
722
723 assert_eq!(response.status(), StatusCode::FORBIDDEN);
724 }
725
726 #[tokio::test]
729 async fn test_change_pwd_forbidden_when_sso_enforced() {
730 let (state, db) = sqlite_test_state().await;
731 let cookie = session_cookie(&db, "user", UserFlags::normal()).await;
732
733 let response = send(
734 sso_enforced(state),
735 Method::PUT,
736 "/me/password",
737 &cookie,
738 Some(r#"{"old_pwd":"pwd","new_pwd1":"newpassword","new_pwd2":"newpassword"}"#),
739 )
740 .await;
741
742 assert_eq!(response.status(), StatusCode::FORBIDDEN);
743 assert!(has_password(&db, "user", "pwd").await);
744 }
745
746 #[tokio::test]
749 async fn test_reset_pwd_forbidden_when_sso_enforced() {
750 let (state, db) = sqlite_test_state().await;
751 let cookie = session_cookie(&db, "admin", UserFlags::admin()).await;
752 add_user(&db, "someuser", UserFlags::normal()).await;
753
754 let response = send(
755 sso_enforced(state),
756 Method::PUT,
757 "/someuser/password",
758 &cookie,
759 None,
760 )
761 .await;
762
763 assert_eq!(response.status(), StatusCode::FORBIDDEN);
764 assert!(has_password(&db, "someuser", "pwd").await);
765 }
766
767 #[tokio::test]
770 async fn test_add_user_forbidden_when_sso_enforced() {
771 let (state, db) = sqlite_test_state().await;
772 let cookie = session_cookie(&db, "admin", UserFlags::admin()).await;
773
774 let response = send(
775 sso_enforced(state),
776 Method::POST,
777 "/",
778 &cookie,
779 Some(
780 r#"{"name":"new_user","pwd1":"password","pwd2":"password","is_admin":false,"is_read_only":false}"#,
781 ),
782 )
783 .await;
784
785 assert_eq!(response.status(), StatusCode::FORBIDDEN);
786 assert!(
787 db.get_user(&UserName::new("new_user").unwrap())
788 .await
789 .is_err()
790 );
791 }
792
793 async fn post_login(state: &AppStateData, user: &str) -> StatusCode {
794 send(
795 state.clone(),
796 Method::POST,
797 "/login",
798 "",
799 Some(&format!(r#"{{"user":"{user}","pwd":"guess"}}"#)),
800 )
801 .await
802 .status()
803 }
804
805 #[tokio::test]
808 async fn test_login_throttled_after_failed_attempts() {
809 let (state, db) = sqlite_test_state().await;
810 add_user(&db, "other", UserFlags::normal()).await;
811 let state = AppStateData {
812 login_throttle: Arc::new(LoginThrottle::new(2, Duration::from_mins(5))),
813 ..state
814 };
815
816 assert_eq!(post_login(&state, "admin").await, StatusCode::UNAUTHORIZED);
817 assert_eq!(post_login(&state, "admin").await, StatusCode::UNAUTHORIZED);
818 assert_eq!(
819 post_login(&state, "admin").await,
820 StatusCode::TOO_MANY_REQUESTS
821 );
822 assert_eq!(post_login(&state, "other").await, StatusCode::UNAUTHORIZED);
823 }
824
825 #[tokio::test]
828 async fn test_change_pwd_throttled_after_failed_attempts() {
829 let (state, db) = sqlite_test_state().await;
830 let cookie = session_cookie(&db, "user", UserFlags::normal()).await;
831 let state = AppStateData {
832 login_throttle: Arc::new(LoginThrottle::new(1, Duration::from_mins(5))),
833 ..state
834 };
835
836 let mut statuses = Vec::new();
837 for _ in 0..2 {
838 let response = send(
839 state.clone(),
840 Method::PUT,
841 "/me/password",
842 &cookie,
843 Some(r#"{"old_pwd":"guess","new_pwd1":"newpassword","new_pwd2":"newpassword"}"#),
844 )
845 .await;
846 statuses.push(response.status());
847 }
848
849 assert_eq!(
850 statuses,
851 [StatusCode::BAD_REQUEST, StatusCode::TOO_MANY_REQUESTS]
852 );
853 assert!(has_password(&db, "user", "pwd").await);
854 }
855
856 #[tokio::test]
859 async fn test_logout_removes_session_cookie_with_root_path() {
860 let (state, db) = sqlite_test_state().await;
861 add_session(&db, "admin", "session").await;
862
863 let response = send(
864 state,
865 Method::POST,
866 "/logout",
867 &encode_cookies([(COOKIE_SESSION_ID, "session")]),
868 None,
869 )
870 .await;
871
872 assert_eq!(response.status(), StatusCode::OK);
873 let removal = response
874 .headers()
875 .get_all(header::SET_COOKIE)
876 .iter()
877 .map(|v| v.to_str().unwrap().to_string())
878 .find(|c| c.starts_with(COOKIE_SESSION_ID))
879 .expect("session cookie must be removed");
880 assert!(removal.contains("Path=/"), "{removal}");
881 assert!(removal.contains("Max-Age=0"), "{removal}");
882 assert!(!has_session(&db, "session").await);
883 }
884
885 #[tokio::test]
886 async fn test_logout_removes_session_cookie_if_session_cannot_be_deleted() {
887 let (state, db) = sqlite_test_state().await;
888 let cookie = session_cookie(&db, "admin", UserFlags::admin()).await;
889 test_drop_table(&db, "session").await.unwrap();
890
891 let response = send(state, Method::POST, "/logout", &cookie, None).await;
892
893 assert_eq!(response.status(), StatusCode::OK);
894 assert!(
895 response
896 .headers()
897 .get_all(header::SET_COOKIE)
898 .iter()
899 .any(|v| v.to_str().unwrap().starts_with(COOKIE_SESSION_ID))
900 );
901 }
902
903 fn new_user(name: &str) -> NewUser {
904 NewUser {
905 pwd1: "pwd".to_string(),
906 pwd2: "pwd".to_string(),
907 name: name.to_string(),
908 is_admin: false,
909 is_read_only: false,
910 }
911 }
912
913 #[tokio::test]
915 async fn test_add_user_with_invalid_name_is_bad_request() {
916 let db = TestDb::sqlite().await;
917 for name in ["", " ", "a:b", "line\nbreak", &"a".repeat(65)] {
918 let result = add(
919 AdminUser(kellnr_http::test_support::session_identity("admin", true)),
920 State(db.provider()),
921 State(token_cache()),
922 State(Arc::new(Settings::default())),
923 Json(new_user(name)),
924 )
925 .await;
926
927 assert!(
928 result
929 .as_ref()
930 .is_err_and(|e| e.status() == StatusCode::BAD_REQUEST),
931 "{name:?}: {result:?}"
932 );
933 }
934 assert_eq!(db.get_users().await.unwrap().len(), 1);
935 }
936
937 #[tokio::test]
938 async fn test_add_token_with_invalid_name_is_bad_request() {
939 let db = TestDb::sqlite().await;
940 let admin = UserName::new("admin").unwrap();
941 let tokens_before = db.get_auth_tokens(&admin).await.unwrap().len();
942 for name in [String::new(), "a".repeat(129)] {
943 let result = add_token(
944 SessionUser(kellnr_http::test_support::session_identity("admin", false)),
945 State(db.provider()),
946 State(token_cache()),
947 Json(token::NewTokenReqData { name }),
948 )
949 .await;
950
951 assert!(
952 result
953 .as_ref()
954 .is_err_and(|e| e.status() == StatusCode::BAD_REQUEST)
955 );
956 }
957 assert_eq!(
958 db.get_auth_tokens(&admin).await.unwrap().len(),
959 tokens_before
960 );
961 }
962
963 #[tokio::test]
964 async fn test_add_existing_user_is_conflict() {
965 let db = TestDb::sqlite().await;
966 let cache = token_cache();
967 let settings = Arc::new(Settings::default());
968 let add_bob = || {
969 add(
970 AdminUser(kellnr_http::test_support::session_identity("admin", true)),
971 State(db.provider()),
972 State(cache.clone()),
973 State(settings.clone()),
974 Json(new_user("bob")),
975 )
976 };
977
978 add_bob().await.unwrap();
979 let response = add_bob().await.unwrap_err().into_response();
980
981 assert_eq!(response.status(), StatusCode::CONFLICT);
982 }
983
984 const CHANGE_PWD: &str =
985 r#"{"old_pwd":"pwd","new_pwd1":"newpassword","new_pwd2":"newpassword"}"#;
986
987 #[tokio::test]
990 async fn test_change_pwd_invalidates_cache() {
991 let (state, db) = sqlite_test_state().await;
992 let cookie = session_cookie(&db, "user", UserFlags::normal()).await;
993 let cache = token_cache_with("basic:credentials", "user").await;
994 let state = AppStateData {
995 token_cache: cache.clone(),
996 ..state
997 };
998
999 let response = send(
1000 state,
1001 Method::PUT,
1002 "/me/password",
1003 &cookie,
1004 Some(CHANGE_PWD),
1005 )
1006 .await;
1007
1008 assert_eq!(response.status(), StatusCode::OK);
1009 assert!(has_password(&db, "user", "newpassword").await);
1010 assert!(cache.get("basic:credentials").await.is_none());
1011 }
1012
1013 #[tokio::test]
1014 async fn test_reset_pwd_invalidates_cache() {
1015 let (state, db) = sqlite_test_state().await;
1016 let cookie = session_cookie(&db, "admin", UserFlags::admin()).await;
1017 add_user(&db, "user", UserFlags::normal()).await;
1018 let cache = token_cache_with("basic:credentials", "user").await;
1019 let state = AppStateData {
1020 token_cache: cache.clone(),
1021 ..state
1022 };
1023
1024 let response = send(state, Method::PUT, "/user/password", &cookie, None).await;
1025
1026 assert_eq!(response.status(), StatusCode::OK);
1027 assert!(!has_password(&db, "user", "pwd").await);
1028 assert!(cache.get("basic:credentials").await.is_none());
1029 }
1030
1031 #[tokio::test]
1034 async fn test_change_pwd_revokes_other_sessions() {
1035 let (state, db) = sqlite_test_state().await;
1036 add_user(&db, "user", UserFlags::normal()).await;
1037 add_session(&db, "user", "current").await;
1038 add_session(&db, "user", "other").await;
1039
1040 let response = send(
1041 state,
1042 Method::PUT,
1043 "/me/password",
1044 &encode_cookies([(COOKIE_SESSION_ID, "current")]),
1045 Some(CHANGE_PWD),
1046 )
1047 .await;
1048
1049 assert_eq!(response.status(), StatusCode::OK);
1050 assert!(has_session(&db, "current").await);
1051 assert!(!has_session(&db, "other").await);
1052 }
1053
1054 #[tokio::test]
1057 async fn test_reset_pwd_revokes_all_sessions_of_user() {
1058 let (state, db) = sqlite_test_state().await;
1059 let cookie = session_cookie(&db, "admin", UserFlags::admin()).await;
1060 add_user(&db, "user", UserFlags::normal()).await;
1061 add_session(&db, "user", "user-session").await;
1062
1063 let response = send(state, Method::PUT, "/user/password", &cookie, None).await;
1064
1065 assert_eq!(response.status(), StatusCode::OK);
1066 assert!(!has_session(&db, "user-session").await);
1067 }
1068
1069 #[tokio::test]
1070 async fn test_reset_own_pwd_keeps_current_session() {
1071 let (state, db) = sqlite_test_state().await;
1072 add_session(&db, "admin", "current").await;
1073 add_session(&db, "admin", "other").await;
1074
1075 let response = send(
1076 state,
1077 Method::PUT,
1078 "/admin/password",
1079 &encode_cookies([(COOKIE_SESSION_ID, "current")]),
1080 None,
1081 )
1082 .await;
1083
1084 assert_eq!(response.status(), StatusCode::OK);
1085 assert!(has_session(&db, "current").await);
1086 assert!(!has_session(&db, "other").await);
1087 }
1088
1089 #[tokio::test]
1090 async fn test_add_token_invalidates_cache() {
1091 let (state, db) = sqlite_test_state().await;
1092 let cookie = session_cookie(&db, "test_user", UserFlags::normal()).await;
1093 let cache = token_cache_with("existing_token", "test_user").await;
1094 let state = AppStateData {
1095 token_cache: cache.clone(),
1096 ..state
1097 };
1098
1099 let response = send(
1100 state,
1101 Method::POST,
1102 "/me/tokens",
1103 &cookie,
1104 Some(r#"{"name":"new_token"}"#),
1105 )
1106 .await;
1107
1108 assert_eq!(response.status(), StatusCode::OK);
1109 let tokens = db
1110 .get_auth_tokens(&UserName::new("test_user").unwrap())
1111 .await
1112 .unwrap();
1113 assert_eq!(
1114 tokens.iter().map(|t| t.name.as_str()).collect::<Vec<_>>(),
1115 ["new_token"]
1116 );
1117 assert!(cache.get("existing_token").await.is_none());
1118 }
1119
1120 async fn add_auth_token(db: &TestDb, user: &str) -> AuthTokenId {
1122 let user = UserName::new(user).unwrap();
1123 db.add_auth_token("ci", &PlainToken::from(format!("{user}-token")), &user)
1124 .await
1125 .unwrap();
1126 db.get_auth_tokens(&user).await.unwrap()[0].id
1127 }
1128
1129 #[tokio::test]
1130 async fn test_delete_token_invalidates_cache() {
1131 let (state, db) = sqlite_test_state().await;
1132 let cookie = session_cookie(&db, "test_user", UserFlags::normal()).await;
1133 let id = add_auth_token(&db, "test_user").await;
1134 let cache = token_cache_with("token_to_keep", "test_user").await;
1135 let state = AppStateData {
1136 token_cache: cache.clone(),
1137 ..state
1138 };
1139
1140 let response = send(
1141 state,
1142 Method::DELETE,
1143 &format!("/me/tokens/{id}"),
1144 &cookie,
1145 None,
1146 )
1147 .await;
1148
1149 assert_eq!(response.status(), StatusCode::OK);
1150 let user = UserName::new("test_user").unwrap();
1151 assert!(db.get_auth_tokens(&user).await.unwrap().is_empty());
1152 assert!(cache.get("token_to_keep").await.is_none());
1153 }
1154
1155 #[tokio::test]
1156 async fn test_delete_token_of_other_user_is_not_found() {
1157 let (state, db) = sqlite_test_state().await;
1158 let cookie = session_cookie(&db, "test_user", UserFlags::normal()).await;
1159 add_user(&db, "other", UserFlags::normal()).await;
1160 let id = add_auth_token(&db, "other").await;
1161
1162 let response = send(
1163 state,
1164 Method::DELETE,
1165 &format!("/me/tokens/{id}"),
1166 &cookie,
1167 None,
1168 )
1169 .await;
1170
1171 assert_eq!(StatusCode::NOT_FOUND, response.status());
1172 let other = UserName::new("other").unwrap();
1173 assert_eq!(db.get_auth_tokens(&other).await.unwrap().len(), 1);
1174 }
1175
1176 #[tokio::test]
1177 async fn test_delete_user_invalidates_cache() {
1178 let (state, db) = sqlite_test_state().await;
1179 let cookie = session_cookie(&db, "admin", UserFlags::admin()).await;
1180 add_user(&db, "user_to_delete", UserFlags::normal()).await;
1181 let cache = token_cache_with("user_token", "user_to_delete").await;
1182 let state = AppStateData {
1183 token_cache: cache.clone(),
1184 ..state
1185 };
1186
1187 let response = send(state, Method::DELETE, "/user_to_delete", &cookie, None).await;
1188
1189 assert_eq!(response.status(), StatusCode::OK);
1190 let name = UserName::new("user_to_delete").unwrap();
1191 assert!(db.get_user(&name).await.is_err());
1192 assert!(cache.get("user_token").await.is_none());
1193 }
1194
1195 #[tokio::test]
1196 async fn test_read_only_change_invalidates_cache() {
1197 let (state, db) = sqlite_test_state().await;
1198 let cookie = session_cookie(&db, "admin", UserFlags::admin()).await;
1199 add_user(&db, "target_user", UserFlags::normal()).await;
1200 let cache = token_cache_with("user_token", "target_user").await;
1201 let state = AppStateData {
1202 token_cache: cache.clone(),
1203 ..state
1204 };
1205
1206 let response = send(
1207 state,
1208 Method::POST,
1209 "/target_user/read-only",
1210 &cookie,
1211 Some(r#"{"state":true}"#),
1212 )
1213 .await;
1214
1215 assert_eq!(response.status(), StatusCode::OK);
1216 assert!(user(&db, "target_user").await.is_read_only);
1217 assert!(cache.get("user_token").await.is_none());
1219 }
1220
1221 #[tokio::test]
1222 async fn test_admin_self_locking_prevented() {
1223 let (state, db) = sqlite_test_state().await;
1224 let cookie = session_cookie(&db, "admin", UserFlags::admin()).await;
1225
1226 let response = send(
1227 state,
1228 Method::POST,
1229 "/admin/read-only",
1230 &cookie,
1231 Some(r#"{"state":true}"#),
1232 )
1233 .await;
1234
1235 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
1236 assert!(!user(&db, "admin").await.is_read_only);
1237 }
1238
1239 #[tokio::test]
1241 async fn test_admin_self_unlocking_allowed() {
1242 let (state, db) = sqlite_test_state().await;
1243 let cookie = session_cookie(&db, "admin", UserFlags::admin()).await;
1244
1245 let response = send(
1246 state,
1247 Method::POST,
1248 "/admin/read-only",
1249 &cookie,
1250 Some(r#"{"state":false}"#),
1251 )
1252 .await;
1253
1254 assert_eq!(response.status(), StatusCode::OK);
1255 assert!(!user(&db, "admin").await.is_read_only);
1256 }
1257
1258 #[tokio::test]
1259 async fn test_admin_locking_other_user_works() {
1260 let (state, db) = sqlite_test_state().await;
1261 let cookie = session_cookie(&db, "admin", UserFlags::admin()).await;
1262 add_user(&db, "other_user", UserFlags::normal()).await;
1263
1264 let response = send(
1265 state,
1266 Method::POST,
1267 "/other_user/read-only",
1268 &cookie,
1269 Some(r#"{"state":true}"#),
1270 )
1271 .await;
1272
1273 assert_eq!(response.status(), StatusCode::OK);
1274 assert!(user(&db, "other_user").await.is_read_only);
1275 }
1276
1277 #[tokio::test]
1278 async fn test_add_user_invalidates_cache() {
1279 let (state, db) = sqlite_test_state().await;
1280 let cookie = session_cookie(&db, "admin", UserFlags::admin()).await;
1281 let cache = token_cache_with("existing_token", "existing_user").await;
1282 let state = AppStateData {
1283 token_cache: cache.clone(),
1284 ..state
1285 };
1286
1287 let response = send(
1288 state,
1289 Method::POST,
1290 "/",
1291 &cookie,
1292 Some(
1293 r#"{"name":"new_user","pwd1":"password","pwd2":"password","is_admin":false,"is_read_only":false}"#,
1294 ),
1295 )
1296 .await;
1297
1298 assert_eq!(response.status(), StatusCode::OK);
1299 assert!(has_password(&db, "new_user", "password").await);
1300 assert!(cache.get("existing_token").await.is_none());
1301 }
1302
1303 #[tokio::test]
1304 async fn test_cache_not_invalidated_on_db_failure() {
1305 let (state, db) = sqlite_test_state().await;
1306 let cookie = session_cookie(&db, "test_user", UserFlags::normal()).await;
1307 test_drop_table(&db, "auth_token").await.unwrap();
1308 let cache = token_cache_with("existing_token", "test_user").await;
1309 let state = AppStateData {
1310 token_cache: cache.clone(),
1311 ..state
1312 };
1313
1314 let response = send(
1315 state,
1316 Method::POST,
1317 "/me/tokens",
1318 &cookie,
1319 Some(r#"{"name":"new_token"}"#),
1320 )
1321 .await;
1322
1323 assert!(!response.status().is_success());
1324 assert!(cache.get("existing_token").await.is_some());
1325 }
1326
1327 #[tokio::test]
1328 async fn test_admin_change_invalidates_cache() {
1329 let (state, db) = sqlite_test_state().await;
1330 let cookie = session_cookie(&db, "admin", UserFlags::admin()).await;
1331 add_user(&db, "target_user", UserFlags::normal()).await;
1332 let cache = token_cache_with("user_token", "target_user").await;
1333 let state = AppStateData {
1334 token_cache: cache.clone(),
1335 ..state
1336 };
1337
1338 let response = send(
1339 state,
1340 Method::POST,
1341 "/target_user/admin",
1342 &cookie,
1343 Some(r#"{"state":true}"#),
1344 )
1345 .await;
1346
1347 assert_eq!(response.status(), StatusCode::OK);
1348 assert!(user(&db, "target_user").await.is_admin);
1349 assert!(cache.get("user_token").await.is_none());
1351 }
1352
1353 #[tokio::test]
1354 async fn test_admin_self_demotion_prevented() {
1355 let (state, db) = sqlite_test_state().await;
1356 let cookie = session_cookie(&db, "admin", UserFlags::admin()).await;
1357
1358 let response = send(
1359 state,
1360 Method::POST,
1361 "/admin/admin",
1362 &cookie,
1363 Some(r#"{"state":false}"#),
1364 )
1365 .await;
1366
1367 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
1368 assert!(user(&db, "admin").await.is_admin);
1369 }
1370
1371 #[tokio::test]
1372 async fn test_non_admin_cannot_change_admin_status() {
1373 let (state, db) = sqlite_test_state().await;
1374 let cookie = session_cookie(&db, "regular_user", UserFlags::normal()).await;
1375 add_user(&db, "target_user", UserFlags::normal()).await;
1376
1377 let response = send(
1378 state,
1379 Method::POST,
1380 "/target_user/admin",
1381 &cookie,
1382 Some(r#"{"state":true}"#),
1383 )
1384 .await;
1385
1386 assert_eq!(response.status(), StatusCode::FORBIDDEN);
1387 assert!(!user(&db, "target_user").await.is_admin);
1388 }
1389
1390 #[tokio::test]
1391 async fn test_admin_demotion_works() {
1392 let (state, db) = sqlite_test_state().await;
1393 let cookie = session_cookie(&db, "admin", UserFlags::admin()).await;
1394 add_user(&db, "other_admin", UserFlags::admin()).await;
1395
1396 let response = send(
1397 state,
1398 Method::POST,
1399 "/other_admin/admin",
1400 &cookie,
1401 Some(r#"{"state":false}"#),
1402 )
1403 .await;
1404
1405 assert_eq!(response.status(), StatusCode::OK);
1406 assert!(!user(&db, "other_admin").await.is_admin);
1407 }
1408
1409 #[tokio::test]
1410 async fn test_admin_nonexistent_user_returns_error() {
1411 let (state, db) = sqlite_test_state().await;
1412 let cookie = session_cookie(&db, "admin", UserFlags::admin()).await;
1413
1414 let response = send(
1415 state,
1416 Method::POST,
1417 "/nonexistent/admin",
1418 &cookie,
1419 Some(r#"{"state":true}"#),
1420 )
1421 .await;
1422
1423 assert_eq!(response.status(), StatusCode::NOT_FOUND);
1424 }
1425
1426 #[tokio::test]
1428 async fn test_admin_self_promotion_allowed() {
1429 let (state, db) = sqlite_test_state().await;
1430 let cookie = session_cookie(&db, "admin", UserFlags::admin()).await;
1431
1432 let response = send(
1433 state,
1434 Method::POST,
1435 "/admin/admin",
1436 &cookie,
1437 Some(r#"{"state":true}"#),
1438 )
1439 .await;
1440
1441 assert_eq!(response.status(), StatusCode::OK);
1442 assert!(user(&db, "admin").await.is_admin);
1443 }
1444
1445 #[tokio::test]
1446 async fn test_admin_self_deletion_prevented() {
1447 let (state, db) = sqlite_test_state().await;
1448 let cookie = session_cookie(&db, "admin", UserFlags::admin()).await;
1449
1450 let response = send(state, Method::DELETE, "/admin", &cookie, None).await;
1451
1452 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
1453 user(&db, "admin").await;
1454 }
1455
1456 #[tokio::test]
1457 async fn test_admin_deletion_of_other_user_works() {
1458 let (state, db) = sqlite_test_state().await;
1459 let cookie = session_cookie(&db, "admin", UserFlags::admin()).await;
1460 add_user(&db, "other_user", UserFlags::normal()).await;
1461
1462 let response = send(state, Method::DELETE, "/other_user", &cookie, None).await;
1463
1464 assert_eq!(response.status(), StatusCode::OK);
1465 let name = UserName::new("other_user").unwrap();
1466 assert!(db.get_user(&name).await.is_err());
1467 }
1468
1469 #[tokio::test]
1471 async fn test_admin_deletion_of_user_with_name_against_the_rules_works() {
1472 let legacy_name = "legacy:user";
1473 assert!(UserName::for_new_user(legacy_name).is_err());
1474 let (state, db) = sqlite_test_state().await;
1475 let cookie = session_cookie(&db, "admin", UserFlags::admin()).await;
1476 add_user(&db, legacy_name, UserFlags::normal()).await;
1477
1478 let response = send(
1479 state,
1480 Method::DELETE,
1481 &format!("/{legacy_name}"),
1482 &cookie,
1483 None,
1484 )
1485 .await;
1486
1487 assert_eq!(response.status(), StatusCode::OK);
1488 let name = UserName::new(legacy_name).unwrap();
1489 assert!(db.get_user(&name).await.is_err());
1490 }
1491}