feat: add VisibilityFilteredAssetRepository decorator for automatic access control on asset queries

This commit is contained in:
2026-05-31 19:06:49 +02:00
parent 0b2237860e
commit b5cda3afeb
6 changed files with 358 additions and 7 deletions

View File

@@ -1,8 +1,9 @@
use crate::catalog::visibility::VisibilityFilteredAssetRepository;
use domain::{
catalog::entities::Asset,
catalog::services::resolve_metadata,
errors::DomainError,
ports::{AssetMetadataRepository, AssetRepository},
ports::{AssetMetadataRepository, AssetRepository, ShareRepository},
value_objects::{StructuredData, SystemId},
};
use std::sync::Arc;
@@ -16,6 +17,7 @@ pub struct GetAssetQuery {
pub struct GetAssetHandler {
asset_repo: Arc<dyn AssetRepository>,
metadata_repo: Arc<dyn AssetMetadataRepository>,
share_repo: Option<Arc<dyn ShareRepository>>,
}
impl GetAssetHandler {
@@ -26,6 +28,28 @@ impl GetAssetHandler {
Self {
asset_repo,
metadata_repo,
share_repo: None,
}
}
/// Enable sharing-aware visibility filtering. When set, the handler
/// wraps the inner `AssetRepository` with a `VisibilityFilteredAssetRepository`
/// so that shared assets are visible to the caller.
pub fn with_visibility_filter(mut self, share_repo: Arc<dyn ShareRepository>) -> Self {
self.share_repo = Some(share_repo);
self
}
/// Returns the effective asset repo — wrapped with a visibility filter
/// when a `ShareRepository` has been configured, otherwise the raw inner repo.
fn effective_repo(&self, caller_id: SystemId) -> Arc<dyn AssetRepository> {
match &self.share_repo {
Some(share_repo) => Arc::new(VisibilityFilteredAssetRepository::new(
self.asset_repo.clone(),
share_repo.clone(),
caller_id,
)),
None => self.asset_repo.clone(),
}
}
@@ -33,13 +57,16 @@ impl GetAssetHandler {
&self,
query: GetAssetQuery,
) -> Result<(Asset, StructuredData), DomainError> {
let asset = self
.asset_repo
let repo = self.effective_repo(query.user_id);
let asset = repo
.find_by_id(&query.asset_id)
.await?
.ok_or_else(|| DomainError::NotFound(format!("Asset {} not found", query.asset_id)))?;
if asset.owner_user_id != query.user_id {
// When the visibility filter is active it already enforces access.
// When it is not, fall back to the original owner-only check.
if self.share_repo.is_none() && asset.owner_user_id != query.user_id {
return Err(DomainError::Forbidden("Access denied".to_string()));
}

View File

@@ -1,8 +1,9 @@
use crate::catalog::visibility::VisibilityFilteredAssetRepository;
use domain::{
catalog::entities::Asset,
catalog::services::resolve_metadata,
errors::DomainError,
ports::{AssetMetadataRepository, AssetRepository},
ports::{AssetMetadataRepository, AssetRepository, ShareRepository},
value_objects::{StructuredData, SystemId},
};
use std::sync::Arc;
@@ -10,6 +11,7 @@ use std::sync::Arc;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct GetTimelineQuery {
pub owner_id: SystemId,
pub caller_id: Option<SystemId>,
pub limit: u32,
pub offset: u32,
}
@@ -17,6 +19,7 @@ pub struct GetTimelineQuery {
pub struct GetTimelineHandler {
asset_repo: Arc<dyn AssetRepository>,
metadata_repo: Arc<dyn AssetMetadataRepository>,
share_repo: Option<Arc<dyn ShareRepository>>,
}
impl GetTimelineHandler {
@@ -27,6 +30,24 @@ impl GetTimelineHandler {
Self {
asset_repo,
metadata_repo,
share_repo: None,
}
}
/// Enable sharing-aware visibility filtering on timeline queries.
pub fn with_visibility_filter(mut self, share_repo: Arc<dyn ShareRepository>) -> Self {
self.share_repo = Some(share_repo);
self
}
fn effective_repo(&self, caller_id: SystemId) -> Arc<dyn AssetRepository> {
match &self.share_repo {
Some(share_repo) => Arc::new(VisibilityFilteredAssetRepository::new(
self.asset_repo.clone(),
share_repo.clone(),
caller_id,
)),
None => self.asset_repo.clone(),
}
}
@@ -34,8 +55,10 @@ impl GetTimelineHandler {
&self,
query: GetTimelineQuery,
) -> Result<Vec<(Asset, StructuredData)>, DomainError> {
let assets = self
.asset_repo
let caller_id = query.caller_id.unwrap_or(query.owner_id);
let repo = self.effective_repo(caller_id);
let assets = repo
.find_by_owner(&query.owner_id, query.limit, query.offset)
.await?;