Skip to main content

headless_lms_server/domain/
authorization.rs

1//! Actix plumbing around the authorization policy engine in [headless_lms_authorization].
2
3use crate::prelude::*;
4use actix_web::{HttpRequest, Responder};
5
6pub use headless_lms_authorization::error::{AuthorizationError, AuthorizationErrorType};
7pub use headless_lms_authorization::{
8    Action, ActionOnResource, AuthorizationToken, Resource, authorize, authorize_access_to_chatbot,
9    authorize_access_to_course_material, authorize_with_fetched_list_of_roles,
10    can_user_view_chapter, is_permitted, is_user_global_admin, skip_authorize,
11};
12
13/// A controller's response payload. Only [AuthorizedOk::authorized_ok] can build one, so
14/// answering a request requires having passed an authorization check.
15#[derive(Copy, Clone)]
16pub struct AuthorizedResponse<T> {
17    pub data: T,
18}
19
20impl<T: Responder> Responder for AuthorizedResponse<T> {
21    type Body = T::Body;
22
23    fn respond_to(self, req: &HttpRequest) -> actix_web::HttpResponse<Self::Body> {
24        T::respond_to(self.data, req)
25    }
26}
27
28/// Turns a token into a [ControllerResult], which is the only way for a controller to build
29/// one. Lives here rather than on [AuthorizationToken] itself because the response type is
30/// tied to actix, which the policy engine deliberately does not depend on.
31pub trait AuthorizedOk {
32    fn authorized_ok<T>(self, t: T) -> ControllerResult<T>;
33}
34
35impl AuthorizedOk for AuthorizationToken {
36    fn authorized_ok<T>(self, t: T) -> ControllerResult<T> {
37        Ok(AuthorizedResponse { data: t })
38    }
39}
40
41#[cfg(test)]
42mod test {
43    use super::*;
44    // Explicit: a workspace-wide test build enables the models crate's test-helpers feature, whose
45    // own insert_data! then clashes with ours in the globs below.
46    use crate::insert_data;
47    use crate::test_helper::*;
48    use headless_lms_models::*;
49    use models::roles::{RoleDomain, UserRole};
50
51    #[actix_web::test]
52    async fn test_authorization() {
53        let mut conn = Conn::init().await;
54        let mut tx = conn.begin().await;
55
56        let user = users::insert(
57            tx.as_mut(),
58            PKeyPolicy::Generate,
59            "auth@example.com",
60            None,
61            None,
62        )
63        .await
64        .unwrap();
65        let org = organizations::insert(
66            tx.as_mut(),
67            PKeyPolicy::Generate,
68            "auth",
69            "auth",
70            Some("auth"),
71            false,
72        )
73        .await
74        .unwrap();
75
76        authorize(
77            tx.as_mut(),
78            Action::Edit,
79            Some(user),
80            Resource::Organization(org),
81        )
82        .await
83        .unwrap_err();
84
85        roles::insert(
86            tx.as_mut(),
87            user,
88            UserRole::Teacher,
89            RoleDomain::Organization(org),
90        )
91        .await
92        .unwrap();
93
94        authorize(
95            tx.as_mut(),
96            Action::Edit,
97            Some(user),
98            Resource::Organization(org),
99        )
100        .await
101        .unwrap();
102    }
103
104    #[actix_web::test]
105    async fn course_role_chapter_resource() {
106        insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module, :chapter);
107
108        authorize(
109            tx.as_mut(),
110            Action::Edit,
111            Some(user),
112            Resource::Chapter(chapter),
113        )
114        .await
115        .unwrap_err();
116
117        roles::insert(
118            tx.as_mut(),
119            user,
120            UserRole::Teacher,
121            RoleDomain::Course(course),
122        )
123        .await
124        .unwrap();
125
126        authorize(
127            tx.as_mut(),
128            Action::Edit,
129            Some(user),
130            Resource::Chapter(chapter),
131        )
132        .await
133        .unwrap();
134    }
135
136    #[actix_web::test]
137    async fn anonymous_user_can_view_open_course() {
138        insert_data!(:tx, :user, :org, :course);
139
140        authorize(tx.as_mut(), Action::View, None, Resource::Course(course))
141            .await
142            .unwrap();
143    }
144}