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 == "teaching-and-learning-services@example.com"
898        && password == "teaching-and-learning-services"
899    {
900        models::users::get_by_email(conn, "teaching-and-learning-services@example.com").await?
901    } else if email == "student-without-research-consent@example.com"
902        && password == "student-without-research-consent"
903    {
904        models::users::get_by_email(conn, "student-without-research-consent@example.com").await?
905    } else if email == "student-without-country@example.com"
906        && password == "student-without-country"
907    {
908        models::users::get_by_email(conn, "student-without-country@example.com").await?
909    } else if email == "langs@example.com" && password == "langs" {
910        models::users::get_by_email(conn, "langs@example.com").await?
911    } else if email == "sign-up-user@example.com" && password == "sign-up-user" {
912        models::users::get_by_email(conn, "sign-up-user@example.com").await?
913    } else {
914        info!("Authentication failed: incorrect test credentials");
915        return Ok(false);
916    };
917    info!("Successfully authenticated test user {}", email);
918    Ok(true)
919}
920
921// Only used for testing, not to use in production.
922pub async fn authenticate_test_token(
923    conn: &mut PgConnection,
924    _token: &SecretString,
925    application_configuration: &ApplicationConfiguration,
926) -> anyhow::Result<User> {
927    // 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.
928    assert!(application_configuration.test_mode);
929    // TODO: this has never worked
930    let user = models::users::get_by_email(conn, "TODO").await?;
931    Ok(user)
932}
933
934/**
935 Gets the rate limit protection API key from environment variables and converts it to a header value.
936 This key is used to bypass rate limiting when making requests to TMC server.
937*/
938fn get_ratelimit_api_key() -> Result<reqwest::header::HeaderValue, HttpClientError<reqwest::Error>>
939{
940    let key = match std::env::var("RATELIMIT_PROTECTION_SAFE_API_KEY") {
941        Ok(key) => {
942            debug!("Found RATELIMIT_PROTECTION_SAFE_API_KEY");
943            key
944        }
945        Err(e) => {
946            error!(
947                "RATELIMIT_PROTECTION_SAFE_API_KEY environment variable not set: {}",
948                e
949            );
950            return Err(HttpClientError::Other(
951                "RATELIMIT_PROTECTION_SAFE_API_KEY must be defined".to_string(),
952            ));
953        }
954    };
955
956    key.parse::<reqwest::header::HeaderValue>().map_err(|err| {
957        error!("Invalid RATELIMIT API key format: {}", err);
958        HttpClientError::Other("Invalid RATELIMIT API key.".to_string())
959    })
960}
961
962/**
963 HTTP Client used only for authenticating with TMC server. This function:
964 1. Ensures TMC server does not rate limit auth requests from backend by adding a special header
965 2. Converts between oauth2 crate's internal http types and our reqwest types:
966    - Converts oauth2::HttpRequest to a reqwest::Request
967    - Makes the request using our REQWEST_CLIENT
968    - Converts the reqwest::Response back to oauth2::HttpResponse
969*/
970async fn async_http_client_with_headers(
971    oauth_request: oauth2::HttpRequest,
972) -> Result<oauth2::HttpResponse, HttpClientError<reqwest::Error>> {
973    debug!("Making OAuth request to TMC server");
974
975    if log::log_enabled!(log::Level::Trace) {
976        // Only log the URL path, not query parameters which may contain credentials
977        if let Ok(url) = oauth_request.uri().to_string().parse::<reqwest::Url>() {
978            trace!("OAuth request path: {}", url.path());
979        }
980    }
981
982    let parsed_key = get_ratelimit_api_key()?;
983
984    debug!("Building request to TMC server");
985    let request = REQWEST_CLIENT
986        .request(
987            oauth_request.method().clone(),
988            oauth_request
989                .uri()
990                .to_string()
991                .parse::<reqwest::Url>()
992                .map_err(|e| HttpClientError::Other(format!("Invalid URL: {}", e)))?,
993        )
994        .headers(oauth_request.headers().clone())
995        .version(oauth_request.version())
996        .header("RATELIMIT-PROTECTION-SAFE-API-KEY", parsed_key)
997        .body(oauth_request.body().to_vec());
998
999    debug!("Sending request to TMC server");
1000    let response = request
1001        .send()
1002        .await
1003        .map_err(|e| HttpClientError::Other(format!("Failed to execute request: {}", e)))?;
1004
1005    // Log response status and version, but not headers or body which may contain tokens
1006    debug!(
1007        "Received response from TMC server - Status: {}, Version: {:?}",
1008        response.status(),
1009        response.version()
1010    );
1011
1012    let status = response.status();
1013    let version = response.version();
1014    let headers = response.headers().clone();
1015
1016    debug!("Reading response body");
1017    let body_bytes = response
1018        .bytes()
1019        .await
1020        .map_err(|e| HttpClientError::Other(format!("Failed to read response body: {}", e)))?
1021        .to_vec();
1022
1023    debug!("Building OAuth response");
1024    let mut builder = oauth2::http::Response::builder()
1025        .status(status)
1026        .version(version);
1027
1028    if let Some(builder_headers) = builder.headers_mut() {
1029        builder_headers.extend(headers.iter().map(|(k, v)| (k.clone(), v.clone())));
1030    }
1031
1032    let oauth_response = builder
1033        .body(body_bytes)
1034        .map_err(|e| HttpClientError::Other(format!("Failed to construct response: {}", e)))?;
1035
1036    debug!("Successfully completed OAuth request");
1037    Ok(oauth_response)
1038}
1039
1040#[cfg(test)]
1041mod test {
1042    use super::*;
1043    use crate::test_helper::*;
1044    use headless_lms_models::*;
1045    use models::roles::RoleDomain;
1046
1047    #[actix_web::test]
1048    async fn test_authorization() {
1049        let mut conn = Conn::init().await;
1050        let mut tx = conn.begin().await;
1051
1052        let user = users::insert(
1053            tx.as_mut(),
1054            PKeyPolicy::Generate,
1055            "auth@example.com",
1056            None,
1057            None,
1058        )
1059        .await
1060        .unwrap();
1061        let org = organizations::insert(
1062            tx.as_mut(),
1063            PKeyPolicy::Generate,
1064            "auth",
1065            "auth",
1066            Some("auth"),
1067            false,
1068        )
1069        .await
1070        .unwrap();
1071
1072        authorize(
1073            tx.as_mut(),
1074            Action::Edit,
1075            Some(user),
1076            Resource::Organization(org),
1077        )
1078        .await
1079        .unwrap_err();
1080
1081        roles::insert(
1082            tx.as_mut(),
1083            user,
1084            UserRole::Teacher,
1085            RoleDomain::Organization(org),
1086        )
1087        .await
1088        .unwrap();
1089
1090        authorize(
1091            tx.as_mut(),
1092            Action::Edit,
1093            Some(user),
1094            Resource::Organization(org),
1095        )
1096        .await
1097        .unwrap();
1098    }
1099
1100    #[actix_web::test]
1101    async fn course_role_chapter_resource() {
1102        insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module, :chapter);
1103
1104        authorize(
1105            tx.as_mut(),
1106            Action::Edit,
1107            Some(user),
1108            Resource::Chapter(chapter),
1109        )
1110        .await
1111        .unwrap_err();
1112
1113        roles::insert(
1114            tx.as_mut(),
1115            user,
1116            UserRole::Teacher,
1117            RoleDomain::Course(course),
1118        )
1119        .await
1120        .unwrap();
1121
1122        authorize(
1123            tx.as_mut(),
1124            Action::Edit,
1125            Some(user),
1126            Resource::Chapter(chapter),
1127        )
1128        .await
1129        .unwrap();
1130    }
1131
1132    #[actix_web::test]
1133    async fn anonymous_user_can_view_open_course() {
1134        insert_data!(:tx, :user, :org, :course);
1135
1136        authorize(tx.as_mut(), Action::View, None, Resource::Course(course))
1137            .await
1138            .unwrap();
1139    }
1140}