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