diff --git a/src/admin.rs b/src/admin.rs index 37ec459..f531964 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -26,6 +26,14 @@ pub struct AddPermissionRequest { pub actions: Vec, } +#[derive(Debug, Deserialize, Serialize, ToSchema)] +pub struct AddPermissionWithUsernameRequest { + pub username: String, + pub repository: String, + pub tag: String, + pub actions: Vec, +} + /// Check if user is admin (has wildcard delete permission) fn is_admin(user: &state::User) -> bool { permissions::has_permission(user, "*", Some("*"), permissions::Action::Delete) @@ -74,7 +82,10 @@ pub async fn list_users(State(state): State>, headers: HeaderMap .status(StatusCode::OK) .header("Content-Type", "application/json") .body(Body::from( - serde_json::to_string_pretty(&user_list).unwrap(), + serde_json::json!({ + "users": user_list + }) + .to_string(), )) .unwrap() } @@ -229,7 +240,10 @@ pub async fn delete_user( log::info!("Deleted user: {}", username); - response::no_content() + Response::builder() + .status(StatusCode::OK) + .body(Body::empty()) + .unwrap() } /// Add permission to user (admin only) @@ -334,6 +348,104 @@ pub async fn add_permission( .unwrap() } +/// Add permission to user via body (admin only) - alternative endpoint with username in body +#[utoipa::path( + post, + path = "/admin/permissions", + request_body = AddPermissionWithUsernameRequest, + responses( + (status = 201, description = "Permission added successfully", content_type = "application/json"), + (status = 400, description = "Bad request - invalid JSON"), + (status = 401, description = "Unauthorized - authentication required"), + (status = 403, description = "Forbidden - admin permission required"), + (status = 404, description = "Not found - user does not exist"), + (status = 500, description = "Internal server error - failed to save users") + ), + security( + ("basic_auth" = []) + ) +)] +pub async fn add_permission_with_username( + State(state): State>, + headers: HeaderMap, + body: Bytes, +) -> Response { + let host = &state.args.host; + + // Authenticate + let user = match auth::authenticate_user(&state, &headers).await { + Ok(u) => u, + Err(_) => return response::unauthorized(host), + }; + + // Check admin permission + if !is_admin(&user) { + return response::forbidden(); + } + + // Parse request + let req: AddPermissionWithUsernameRequest = match serde_json::from_slice(&body) { + Ok(r) => r, + Err(e) => { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(format!("Invalid request: {}", e))) + .unwrap(); + } + }; + + let new_permission = state::Permission { + repository: req.repository, + tag: req.tag, + actions: req.actions, + }; + + // Add permission to user + { + let mut users = state.users.lock().await; + let mut user_found = false; + + // Create new set with updated user + let updated_users: std::collections::HashSet<_> = users + .iter() + .map(|u| { + if u.username == req.username { + user_found = true; + let mut updated = u.clone(); + updated.permissions.push(new_permission.clone()); + updated + } else { + u.clone() + } + }) + .collect(); + + if !user_found { + return response::not_found(); + } + + *users = updated_users; + } + + // Persist to file + if let Err(e) = save_users(&state).await { + log::error!("Failed to save users: {}", e); + return response::internal_error(); + } + + log::info!( + "Added permission for user {}: {:?}", + req.username, + new_permission + ); + + Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "application/json") + .body(Body::from(serde_json::to_string(&new_permission).unwrap())) + .unwrap() +} + /// Save users to file async fn save_users(state: &Arc) -> Result<(), Box> { let users = state.users.lock().await; diff --git a/src/main.rs b/src/main.rs index a6baea7..4bc481d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -96,6 +96,10 @@ async fn main() { "/admin/users/{username}/permissions", post(admin::add_permission), ) + .route( + "/admin/permissions", + post(admin::add_permission_with_username), + ) .route("/admin/gc", post(admin::run_garbage_collection)) // Catch-all routes for debugging .route("/{*path}", head(meta::catch_all_head)) diff --git a/src/response.rs b/src/response.rs index f6b7cdc..e86174f 100644 --- a/src/response.rs +++ b/src/response.rs @@ -86,13 +86,6 @@ pub(crate) fn internal_error() -> Response { .unwrap() } -pub(crate) fn no_content() -> Response { - Response::builder() - .status(StatusCode::NO_CONTENT) - .body(Body::empty()) - .unwrap() -} - pub(crate) fn conflict(message: &str) -> Response { Response::builder() .status(StatusCode::CONFLICT)