feat(drive): prepare removal of user_id

this commit changes GET /api/<resources> to return resource caller has access to
    this is not anymmore resources users is owner of
This commit is contained in:
Edouard Vanbelle
2026-07-02 00:01:15 +02:00
parent 09339ea63f
commit 858139ef3b
18 changed files with 298 additions and 281 deletions
+9 -10
View File
@@ -358,18 +358,18 @@ impl FolderUseCase for FolderService {
.await?;
return self.list_folders(parent_id).await;
}
// No parent → list the user's root folders.
// No parent → list the caller's readable root folders. The
// predicate scopes by drive-membership grants (post-PR-B),
// closing the pre-D7 gap where the legacy `user_id` filter
// surfaced admin-created folders that admin had no role on.
let folders = self
.folder_storage
.list_folders_by_owner(parent_id, caller_id)
.list_root_folders_for_caller(caller_id)
.await
.map_err(|e| {
DomainError::internal_error(
"FolderStorage",
format!(
"Failed to list folders for owner '{}' in parent {:?}: {}",
caller_id, parent_id, e
),
format!("Failed to list root folders for caller '{caller_id}': {e}"),
)
})?;
Ok(folders.into_iter().map(FolderDto::from).collect())
@@ -432,8 +432,7 @@ impl FolderUseCase for FolderService {
} else {
let (folders, total_items) = self
.folder_storage
.list_folders_by_owner_paginated(
parent_id,
.list_root_folders_for_caller_paginated(
owner_id,
pagination.offset(),
pagination.limit(),
@@ -444,8 +443,8 @@ impl FolderUseCase for FolderService {
DomainError::internal_error(
"FolderStorage",
format!(
"Failed to list folders for owner '{}' with pagination in parent {:?}: {}",
owner_id, parent_id, e
"Failed to list root folders for caller '{}' with pagination: {}",
owner_id, e
),
)
})?;
+6 -16
View File
@@ -4,29 +4,23 @@ use uuid::Uuid;
use crate::application::dtos::geo_dto::{GeoBounds, GeoCluster};
use crate::common::errors::DomainError;
use crate::domain::services::authorization::Subject;
use crate::infrastructure::repositories::pg::FileBlobReadRepository;
use crate::infrastructure::services::pg_acl_engine::PgAclEngine;
/// "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. The repository query joins `role_grants` on the drive
/// resource type; group-mediated grants are honoured via the caller
/// expansion done here.
/// has Read. 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 {
file_read: Arc<FileBlobReadRepository>,
authorization: Arc<PgAclEngine>,
}
impl PlacesService {
pub fn new(file_read: Arc<FileBlobReadRepository>, authorization: Arc<PgAclEngine>) -> Self {
Self {
file_read,
authorization,
}
pub fn new(file_read: Arc<FileBlobReadRepository>) -> Self {
Self { file_read }
}
/// Aggregation cell side, in degrees, for a slippy-map zoom level. The
@@ -46,12 +40,8 @@ impl PlacesService {
zoom: u8,
) -> Result<Vec<GeoCluster>, DomainError> {
let cell = Self::cell_for_zoom(zoom);
let (subject_types, subject_ids) = self
.authorization
.expand_subject_for_listing(Subject::User(caller_id))
.await?;
self.file_read
.list_geo_clusters(&subject_types, &subject_ids, bounds, cell)
.list_geo_clusters(caller_id, bounds, cell)
.await
}
}
+5 -15
View File
@@ -283,20 +283,10 @@ impl SearchService {
return Vec::new();
};
// Resolve the caller's accessible drive set via the engine
// (handles group-mediated drive grants) + the repo lookup.
let caller = Subject::User(user_id);
let (subject_types, subject_ids) = match authz.expand_subject_for_listing(caller).await {
Ok(pair) => pair,
Err(e) => {
tracing::warn!("Content-index: subject expansion failed — degrading to empty: {e}");
return Vec::new();
}
};
let accessible_drives: Vec<Uuid> = match drive_repo
.list_for_subjects(&subject_types, &subject_ids)
.await
{
// Resolve the caller's accessible drive set. Group-mediated
// grants are honoured inline by `storage.caller_group_ids` on
// the SQL side, so no Rust-side subject expansion here.
let accessible_drives: Vec<Uuid> = match drive_repo.list_readable_by(user_id).await {
Ok(drives) => drives.into_iter().map(|d| d.drive.id).collect(),
Err(e) => {
tracing::warn!("Content-index: drive lookup failed — degrading to empty: {e}");
@@ -338,7 +328,7 @@ impl SearchService {
}
};
match authz
.check(caller, Permission::Read, Resource::File(file_uuid))
.check(Subject::User(user_id), Permission::Read, Resource::File(file_uuid))
.await
{
Ok(true) => verified.push(hit),
+4 -6
View File
@@ -983,10 +983,9 @@ mod tests {
unimplemented!()
}
async fn list_folders_by_owner(
async fn list_root_folders_for_caller(
&self,
_parent_id: Option<&str>,
_owner_id: Uuid,
_caller_id: Uuid,
) -> Result<Vec<crate::domain::entities::folder::Folder>, DomainError> {
unimplemented!()
}
@@ -1002,10 +1001,9 @@ mod tests {
unimplemented!()
}
async fn list_folders_by_owner_paginated(
async fn list_root_folders_for_caller_paginated(
&self,
_parent_id: Option<&str>,
_owner_id: Uuid,
_caller_id: Uuid,
_offset: usize,
_limit: usize,
_include_total: bool,
+2 -14
View File
@@ -786,13 +786,9 @@ impl TrashService {
/// keeps the two HTTP surfaces semantically consistent and avoids
/// duplicating the subject-expansion plumbing.
async fn drives_with_delete_for(&self, user_id: Uuid) -> Result<Vec<Uuid>> {
let (subject_types, subject_ids) = self
.authz
.expand_subject_for_listing(Subject::User(user_id))
.await?;
let drives = self
.drive_repo
.list_for_subjects(&subject_types, &subject_ids)
.list_readable_by(user_id)
.await
.map_err(|e| {
DomainError::internal_error(
@@ -900,15 +896,7 @@ impl TrashService {
// D2b: scope by drives the caller can read (resolved through
// role_grants on resource_type='drive', including group-mediated
// grants). Empty set → empty page without a SQL round-trip.
let (subject_types, subject_ids) = self
.authz
.expand_subject_for_listing(Subject::User(user_id))
.await?;
let drive_ids: Vec<Uuid> = match self
.drive_repo
.list_for_subjects(&subject_types, &subject_ids)
.await
{
let drive_ids: Vec<Uuid> = match self.drive_repo.list_readable_by(user_id).await {
Ok(drives) => drives.into_iter().map(|d| d.drive.id).collect(),
Err(e) => {
return Err(DomainError::internal_error(
@@ -745,10 +745,9 @@ impl FolderRepository for MockFolderRepository {
Ok(vec![])
}
async fn list_folders_by_owner(
async fn list_root_folders_for_caller(
&self,
_parent_id: Option<&str>,
_owner_id: Uuid,
_caller_id: Uuid,
) -> std::result::Result<Vec<Folder>, DomainError> {
Ok(vec![])
}
@@ -763,10 +762,9 @@ impl FolderRepository for MockFolderRepository {
Ok((vec![], Some(0)))
}
async fn list_folders_by_owner_paginated(
async fn list_root_folders_for_caller_paginated(
&self,
_parent_id: Option<&str>,
_owner_id: Uuid,
_caller_id: Uuid,
_offset: usize,
_limit: usize,
_include_total: bool,