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 models::join_code_uses::check_if_user_has_access_to_course(
299            conn,
300            user_id.unwrap(),
301            course_id,
302        )
303        .await
304        .is_err()
305        {
306            authorize(conn, Act::ViewMaterial, user_id, Res::Course(course_id)).await?;
307        }
308        skip_authorize()
309    } else {
310        // The course is publicly available, no need to authorize
311        skip_authorize()
312    };
313
314    Ok(token)
315}
316
317/** 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.
318 */
319pub async fn authorize_access_from_tmc_server_to_course_mooc_fi(
320    request: &HttpRequest,
321) -> Result<AuthorizationToken, ControllerError> {
322    let tmc_server_secret_for_communicating_to_secret_project =
323        env::var("TMC_SERVER_SECRET_FOR_COMMUNICATING_TO_SECRET_PROJECT")
324            .expect("TMC_SERVER_SECRET_FOR_COMMUNICATING_TO_SECRET_PROJECT must be defined");
325    // check authorization header
326    let auth_header = request
327        .headers()
328        .get("Authorization")
329        .ok_or_else(|| {
330            ControllerError::new(
331                ControllerErrorType::Unauthorized,
332                "TMC server authorization failed: Missing Authorization header.".to_string(),
333                None,
334            )
335        })?
336        .to_str()
337        .map_err(|_| {
338            ControllerError::new(
339                ControllerErrorType::Unauthorized,
340                "TMC server authorization failed: Invalid Authorization header format.".to_string(),
341                None,
342            )
343        })?;
344    // If auth header correct one, grant access
345    if auth_header == tmc_server_secret_for_communicating_to_secret_project {
346        return Ok(skip_authorize());
347    }
348    Err(ControllerError::new(
349        ControllerErrorType::Unauthorized,
350        "TMC server authorization failed: Invalid authorization token.".to_string(),
351        None,
352    ))
353}
354
355/**  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. */
356pub async fn can_user_view_chapter(
357    conn: &mut PgConnection,
358    user_id: Option<Uuid>,
359    course_id: Option<Uuid>,
360    chapter_id: Option<Uuid>,
361) -> Result<bool, ControllerError> {
362    if let Some(course_id) = course_id {
363        if let Some(chapter_id) = chapter_id {
364            if !models::chapters::is_open(&mut *conn, chapter_id).await? {
365                if user_id.is_none() {
366                    return Ok(false);
367                }
368                // If the user has been granted access to view the material, then they can see the unopened chapters too
369                // This is important because sometimes teachers wish to test unopened chapters with real students
370                let permission =
371                    authorize(conn, Act::ViewMaterial, user_id, Res::Course(course_id)).await;
372
373                return Ok(permission.is_ok());
374            }
375        }
376    }
377    Ok(true)
378}
379
380/**
381The 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.
382
383
384let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Page(*page_id)).await?;
385
386token.authorized_ok(web::Json(cms_page_info))
387
388
389*/
390pub async fn authorize(
391    conn: &mut PgConnection,
392    action: Action,
393    user_id: Option<Uuid>,
394    resource: Resource,
395) -> Result<AuthorizationToken, ControllerError> {
396    let user_roles = if let Some(user_id) = user_id {
397        models::roles::get_roles(conn, user_id)
398            .await
399            .map_err(|original_err| {
400                ControllerError::new(
401                    ControllerErrorType::InternalServerError,
402                    format!("Failed to fetch user roles: {}", original_err),
403                    Some(original_err.into()),
404                )
405            })?
406    } else {
407        Vec::new()
408    };
409
410    authorize_with_fetched_list_of_roles(conn, action, user_id, resource, &user_roles).await
411}
412
413/// Creates a ControllerError for authorization failures with more information in the source error
414fn create_authorization_error(user_roles: &[Role], action: Option<Action>) -> ControllerError {
415    let mut detail_message = String::new();
416
417    if user_roles.is_empty() {
418        detail_message.push_str("You don't have any assigned roles.");
419    } else {
420        detail_message.push_str("Your current roles are: ");
421        let roles_str = user_roles
422            .iter()
423            .map(|r| format!("{:?} ({})", r.role, r.domain_description()))
424            .collect::<Vec<_>>()
425            .join(", ");
426        detail_message.push_str(&roles_str);
427    }
428
429    if let Some(act) = action {
430        detail_message.push_str(&format!("\nAction attempted: {:?}", act));
431    }
432
433    // Create the controller error
434    ControllerError::new(
435        ControllerErrorType::Forbidden,
436        "Unauthorized. Please contact course staff if you believe you should have access."
437            .to_string(),
438        Some(ControllerError::new(ControllerErrorType::Forbidden, detail_message, None).into()),
439    )
440}
441
442/// 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.
443pub async fn authorize_with_fetched_list_of_roles(
444    conn: &mut PgConnection,
445    action: Action,
446    _user_id: Option<Uuid>,
447    resource: Resource,
448    user_roles: &[Role],
449) -> Result<AuthorizationToken, ControllerError> {
450    // check global role
451    for role in user_roles {
452        if role.is_global() && has_permission(role.role, action) {
453            return Ok(AuthorizationToken(()));
454        }
455    }
456
457    // for this resource, the domain of the role does not matter (e.g. organization role, course role, etc.)
458    if resource == Resource::AnyCourse {
459        for role in user_roles {
460            if has_permission(role.role, action) {
461                return Ok(AuthorizationToken(()));
462            }
463        }
464    }
465
466    // for some resources, we need to get more information from the database
467    match resource {
468        Resource::Chapter(id) => {
469            // if trying to View a chapter that is not open, check for permission to view the material
470            let action =
471                if matches!(action, Action::View) && !models::chapters::is_open(conn, id).await? {
472                    Action::ViewMaterial
473                } else {
474                    action
475                };
476            // there are no chapter roles so we check the course instead
477            let course_id = models::chapters::get_course_id(conn, id).await?;
478            check_course_permission(conn, user_roles, action, course_id).await
479        }
480        Resource::Course(id) => check_course_permission(conn, user_roles, action, id).await,
481        Resource::CourseInstance(id) => {
482            check_course_instance_permission(conn, user_roles, action, id).await
483        }
484        Resource::Exercise(id) => {
485            // an exercise can be part of a course or an exam
486            let course_or_exam_id = models::exercises::get_course_or_exam_id(conn, id).await?;
487            check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
488        }
489        Resource::ExerciseSlideSubmission(id) => {
490            //an exercise slide submissions can be part of a course or an exam
491            let course_or_exam_id =
492                models::exercise_slide_submissions::get_course_and_exam_id(conn, id).await?;
493            check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
494        }
495        Resource::ExerciseTask(id) => {
496            // an exercise task can be part of a course or an exam
497            let course_or_exam_id = models::exercise_tasks::get_course_or_exam_id(conn, id).await?;
498            check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
499        }
500        Resource::ExerciseTaskSubmission(id) => {
501            // an exercise task submission can be part of a course or an exam
502            let course_or_exam_id =
503                models::exercise_task_submissions::get_course_and_exam_id(conn, id).await?;
504            check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
505        }
506        Resource::ExerciseTaskGrading(id) => {
507            // a grading can be part of a course or an exam
508            let course_or_exam_id =
509                models::exercise_task_gradings::get_course_or_exam_id(conn, id).await?;
510            check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
511        }
512        Resource::Organization(id) => check_organization_permission(user_roles, action, id).await,
513        Resource::Page(id) => {
514            // a page can be part of a course or an exam
515            let course_or_exam_id = models::pages::get_course_and_exam_id(conn, id).await?;
516            check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
517        }
518        Resource::StudyRegistry(secret_key) => {
519            check_study_registry_permission(conn, secret_key, action).await
520        }
521        Resource::Exam(exam_id) => check_exam_permission(conn, user_roles, action, exam_id).await,
522        Resource::Role
523        | Resource::User
524        | Resource::AnyCourse
525        | Resource::PlaygroundExample
526        | Resource::ExerciseService
527        | Resource::GlobalPermissions => {
528            // permissions for these resources have already been checked
529            Err(create_authorization_error(user_roles, Some(action)))
530        }
531    }
532}
533
534async fn check_organization_permission(
535    roles: &[Role],
536    action: Action,
537    organization_id: Uuid,
538) -> Result<AuthorizationToken, ControllerError> {
539    if action == Action::View {
540        // anyone can view an organization regardless of roles
541        return Ok(AuthorizationToken(()));
542    };
543
544    // check organization role
545    for role in roles {
546        if role.is_role_for_organization(organization_id) && has_permission(role.role, action) {
547            return Ok(AuthorizationToken(()));
548        }
549    }
550    Err(create_authorization_error(roles, Some(action)))
551}
552
553/// Also checks organization role which is valid for courses.
554async fn check_course_permission(
555    conn: &mut PgConnection,
556    roles: &[Role],
557    action: Action,
558    course_id: Uuid,
559) -> Result<AuthorizationToken, ControllerError> {
560    // check course role
561    for role in roles {
562        if role.is_role_for_course(course_id) && has_permission(role.role, action) {
563            return Ok(AuthorizationToken(()));
564        }
565    }
566    let organization_id = models::courses::get_organization_id(conn, course_id).await?;
567    check_organization_permission(roles, action, organization_id).await
568}
569
570/// Also checks organization and course roles which are valid for course instances.
571async fn check_course_instance_permission(
572    conn: &mut PgConnection,
573    roles: &[Role],
574    mut action: Action,
575    course_instance_id: Uuid,
576) -> Result<AuthorizationToken, ControllerError> {
577    // if trying to View a course instance that is not open, we check for permission to Teach
578    if action == Action::View
579        && !models::course_instances::is_open(conn, course_instance_id).await?
580    {
581        action = Action::Teach;
582    }
583
584    // check course instance role
585    for role in roles {
586        if role.is_role_for_course_instance(course_instance_id) && has_permission(role.role, action)
587        {
588            return Ok(AuthorizationToken(()));
589        }
590    }
591    let course_id = models::course_instances::get_course_id(conn, course_instance_id).await?;
592    check_course_permission(conn, roles, action, course_id).await
593}
594
595/// Also checks organization role which is valid for exams.
596async fn check_exam_permission(
597    conn: &mut PgConnection,
598    roles: &[Role],
599    action: Action,
600    exam_id: Uuid,
601) -> Result<AuthorizationToken, ControllerError> {
602    // check exam role
603    for role in roles {
604        if role.is_role_for_exam(exam_id) && has_permission(role.role, action) {
605            return Ok(AuthorizationToken(()));
606        }
607    }
608    let organization_id = models::exams::get_organization_id(conn, exam_id).await?;
609    check_organization_permission(roles, action, organization_id).await
610}
611
612async fn check_course_or_exam_permission(
613    conn: &mut PgConnection,
614    roles: &[Role],
615    action: Action,
616    course_or_exam_id: CourseOrExamId,
617) -> Result<AuthorizationToken, ControllerError> {
618    match course_or_exam_id {
619        CourseOrExamId::Course(course_id) => {
620            check_course_permission(conn, roles, action, course_id).await
621        }
622        CourseOrExamId::Exam(exam_id) => check_exam_permission(conn, roles, action, exam_id).await,
623    }
624}
625
626async fn check_study_registry_permission(
627    conn: &mut PgConnection,
628    secret_key: String,
629    action: Action,
630) -> Result<AuthorizationToken, ControllerError> {
631    let _registrar = models::study_registry_registrars::get_by_secret_key(conn, &secret_key)
632        .await
633        .map_err(|original_error| {
634            ControllerError::new(
635                ControllerErrorType::Forbidden,
636                format!("Study registry access denied: Invalid or missing secret key. The operation {:?} cannot be performed.", action),
637                Some(original_error.into()),
638            )
639        })?;
640    Ok(AuthorizationToken(()))
641}
642
643// checks whether the role is allowed to perform the action
644fn has_permission(user_role: UserRole, action: Action) -> bool {
645    use Action::*;
646    use UserRole::*;
647
648    match user_role {
649        Admin => true,
650        Teacher => matches!(
651            action,
652            View | Teach
653                | Edit
654                | Grade
655                | Duplicate
656                | DeleteAnswer
657                | EditRole(Teacher | Assistant | Reviewer | MaterialViewer | StatsViewer)
658                | CreateCoursesOrExams
659                | ViewMaterial
660                | UploadFile
661                | ViewUserProgressOrDetails
662                | ViewInternalCourseStructure
663                | ViewStats
664        ),
665        Assistant => matches!(
666            action,
667            View | Edit
668                | Grade
669                | DeleteAnswer
670                | EditRole(Assistant | Reviewer | MaterialViewer)
671                | Teach
672                | ViewMaterial
673                | ViewUserProgressOrDetails
674                | ViewInternalCourseStructure
675        ),
676        Reviewer => matches!(
677            action,
678            View | Grade | ViewMaterial | ViewInternalCourseStructure
679        ),
680        CourseOrExamCreator => matches!(action, CreateCoursesOrExams),
681        MaterialViewer => matches!(action, ViewMaterial),
682        TeachingAndLearningServices => {
683            matches!(
684                action,
685                View | ViewMaterial
686                    | ViewUserProgressOrDetails
687                    | ViewInternalCourseStructure
688                    | ViewStats
689            )
690        }
691        StatsViewer => matches!(action, ViewStats),
692    }
693}
694
695pub fn parse_secret_key_from_header(header: &HttpRequest) -> Result<&str, ControllerError> {
696    let raw_token = header
697        .headers()
698        .get("Authorization")
699        .map_or(Ok(""), |x| x.to_str())
700        .map_err(|_| anyhow::anyhow!("Authorization header contains invalid characters."))?;
701    if !raw_token.starts_with("Basic") {
702        return Err(ControllerError::new(
703            ControllerErrorType::Forbidden,
704            "Access denied: Authorization header must use Basic authentication format.".to_string(),
705            None,
706        ));
707    }
708    let secret_key = raw_token.split(' ').nth(1).ok_or_else(|| {
709        ControllerError::new(
710            ControllerErrorType::Forbidden,
711            "Access denied: Malformed authorization token, expected 'Basic <token>' format."
712                .to_string(),
713            None,
714        )
715    })?;
716    Ok(secret_key)
717}
718
719/// Authenticates the user with mooc.fi, returning the authenticated user and their oauth token.
720pub async fn authenticate_moocfi_user(
721    conn: &mut PgConnection,
722    client: &OAuthClient,
723    email: String,
724    password: String,
725    tmc_client: &TmcClient,
726) -> anyhow::Result<Option<(User, SecretString)>> {
727    info!("Attempting to authenticate user with TMC");
728    let token = match exchange_password_with_tmc(client, email.clone(), password).await? {
729        Some(token) => token,
730        None => return Ok(None),
731    };
732    debug!("Successfully obtained OAuth token from TMC");
733
734    let tmc_user = tmc_client
735        .get_user_from_tmc_mooc_fi_by_tmc_access_token(&token.clone())
736        .await?;
737    debug!(
738        "Creating or fetching user with TMC id {} and mooc.fi UUID {}",
739        tmc_user.id,
740        tmc_user
741            .courses_mooc_fi_user_id
742            .map(|uuid| uuid.to_string())
743            .unwrap_or_else(|| "None (will generate new UUID)".to_string())
744    );
745    let user = get_or_create_user_from_tmc_mooc_fi_response(&mut *conn, tmc_user).await?;
746    info!(
747        "Successfully got user details from mooc.fi for user {}",
748        user.id
749    );
750    info!("Successfully authenticated user {} with mooc.fi", user.id);
751    Ok(Some((user, token)))
752}
753
754pub type LoginToken = StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>;
755
756/**
757Exchanges user credentials with TMC server to obtain an OAuth token.
758
759This function attempts to authenticate a user with the TMC server using their email and password.
760It returns different results based on the authentication outcome:
761
762- `Ok(Some(token))` - Authentication successful, returns the OAuth token
763- `Ok(None)` - Authentication failed due to invalid credentials (email/password)
764- `Err(...)` - Authentication failed due to other errors (server issues, network problems, etc.)
765*/
766pub async fn exchange_password_with_tmc(
767    client: &OAuthClient,
768    email: String,
769    password: String,
770) -> anyhow::Result<Option<SecretString>> {
771    let token_result = client
772        .exchange_password(
773            &ResourceOwnerUsername::new(email),
774            &ResourceOwnerPassword::new(password),
775        )
776        .request_async(&async_http_client_with_headers)
777        .await;
778    match token_result {
779        Ok(token) => Ok(Some(SecretString::new(
780            token.access_token().secret().to_owned().into(),
781        ))),
782        Err(RequestTokenError::ServerResponse(server_response)) => {
783            let error = server_response.error();
784            let error_description = server_response.error_description();
785            let error_uri = server_response.error_uri();
786
787            // Only return Ok(None) for InvalidGrant errors (wrong email/password)
788            if let oauth2::basic::BasicErrorResponseType::InvalidGrant = error {
789                warn!(
790                    ?error_description,
791                    ?error_uri,
792                    "TMC did not accept the credentials: {}",
793                    error
794                );
795                Ok(None)
796            } else {
797                // For all other error types, return an error
798                error!(
799                    ?error_description,
800                    ?error_uri,
801                    "TMC authentication error: {}",
802                    error
803                );
804                Err(anyhow::anyhow!("Authentication error: {}", error))
805            }
806        }
807        Err(e) => {
808            error!("Failed to exchange password with TMC: {}", e);
809            Err(e.into())
810        }
811    }
812}
813
814pub async fn get_or_create_user_from_tmc_mooc_fi_response(
815    conn: &mut PgConnection,
816    tmc_mooc_fi_user: TMCUser,
817) -> anyhow::Result<User> {
818    let TMCUser {
819        id: upstream_id,
820        email,
821        courses_mooc_fi_user_id: moocfi_id,
822        user_field,
823        ..
824    } = tmc_mooc_fi_user;
825
826    let id = moocfi_id.unwrap_or_else(Uuid::new_v4);
827
828    // fetch existing user or create new one
829    let user = match models::users::find_by_upstream_id(conn, upstream_id).await? {
830        Some(existing_user) => existing_user,
831        None => {
832            models::users::insert_with_upstream_id_and_moocfi_id(
833                conn,
834                &email,
835                // convert empty names to None
836                if user_field.first_name.trim().is_empty() {
837                    None
838                } else {
839                    Some(user_field.first_name.as_str())
840                },
841                if user_field.last_name.trim().is_empty() {
842                    None
843                } else {
844                    Some(user_field.last_name.as_str())
845                },
846                upstream_id,
847                id,
848            )
849            .await?
850        }
851    };
852    Ok(user)
853}
854
855/// Authenticates a test user with predefined credentials.
856/// Returns Ok(true) if authentication succeeds, Ok(false) if credentials are incorrect,
857/// and Err for other errors.
858pub async fn authenticate_test_user(
859    conn: &mut PgConnection,
860    email: &str,
861    password: &str,
862    application_configuration: &ApplicationConfiguration,
863) -> anyhow::Result<bool> {
864    // 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.
865    assert!(application_configuration.test_mode);
866
867    let _user = if email == "admin@example.com" && password == "admin" {
868        models::users::get_by_email(conn, "admin@example.com").await?
869    } else if email == "teacher@example.com" && password == "teacher" {
870        models::users::get_by_email(conn, "teacher@example.com").await?
871    } else if email == "language.teacher@example.com" && password == "language.teacher" {
872        models::users::get_by_email(conn, "language.teacher@example.com").await?
873    } else if email == "material.viewer@example.com" && password == "material.viewer" {
874        models::users::get_by_email(conn, "material.viewer@example.com").await?
875    } else if email == "user@example.com" && password == "user" {
876        models::users::get_by_email(conn, "user@example.com").await?
877    } else if email == "assistant@example.com" && password == "assistant" {
878        models::users::get_by_email(conn, "assistant@example.com").await?
879    } else if email == "creator@example.com" && password == "creator" {
880        models::users::get_by_email(conn, "creator@example.com").await?
881    } else if email == "student1@example.com" && password == "student1" {
882        models::users::get_by_email(conn, "student1@example.com").await?
883    } else if email == "student2@example.com" && password == "student2" {
884        models::users::get_by_email(conn, "student2@example.com").await?
885    } else if email == "student3@example.com" && password == "student3" {
886        models::users::get_by_email(conn, "student3@example.com").await?
887    } else if email == "student4@example.com" && password == "student4" {
888        models::users::get_by_email(conn, "student4@example.com").await?
889    } else if email == "student5@example.com" && password == "student5" {
890        models::users::get_by_email(conn, "student5@example.com").await?
891    } else if email == "teaching-and-learning-services@example.com"
892        && password == "teaching-and-learning-services"
893    {
894        models::users::get_by_email(conn, "teaching-and-learning-services@example.com").await?
895    } else if email == "student-without-research-consent@example.com"
896        && password == "student-without-research-consent"
897    {
898        models::users::get_by_email(conn, "student-without-research-consent@example.com").await?
899    } else if email == "student-without-country@example.com"
900        && password == "student-without-country"
901    {
902        models::users::get_by_email(conn, "student-without-country@example.com").await?
903    } else if email == "langs@example.com" && password == "langs" {
904        models::users::get_by_email(conn, "langs@example.com").await?
905    } else if email == "sign-up-user@example.com" && password == "sign-up-user" {
906        models::users::get_by_email(conn, "sign-up-user@example.com").await?
907    } else {
908        info!("Authentication failed: incorrect test credentials");
909        return Ok(false);
910    };
911    info!("Successfully authenticated test user {}", email);
912    Ok(true)
913}
914
915// Only used for testing, not to use in production.
916pub async fn authenticate_test_token(
917    conn: &mut PgConnection,
918    _token: &SecretString,
919    application_configuration: &ApplicationConfiguration,
920) -> anyhow::Result<User> {
921    // 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.
922    assert!(application_configuration.test_mode);
923    // TODO: this has never worked
924    let user = models::users::get_by_email(conn, "TODO").await?;
925    Ok(user)
926}
927
928/**
929 Gets the rate limit protection API key from environment variables and converts it to a header value.
930 This key is used to bypass rate limiting when making requests to TMC server.
931*/
932fn get_ratelimit_api_key() -> Result<reqwest::header::HeaderValue, HttpClientError<reqwest::Error>>
933{
934    let key = match std::env::var("RATELIMIT_PROTECTION_SAFE_API_KEY") {
935        Ok(key) => {
936            debug!("Found RATELIMIT_PROTECTION_SAFE_API_KEY");
937            key
938        }
939        Err(e) => {
940            error!(
941                "RATELIMIT_PROTECTION_SAFE_API_KEY environment variable not set: {}",
942                e
943            );
944            return Err(HttpClientError::Other(
945                "RATELIMIT_PROTECTION_SAFE_API_KEY must be defined".to_string(),
946            ));
947        }
948    };
949
950    key.parse::<reqwest::header::HeaderValue>().map_err(|err| {
951        error!("Invalid RATELIMIT API key format: {}", err);
952        HttpClientError::Other("Invalid RATELIMIT API key.".to_string())
953    })
954}
955
956/**
957 HTTP Client used only for authenticating with TMC server. This function:
958 1. Ensures TMC server does not rate limit auth requests from backend by adding a special header
959 2. Converts between oauth2 crate's internal http types and our reqwest types:
960    - Converts oauth2::HttpRequest to a reqwest::Request
961    - Makes the request using our REQWEST_CLIENT
962    - Converts the reqwest::Response back to oauth2::HttpResponse
963*/
964async fn async_http_client_with_headers(
965    oauth_request: oauth2::HttpRequest,
966) -> Result<oauth2::HttpResponse, HttpClientError<reqwest::Error>> {
967    debug!("Making OAuth request to TMC server");
968
969    if log::log_enabled!(log::Level::Trace) {
970        // Only log the URL path, not query parameters which may contain credentials
971        if let Ok(url) = oauth_request.uri().to_string().parse::<reqwest::Url>() {
972            trace!("OAuth request path: {}", url.path());
973        }
974    }
975
976    let parsed_key = get_ratelimit_api_key()?;
977
978    debug!("Building request to TMC server");
979    let request = REQWEST_CLIENT
980        .request(
981            oauth_request.method().clone(),
982            oauth_request
983                .uri()
984                .to_string()
985                .parse::<reqwest::Url>()
986                .map_err(|e| HttpClientError::Other(format!("Invalid URL: {}", e)))?,
987        )
988        .headers(oauth_request.headers().clone())
989        .version(oauth_request.version())
990        .header("RATELIMIT-PROTECTION-SAFE-API-KEY", parsed_key)
991        .body(oauth_request.body().to_vec());
992
993    debug!("Sending request to TMC server");
994    let response = request
995        .send()
996        .await
997        .map_err(|e| HttpClientError::Other(format!("Failed to execute request: {}", e)))?;
998
999    // Log response status and version, but not headers or body which may contain tokens
1000    debug!(
1001        "Received response from TMC server - Status: {}, Version: {:?}",
1002        response.status(),
1003        response.version()
1004    );
1005
1006    let status = response.status();
1007    let version = response.version();
1008    let headers = response.headers().clone();
1009
1010    debug!("Reading response body");
1011    let body_bytes = response
1012        .bytes()
1013        .await
1014        .map_err(|e| HttpClientError::Other(format!("Failed to read response body: {}", e)))?
1015        .to_vec();
1016
1017    debug!("Building OAuth response");
1018    let mut builder = oauth2::http::Response::builder()
1019        .status(status)
1020        .version(version);
1021
1022    if let Some(builder_headers) = builder.headers_mut() {
1023        builder_headers.extend(headers.iter().map(|(k, v)| (k.clone(), v.clone())));
1024    }
1025
1026    let oauth_response = builder
1027        .body(body_bytes)
1028        .map_err(|e| HttpClientError::Other(format!("Failed to construct response: {}", e)))?;
1029
1030    debug!("Successfully completed OAuth request");
1031    Ok(oauth_response)
1032}
1033
1034#[cfg(test)]
1035mod test {
1036    use super::*;
1037    use crate::test_helper::*;
1038    use headless_lms_models::*;
1039    use models::roles::RoleDomain;
1040
1041    #[actix_web::test]
1042    async fn test_authorization() {
1043        let mut conn = Conn::init().await;
1044        let mut tx = conn.begin().await;
1045
1046        let user = users::insert(
1047            tx.as_mut(),
1048            PKeyPolicy::Generate,
1049            "auth@example.com",
1050            None,
1051            None,
1052        )
1053        .await
1054        .unwrap();
1055        let org = organizations::insert(
1056            tx.as_mut(),
1057            PKeyPolicy::Generate,
1058            "auth",
1059            "auth",
1060            Some("auth"),
1061            false,
1062        )
1063        .await
1064        .unwrap();
1065
1066        authorize(
1067            tx.as_mut(),
1068            Action::Edit,
1069            Some(user),
1070            Resource::Organization(org),
1071        )
1072        .await
1073        .unwrap_err();
1074
1075        roles::insert(
1076            tx.as_mut(),
1077            user,
1078            UserRole::Teacher,
1079            RoleDomain::Organization(org),
1080        )
1081        .await
1082        .unwrap();
1083
1084        authorize(
1085            tx.as_mut(),
1086            Action::Edit,
1087            Some(user),
1088            Resource::Organization(org),
1089        )
1090        .await
1091        .unwrap();
1092    }
1093
1094    #[actix_web::test]
1095    async fn course_role_chapter_resource() {
1096        insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module, :chapter);
1097
1098        authorize(
1099            tx.as_mut(),
1100            Action::Edit,
1101            Some(user),
1102            Resource::Chapter(chapter),
1103        )
1104        .await
1105        .unwrap_err();
1106
1107        roles::insert(
1108            tx.as_mut(),
1109            user,
1110            UserRole::Teacher,
1111            RoleDomain::Course(course),
1112        )
1113        .await
1114        .unwrap();
1115
1116        authorize(
1117            tx.as_mut(),
1118            Action::Edit,
1119            Some(user),
1120            Resource::Chapter(chapter),
1121        )
1122        .await
1123        .unwrap();
1124    }
1125
1126    #[actix_web::test]
1127    async fn anonymous_user_can_view_open_course() {
1128        insert_data!(:tx, :user, :org, :course);
1129
1130        authorize(tx.as_mut(), Action::View, None, Resource::Course(course))
1131            .await
1132            .unwrap();
1133    }
1134}