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::ExposeSecret;
26use secrecy::SecretString;
27use serde::{Deserialize, Serialize};
28use serde_json::json;
29use sqlx::PgConnection;
30use std::env;
31use std::pin::Pin;
32use tracing_log::log;
33use utoipa::ToSchema;
34
35use uuid::Uuid;
36
37const SESSION_KEY: &str = "user";
38
39const MOOCFI_GRAPHQL_URL: &str = "https://www.mooc.fi/api";
40
41#[derive(Debug, Serialize, Deserialize)]
42struct GraphQLRequest<'a> {
43 query: &'a str,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 variables: Option<serde_json::Value>,
46}
47
48#[derive(Debug, Serialize, Deserialize)]
49struct MoocfiUserResponse {
50 pub data: MoocfiUserResponseData,
51}
52
53#[derive(Debug, Serialize, Deserialize)]
54struct MoocfiUserResponseData {
55 pub user: MoocfiUserData,
56}
57
58#[derive(Debug, Serialize, Deserialize)]
59struct MoocfiUserData {
60 pub id: Uuid,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
67pub struct AuthUser {
68 pub id: Uuid,
69 pub created_at: DateTime<Utc>,
70 pub updated_at: DateTime<Utc>,
71 pub deleted_at: Option<DateTime<Utc>>,
72 pub fetched_from_db_at: Option<DateTime<Utc>>,
73 upstream_id: Option<i32>,
74}
75
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
77#[serde(rename_all = "snake_case")]
78pub struct ActionOnResource {
79 pub action: Action,
80 pub resource: Resource,
81}
82
83impl AuthUser {
84 pub fn upstream_id(&self) -> Option<i32> {
86 self.upstream_id
87 }
88}
89
90impl FromRequest for AuthUser {
91 type Error = ControllerError;
92 type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
93
94 fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
95 let req = req.clone();
96 Box::pin(async move {
97 let req = req.clone();
98 let session = req.get_session();
99 let pool: Option<&web::Data<PgPool>> = req.app_data();
100 match session.get::<AuthUser>(SESSION_KEY) {
101 Ok(Some(user)) => Ok(verify_auth_user_exists(user, pool, &session).await?),
102 Ok(None) => Err(ControllerError::new(
103 ControllerErrorType::Unauthorized,
104 "You are not currently logged in. Please sign in to continue.".to_string(),
105 None,
106 )),
107 Err(_) => {
108 session.remove(SESSION_KEY);
110 Err(ControllerError::new(
111 ControllerErrorType::Unauthorized,
112 "Your session is invalid or has expired. Please sign in again.".to_string(),
113 None,
114 ))
115 }
116 }
117 })
118 }
119}
120
121async fn verify_auth_user_exists(
125 auth_user: AuthUser,
126 pool: Option<&web::Data<PgPool>>,
127 session: &Session,
128) -> Result<AuthUser, ControllerError> {
129 if let Some(fetched_from_db_at) = auth_user.fetched_from_db_at {
130 let time_now = Utc::now();
131 let time_hour_ago = time_now - Duration::hours(3);
132 if fetched_from_db_at > time_hour_ago {
133 return Ok(auth_user);
135 }
136 }
137 if let Some(pool) = pool {
138 info!("Checking whether the user saved in the session still exists in the database.");
139 let mut conn = pool.acquire().await?;
140 let user = models::users::get_by_id(&mut conn, auth_user.id).await?;
141 remember(session, user)?;
142 match session.get::<AuthUser>(SESSION_KEY) {
143 Ok(Some(session_user)) => Ok(session_user),
144 Ok(None) => Err(ControllerError::new(
145 ControllerErrorType::InternalServerError,
146 "User did not persist in the session".to_string(),
147 None,
148 )),
149 Err(e) => Err(ControllerError::new(
150 ControllerErrorType::InternalServerError,
151 "User did not persist in the session".to_string(),
152 Some(e.into()),
153 )),
154 }
155 } else {
156 warn!("No database pool provided to verify_auth_user_exists");
157 Err(ControllerError::new(
158 ControllerErrorType::InternalServerError,
159 "Unable to verify your user account. The database connection is unavailable."
160 .to_string(),
161 None,
162 ))
163 }
164}
165
166pub fn remember(session: &Session, user: models::users::User) -> Result<()> {
168 let auth_user = AuthUser {
169 id: user.id,
170 created_at: user.created_at,
171 updated_at: user.updated_at,
172 deleted_at: user.deleted_at,
173 upstream_id: user.upstream_id,
174 fetched_from_db_at: Some(Utc::now()),
175 };
176 session
177 .insert(SESSION_KEY, auth_user)
178 .map_err(|_| anyhow::anyhow!("Failed to insert to session"))
179}
180
181pub async fn has_auth_user_session(session: &Session, pool: web::Data<PgPool>) -> bool {
183 match session.get::<AuthUser>(SESSION_KEY) {
184 Ok(Some(sesssion_auth_user)) => {
185 verify_auth_user_exists(sesssion_auth_user, Some(&pool), session)
186 .await
187 .is_ok()
188 }
189 _ => false,
190 }
191}
192
193pub fn forget(session: &Session) {
195 session.purge();
196}
197
198#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, ToSchema)]
200#[serde(rename_all = "snake_case", tag = "type", content = "variant")]
201pub enum Action {
202 ViewMaterial,
203 View,
204 Edit,
205 Grade,
206 Teach,
207 Download,
208 Duplicate,
209 DeleteAnswer,
210 EditRole(UserRole),
211 CreateCoursesOrExams,
212 UsuallyUnacceptableDeletion,
214 UploadFile,
215 ViewUserProgressOrDetails,
216 ViewInternalCourseStructure,
217 ViewStats,
218 Administrate,
219}
220
221#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
223#[serde(rename_all = "snake_case", tag = "type", content = "id")]
224pub enum Resource {
225 GlobalPermissions,
226 Chapter(Uuid),
227 Course(Uuid),
228 CourseInstance(Uuid),
229 Exam(Uuid),
230 Exercise(Uuid),
231 ExerciseSlideSubmission(Uuid),
232 ExerciseTask(Uuid),
233 ExerciseTaskGrading(Uuid),
234 ExerciseTaskSubmission(Uuid),
235 Organization(Uuid),
236 Page(Uuid),
237 StudyRegistry(String),
238 AnyCourse,
239 Role,
240 User,
241 PlaygroundExample,
242 ExerciseService,
243}
244
245impl Resource {
246 pub fn from_course_or_exam_id(course_or_exam_id: CourseOrExamId) -> Self {
247 match course_or_exam_id {
248 CourseOrExamId::Course(id) => Self::Course(id),
249 CourseOrExamId::Exam(id) => Self::Exam(id),
250 }
251 }
252}
253
254#[derive(Copy, Clone, Debug)]
256pub struct AuthorizationToken(());
257
258impl AuthorizationToken {
259 pub fn authorized_ok<T>(self, t: T) -> ControllerResult<T> {
260 Ok(AuthorizedResponse {
261 data: t,
262 token: self,
263 })
264 }
265}
266
267#[derive(Copy, Clone)]
269pub struct AuthorizedResponse<T> {
270 pub data: T,
271 pub token: AuthorizationToken,
272}
273
274impl<T: Responder> Responder for AuthorizedResponse<T> {
275 type Body = T::Body;
276
277 fn respond_to(self, req: &HttpRequest) -> actix_web::HttpResponse<Self::Body> {
278 T::respond_to(self.data, req)
279 }
280}
281
282pub fn skip_authorize() -> AuthorizationToken {
300 AuthorizationToken(())
301}
302
303pub async fn authorize_access_to_course_material(
305 conn: &mut PgConnection,
306 user_id: Option<Uuid>,
307 course_id: Uuid,
308) -> Result<AuthorizationToken, ControllerError> {
309 let token = if models::courses::is_draft(conn, course_id).await? {
310 info!("Course is in draft mode");
311 if user_id.is_none() {
312 return Err(ControllerError::new(
313 ControllerErrorType::Unauthorized,
314 "This course is currently in draft mode and not publicly available. Please log in if you have access permissions.".to_string(),
315 None,
316 ));
317 }
318 authorize(conn, Act::ViewMaterial, user_id, Res::Course(course_id)).await?
319 } else if models::courses::is_joinable_by_code_only(conn, course_id).await? {
320 info!("Course is joinable by code only");
321 if let Some(user_id_value) = user_id {
322 if models::join_code_uses::check_if_user_has_access_to_course(
323 conn,
324 user_id_value,
325 course_id,
326 )
327 .await
328 .is_err()
329 {
330 authorize(conn, Act::ViewMaterial, user_id, Res::Course(course_id)).await?;
331 }
332 } else {
333 return Err(ControllerError::new(
334 ControllerErrorType::Unauthorized,
335 "This course requires authentication to access".to_string(),
336 None,
337 ));
338 }
339 skip_authorize()
340 } else {
341 skip_authorize()
343 };
344
345 Ok(token)
346}
347
348pub async fn authorize_access_from_tmc_server_to_course_mooc_fi(
351 request: &HttpRequest,
352) -> Result<AuthorizationToken, ControllerError> {
353 let tmc_server_secret_for_communicating_to_secret_project =
354 env::var("TMC_SERVER_SECRET_FOR_COMMUNICATING_TO_SECRET_PROJECT")
355 .expect("TMC_SERVER_SECRET_FOR_COMMUNICATING_TO_SECRET_PROJECT must be defined");
356 let auth_header = request
358 .headers()
359 .get("Authorization")
360 .ok_or_else(|| {
361 ControllerError::new(
362 ControllerErrorType::Unauthorized,
363 "TMC server authorization failed: Missing Authorization header.".to_string(),
364 None,
365 )
366 })?
367 .to_str()
368 .map_err(|_| {
369 ControllerError::new(
370 ControllerErrorType::Unauthorized,
371 "TMC server authorization failed: Invalid Authorization header format.".to_string(),
372 None,
373 )
374 })?;
375 if auth_header == tmc_server_secret_for_communicating_to_secret_project {
377 return Ok(skip_authorize());
378 }
379 Err(ControllerError::new(
380 ControllerErrorType::Unauthorized,
381 "TMC server authorization failed: Invalid authorization token.".to_string(),
382 None,
383 ))
384}
385
386pub async fn can_user_view_chapter(
388 conn: &mut PgConnection,
389 user_id: Option<Uuid>,
390 course_id: Option<Uuid>,
391 chapter_id: Option<Uuid>,
392) -> Result<bool, ControllerError> {
393 if let Some(course_id) = course_id
394 && let Some(chapter_id) = chapter_id
395 && !models::chapters::is_open(&mut *conn, chapter_id).await?
396 {
397 if user_id.is_none() {
398 return Ok(false);
399 }
400 let permission = authorize(conn, Act::ViewMaterial, user_id, Res::Course(course_id)).await;
403
404 return Ok(permission.is_ok());
405 }
406 Ok(true)
407}
408
409pub async fn authorize(
420 conn: &mut PgConnection,
421 action: Action,
422 user_id: Option<Uuid>,
423 resource: Resource,
424) -> Result<AuthorizationToken, ControllerError> {
425 let user_roles = if let Some(user_id) = user_id {
426 models::roles::get_roles(conn, user_id)
427 .await
428 .map_err(|original_err| {
429 ControllerError::new(
430 ControllerErrorType::InternalServerError,
431 format!("Failed to fetch user roles: {}", original_err),
432 Some(original_err.into()),
433 )
434 })?
435 } else {
436 Vec::new()
437 };
438
439 authorize_with_fetched_list_of_roles(conn, action, user_id, resource, &user_roles).await
440}
441
442fn create_authorization_error(user_roles: &[Role], action: Option<Action>) -> ControllerError {
444 let mut detail_message = String::new();
445
446 if user_roles.is_empty() {
447 detail_message.push_str("You don't have any assigned roles.");
448 } else {
449 detail_message.push_str("Your current roles are: ");
450 let roles_str = user_roles
451 .iter()
452 .map(|r| format!("{:?} ({})", r.role, r.domain_description()))
453 .collect::<Vec<_>>()
454 .join(", ");
455 detail_message.push_str(&roles_str);
456 }
457
458 if let Some(act) = action {
459 detail_message.push_str(&format!("\nAction attempted: {:?}", act));
460 }
461
462 ControllerError::new(
464 ControllerErrorType::Forbidden,
465 "Unauthorized. Please contact course staff if you believe you should have access."
466 .to_string(),
467 Some(ControllerError::new(ControllerErrorType::Forbidden, detail_message, None).into()),
468 )
469}
470
471pub async fn authorize_with_fetched_list_of_roles(
473 conn: &mut PgConnection,
474 action: Action,
475 _user_id: Option<Uuid>,
476 resource: Resource,
477 user_roles: &[Role],
478) -> Result<AuthorizationToken, ControllerError> {
479 for role in user_roles {
481 if role.is_global() && has_permission(role.role, action) {
482 return Ok(AuthorizationToken(()));
483 }
484 }
485
486 if resource == Resource::AnyCourse {
488 for role in user_roles {
489 if has_permission(role.role, action) {
490 return Ok(AuthorizationToken(()));
491 }
492 }
493 }
494
495 match resource {
497 Resource::Chapter(id) => {
498 let action =
500 if matches!(action, Action::View) && !models::chapters::is_open(conn, id).await? {
501 Action::ViewMaterial
502 } else {
503 action
504 };
505 let course_id = models::chapters::get_course_id(conn, id).await?;
507 check_course_permission(conn, user_roles, action, course_id).await
508 }
509 Resource::Course(id) => check_course_permission(conn, user_roles, action, id).await,
510 Resource::CourseInstance(id) => {
511 check_course_instance_permission(conn, user_roles, action, id).await
512 }
513 Resource::Exercise(id) => {
514 let course_or_exam_id = models::exercises::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::ExerciseSlideSubmission(id) => {
519 let course_or_exam_id =
521 models::exercise_slide_submissions::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::ExerciseTask(id) => {
525 let course_or_exam_id = models::exercise_tasks::get_course_or_exam_id(conn, id).await?;
527 check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
528 }
529 Resource::ExerciseTaskSubmission(id) => {
530 let course_or_exam_id =
532 models::exercise_task_submissions::get_course_and_exam_id(conn, id).await?;
533 check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
534 }
535 Resource::ExerciseTaskGrading(id) => {
536 let course_or_exam_id =
538 models::exercise_task_gradings::get_course_or_exam_id(conn, id).await?;
539 check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
540 }
541 Resource::Organization(id) => check_organization_permission(user_roles, action, id).await,
542 Resource::Page(id) => {
543 let course_or_exam_id = models::pages::get_course_and_exam_id(conn, id).await?;
545 check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
546 }
547 Resource::StudyRegistry(secret_key) => {
548 check_study_registry_permission(conn, secret_key, action).await
549 }
550 Resource::Exam(exam_id) => check_exam_permission(conn, user_roles, action, exam_id).await,
551 Resource::Role
552 | Resource::User
553 | Resource::AnyCourse
554 | Resource::PlaygroundExample
555 | Resource::ExerciseService
556 | Resource::GlobalPermissions => {
557 Err(create_authorization_error(user_roles, Some(action)))
559 }
560 }
561}
562
563async fn check_organization_permission(
564 roles: &[Role],
565 action: Action,
566 organization_id: Uuid,
567) -> Result<AuthorizationToken, ControllerError> {
568 if action == Action::View {
569 return Ok(AuthorizationToken(()));
571 };
572
573 for role in roles {
575 if role.is_role_for_organization(organization_id) && has_permission(role.role, action) {
576 return Ok(AuthorizationToken(()));
577 }
578 }
579 Err(create_authorization_error(roles, Some(action)))
580}
581
582async fn check_course_permission(
584 conn: &mut PgConnection,
585 roles: &[Role],
586 action: Action,
587 course_id: Uuid,
588) -> Result<AuthorizationToken, ControllerError> {
589 for role in roles {
591 if role.is_role_for_course(course_id) && has_permission(role.role, action) {
592 return Ok(AuthorizationToken(()));
593 }
594 }
595 let organization_id = models::courses::get_organization_id(conn, course_id).await?;
596 check_organization_permission(roles, action, organization_id).await
597}
598
599async fn check_course_instance_permission(
601 conn: &mut PgConnection,
602 roles: &[Role],
603 mut action: Action,
604 course_instance_id: Uuid,
605) -> Result<AuthorizationToken, ControllerError> {
606 if action == Action::View
608 && !models::course_instances::is_open(conn, course_instance_id).await?
609 {
610 action = Action::Teach;
611 }
612
613 for role in roles {
615 if role.is_role_for_course_instance(course_instance_id) && has_permission(role.role, action)
616 {
617 return Ok(AuthorizationToken(()));
618 }
619 }
620 let course_id = models::course_instances::get_course_id(conn, course_instance_id).await?;
621 check_course_permission(conn, roles, action, course_id).await
622}
623
624async fn check_exam_permission(
626 conn: &mut PgConnection,
627 roles: &[Role],
628 action: Action,
629 exam_id: Uuid,
630) -> Result<AuthorizationToken, ControllerError> {
631 for role in roles {
633 if role.is_role_for_exam(exam_id) && has_permission(role.role, action) {
634 return Ok(AuthorizationToken(()));
635 }
636 }
637 let organization_id = models::exams::get_organization_id(conn, exam_id).await?;
638 check_organization_permission(roles, action, organization_id).await
639}
640
641async fn check_course_or_exam_permission(
642 conn: &mut PgConnection,
643 roles: &[Role],
644 action: Action,
645 course_or_exam_id: CourseOrExamId,
646) -> Result<AuthorizationToken, ControllerError> {
647 match course_or_exam_id {
648 CourseOrExamId::Course(course_id) => {
649 check_course_permission(conn, roles, action, course_id).await
650 }
651 CourseOrExamId::Exam(exam_id) => check_exam_permission(conn, roles, action, exam_id).await,
652 }
653}
654
655async fn check_study_registry_permission(
656 conn: &mut PgConnection,
657 secret_key: String,
658 action: Action,
659) -> Result<AuthorizationToken, ControllerError> {
660 let _registrar = models::study_registry_registrars::get_by_secret_key(conn, &secret_key)
661 .await
662 .map_err(|original_error| {
663 ControllerError::new(
664 ControllerErrorType::Forbidden,
665 format!("Study registry access denied: Invalid or missing secret key. The operation {:?} cannot be performed.", action),
666 Some(original_error.into()),
667 )
668 })?;
669 Ok(AuthorizationToken(()))
670}
671
672fn has_permission(user_role: UserRole, action: Action) -> bool {
674 use Action::*;
675 use UserRole::*;
676
677 match user_role {
678 Admin => true,
679 Teacher => matches!(
680 action,
681 View | Teach
682 | Edit
683 | Grade
684 | Duplicate
685 | DeleteAnswer
686 | EditRole(Teacher | Assistant | Reviewer | MaterialViewer | StatsViewer)
687 | CreateCoursesOrExams
688 | ViewMaterial
689 | UploadFile
690 | ViewUserProgressOrDetails
691 | ViewInternalCourseStructure
692 | ViewStats
693 ),
694 Assistant => matches!(
695 action,
696 View | Edit
697 | Grade
698 | DeleteAnswer
699 | EditRole(Assistant | Reviewer | MaterialViewer)
700 | Teach
701 | ViewMaterial
702 | ViewUserProgressOrDetails
703 | ViewInternalCourseStructure
704 ),
705 Reviewer => matches!(
706 action,
707 View | Grade | ViewMaterial | ViewInternalCourseStructure
708 ),
709 CourseOrExamCreator => matches!(action, CreateCoursesOrExams),
710 MaterialViewer => matches!(action, ViewMaterial),
711 TeachingAndLearningServices => {
712 matches!(
713 action,
714 View | ViewMaterial
715 | ViewUserProgressOrDetails
716 | ViewInternalCourseStructure
717 | ViewStats
718 )
719 }
720 StatsViewer => matches!(action, ViewStats),
721 }
722}
723
724pub fn parse_secret_key_from_header(header: &HttpRequest) -> Result<&str, ControllerError> {
725 let raw_token = header
726 .headers()
727 .get("Authorization")
728 .map_or(Ok(""), |x| x.to_str())
729 .map_err(|_| anyhow::anyhow!("Authorization header contains invalid characters."))?;
730 if !raw_token.starts_with("Basic") {
731 return Err(ControllerError::new(
732 ControllerErrorType::Forbidden,
733 "Access denied: Authorization header must use Basic authentication format.".to_string(),
734 None,
735 ));
736 }
737 let secret_key = raw_token.split(' ').nth(1).ok_or_else(|| {
738 ControllerError::new(
739 ControllerErrorType::Forbidden,
740 "Access denied: Malformed authorization token, expected 'Basic <token>' format."
741 .to_string(),
742 None,
743 )
744 })?;
745 Ok(secret_key)
746}
747
748pub async fn authenticate_tmc_mooc_fi_user(
750 conn: &mut PgConnection,
751 client: &OAuthClient,
752 email: String,
753 password: String,
754 tmc_client: &TmcClient,
755) -> anyhow::Result<Option<(User, SecretString)>> {
756 info!("Attempting to authenticate user with TMC");
757 let token = match exchange_password_with_tmc(client, email.clone(), password).await? {
758 Some(token) => token,
759 None => return Ok(None),
760 };
761 debug!("Successfully obtained OAuth token from TMC");
762
763 let tmc_user = tmc_client
764 .get_user_from_tmc_mooc_fi_by_tmc_access_token(&token.clone())
765 .await?;
766 debug!(
767 "Creating or fetching user with TMC id {} and mooc.fi UUID {}",
768 tmc_user.id,
769 tmc_user
770 .courses_mooc_fi_user_id
771 .map(|uuid| uuid.to_string())
772 .unwrap_or_else(|| "None (will fetch from mooc.fi or generate new UUID)".to_string())
773 );
774 let user = get_or_create_user_from_tmc_mooc_fi_response(&mut *conn, tmc_user, &token).await?;
775 info!(
776 "Successfully got user details from mooc.fi for user {}",
777 user.id
778 );
779 info!("Successfully authenticated user {} with mooc.fi", user.id);
780 Ok(Some((user, token)))
781}
782
783pub type LoginToken = StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>;
784
785pub async fn exchange_password_with_tmc(
796 client: &OAuthClient,
797 email: String,
798 password: String,
799) -> anyhow::Result<Option<SecretString>> {
800 let token_result = client
801 .exchange_password(
802 &ResourceOwnerUsername::new(email),
803 &ResourceOwnerPassword::new(password),
804 )
805 .request_async(&async_http_client_with_headers)
806 .await;
807 match token_result {
808 Ok(token) => Ok(Some(SecretString::new(
809 token.access_token().secret().to_owned().into(),
810 ))),
811 Err(RequestTokenError::ServerResponse(server_response)) => {
812 let error = server_response.error();
813 let error_description = server_response.error_description();
814 let error_uri = server_response.error_uri();
815
816 if let oauth2::basic::BasicErrorResponseType::InvalidGrant = error {
818 warn!(
819 ?error_description,
820 ?error_uri,
821 "TMC did not accept the credentials: {}",
822 error
823 );
824 Ok(None)
825 } else {
826 error!(
828 ?error_description,
829 ?error_uri,
830 "TMC authentication error: {}",
831 error
832 );
833 Err(anyhow::anyhow!("Authentication error: {}", error))
834 }
835 }
836 Err(e) => {
837 error!("Failed to exchange password with TMC: {}", e);
838 Err(e.into())
839 }
840 }
841}
842
843async fn fetch_moocfi_id_by_upstream_id(
845 tmc_access_token: &str,
846 upstream_id: i32,
847) -> anyhow::Result<Option<Uuid>> {
848 info!("Fetching mooc.fi UUID for upstream user id {}", upstream_id);
849
850 let res = REQWEST_CLIENT
851 .post(MOOCFI_GRAPHQL_URL)
852 .header(reqwest::header::CONTENT_TYPE, "application/json")
853 .header(reqwest::header::ACCEPT, "application/json")
854 .bearer_auth(tmc_access_token)
855 .json(&GraphQLRequest {
856 query: r#"
857query ($upstreamId: Int) {
858 user(upstream_id: $upstreamId) {
859 id
860 }
861}"#,
862 variables: Some(json!({ "upstreamId": upstream_id })),
863 })
864 .send()
865 .await;
866
867 match res {
868 Ok(response) => {
869 if !response.status().is_success() {
870 debug!(
871 "Failed to fetch mooc.fi user with status {}. Will generate new UUID instead.",
872 response.status()
873 );
874 return Ok(None);
875 }
876
877 match response.json::<MoocfiUserResponse>().await {
878 Ok(current_user_response) => {
879 info!(
880 "Successfully fetched mooc.fi UUID {} for upstream id {}",
881 current_user_response.data.user.id, upstream_id
882 );
883 Ok(Some(current_user_response.data.user.id))
884 }
885 Err(e) => {
886 debug!(
887 "Failed to parse mooc.fi response: {}. Will generate new UUID instead.",
888 e
889 );
890 Ok(None)
891 }
892 }
893 }
894 Err(e) => {
895 debug!(
896 "Failed to fetch from mooc.fi: {}. Will generate new UUID instead.",
897 e
898 );
899 Ok(None)
900 }
901 }
902}
903
904pub async fn get_or_create_user_from_tmc_mooc_fi_response(
905 conn: &mut PgConnection,
906 tmc_mooc_fi_user: TMCUser,
907 tmc_access_token: &SecretString,
908) -> anyhow::Result<User> {
909 let TMCUser {
910 id: upstream_id,
911 email,
912 courses_mooc_fi_user_id: moocfi_id,
913 user_field,
914 ..
915 } = tmc_mooc_fi_user;
916
917 let id = match moocfi_id {
919 Some(id) => id,
920 None => match fetch_moocfi_id_by_upstream_id(tmc_access_token.expose_secret(), upstream_id)
921 .await?
922 {
923 Some(fetched_id) => {
924 info!("Successfully fetched mooc.fi UUID {} for user", fetched_id);
925 fetched_id
926 }
927 None => {
928 info!("No mooc.fi UUID found, generating new UUID for user");
929 Uuid::new_v4()
930 }
931 },
932 };
933
934 let user = match models::users::find_by_upstream_id(conn, upstream_id).await? {
936 Some(existing_user) => existing_user,
937 None => {
938 models::users::insert_with_upstream_id_and_moocfi_id(
939 conn,
940 &email,
941 if user_field.first_name.trim().is_empty() {
943 None
944 } else {
945 Some(user_field.first_name.as_str())
946 },
947 if user_field.last_name.trim().is_empty() {
948 None
949 } else {
950 Some(user_field.last_name.as_str())
951 },
952 upstream_id,
953 id,
954 )
955 .await?
956 }
957 };
958 Ok(user)
959}
960
961pub async fn authenticate_test_user(
965 conn: &mut PgConnection,
966 email: &str,
967 password: &str,
968 application_configuration: &ApplicationConfiguration,
969) -> anyhow::Result<bool> {
970 assert!(application_configuration.test_mode);
972
973 let _user = if email == "admin@example.com" && password == "admin" {
974 models::users::get_by_email(conn, "admin@example.com").await?
975 } else if email == "teacher@example.com" && password == "teacher" {
976 models::users::get_by_email(conn, "teacher@example.com").await?
977 } else if email == "language.teacher@example.com" && password == "language.teacher" {
978 models::users::get_by_email(conn, "language.teacher@example.com").await?
979 } else if email == "material.viewer@example.com" && password == "material.viewer" {
980 models::users::get_by_email(conn, "material.viewer@example.com").await?
981 } else if email == "user@example.com" && password == "user" {
982 models::users::get_by_email(conn, "user@example.com").await?
983 } else if email == "assistant@example.com" && password == "assistant" {
984 models::users::get_by_email(conn, "assistant@example.com").await?
985 } else if email == "creator@example.com" && password == "creator" {
986 models::users::get_by_email(conn, "creator@example.com").await?
987 } else if email == "student1@example.com" && password == "student1" {
988 models::users::get_by_email(conn, "student1@example.com").await?
989 } else if email == "student2@example.com" && password == "student2" {
990 models::users::get_by_email(conn, "student2@example.com").await?
991 } else if email == "student3@example.com" && password == "student3" {
992 models::users::get_by_email(conn, "student3@example.com").await?
993 } else if email == "student4@example.com" && password == "student4" {
994 models::users::get_by_email(conn, "student4@example.com").await?
995 } else if email == "student5@example.com" && password == "student5" {
996 models::users::get_by_email(conn, "student5@example.com").await?
997 } else if email == "student6@example.com" && password == "student6" {
998 models::users::get_by_email(conn, "student6@example.com").await?
999 } else if email == "student7@example.com" && password == "student7" {
1000 models::users::get_by_email(conn, "student7@example.com").await?
1001 } else if email == "student8@example.com" && password == "student8" {
1002 models::users::get_by_email(conn, "student8@example.com").await?
1003 } else if email == "teaching-and-learning-services@example.com"
1004 && password == "teaching-and-learning-services"
1005 {
1006 models::users::get_by_email(conn, "teaching-and-learning-services@example.com").await?
1007 } else if email == "student-without-research-consent@example.com"
1008 && password == "student-without-research-consent"
1009 {
1010 models::users::get_by_email(conn, "student-without-research-consent@example.com").await?
1011 } else if email == "student-without-country@example.com"
1012 && password == "student-without-country"
1013 {
1014 models::users::get_by_email(conn, "student-without-country@example.com").await?
1015 } else if email == "langs@example.com" && password == "langs" {
1016 models::users::get_by_email(conn, "langs@example.com").await?
1017 } else if email == "sign-up-user@example.com" && password == "sign-up-user" {
1018 models::users::get_by_email(conn, "sign-up-user@example.com").await?
1019 } else {
1020 info!("Authentication failed: incorrect test credentials");
1021 return Ok(false);
1022 };
1023 info!("Successfully authenticated test user {}", email);
1024 Ok(true)
1025}
1026
1027pub async fn authenticate_test_token(
1029 conn: &mut PgConnection,
1030 _token: &SecretString,
1031 application_configuration: &ApplicationConfiguration,
1032) -> anyhow::Result<User> {
1033 assert!(application_configuration.test_mode);
1035 let user = models::users::get_by_email(conn, "TODO").await?;
1037 Ok(user)
1038}
1039
1040fn get_ratelimit_api_key() -> Result<reqwest::header::HeaderValue, HttpClientError<reqwest::Error>>
1045{
1046 let key = match std::env::var("RATELIMIT_PROTECTION_SAFE_API_KEY") {
1047 Ok(key) => {
1048 debug!("Found RATELIMIT_PROTECTION_SAFE_API_KEY");
1049 key
1050 }
1051 Err(e) => {
1052 error!(
1053 "RATELIMIT_PROTECTION_SAFE_API_KEY environment variable not set: {}",
1054 e
1055 );
1056 return Err(HttpClientError::Other(
1057 "RATELIMIT_PROTECTION_SAFE_API_KEY must be defined".to_string(),
1058 ));
1059 }
1060 };
1061
1062 key.parse::<reqwest::header::HeaderValue>().map_err(|err| {
1063 error!("Invalid RATELIMIT API key format: {}", err);
1064 HttpClientError::Other("Invalid RATELIMIT API key.".to_string())
1065 })
1066}
1067
1068async fn async_http_client_with_headers(
1077 oauth_request: oauth2::HttpRequest,
1078) -> Result<oauth2::HttpResponse, HttpClientError<reqwest::Error>> {
1079 debug!("Making OAuth request to TMC server");
1080
1081 if log::log_enabled!(log::Level::Trace) {
1082 if let Ok(url) = oauth_request.uri().to_string().parse::<reqwest::Url>() {
1084 trace!("OAuth request path: {}", url.path());
1085 }
1086 }
1087
1088 let parsed_key = get_ratelimit_api_key()?;
1089
1090 debug!("Building request to TMC server");
1091 let request = REQWEST_CLIENT
1092 .request(
1093 oauth_request.method().clone(),
1094 oauth_request
1095 .uri()
1096 .to_string()
1097 .parse::<reqwest::Url>()
1098 .map_err(|e| HttpClientError::Other(format!("Invalid URL: {}", e)))?,
1099 )
1100 .headers(oauth_request.headers().clone())
1101 .version(oauth_request.version())
1102 .header("RATELIMIT-PROTECTION-SAFE-API-KEY", parsed_key)
1103 .body(oauth_request.body().to_vec());
1104
1105 debug!("Sending request to TMC server");
1106 let response = request
1107 .send()
1108 .await
1109 .map_err(|e| HttpClientError::Other(format!("Failed to execute request: {}", e)))?;
1110
1111 debug!(
1113 "Received response from TMC server - Status: {}, Version: {:?}",
1114 response.status(),
1115 response.version()
1116 );
1117
1118 let status = response.status();
1119 let version = response.version();
1120 let headers = response.headers().clone();
1121
1122 debug!("Reading response body");
1123 let body_bytes = response
1124 .bytes()
1125 .await
1126 .map_err(|e| HttpClientError::Other(format!("Failed to read response body: {}", e)))?
1127 .to_vec();
1128
1129 debug!("Building OAuth response");
1130 let mut builder = oauth2::http::Response::builder()
1131 .status(status)
1132 .version(version);
1133
1134 if let Some(builder_headers) = builder.headers_mut() {
1135 builder_headers.extend(headers.iter().map(|(k, v)| (k.clone(), v.clone())));
1136 }
1137
1138 let oauth_response = builder
1139 .body(body_bytes)
1140 .map_err(|e| HttpClientError::Other(format!("Failed to construct response: {}", e)))?;
1141
1142 debug!("Successfully completed OAuth request");
1143 Ok(oauth_response)
1144}
1145
1146#[cfg(test)]
1147mod test {
1148 use super::*;
1149 use crate::test_helper::*;
1150 use headless_lms_models::*;
1151 use models::roles::RoleDomain;
1152
1153 #[actix_web::test]
1154 async fn test_authorization() {
1155 let mut conn = Conn::init().await;
1156 let mut tx = conn.begin().await;
1157
1158 let user = users::insert(
1159 tx.as_mut(),
1160 PKeyPolicy::Generate,
1161 "auth@example.com",
1162 None,
1163 None,
1164 )
1165 .await
1166 .unwrap();
1167 let org = organizations::insert(
1168 tx.as_mut(),
1169 PKeyPolicy::Generate,
1170 "auth",
1171 "auth",
1172 Some("auth"),
1173 false,
1174 )
1175 .await
1176 .unwrap();
1177
1178 authorize(
1179 tx.as_mut(),
1180 Action::Edit,
1181 Some(user),
1182 Resource::Organization(org),
1183 )
1184 .await
1185 .unwrap_err();
1186
1187 roles::insert(
1188 tx.as_mut(),
1189 user,
1190 UserRole::Teacher,
1191 RoleDomain::Organization(org),
1192 )
1193 .await
1194 .unwrap();
1195
1196 authorize(
1197 tx.as_mut(),
1198 Action::Edit,
1199 Some(user),
1200 Resource::Organization(org),
1201 )
1202 .await
1203 .unwrap();
1204 }
1205
1206 #[actix_web::test]
1207 async fn course_role_chapter_resource() {
1208 insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module, :chapter);
1209
1210 authorize(
1211 tx.as_mut(),
1212 Action::Edit,
1213 Some(user),
1214 Resource::Chapter(chapter),
1215 )
1216 .await
1217 .unwrap_err();
1218
1219 roles::insert(
1220 tx.as_mut(),
1221 user,
1222 UserRole::Teacher,
1223 RoleDomain::Course(course),
1224 )
1225 .await
1226 .unwrap();
1227
1228 authorize(
1229 tx.as_mut(),
1230 Action::Edit,
1231 Some(user),
1232 Resource::Chapter(chapter),
1233 )
1234 .await
1235 .unwrap();
1236 }
1237
1238 #[actix_web::test]
1239 async fn anonymous_user_can_view_open_course() {
1240 insert_data!(:tx, :user, :org, :course);
1241
1242 authorize(tx.as_mut(), Action::View, None, Resource::Course(course))
1243 .await
1244 .unwrap();
1245 }
1246}