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