Skip to main content

potree_auth/project_asset/application/
service.rs

1use std::path::Path;
2use std::sync::Arc;
3
4use http::HeaderMap;
5
6use super::super::ports::project_asset_store::ProjectAssetStore;
7use super::error::ProjectAssetsServiceError;
8use crate::authorization::domain::action::Action;
9use crate::authorization::ports::authorization_engine::AuthorizationEngine;
10use crate::common::domain::StaticAsset;
11use crate::project::domain::ProjectId;
12use crate::project::ports::project_repository::ProjectRepository;
13use crate::project_asset::domain::authorization::ProjectAssetResource;
14use crate::user::domain::User;
15
16/// A service for interacting with project assets.
17#[derive(Debug, Clone)]
18pub struct ProjectAssetService {
19    project_datastore: Arc<dyn ProjectRepository>,
20    project_asset_store: Arc<dyn ProjectAssetStore>,
21    authorization_engine: Arc<dyn AuthorizationEngine>,
22}
23
24impl ProjectAssetService {
25    pub fn new(
26        project_datastore: Arc<dyn ProjectRepository>,
27        project_asset_store: Arc<dyn ProjectAssetStore>,
28        authorization_engine: Arc<dyn AuthorizationEngine>,
29    ) -> Self {
30        Self {
31            project_datastore,
32            project_asset_store,
33            authorization_engine,
34        }
35    }
36
37    /// Read a specific project asset. Optional `request_headers` can be
38    /// provided to specify various instructions as to how to read and format
39    /// the resulting data.
40    pub async fn read_asset(
41        &self,
42        user: &Option<User>,
43        project_id: &ProjectId,
44        asset_path: &Path,
45        request_headers: Option<HeaderMap>,
46    ) -> Result<StaticAsset, ProjectAssetsServiceError> {
47        let project = self.project_datastore.read(project_id).await?;
48
49        let project_asset = ProjectAssetResource {
50            associated_project: &project,
51            asset_path,
52        };
53        self.authorization_engine
54            .can_on_instance(user, &Action::Read, &project_asset)?;
55
56        // Build a path to the asset. The asset would be within its project directory.
57        let asset_path = Path::new(project_id.as_str()).join(asset_path);
58
59        Ok(self
60            .project_asset_store
61            .get_asset(&asset_path, request_headers)
62            .await?)
63    }
64}
65
66#[cfg(test)]
67mod project_asset_service_tests {
68    use fake::Fake;
69    use fake::Faker;
70
71    use super::super::super::ports::project_asset_store::MockProjectAssetStore;
72    use super::*;
73    use crate::authorization::domain::error::AuthorizationEngineError;
74    use crate::authorization::ports::authorization_engine::MockAuthorizationEngine;
75    use crate::project::ports::project_repository::MockProjectRepository;
76
77    mod request_asset {
78
79        use super::*;
80
81        #[tokio::test]
82        async fn should_return_the_correct_error_if_user_not_authenticated() {
83            // Arrange
84            let mut project_datastore = MockProjectRepository::new();
85            project_datastore
86                .expect_read()
87                .return_const(Ok(Faker.fake()));
88            let mut authorization_engine = MockAuthorizationEngine::new();
89            authorization_engine
90                .expect_can_on_instance()
91                .return_const(Err(AuthorizationEngineError::NotAuthenticated));
92            let project_asset_store = MockProjectAssetStore::new();
93
94            let project_asset_service = ProjectAssetService::new(
95                Arc::new(project_datastore),
96                Arc::new(project_asset_store),
97                Arc::new(authorization_engine),
98            );
99
100            // Act
101            let res = project_asset_service
102                .read_asset(&Faker.fake(), &Faker.fake(), Path::new(""), Faker.fake())
103                .await;
104
105            // Assert
106            assert!(matches!(
107                res,
108                Err(ProjectAssetsServiceError::NotAuthenticated)
109            ))
110        }
111    }
112
113    #[tokio::test]
114    async fn should_return_the_correct_error_if_user_not_authorized() {
115        // Arrange
116        let dummy_user = Faker.fake::<User>();
117
118        let mut project_datastore = MockProjectRepository::new();
119        project_datastore
120            .expect_read()
121            .return_const(Ok(Faker.fake()));
122        let mut authorization_engine = MockAuthorizationEngine::new();
123        authorization_engine
124            .expect_can_on_instance()
125            .return_const(Err(AuthorizationEngineError::NotAuthorized {
126                user: Box::new(dummy_user.clone()),
127                action: Action::Read,
128                resource_identifier: Some(Faker.fake()),
129                resource_type: Faker.fake(),
130            }));
131        let project_asset_store = MockProjectAssetStore::new();
132
133        let project_asset_service = ProjectAssetService::new(
134            Arc::new(project_datastore),
135            Arc::new(project_asset_store),
136            Arc::new(authorization_engine),
137        );
138
139        // Act
140        let res = project_asset_service
141            .read_asset(&Faker.fake(), &Faker.fake(), Path::new(""), Faker.fake())
142            .await;
143
144        // Assert
145        assert!(matches!(
146            res,
147            Err(ProjectAssetsServiceError::NotAuthorized { .. })
148        ))
149    }
150}