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::tmc::TMCUser;
16use headless_lms_utils::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_course_material(
310 conn: &mut PgConnection,
311 user_id: Option<Uuid>,
312 course_id: Uuid,
313) -> Result<AuthorizationToken, ControllerError> {
314 let token = if models::courses::is_draft(conn, course_id).await? {
315 info!("Course is in draft mode");
316 if user_id.is_none() {
317 return Err(ControllerError::new(
318 ControllerErrorType::Unauthorized,
319 "This course is currently in draft mode and not publicly available. Please log in if you have access permissions.".to_string(),
320 None,
321 ));
322 }
323 authorize(conn, Act::ViewMaterial, user_id, Res::Course(course_id)).await?
324 } else if models::courses::is_joinable_by_code_only(conn, course_id).await? {
325 info!("Course is joinable by code only");
326 if let Some(user_id_value) = user_id {
327 if models::join_code_uses::check_if_user_has_access_to_course(
328 conn,
329 user_id_value,
330 course_id,
331 )
332 .await
333 .is_err()
334 {
335 authorize(conn, Act::ViewMaterial, user_id, Res::Course(course_id)).await?;
336 }
337 } else {
338 return Err(ControllerError::new(
339 ControllerErrorType::Unauthorized,
340 "This course requires authentication to access".to_string(),
341 None,
342 ));
343 }
344 skip_authorize()
345 } else {
346 skip_authorize()
348 };
349
350 Ok(token)
351}
352
353pub async fn authorize_access_from_tmc_server_to_course_mooc_fi(
356 request: &HttpRequest,
357) -> Result<AuthorizationToken, ControllerError> {
358 let tmc_server_secret_for_communicating_to_secret_project =
359 &server_runtime_config().tmc_server_secret_for_communicating_to_secret_project;
360 let auth_header = request
362 .headers()
363 .get("Authorization")
364 .ok_or_else(|| {
365 ControllerError::new(
366 ControllerErrorType::Unauthorized,
367 "TMC server authorization failed: Missing Authorization header.".to_string(),
368 None,
369 )
370 })?
371 .to_str()
372 .map_err(|_| {
373 ControllerError::new(
374 ControllerErrorType::Unauthorized,
375 "TMC server authorization failed: Invalid Authorization header format.".to_string(),
376 None,
377 )
378 })?;
379 if constant_time_eq_str(
381 auth_header,
382 tmc_server_secret_for_communicating_to_secret_project.expose_secret(),
383 ) {
384 return Ok(skip_authorize());
385 }
386 Err(ControllerError::new(
387 ControllerErrorType::Unauthorized,
388 "TMC server authorization failed: Invalid authorization token.".to_string(),
389 None,
390 ))
391}
392
393pub async fn can_user_view_chapter(
395 conn: &mut PgConnection,
396 user_id: Option<Uuid>,
397 course_id: Option<Uuid>,
398 chapter_id: Option<Uuid>,
399) -> Result<bool, ControllerError> {
400 if let Some(course_id) = course_id
401 && let Some(chapter_id) = chapter_id
402 && !models::chapters::is_open(&mut *conn, chapter_id).await?
403 {
404 if user_id.is_none() {
405 return Ok(false);
406 }
407 let permission = authorize(conn, Act::ViewMaterial, user_id, Res::Course(course_id)).await;
410
411 return Ok(permission.is_ok());
412 }
413 Ok(true)
414}
415
416pub async fn authorize(
427 conn: &mut PgConnection,
428 action: Action,
429 user_id: Option<Uuid>,
430 resource: Resource,
431) -> Result<AuthorizationToken, ControllerError> {
432 let user_roles = if let Some(user_id) = user_id {
433 models::roles::get_roles(conn, user_id)
434 .await
435 .map_err(|original_err| {
436 ControllerError::new(
437 ControllerErrorType::InternalServerError,
438 format!("Failed to fetch user roles: {}", original_err),
439 Some(original_err.into()),
440 )
441 })?
442 } else {
443 Vec::new()
444 };
445
446 authorize_with_fetched_list_of_roles(conn, action, user_id, resource, &user_roles).await
447}
448
449fn create_authorization_error(user_roles: &[Role], action: Option<Action>) -> ControllerError {
451 let mut detail_message = String::new();
452
453 if user_roles.is_empty() {
454 detail_message.push_str("You don't have any assigned roles.");
455 } else {
456 detail_message.push_str("Your current roles are: ");
457 let roles_str = user_roles
458 .iter()
459 .map(|r| format!("{:?} ({})", r.role, r.domain_description()))
460 .collect::<Vec<_>>()
461 .join(", ");
462 detail_message.push_str(&roles_str);
463 }
464
465 if let Some(act) = action {
466 detail_message.push_str(&format!("\nAction attempted: {:?}", act));
467 }
468
469 ControllerError::new(
471 ControllerErrorType::Forbidden,
472 "Unauthorized. Please contact course staff if you believe you should have access."
473 .to_string(),
474 Some(ControllerError::new(ControllerErrorType::Forbidden, detail_message, None).into()),
475 )
476}
477
478pub async fn authorize_with_fetched_list_of_roles(
480 conn: &mut PgConnection,
481 action: Action,
482 _user_id: Option<Uuid>,
483 resource: Resource,
484 user_roles: &[Role],
485) -> Result<AuthorizationToken, ControllerError> {
486 for role in user_roles {
488 if role.is_global() && has_permission(role.role, action) {
489 return Ok(AuthorizationToken(()));
490 }
491 }
492
493 if resource == Resource::AnyCourse {
495 for role in user_roles {
496 if has_permission(role.role, action) {
497 return Ok(AuthorizationToken(()));
498 }
499 }
500 }
501
502 match resource {
504 Resource::Chapter(id) => {
505 let action =
507 if matches!(action, Action::View) && !models::chapters::is_open(conn, id).await? {
508 Action::ViewMaterial
509 } else {
510 action
511 };
512 let course_id = models::chapters::get_course_id(conn, id).await?;
514 check_course_permission(conn, user_roles, action, course_id).await
515 }
516 Resource::Course(id) => check_course_permission(conn, user_roles, action, id).await,
517 Resource::CourseInstance(id) => {
518 check_course_instance_permission(conn, user_roles, action, id).await
519 }
520 Resource::Exercise(id) => {
521 let course_or_exam_id = models::exercises::get_course_or_exam_id(conn, id).await?;
523 check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
524 }
525 Resource::ExerciseSlideSubmission(id) => {
526 let course_or_exam_id =
528 models::exercise_slide_submissions::get_course_and_exam_id(conn, id).await?;
529 check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
530 }
531 Resource::ExerciseTask(id) => {
532 let course_or_exam_id = models::exercise_tasks::get_course_or_exam_id(conn, id).await?;
534 check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
535 }
536 Resource::ExerciseTaskSubmission(id) => {
537 let course_or_exam_id =
539 models::exercise_task_submissions::get_course_and_exam_id(conn, id).await?;
540 check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
541 }
542 Resource::ExerciseTaskGrading(id) => {
543 let course_or_exam_id =
545 models::exercise_task_gradings::get_course_or_exam_id(conn, id).await?;
546 check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
547 }
548 Resource::Organization(id) => check_organization_permission(user_roles, action, id).await,
549 Resource::Page(id) => {
550 let course_or_exam_id = models::pages::get_course_and_exam_id(conn, id).await?;
552 check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
553 }
554 Resource::StudyRegistry(secret_key) => {
555 check_study_registry_permission(conn, secret_key, action).await
556 }
557 Resource::Exam(exam_id) => check_exam_permission(conn, user_roles, action, exam_id).await,
558 Resource::Role
559 | Resource::User
560 | Resource::AnyCourse
561 | Resource::PlaygroundExample
562 | Resource::ExerciseService
563 | Resource::GlobalPermissions => {
564 Err(create_authorization_error(user_roles, Some(action)))
566 }
567 }
568}
569
570async fn check_organization_permission(
571 roles: &[Role],
572 action: Action,
573 organization_id: Uuid,
574) -> Result<AuthorizationToken, ControllerError> {
575 if action == Action::View {
576 return Ok(AuthorizationToken(()));
578 };
579
580 for role in roles {
582 if role.is_role_for_organization(organization_id) && has_permission(role.role, action) {
583 return Ok(AuthorizationToken(()));
584 }
585 }
586 Err(create_authorization_error(roles, Some(action)))
587}
588
589async fn check_course_permission(
591 conn: &mut PgConnection,
592 roles: &[Role],
593 action: Action,
594 course_id: Uuid,
595) -> Result<AuthorizationToken, ControllerError> {
596 for role in roles {
598 if role.is_role_for_course(course_id) && has_permission(role.role, action) {
599 return Ok(AuthorizationToken(()));
600 }
601 }
602 let organization_id = models::courses::get_organization_id(conn, course_id).await?;
603 check_organization_permission(roles, action, organization_id).await
604}
605
606async fn check_course_instance_permission(
608 conn: &mut PgConnection,
609 roles: &[Role],
610 mut action: Action,
611 course_instance_id: Uuid,
612) -> Result<AuthorizationToken, ControllerError> {
613 if action == Action::View
615 && !models::course_instances::is_open(conn, course_instance_id).await?
616 {
617 action = Action::Teach;
618 }
619
620 for role in roles {
622 if role.is_role_for_course_instance(course_instance_id) && has_permission(role.role, action)
623 {
624 return Ok(AuthorizationToken(()));
625 }
626 }
627 let course_id = models::course_instances::get_course_id(conn, course_instance_id).await?;
628 check_course_permission(conn, roles, action, course_id).await
629}
630
631async fn check_exam_permission(
633 conn: &mut PgConnection,
634 roles: &[Role],
635 action: Action,
636 exam_id: Uuid,
637) -> Result<AuthorizationToken, ControllerError> {
638 for role in roles {
640 if role.is_role_for_exam(exam_id) && has_permission(role.role, action) {
641 return Ok(AuthorizationToken(()));
642 }
643 }
644 let organization_id = models::exams::get_organization_id(conn, exam_id).await?;
645 check_organization_permission(roles, action, organization_id).await
646}
647
648async fn check_course_or_exam_permission(
649 conn: &mut PgConnection,
650 roles: &[Role],
651 action: Action,
652 course_or_exam_id: CourseOrExamId,
653) -> Result<AuthorizationToken, ControllerError> {
654 match course_or_exam_id {
655 CourseOrExamId::Course(course_id) => {
656 check_course_permission(conn, roles, action, course_id).await
657 }
658 CourseOrExamId::Exam(exam_id) => check_exam_permission(conn, roles, action, exam_id).await,
659 }
660}
661
662async fn check_study_registry_permission(
663 conn: &mut PgConnection,
664 secret_key: String,
665 action: Action,
666) -> Result<AuthorizationToken, ControllerError> {
667 let _registrar = models::study_registry_registrars::get_by_secret_key(conn, &secret_key)
668 .await
669 .map_err(|original_error| {
670 ControllerError::new(
671 ControllerErrorType::Forbidden,
672 format!("Study registry access denied: Invalid or missing secret key. The operation {:?} cannot be performed.", action),
673 Some(original_error.into()),
674 )
675 })?;
676 Ok(AuthorizationToken(()))
677}
678
679fn has_permission(user_role: UserRole, action: Action) -> bool {
681 use Action::*;
682 use UserRole::*;
683
684 match user_role {
685 Admin => true,
686 Teacher => matches!(
687 action,
688 View | Teach
689 | Edit
690 | Grade
691 | Duplicate
692 | DeleteAnswer
693 | EditRole(Teacher | Assistant | Reviewer | MaterialViewer | StatsViewer)
694 | CreateCoursesOrExams
695 | ViewMaterial
696 | UploadFile
697 | ViewUserProgressOrDetails
698 | ViewInternalCourseStructure
699 | ViewStats
700 ),
701 Assistant => matches!(
702 action,
703 View | Edit
704 | Grade
705 | DeleteAnswer
706 | EditRole(Assistant | Reviewer | MaterialViewer)
707 | Teach
708 | ViewMaterial
709 | ViewUserProgressOrDetails
710 | ViewInternalCourseStructure
711 ),
712 Reviewer => matches!(
713 action,
714 View | Grade | ViewMaterial | ViewInternalCourseStructure
715 ),
716 CourseOrExamCreator => matches!(action, CreateCoursesOrExams),
717 MaterialViewer => matches!(action, ViewMaterial),
718 TeachingAndLearningServices => {
719 matches!(
720 action,
721 View | ViewMaterial
722 | ViewUserProgressOrDetails
723 | ViewInternalCourseStructure
724 | ViewStats
725 )
726 }
727 StatsViewer => matches!(action, ViewStats),
728 }
729}
730
731pub fn parse_secret_key_from_header(header: &HttpRequest) -> Result<&str, ControllerError> {
732 let raw_token = header
733 .headers()
734 .get("Authorization")
735 .map_or(Ok(""), |x| x.to_str())
736 .map_err(|_| anyhow::anyhow!("Authorization header contains invalid characters."))?;
737 if !raw_token.starts_with("Basic") {
738 return Err(ControllerError::new(
739 ControllerErrorType::Forbidden,
740 "Access denied: Authorization header must use Basic authentication format.".to_string(),
741 None,
742 ));
743 }
744 let secret_key = raw_token.split(' ').nth(1).ok_or_else(|| {
745 ControllerError::new(
746 ControllerErrorType::Forbidden,
747 "Access denied: Malformed authorization token, expected 'Basic <token>' format."
748 .to_string(),
749 None,
750 )
751 })?;
752 Ok(secret_key)
753}
754
755pub async fn authenticate_tmc_mooc_fi_user(
757 conn: &mut PgConnection,
758 client: &OAuthClient,
759 email: String,
760 password: SecretString,
761 tmc_client: &TmcClient,
762) -> anyhow::Result<Option<(User, SecretString)>> {
763 info!("Attempting to authenticate user with TMC");
764 let token = match exchange_password_with_tmc(client, email.clone(), password).await? {
765 Some(token) => token,
766 None => return Ok(None),
767 };
768 debug!("Successfully obtained OAuth token from TMC");
769
770 let tmc_user = tmc_client
771 .get_user_from_tmc_mooc_fi_by_tmc_access_token(&token.clone())
772 .await?;
773 debug!(
774 "Creating or fetching user with TMC id {} and mooc.fi UUID {}",
775 tmc_user.id,
776 tmc_user
777 .courses_mooc_fi_user_id
778 .map(|uuid| uuid.to_string())
779 .unwrap_or_else(|| "None (will fetch from mooc.fi or generate new UUID)".to_string())
780 );
781 let user = get_or_create_user_from_tmc_mooc_fi_response(&mut *conn, tmc_user, &token).await?;
782 info!(
783 "Successfully got user details from mooc.fi for user {}",
784 user.id
785 );
786 info!("Successfully authenticated user {} with mooc.fi", user.id);
787 Ok(Some((user, token)))
788}
789
790pub type LoginToken = StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>;
791
792pub async fn exchange_password_with_tmc(
803 client: &OAuthClient,
804 email: String,
805 password: SecretString,
806) -> anyhow::Result<Option<SecretString>> {
807 let token_result = client
808 .exchange_password(
809 &ResourceOwnerUsername::new(email),
810 &ResourceOwnerPassword::new(password.expose_secret().to_string()),
812 )
813 .request_async(&async_http_client_with_headers)
814 .await;
815 match token_result {
816 Ok(token) => Ok(Some(SecretString::new(
817 token.access_token().secret().to_owned().into(),
818 ))),
819 Err(RequestTokenError::ServerResponse(server_response)) => {
820 let error = server_response.error();
821 let error_description = server_response.error_description();
822 let error_uri = server_response.error_uri();
823
824 if let oauth2::basic::BasicErrorResponseType::InvalidGrant = error {
826 warn!(
827 ?error_description,
828 ?error_uri,
829 "TMC did not accept the credentials: {}",
830 error
831 );
832 Ok(None)
833 } else {
834 error!(
836 ?error_description,
837 ?error_uri,
838 "TMC authentication error: {}",
839 error
840 );
841 Err(anyhow::anyhow!("Authentication error: {}", error))
842 }
843 }
844 Err(e) => {
845 error!("Failed to exchange password with TMC: {}", e);
846 Err(e.into())
847 }
848 }
849}
850
851async fn fetch_moocfi_id_by_upstream_id(
853 tmc_access_token: &SecretString,
854 upstream_id: i32,
855) -> anyhow::Result<Option<Uuid>> {
856 info!("Fetching mooc.fi UUID for upstream user id {}", upstream_id);
857
858 let res = REQWEST_CLIENT
859 .post(MOOCFI_GRAPHQL_URL)
860 .header(reqwest::header::CONTENT_TYPE, "application/json")
861 .header(reqwest::header::ACCEPT, "application/json")
862 .bearer_auth(tmc_access_token.expose_secret())
864 .json(&GraphQLRequest {
865 query: r#"
866query ($upstreamId: Int) {
867 user(upstream_id: $upstreamId) {
868 id
869 }
870}"#,
871 variables: Some(json!({ "upstreamId": upstream_id })),
872 })
873 .send()
874 .await;
875
876 match res {
877 Ok(response) => {
878 if !response.status().is_success() {
879 debug!(
880 "Failed to fetch mooc.fi user with status {}. Will generate new UUID instead.",
881 response.status()
882 );
883 return Ok(None);
884 }
885
886 match response.json::<MoocfiUserResponse>().await {
887 Ok(current_user_response) => {
888 info!(
889 "Successfully fetched mooc.fi UUID {} for upstream id {}",
890 current_user_response.data.user.id, upstream_id
891 );
892 Ok(Some(current_user_response.data.user.id))
893 }
894 Err(e) => {
895 debug!(
896 "Failed to parse mooc.fi response: {}. Will generate new UUID instead.",
897 e
898 );
899 Ok(None)
900 }
901 }
902 }
903 Err(e) => {
904 debug!(
905 "Failed to fetch from mooc.fi: {}. Will generate new UUID instead.",
906 e
907 );
908 Ok(None)
909 }
910 }
911}
912
913pub async fn get_or_create_user_from_tmc_mooc_fi_response(
914 conn: &mut PgConnection,
915 tmc_mooc_fi_user: TMCUser,
916 tmc_access_token: &SecretString,
917) -> anyhow::Result<User> {
918 let TMCUser {
919 id: upstream_id,
920 email,
921 courses_mooc_fi_user_id: moocfi_id,
922 user_field,
923 ..
924 } = tmc_mooc_fi_user;
925
926 let id = match moocfi_id {
928 Some(id) => id,
929 None => match fetch_moocfi_id_by_upstream_id(tmc_access_token, upstream_id).await? {
930 Some(fetched_id) => {
931 info!("Successfully fetched mooc.fi UUID {} for user", fetched_id);
932 fetched_id
933 }
934 None => {
935 info!("No mooc.fi UUID found, generating new UUID for user");
936 Uuid::new_v4()
937 }
938 },
939 };
940
941 let user = match models::users::find_by_upstream_id(conn, upstream_id).await? {
943 Some(existing_user) => existing_user,
944 None => {
945 models::users::insert_with_upstream_id_and_moocfi_id(
946 conn,
947 &email,
948 if user_field.first_name.trim().is_empty() {
950 None
951 } else {
952 Some(user_field.first_name.as_str())
953 },
954 if user_field.last_name.trim().is_empty() {
955 None
956 } else {
957 Some(user_field.last_name.as_str())
958 },
959 upstream_id,
960 id,
961 )
962 .await?
963 }
964 };
965 Ok(user)
966}
967
968pub async fn authenticate_test_user(
972 conn: &mut PgConnection,
973 email: &str,
974 password: &SecretString,
975 application_configuration: &ApplicationConfiguration,
976) -> anyhow::Result<bool> {
977 assert!(application_configuration.test_mode);
979
980 let password = password.expose_secret();
982
983 let _user = if email == "admin@example.com" && password == "admin" {
984 models::users::get_by_email(conn, "admin@example.com").await?
985 } else if email == "teacher@example.com" && password == "teacher" {
986 models::users::get_by_email(conn, "teacher@example.com").await?
987 } else if email == "language.teacher@example.com" && password == "language.teacher" {
988 models::users::get_by_email(conn, "language.teacher@example.com").await?
989 } else if email == "material.viewer@example.com" && password == "material.viewer" {
990 models::users::get_by_email(conn, "material.viewer@example.com").await?
991 } else if email == "user@example.com" && password == "user" {
992 models::users::get_by_email(conn, "user@example.com").await?
993 } else if email == "assistant@example.com" && password == "assistant" {
994 models::users::get_by_email(conn, "assistant@example.com").await?
995 } else if email == "creator@example.com" && password == "creator" {
996 models::users::get_by_email(conn, "creator@example.com").await?
997 } else if email == "student1@example.com" && password == "student1" {
998 models::users::get_by_email(conn, "student1@example.com").await?
999 } else if email == "student2@example.com" && password == "student2" {
1000 models::users::get_by_email(conn, "student2@example.com").await?
1001 } else if email == "student3@example.com" && password == "student3" {
1002 models::users::get_by_email(conn, "student3@example.com").await?
1003 } else if email == "student4@example.com" && password == "student4" {
1004 models::users::get_by_email(conn, "student4@example.com").await?
1005 } else if email == "student5@example.com" && password == "student5" {
1006 models::users::get_by_email(conn, "student5@example.com").await?
1007 } else if email == "student6@example.com" && password == "student6" {
1008 models::users::get_by_email(conn, "student6@example.com").await?
1009 } else if email == "student7@example.com" && password == "student7" {
1010 models::users::get_by_email(conn, "student7@example.com").await?
1011 } else if email == "student8@example.com" && password == "student8" {
1012 models::users::get_by_email(conn, "student8@example.com").await?
1013 } else if email == "teaching-and-learning-services@example.com"
1014 && password == "teaching-and-learning-services"
1015 {
1016 models::users::get_by_email(conn, "teaching-and-learning-services@example.com").await?
1017 } else if email == "student-without-research-consent@example.com"
1018 && password == "student-without-research-consent"
1019 {
1020 models::users::get_by_email(conn, "student-without-research-consent@example.com").await?
1021 } else if email == "student-without-country@example.com"
1022 && password == "student-without-country"
1023 {
1024 models::users::get_by_email(conn, "student-without-country@example.com").await?
1025 } else if email == "langs@example.com" && password == "langs" {
1026 models::users::get_by_email(conn, "langs@example.com").await?
1027 } else if email == "sign-up-user@example.com" && password == "sign-up-user" {
1028 models::users::get_by_email(conn, "sign-up-user@example.com").await?
1029 } else {
1030 info!("Authentication failed: incorrect test credentials");
1031 return Ok(false);
1032 };
1033 info!("Successfully authenticated test user {}", email);
1034 Ok(true)
1035}
1036
1037pub async fn authenticate_test_token(
1039 conn: &mut PgConnection,
1040 _token: &SecretString,
1041 application_configuration: &ApplicationConfiguration,
1042) -> anyhow::Result<User> {
1043 assert!(application_configuration.test_mode);
1045 let user = models::users::get_by_email(conn, "TODO").await?;
1047 Ok(user)
1048}
1049
1050fn get_ratelimit_api_key() -> Result<reqwest::header::HeaderValue, HttpClientError<reqwest::Error>>
1055{
1056 let key = server_runtime_config()
1057 .ratelimit_protection_safe_api_key
1058 .clone();
1059 debug!("Using ratelimit API key from runtime config");
1060
1061 key.expose_secret()
1062 .parse::<reqwest::header::HeaderValue>()
1063 .map_err(|err| {
1064 error!("Invalid RATELIMIT API key format: {}", err);
1065 HttpClientError::Other("Invalid RATELIMIT API key.".to_string())
1066 })
1067}
1068
1069async fn async_http_client_with_headers(
1078 oauth_request: oauth2::HttpRequest,
1079) -> Result<oauth2::HttpResponse, HttpClientError<reqwest::Error>> {
1080 debug!("Making OAuth request to TMC server");
1081
1082 if log::log_enabled!(log::Level::Trace) {
1083 if let Ok(url) = oauth_request.uri().to_string().parse::<reqwest::Url>() {
1085 trace!("OAuth request path: {}", url.path());
1086 }
1087 }
1088
1089 let parsed_key = get_ratelimit_api_key()?;
1090
1091 debug!("Building request to TMC server");
1092 let request = REQWEST_CLIENT
1093 .request(
1094 oauth_request.method().clone(),
1095 oauth_request
1096 .uri()
1097 .to_string()
1098 .parse::<reqwest::Url>()
1099 .map_err(|e| HttpClientError::Other(format!("Invalid URL: {}", e)))?,
1100 )
1101 .headers(oauth_request.headers().clone())
1102 .version(oauth_request.version())
1103 .header("RATELIMIT-PROTECTION-SAFE-API-KEY", parsed_key)
1104 .body(oauth_request.body().to_vec());
1105
1106 debug!("Sending request to TMC server");
1107 let response = request
1108 .send()
1109 .await
1110 .map_err(|e| HttpClientError::Other(format!("Failed to execute request: {}", e)))?;
1111
1112 debug!(
1114 "Received response from TMC server - Status: {}, Version: {:?}",
1115 response.status(),
1116 response.version()
1117 );
1118
1119 let status = response.status();
1120 let version = response.version();
1121 let headers = response.headers().clone();
1122
1123 debug!("Reading response body");
1124 let body_bytes = response
1125 .bytes()
1126 .await
1127 .map_err(|e| HttpClientError::Other(format!("Failed to read response body: {}", e)))?
1128 .to_vec();
1129
1130 debug!("Building OAuth response");
1131 let mut builder = oauth2::http::Response::builder()
1132 .status(status)
1133 .version(version);
1134
1135 if let Some(builder_headers) = builder.headers_mut() {
1136 builder_headers.extend(headers.iter().map(|(k, v)| (k.clone(), v.clone())));
1137 }
1138
1139 let oauth_response = builder
1140 .body(body_bytes)
1141 .map_err(|e| HttpClientError::Other(format!("Failed to construct response: {}", e)))?;
1142
1143 debug!("Successfully completed OAuth request");
1144 Ok(oauth_response)
1145}
1146
1147#[cfg(test)]
1148mod test {
1149 use super::*;
1150 use crate::test_helper::*;
1151 use headless_lms_models::*;
1152 use models::roles::RoleDomain;
1153
1154 #[actix_web::test]
1155 async fn test_authorization() {
1156 let mut conn = Conn::init().await;
1157 let mut tx = conn.begin().await;
1158
1159 let user = users::insert(
1160 tx.as_mut(),
1161 PKeyPolicy::Generate,
1162 "auth@example.com",
1163 None,
1164 None,
1165 )
1166 .await
1167 .unwrap();
1168 let org = organizations::insert(
1169 tx.as_mut(),
1170 PKeyPolicy::Generate,
1171 "auth",
1172 "auth",
1173 Some("auth"),
1174 false,
1175 )
1176 .await
1177 .unwrap();
1178
1179 authorize(
1180 tx.as_mut(),
1181 Action::Edit,
1182 Some(user),
1183 Resource::Organization(org),
1184 )
1185 .await
1186 .unwrap_err();
1187
1188 roles::insert(
1189 tx.as_mut(),
1190 user,
1191 UserRole::Teacher,
1192 RoleDomain::Organization(org),
1193 )
1194 .await
1195 .unwrap();
1196
1197 authorize(
1198 tx.as_mut(),
1199 Action::Edit,
1200 Some(user),
1201 Resource::Organization(org),
1202 )
1203 .await
1204 .unwrap();
1205 }
1206
1207 #[actix_web::test]
1208 async fn course_role_chapter_resource() {
1209 insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module, :chapter);
1210
1211 authorize(
1212 tx.as_mut(),
1213 Action::Edit,
1214 Some(user),
1215 Resource::Chapter(chapter),
1216 )
1217 .await
1218 .unwrap_err();
1219
1220 roles::insert(
1221 tx.as_mut(),
1222 user,
1223 UserRole::Teacher,
1224 RoleDomain::Course(course),
1225 )
1226 .await
1227 .unwrap();
1228
1229 authorize(
1230 tx.as_mut(),
1231 Action::Edit,
1232 Some(user),
1233 Resource::Chapter(chapter),
1234 )
1235 .await
1236 .unwrap();
1237 }
1238
1239 #[actix_web::test]
1240 async fn anonymous_user_can_view_open_course() {
1241 insert_data!(:tx, :user, :org, :course);
1242
1243 authorize(tx.as_mut(), Action::View, None, Resource::Course(course))
1244 .await
1245 .unwrap();
1246 }
1247}