Skip to main content

potree_auth/render/application/
service.rs

1use std::sync::Arc;
2
3use web_route::ParameterizedRoute;
4use web_route::WebRoute;
5
6use super::super::domain::authorization::PotreeRenderResource;
7use super::super::domain::authorization::ProjectDashboardResource;
8use super::super::domain::not_found_render::NotFound;
9use super::super::domain::potree_render::PotreeRender;
10use super::super::domain::project_dashboard_render::ProjectDashboard;
11use super::error::RenderingServiceError;
12use crate::authorization::domain::action::Action;
13use crate::authorization::ports::authorization_engine::AuthorizationEngine;
14use crate::project::domain::Project;
15use crate::project::domain::ProjectId;
16use crate::project::domain::authorization::ProjectTypeResource;
17use crate::project::ports::project_repository::ProjectRepository;
18use crate::user::domain::User;
19
20/// A service for rendering a project.
21#[derive(Debug, Clone)]
22pub struct RenderingService {
23    /// Used to load project information.
24    project_repository: Arc<dyn ProjectRepository>,
25
26    /// Used to determine if a user ir authorized to view a project.
27    authorization_engine: Arc<dyn AuthorizationEngine>,
28
29    /// The route (parametrized) at which the project assets can be accessed.
30    project_assets_route: ParameterizedRoute,
31
32    /// The top-level route from which `potree` static assets are served.
33    potree_assets_route: WebRoute,
34}
35
36impl RenderingService {
37    pub fn new(
38        project_datastore: Arc<dyn ProjectRepository>,
39        authorization_engine: Arc<dyn AuthorizationEngine>,
40        project_assets_route: ParameterizedRoute,
41        potree_assets_route: WebRoute,
42    ) -> Self {
43        Self {
44            project_repository: project_datastore,
45            authorization_engine,
46            project_assets_route,
47            potree_assets_route,
48        }
49    }
50
51    /// Render a `potree` project.
52    ///
53    /// # Errors
54    ///
55    /// Will return an error if the project can not be found or the `user` is
56    /// not authorized to view it.
57    pub async fn render_potree(
58        &self,
59        user: &Option<User>,
60        project_id: &ProjectId,
61    ) -> Result<PotreeRender, RenderingServiceError> {
62        let project = self.project_repository.read(project_id).await?;
63
64        let potree_render_resource = PotreeRenderResource {
65            associated_project: &project,
66        };
67        self.authorization_engine
68            .can_on_instance(user, &Action::Read, &potree_render_resource)?;
69
70        Ok(PotreeRender {
71            project_title: project.name,
72            potree_static_assets_path: self.potree_assets_route.clone(),
73            potree_config_path: self.potree_config_route(project_id)?,
74        })
75    }
76
77    /// Provides a dashboard showing all of the `user`'s projects.
78    pub async fn project_dashboard(
79        &self,
80        user: &Option<User>,
81        default_project_render_route: &ParameterizedRoute,
82    ) -> Result<ProjectDashboard, RenderingServiceError> {
83        self.authorization_engine
84            .can_on_type(user, &Action::Read, &ProjectDashboardResource)?;
85
86        let projects = self.list_allowed_projects(user).await?;
87
88        Ok(ProjectDashboard::from_domain_projects(
89            projects,
90            default_project_render_route,
91        )?)
92    }
93
94    /// Provides a 404 page.
95    pub async fn not_found(&self) -> Result<NotFound, RenderingServiceError> {
96        Ok(NotFound)
97    }
98
99    /// Determine the route to the potree config file for a specific project
100    /// (`project_id`).
101    fn potree_config_route(
102        &self,
103        project_id: &ProjectId,
104    ) -> Result<WebRoute, RenderingServiceError> {
105        // This is copying the parameters from the `project_assets_server` project
106        // assets route. It is possible for these to go out of date unless a common
107        // import is used.
108        //
109        // Currently the only way to check that this works will be a top level
110        // integration/e2e test.
111        let params = serde_json::json!({
112            "project_id": project_id,
113            "path": "potree.json5",
114        });
115
116        Ok(self.project_assets_route.to_web_route(&params)?)
117    }
118
119    /// List all of the projects a `user` is allowed to read.
120    async fn list_allowed_projects(
121        &self,
122        user: &Option<User>,
123    ) -> Result<Vec<Project>, RenderingServiceError> {
124        self.authorization_engine
125            .can_on_type(user, &Action::List, &ProjectTypeResource)?;
126
127        let projects = self.project_repository.list().await?;
128
129        // Filter out the projects that the user is not allowed to read.
130        let allowed_projects = projects
131            .into_iter()
132            .filter(|p| {
133                self.authorization_engine
134                    .can_on_instance(user, &Action::Read, p)
135                    .is_ok()
136            })
137            .collect();
138
139        Ok(allowed_projects)
140    }
141}
142
143#[cfg(test)]
144mod project_rendering_service_tests {
145    use std::sync::Arc;
146
147    use fake::Fake;
148    use fake::Faker;
149    use web_route::ParameterizedRoute;
150    use web_route::WebRoute;
151
152    use super::super::super::application::error::RenderingServiceError;
153    use super::super::super::application::service::RenderingService;
154    use crate::authorization::domain::action::Action;
155    use crate::authorization::domain::error::AuthorizationEngineError;
156    use crate::authorization::ports::authorization_engine::MockAuthorizationEngine;
157    use crate::project::ports::project_repository::MockProjectRepository;
158
159    mod render_potree {
160
161        use super::*;
162
163        #[tokio::test]
164        async fn should_return_the_correct_error_if_user_not_authenticated() {
165            // Arrange
166            let mut project_datastore = MockProjectRepository::new();
167            project_datastore
168                .expect_read()
169                .return_const(Ok(Faker.fake()));
170            let mut authorization_engine = MockAuthorizationEngine::new();
171            authorization_engine
172                .expect_can_on_instance()
173                .return_const(Err(AuthorizationEngineError::NotAuthenticated));
174
175            let rendering_service = RenderingService::new(
176                Arc::new(project_datastore),
177                Arc::new(authorization_engine),
178                ParameterizedRoute::new(Faker.fake::<WebRoute>()).join("/{project_id}/{*path}"),
179                Faker.fake(),
180            );
181
182            // Act
183            let res = rendering_service
184                .render_potree(&Faker.fake(), &Faker.fake())
185                .await;
186
187            // Assert
188            assert!(matches!(res, Err(RenderingServiceError::NotAuthenticated)));
189        }
190
191        #[tokio::test]
192        async fn should_return_the_correct_error_if_user_not_authorized() {
193            // Arrange
194            let mut project_datastore = MockProjectRepository::new();
195            project_datastore
196                .expect_read()
197                .return_const(Ok(Faker.fake()));
198            let mut authorization_engine = MockAuthorizationEngine::new();
199            authorization_engine
200                .expect_can_on_instance()
201                .return_const(Err(AuthorizationEngineError::NotAuthorized {
202                    user: Faker.fake(),
203                    action: Action::Read,
204                    resource_identifier: Faker.fake(),
205                    resource_type: Faker.fake(),
206                }));
207
208            let rendering_service = RenderingService::new(
209                Arc::new(project_datastore),
210                Arc::new(authorization_engine),
211                ParameterizedRoute::new(Faker.fake::<WebRoute>()).join("/{project_id}/{*path}"),
212                Faker.fake(),
213            );
214
215            // Act
216            let res = rendering_service
217                .render_potree(&Faker.fake(), &Faker.fake())
218                .await;
219
220            // Assert
221            assert!(matches!(
222                res,
223                Err(RenderingServiceError::NotAuthorized { .. })
224            ));
225        }
226    }
227
228    mod list_allowed_projects {
229        use std::sync::Mutex;
230
231        use super::*;
232        use crate::project::domain::Project;
233
234        #[tokio::test]
235        async fn should_return_the_projects_that_the_user_is_allowed_to_read() {
236            // Arrange
237            let dummy_projects = Faker.fake::<Vec<Project>>();
238
239            let mut project_datastore = MockProjectRepository::new();
240            project_datastore
241                .expect_list()
242                .return_const(Ok(dummy_projects.clone()));
243            let mut authorization_engine = MockAuthorizationEngine::new();
244            // The first call to the authZ engine is checking that the user is allowed to
245            // list projects.
246            authorization_engine
247                .expect_can_on_type()
248                .once()
249                .return_const(Ok(()));
250            // Subsequent calls determines if the user is allow to read a specific project.
251            let authorization_engine_call_count = Arc::new(Mutex::new(0));
252            let authorization_engine_call_count_clone =
253                Arc::clone(&authorization_engine_call_count);
254            authorization_engine
255                .expect_can_on_instance()
256                .returning(move |_, _, _| {
257                    let mut count = authorization_engine_call_count_clone.lock().unwrap();
258                    let res = if *count % 2 == 0 {
259                        Err(AuthorizationEngineError::NotAuthorized {
260                            user: Faker.fake(),
261                            action: Action::List,
262                            resource_identifier: Some(Faker.fake()),
263                            resource_type: Faker.fake(),
264                        })
265                    } else {
266                        Ok(())
267                    };
268                    *count += 1;
269                    res
270                });
271
272            let rendering_service = RenderingService::new(
273                Arc::new(project_datastore),
274                Arc::new(authorization_engine),
275                ParameterizedRoute::new(Faker.fake::<WebRoute>()).join("/{project_id}/{*path}"),
276                Faker.fake(),
277            );
278
279            // Act
280            let allowed_projects = rendering_service
281                .list_allowed_projects(&Faker.fake())
282                .await
283                .unwrap();
284
285            // Assert
286            assert_eq!(allowed_projects.len(), dummy_projects.len() / 2);
287        }
288
289        #[tokio::test]
290        async fn should_return_the_correct_error_if_user_not_allowed_to_list_projects() {
291            // Arrange
292            let project_datastore = MockProjectRepository::new();
293            let mut authorization_engine = MockAuthorizationEngine::new();
294            authorization_engine
295                .expect_can_on_type()
296                .once()
297                .return_const(Err(AuthorizationEngineError::NotAuthorized {
298                    user: Faker.fake(),
299                    action: Action::List,
300                    resource_identifier: None,
301                    resource_type: Faker.fake(),
302                }));
303
304            let rendering_service = RenderingService::new(
305                Arc::new(project_datastore),
306                Arc::new(authorization_engine),
307                ParameterizedRoute::new(Faker.fake::<WebRoute>()).join("/{project_id}/{*path}"),
308                Faker.fake(),
309            );
310
311            // Act
312            let res = rendering_service.list_allowed_projects(&Faker.fake()).await;
313
314            // Assert
315            assert!(matches!(
316                res,
317                Err(RenderingServiceError::NotAuthorized { .. })
318            ));
319        }
320    }
321}