1use 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#[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 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.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
96async 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 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
141pub 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
156pub 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
168pub fn forget(session: &Session) {
170 session.purge();
171}
172
173#[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 UsuallyUnacceptableDeletion,
190 UploadFile,
191 ViewUserProgressOrDetails,
192 ViewInternalCourseStructure,
193 ViewStats,
194 Administrate,
195}
196
197#[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#[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#[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
259pub fn skip_authorize() -> AuthorizationToken {
277 AuthorizationToken(())
278}
279
280pub 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 skip_authorize()
312 };
313
314 Ok(token)
315}
316
317pub 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 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 == 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
355pub 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 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
380pub 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
413fn 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 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
442pub 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 for role in user_roles {
452 if role.is_global() && has_permission(role.role, action) {
453 return Ok(AuthorizationToken(()));
454 }
455 }
456
457 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 match resource {
468 Resource::Chapter(id) => {
469 let action =
471 if matches!(action, Action::View) && !models::chapters::is_open(conn, id).await? {
472 Action::ViewMaterial
473 } else {
474 action
475 };
476 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 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 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 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 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 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 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 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 return Ok(AuthorizationToken(()));
542 };
543
544 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
553async fn check_course_permission(
555 conn: &mut PgConnection,
556 roles: &[Role],
557 action: Action,
558 course_id: Uuid,
559) -> Result<AuthorizationToken, ControllerError> {
560 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
570async 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 action == Action::View
579 && !models::course_instances::is_open(conn, course_instance_id).await?
580 {
581 action = Action::Teach;
582 }
583
584 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
595async fn check_exam_permission(
597 conn: &mut PgConnection,
598 roles: &[Role],
599 action: Action,
600 exam_id: Uuid,
601) -> Result<AuthorizationToken, ControllerError> {
602 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
643fn 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
719pub 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
756pub 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 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 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 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 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
855pub async fn authenticate_test_user(
859 conn: &mut PgConnection,
860 email: &str,
861 password: &str,
862 application_configuration: &ApplicationConfiguration,
863) -> anyhow::Result<bool> {
864 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
915pub async fn authenticate_test_token(
917 conn: &mut PgConnection,
918 _token: &SecretString,
919 application_configuration: &ApplicationConfiguration,
920) -> anyhow::Result<User> {
921 assert!(application_configuration.test_mode);
923 let user = models::users::get_by_email(conn, "TODO").await?;
925 Ok(user)
926}
927
928fn 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
956async 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 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 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}