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 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 skip_authorize()
320 };
321
322 Ok(token)
323}
324
325pub 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 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 == 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
363pub 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 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
386pub 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
419fn 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 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
448pub 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 for role in user_roles {
458 if role.is_global() && has_permission(role.role, action) {
459 return Ok(AuthorizationToken(()));
460 }
461 }
462
463 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 match resource {
474 Resource::Chapter(id) => {
475 let action =
477 if matches!(action, Action::View) && !models::chapters::is_open(conn, id).await? {
478 Action::ViewMaterial
479 } else {
480 action
481 };
482 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 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 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 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 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 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 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 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 return Ok(AuthorizationToken(()));
548 };
549
550 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
559async fn check_course_permission(
561 conn: &mut PgConnection,
562 roles: &[Role],
563 action: Action,
564 course_id: Uuid,
565) -> Result<AuthorizationToken, ControllerError> {
566 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
576async 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 action == Action::View
585 && !models::course_instances::is_open(conn, course_instance_id).await?
586 {
587 action = Action::Teach;
588 }
589
590 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
601async fn check_exam_permission(
603 conn: &mut PgConnection,
604 roles: &[Role],
605 action: Action,
606 exam_id: Uuid,
607) -> Result<AuthorizationToken, ControllerError> {
608 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
649fn 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
725pub 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
762pub 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 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 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 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 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
861pub async fn authenticate_test_user(
865 conn: &mut PgConnection,
866 email: &str,
867 password: &str,
868 application_configuration: &ApplicationConfiguration,
869) -> anyhow::Result<bool> {
870 assert!(application_configuration.test_mode);
872
873 let _user = if email == "admin@example.com" && password == "admin" {
874 models::users::get_by_email(conn, "admin@example.com").await?
875 } else if email == "teacher@example.com" && password == "teacher" {
876 models::users::get_by_email(conn, "teacher@example.com").await?
877 } else if email == "language.teacher@example.com" && password == "language.teacher" {
878 models::users::get_by_email(conn, "language.teacher@example.com").await?
879 } else if email == "material.viewer@example.com" && password == "material.viewer" {
880 models::users::get_by_email(conn, "material.viewer@example.com").await?
881 } else if email == "user@example.com" && password == "user" {
882 models::users::get_by_email(conn, "user@example.com").await?
883 } else if email == "assistant@example.com" && password == "assistant" {
884 models::users::get_by_email(conn, "assistant@example.com").await?
885 } else if email == "creator@example.com" && password == "creator" {
886 models::users::get_by_email(conn, "creator@example.com").await?
887 } else if email == "student1@example.com" && password == "student1" {
888 models::users::get_by_email(conn, "student1@example.com").await?
889 } else if email == "student2@example.com" && password == "student2" {
890 models::users::get_by_email(conn, "student2@example.com").await?
891 } else if email == "student3@example.com" && password == "student3" {
892 models::users::get_by_email(conn, "student3@example.com").await?
893 } else if email == "student4@example.com" && password == "student4" {
894 models::users::get_by_email(conn, "student4@example.com").await?
895 } else if email == "student5@example.com" && password == "student5" {
896 models::users::get_by_email(conn, "student5@example.com").await?
897 } else if email == "student6@example.com" && password == "student6" {
898 models::users::get_by_email(conn, "student6@example.com").await?
899 } else if email == "student7@example.com" && password == "student7" {
900 models::users::get_by_email(conn, "student7@example.com").await?
901 } else if email == "student8@example.com" && password == "student8" {
902 models::users::get_by_email(conn, "student8@example.com").await?
903 } else if email == "teaching-and-learning-services@example.com"
904 && password == "teaching-and-learning-services"
905 {
906 models::users::get_by_email(conn, "teaching-and-learning-services@example.com").await?
907 } else if email == "student-without-research-consent@example.com"
908 && password == "student-without-research-consent"
909 {
910 models::users::get_by_email(conn, "student-without-research-consent@example.com").await?
911 } else if email == "student-without-country@example.com"
912 && password == "student-without-country"
913 {
914 models::users::get_by_email(conn, "student-without-country@example.com").await?
915 } else if email == "langs@example.com" && password == "langs" {
916 models::users::get_by_email(conn, "langs@example.com").await?
917 } else if email == "sign-up-user@example.com" && password == "sign-up-user" {
918 models::users::get_by_email(conn, "sign-up-user@example.com").await?
919 } else {
920 info!("Authentication failed: incorrect test credentials");
921 return Ok(false);
922 };
923 info!("Successfully authenticated test user {}", email);
924 Ok(true)
925}
926
927pub async fn authenticate_test_token(
929 conn: &mut PgConnection,
930 _token: &SecretString,
931 application_configuration: &ApplicationConfiguration,
932) -> anyhow::Result<User> {
933 assert!(application_configuration.test_mode);
935 let user = models::users::get_by_email(conn, "TODO").await?;
937 Ok(user)
938}
939
940fn get_ratelimit_api_key() -> Result<reqwest::header::HeaderValue, HttpClientError<reqwest::Error>>
945{
946 let key = match std::env::var("RATELIMIT_PROTECTION_SAFE_API_KEY") {
947 Ok(key) => {
948 debug!("Found RATELIMIT_PROTECTION_SAFE_API_KEY");
949 key
950 }
951 Err(e) => {
952 error!(
953 "RATELIMIT_PROTECTION_SAFE_API_KEY environment variable not set: {}",
954 e
955 );
956 return Err(HttpClientError::Other(
957 "RATELIMIT_PROTECTION_SAFE_API_KEY must be defined".to_string(),
958 ));
959 }
960 };
961
962 key.parse::<reqwest::header::HeaderValue>().map_err(|err| {
963 error!("Invalid RATELIMIT API key format: {}", err);
964 HttpClientError::Other("Invalid RATELIMIT API key.".to_string())
965 })
966}
967
968async fn async_http_client_with_headers(
977 oauth_request: oauth2::HttpRequest,
978) -> Result<oauth2::HttpResponse, HttpClientError<reqwest::Error>> {
979 debug!("Making OAuth request to TMC server");
980
981 if log::log_enabled!(log::Level::Trace) {
982 if let Ok(url) = oauth_request.uri().to_string().parse::<reqwest::Url>() {
984 trace!("OAuth request path: {}", url.path());
985 }
986 }
987
988 let parsed_key = get_ratelimit_api_key()?;
989
990 debug!("Building request to TMC server");
991 let request = REQWEST_CLIENT
992 .request(
993 oauth_request.method().clone(),
994 oauth_request
995 .uri()
996 .to_string()
997 .parse::<reqwest::Url>()
998 .map_err(|e| HttpClientError::Other(format!("Invalid URL: {}", e)))?,
999 )
1000 .headers(oauth_request.headers().clone())
1001 .version(oauth_request.version())
1002 .header("RATELIMIT-PROTECTION-SAFE-API-KEY", parsed_key)
1003 .body(oauth_request.body().to_vec());
1004
1005 debug!("Sending request to TMC server");
1006 let response = request
1007 .send()
1008 .await
1009 .map_err(|e| HttpClientError::Other(format!("Failed to execute request: {}", e)))?;
1010
1011 debug!(
1013 "Received response from TMC server - Status: {}, Version: {:?}",
1014 response.status(),
1015 response.version()
1016 );
1017
1018 let status = response.status();
1019 let version = response.version();
1020 let headers = response.headers().clone();
1021
1022 debug!("Reading response body");
1023 let body_bytes = response
1024 .bytes()
1025 .await
1026 .map_err(|e| HttpClientError::Other(format!("Failed to read response body: {}", e)))?
1027 .to_vec();
1028
1029 debug!("Building OAuth response");
1030 let mut builder = oauth2::http::Response::builder()
1031 .status(status)
1032 .version(version);
1033
1034 if let Some(builder_headers) = builder.headers_mut() {
1035 builder_headers.extend(headers.iter().map(|(k, v)| (k.clone(), v.clone())));
1036 }
1037
1038 let oauth_response = builder
1039 .body(body_bytes)
1040 .map_err(|e| HttpClientError::Other(format!("Failed to construct response: {}", e)))?;
1041
1042 debug!("Successfully completed OAuth request");
1043 Ok(oauth_response)
1044}
1045
1046#[cfg(test)]
1047mod test {
1048 use super::*;
1049 use crate::test_helper::*;
1050 use headless_lms_models::*;
1051 use models::roles::RoleDomain;
1052
1053 #[actix_web::test]
1054 async fn test_authorization() {
1055 let mut conn = Conn::init().await;
1056 let mut tx = conn.begin().await;
1057
1058 let user = users::insert(
1059 tx.as_mut(),
1060 PKeyPolicy::Generate,
1061 "auth@example.com",
1062 None,
1063 None,
1064 )
1065 .await
1066 .unwrap();
1067 let org = organizations::insert(
1068 tx.as_mut(),
1069 PKeyPolicy::Generate,
1070 "auth",
1071 "auth",
1072 Some("auth"),
1073 false,
1074 )
1075 .await
1076 .unwrap();
1077
1078 authorize(
1079 tx.as_mut(),
1080 Action::Edit,
1081 Some(user),
1082 Resource::Organization(org),
1083 )
1084 .await
1085 .unwrap_err();
1086
1087 roles::insert(
1088 tx.as_mut(),
1089 user,
1090 UserRole::Teacher,
1091 RoleDomain::Organization(org),
1092 )
1093 .await
1094 .unwrap();
1095
1096 authorize(
1097 tx.as_mut(),
1098 Action::Edit,
1099 Some(user),
1100 Resource::Organization(org),
1101 )
1102 .await
1103 .unwrap();
1104 }
1105
1106 #[actix_web::test]
1107 async fn course_role_chapter_resource() {
1108 insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module, :chapter);
1109
1110 authorize(
1111 tx.as_mut(),
1112 Action::Edit,
1113 Some(user),
1114 Resource::Chapter(chapter),
1115 )
1116 .await
1117 .unwrap_err();
1118
1119 roles::insert(
1120 tx.as_mut(),
1121 user,
1122 UserRole::Teacher,
1123 RoleDomain::Course(course),
1124 )
1125 .await
1126 .unwrap();
1127
1128 authorize(
1129 tx.as_mut(),
1130 Action::Edit,
1131 Some(user),
1132 Resource::Chapter(chapter),
1133 )
1134 .await
1135 .unwrap();
1136 }
1137
1138 #[actix_web::test]
1139 async fn anonymous_user_can_view_open_course() {
1140 insert_data!(:tx, :user, :org, :course);
1141
1142 authorize(tx.as_mut(), Action::View, None, Resource::Course(course))
1143 .await
1144 .unwrap();
1145 }
1146}