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::common::errors::ErrorKind;
|
||||||
use crate::application::ports::inbound::FolderUseCase;
|
use crate::application::ports::inbound::FolderUseCase;
|
||||||
use crate::common::di::AppState as GlobalAppState;
|
use crate::common::di::AppState as GlobalAppState;
|
||||||
use crate::interfaces::middleware::auth::OptionalAuthUser;
|
use crate::interfaces::middleware::auth::{OptionalAuthUser, AuthUser};
|
||||||
|
|
||||||
type AppState = Arc<FolderService>;
|
type AppState = Arc<FolderService>;
|
||||||
|
|
||||||
@@ -59,10 +59,12 @@ impl FolderHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Lists root folders (no parent ID)
|
/// Lists root folders (no parent ID)
|
||||||
|
/// Non-admin users only see their own home folder.
|
||||||
pub async fn list_root_folders(
|
pub async fn list_root_folders(
|
||||||
State(service): State<AppState>,
|
State(service): State<AppState>,
|
||||||
|
auth_user: AuthUser,
|
||||||
) -> impl IntoResponse {
|
) -> 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
|
/// Lists contents of a specific folder by its ID
|
||||||
@@ -76,9 +78,12 @@ impl FolderHandler {
|
|||||||
/// Lists root folders with pagination support
|
/// Lists root folders with pagination support
|
||||||
pub async fn list_root_folders_paginated(
|
pub async fn list_root_folders_paginated(
|
||||||
State(service): State<AppState>,
|
State(service): State<AppState>,
|
||||||
pagination: Query<PaginationRequestDto>,
|
auth_user: AuthUser,
|
||||||
|
_pagination: Query<PaginationRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> 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
|
/// Lists contents of a specific folder with pagination
|
||||||
@@ -90,16 +95,25 @@ impl FolderHandler {
|
|||||||
Self::list_folders_paginated(State(service), pagination, Some(&id)).await
|
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
|
/// Lists folders, optionally filtered by parent ID
|
||||||
pub async fn list_folders(
|
pub async fn list_folders(
|
||||||
State(service): State<AppState>,
|
State(service): State<AppState>,
|
||||||
parent_id: Option<&str>,
|
parent_id: Option<&str>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// Parent ID is already a &str
|
|
||||||
|
|
||||||
match service.list_folders(parent_id).await {
|
match service.list_folders(parent_id).await {
|
||||||
Ok(folders) => {
|
Ok(folders) => {
|
||||||
// Always return an array even if empty
|
|
||||||
(StatusCode::OK, Json(folders)).into_response()
|
(StatusCode::OK, Json(folders)).into_response()
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -108,7 +122,46 @@ impl FolderHandler {
|
|||||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
_ => 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!({
|
(status, Json(serde_json::json!({
|
||||||
"error": err.to_string()
|
"error": err.to_string()
|
||||||
}))).into_response()
|
}))).into_response()
|
||||||
|
|||||||
+23
-30
@@ -754,8 +754,10 @@ async function loadFiles(options = {}) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip other users' folders when at root
|
// Skip other users' folders when at root (both naming conventions)
|
||||||
if (!app.currentPath && folder.name.startsWith('My Folder - ') && !folder.name.includes(username)) {
|
if (!app.currentPath &&
|
||||||
|
(folder.name.startsWith('My Folder - ') || folder.name.startsWith('Mi Carpeta - ')) &&
|
||||||
|
!folder.name.includes(username)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1835,26 +1837,30 @@ async function findUserHomeFolder(username) {
|
|||||||
console.log(`Found ${folderList.length} folders at root`);
|
console.log(`Found ${folderList.length} folders at root`);
|
||||||
|
|
||||||
// Look for a folder with a name pattern that matches the user's home folder
|
// Look for a folder with a name pattern that matches the user's home folder
|
||||||
// Only exact match "Mi Carpeta - username"
|
// Match both naming conventions (English and Spanish)
|
||||||
const homeFolderPattern = `Mi Carpeta - ${username}`;
|
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 => {
|
const visibleFolders = folderList.filter(folder => {
|
||||||
// Skip system folders (starting with dot)
|
// Skip system folders (starting with dot)
|
||||||
if (folder.name.startsWith('.')) {
|
if (folder.name.startsWith('.')) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip other users' folders
|
// Skip other users' home folders (both naming conventions)
|
||||||
if (folder.name.startsWith('Mi Carpeta - ') && !folder.name.includes(username)) {
|
if ((folder.name.startsWith('Mi Carpeta - ') || folder.name.startsWith('My Folder - '))
|
||||||
|
&& !folder.name.includes(username)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Find the user's home folder from filtered list
|
// Find the user's home folder from filtered list (try both patterns)
|
||||||
let homeFolder = visibleFolders.find(folder => folder.name === homeFolderPattern);
|
let homeFolder = visibleFolders.find(folder =>
|
||||||
|
folder.name === homeFolderPatternEn || folder.name === homeFolderPatternEs
|
||||||
|
);
|
||||||
|
|
||||||
if (homeFolder) {
|
if (homeFolder) {
|
||||||
console.log(`Found user's home folder: ${homeFolder.name} (${homeFolder.id})`);
|
console.log(`Found user's home folder: ${homeFolder.name} (${homeFolder.id})`);
|
||||||
@@ -1870,28 +1876,15 @@ async function findUserHomeFolder(username) {
|
|||||||
loadFiles();
|
loadFiles();
|
||||||
return; // Success! Exit function
|
return; // Success! Exit function
|
||||||
} else {
|
} 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,
|
// SECURITY: Never fall back to another user's folder.
|
||||||
// use the first folder as the user's home
|
// If user's own folder doesn't exist, show root (empty state).
|
||||||
if (folderList.length > 0) {
|
console.log('User home folder not found, showing root');
|
||||||
const fallbackFolder = folderList[0];
|
app.currentPath = '';
|
||||||
console.log(`Using first folder as fallback: ${fallbackFolder.name} (${fallbackFolder.id})`);
|
ui.updateBreadcrumb('');
|
||||||
|
loadFiles();
|
||||||
app.userHomeFolderId = fallbackFolder.id;
|
return;
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we get here, we've successfully processed the response
|
// If we get here, we've successfully processed the response
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
// OxiCloud Service Worker
|
// OxiCloud Service Worker
|
||||||
const CACHE_NAME = 'oxicloud-cache-v4';
|
const CACHE_NAME = 'oxicloud-cache-v5';
|
||||||
const ASSETS_TO_CACHE = [
|
const ASSETS_TO_CACHE = [
|
||||||
'/',
|
'/',
|
||||||
'/index.html',
|
'/index.html',
|
||||||
|
|||||||
Reference in New Issue
Block a user