//! Admin API: user management, lockout guards, server settings. use crate::common::*; use axum::http::StatusCode; use serde_json::json; #[tokio::test] async fn admin_routes_require_admin() { let env = Env::new().await; let admin = env.admin().await; // No session → 401. let anon = Client::new(env.app.clone()); assert_eq!( anon.get("/api/admin/users").await.status, StatusCode::UNAUTHORIZED ); assert_eq!( anon.get("/api/admin/settings").await.status, StatusCode::UNAUTHORIZED ); assert_eq!( anon.get("/api/admin/shares").await.status, StatusCode::UNAUTHORIZED, "the listing of every share link on the server is not public" ); assert_eq!( anon.delete("/api/admin/shares/1").await.status, StatusCode::UNAUTHORIZED ); // Non-admin session → 403. create_user(&admin, "bob", "bobpass123", &[("docs", "rw")]).await; let bob = login(&env, "bob", "bobpass123").await; assert_eq!( bob.get("/api/admin/users").await.status, StatusCode::FORBIDDEN ); assert_eq!( bob.get("/api/admin/shares").await.status, StatusCode::FORBIDDEN, "a plain user must not read the links of others" ); assert_eq!( bob.delete("/api/admin/shares/1").await.status, StatusCode::FORBIDDEN, "nor revoke them" ); assert_eq!( bob.put_json( "/api/admin/settings", &json!({ "allow_writable_shares": true }) ) .await .status, StatusCode::FORBIDDEN ); } #[tokio::test] async fn user_lifecycle() { let env = Env::new().await; let admin = env.admin().await; // Create. let j = create_user( &admin, "bob", "bobpass123", &[("docs", "rw"), ("src", "ro")], ) .await; let bob_id = j["id"].as_i64().unwrap(); assert_eq!(j["name"], "bob"); assert_eq!(j["is_admin"], false); assert_eq!(j["active"], true); assert_eq!(j["roots"].as_array().unwrap().len(), 2); // bob can log in and sees his roots. let bob = login(&env, "bob", "bobpass123").await; let me = bob.get("/api/auth/me").await.json(); let roots = me["roots"].as_array().unwrap(); assert_eq!(roots.len(), 2); // Duplicate name (case-insensitive) → 409. let r = admin .post_json( "/api/admin/users", &json!({ "name": "BOB", "password": "whatever12", "roots": [] }), ) .await; assert_eq!(r.status, StatusCode::CONFLICT); // Weak password / bad name → 400. let r = admin .post_json( "/api/admin/users", &json!({ "name": "carol", "password": "short", "roots": [] }), ) .await; assert_eq!(r.status, StatusCode::BAD_REQUEST); let r = admin .post_json( "/api/admin/users", &json!({ "name": " ", "password": "longenough1", "roots": [] }), ) .await; assert_eq!(r.status, StatusCode::BAD_REQUEST); // Bad root paths → 400. for bad in ["no-such-dir", "../escape", "config.json"] { let r = admin .post_json( "/api/admin/users", &json!({ "name": "dave", "password": "longenough1", "roots": [{ "path": bad, "mode": "rw" }] }), ) .await; assert_eq!( r.status, StatusCode::BAD_REQUEST, "root '{bad}' should be rejected" ); } // Bad mode → 422: `api_types::Mode` only accepts "rw"/"ro", so the JSON // extractor rejects the body before the handler runs. let r = admin .post_json( "/api/admin/users", &json!({ "name": "dave", "password": "longenough1", "roots": [{ "path": "docs", "mode": "both" }] }), ) .await; assert_eq!(r.status, StatusCode::UNPROCESSABLE_ENTITY); // Update: password reset. let r = admin .put_json( &format!("/api/admin/users/{bob_id}"), &json!({ "password": "newpass123" }), ) .await; assert_eq!(r.status, StatusCode::OK); assert!( login(&env, "bob", "newpass123") .await .get("/api/auth/me") .await .status == StatusCode::OK ); let anon = Client::new(env.app.clone()); assert_eq!( anon.post_json( "/api/auth/login", &json!({ "name": "bob", "password": "bobpass123" }) ) .await .status, StatusCode::UNAUTHORIZED ); // Update: replace roots. let r = admin .put_json( &format!("/api/admin/users/{bob_id}"), &json!({ "roots": [{ "path": "src", "mode": "rw" }] }), ) .await; assert_eq!(r.status, StatusCode::OK); let updated = r.json(); let roots = updated["roots"].as_array().unwrap(); assert_eq!(roots.len(), 1); assert_eq!(roots[0]["path"], "src"); // Update: make admin, then demote. let r = admin .put_json( &format!("/api/admin/users/{bob_id}"), &json!({ "is_admin": true }), ) .await; assert_eq!(r.json()["is_admin"], true); // bob can now use the admin API. let bob = login(&env, "bob", "newpass123").await; assert_eq!(bob.get("/api/admin/users").await.status, StatusCode::OK); let r = admin .put_json( &format!("/api/admin/users/{bob_id}"), &json!({ "is_admin": false }), ) .await; assert_eq!(r.json()["is_admin"], false); // Update: disable → re-enable. let r = admin .put_json( &format!("/api/admin/users/{bob_id}"), &json!({ "active": false }), ) .await; assert_eq!(r.json()["active"], false); let anon = Client::new(env.app.clone()); assert_eq!( anon.post_json( "/api/auth/login", &json!({ "name": "bob", "password": "newpass123" }) ) .await .status, StatusCode::UNAUTHORIZED ); let r = admin .put_json( &format!("/api/admin/users/{bob_id}"), &json!({ "active": true }), ) .await; assert_eq!(r.json()["active"], true); // Delete. let r = admin.delete(&format!("/api/admin/users/{bob_id}")).await; assert_eq!(r.status, StatusCode::OK); let r = admin.get("/api/admin/users").await; let names: Vec = r .json() .as_array() .unwrap() .iter() .map(|u| u["name"].as_str().unwrap().to_string()) .collect(); assert_eq!(names, vec!["admin"]); assert_eq!( admin .delete(&format!("/api/admin/users/{bob_id}")) .await .status, StatusCode::NOT_FOUND ); } #[tokio::test] async fn lockout_guards() { let env = Env::new().await; let admin = env.admin().await; let me = admin.get("/api/auth/me").await; let my_id = me.json()["user"]["id"].as_i64().unwrap(); // Cannot demote yourself. let r = admin .put_json( &format!("/api/admin/users/{my_id}"), &json!({ "is_admin": false }), ) .await; assert_eq!(r.status, StatusCode::BAD_REQUEST); // Cannot disable yourself. let r = admin .put_json( &format!("/api/admin/users/{my_id}"), &json!({ "active": false }), ) .await; assert_eq!(r.status, StatusCode::BAD_REQUEST); // Cannot delete yourself. let r = admin.delete(&format!("/api/admin/users/{my_id}")).await; assert_eq!(r.status, StatusCode::BAD_REQUEST); // You're still fine. assert_eq!(admin.get("/api/auth/me").await.status, StatusCode::OK); // Promote a second admin, so the first is no longer the "last" admin. create_user(&admin, "eve", "evepass123", &[("docs", "rw")]).await; let eve_id = user_id(&admin, "eve").await; let r = admin .put_json( &format!("/api/admin/users/{eve_id}"), &json!({ "is_admin": true }), ) .await; assert_eq!(r.status, StatusCode::OK); // Now demoting eve is allowed (two active admins exist). let r = admin .put_json( &format!("/api/admin/users/{eve_id}"), &json!({ "is_admin": false }), ) .await; assert_eq!(r.status, StatusCode::OK); assert_eq!(r.json()["is_admin"], false); // But demoting the (now only) remaining admin via another account is // blocked: promote eve again, demote admin would need admin to act on // themselves — already covered. Instead: with exactly one active admin // left (admin), demoting eve (not an admin anymore) is fine, and the // last-admin guard still fires for admins. Re-promote eve and confirm // the guard message when count == 1 after disabling admin — but admin // can't disable self. So: two admins, disable one (allowed), then the // remaining one is the last → deleting the *disabled* one is allowed. let r = admin .put_json( &format!("/api/admin/users/{eve_id}"), &json!({ "is_admin": true, "active": false }), ) .await; // eve was an admin but is now disabled → not counted. Deleting her: // target.active == false → the guard (which requires active) does not // fire → allowed. assert_eq!(r.status, StatusCode::OK); let r = admin.delete(&format!("/api/admin/users/{eve_id}")).await; assert_eq!(r.status, StatusCode::OK); // admin still works. assert_eq!(admin.get("/api/auth/me").await.status, StatusCode::OK); } #[tokio::test] async fn settings_round_trip_visible_in_me() { let env = Env::new().await; let admin = env.admin().await; let r = admin.get("/api/admin/settings").await; assert_eq!(r.status, StatusCode::OK); assert_eq!(r.json()["allow_writable_shares"], false); let r = admin .put_json( "/api/admin/settings", &json!({ "allow_writable_shares": true }), ) .await; assert_eq!(r.json()["allow_writable_shares"], true); // Reflected in /me for clients. assert_eq!( admin.get("/api/auth/me").await.json()["allow_writable_shares"], true ); let r = admin .put_json( "/api/admin/settings", &json!({ "allow_writable_shares": false }), ) .await; assert_eq!(r.json()["allow_writable_shares"], false); } #[tokio::test] async fn update_unknown_user_is_404() { let env = Env::new().await; let admin = env.admin().await; let r = admin .put_json("/api/admin/users/9999", &json!({ "active": true })) .await; assert_eq!(r.status, StatusCode::NOT_FOUND); } #[tokio::test] async fn admins_can_browse_the_server_root_read_only() { let env = Env::new().await; let admin = env.admin().await; // Listing and descending work through the normal files API. let r = admin.get("/api/files/-1").await; assert_eq!(r.status, StatusCode::OK); let j = r.json(); let names: Vec<&str> = j["entries"] .as_array() .unwrap() .iter() .filter(|e| e["is_dir"] == true) .map(|e| e["name"].as_str().unwrap()) .collect(); assert_eq!(names, ["docs", "src"]); assert_eq!(admin.get("/api/files/-1/docs").await.status, StatusCode::OK); // Read-only: no writes through the pseudo root. let r = admin .raw( axum::http::Method::POST, "/api/files/-1/newdir?action=mkdir", &[], Vec::new(), ) .await; assert_eq!(r.status, StatusCode::FORBIDDEN); assert!(!env.file("newdir").exists()); // Not an admin → no such folder. create_user(&admin, "bob", "bobpass123", &[(".", "rw")]).await; let bob = login(&env, "bob", "bobpass123").await; assert_eq!(bob.get("/api/files/-1").await.status, StatusCode::FORBIDDEN); }