Rust implementation of OCI Distribution Spec with granular access control
1use axum::{
2 body::Body,
3 extract::{Path, Query, State},
4 http::{HeaderMap, StatusCode},
5 response::Response,
6};
7use bytes::Bytes;
8use serde::{Deserialize, Serialize};
9use std::sync::Arc;
10use utoipa::ToSchema;
11
12use crate::{auth, gc, permissions, response, state};
13
14#[derive(Debug, Deserialize, Serialize, ToSchema)]
15pub struct CreateUserRequest {
16 pub username: String,
17 pub password: String,
18 #[serde(default)]
19 pub permissions: Vec<state::Permission>,
20}
21
22#[derive(Debug, Deserialize, Serialize, ToSchema)]
23pub struct AddPermissionRequest {
24 pub repository: String,
25 pub tag: String,
26 pub actions: Vec<String>,
27}
28
29#[derive(Debug, Deserialize, Serialize, ToSchema)]
30pub struct AddPermissionWithUsernameRequest {
31 pub username: String,
32 pub repository: String,
33 pub tag: String,
34 pub actions: Vec<String>,
35}
36
37/// Check if user is admin (has wildcard delete permission)
38fn is_admin(user: &state::User) -> bool {
39 permissions::has_permission(user, "*", Some("*"), permissions::Action::Delete)
40}
41
42/// List all users (admin only)
43#[utoipa::path(
44 get,
45 path = "/admin/users",
46 responses(
47 (status = 200, description = "List of all users with their permissions", content_type = "application/json"),
48 (status = 401, description = "Unauthorized - authentication required"),
49 (status = 403, description = "Forbidden - admin permission required")
50 ),
51 security(
52 ("basic_auth" = [])
53 )
54)]
55pub async fn list_users(State(state): State<Arc<state::App>>, headers: HeaderMap) -> Response {
56 let host = &state.args.host;
57
58 // Authenticate
59 let user = match auth::authenticate_user(&state, &headers).await {
60 Ok(u) => u,
61 Err(_) => return response::unauthorized(host),
62 };
63
64 // Check admin permission
65 if !is_admin(&user) {
66 return response::forbidden();
67 }
68
69 // Get users
70 let users = state.users.lock().await;
71 let user_list: Vec<_> = users
72 .iter()
73 .map(|u| {
74 serde_json::json!({
75 "username": u.username,
76 "permissions": u.permissions,
77 })
78 })
79 .collect();
80
81 Response::builder()
82 .status(StatusCode::OK)
83 .header("Content-Type", "application/json")
84 .body(Body::from(
85 serde_json::json!({
86 "users": user_list
87 })
88 .to_string(),
89 ))
90 .unwrap()
91}
92
93/// Create new user (admin only)
94#[utoipa::path(
95 post,
96 path = "/admin/users",
97 request_body = CreateUserRequest,
98 responses(
99 (status = 201, description = "User created successfully", content_type = "application/json"),
100 (status = 400, description = "Bad request - invalid JSON"),
101 (status = 401, description = "Unauthorized - authentication required"),
102 (status = 403, description = "Forbidden - admin permission required"),
103 (status = 409, description = "Conflict - user already exists"),
104 (status = 500, description = "Internal server error - failed to save users")
105 ),
106 security(
107 ("basic_auth" = [])
108 )
109)]
110pub async fn create_user(
111 State(state): State<Arc<state::App>>,
112 headers: HeaderMap,
113 body: Bytes,
114) -> Response {
115 let host = &state.args.host;
116
117 // Authenticate
118 let user = match auth::authenticate_user(&state, &headers).await {
119 Ok(u) => u,
120 Err(_) => return response::unauthorized(host),
121 };
122
123 // Check admin permission
124 if !is_admin(&user) {
125 return response::forbidden();
126 }
127
128 // Parse request
129 let req: CreateUserRequest = match serde_json::from_slice(&body) {
130 Ok(r) => r,
131 Err(e) => {
132 return Response::builder()
133 .status(StatusCode::BAD_REQUEST)
134 .body(Body::from(format!("Invalid request: {}", e)))
135 .unwrap();
136 }
137 };
138
139 // Create new user
140 let new_user = state::User {
141 username: req.username.clone(),
142 password: req.password,
143 permissions: req.permissions,
144 };
145
146 // Add to users set
147 {
148 let mut users = state.users.lock().await;
149
150 // Check if user already exists
151 if users.iter().any(|u| u.username == new_user.username) {
152 return response::conflict("User already exists");
153 }
154
155 users.insert(new_user.clone());
156 }
157
158 // Persist to file
159 if let Err(e) = save_users(&state).await {
160 log::error!("Failed to save users: {}", e);
161 return response::internal_error();
162 }
163
164 log::info!("Created user: {}", new_user.username);
165
166 Response::builder()
167 .status(StatusCode::CREATED)
168 .header("Content-Type", "application/json")
169 .body(Body::from(
170 serde_json::json!({
171 "username": new_user.username,
172 "permissions": new_user.permissions,
173 })
174 .to_string(),
175 ))
176 .unwrap()
177}
178
179/// Delete user (admin only)
180#[utoipa::path(
181 delete,
182 path = "/admin/users/{username}",
183 params(
184 ("username" = String, Path, description = "Username of the user to delete")
185 ),
186 responses(
187 (status = 204, description = "User deleted successfully"),
188 (status = 400, description = "Bad request - cannot delete yourself"),
189 (status = 401, description = "Unauthorized - authentication required"),
190 (status = 403, description = "Forbidden - admin permission required"),
191 (status = 404, description = "Not found - user does not exist"),
192 (status = 500, description = "Internal server error - failed to save users")
193 ),
194 security(
195 ("basic_auth" = [])
196 )
197)]
198pub async fn delete_user(
199 State(state): State<Arc<state::App>>,
200 Path(username): Path<String>,
201 headers: HeaderMap,
202) -> Response {
203 let host = &state.args.host;
204
205 // Authenticate
206 let user = match auth::authenticate_user(&state, &headers).await {
207 Ok(u) => u,
208 Err(_) => return response::unauthorized(host),
209 };
210
211 // Check admin permission
212 if !is_admin(&user) {
213 return response::forbidden();
214 }
215
216 // Prevent deleting yourself
217 if user.username == username {
218 return Response::builder()
219 .status(StatusCode::BAD_REQUEST)
220 .body(Body::from("Cannot delete yourself"))
221 .unwrap();
222 }
223
224 // Remove user
225 {
226 let mut users = state.users.lock().await;
227 let before_len = users.len();
228 users.retain(|u| u.username != username);
229
230 if users.len() == before_len {
231 return response::not_found();
232 }
233 }
234
235 // Persist to file
236 if let Err(e) = save_users(&state).await {
237 log::error!("Failed to save users: {}", e);
238 return response::internal_error();
239 }
240
241 log::info!("Deleted user: {}", username);
242
243 Response::builder()
244 .status(StatusCode::OK)
245 .body(Body::empty())
246 .unwrap()
247}
248
249/// Add permission to user (admin only)
250#[utoipa::path(
251 post,
252 path = "/admin/users/{username}/permissions",
253 params(
254 ("username" = String, Path, description = "Username of the user to add permission to")
255 ),
256 request_body = AddPermissionRequest,
257 responses(
258 (status = 200, description = "Permission added successfully", content_type = "application/json"),
259 (status = 400, description = "Bad request - invalid JSON"),
260 (status = 401, description = "Unauthorized - authentication required"),
261 (status = 403, description = "Forbidden - admin permission required"),
262 (status = 404, description = "Not found - user does not exist"),
263 (status = 500, description = "Internal server error - failed to save users")
264 ),
265 security(
266 ("basic_auth" = [])
267 )
268)]
269pub async fn add_permission(
270 State(state): State<Arc<state::App>>,
271 Path(username): Path<String>,
272 headers: HeaderMap,
273 body: Bytes,
274) -> Response {
275 let host = &state.args.host;
276
277 // Authenticate
278 let user = match auth::authenticate_user(&state, &headers).await {
279 Ok(u) => u,
280 Err(_) => return response::unauthorized(host),
281 };
282
283 // Check admin permission
284 if !is_admin(&user) {
285 return response::forbidden();
286 }
287
288 // Parse request
289 let req: AddPermissionRequest = match serde_json::from_slice(&body) {
290 Ok(r) => r,
291 Err(e) => {
292 return Response::builder()
293 .status(StatusCode::BAD_REQUEST)
294 .body(Body::from(format!("Invalid request: {}", e)))
295 .unwrap();
296 }
297 };
298
299 let new_permission = state::Permission {
300 repository: req.repository,
301 tag: req.tag,
302 actions: req.actions,
303 };
304
305 // Add permission to user
306 {
307 let mut users = state.users.lock().await;
308 let mut user_found = false;
309
310 // Create new set with updated user
311 let updated_users: std::collections::HashSet<_> = users
312 .iter()
313 .map(|u| {
314 if u.username == username {
315 user_found = true;
316 let mut updated = u.clone();
317 updated.permissions.push(new_permission.clone());
318 updated
319 } else {
320 u.clone()
321 }
322 })
323 .collect();
324
325 if !user_found {
326 return response::not_found();
327 }
328
329 *users = updated_users;
330 }
331
332 // Persist to file
333 if let Err(e) = save_users(&state).await {
334 log::error!("Failed to save users: {}", e);
335 return response::internal_error();
336 }
337
338 log::info!(
339 "Added permission for user {}: {:?}",
340 username,
341 new_permission
342 );
343
344 Response::builder()
345 .status(StatusCode::OK)
346 .header("Content-Type", "application/json")
347 .body(Body::from(serde_json::to_string(&new_permission).unwrap()))
348 .unwrap()
349}
350
351/// Add permission to user via body (admin only) - alternative endpoint with username in body
352#[utoipa::path(
353 post,
354 path = "/admin/permissions",
355 request_body = AddPermissionWithUsernameRequest,
356 responses(
357 (status = 201, description = "Permission added successfully", content_type = "application/json"),
358 (status = 400, description = "Bad request - invalid JSON"),
359 (status = 401, description = "Unauthorized - authentication required"),
360 (status = 403, description = "Forbidden - admin permission required"),
361 (status = 404, description = "Not found - user does not exist"),
362 (status = 500, description = "Internal server error - failed to save users")
363 ),
364 security(
365 ("basic_auth" = [])
366 )
367)]
368pub async fn add_permission_with_username(
369 State(state): State<Arc<state::App>>,
370 headers: HeaderMap,
371 body: Bytes,
372) -> Response {
373 let host = &state.args.host;
374
375 // Authenticate
376 let user = match auth::authenticate_user(&state, &headers).await {
377 Ok(u) => u,
378 Err(_) => return response::unauthorized(host),
379 };
380
381 // Check admin permission
382 if !is_admin(&user) {
383 return response::forbidden();
384 }
385
386 // Parse request
387 let req: AddPermissionWithUsernameRequest = match serde_json::from_slice(&body) {
388 Ok(r) => r,
389 Err(e) => {
390 return Response::builder()
391 .status(StatusCode::BAD_REQUEST)
392 .body(Body::from(format!("Invalid request: {}", e)))
393 .unwrap();
394 }
395 };
396
397 let new_permission = state::Permission {
398 repository: req.repository,
399 tag: req.tag,
400 actions: req.actions,
401 };
402
403 // Add permission to user
404 {
405 let mut users = state.users.lock().await;
406 let mut user_found = false;
407
408 // Create new set with updated user
409 let updated_users: std::collections::HashSet<_> = users
410 .iter()
411 .map(|u| {
412 if u.username == req.username {
413 user_found = true;
414 let mut updated = u.clone();
415 updated.permissions.push(new_permission.clone());
416 updated
417 } else {
418 u.clone()
419 }
420 })
421 .collect();
422
423 if !user_found {
424 return response::not_found();
425 }
426
427 *users = updated_users;
428 }
429
430 // Persist to file
431 if let Err(e) = save_users(&state).await {
432 log::error!("Failed to save users: {}", e);
433 return response::internal_error();
434 }
435
436 log::info!(
437 "Added permission for user {}: {:?}",
438 req.username,
439 new_permission
440 );
441
442 Response::builder()
443 .status(StatusCode::OK)
444 .header("Content-Type", "application/json")
445 .body(Body::from(serde_json::to_string(&new_permission).unwrap()))
446 .unwrap()
447}
448
449/// Save users to file
450async fn save_users(state: &Arc<state::App>) -> Result<(), Box<dyn std::error::Error>> {
451 let users = state.users.lock().await;
452
453 let users_file = state::UsersFile {
454 users: users.iter().cloned().collect(),
455 };
456
457 let json = serde_json::to_string_pretty(&users_file)?;
458 std::fs::write(&state.args.users_file, json)?;
459
460 Ok(())
461}
462
463#[derive(Debug, Deserialize, ToSchema)]
464pub struct GcQuery {
465 #[serde(default)]
466 pub dry_run: bool,
467 #[serde(default = "default_grace_period")]
468 pub grace_period_hours: u64,
469}
470
471fn default_grace_period() -> u64 {
472 24
473}
474
475/// Run garbage collection (admin only)
476#[utoipa::path(
477 post,
478 path = "/admin/gc",
479 params(
480 ("dry_run" = Option<bool>, Query, description = "Run in dry-run mode without deleting blobs"),
481 ("grace_period_hours" = Option<u64>, Query, description = "Grace period in hours before deleting unreferenced blobs (default: 24)")
482 ),
483 responses(
484 (status = 200, description = "Garbage collection statistics", content_type = "application/json"),
485 (status = 401, description = "Unauthorized - authentication required"),
486 (status = 403, description = "Forbidden - admin permission required"),
487 (status = 500, description = "Internal server error")
488 ),
489 security(
490 ("basic_auth" = [])
491 )
492)]
493pub async fn run_garbage_collection(
494 State(state): State<Arc<state::App>>,
495 headers: HeaderMap,
496 Query(params): Query<GcQuery>,
497) -> Response {
498 let host = &state.args.host;
499
500 // Authenticate
501 let user = match auth::authenticate_user(&state, &headers).await {
502 Ok(u) => u,
503 Err(_) => return response::unauthorized(host),
504 };
505
506 // Check admin permission
507 if !is_admin(&user) {
508 return response::forbidden();
509 }
510
511 let dry_run = params.dry_run;
512 let grace_period = params.grace_period_hours;
513
514 log::info!(
515 "Admin {} initiated GC (dry_run: {}, grace_period: {}h)",
516 user.username,
517 dry_run,
518 grace_period
519 );
520
521 match gc::run_gc(dry_run, grace_period) {
522 Ok(stats) => Response::builder()
523 .status(StatusCode::OK)
524 .header("Content-Type", "application/json")
525 .body(Body::from(serde_json::to_string_pretty(&stats).unwrap()))
526 .unwrap(),
527 Err(e) => {
528 log::error!("GC failed: {}", e);
529 response::internal_error()
530 }
531 }
532}