feat: add VisibilityFilteredAssetRepository decorator for automatic access control on asset queries
This commit is contained in:
@@ -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()));
|
||||
}
|
||||
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user