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