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