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 Administrate,
195}
196
197#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
199#[cfg_attr(feature = "ts_rs", derive(TS))]
200#[serde(rename_all = "snake_case", tag = "type", content = "id")]
201pub enum Resource {
202 GlobalPermissions,
203 Chapter(Uuid),
204 Course(Uuid),
205 CourseInstance(Uuid),
206 Exam(Uuid),
207 Exercise(Uuid),
208 ExerciseSlideSubmission(Uuid),
209 ExerciseTask(Uuid),
210 ExerciseTaskGrading(Uuid),
211 ExerciseTaskSubmission(Uuid),
212 Organization(Uuid),
213 Page(Uuid),
214 StudyRegistry(String),
215 AnyCourse,
216 Role,
217 User,
218 PlaygroundExample,
219 ExerciseService,
220}
221
222impl Resource {
223 pub fn from_course_or_exam_id(course_or_exam_id: CourseOrExamId) -> Self {
224 match course_or_exam_id {
225 CourseOrExamId::Course(id) => Self::Course(id),
226 CourseOrExamId::Exam(id) => Self::Exam(id),
227 }
228 }
229}
230
231#[derive(Copy, Clone, Debug)]
233pub struct AuthorizationToken(());
234
235impl AuthorizationToken {
236 pub fn authorized_ok<T>(self, t: T) -> ControllerResult<T> {
237 Ok(AuthorizedResponse {
238 data: t,
239 token: self,
240 })
241 }
242}
243
244#[derive(Copy, Clone)]
246pub struct AuthorizedResponse<T> {
247 pub data: T,
248 pub token: AuthorizationToken,
249}
250
251impl<T: Responder> Responder for AuthorizedResponse<T> {
252 type Body = T::Body;
253
254 fn respond_to(self, req: &HttpRequest) -> actix_web::HttpResponse<Self::Body> {
255 T::respond_to(self.data, req)
256 }
257}
258
259pub fn skip_authorize() -> AuthorizationToken {
277 AuthorizationToken(())
278}
279
280pub async fn authorize_access_to_course_material(
282 conn: &mut PgConnection,
283 user_id: Option<Uuid>,
284 course_id: Uuid,
285) -> Result<AuthorizationToken, ControllerError> {
286 let token = if models::courses::is_draft(conn, course_id).await? {
287 info!("Course is in draft mode");
288 if user_id.is_none() {
289 return Err(ControllerError::new(
290 ControllerErrorType::Unauthorized,
291 "This course is currently in draft mode and not publicly available. Please log in if you have access permissions.".to_string(),
292 None,
293 ));
294 }
295 authorize(conn, Act::ViewMaterial, user_id, Res::Course(course_id)).await?
296 } else if models::courses::is_joinable_by_code_only(conn, course_id).await? {
297 info!("Course is joinable by code only");
298 if models::join_code_uses::check_if_user_has_access_to_course(
299 conn,
300 user_id.unwrap(),
301 course_id,
302 )
303 .await
304 .is_err()
305 {
306 authorize(conn, Act::ViewMaterial, user_id, Res::Course(course_id)).await?;
307 }
308 skip_authorize()
309 } else {
310 skip_authorize()
312 };
313
314 Ok(token)
315}
316
317pub async fn authorize_access_to_tmc_server(
319 request: &HttpRequest,
320) -> Result<AuthorizationToken, ControllerError> {
321 let tmc_server_secret_for_communicating_to_secret_project =
322 env::var("TMC_SERVER_SECRET_FOR_COMMUNICATING_TO_SECRET_PROJECT")
323 .expect("TMC_SERVER_SECRET_FOR_COMMUNICATING_TO_SECRET_PROJECT must be defined");
324 let auth_header = request
326 .headers()
327 .get("Authorization")
328 .ok_or_else(|| {
329 ControllerError::new(
330 ControllerErrorType::Unauthorized,
331 "TMC server authorization failed: Missing Authorization header.".to_string(),
332 None,
333 )
334 })?
335 .to_str()
336 .map_err(|_| {
337 ControllerError::new(
338 ControllerErrorType::Unauthorized,
339 "TMC server authorization failed: Invalid Authorization header format.".to_string(),
340 None,
341 )
342 })?;
343 if auth_header == tmc_server_secret_for_communicating_to_secret_project {
345 return Ok(skip_authorize());
346 }
347 Err(ControllerError::new(
348 ControllerErrorType::Unauthorized,
349 "TMC server authorization failed: Invalid authorization token.".to_string(),
350 None,
351 ))
352}
353
354pub async fn can_user_view_chapter(
356 conn: &mut PgConnection,
357 user_id: Option<Uuid>,
358 course_id: Option<Uuid>,
359 chapter_id: Option<Uuid>,
360) -> Result<bool, ControllerError> {
361 if let Some(course_id) = course_id {
362 if let Some(chapter_id) = chapter_id {
363 if !models::chapters::is_open(&mut *conn, chapter_id).await? {
364 if user_id.is_none() {
365 return Ok(false);
366 }
367 let permission =
370 authorize(conn, Act::ViewMaterial, user_id, Res::Course(course_id)).await;
371
372 return Ok(permission.is_ok());
373 }
374 }
375 }
376 Ok(true)
377}
378
379pub async fn authorize(
390 conn: &mut PgConnection,
391 action: Action,
392 user_id: Option<Uuid>,
393 resource: Resource,
394) -> Result<AuthorizationToken, ControllerError> {
395 let user_roles = if let Some(user_id) = user_id {
396 models::roles::get_roles(conn, user_id)
397 .await
398 .map_err(|original_err| {
399 ControllerError::new(
400 ControllerErrorType::InternalServerError,
401 format!("Failed to fetch user roles: {}", original_err),
402 Some(original_err.into()),
403 )
404 })?
405 } else {
406 Vec::new()
407 };
408
409 authorize_with_fetched_list_of_roles(conn, action, user_id, resource, &user_roles).await
410}
411
412fn create_authorization_error(user_roles: &[Role], action: Option<Action>) -> ControllerError {
414 let mut detail_message = String::new();
415
416 if user_roles.is_empty() {
417 detail_message.push_str("You don't have any assigned roles.");
418 } else {
419 detail_message.push_str("Your current roles are: ");
420 let roles_str = user_roles
421 .iter()
422 .map(|r| format!("{:?} ({})", r.role, r.domain_description()))
423 .collect::<Vec<_>>()
424 .join(", ");
425 detail_message.push_str(&roles_str);
426 }
427
428 if let Some(act) = action {
429 detail_message.push_str(&format!("\nAction attempted: {:?}", act));
430 }
431
432 ControllerError::new(
434 ControllerErrorType::Forbidden,
435 "Unauthorized. Please contact course staff if you believe you should have access."
436 .to_string(),
437 Some(ControllerError::new(ControllerErrorType::Forbidden, detail_message, None).into()),
438 )
439}
440
441pub async fn authorize_with_fetched_list_of_roles(
443 conn: &mut PgConnection,
444 action: Action,
445 _user_id: Option<Uuid>,
446 resource: Resource,
447 user_roles: &[Role],
448) -> Result<AuthorizationToken, ControllerError> {
449 for role in user_roles {
451 if role.is_global() && has_permission(role.role, action) {
452 return Ok(AuthorizationToken(()));
453 }
454 }
455
456 if resource == Resource::AnyCourse {
458 for role in user_roles {
459 if has_permission(role.role, action) {
460 return Ok(AuthorizationToken(()));
461 }
462 }
463 }
464
465 match resource {
467 Resource::Chapter(id) => {
468 let action =
470 if matches!(action, Action::View) && !models::chapters::is_open(conn, id).await? {
471 Action::ViewMaterial
472 } else {
473 action
474 };
475 let course_id = models::chapters::get_course_id(conn, id).await?;
477 check_course_permission(conn, user_roles, action, course_id).await
478 }
479 Resource::Course(id) => check_course_permission(conn, user_roles, action, id).await,
480 Resource::CourseInstance(id) => {
481 check_course_instance_permission(conn, user_roles, action, id).await
482 }
483 Resource::Exercise(id) => {
484 let course_or_exam_id = models::exercises::get_course_or_exam_id(conn, id).await?;
486 check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
487 }
488 Resource::ExerciseSlideSubmission(id) => {
489 let course_or_exam_id =
491 models::exercise_slide_submissions::get_course_and_exam_id(conn, id).await?;
492 check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
493 }
494 Resource::ExerciseTask(id) => {
495 let course_or_exam_id = models::exercise_tasks::get_course_or_exam_id(conn, id).await?;
497 check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
498 }
499 Resource::ExerciseTaskSubmission(id) => {
500 let course_or_exam_id =
502 models::exercise_task_submissions::get_course_and_exam_id(conn, id).await?;
503 check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
504 }
505 Resource::ExerciseTaskGrading(id) => {
506 let course_or_exam_id =
508 models::exercise_task_gradings::get_course_or_exam_id(conn, id).await?;
509 check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
510 }
511 Resource::Organization(id) => check_organization_permission(user_roles, action, id).await,
512 Resource::Page(id) => {
513 let course_or_exam_id = models::pages::get_course_and_exam_id(conn, id).await?;
515 check_course_or_exam_permission(conn, user_roles, action, course_or_exam_id).await
516 }
517 Resource::StudyRegistry(secret_key) => {
518 check_study_registry_permission(conn, secret_key, action).await
519 }
520 Resource::Exam(exam_id) => check_exam_permission(conn, user_roles, action, exam_id).await,
521 Resource::Role
522 | Resource::User
523 | Resource::AnyCourse
524 | Resource::PlaygroundExample
525 | Resource::ExerciseService
526 | Resource::GlobalPermissions => {
527 Err(create_authorization_error(user_roles, Some(action)))
529 }
530 }
531}
532
533async fn check_organization_permission(
534 roles: &[Role],
535 action: Action,
536 organization_id: Uuid,
537) -> Result<AuthorizationToken, ControllerError> {
538 if action == Action::View {
539 return Ok(AuthorizationToken(()));
541 };
542
543 for role in roles {
545 if role.is_role_for_organization(organization_id) && has_permission(role.role, action) {
546 return Ok(AuthorizationToken(()));
547 }
548 }
549 Err(create_authorization_error(roles, Some(action)))
550}
551
552async fn check_course_permission(
554 conn: &mut PgConnection,
555 roles: &[Role],
556 action: Action,
557 course_id: Uuid,
558) -> Result<AuthorizationToken, ControllerError> {
559 for role in roles {
561 if role.is_role_for_course(course_id) && has_permission(role.role, action) {
562 return Ok(AuthorizationToken(()));
563 }
564 }
565 let organization_id = models::courses::get_organization_id(conn, course_id).await?;
566 check_organization_permission(roles, action, organization_id).await
567}
568
569async fn check_course_instance_permission(
571 conn: &mut PgConnection,
572 roles: &[Role],
573 mut action: Action,
574 course_instance_id: Uuid,
575) -> Result<AuthorizationToken, ControllerError> {
576 if action == Action::View
578 && !models::course_instances::is_open(conn, course_instance_id).await?
579 {
580 action = Action::Teach;
581 }
582
583 for role in roles {
585 if role.is_role_for_course_instance(course_instance_id) && has_permission(role.role, action)
586 {
587 return Ok(AuthorizationToken(()));
588 }
589 }
590 let course_id = models::course_instances::get_course_id(conn, course_instance_id).await?;
591 check_course_permission(conn, roles, action, course_id).await
592}
593
594async fn check_exam_permission(
596 conn: &mut PgConnection,
597 roles: &[Role],
598 action: Action,
599 exam_id: Uuid,
600) -> Result<AuthorizationToken, ControllerError> {
601 for role in roles {
603 if role.is_role_for_exam(exam_id) && has_permission(role.role, action) {
604 return Ok(AuthorizationToken(()));
605 }
606 }
607 let organization_id = models::exams::get_organization_id(conn, exam_id).await?;
608 check_organization_permission(roles, action, organization_id).await
609}
610
611async fn check_course_or_exam_permission(
612 conn: &mut PgConnection,
613 roles: &[Role],
614 action: Action,
615 course_or_exam_id: CourseOrExamId,
616) -> Result<AuthorizationToken, ControllerError> {
617 match course_or_exam_id {
618 CourseOrExamId::Course(course_id) => {
619 check_course_permission(conn, roles, action, course_id).await
620 }
621 CourseOrExamId::Exam(exam_id) => check_exam_permission(conn, roles, action, exam_id).await,
622 }
623}
624
625async fn check_study_registry_permission(
626 conn: &mut PgConnection,
627 secret_key: String,
628 action: Action,
629) -> Result<AuthorizationToken, ControllerError> {
630 let _registrar = models::study_registry_registrars::get_by_secret_key(conn, &secret_key)
631 .await
632 .map_err(|original_error| {
633 ControllerError::new(
634 ControllerErrorType::Forbidden,
635 format!("Study registry access denied: Invalid or missing secret key. The operation {:?} cannot be performed.", action),
636 Some(original_error.into()),
637 )
638 })?;
639 Ok(AuthorizationToken(()))
640}
641
642fn has_permission(user_role: UserRole, action: Action) -> bool {
644 use Action::*;
645 use UserRole::*;
646
647 match user_role {
648 Admin => true,
649 Teacher => matches!(
650 action,
651 View | Teach
652 | Edit
653 | Grade
654 | Duplicate
655 | DeleteAnswer
656 | EditRole(Teacher | Assistant | Reviewer | MaterialViewer | StatsViewer)
657 | CreateCoursesOrExams
658 | ViewMaterial
659 | UploadFile
660 | ViewUserProgressOrDetails
661 | ViewInternalCourseStructure
662 | ViewStats
663 ),
664 Assistant => matches!(
665 action,
666 View | Edit
667 | Grade
668 | DeleteAnswer
669 | EditRole(Assistant | Reviewer | MaterialViewer)
670 | Teach
671 | ViewMaterial
672 | ViewUserProgressOrDetails
673 | ViewInternalCourseStructure
674 ),
675 Reviewer => matches!(
676 action,
677 View | Grade | ViewMaterial | ViewInternalCourseStructure
678 ),
679 CourseOrExamCreator => matches!(action, CreateCoursesOrExams),
680 MaterialViewer => matches!(action, ViewMaterial),
681 TeachingAndLearningServices => {
682 matches!(
683 action,
684 View | ViewMaterial
685 | ViewUserProgressOrDetails
686 | ViewInternalCourseStructure
687 | ViewStats
688 )
689 }
690 StatsViewer => matches!(action, ViewStats),
691 }
692}
693
694pub fn parse_secret_key_from_header(header: &HttpRequest) -> Result<&str, ControllerError> {
695 let raw_token = header
696 .headers()
697 .get("Authorization")
698 .map_or(Ok(""), |x| x.to_str())
699 .map_err(|_| anyhow::anyhow!("Authorization header contains invalid characters."))?;
700 if !raw_token.starts_with("Basic") {
701 return Err(ControllerError::new(
702 ControllerErrorType::Forbidden,
703 "Access denied: Authorization header must use Basic authentication format.".to_string(),
704 None,
705 ));
706 }
707 let secret_key = raw_token.split(' ').nth(1).ok_or_else(|| {
708 ControllerError::new(
709 ControllerErrorType::Forbidden,
710 "Access denied: Malformed authorization token, expected 'Basic <token>' format."
711 .to_string(),
712 None,
713 )
714 })?;
715 Ok(secret_key)
716}
717
718pub async fn authenticate_moocfi_user(
720 conn: &mut PgConnection,
721 client: &OAuthClient,
722 email: String,
723 password: String,
724) -> anyhow::Result<Option<(User, LoginToken)>> {
725 info!("Attempting to authenticate user with TMC");
726 let token = match exchange_password_with_tmc(client, email.clone(), password).await? {
727 Some(token) => token,
728 None => return Ok(None),
729 };
730 debug!("Successfully obtained OAuth token from TMC");
731 let user = get_user_from_moocfi_by_login_token(&token, conn).await?;
732 info!("Successfully authenticated user {} with mooc.fi", user.id);
733 Ok(Some((user, token)))
734}
735
736pub type LoginToken = StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>;
737
738pub async fn exchange_password_with_tmc(
749 client: &OAuthClient,
750 email: String,
751 password: String,
752) -> anyhow::Result<Option<LoginToken>> {
753 let token_result = client
754 .exchange_password(
755 &ResourceOwnerUsername::new(email),
756 &ResourceOwnerPassword::new(password),
757 )
758 .request_async(&async_http_client_with_headers)
759 .await;
760 match token_result {
761 Ok(token) => Ok(Some(token)),
762 Err(RequestTokenError::ServerResponse(server_response)) => {
763 let error = server_response.error();
764 let error_description = server_response.error_description();
765 let error_uri = server_response.error_uri();
766
767 if let oauth2::basic::BasicErrorResponseType::InvalidGrant = error {
769 warn!(
770 ?error_description,
771 ?error_uri,
772 "TMC did not accept the credentials: {}",
773 error
774 );
775 Ok(None)
776 } else {
777 error!(
779 ?error_description,
780 ?error_uri,
781 "TMC authentication error: {}",
782 error
783 );
784 Err(anyhow::anyhow!("Authentication error: {}", error))
785 }
786 }
787 Err(e) => {
788 error!("Failed to exchange password with TMC: {}", e);
789 Err(e.into())
790 }
791 }
792}
793
794#[derive(Debug, Serialize, Deserialize)]
795struct GraphQLRequest<'a> {
796 query: &'a str,
797 #[serde(skip_serializing_if = "Option::is_none")]
798 variables: Option<serde_json::Value>,
799}
800
801#[derive(Debug, Serialize, Deserialize)]
802struct MoocfiUserResponse {
803 pub data: MoocfiUserResponseData,
804}
805
806#[derive(Debug, Serialize, Deserialize)]
807struct MoocfiUserResponseData {
808 pub user: MoocfiUser,
809}
810
811#[derive(Debug, Serialize, Deserialize)]
812struct MoocfiUser {
813 pub id: Uuid,
814 pub first_name: Option<String>,
815 pub last_name: Option<String>,
816 pub email: String,
817 pub upstream_id: i32,
818}
819
820pub async fn get_user_from_moocfi_by_login_token(
821 token: &LoginToken,
822 conn: &mut PgConnection,
823) -> anyhow::Result<User> {
824 info!("Getting user details from mooc.fi");
825
826 let res = REQWEST_CLIENT
827 .post(MOOCFI_GRAPHQL_URL)
828 .header(reqwest::header::CONTENT_TYPE, "application/json")
829 .header(reqwest::header::ACCEPT, "application/json")
830 .json(&GraphQLRequest {
831 query: r#"
832{
833 user: currentUser {
834 id
835 email
836 first_name
837 last_name
838 upstream_id
839 }
840}"#,
841 variables: None,
842 })
843 .bearer_auth(token.access_token().secret())
844 .send()
845 .await
846 .context("Failed to send request to Mooc.fi")?;
847
848 if !res.status().is_success() {
849 error!(
850 "Failed to get user from mooc.fi with status {}",
851 res.status()
852 );
853 return Err(anyhow::anyhow!("Failed to get current user from Mooc.fi"));
854 }
855
856 debug!("Received response from mooc.fi, parsing user data");
857 let current_user_response: MoocfiUserResponse = res
858 .json()
859 .await
860 .context("Unexpected response from Mooc.fi")?;
861
862 debug!(
863 "Creating or fetching user with mooc.fi id {}",
864 current_user_response.data.user.id
865 );
866 let user = get_or_create_user_from_moocfi_response(&mut *conn, current_user_response.data.user)
867 .await?;
868 info!(
869 "Successfully got user details from mooc.fi for user {}",
870 user.id
871 );
872
873 Ok(user)
874}
875
876pub async fn get_user_from_moocfi_by_tmc_access_token_and_upstream_id(
877 conn: &mut PgConnection,
878 tmc_access_token: &str,
879 upstream_id: &i32,
880) -> anyhow::Result<User> {
881 info!("Getting user details from mooc.fi");
882 let client = reqwest::Client::default();
883
884 let res = client
885 .post(MOOCFI_GRAPHQL_URL)
886 .header(reqwest::header::CONTENT_TYPE, "application/json")
887 .header(reqwest::header::ACCEPT, "application/json")
888 .bearer_auth(tmc_access_token)
889 .json(&GraphQLRequest {
890 query: r#"
891query ($upstreamId: Int) {
892 user(upstream_id: $upstreamId) {
893 id
894 email
895 first_name
896 last_name
897 upstream_id
898 }
899}"#,
900 variables: Some(json!({ "upstreamId": upstream_id })),
901 })
902 .send()
903 .await
904 .context("Failed to send request to Mooc.fi")?;
905 if !res.status().is_success() {
906 return Err(anyhow::anyhow!("Failed to get current user from Mooc.fi"));
907 }
908 let current_user_response: MoocfiUserResponse = res
909 .json()
910 .await
911 .context("Unexpected response from Mooc.fi")?;
912
913 let user = get_or_create_user_from_moocfi_response(&mut *conn, current_user_response.data.user)
914 .await?;
915 Ok(user)
916}
917
918async fn get_or_create_user_from_moocfi_response(
919 conn: &mut PgConnection,
920 moocfi_user: MoocfiUser,
921) -> anyhow::Result<User> {
922 let MoocfiUser {
923 id: moocfi_id,
924 first_name,
925 last_name,
926 email,
927 upstream_id,
928 } = moocfi_user;
929
930 let user = match models::users::find_by_upstream_id(conn, upstream_id).await? {
932 Some(existing_user) => existing_user,
933 None => {
934 models::users::insert_with_upstream_id_and_moocfi_id(
935 conn,
936 &email,
937 first_name
939 .as_deref()
940 .and_then(|n| if n.trim().is_empty() { None } else { Some(n) }),
941 last_name
942 .as_deref()
943 .and_then(|n| if n.trim().is_empty() { None } else { Some(n) }),
944 upstream_id,
945 moocfi_id,
946 )
947 .await?
948 }
949 };
950 Ok(user)
951}
952
953pub async fn authenticate_test_user(
957 conn: &mut PgConnection,
958 email: &str,
959 password: &str,
960 application_configuration: &ApplicationConfiguration,
961) -> anyhow::Result<bool> {
962 assert!(application_configuration.test_mode);
964
965 let _user = if email == "admin@example.com" && password == "admin" {
966 models::users::get_by_email(conn, "admin@example.com").await?
967 } else if email == "teacher@example.com" && password == "teacher" {
968 models::users::get_by_email(conn, "teacher@example.com").await?
969 } else if email == "language.teacher@example.com" && password == "language.teacher" {
970 models::users::get_by_email(conn, "language.teacher@example.com").await?
971 } else if email == "material.viewer@example.com" && password == "material.viewer" {
972 models::users::get_by_email(conn, "material.viewer@example.com").await?
973 } else if email == "user@example.com" && password == "user" {
974 models::users::get_by_email(conn, "user@example.com").await?
975 } else if email == "assistant@example.com" && password == "assistant" {
976 models::users::get_by_email(conn, "assistant@example.com").await?
977 } else if email == "creator@example.com" && password == "creator" {
978 models::users::get_by_email(conn, "creator@example.com").await?
979 } else if email == "student1@example.com" && password == "student1" {
980 models::users::get_by_email(conn, "student1@example.com").await?
981 } else if email == "student2@example.com" && password == "student2" {
982 models::users::get_by_email(conn, "student2@example.com").await?
983 } else if email == "student3@example.com" && password == "student3" {
984 models::users::get_by_email(conn, "student3@example.com").await?
985 } else if email == "student4@example.com" && password == "student4" {
986 models::users::get_by_email(conn, "student4@example.com").await?
987 } else if email == "student5@example.com" && password == "student5" {
988 models::users::get_by_email(conn, "student5@example.com").await?
989 } else if email == "teaching-and-learning-services@example.com"
990 && password == "teaching-and-learning-services"
991 {
992 models::users::get_by_email(conn, "teaching-and-learning-services@example.com").await?
993 } else if email == "student-without-research-consent@example.com"
994 && password == "student-without-research-consent"
995 {
996 models::users::get_by_email(conn, "student-without-research-consent@example.com").await?
997 } else if email == "student-without-country@example.com"
998 && password == "student-without-country"
999 {
1000 models::users::get_by_email(conn, "student-without-country@example.com").await?
1001 } else if email == "langs@example.com" && password == "langs" {
1002 models::users::get_by_email(conn, "langs@example.com").await?
1003 } else if email == "sign-up-user@example.com" && password == "sign-up-user" {
1004 models::users::get_by_email(conn, "sign-up-user@example.com").await?
1005 } else {
1006 info!("Authentication failed: incorrect test credentials");
1007 return Ok(false);
1008 };
1009 info!("Successfully authenticated test user {}", email);
1010 Ok(true)
1011}
1012
1013pub async fn authenticate_test_token(
1015 conn: &mut PgConnection,
1016 token: &str,
1017 application_configuration: &ApplicationConfiguration,
1018) -> anyhow::Result<User> {
1019 assert!(application_configuration.test_mode);
1021 let user = models::users::get_by_email(conn, token).await?;
1022 Ok(user)
1023}
1024
1025fn get_ratelimit_api_key() -> Result<reqwest::header::HeaderValue, HttpClientError<reqwest::Error>>
1030{
1031 let key = match std::env::var("RATELIMIT_PROTECTION_SAFE_API_KEY") {
1032 Ok(key) => {
1033 debug!("Found RATELIMIT_PROTECTION_SAFE_API_KEY");
1034 key
1035 }
1036 Err(e) => {
1037 error!(
1038 "RATELIMIT_PROTECTION_SAFE_API_KEY environment variable not set: {}",
1039 e
1040 );
1041 return Err(HttpClientError::Other(
1042 "RATELIMIT_PROTECTION_SAFE_API_KEY must be defined".to_string(),
1043 ));
1044 }
1045 };
1046
1047 key.parse::<reqwest::header::HeaderValue>().map_err(|err| {
1048 error!("Invalid RATELIMIT API key format: {}", err);
1049 HttpClientError::Other("Invalid RATELIMIT API key.".to_string())
1050 })
1051}
1052
1053async fn async_http_client_with_headers(
1062 oauth_request: oauth2::HttpRequest,
1063) -> Result<oauth2::HttpResponse, HttpClientError<reqwest::Error>> {
1064 debug!("Making OAuth request to TMC server");
1065
1066 if log::log_enabled!(log::Level::Trace) {
1067 if let Ok(url) = oauth_request.uri().to_string().parse::<reqwest::Url>() {
1069 trace!("OAuth request path: {}", url.path());
1070 }
1071 }
1072
1073 let parsed_key = get_ratelimit_api_key()?;
1074
1075 debug!("Building request to TMC server");
1076 let request = REQWEST_CLIENT
1077 .request(
1078 oauth_request.method().clone(),
1079 oauth_request
1080 .uri()
1081 .to_string()
1082 .parse::<reqwest::Url>()
1083 .map_err(|e| HttpClientError::Other(format!("Invalid URL: {}", e)))?,
1084 )
1085 .headers(oauth_request.headers().clone())
1086 .version(oauth_request.version())
1087 .header("RATELIMIT-PROTECTION-SAFE-API-KEY", parsed_key)
1088 .body(oauth_request.body().to_vec());
1089
1090 debug!("Sending request to TMC server");
1091 let response = request
1092 .send()
1093 .await
1094 .map_err(|e| HttpClientError::Other(format!("Failed to execute request: {}", e)))?;
1095
1096 debug!(
1098 "Received response from TMC server - Status: {}, Version: {:?}",
1099 response.status(),
1100 response.version()
1101 );
1102
1103 let status = response.status();
1104 let version = response.version();
1105 let headers = response.headers().clone();
1106
1107 debug!("Reading response body");
1108 let body_bytes = response
1109 .bytes()
1110 .await
1111 .map_err(|e| HttpClientError::Other(format!("Failed to read response body: {}", e)))?
1112 .to_vec();
1113
1114 debug!("Building OAuth response");
1115 let mut builder = oauth2::http::Response::builder()
1116 .status(status)
1117 .version(version);
1118
1119 if let Some(builder_headers) = builder.headers_mut() {
1120 builder_headers.extend(headers.iter().map(|(k, v)| (k.clone(), v.clone())));
1121 }
1122
1123 let oauth_response = builder
1124 .body(body_bytes)
1125 .map_err(|e| HttpClientError::Other(format!("Failed to construct response: {}", e)))?;
1126
1127 debug!("Successfully completed OAuth request");
1128 Ok(oauth_response)
1129}
1130
1131#[cfg(test)]
1132mod test {
1133 use super::*;
1134 use crate::test_helper::*;
1135 use headless_lms_models::*;
1136 use models::roles::RoleDomain;
1137
1138 #[actix_web::test]
1139 async fn test_authorization() {
1140 let mut conn = Conn::init().await;
1141 let mut tx = conn.begin().await;
1142
1143 let user = users::insert(
1144 tx.as_mut(),
1145 PKeyPolicy::Generate,
1146 "auth@example.com",
1147 None,
1148 None,
1149 )
1150 .await
1151 .unwrap();
1152 let org = organizations::insert(
1153 tx.as_mut(),
1154 PKeyPolicy::Generate,
1155 "auth",
1156 "auth",
1157 Some("auth"),
1158 false,
1159 )
1160 .await
1161 .unwrap();
1162
1163 authorize(
1164 tx.as_mut(),
1165 Action::Edit,
1166 Some(user),
1167 Resource::Organization(org),
1168 )
1169 .await
1170 .unwrap_err();
1171
1172 roles::insert(
1173 tx.as_mut(),
1174 user,
1175 UserRole::Teacher,
1176 RoleDomain::Organization(org),
1177 )
1178 .await
1179 .unwrap();
1180
1181 authorize(
1182 tx.as_mut(),
1183 Action::Edit,
1184 Some(user),
1185 Resource::Organization(org),
1186 )
1187 .await
1188 .unwrap();
1189 }
1190
1191 #[actix_web::test]
1192 async fn course_role_chapter_resource() {
1193 insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module, :chapter);
1194
1195 authorize(
1196 tx.as_mut(),
1197 Action::Edit,
1198 Some(user),
1199 Resource::Chapter(chapter),
1200 )
1201 .await
1202 .unwrap_err();
1203
1204 roles::insert(
1205 tx.as_mut(),
1206 user,
1207 UserRole::Teacher,
1208 RoleDomain::Course(course),
1209 )
1210 .await
1211 .unwrap();
1212
1213 authorize(
1214 tx.as_mut(),
1215 Action::Edit,
1216 Some(user),
1217 Resource::Chapter(chapter),
1218 )
1219 .await
1220 .unwrap();
1221 }
1222
1223 #[actix_web::test]
1224 async fn anonymous_user_can_view_open_course() {
1225 insert_data!(:tx, :user, :org, :course);
1226
1227 authorize(tx.as_mut(), Action::View, None, Resource::Course(course))
1228 .await
1229 .unwrap();
1230 }
1231}