feat(drive): repl user_id by caller has read access
repl user_id by caller has read access in readonly functions
using CALLER_CAN_READ_DRIVE constant
ensure webdav preview is using the permission handler
This commit is contained in:
@@ -195,7 +195,10 @@ pub trait FileReadPort: Send + Sync + 'static {
|
||||
/// # Arguments
|
||||
/// * `folder_id` - Optional folder ID to scope the search (for recursive search, pass None)
|
||||
/// * `criteria` - Search criteria including name_contains, file_types, date ranges, size ranges
|
||||
/// * `user_id` - User ID for ownership filtering
|
||||
/// * `caller_id` - Caller user id — scoped by drive-membership grants
|
||||
/// (`role_grants` on `resource_type='drive'`) rather than the legacy
|
||||
/// `files.user_id` column. Group memberships (direct + transitive)
|
||||
/// are expanded inline via `storage.caller_group_ids($caller)`.
|
||||
///
|
||||
/// # Returns
|
||||
/// A tuple of (files, total_count) where files are paginated and filtered
|
||||
@@ -203,36 +206,39 @@ pub trait FileReadPort: Send + Sync + 'static {
|
||||
&self,
|
||||
folder_id: Option<&str>,
|
||||
criteria: &SearchCriteriaDto,
|
||||
user_id: Uuid,
|
||||
caller_id: Uuid,
|
||||
) -> Result<(Vec<File>, usize), DomainError>;
|
||||
|
||||
/// Search files recursively in a folder subtree using ltree.
|
||||
///
|
||||
/// When `root_folder_id` is Some, uses ltree descendant queries to find
|
||||
/// all files within the subtree rooted at that folder. When None, searches
|
||||
/// all files for the user. This replaces the O(N) recursive spawn-per-folder
|
||||
/// approach with O(1) SQL queries.
|
||||
/// all files within the subtree rooted at that folder. When None,
|
||||
/// delegates to `search_files_paginated`.
|
||||
///
|
||||
/// Post-PR-B: scoped by drive-membership grants (same semantics as
|
||||
/// `search_files_paginated`), not by `files.user_id`.
|
||||
///
|
||||
/// Returns a tuple of (matching files, total count for pagination).
|
||||
async fn search_files_in_subtree(
|
||||
&self,
|
||||
root_folder_id: Option<&str>,
|
||||
criteria: &SearchCriteriaDto,
|
||||
user_id: Uuid,
|
||||
caller_id: Uuid,
|
||||
) -> Result<(Vec<File>, usize), DomainError> {
|
||||
// Default: delegate to paginated search (non-recursive fallback)
|
||||
self.search_files_paginated(root_folder_id, criteria, user_id)
|
||||
self.search_files_paginated(root_folder_id, criteria, caller_id)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Count files matching the search criteria (without loading them).
|
||||
///
|
||||
/// Used for pagination metadata without fetching the actual files.
|
||||
/// Same drive-membership scoping as `search_files_paginated`.
|
||||
async fn count_files(
|
||||
&self,
|
||||
folder_id: Option<&str>,
|
||||
criteria: &SearchCriteriaDto,
|
||||
user_id: Uuid,
|
||||
caller_id: Uuid,
|
||||
) -> Result<usize, DomainError>;
|
||||
|
||||
/// Return up to `limit` files whose name contains `query` (case-insensitive).
|
||||
|
||||
@@ -431,23 +431,23 @@ impl FolderUseCase for FolderService {
|
||||
return self.list_folders_paginated(parent_id, &pagination).await;
|
||||
} else {
|
||||
let (folders, total_items) = self
|
||||
.folder_storage
|
||||
.list_root_folders_for_caller_paginated(
|
||||
owner_id,
|
||||
pagination.offset(),
|
||||
pagination.limit(),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DomainError::internal_error(
|
||||
"FolderStorage",
|
||||
format!(
|
||||
"Failed to list root folders for caller '{}' with pagination: {}",
|
||||
owner_id, e
|
||||
),
|
||||
.folder_storage
|
||||
.list_root_folders_for_caller_paginated(
|
||||
owner_id,
|
||||
pagination.offset(),
|
||||
pagination.limit(),
|
||||
true,
|
||||
)
|
||||
})?;
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DomainError::internal_error(
|
||||
"FolderStorage",
|
||||
format!(
|
||||
"Failed to list root folders for caller '{}' with pagination: {}",
|
||||
owner_id, e
|
||||
),
|
||||
)
|
||||
})?;
|
||||
|
||||
let total = total_items.unwrap_or(folders.len());
|
||||
|
||||
|
||||
@@ -9,9 +9,10 @@ use crate::infrastructure::repositories::pg::FileBlobReadRepository;
|
||||
/// "Places" use case: the caller's geotagged photos aggregated into map
|
||||
/// clusters.
|
||||
///
|
||||
/// Post-§15 the surface follows the Photos scope: default personal drive
|
||||
/// + drives where `policies.include_in_photo_index = true` AND caller
|
||||
/// has Read. Group-membership expansion is handled inline by
|
||||
/// Post-§15 the surface follows the Photos scope: drives where the
|
||||
/// caller has Read AND `policies.include_in_photo_index = true`
|
||||
/// (default personal drives materialise the flag at creation).
|
||||
/// Group-membership expansion is handled inline by
|
||||
/// `storage.caller_group_ids(caller)` inside the repo's SQL, so this
|
||||
/// service is a thin coordinate-math wrapper — no engine dependency.
|
||||
pub struct PlacesService {
|
||||
|
||||
@@ -328,7 +328,11 @@ impl SearchService {
|
||||
}
|
||||
};
|
||||
match authz
|
||||
.check(Subject::User(user_id), Permission::Read, Resource::File(file_uuid))
|
||||
.check(
|
||||
Subject::User(user_id),
|
||||
Permission::Read,
|
||||
Resource::File(file_uuid),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(true) => verified.push(hit),
|
||||
|
||||
Reference in New Issue
Block a user