diff --git a/.gitignore b/.gitignore index 12bf685..b38890d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ frontend/dist/ readur_uploads/ readur_watch/ test-results/ +uploads/ diff --git a/src/routes/metrics.rs b/src/routes/metrics.rs index a0cd291..fa1916d 100644 --- a/src/routes/metrics.rs +++ b/src/routes/metrics.rs @@ -9,7 +9,15 @@ use std::sync::Arc; use serde::Serialize; use utoipa::ToSchema; -use crate::{auth::AuthUser, AppState}; +use crate::{auth::AuthUser, AppState, models::UserRole}; + +fn require_admin(auth_user: &AuthUser) -> Result<(), StatusCode> { + if auth_user.user.role != UserRole::Admin { + Err(StatusCode::FORBIDDEN) + } else { + Ok(()) + } +} #[derive(Serialize, ToSchema)] pub struct SystemMetrics { @@ -83,8 +91,9 @@ pub fn router() -> Router> { )] pub async fn get_system_metrics( State(state): State>, - _auth_user: AuthUser, // Require authentication + auth_user: AuthUser, ) -> Result, StatusCode> { + require_admin(&auth_user)?; let timestamp = chrono::Utc::now().timestamp(); // Collect all metrics concurrently for better performance diff --git a/src/routes/queue.rs b/src/routes/queue.rs index dc248ac..3298cba 100644 --- a/src/routes/queue.rs +++ b/src/routes/queue.rs @@ -41,8 +41,9 @@ pub fn router() -> Router> { )] async fn get_queue_stats( State(state): State>, - _auth_user: AuthUser, // Require authentication + auth_user: AuthUser, ) -> Result, StatusCode> { + require_admin(&auth_user)?; let queue_service = OcrQueueService::new(state.db.clone(), state.db.get_pool().clone(), 1); let stats = queue_service @@ -74,8 +75,9 @@ async fn get_queue_stats( )] async fn requeue_failed( State(state): State>, - _auth_user: AuthUser, // Require authentication + auth_user: AuthUser, ) -> Result, StatusCode> { + require_admin(&auth_user)?; let queue_service = OcrQueueService::new(state.db.clone(), state.db.get_pool().clone(), 1); let count = queue_service diff --git a/src/routes/users.rs b/src/routes/users.rs index 8a456cc..9deb20a 100644 --- a/src/routes/users.rs +++ b/src/routes/users.rs @@ -10,10 +10,18 @@ use uuid::Uuid; use crate::{ auth::AuthUser, - models::{CreateUser, UpdateUser, UserResponse}, + models::{CreateUser, UpdateUser, UserResponse, UserRole}, AppState, }; +fn require_admin(auth_user: &AuthUser) -> Result<(), StatusCode> { + if auth_user.user.role != UserRole::Admin { + Err(StatusCode::FORBIDDEN) + } else { + Ok(()) + } +} + pub fn router() -> Router> { Router::new() .route("/", get(list_users).post(create_user)) @@ -33,9 +41,10 @@ pub fn router() -> Router> { ) )] async fn list_users( - _auth_user: AuthUser, + auth_user: AuthUser, State(state): State>, ) -> Result>, StatusCode> { + require_admin(&auth_user)?; let users = state .db .get_all_users() @@ -63,10 +72,11 @@ async fn list_users( ) )] async fn get_user( - _auth_user: AuthUser, + auth_user: AuthUser, State(state): State>, Path(id): Path, ) -> Result, StatusCode> { + require_admin(&auth_user)?; let user = state .db .get_user_by_id(id) @@ -92,10 +102,11 @@ async fn get_user( ) )] async fn create_user( - _auth_user: AuthUser, + auth_user: AuthUser, State(state): State>, Json(user_data): Json, ) -> Result, StatusCode> { + require_admin(&auth_user)?; let user = state .db .create_user(user_data) @@ -123,11 +134,12 @@ async fn create_user( ) )] async fn update_user( - _auth_user: AuthUser, + auth_user: AuthUser, State(state): State>, Path(id): Path, Json(update_data): Json, ) -> Result, StatusCode> { + require_admin(&auth_user)?; let user = state .db .update_user(id, update_data.username, update_data.email, update_data.password) @@ -158,6 +170,8 @@ async fn delete_user( State(state): State>, Path(id): Path, ) -> Result { + require_admin(&auth_user)?; + // Prevent users from deleting themselves if auth_user.user.id == id { return Err(StatusCode::FORBIDDEN); diff --git a/tests/role_based_access_control_tests.rs b/tests/role_based_access_control_tests.rs index 133312f..942461f 100644 --- a/tests/role_based_access_control_tests.rs +++ b/tests/role_based_access_control_tests.rs @@ -48,28 +48,30 @@ impl RBACTestClient { /// Setup all test users (admin, user1, user2) async fn setup_all_users(&mut self) -> Result<(), Box> { - let timestamp = std::time::SystemTime::now() + // Use UUID for guaranteed uniqueness across concurrent test execution + let test_id = Uuid::new_v4().simple().to_string(); + let nanos = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() - .as_millis(); + .as_nanos(); // Setup admin user - let admin_username = format!("rbac_admin_{}", timestamp); - let admin_email = format!("rbac_admin_{}@example.com", timestamp); + let admin_username = format!("rbac_admin_{}_{}", test_id, nanos); + let admin_email = format!("rbac_admin_{}@{}.example.com", test_id, nanos); let (admin_token, admin_id) = self.register_and_login_user(&admin_username, &admin_email, UserRole::Admin).await?; self.admin_token = Some(admin_token); self.admin_user_id = admin_id; // Setup first regular user - let user1_username = format!("rbac_user1_{}", timestamp); - let user1_email = format!("rbac_user1_{}@example.com", timestamp); + let user1_username = format!("rbac_user1_{}_{}", test_id, nanos); + let user1_email = format!("rbac_user1_{}@{}.example.com", test_id, nanos); let (user1_token, user1_id) = self.register_and_login_user(&user1_username, &user1_email, UserRole::User).await?; self.user1_token = Some(user1_token); self.user1_user_id = user1_id; // Setup second regular user - let user2_username = format!("rbac_user2_{}", timestamp); - let user2_email = format!("rbac_user2_{}@example.com", timestamp); + let user2_username = format!("rbac_user2_{}_{}", test_id, nanos); + let user2_email = format!("rbac_user2_{}@{}.example.com", test_id, nanos); let (user2_token, user2_id) = self.register_and_login_user(&user2_username, &user2_email, UserRole::User).await?; self.user2_token = Some(user2_token); self.user2_user_id = user2_id; @@ -79,26 +81,57 @@ impl RBACTestClient { /// Helper to register and login a single user async fn register_and_login_user(&self, username: &str, email: &str, role: UserRole) -> Result<(String, Option), Box> { - let password = "rbacpassword123"; - - // Register user - let user_data = CreateUser { - username: username.to_string(), - email: email.to_string(), - password: password.to_string(), - role: Some(role), - }; - - let register_response = self.client - .post(&format!("{}/api/auth/register", get_base_url())) - .json(&user_data) - .send() - .await?; - - if !register_response.status().is_success() { - return Err(format!("Registration failed for {}: {}", username, register_response.text().await?).into()); + // Try up to 3 times with different unique identifiers if we get conflicts + for attempt in 0..3 { + let (actual_username, actual_email) = if attempt == 0 { + (username.to_string(), email.to_string()) + } else { + let retry_id = Uuid::new_v4().simple().to_string(); + (format!("{}_{}", username, retry_id), format!("retry_{}_{}", retry_id, email)) + }; + + let password = "rbacpassword123"; + + // Register user + let user_data = CreateUser { + username: actual_username.clone(), + email: actual_email.clone(), + password: password.to_string(), + role: Some(role), + }; + + let register_response = self.client + .post(&format!("{}/api/auth/register", get_base_url())) + .json(&user_data) + .send() + .await?; + + if register_response.status().is_success() { + // Registration successful, now login + return self.login_user(&actual_username, password).await; + } + + let error_text = register_response.text().await?; + + // If it's not a duplicate key error, fail immediately + if !error_text.contains("duplicate key") && !error_text.contains("already exists") { + return Err(format!("Registration failed for {}: {}", actual_username, error_text).into()); + } + + // If it's a duplicate key error and this was our last attempt, fail + if attempt == 2 { + return Err(format!("Registration failed after 3 attempts for {}: {}", username, error_text).into()); + } + + // Otherwise, try again with a different unique identifier } + Err("Unexpected error in register_and_login_user".into()) + } + + /// Helper to login a user and return token and user ID + async fn login_user(&self, username: &str, password: &str) -> Result<(String, Option), Box> { + // Login to get token let login_data = LoginRequest { username: username.to_string(), @@ -142,7 +175,16 @@ impl RBACTestClient { UserType::User2 => self.user2_token.as_ref(), }.ok_or("User not set up")?; - let part = reqwest::multipart::Part::text(content.to_string()) + // Generate unique content to prevent file hash collisions + let unique_id = Uuid::new_v4(); + let nanos = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos(); + let unique_content = format!("{}\n\nUnique ID: {}\nTimestamp: {}\nRandom: {}", + content, unique_id, nanos, Uuid::new_v4()); + + let part = reqwest::multipart::Part::text(unique_content) .file_name(filename.to_string()) .mime_str("text/plain")?; let form = reqwest::multipart::Form::new() @@ -181,7 +223,10 @@ impl RBACTestClient { return Err(format!("Get documents failed: {}", response.text().await?).into()); } - let documents: Vec = response.json().await?; + let response_json: Value = response.json().await?; + let documents: Vec = serde_json::from_value( + response_json["documents"].clone() + )?; Ok(documents) }