Rust implementation of OCI Distribution Spec with granular access control
at main 532 lines 15 kB view raw
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}