Skip to main content

potree_auth/authorization/adapters/
basic_authorization.rs

1//! A pretty rudimentary authorization service that implements the
2//! [`AuthorizationEngine`] trait.
3//!
4//! This could be replaced by more robust policy engine in the future.
5
6use super::super::domain::action::Action;
7use super::super::domain::error::AuthorizationEngineError;
8use super::super::domain::resource::Resource;
9use super::super::domain::resource::ResourceInstance;
10use super::super::ports::authorization_engine::AuthorizationEngine;
11use crate::common::domain::resource_type;
12use crate::user::domain::User;
13
14/// Handles authorization business logic for the application.
15#[derive(Debug, Clone)]
16pub struct SimpleAuthorizationEngine;
17
18impl SimpleAuthorizationEngine {
19    #[tracing::instrument(
20        name = "`simple_authorization_engine`: evaluating on resource type",
21        err
22    )]
23    pub fn can_on_type(
24        &self,
25        user: &Option<User>,
26        action: &Action,
27        resource: &dyn Resource,
28    ) -> Result<(), AuthorizationEngineError> {
29        // If there is no user then the user is not authenticated.
30        let Some(user) = user else {
31            return Err(AuthorizationEngineError::NotAuthenticated);
32        };
33
34        // An admin should always be allowed to action.
35        if user.is_admin() {
36            return Ok(());
37        };
38
39        match (action, resource.resource_type().as_str()) {
40            (&Action::List, resource_type::PROJECT) => Ok(()),
41            (&Action::Read, resource_type::PROJECTS_DASHBOARD) => Ok(()),
42            _ => Err(AuthorizationEngineError::NotAuthorized {
43                user: Box::new(user.clone()),
44                action: action.clone(),
45                resource_identifier: None,
46                resource_type: resource.resource_type(),
47            }),
48        }
49    }
50
51    #[tracing::instrument(
52        name = "`simple_authorization_engine`: evaluating on resource instance",
53        err
54    )]
55    pub fn can_on_instance(
56        &self,
57        user: &Option<User>,
58        action: &Action,
59        resource: &dyn ResourceInstance,
60    ) -> Result<(), AuthorizationEngineError> {
61        // If there is no user then the user is not authenticated.
62        let Some(user) = user else {
63            return Err(AuthorizationEngineError::NotAuthenticated);
64        };
65
66        // Allows a user to _read_ any resource of which they share a group.
67        match action {
68            &Action::Read => {
69                if user.is_admin()
70                    || resource.groups().map_or(false, |groups| {
71                        groups.iter().any(|group| user.groups.contains(group))
72                    })
73                {
74                    Ok(())
75                } else {
76                    Err(AuthorizationEngineError::NotAuthorized {
77                        user: Box::new(user.clone()),
78                        action: action.clone(),
79                        resource_identifier: Some(resource.resource_identifier()),
80                        resource_type: resource.resource_type(),
81                    })
82                }
83            }
84            _ => Err(AuthorizationEngineError::NotAuthorized {
85                user: Box::new(user.clone()),
86                action: action.clone(),
87                resource_identifier: Some(resource.resource_identifier()),
88                resource_type: resource.resource_type(),
89            }),
90        }
91    }
92}
93
94impl AuthorizationEngine for SimpleAuthorizationEngine {
95    fn can_on_type(
96        &self,
97        user: &Option<User>,
98        action: &Action,
99        resource: &dyn Resource,
100    ) -> Result<(), AuthorizationEngineError> {
101        Self::can_on_type(self, user, action, resource)
102    }
103
104    fn can_on_instance(
105        &self,
106        user: &Option<User>,
107        action: &Action,
108        resource: &dyn ResourceInstance,
109    ) -> Result<(), AuthorizationEngineError> {
110        Self::can_on_instance(self, user, action, resource)
111    }
112}
113
114#[cfg(test)]
115mod authorization_service_tests {
116    use fake::Fake;
117    use fake::Faker;
118
119    use super::super::super::domain::resource::mocked_resource::MockedResource;
120    use super::*;
121    use crate::common::domain::group::Group;
122
123    mod can_on_type {
124
125        use super::*;
126        use crate::authorization::domain::resource::ResourceType;
127
128        #[test]
129        fn should_return_ok_if_the_user_is_an_admin() {
130            // Arrange
131            let authorization_service = SimpleAuthorizationEngine;
132
133            let user = User::dummy_admin();
134            let resource = Faker.fake::<MockedResource>();
135
136            // Act
137            let res = authorization_service.can_on_type(&Some(user), &Action::Read, &resource);
138
139            // Assert
140            assert!(res.is_ok())
141        }
142
143        #[test]
144        fn should_return_ok_if_listing_projects() {
145            // Arrange
146            let authorization_service = SimpleAuthorizationEngine;
147
148            let user = Faker.fake::<User>();
149            let resource = MockedResource {
150                resource_type: ResourceType::new(resource_type::PROJECT.to_owned()),
151                ..Faker.fake()
152            };
153            let action = Action::List;
154
155            // Act
156            let res = authorization_service.can_on_type(&Some(user), &action, &resource);
157
158            // Assert
159            assert!(res.is_ok())
160        }
161
162        #[test_case::test_matrix(
163            [&Action::Create, &Action::Read, &Action::Update, &Action::Delete]
164            )]
165        fn should_return_err_if_not_listing_projects(action: &Action) {
166            // Arrange
167            let authorization_service = SimpleAuthorizationEngine;
168
169            let user = Faker.fake::<User>();
170            let resource = MockedResource {
171                resource_type: ResourceType::new(resource_type::PROJECT.to_owned()),
172                ..Faker.fake()
173            };
174
175            // Act
176            let res = authorization_service.can_on_type(&Some(user), action, &resource);
177
178            // Assert
179            assert!(matches!(
180                res,
181                Err(AuthorizationEngineError::NotAuthorized { .. })
182            ))
183        }
184
185        #[test]
186        fn should_return_ok_if_reading_a_project_dashboard() {
187            // Arrange
188            let authorization_service = SimpleAuthorizationEngine;
189
190            let user = Faker.fake::<User>();
191            let resource = MockedResource {
192                resource_type: ResourceType::new(resource_type::PROJECTS_DASHBOARD.to_owned()),
193                ..Faker.fake()
194            };
195            let action = Action::Read;
196
197            // Act
198            let res = authorization_service.can_on_type(&Some(user), &action, &resource);
199
200            // Assert
201            assert!(res.is_ok())
202        }
203
204        #[test_case::test_matrix(
205            [&Action::Create, &Action::List, &Action::Update, &Action::Delete]
206            )]
207        fn should_return_err_if_not_reading_a_project_dashboard(action: &Action) {
208            // Arrange
209            let authorization_service = SimpleAuthorizationEngine;
210
211            let user = Faker.fake::<User>();
212            let resource = MockedResource {
213                resource_type: ResourceType::new(resource_type::PROJECTS_DASHBOARD.to_owned()),
214                ..Faker.fake()
215            };
216
217            // Act
218            let res = authorization_service.can_on_type(&Some(user), action, &resource);
219
220            // Assert
221            assert!(matches!(
222                res,
223                Err(AuthorizationEngineError::NotAuthorized { .. })
224            ))
225        }
226
227        #[test]
228        fn should_return_err_for_other_combinations() {
229            // Arrange
230            let authorization_service = SimpleAuthorizationEngine;
231
232            let user = Faker.fake::<User>();
233            let resource = Faker.fake::<MockedResource>();
234            let action = Faker.fake::<Action>();
235
236            // Act
237            let res = authorization_service.can_on_type(&Some(user), &action, &resource);
238
239            // Assert
240            assert!(matches!(
241                res,
242                Err(AuthorizationEngineError::NotAuthorized { .. })
243            ))
244        }
245    }
246
247    mod can_on_instance {
248
249        use super::*;
250
251        #[test]
252        fn should_return_ok_if_the_user_is_an_admin() {
253            // Arrange
254            let authorization_service = SimpleAuthorizationEngine;
255
256            let user = User::dummy_admin();
257            let resource = Faker.fake::<MockedResource>();
258
259            // Act
260            let res = authorization_service.can_on_instance(&Some(user), &Action::Read, &resource);
261
262            // Assert
263            assert!(res.is_ok())
264        }
265
266        #[test]
267        fn should_return_ok_if_the_user_shares_a_group_with_the_resource() {
268            // Arrange
269            let authorization_service = SimpleAuthorizationEngine;
270
271            let shared_group = Faker.fake::<Group>();
272            let user = User {
273                groups: [Faker.fake(), shared_group.clone()].into(),
274                ..Faker.fake()
275            };
276            let resource = MockedResource {
277                groups: Some(vec![shared_group]),
278                user_emails: None,
279                ..Faker.fake()
280            };
281
282            // Act
283            let res = authorization_service.can_on_instance(&Some(user), &Action::Read, &resource);
284
285            // Assert
286            assert!(res.is_ok())
287        }
288
289        #[test]
290        fn should_return_err_if_the_user_does_not_share_a_group_with_the_resource() {
291            // Arrange
292            let authorization_service = SimpleAuthorizationEngine;
293
294            let user = Faker.fake::<User>();
295            let resource = MockedResource {
296                groups: Some(vec![]),
297                user_emails: None,
298                ..Faker.fake()
299            };
300
301            // Act
302            let res = authorization_service.can_on_instance(&Some(user), &Action::Read, &resource);
303
304            // Assert
305            assert!(matches!(
306                res,
307                Err(AuthorizationEngineError::NotAuthorized { .. })
308            ))
309        }
310
311        #[test]
312        fn should_return_err_if_the_user_is_not_authenticated() {
313            // Arrange
314            let authorization_service = SimpleAuthorizationEngine;
315
316            // Act
317            let res = authorization_service.can_on_instance(
318                &None,
319                &Action::Read,
320                &Faker.fake::<MockedResource>(),
321            );
322
323            // Assert
324            assert!(matches!(
325                res,
326                Err(AuthorizationEngineError::NotAuthenticated)
327            ))
328        }
329
330        #[test_case::test_case(&Action::List; "list")]
331        #[test_case::test_case(&Action::Create; "create")]
332        #[test_case::test_case(&Action::Update; "update")]
333        #[test_case::test_case(&Action::Delete; "delete")]
334        fn should_return_err_if_anything_other_than_read(action: &Action) {
335            // Arrange
336            let authorization_service = SimpleAuthorizationEngine;
337
338            let shared_group = Faker.fake::<Group>();
339            let user = User {
340                groups: [Faker.fake(), shared_group.clone()].into(),
341                ..Faker.fake()
342            };
343            let resource = MockedResource {
344                groups: Some(vec![shared_group]),
345                user_emails: None,
346                ..Faker.fake()
347            };
348
349            // Act
350            let res = authorization_service.can_on_instance(&Some(user), action, &resource);
351
352            // Assert
353            assert!(matches!(
354                res,
355                Err(AuthorizationEngineError::NotAuthorized { .. })
356            ))
357        }
358    }
359}