api_admin.rs
⎇
Raw
1//! Admin API: user management, lockout guards, server settings.
2
3mod common;
4
5use axum::http::StatusCode;
6use common::*;
7use serde_json::json;
8
9#[tokio::test]
10async fn admin_routes_require_admin() {
11 let env = Env::new().await;
12 let admin = env.admin().await;
13
14 // No session → 401.
15 let anon = Client::new(env.app.clone());
16 assert_eq!(
17 anon.get("/api/admin/users").await.status,
18 StatusCode::UNAUTHORIZED
19 );
20 assert_eq!(
21 anon.get("/api/admin/settings").await.status,
22 StatusCode::UNAUTHORIZED
23 );
24
25 // Non-admin session → 403.
26 create_user(&admin, "bob", "bobpass123", &[("docs", "rw")]).await;
27 let bob = login(&env, "bob", "bobpass123").await;
28 assert_eq!(
29 bob.get("/api/admin/users").await.status,
30 StatusCode::FORBIDDEN
31 );
32 assert_eq!(
33 bob.put_json(
34 "/api/admin/settings",
35 &json!({ "allow_writable_shares": true })
36 )
37 .await
38 .status,
39 StatusCode::FORBIDDEN
40 );
41}
42
43#[tokio::test]
44async fn user_lifecycle() {
45 let env = Env::new().await;
46 let admin = env.admin().await;
47
48 // Create.
49 let j = create_user(
50 &admin,
51 "bob",
52 "bobpass123",
53 &[("docs", "rw"), ("src", "ro")],
54 )
55 .await;
56 let bob_id = j["id"].as_i64().unwrap();
57 assert_eq!(j["name"], "bob");
58 assert_eq!(j["is_admin"], false);
59 assert_eq!(j["active"], true);
60 assert_eq!(j["roots"].as_array().unwrap().len(), 2);
61
62 // bob can log in and sees his roots.
63 let bob = login(&env, "bob", "bobpass123").await;
64 let me = bob.get("/api/auth/me").await.json();
65 let roots = me["roots"].as_array().unwrap();
66 assert_eq!(roots.len(), 2);
67
68 // Duplicate name (case-insensitive) → 409.
69 let r = admin
70 .post_json(
71 "/api/admin/users",
72 &json!({ "name": "BOB", "password": "whatever12", "roots": [] }),
73 )
74 .await;
75 assert_eq!(r.status, StatusCode::CONFLICT);
76 // Weak password / bad name → 400.
77 let r = admin
78 .post_json(
79 "/api/admin/users",
80 &json!({ "name": "carol", "password": "short", "roots": [] }),
81 )
82 .await;
83 assert_eq!(r.status, StatusCode::BAD_REQUEST);
84 let r = admin
85 .post_json(
86 "/api/admin/users",
87 &json!({ "name": " ", "password": "longenough1", "roots": [] }),
88 )
89 .await;
90 assert_eq!(r.status, StatusCode::BAD_REQUEST);
91 // Bad root paths → 400.
92 for bad in ["no-such-dir", "../escape", "config.json"] {
93 let r = admin
94 .post_json(
95 "/api/admin/users",
96 &json!({ "name": "dave", "password": "longenough1", "roots": [{ "path": bad, "mode": "rw" }] }),
97 )
98 .await;
99 assert_eq!(
100 r.status,
101 StatusCode::BAD_REQUEST,
102 "root '{bad}' should be rejected"
103 );
104 }
105 // Bad mode → 422: `api_types::Mode` only accepts "rw"/"ro", so the JSON
106 // extractor rejects the body before the handler runs.
107 let r = admin
108 .post_json(
109 "/api/admin/users",
110 &json!({ "name": "dave", "password": "longenough1", "roots": [{ "path": "docs", "mode": "both" }] }),
111 )
112 .await;
113 assert_eq!(r.status, StatusCode::UNPROCESSABLE_ENTITY);
114
115 // Update: password reset.
116 let r = admin
117 .put_json(
118 &format!("/api/admin/users/{bob_id}"),
119 &json!({ "password": "newpass123" }),
120 )
121 .await;
122 assert_eq!(r.status, StatusCode::OK);
123 assert!(
124 login(&env, "bob", "newpass123")
125 .await
126 .get("/api/auth/me")
127 .await
128 .status
129 == StatusCode::OK
130 );
131 let anon = Client::new(env.app.clone());
132 assert_eq!(
133 anon.post_json(
134 "/api/auth/login",
135 &json!({ "name": "bob", "password": "bobpass123" })
136 )
137 .await
138 .status,
139 StatusCode::UNAUTHORIZED
140 );
141
142 // Update: replace roots.
143 let r = admin
144 .put_json(
145 &format!("/api/admin/users/{bob_id}"),
146 &json!({ "roots": [{ "path": "src", "mode": "rw" }] }),
147 )
148 .await;
149 assert_eq!(r.status, StatusCode::OK);
150 let updated = r.json();
151 let roots = updated["roots"].as_array().unwrap();
152 assert_eq!(roots.len(), 1);
153 assert_eq!(roots[0]["path"], "src");
154
155 // Update: make admin, then demote.
156 let r = admin
157 .put_json(
158 &format!("/api/admin/users/{bob_id}"),
159 &json!({ "is_admin": true }),
160 )
161 .await;
162 assert_eq!(r.json()["is_admin"], true);
163 // bob can now use the admin API.
164 let bob = login(&env, "bob", "newpass123").await;
165 assert_eq!(bob.get("/api/admin/users").await.status, StatusCode::OK);
166 let r = admin
167 .put_json(
168 &format!("/api/admin/users/{bob_id}"),
169 &json!({ "is_admin": false }),
170 )
171 .await;
172 assert_eq!(r.json()["is_admin"], false);
173
174 // Update: disable → re-enable.
175 let r = admin
176 .put_json(
177 &format!("/api/admin/users/{bob_id}"),
178 &json!({ "active": false }),
179 )
180 .await;
181 assert_eq!(r.json()["active"], false);
182 let anon = Client::new(env.app.clone());
183 assert_eq!(
184 anon.post_json(
185 "/api/auth/login",
186 &json!({ "name": "bob", "password": "newpass123" })
187 )
188 .await
189 .status,
190 StatusCode::UNAUTHORIZED
191 );
192 let r = admin
193 .put_json(
194 &format!("/api/admin/users/{bob_id}"),
195 &json!({ "active": true }),
196 )
197 .await;
198 assert_eq!(r.json()["active"], true);
199
200 // Delete.
201 let r = admin.delete(&format!("/api/admin/users/{bob_id}")).await;
202 assert_eq!(r.status, StatusCode::OK);
203 let r = admin.get("/api/admin/users").await;
204 let names: Vec<String> = r
205 .json()
206 .as_array()
207 .unwrap()
208 .iter()
209 .map(|u| u["name"].as_str().unwrap().to_string())
210 .collect();
211 assert_eq!(names, vec!["admin"]);
212 assert_eq!(
213 admin
214 .delete(&format!("/api/admin/users/{bob_id}"))
215 .await
216 .status,
217 StatusCode::NOT_FOUND
218 );
219}
220
221#[tokio::test]
222async fn lockout_guards() {
223 let env = Env::new().await;
224 let admin = env.admin().await;
225 let me = admin.get("/api/auth/me").await;
226 let my_id = me.json()["user"]["id"].as_i64().unwrap();
227
228 // Cannot demote yourself.
229 let r = admin
230 .put_json(
231 &format!("/api/admin/users/{my_id}"),
232 &json!({ "is_admin": false }),
233 )
234 .await;
235 assert_eq!(r.status, StatusCode::BAD_REQUEST);
236 // Cannot disable yourself.
237 let r = admin
238 .put_json(
239 &format!("/api/admin/users/{my_id}"),
240 &json!({ "active": false }),
241 )
242 .await;
243 assert_eq!(r.status, StatusCode::BAD_REQUEST);
244 // Cannot delete yourself.
245 let r = admin.delete(&format!("/api/admin/users/{my_id}")).await;
246 assert_eq!(r.status, StatusCode::BAD_REQUEST);
247 // You're still fine.
248 assert_eq!(admin.get("/api/auth/me").await.status, StatusCode::OK);
249
250 // Promote a second admin, so the first is no longer the "last" admin.
251 create_user(&admin, "eve", "evepass123", &[("docs", "rw")]).await;
252 let eve_id = user_id(&admin, "eve").await;
253 let r = admin
254 .put_json(
255 &format!("/api/admin/users/{eve_id}"),
256 &json!({ "is_admin": true }),
257 )
258 .await;
259 assert_eq!(r.status, StatusCode::OK);
260 // Now demoting eve is allowed (two active admins exist).
261 let r = admin
262 .put_json(
263 &format!("/api/admin/users/{eve_id}"),
264 &json!({ "is_admin": false }),
265 )
266 .await;
267 assert_eq!(r.status, StatusCode::OK);
268 assert_eq!(r.json()["is_admin"], false);
269 // But demoting the (now only) remaining admin via another account is
270 // blocked: promote eve again, demote admin would need admin to act on
271 // themselves — already covered. Instead: with exactly one active admin
272 // left (admin), demoting eve (not an admin anymore) is fine, and the
273 // last-admin guard still fires for admins. Re-promote eve and confirm
274 // the guard message when count == 1 after disabling admin — but admin
275 // can't disable self. So: two admins, disable one (allowed), then the
276 // remaining one is the last → deleting the *disabled* one is allowed.
277 let r = admin
278 .put_json(
279 &format!("/api/admin/users/{eve_id}"),
280 &json!({ "is_admin": true, "active": false }),
281 )
282 .await;
283 // eve was an admin but is now disabled → not counted. Deleting her:
284 // target.active == false → the guard (which requires active) does not
285 // fire → allowed.
286 assert_eq!(r.status, StatusCode::OK);
287 let r = admin.delete(&format!("/api/admin/users/{eve_id}")).await;
288 assert_eq!(r.status, StatusCode::OK);
289 // admin still works.
290 assert_eq!(admin.get("/api/auth/me").await.status, StatusCode::OK);
291}
292
293#[tokio::test]
294async fn settings_round_trip_visible_in_me() {
295 let env = Env::new().await;
296 let admin = env.admin().await;
297
298 let r = admin.get("/api/admin/settings").await;
299 assert_eq!(r.status, StatusCode::OK);
300 assert_eq!(r.json()["allow_writable_shares"], false);
301
302 let r = admin
303 .put_json(
304 "/api/admin/settings",
305 &json!({ "allow_writable_shares": true }),
306 )
307 .await;
308 assert_eq!(r.json()["allow_writable_shares"], true);
309
310 // Reflected in /me for clients.
311 assert_eq!(
312 admin.get("/api/auth/me").await.json()["allow_writable_shares"],
313 true
314 );
315
316 let r = admin
317 .put_json(
318 "/api/admin/settings",
319 &json!({ "allow_writable_shares": false }),
320 )
321 .await;
322 assert_eq!(r.json()["allow_writable_shares"], false);
323}
324
325#[tokio::test]
326async fn update_unknown_user_is_404() {
327 let env = Env::new().await;
328 let admin = env.admin().await;
329 let r = admin
330 .put_json("/api/admin/users/9999", &json!({ "active": true }))
331 .await;
332 assert_eq!(r.status, StatusCode::NOT_FOUND);
333}
334
335#[tokio::test]
336async fn admins_can_browse_the_server_root_read_only() {
337 let env = Env::new().await;
338 let admin = env.admin().await;
339
340 // Listing and descending work through the normal files API.
341 let r = admin.get("/api/files/-1").await;
342 assert_eq!(r.status, StatusCode::OK);
343 let j = r.json();
344 let names: Vec<&str> = j["entries"]
345 .as_array()
346 .unwrap()
347 .iter()
348 .filter(|e| e["is_dir"] == true)
349 .map(|e| e["name"].as_str().unwrap())
350 .collect();
351 assert_eq!(names, ["docs", "src"]);
352 assert_eq!(admin.get("/api/files/-1/docs").await.status, StatusCode::OK);
353 // Read-only: no writes through the pseudo root.
354 let r = admin
355 .raw(
356 axum::http::Method::POST,
357 "/api/files/-1/newdir?action=mkdir",
358 &[],
359 Vec::new(),
360 )
361 .await;
362 assert_eq!(r.status, StatusCode::FORBIDDEN);
363 assert!(!env.file("newdir").exists());
364
365 // Not an admin → no such folder.
366 create_user(&admin, "bob", "bobpass123", &[(".", "rw")]).await;
367 let bob = login(&env, "bob", "bobpass123").await;
368 assert_eq!(bob.get("/api/files/-1").await.status, StatusCode::FORBIDDEN);
369}
370