fix(security): scope root folder listing to authenticated user
Non-admin users were seeing all users' root folders, including the
admin's. Three root causes fixed:
1. Backend: list_root_folders now extracts AuthUser and filters
results so each user only sees their own home folder at the
root level (folders matching 'My Folder - {username}' or
'Mi Carpeta - {username}').
2. Frontend: findUserHomeFolder() searched only for the Spanish
pattern 'Mi Carpeta - {username}' but the backend creates
folders with the English pattern 'My Folder - {username}'.
Now checks both naming conventions.
3. Frontend: when the home folder was not found, the code fell
back to folderList[0] — which was usually the admin's folder.
Removed that dangerous fallback; now shows empty root instead.
Fixes #94
This commit is contained in:
@@ -13,7 +13,7 @@ use crate::application::dtos::pagination::PaginationRequestDto;
|
||||
use crate::common::errors::ErrorKind;
|
||||
use crate::application::ports::inbound::FolderUseCase;
|
||||
use crate::common::di::AppState as GlobalAppState;
|
||||
use crate::interfaces::middleware::auth::OptionalAuthUser;
|
||||
use crate::interfaces::middleware::auth::{OptionalAuthUser, AuthUser};
|
||||
|
||||
type AppState = Arc<FolderService>;
|
||||
|
||||
@@ -59,10 +59,12 @@ impl FolderHandler {
|
||||
}
|
||||
|
||||
/// Lists root folders (no parent ID)
|
||||
/// Non-admin users only see their own home folder.
|
||||
pub async fn list_root_folders(
|
||||
State(service): State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
) -> impl IntoResponse {
|
||||
Self::list_folders(State(service), None).await
|
||||
Self::list_folders_for_user(State(service), None, &auth_user).await
|
||||
}
|
||||
|
||||
/// Lists contents of a specific folder by its ID
|
||||
@@ -76,9 +78,12 @@ impl FolderHandler {
|
||||
/// Lists root folders with pagination support
|
||||
pub async fn list_root_folders_paginated(
|
||||
State(service): State<AppState>,
|
||||
pagination: Query<PaginationRequestDto>,
|
||||
auth_user: AuthUser,
|
||||
_pagination: Query<PaginationRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
Self::list_folders_paginated(State(service), pagination, None).await
|
||||
// For paginated root listing, filter by user as well
|
||||
// Delegate to non-paginated user-filtered listing for now
|
||||
Self::list_folders_for_user(State(service), None, &auth_user).await
|
||||
}
|
||||
|
||||
/// Lists contents of a specific folder with pagination
|
||||
@@ -90,16 +95,25 @@ impl FolderHandler {
|
||||
Self::list_folders_paginated(State(service), pagination, Some(&id)).await
|
||||
}
|
||||
|
||||
/// Checks if a folder name matches the user home-folder convention.
|
||||
fn is_user_home_folder(folder_name: &str) -> bool {
|
||||
folder_name.starts_with("My Folder - ") || folder_name.starts_with("Mi Carpeta - ")
|
||||
}
|
||||
|
||||
/// Checks if a folder belongs to the given user.
|
||||
fn folder_belongs_to_user(folder_name: &str, username: &str) -> bool {
|
||||
let expected_en = format!("My Folder - {}", username);
|
||||
let expected_es = format!("Mi Carpeta - {}", username);
|
||||
folder_name == expected_en || folder_name == expected_es
|
||||
}
|
||||
|
||||
/// Lists folders, optionally filtered by parent ID
|
||||
pub async fn list_folders(
|
||||
State(service): State<AppState>,
|
||||
parent_id: Option<&str>,
|
||||
) -> impl IntoResponse {
|
||||
// Parent ID is already a &str
|
||||
|
||||
match service.list_folders(parent_id).await {
|
||||
Ok(folders) => {
|
||||
// Always return an array even if empty
|
||||
(StatusCode::OK, Json(folders)).into_response()
|
||||
},
|
||||
Err(err) => {
|
||||
@@ -108,7 +122,46 @@ impl FolderHandler {
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
// Return a JSON error response
|
||||
(status, Json(serde_json::json!({
|
||||
"error": err.to_string()
|
||||
}))).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Lists folders with user-based filtering for root listings.
|
||||
/// Non-admin users only see their own home folder at the root level.
|
||||
pub async fn list_folders_for_user(
|
||||
State(service): State<AppState>,
|
||||
parent_id: Option<&str>,
|
||||
auth_user: &AuthUser,
|
||||
) -> impl IntoResponse {
|
||||
match service.list_folders(parent_id).await {
|
||||
Ok(folders) => {
|
||||
// Only filter at root level (parent_id == None)
|
||||
let filtered = if parent_id.is_none() {
|
||||
folders.into_iter().filter(|f| {
|
||||
// Skip hidden/system folders
|
||||
if f.name.starts_with('.') {
|
||||
return false;
|
||||
}
|
||||
// If it's a user home folder, only show if it belongs to this user
|
||||
if Self::is_user_home_folder(&f.name) {
|
||||
return Self::folder_belongs_to_user(&f.name, &auth_user.username);
|
||||
}
|
||||
// Non-home folders are visible to everyone
|
||||
true
|
||||
}).collect()
|
||||
} else {
|
||||
folders
|
||||
};
|
||||
(StatusCode::OK, Json(filtered)).into_response()
|
||||
},
|
||||
Err(err) => {
|
||||
let status = match err.kind {
|
||||
ErrorKind::NotFound => StatusCode::NOT_FOUND,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
(status, Json(serde_json::json!({
|
||||
"error": err.to_string()
|
||||
}))).into_response()
|
||||
|
||||
+23
-30
@@ -754,8 +754,10 @@ async function loadFiles(options = {}) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip other users' folders when at root
|
||||
if (!app.currentPath && folder.name.startsWith('My Folder - ') && !folder.name.includes(username)) {
|
||||
// Skip other users' folders when at root (both naming conventions)
|
||||
if (!app.currentPath &&
|
||||
(folder.name.startsWith('My Folder - ') || folder.name.startsWith('Mi Carpeta - ')) &&
|
||||
!folder.name.includes(username)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1835,26 +1837,30 @@ async function findUserHomeFolder(username) {
|
||||
console.log(`Found ${folderList.length} folders at root`);
|
||||
|
||||
// Look for a folder with a name pattern that matches the user's home folder
|
||||
// Only exact match "Mi Carpeta - username"
|
||||
const homeFolderPattern = `Mi Carpeta - ${username}`;
|
||||
// Match both naming conventions (English and Spanish)
|
||||
const homeFolderPatternEn = `My Folder - ${username}`;
|
||||
const homeFolderPatternEs = `Mi Carpeta - ${username}`;
|
||||
|
||||
// Filter first to remove system folders like .trash that shouldn't be visible
|
||||
// Filter first to remove system folders and other users' folders
|
||||
const visibleFolders = folderList.filter(folder => {
|
||||
// Skip system folders (starting with dot)
|
||||
if (folder.name.startsWith('.')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip other users' folders
|
||||
if (folder.name.startsWith('Mi Carpeta - ') && !folder.name.includes(username)) {
|
||||
// Skip other users' home folders (both naming conventions)
|
||||
if ((folder.name.startsWith('Mi Carpeta - ') || folder.name.startsWith('My Folder - '))
|
||||
&& !folder.name.includes(username)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Find the user's home folder from filtered list
|
||||
let homeFolder = visibleFolders.find(folder => folder.name === homeFolderPattern);
|
||||
// Find the user's home folder from filtered list (try both patterns)
|
||||
let homeFolder = visibleFolders.find(folder =>
|
||||
folder.name === homeFolderPatternEn || folder.name === homeFolderPatternEs
|
||||
);
|
||||
|
||||
if (homeFolder) {
|
||||
console.log(`Found user's home folder: ${homeFolder.name} (${homeFolder.id})`);
|
||||
@@ -1870,28 +1876,15 @@ async function findUserHomeFolder(username) {
|
||||
loadFiles();
|
||||
return; // Success! Exit function
|
||||
} else {
|
||||
console.warn("Could not find user's home folder, fallback to first folder or root");
|
||||
console.warn("Could not find user's home folder");
|
||||
|
||||
// If we can't find a specific home folder but there are folders,
|
||||
// use the first folder as the user's home
|
||||
if (folderList.length > 0) {
|
||||
const fallbackFolder = folderList[0];
|
||||
console.log(`Using first folder as fallback: ${fallbackFolder.name} (${fallbackFolder.id})`);
|
||||
|
||||
app.userHomeFolderId = fallbackFolder.id;
|
||||
app.userHomeFolderName = fallbackFolder.name;
|
||||
app.currentPath = fallbackFolder.id;
|
||||
ui.updateBreadcrumb(fallbackFolder.name);
|
||||
loadFiles();
|
||||
return; // Success with fallback! Exit function
|
||||
} else {
|
||||
// No folders at all - this is an edge case
|
||||
console.warn("No folders found, using root");
|
||||
app.currentPath = '';
|
||||
ui.updateBreadcrumb('');
|
||||
loadFiles();
|
||||
return; // Success with root! Exit function
|
||||
}
|
||||
// SECURITY: Never fall back to another user's folder.
|
||||
// If user's own folder doesn't exist, show root (empty state).
|
||||
console.log('User home folder not found, showing root');
|
||||
app.currentPath = '';
|
||||
ui.updateBreadcrumb('');
|
||||
loadFiles();
|
||||
return;
|
||||
}
|
||||
|
||||
// If we get here, we've successfully processed the response
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
// OxiCloud Service Worker
|
||||
const CACHE_NAME = 'oxicloud-cache-v4';
|
||||
const CACHE_NAME = 'oxicloud-cache-v5';
|
||||
const ASSETS_TO_CACHE = [
|
||||
'/',
|
||||
'/index.html',
|
||||
|
||||
Reference in New Issue
Block a user