headless_lms_server/domain/
authorization.rs

1//! Common functionality related to authorization
2
3use crate::OAuthClient;
4use crate::prelude::*;
5use actix_http::Payload;
6use actix_session::Session;
7use actix_session::SessionExt;
8use actix_web::{FromRequest, HttpRequest, Responder};
9use anyhow::Result;
10use chrono::{DateTime, Duration, Utc};
11use futures::Future;
12use headless_lms_models::{self as models, roles::UserRole, users::User};
13use headless_lms_utils::http::REQWEST_CLIENT;
14use headless_lms_utils::tmc::TMCUser;
15use headless_lms_utils::tmc::TmcClient;
16use models::{CourseOrExamId, roles::Role};
17use oauth2::EmptyExtraTokenFields;
18use oauth2::HttpClientError;
19use oauth2::RequestTokenError;
20use oauth2::ResourceOwnerPassword;
21use oauth2::ResourceOwnerUsername;
22use oauth2::StandardTokenResponse;
23use oauth2::TokenResponse;
24use oauth2::basic::BasicTokenType;
25use secrecy::SecretString;
26use serde::{Deserialize, Serialize};
27use sqlx::PgConnection;
28use std::env;
29use std::pin::Pin;
30use tracing_log::log;
31#[cfg(feature = "ts_rs")]
32pub use ts_rs::TS;
33use uuid::Uuid;
34
35const SESSION_KEY: &str = "user";
36
37// at least one field should be kept private to prevent initializing the struct outside of this module;
38// this way FromRequest is the only way to create an AuthUser
39/// Extractor for an authenticated user.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
41pub struct AuthUser {
42    pub id: Uuid,
43    pub created_at: DateTime<Utc>,
44    pub updated_at: DateTime<Utc>,
45    pub deleted_at: Option<DateTime<Utc>>,
46    pub fetched_from_db_at: Option<DateTime<Utc>>,
47    upstream_id: Option<i32>,
48}
49
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51#[cfg_attr(feature = "ts_rs", derive(TS))]
52#[serde(rename_all = "snake_case")]
53pub struct ActionOnResource {
54    pub action: Action,
55    pub resource: Resource,
56}
57
58impl AuthUser {
59    /// The user's ID in TMC.
60    pub fn upstream_id(&self) -> Option<i32> {
61        self.upstream_id
62    }
63}
64
65impl FromRequest for AuthUser {
66    type Error = ControllerError;
67    type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
68
69    fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
70        let req = req.clone();
71        Box::pin(async move {
72            let req = req.clone();
73            let session = req.get_session();
74            let pool: Option<&web::Data<PgPool>> = req.app_data();
75            match session.get::<AuthUser>(SESSION_KEY) {
76                Ok(Some(user)) => Ok(verify_auth_user_exists(user, pool, &session).await?),
77                Ok(None) => Err(ControllerError::new(
78                    ControllerErrorType::Unauthorized,
79                    "You are not currently logged in. Please sign in to continue.".to_string(),
80                    None,
81                )),
82                Err(_) => {
83                    // session had an invalid value
84                    session.remove(SESSION_KEY);
85                    Err(ControllerError::new(
86                        ControllerErrorType::Unauthorized,
87                        "Your session is invalid or has expired. Please sign in again.".to_string(),
88                        None,
89                    ))
90                }
91            }
92        })
93    }
94}
95
96/**
97 * For making sure the user saved in the session still exists in the database. Check the user's existance when the session is at least 3 hours old, updates the session automatically, and returns an up-to-date AuthUser.
98 */
99async fn verify_auth_user_exists(
100    auth_user: AuthUser,
101    pool: Option<&web::Data<PgPool>>,
102    session: &Session,
103) -> Result<AuthUser, ControllerError> {
104    if let Some(fetched_from_db_at) = auth_user.fetched_from_db_at {
105        let time_now = Utc::now();
106        let time_hour_ago = time_now - Duration::hours(3);
107        if fetched_from_db_at > time_hour_ago {
108            // No need to check for the auth user yet
109            return Ok(auth_user);
110        }
111    }
112    if let Some(pool) = pool {
113        info!("Checking whether the user saved in the session still exists in the database.");
114        let mut conn = pool.acquire().await?;
115        let user = models::users::get_by_id(&mut conn, auth_user.id).await?;
116        remember(session, user)?;
117        match session.get::<AuthUser>(SESSION_KEY) {
118            Ok(Some(session_user)) => Ok(session_user),
119            Ok(None) => Err(ControllerError::new(
120                ControllerErrorType::InternalServerError,
121                "User did not persist in the session".to_string(),
122                None,
123            )),
124            Err(e) => Err(ControllerError::new(
125                ControllerErrorType::InternalServerError,
126                "User did not persist in the session".to_string(),
127                Some(e.into()),
128            )),
129        }
130    } else {
131        warn!("No database pool provided to verify_auth_user_exists");
132        Err(ControllerError::new(
133            ControllerErrorType::InternalServerError,
134            "Unable to verify your user account. The database connection is unavailable."
135                .to_string(),
136            None,
137        ))
138    }
139}
140
141/// Stores the user as authenticated in the given session.
142pub fn remember(session: &Session, user: models::users::User) -> Result<()> {
143    let auth_user = AuthUser {
144        id: user.id,
145        created_at: user.created_at,
146        updated_at: user.updated_at,
147        deleted_at: user.deleted_at,
148        upstream_id: user.upstream_id,
149        fetched_from_db_at: Some(Utc::now()),
150    };
151    session
152        .insert(SESSION_KEY, auth_user)
153        .map_err(|_| anyhow::anyhow!("Failed to insert to session"))
154}
155
156/// Checks if the user is authenticated in the given session.
157pub async fn has_auth_user_session(session: &Session, pool: web::Data<PgPool>) -> bool {
158    match session.get::<AuthUser>(SESSION_KEY) {
159        Ok(Some(sesssion_auth_user)) => {
160            verify_auth_user_exists(sesssion_auth_user, Some(&pool), session)
161                .await
162                .is_ok()
163        }
164        _ => false,
165    }
166}
167
168/// Forgets authentication from the current session, if any.
169pub fn forget(session: &Session) {
170    session.purge();
171}
172
173/// Describes an action that a user can take on some resource.
174#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)]
175#[cfg_attr(feature = "ts_rs", derive(TS))]
176#[serde(rename_all = "snake_case", tag = "type", content = "variant")]
177pub enum Action {
178    ViewMaterial,
179    View,
180    Edit,
181    Grade,
182    Teach,
183    Download,
184    Duplicate,
185    DeleteAnswer,
186    EditRole(UserRole),
187    CreateCoursesOrExams,
188    /// Deletion that we usually don't want to allow.
189    UsuallyUnacceptableDeletion,
190    UploadFile,
191    ViewUserProgressOrDetails,
192    ViewInternalCourseStructure,
193    ViewStats,
194    Administrate,
195}
196
197/// The target of an action.
198#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
199#[cfg_attr(feature = "ts_rs", derive(TS))]
200#[serde(rename_all = "snake_case", tag = "type", content = "id")]
201pub enum Resource {
202    GlobalPermissions,
203    Chapter(Uuid),
204    Course(Uuid),
205    CourseInstance(Uuid),
206    Exam(Uuid),
207    Exercise(Uuid),
208    ExerciseSlideSubmission(Uuid),
209    ExerciseTask(Uuid),
210    ExerciseTaskGrading(Uuid),
211    ExerciseTaskSubmission(Uuid),
212    Organization(Uuid),
213    Page(Uuid),
214    StudyRegistry(String),
215    AnyCourse,
216    Role,
217    User,
218    PlaygroundExample,
219    ExerciseService,
220}
221
222impl Resource {
223    pub fn from_course_or_exam_id(course_or_exam_id: CourseOrExamId) -> Self {
224        match course_or_exam_id {
225            CourseOrExamId::Course(id) => Self::Course(id),
226            CourseOrExamId::Exam(id) => Self::Exam(id),
227        }
228    }
229}
230
231/// Validates that user has right to function
232#[derive(Copy, Clone, Debug)]
233pub struct AuthorizationToken(());
234
235impl AuthorizationToken {
236    pub fn authorized_ok<T>(self, t: T) -> ControllerResult<T> {
237        Ok(AuthorizedResponse {
238            data: t,
239            token: self,
240        })
241    }
242}
243
244/// Responder for AuthorizationToken
245#[derive(Copy, Clone)]
246pub struct AuthorizedResponse<T> {
247    pub data: T,
248    pub token: AuthorizationToken,
249}
250
251impl<T: Responder> Responder for AuthorizedResponse<T> {
252    type Body = T::Body;
253
254    fn respond_to(self, req: &HttpRequest) -> actix_web::HttpResponse<Self::Body> {
255        T::respond_to(self.data, req)
256    }
257}
258
259/**  Skips the authorize() and returns AuthorizationToken, needed in functions with anonymous and test users
260
261# Example
262
263```ignore
264async fn example_function(
265    // No user mentioned
266) -> ControllerResult<....> {
267    // We need to return ControllerResult -> AuthorizedResponse
268
269    let token = skip_authorize();
270
271    token.authorized_ok(web::Json(organizations))
272
273}
274```
275*/
276pub fn skip_authorize() -> AuthorizationToken {
277    AuthorizationToken(())
278}
279
280/**  Can be used to check whether user is allowed to view some course material */
281pub async fn authorize_access_to_course_material(
282    conn: &mut PgConnection,
283    user_id: Option<Uuid>,
284    course_id: Uuid,
285) -> Result<AuthorizationToken, ControllerError> {
286    let token = if models::courses::is_draft(conn, course_id).await? {
287        info!("Course is in draft mode");
288        if user_id.is_none() {
289            return Err(ControllerError::new(
290                ControllerErrorType::Unauthorized,
291                "This course is currently in draft mode and not publicly available. Please log in if you have access permissions.".to_string(),
292                None,
293            ));
294        }
295        authorize(conn, Act::ViewMaterial, user_id, Res::Course(course_id)).await?
296    } else if models::courses::is_joinable_by_code_only(conn, course_id).await? {
297        info!("Course is joinable by code only");
298        if let Some(user_id_value) = user_id {
299            if models::join_code_uses::check_if_user_has_access_to_course(
300                conn,
301                user_id_value,
302                course_id,
303            )
304            .await
305            .is_err()
306            {
307                authorize(conn, Act::ViewMaterial, user_id, Res::Course(course_id)).await?;
308            }
309        } else {
310            return Err(ControllerError::new(
311                ControllerErrorType::Unauthorized,
312                "This course requires authentication to access".to_string(),
313                None,
314            ));
315        }
316        skip_authorize()
317    } else {
318        // The course is publicly available, no need to authorize
319        skip_authorize()
320    };
321
322    Ok(token)
323}
324
325/** Checks the Authorization header against a secret from environment variables to verify if the request originates from the TMC server. Returns an authorization token if the secret matches, otherwise an unauthorized error.
326 */
327pub async fn authorize_access_from_tmc_server_to_course_mooc_fi(
328    request: &HttpRequest,
329) -> Result<AuthorizationToken, ControllerError> {
330    let tmc_server_secret_for_communicating_to_secret_project =
331        env::var("TMC_SERVER_SECRET_FOR_COMMUNICATING_TO_SECRET_PROJECT")
332            .expect("TMC_SERVER_SECRET_FOR_COMMUNICATING_TO_SECRET_PROJECT must be defined");
333    // check authorization header
334    let auth_header = request
335        .headers()
336        .get("Authorization")
337        .ok_or_else(|| {
338            ControllerError::new(
339                ControllerErrorType::Unauthorized,
340                "TMC server authorization failed: Missing Authorization header.".to_string(),
341                None,
342            )
343        })?
344        .to_str()
345        .map_err(|_| {
346            ControllerError::new(
347                ControllerErrorType::Unauthorized,
348                "TMC server authorization failed: Invalid Authorization header format.".to_string(),
349                None,
350            )
351        })?;
352    // If auth header correct one, grant access
353    if auth_header == tmc_server_secret_for_communicating_to_secret_project {
354        return Ok(skip_authorize());
355    }
356    Err(ControllerError::new(
357        ControllerErrorType::Unauthorized,
358        "TMC server authorization failed: Invalid authorization token.".to_string(),
359        None,
360    ))
361}
362
363/**  Can be used to check whether user is allowed to view some course material. Chapters can be closed and and limited to certain people only. */
364pub async fn can_user_view_chapter(
365    conn: &mut PgConnection,
366    user_id: Option<Uuid>,
367    course_id: Option<Uuid>,
368    chapter_id: Option<Uuid>,
369) -> Result<bool, ControllerError> {
370    if let Some(course_id) = course_id
371        && let Some(chapter_id) = chapter_id
372        && !models::chapters::is_open(&mut *conn, chapter_id).await?
373    {
374        if user_id.is_none() {
375            return Ok(false);
376        }
377        // If the user has been granted access to view the material, then they can see the unopened chapters too
378        // This is important because sometimes teachers wish to test unopened chapters with real students
379        let permission = authorize(conn, Act::ViewMaterial, user_id, Res::Course(course_id)).await;
380
381        return Ok(permission.is_ok());
382    }
383    Ok(true)
384}
385
386/**
387The authorization token is the only way to return a controller result, and should only be used in controller functions that return a response to the user.
388
389
390let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Page(*page_id)).await?;
391
392token.authorized_ok(web::Json(cms_page_info))
393
394
395*/
396pub async fn authorize(
397    conn: &mut PgConnection,
398    action: Action,
399    user_id: Option<Uuid>,
400    resource: Resource,
401) -> Result<AuthorizationToken, ControllerError> {
402    let user_roles = if let Some(user_id) = user_id {
403        models::roles::get_roles(conn, user_id)
404            .await
405            .map_err(|original_err| {
406                ControllerError::new(
407                    ControllerErrorType::InternalServerError,
408                    format!("Failed to fetch user roles: {}", original_err),
409                    Some(original_err.into()),
410                )
411            })?
412    } else {
413        Vec::new()
414    };
415
416    authorize_with_fetched_list_of_roles(conn, action, user_id, resource, &user_roles).await
417}
418
419/// Creates a ControllerError for authorization failures with more information in the source error
420fn create_authorization_error(user_roles: &[Role], action: Option<Action>) -> ControllerError {
421    let mut detail_message = String::new();
422
423    if user_roles.is_empty() {
424        detail_message.push_str("You don't have any assigned roles.");
425    } else {
426        detail_message.push_str("Your current roles are: ");
427        let roles_str = user_roles
428            .iter()
429            .map(|r| format!("{:?} ({})", r.role, r.domain_description()))
430            .collect::<Vec<_>>()
431            .join(", ");
432        detail_message.push_str(&roles_str);
433    }
434
435    if let Some(act) = action {
436        detail_message.push_str(&format!("\nAction attempted: {:?}", act));
437    }
438
439    // Create the controller error
440    ControllerError::new(
441        ControllerErrorType::Forbidden,
442        "Unauthorized. Please contact course staff if you believe you should have access."
443            .to_string(),
444        Some(ControllerError::new(ControllerErrorType::Forbidden, detail_message, None).into()),
445    )
446}
447
448/// Same as `authorize`, but takes as an argument `Vec<Role>` so that we avoid fetching the roles from the database for optimization reasons. This is useful when we're checking multiple authorizations at once.
449pub async fn authorize_with_fetched_list_of_roles(
450    conn: &mut PgConnection,
451    action: Action,
452    _user_id: Option<Uuid>,
453    resource: Resource,
454    user_roles: &[Role],
455) -> Result<AuthorizationToken, ControllerError> {
456    // check global role
457    for role in user_roles {
458        if role.is_global() && has_permission(role.role, action) {
459            return Ok(AuthorizationToken(()));
460        }
461    }
462
463    // for this resource, the domain of the role does not matter (e.g. organization role, course role, etc.)
464    if resource == Resource::AnyCourse {
465        for role in user_roles {
466            if has_permission(role.role, action) {
467                return Ok(AuthorizationToken(()));
468            }
469        }
470    }
471
472    // for some resources, we need to get more information from the database
473    match resource {
474        Resource::Chapter(id) => {
475            // if trying to View a chapter that is not open, check for permission to view the material
476            let action =
477                if matches!(action, Action::View) && !models::chapters::is_open(conn, id).await? {
478                    Action::ViewMaterial
479                } else {
480                    action
481                };
482            // there are no chapter roles so we check the course instead
483            let course_id = models::chapters::get_course_id(conn, id).await?;
484            check_course_permission(conn, user_roles, action, course_id).await
485        }
486        Resource::Course(id) => check_course_permission(conn, user_roles, action, id).await,
487        Resource::CourseInstance(id) => {
488            check_course_instance_permission(conn, user_roles, action, id).await
489        }
490        Resource::Exercise(id) => {
491            // an exercise can be part of a course or an exam
492            let course_or_exam_id = models::exercises::get_course_or_exam_id(conn, id).await?;
493            check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
494        }
495        Resource::ExerciseSlideSubmission(id) => {
496            //an exercise slide submissions can be part of a course or an exam
497            let course_or_exam_id =
498                models::exercise_slide_submissions::get_course_and_exam_id(conn, id).await?;
499            check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
500        }
501        Resource::ExerciseTask(id) => {
502            // an exercise task can be part of a course or an exam
503            let course_or_exam_id = models::exercise_tasks::get_course_or_exam_id(conn, id).await?;
504            check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
505        }
506        Resource::ExerciseTaskSubmission(id) => {
507            // an exercise task submission can be part of a course or an exam
508            let course_or_exam_id =
509                models::exercise_task_submissions::get_course_and_exam_id(conn, id).await?;
510            check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
511        }
512        Resource::ExerciseTaskGrading(id) => {
513            // a grading can be part of a course or an exam
514            let course_or_exam_id =
515                models::exercise_task_gradings::get_course_or_exam_id(conn, id).await?;
516            check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
517        }
518        Resource::Organization(id) => check_organization_permission(user_roles, action, id).await,
519        Resource::Page(id) => {
520            // a page can be part of a course or an exam
521            let course_or_exam_id = models::pages::get_course_and_exam_id(conn, id).await?;
522            check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
523        }
524        Resource::StudyRegistry(secret_key) => {
525            check_study_registry_permission(conn, secret_key, action).await
526        }
527        Resource::Exam(exam_id) => check_exam_permission(conn, user_roles, action, exam_id).await,
528        Resource::Role
529        | Resource::User
530        | Resource::AnyCourse
531        | Resource::PlaygroundExample
532        | Resource::ExerciseService
533        | Resource::GlobalPermissions => {
534            // permissions for these resources have already been checked
535            Err(create_authorization_error(user_roles, Some(action)))
536        }
537    }
538}
539
540async fn check_organization_permission(
541    roles: &[Role],
542    action: Action,
543    organization_id: Uuid,
544) -> Result<AuthorizationToken, ControllerError> {
545    if action == Action::View {
546        // anyone can view an organization regardless of roles
547        return Ok(AuthorizationToken(()));
548    };
549
550    // check organization role
551    for role in roles {
552        if role.is_role_for_organization(organization_id) && has_permission(role.role, action) {
553            return Ok(AuthorizationToken(()));
554        }
555    }
556    Err(create_authorization_error(roles, Some(action)))
557}
558
559/// Also checks organization role which is valid for courses.
560async fn check_course_permission(
561    conn: &mut PgConnection,
562    roles: &[Role],
563    action: Action,
564    course_id: Uuid,
565) -> Result<AuthorizationToken, ControllerError> {
566    // check course role
567    for role in roles {
568        if role.is_role_for_course(course_id) && has_permission(role.role, action) {
569            return Ok(AuthorizationToken(()));
570        }
571    }
572    let organization_id = models::courses::get_organization_id(conn, course_id).await?;
573    check_organization_permission(roles, action, organization_id).await
574}
575
576/// Also checks organization and course roles which are valid for course instances.
577async fn check_course_instance_permission(
578    conn: &mut PgConnection,
579    roles: &[Role],
580    mut action: Action,
581    course_instance_id: Uuid,
582) -> Result<AuthorizationToken, ControllerError> {
583    // if trying to View a course instance that is not open, we check for permission to Teach
584    if action == Action::View
585        && !models::course_instances::is_open(conn, course_instance_id).await?
586    {
587        action = Action::Teach;
588    }
589
590    // check course instance role
591    for role in roles {
592        if role.is_role_for_course_instance(course_instance_id) && has_permission(role.role, action)
593        {
594            return Ok(AuthorizationToken(()));
595        }
596    }
597    let course_id = models::course_instances::get_course_id(conn, course_instance_id).await?;
598    check_course_permission(conn, roles, action, course_id).await
599}
600
601/// Also checks organization role which is valid for exams.
602async fn check_exam_permission(
603    conn: &mut PgConnection,
604    roles: &[Role],
605    action: Action,
606    exam_id: Uuid,
607) -> Result<AuthorizationToken, ControllerError> {
608    // check exam role
609    for role in roles {
610        if role.is_role_for_exam(exam_id) && has_permission(role.role, action) {
611            return Ok(AuthorizationToken(()));
612        }
613    }
614    let organization_id = models::exams::get_organization_id(conn, exam_id).await?;
615    check_organization_permission(roles, action, organization_id).await
616}
617
618async fn check_course_or_exam_permission(
619    conn: &mut PgConnection,
620    roles: &[Role],
621    action: Action,
622    course_or_exam_id: CourseOrExamId,
623) -> Result<AuthorizationToken, ControllerError> {
624    match course_or_exam_id {
625        CourseOrExamId::Course(course_id) => {
626            check_course_permission(conn, roles, action, course_id).await
627        }
628        CourseOrExamId::Exam(exam_id) => check_exam_permission(conn, roles, action, exam_id).await,
629    }
630}
631
632async fn check_study_registry_permission(
633    conn: &mut PgConnection,
634    secret_key: String,
635    action: Action,
636) -> Result<AuthorizationToken, ControllerError> {
637    let _registrar = models::study_registry_registrars::get_by_secret_key(conn, &secret_key)
638        .await
639        .map_err(|original_error| {
640            ControllerError::new(
641                ControllerErrorType::Forbidden,
642                format!("Study registry access denied: Invalid or missing secret key. The operation {:?} cannot be performed.", action),
643                Some(original_error.into()),
644            )
645        })?;
646    Ok(AuthorizationToken(()))
647}
648
649// checks whether the role is allowed to perform the action
650fn has_permission(user_role: UserRole, action: Action) -> bool {
651    use Action::*;
652    use UserRole::*;
653
654    match user_role {
655        Admin => true,
656        Teacher => matches!(
657            action,
658            View | Teach
659                | Edit
660                | Grade
661                | Duplicate
662                | DeleteAnswer
663                | EditRole(Teacher | Assistant | Reviewer | MaterialViewer | StatsViewer)
664                | CreateCoursesOrExams
665                | ViewMaterial
666                | UploadFile
667                | ViewUserProgressOrDetails
668                | ViewInternalCourseStructure
669                | ViewStats
670        ),
671        Assistant => matches!(
672            action,
673            View | Edit
674                | Grade
675                | DeleteAnswer
676                | EditRole(Assistant | Reviewer | MaterialViewer)
677                | Teach
678                | ViewMaterial
679                | ViewUserProgressOrDetails
680                | ViewInternalCourseStructure
681        ),
682        Reviewer => matches!(
683            action,
684            View | Grade | ViewMaterial | ViewInternalCourseStructure
685        ),
686        CourseOrExamCreator => matches!(action, CreateCoursesOrExams),
687        MaterialViewer => matches!(action, ViewMaterial),
688        TeachingAndLearningServices => {
689            matches!(
690                action,
691                View | ViewMaterial
692                    | ViewUserProgressOrDetails
693                    | ViewInternalCourseStructure
694                    | ViewStats
695            )
696        }
697        StatsViewer => matches!(action, ViewStats),
698    }
699}
700
701pub fn parse_secret_key_from_header(header: &HttpRequest) -> Result<&str, ControllerError> {
702    let raw_token = header
703        .headers()
704        .get("Authorization")
705        .map_or(Ok(""), |x| x.to_str())
706        .map_err(|_| anyhow::anyhow!("Authorization header contains invalid characters."))?;
707    if !raw_token.starts_with("Basic") {
708        return Err(ControllerError::new(
709            ControllerErrorType::Forbidden,
710            "Access denied: Authorization header must use Basic authentication format.".to_string(),
711            None,
712        ));
713    }
714    let secret_key = raw_token.split(' ').nth(1).ok_or_else(|| {
715        ControllerError::new(
716            ControllerErrorType::Forbidden,
717            "Access denied: Malformed authorization token, expected 'Basic <token>' format."
718                .to_string(),
719            None,
720        )
721    })?;
722    Ok(secret_key)
723}
724
725/// Authenticates the user with mooc.fi, returning the authenticated user and their oauth token.
726pub async fn authenticate_moocfi_user(
727    conn: &mut PgConnection,
728    client: &OAuthClient,
729    email: String,
730    password: String,
731    tmc_client: &TmcClient,
732) -> anyhow::Result<Option<(User, SecretString)>> {
733    info!("Attempting to authenticate user with TMC");
734    let token = match exchange_password_with_tmc(client, email.clone(), password).await? {
735        Some(token) => token,
736        None => return Ok(None),
737    };
738    debug!("Successfully obtained OAuth token from TMC");
739
740    let tmc_user = tmc_client
741        .get_user_from_tmc_mooc_fi_by_tmc_access_token(&token.clone())
742        .await?;
743    debug!(
744        "Creating or fetching user with TMC id {} and mooc.fi UUID {}",
745        tmc_user.id,
746        tmc_user
747            .courses_mooc_fi_user_id
748            .map(|uuid| uuid.to_string())
749            .unwrap_or_else(|| "None (will generate new UUID)".to_string())
750    );
751    let user = get_or_create_user_from_tmc_mooc_fi_response(&mut *conn, tmc_user).await?;
752    info!(
753        "Successfully got user details from mooc.fi for user {}",
754        user.id
755    );
756    info!("Successfully authenticated user {} with mooc.fi", user.id);
757    Ok(Some((user, token)))
758}
759
760pub type LoginToken = StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>;
761
762/**
763Exchanges user credentials with TMC server to obtain an OAuth token.
764
765This function attempts to authenticate a user with the TMC server using their email and password.
766It returns different results based on the authentication outcome:
767
768- `Ok(Some(token))` - Authentication successful, returns the OAuth token
769- `Ok(None)` - Authentication failed due to invalid credentials (email/password)
770- `Err(...)` - Authentication failed due to other errors (server issues, network problems, etc.)
771*/
772pub async fn exchange_password_with_tmc(
773    client: &OAuthClient,
774    email: String,
775    password: String,
776) -> anyhow::Result<Option<SecretString>> {
777    let token_result = client
778        .exchange_password(
779            &ResourceOwnerUsername::new(email),
780            &ResourceOwnerPassword::new(password),
781        )
782        .request_async(&async_http_client_with_headers)
783        .await;
784    match token_result {
785        Ok(token) => Ok(Some(SecretString::new(
786            token.access_token().secret().to_owned().into(),
787        ))),
788        Err(RequestTokenError::ServerResponse(server_response)) => {
789            let error = server_response.error();
790            let error_description = server_response.error_description();
791            let error_uri = server_response.error_uri();
792
793            // Only return Ok(None) for InvalidGrant errors (wrong email/password)
794            if let oauth2::basic::BasicErrorResponseType::InvalidGrant = error {
795                warn!(
796                    ?error_description,
797                    ?error_uri,
798                    "TMC did not accept the credentials: {}",
799                    error
800                );
801                Ok(None)
802            } else {
803                // For all other error types, return an error
804                error!(
805                    ?error_description,
806                    ?error_uri,
807                    "TMC authentication error: {}",
808                    error
809                );
810                Err(anyhow::anyhow!("Authentication error: {}", error))
811            }
812        }
813        Err(e) => {
814            error!("Failed to exchange password with TMC: {}", e);
815            Err(e.into())
816        }
817    }
818}
819
820pub async fn get_or_create_user_from_tmc_mooc_fi_response(
821    conn: &mut PgConnection,
822    tmc_mooc_fi_user: TMCUser,
823) -> anyhow::Result<User> {
824    let TMCUser {
825        id: upstream_id,
826        email,
827        courses_mooc_fi_user_id: moocfi_id,
828        user_field,
829        ..
830    } = tmc_mooc_fi_user;
831
832    let id = moocfi_id.unwrap_or_else(Uuid::new_v4);
833
834    // fetch existing user or create new one
835    let user = match models::users::find_by_upstream_id(conn, upstream_id).await? {
836        Some(existing_user) => existing_user,
837        None => {
838            models::users::insert_with_upstream_id_and_moocfi_id(
839                conn,
840                &email,
841                // convert empty names to None
842                if user_field.first_name.trim().is_empty() {
843                    None
844                } else {
845                    Some(user_field.first_name.as_str())
846                },
847                if user_field.last_name.trim().is_empty() {
848                    None
849                } else {
850                    Some(user_field.last_name.as_str())
851                },
852                upstream_id,
853                id,
854            )
855            .await?
856        }
857    };
858    Ok(user)
859}
860
861/// Authenticates a test user with predefined credentials.
862/// Returns Ok(true) if authentication succeeds, Ok(false) if credentials are incorrect,
863/// and Err for other errors.
864pub async fn authenticate_test_user(
865    conn: &mut PgConnection,
866    email: &str,
867    password: &str,
868    application_configuration: &ApplicationConfiguration,
869) -> anyhow::Result<bool> {
870    // Sanity check to ensure this is not called outside of test mode. The whole application configuration is passed to this function instead of just the boolean to make mistakes harder.
871    assert!(application_configuration.test_mode);
872
873    let _user = if email == "admin@example.com" && password == "admin" {
874        models::users::get_by_email(conn, "admin@example.com").await?
875    } else if email == "teacher@example.com" && password == "teacher" {
876        models::users::get_by_email(conn, "teacher@example.com").await?
877    } else if email == "language.teacher@example.com" && password == "language.teacher" {
878        models::users::get_by_email(conn, "language.teacher@example.com").await?
879    } else if email == "material.viewer@example.com" && password == "material.viewer" {
880        models::users::get_by_email(conn, "material.viewer@example.com").await?
881    } else if email == "user@example.com" && password == "user" {
882        models::users::get_by_email(conn, "user@example.com").await?
883    } else if email == "assistant@example.com" && password == "assistant" {
884        models::users::get_by_email(conn, "assistant@example.com").await?
885    } else if email == "creator@example.com" && password == "creator" {
886        models::users::get_by_email(conn, "creator@example.com").await?
887    } else if email == "student1@example.com" && password == "student1" {
888        models::users::get_by_email(conn, "student1@example.com").await?
889    } else if email == "student2@example.com" && password == "student2" {
890        models::users::get_by_email(conn, "student2@example.com").await?
891    } else if email == "student3@example.com" && password == "student3" {
892        models::users::get_by_email(conn, "student3@example.com").await?
893    } else if email == "student4@example.com" && password == "student4" {
894        models::users::get_by_email(conn, "student4@example.com").await?
895    } else if email == "student5@example.com" && password == "student5" {
896        models::users::get_by_email(conn, "student5@example.com").await?
897    } else if email == "student6@example.com" && password == "student6" {
898        models::users::get_by_email(conn, "student6@example.com").await?
899    } else if email == "student7@example.com" && password == "student7" {
900        models::users::get_by_email(conn, "student7@example.com").await?
901    } else if email == "student8@example.com" && password == "student8" {
902        models::users::get_by_email(conn, "student8@example.com").await?
903    } else if email == "teaching-and-learning-services@example.com"
904        && password == "teaching-and-learning-services"
905    {
906        models::users::get_by_email(conn, "teaching-and-learning-services@example.com").await?
907    } else if email == "student-without-research-consent@example.com"
908        && password == "student-without-research-consent"
909    {
910        models::users::get_by_email(conn, "student-without-research-consent@example.com").await?
911    } else if email == "student-without-country@example.com"
912        && password == "student-without-country"
913    {
914        models::users::get_by_email(conn, "student-without-country@example.com").await?
915    } else if email == "langs@example.com" && password == "langs" {
916        models::users::get_by_email(conn, "langs@example.com").await?
917    } else if email == "sign-up-user@example.com" && password == "sign-up-user" {
918        models::users::get_by_email(conn, "sign-up-user@example.com").await?
919    } else {
920        info!("Authentication failed: incorrect test credentials");
921        return Ok(false);
922    };
923    info!("Successfully authenticated test user {}", email);
924    Ok(true)
925}
926
927// Only used for testing, not to use in production.
928pub async fn authenticate_test_token(
929    conn: &mut PgConnection,
930    _token: &SecretString,
931    application_configuration: &ApplicationConfiguration,
932) -> anyhow::Result<User> {
933    // Sanity check to ensure this is not called outside of test mode. The whole application configuration is passed to this function instead of just the boolean to make mistakes harder.
934    assert!(application_configuration.test_mode);
935    // TODO: this has never worked
936    let user = models::users::get_by_email(conn, "TODO").await?;
937    Ok(user)
938}
939
940/**
941 Gets the rate limit protection API key from environment variables and converts it to a header value.
942 This key is used to bypass rate limiting when making requests to TMC server.
943*/
944fn get_ratelimit_api_key() -> Result<reqwest::header::HeaderValue, HttpClientError<reqwest::Error>>
945{
946    let key = match std::env::var("RATELIMIT_PROTECTION_SAFE_API_KEY") {
947        Ok(key) => {
948            debug!("Found RATELIMIT_PROTECTION_SAFE_API_KEY");
949            key
950        }
951        Err(e) => {
952            error!(
953                "RATELIMIT_PROTECTION_SAFE_API_KEY environment variable not set: {}",
954                e
955            );
956            return Err(HttpClientError::Other(
957                "RATELIMIT_PROTECTION_SAFE_API_KEY must be defined".to_string(),
958            ));
959        }
960    };
961
962    key.parse::<reqwest::header::HeaderValue>().map_err(|err| {
963        error!("Invalid RATELIMIT API key format: {}", err);
964        HttpClientError::Other("Invalid RATELIMIT API key.".to_string())
965    })
966}
967
968/**
969 HTTP Client used only for authenticating with TMC server. This function:
970 1. Ensures TMC server does not rate limit auth requests from backend by adding a special header
971 2. Converts between oauth2 crate's internal http types and our reqwest types:
972    - Converts oauth2::HttpRequest to a reqwest::Request
973    - Makes the request using our REQWEST_CLIENT
974    - Converts the reqwest::Response back to oauth2::HttpResponse
975*/
976async fn async_http_client_with_headers(
977    oauth_request: oauth2::HttpRequest,
978) -> Result<oauth2::HttpResponse, HttpClientError<reqwest::Error>> {
979    debug!("Making OAuth request to TMC server");
980
981    if log::log_enabled!(log::Level::Trace) {
982        // Only log the URL path, not query parameters which may contain credentials
983        if let Ok(url) = oauth_request.uri().to_string().parse::<reqwest::Url>() {
984            trace!("OAuth request path: {}", url.path());
985        }
986    }
987
988    let parsed_key = get_ratelimit_api_key()?;
989
990    debug!("Building request to TMC server");
991    let request = REQWEST_CLIENT
992        .request(
993            oauth_request.method().clone(),
994            oauth_request
995                .uri()
996                .to_string()
997                .parse::<reqwest::Url>()
998                .map_err(|e| HttpClientError::Other(format!("Invalid URL: {}", e)))?,
999        )
1000        .headers(oauth_request.headers().clone())
1001        .version(oauth_request.version())
1002        .header("RATELIMIT-PROTECTION-SAFE-API-KEY", parsed_key)
1003        .body(oauth_request.body().to_vec());
1004
1005    debug!("Sending request to TMC server");
1006    let response = request
1007        .send()
1008        .await
1009        .map_err(|e| HttpClientError::Other(format!("Failed to execute request: {}", e)))?;
1010
1011    // Log response status and version, but not headers or body which may contain tokens
1012    debug!(
1013        "Received response from TMC server - Status: {}, Version: {:?}",
1014        response.status(),
1015        response.version()
1016    );
1017
1018    let status = response.status();
1019    let version = response.version();
1020    let headers = response.headers().clone();
1021
1022    debug!("Reading response body");
1023    let body_bytes = response
1024        .bytes()
1025        .await
1026        .map_err(|e| HttpClientError::Other(format!("Failed to read response body: {}", e)))?
1027        .to_vec();
1028
1029    debug!("Building OAuth response");
1030    let mut builder = oauth2::http::Response::builder()
1031        .status(status)
1032        .version(version);
1033
1034    if let Some(builder_headers) = builder.headers_mut() {
1035        builder_headers.extend(headers.iter().map(|(k, v)| (k.clone(), v.clone())));
1036    }
1037
1038    let oauth_response = builder
1039        .body(body_bytes)
1040        .map_err(|e| HttpClientError::Other(format!("Failed to construct response: {}", e)))?;
1041
1042    debug!("Successfully completed OAuth request");
1043    Ok(oauth_response)
1044}
1045
1046#[cfg(test)]
1047mod test {
1048    use super::*;
1049    use crate::test_helper::*;
1050    use headless_lms_models::*;
1051    use models::roles::RoleDomain;
1052
1053    #[actix_web::test]
1054    async fn test_authorization() {
1055        let mut conn = Conn::init().await;
1056        let mut tx = conn.begin().await;
1057
1058        let user = users::insert(
1059            tx.as_mut(),
1060            PKeyPolicy::Generate,
1061            "auth@example.com",
1062            None,
1063            None,
1064        )
1065        .await
1066        .unwrap();
1067        let org = organizations::insert(
1068            tx.as_mut(),
1069            PKeyPolicy::Generate,
1070            "auth",
1071            "auth",
1072            Some("auth"),
1073            false,
1074        )
1075        .await
1076        .unwrap();
1077
1078        authorize(
1079            tx.as_mut(),
1080            Action::Edit,
1081            Some(user),
1082            Resource::Organization(org),
1083        )
1084        .await
1085        .unwrap_err();
1086
1087        roles::insert(
1088            tx.as_mut(),
1089            user,
1090            UserRole::Teacher,
1091            RoleDomain::Organization(org),
1092        )
1093        .await
1094        .unwrap();
1095
1096        authorize(
1097            tx.as_mut(),
1098            Action::Edit,
1099            Some(user),
1100            Resource::Organization(org),
1101        )
1102        .await
1103        .unwrap();
1104    }
1105
1106    #[actix_web::test]
1107    async fn course_role_chapter_resource() {
1108        insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module, :chapter);
1109
1110        authorize(
1111            tx.as_mut(),
1112            Action::Edit,
1113            Some(user),
1114            Resource::Chapter(chapter),
1115        )
1116        .await
1117        .unwrap_err();
1118
1119        roles::insert(
1120            tx.as_mut(),
1121            user,
1122            UserRole::Teacher,
1123            RoleDomain::Course(course),
1124        )
1125        .await
1126        .unwrap();
1127
1128        authorize(
1129            tx.as_mut(),
1130            Action::Edit,
1131            Some(user),
1132            Resource::Chapter(chapter),
1133        )
1134        .await
1135        .unwrap();
1136    }
1137
1138    #[actix_web::test]
1139    async fn anonymous_user_can_view_open_course() {
1140        insert_data!(:tx, :user, :org, :course);
1141
1142        authorize(tx.as_mut(), Action::View, None, Resource::Course(course))
1143            .await
1144            .unwrap();
1145    }
1146}