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.as_str(),
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: String,
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: String,
806) -> anyhow::Result<Option<SecretString>> {
807 let token_result = client
808 .exchange_password(
809 &ResourceOwnerUsername::new(email),
810 &ResourceOwnerPassword::new(password),
811 )
812 .request_async(&async_http_client_with_headers)
813 .await;
814 match token_result {
815 Ok(token) => Ok(Some(SecretString::new(
816 token.access_token().secret().to_owned().into(),
817 ))),
818 Err(RequestTokenError::ServerResponse(server_response)) => {
819 let error = server_response.error();
820 let error_description = server_response.error_description();
821 let error_uri = server_response.error_uri();
822
823 if let oauth2::basic::BasicErrorResponseType::InvalidGrant = error {
825 warn!(
826 ?error_description,
827 ?error_uri,
828 "TMC did not accept the credentials: {}",
829 error
830 );
831 Ok(None)
832 } else {
833 error!(
835 ?error_description,
836 ?error_uri,
837 "TMC authentication error: {}",
838 error
839 );
840 Err(anyhow::anyhow!("Authentication error: {}", error))
841 }
842 }
843 Err(e) => {
844 error!("Failed to exchange password with TMC: {}", e);
845 Err(e.into())
846 }
847 }
848}
849
850async fn fetch_moocfi_id_by_upstream_id(
852 tmc_access_token: &str,
853 upstream_id: i32,
854) -> anyhow::Result<Option<Uuid>> {
855 info!("Fetching mooc.fi UUID for upstream user id {}", upstream_id);
856
857 let res = REQWEST_CLIENT
858 .post(MOOCFI_GRAPHQL_URL)
859 .header(reqwest::header::CONTENT_TYPE, "application/json")
860 .header(reqwest::header::ACCEPT, "application/json")
861 .bearer_auth(tmc_access_token)
862 .json(&GraphQLRequest {
863 query: r#"
864query ($upstreamId: Int) {
865 user(upstream_id: $upstreamId) {
866 id
867 }
868}"#,
869 variables: Some(json!({ "upstreamId": upstream_id })),
870 })
871 .send()
872 .await;
873
874 match res {
875 Ok(response) => {
876 if !response.status().is_success() {
877 debug!(
878 "Failed to fetch mooc.fi user with status {}. Will generate new UUID instead.",
879 response.status()
880 );
881 return Ok(None);
882 }
883
884 match response.json::<MoocfiUserResponse>().await {
885 Ok(current_user_response) => {
886 info!(
887 "Successfully fetched mooc.fi UUID {} for upstream id {}",
888 current_user_response.data.user.id, upstream_id
889 );
890 Ok(Some(current_user_response.data.user.id))
891 }
892 Err(e) => {
893 debug!(
894 "Failed to parse mooc.fi response: {}. Will generate new UUID instead.",
895 e
896 );
897 Ok(None)
898 }
899 }
900 }
901 Err(e) => {
902 debug!(
903 "Failed to fetch from mooc.fi: {}. Will generate new UUID instead.",
904 e
905 );
906 Ok(None)
907 }
908 }
909}
910
911pub async fn get_or_create_user_from_tmc_mooc_fi_response(
912 conn: &mut PgConnection,
913 tmc_mooc_fi_user: TMCUser,
914 tmc_access_token: &SecretString,
915) -> anyhow::Result<User> {
916 let TMCUser {
917 id: upstream_id,
918 email,
919 courses_mooc_fi_user_id: moocfi_id,
920 user_field,
921 ..
922 } = tmc_mooc_fi_user;
923
924 let id = match moocfi_id {
926 Some(id) => id,
927 None => match fetch_moocfi_id_by_upstream_id(tmc_access_token.expose_secret(), upstream_id)
928 .await?
929 {
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: &str,
975 application_configuration: &ApplicationConfiguration,
976) -> anyhow::Result<bool> {
977 assert!(application_configuration.test_mode);
979
980 let _user = if email == "admin@example.com" && password == "admin" {
981 models::users::get_by_email(conn, "admin@example.com").await?
982 } else if email == "teacher@example.com" && password == "teacher" {
983 models::users::get_by_email(conn, "teacher@example.com").await?
984 } else if email == "language.teacher@example.com" && password == "language.teacher" {
985 models::users::get_by_email(conn, "language.teacher@example.com").await?
986 } else if email == "material.viewer@example.com" && password == "material.viewer" {
987 models::users::get_by_email(conn, "material.viewer@example.com").await?
988 } else if email == "user@example.com" && password == "user" {
989 models::users::get_by_email(conn, "user@example.com").await?
990 } else if email == "assistant@example.com" && password == "assistant" {
991 models::users::get_by_email(conn, "assistant@example.com").await?
992 } else if email == "creator@example.com" && password == "creator" {
993 models::users::get_by_email(conn, "creator@example.com").await?
994 } else if email == "student1@example.com" && password == "student1" {
995 models::users::get_by_email(conn, "student1@example.com").await?
996 } else if email == "student2@example.com" && password == "student2" {
997 models::users::get_by_email(conn, "student2@example.com").await?
998 } else if email == "student3@example.com" && password == "student3" {
999 models::users::get_by_email(conn, "student3@example.com").await?
1000 } else if email == "student4@example.com" && password == "student4" {
1001 models::users::get_by_email(conn, "student4@example.com").await?
1002 } else if email == "student5@example.com" && password == "student5" {
1003 models::users::get_by_email(conn, "student5@example.com").await?
1004 } else if email == "student6@example.com" && password == "student6" {
1005 models::users::get_by_email(conn, "student6@example.com").await?
1006 } else if email == "student7@example.com" && password == "student7" {
1007 models::users::get_by_email(conn, "student7@example.com").await?
1008 } else if email == "student8@example.com" && password == "student8" {
1009 models::users::get_by_email(conn, "student8@example.com").await?
1010 } else if email == "teaching-and-learning-services@example.com"
1011 && password == "teaching-and-learning-services"
1012 {
1013 models::users::get_by_email(conn, "teaching-and-learning-services@example.com").await?
1014 } else if email == "student-without-research-consent@example.com"
1015 && password == "student-without-research-consent"
1016 {
1017 models::users::get_by_email(conn, "student-without-research-consent@example.com").await?
1018 } else if email == "student-without-country@example.com"
1019 && password == "student-without-country"
1020 {
1021 models::users::get_by_email(conn, "student-without-country@example.com").await?
1022 } else if email == "langs@example.com" && password == "langs" {
1023 models::users::get_by_email(conn, "langs@example.com").await?
1024 } else if email == "sign-up-user@example.com" && password == "sign-up-user" {
1025 models::users::get_by_email(conn, "sign-up-user@example.com").await?
1026 } else {
1027 info!("Authentication failed: incorrect test credentials");
1028 return Ok(false);
1029 };
1030 info!("Successfully authenticated test user {}", email);
1031 Ok(true)
1032}
1033
1034pub async fn authenticate_test_token(
1036 conn: &mut PgConnection,
1037 _token: &SecretString,
1038 application_configuration: &ApplicationConfiguration,
1039) -> anyhow::Result<User> {
1040 assert!(application_configuration.test_mode);
1042 let user = models::users::get_by_email(conn, "TODO").await?;
1044 Ok(user)
1045}
1046
1047fn get_ratelimit_api_key() -> Result<reqwest::header::HeaderValue, HttpClientError<reqwest::Error>>
1052{
1053 let key = server_runtime_config()
1054 .ratelimit_protection_safe_api_key
1055 .clone();
1056 debug!("Using ratelimit API key from runtime config");
1057
1058 key.parse::<reqwest::header::HeaderValue>().map_err(|err| {
1059 error!("Invalid RATELIMIT API key format: {}", err);
1060 HttpClientError::Other("Invalid RATELIMIT API key.".to_string())
1061 })
1062}
1063
1064async fn async_http_client_with_headers(
1073 oauth_request: oauth2::HttpRequest,
1074) -> Result<oauth2::HttpResponse, HttpClientError<reqwest::Error>> {
1075 debug!("Making OAuth request to TMC server");
1076
1077 if log::log_enabled!(log::Level::Trace) {
1078 if let Ok(url) = oauth_request.uri().to_string().parse::<reqwest::Url>() {
1080 trace!("OAuth request path: {}", url.path());
1081 }
1082 }
1083
1084 let parsed_key = get_ratelimit_api_key()?;
1085
1086 debug!("Building request to TMC server");
1087 let request = REQWEST_CLIENT
1088 .request(
1089 oauth_request.method().clone(),
1090 oauth_request
1091 .uri()
1092 .to_string()
1093 .parse::<reqwest::Url>()
1094 .map_err(|e| HttpClientError::Other(format!("Invalid URL: {}", e)))?,
1095 )
1096 .headers(oauth_request.headers().clone())
1097 .version(oauth_request.version())
1098 .header("RATELIMIT-PROTECTION-SAFE-API-KEY", parsed_key)
1099 .body(oauth_request.body().to_vec());
1100
1101 debug!("Sending request to TMC server");
1102 let response = request
1103 .send()
1104 .await
1105 .map_err(|e| HttpClientError::Other(format!("Failed to execute request: {}", e)))?;
1106
1107 debug!(
1109 "Received response from TMC server - Status: {}, Version: {:?}",
1110 response.status(),
1111 response.version()
1112 );
1113
1114 let status = response.status();
1115 let version = response.version();
1116 let headers = response.headers().clone();
1117
1118 debug!("Reading response body");
1119 let body_bytes = response
1120 .bytes()
1121 .await
1122 .map_err(|e| HttpClientError::Other(format!("Failed to read response body: {}", e)))?
1123 .to_vec();
1124
1125 debug!("Building OAuth response");
1126 let mut builder = oauth2::http::Response::builder()
1127 .status(status)
1128 .version(version);
1129
1130 if let Some(builder_headers) = builder.headers_mut() {
1131 builder_headers.extend(headers.iter().map(|(k, v)| (k.clone(), v.clone())));
1132 }
1133
1134 let oauth_response = builder
1135 .body(body_bytes)
1136 .map_err(|e| HttpClientError::Other(format!("Failed to construct response: {}", e)))?;
1137
1138 debug!("Successfully completed OAuth request");
1139 Ok(oauth_response)
1140}
1141
1142#[cfg(test)]
1143mod test {
1144 use super::*;
1145 use crate::test_helper::*;
1146 use headless_lms_models::*;
1147 use models::roles::RoleDomain;
1148
1149 #[actix_web::test]
1150 async fn test_authorization() {
1151 let mut conn = Conn::init().await;
1152 let mut tx = conn.begin().await;
1153
1154 let user = users::insert(
1155 tx.as_mut(),
1156 PKeyPolicy::Generate,
1157 "auth@example.com",
1158 None,
1159 None,
1160 )
1161 .await
1162 .unwrap();
1163 let org = organizations::insert(
1164 tx.as_mut(),
1165 PKeyPolicy::Generate,
1166 "auth",
1167 "auth",
1168 Some("auth"),
1169 false,
1170 )
1171 .await
1172 .unwrap();
1173
1174 authorize(
1175 tx.as_mut(),
1176 Action::Edit,
1177 Some(user),
1178 Resource::Organization(org),
1179 )
1180 .await
1181 .unwrap_err();
1182
1183 roles::insert(
1184 tx.as_mut(),
1185 user,
1186 UserRole::Teacher,
1187 RoleDomain::Organization(org),
1188 )
1189 .await
1190 .unwrap();
1191
1192 authorize(
1193 tx.as_mut(),
1194 Action::Edit,
1195 Some(user),
1196 Resource::Organization(org),
1197 )
1198 .await
1199 .unwrap();
1200 }
1201
1202 #[actix_web::test]
1203 async fn course_role_chapter_resource() {
1204 insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module, :chapter);
1205
1206 authorize(
1207 tx.as_mut(),
1208 Action::Edit,
1209 Some(user),
1210 Resource::Chapter(chapter),
1211 )
1212 .await
1213 .unwrap_err();
1214
1215 roles::insert(
1216 tx.as_mut(),
1217 user,
1218 UserRole::Teacher,
1219 RoleDomain::Course(course),
1220 )
1221 .await
1222 .unwrap();
1223
1224 authorize(
1225 tx.as_mut(),
1226 Action::Edit,
1227 Some(user),
1228 Resource::Chapter(chapter),
1229 )
1230 .await
1231 .unwrap();
1232 }
1233
1234 #[actix_web::test]
1235 async fn anonymous_user_can_view_open_course() {
1236 insert_data!(:tx, :user, :org, :course);
1237
1238 authorize(tx.as_mut(), Action::View, None, Resource::Course(course))
1239 .await
1240 .unwrap();
1241 }
1242}