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