1use argon2::Argon2;
2use argon2::password_hash::{PasswordHasher, SaltString};
3use base64::Engine;
4use core_db_types::models::{
5 MfaRecoveryCode, MfaRecoveryCodeId, MfaTotpCredential, MfaTotpCredentialId, MfaTotpRegistrationSession,
6 MfaWebauthnAuthenticationSession, MfaWebauthnCredential, MfaWebauthnCredentialId, MfaWebauthnRegistrationSession,
7 NewUserEmailRequest, NewUserEmailRequestId, User, UserEmail, UserId,
8};
9use core_db_types::schema::{
10 mfa_recovery_codes, mfa_totp_credentials, mfa_totp_reg_sessions, mfa_webauthn_auth_sessions, mfa_webauthn_credentials,
11 mfa_webauthn_reg_sessions, new_user_email_requests, user_emails, users,
12};
13use core_traits::EmailServiceClient;
14use diesel::{BoolExpressionMethods, ExpressionMethods, OptionalExtension, QueryDsl, SelectableHelper};
15use diesel_async::RunQueryDsl;
16use ext_traits::{DisplayExt, OptionExt, RequestExt, ResultExt};
17use rand::distributions::DistString;
18use tonic::Code;
19use tonic_types::{ErrorDetails, StatusExt};
20
21use crate::cedar::Action;
22use crate::http_ext::CoreRequestExt;
23use crate::operations::{Operation, OperationDriver};
24use crate::totp::TotpError;
25use crate::{common, totp};
26
27impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::GetUserRequest> {
28 type Principal = User;
29 type Resource = User;
30 type Response = pb::scufflecloud::core::v1::User;
31
32 const ACTION: Action = Action::GetUser;
33
34 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
35 let global = &self.global::<G>()?;
36 let session = self.session_or_err()?;
37 common::get_user_by_id(global, session.user_id).await
38 }
39
40 async fn load_resource(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
41 let global = &self.global::<G>()?;
42 let user_id: UserId = self
43 .get_ref()
44 .id
45 .parse()
46 .into_tonic_err_with_field_violation("id", "invalid ID")?;
47
48 common::get_user_by_id(global, user_id).await
49 }
50
51 async fn execute(
52 self,
53 _driver: &mut OperationDriver<'_, G>,
54 _principal: Self::Principal,
55 resource: Self::Resource,
56 ) -> Result<Self::Response, tonic::Status> {
57 Ok(resource.into())
58 }
59}
60
61impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::UpdateUserRequest> {
62 type Principal = User;
63 type Resource = User;
64 type Response = pb::scufflecloud::core::v1::User;
65
66 const ACTION: Action = Action::UpdateUser;
67
68 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
69 let global = &self.global::<G>()?;
70 let session = self.session_or_err()?;
71 common::get_user_by_id(global, session.user_id).await
72 }
73
74 async fn load_resource(&mut self, driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
75 let user_id: UserId = self
76 .get_ref()
77 .id
78 .parse()
79 .into_tonic_err_with_field_violation("id", "invalid ID")?;
80
81 let conn = driver.conn().await?;
82 common::get_user_by_id_in_tx(conn, user_id).await
83 }
84
85 async fn execute(
86 self,
87 driver: &mut OperationDriver<'_, G>,
88 _principal: Self::Principal,
89 mut resource: Self::Resource,
90 ) -> Result<Self::Response, tonic::Status> {
91 let payload = self.into_inner();
92 let conn = driver.conn().await?;
93
94 if let Some(password_update) = payload.password {
95 if let Some(password_hash) = &resource.password_hash {
97 common::verify_password(password_hash, &password_update.current_password.require("current_password")?)?;
98 }
99
100 let salt = SaltString::generate(&mut argon2::password_hash::rand_core::OsRng);
101 let new_hash = Argon2::default()
102 .hash_password(password_update.new_password.as_bytes(), &salt)
103 .into_tonic_internal_err("failed to hash password")?
104 .to_string();
105
106 resource = diesel::update(users::dsl::users)
107 .filter(users::dsl::id.eq(resource.id))
108 .set(users::dsl::password_hash.eq(&new_hash))
109 .returning(User::as_returning())
110 .get_result::<User>(conn)
111 .await
112 .into_tonic_internal_err("failed to update user password")?;
113 }
114
115 if let Some(names_update) = payload.names {
116 resource = diesel::update(users::dsl::users)
117 .filter(users::dsl::id.eq(resource.id))
118 .set((
119 users::dsl::preferred_name.eq(&names_update.preferred_name),
120 users::dsl::first_name.eq(&names_update.first_name),
121 users::dsl::last_name.eq(&names_update.last_name),
122 ))
123 .returning(User::as_returning())
124 .get_result::<User>(conn)
125 .await
126 .into_tonic_internal_err("failed to update user password")?;
127 }
128
129 if let Some(primary_email_update) = payload.primary_email {
130 let email = common::normalize_email(&primary_email_update.primary_email);
131
132 let email = user_emails::dsl::user_emails
133 .filter(
134 user_emails::dsl::email
135 .eq(&email)
136 .and(user_emails::dsl::user_id.eq(resource.id)),
137 )
138 .select(user_emails::dsl::email)
139 .first::<String>(conn)
140 .await
141 .optional()
142 .into_tonic_internal_err("failed to query user email")?
143 .into_tonic_not_found("user email not found")?;
144
145 resource = diesel::update(users::dsl::users)
146 .filter(users::dsl::id.eq(resource.id))
147 .set(users::dsl::primary_email.eq(&email))
148 .returning(User::as_returning())
149 .get_result::<User>(conn)
150 .await
151 .into_tonic_internal_err("failed to update user password")?;
152 }
153
154 Ok(resource.into())
155 }
156}
157
158impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::ListUserEmailsRequest> {
159 type Principal = User;
160 type Resource = User;
161 type Response = pb::scufflecloud::core::v1::UserEmailsList;
162
163 const ACTION: Action = Action::ListUserEmails;
164
165 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
166 let global = &self.global::<G>()?;
167 let session = self.session_or_err()?;
168 common::get_user_by_id(global, session.user_id).await
169 }
170
171 async fn load_resource(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
172 let global = &self.global::<G>()?;
173 let user_id: UserId = self
174 .get_ref()
175 .id
176 .parse()
177 .into_tonic_err_with_field_violation("id", "invalid ID")?;
178
179 common::get_user_by_id(global, user_id).await
180 }
181
182 async fn execute(
183 self,
184 _driver: &mut OperationDriver<'_, G>,
185 _principal: Self::Principal,
186 resource: Self::Resource,
187 ) -> Result<Self::Response, tonic::Status> {
188 let global = &self.global::<G>()?;
189 let mut db = global.db().await.into_tonic_internal_err("failed to connect to database")?;
190
191 let emails = user_emails::dsl::user_emails
192 .filter(user_emails::dsl::user_id.eq(resource.id))
193 .select(UserEmail::as_select())
194 .load::<UserEmail>(&mut db)
195 .await
196 .into_tonic_internal_err("failed to query user emails")?;
197
198 Ok(pb::scufflecloud::core::v1::UserEmailsList {
199 emails: emails.into_iter().map(Into::into).collect(),
200 })
201 }
202}
203
204impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::CreateUserEmailRequest> {
205 type Principal = User;
206 type Resource = UserEmail;
207 type Response = ();
208
209 const ACTION: Action = Action::CreateUserEmail;
210
211 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
212 let global = &self.global::<G>()?;
213 let session = self.session_or_err()?;
214 common::get_user_by_id(global, session.user_id).await
215 }
216
217 async fn load_resource(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
218 let user_id: UserId = self
219 .get_ref()
220 .id
221 .parse()
222 .into_tonic_err_with_field_violation("id", "invalid ID")?;
223
224 Ok(UserEmail {
225 email: common::normalize_email(&self.get_ref().email),
226 user_id,
227 created_at: chrono::Utc::now(),
228 })
229 }
230
231 async fn execute(
232 self,
233 driver: &mut OperationDriver<'_, G>,
234 _principal: Self::Principal,
235 resource: Self::Resource,
236 ) -> Result<Self::Response, tonic::Status> {
237 let global = &self.global::<G>()?;
238
239 let code = common::generate_random_bytes().into_tonic_internal_err("failed to generate registration code")?;
241 let code_base64 = base64::prelude::BASE64_URL_SAFE.encode(code);
242 let conn = driver.conn().await?;
243
244 if user_emails::dsl::user_emails
246 .find(&resource.email)
247 .select(user_emails::dsl::email)
248 .first::<String>(conn)
249 .await
250 .optional()
251 .into_tonic_internal_err("failed to query database")?
252 .is_some()
253 {
254 return Err(tonic::Status::with_error_details(
255 Code::AlreadyExists,
256 "email is already registered",
257 ErrorDetails::new(),
258 ));
259 }
260
261 let user = common::get_user_by_id(global, resource.user_id).await?;
262
263 let timeout = global.timeout_config().new_user_email_request;
264
265 let registration_request = NewUserEmailRequest {
267 id: NewUserEmailRequestId::new(),
268 user_id: resource.user_id,
269 email: resource.email.clone(),
270 code: code.to_vec(),
271 expires_at: chrono::Utc::now() + timeout,
272 };
273
274 diesel::insert_into(new_user_email_requests::dsl::new_user_email_requests)
275 .values(registration_request)
276 .execute(conn)
277 .await
278 .into_tonic_internal_err("failed to insert email registration request")?;
279
280 let email = core_emails::add_new_email_email(&self.dashboard_origin::<G>()?, code_base64, timeout)
282 .into_tonic_internal_err("failed to render add new email email")?;
283 let email = common::email_to_pb(global, resource.email.clone(), user.preferred_name, email);
284
285 global
286 .email_service()
287 .send_email(email)
288 .await
289 .into_tonic_internal_err("failed to send add new email email")?;
290
291 Ok(())
292 }
293}
294
295impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::CompleteCreateUserEmailRequest> {
296 type Principal = User;
297 type Resource = UserEmail;
298 type Response = pb::scufflecloud::core::v1::UserEmail;
299
300 const ACTION: Action = Action::CreateUserEmail;
301
302 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
303 let global = &self.global::<G>()?;
304 let session = self.session_or_err()?;
305 common::get_user_by_id(global, session.user_id).await
306 }
307
308 async fn load_resource(&mut self, driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
309 let user_id: UserId = self
310 .get_ref()
311 .id
312 .parse()
313 .into_tonic_err_with_field_violation("id", "invalid ID")?;
314
315 let conn = driver.conn().await?;
316
317 let Some(registration_request) = diesel::delete(new_user_email_requests::dsl::new_user_email_requests)
319 .filter(
320 new_user_email_requests::dsl::code
321 .eq(&self.get_ref().code)
322 .and(new_user_email_requests::dsl::user_id.eq(user_id))
323 .and(new_user_email_requests::dsl::expires_at.gt(chrono::Utc::now())),
324 )
325 .returning(NewUserEmailRequest::as_select())
326 .get_result::<NewUserEmailRequest>(conn)
327 .await
328 .optional()
329 .into_tonic_internal_err("failed to delete email registration request")?
330 else {
331 return Err(tonic::Status::with_error_details(
332 Code::NotFound,
333 "unknown code",
334 ErrorDetails::new(),
335 ));
336 };
337
338 if user_emails::dsl::user_emails
340 .find(®istration_request.email)
341 .select(user_emails::dsl::email)
342 .first::<String>(conn)
343 .await
344 .optional()
345 .into_tonic_internal_err("failed to query user emails")?
346 .is_some()
347 {
348 return Err(tonic::Status::with_error_details(
349 Code::AlreadyExists,
350 "email is already registered",
351 ErrorDetails::new(),
352 ));
353 }
354
355 Ok(UserEmail {
356 email: registration_request.email,
357 user_id,
358 created_at: chrono::Utc::now(),
359 })
360 }
361
362 async fn execute(
363 self,
364 driver: &mut OperationDriver<'_, G>,
365 _principal: Self::Principal,
366 resource: Self::Resource,
367 ) -> Result<Self::Response, tonic::Status> {
368 let conn = driver.conn().await?;
369
370 diesel::insert_into(user_emails::dsl::user_emails)
371 .values(&resource)
372 .execute(conn)
373 .await
374 .into_tonic_internal_err("failed to insert user email")?;
375
376 Ok(resource.into())
377 }
378}
379
380impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::DeleteUserEmailRequest> {
381 type Principal = User;
382 type Resource = UserEmail;
383 type Response = pb::scufflecloud::core::v1::UserEmail;
384
385 const ACTION: Action = Action::DeleteUserEmail;
386
387 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
388 let global = &self.global::<G>()?;
389 let session = self.session_or_err()?;
390 common::get_user_by_id(global, session.user_id).await
391 }
392
393 async fn load_resource(&mut self, driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
394 let user_id: UserId = self
395 .get_ref()
396 .id
397 .parse()
398 .into_tonic_err_with_field_violation("id", "invalid ID")?;
399
400 let conn = driver.conn().await?;
401
402 let user_email = user_emails::dsl::user_emails
403 .filter(
404 user_emails::dsl::user_id
405 .eq(user_id)
406 .and(user_emails::dsl::email.eq(&self.get_ref().email)),
407 )
408 .select(UserEmail::as_select())
409 .first::<UserEmail>(conn)
410 .await
411 .into_tonic_internal_err("failed to delete user email")?;
412
413 Ok(user_email)
414 }
415
416 async fn execute(
417 self,
418 driver: &mut OperationDriver<'_, G>,
419 _principal: Self::Principal,
420 resource: Self::Resource,
421 ) -> Result<Self::Response, tonic::Status> {
422 let conn = driver.conn().await?;
423
424 diesel::delete(user_emails::dsl::user_emails)
425 .filter(user_emails::dsl::email.eq(&resource.email))
426 .execute(conn)
427 .await
428 .into_tonic_internal_err("failed to delete user email")?;
429
430 Ok(resource.into())
431 }
432}
433
434impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::CreateWebauthnCredentialRequest> {
435 type Principal = User;
436 type Resource = User;
437 type Response = pb::scufflecloud::core::v1::CreateWebauthnCredentialResponse;
438
439 const ACTION: Action = Action::CreateWebauthnCredential;
440
441 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
442 let global = &self.global::<G>()?;
443 let session = self.session_or_err()?;
444 common::get_user_by_id(global, session.user_id).await
445 }
446
447 async fn load_resource(&mut self, driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
448 let user_id: UserId = self
449 .get_ref()
450 .id
451 .parse()
452 .into_tonic_err_with_field_violation("id", "invalid ID")?;
453
454 let conn = driver.conn().await?;
455 common::get_user_by_id_in_tx(conn, user_id).await
456 }
457
458 async fn execute(
459 self,
460 driver: &mut OperationDriver<'_, G>,
461 _principal: Self::Principal,
462 resource: Self::Resource,
463 ) -> Result<Self::Response, tonic::Status> {
464 let global = &self.global::<G>()?;
465
466 let conn = driver.conn().await?;
467 let exclude_credentials: Vec<_> = mfa_webauthn_credentials::dsl::mfa_webauthn_credentials
468 .filter(mfa_webauthn_credentials::dsl::user_id.eq(resource.id))
469 .select(mfa_webauthn_credentials::dsl::credential_id)
470 .load::<Vec<u8>>(conn)
471 .await
472 .into_tonic_internal_err("failed to query webauthn credentials")?
473 .into_iter()
474 .map(webauthn_rs::prelude::CredentialID::from)
475 .collect();
476
477 let user_name = resource.primary_email.unwrap_or(resource.id.to_string());
478 let user_display_name = resource.preferred_name.or_else(|| {
479 if let (Some(first_name), Some(last_name)) = (resource.first_name, resource.last_name) {
480 Some(format!("{} {}", first_name, last_name))
481 } else {
482 None
483 }
484 });
485
486 let (response, state) = global
487 .webauthn()
488 .start_passkey_registration(
489 resource.id.into(),
490 &user_name,
491 user_display_name.as_ref().unwrap_or(&user_name),
492 Some(exclude_credentials),
493 )
494 .into_tonic_internal_err("failed to start webauthn registration")?;
495
496 let reg_session = MfaWebauthnRegistrationSession {
497 user_id: resource.id,
498 state: serde_json::to_value(&state).into_tonic_internal_err("failed to serialize webauthn state")?,
499 expires_at: chrono::Utc::now() + global.timeout_config().mfa,
500 };
501
502 let options_json =
503 serde_json::to_string(&response).into_tonic_internal_err("failed to serialize webauthn options")?;
504
505 diesel::insert_into(mfa_webauthn_reg_sessions::dsl::mfa_webauthn_reg_sessions)
506 .values(®_session)
507 .on_conflict(mfa_webauthn_reg_sessions::dsl::user_id)
508 .do_update()
509 .set((
510 mfa_webauthn_reg_sessions::dsl::state.eq(®_session.state),
511 mfa_webauthn_reg_sessions::dsl::expires_at.eq(®_session.expires_at),
512 ))
513 .execute(conn)
514 .await
515 .into_tonic_internal_err("failed to insert webauthn registration session")?;
516
517 Ok(pb::scufflecloud::core::v1::CreateWebauthnCredentialResponse { options_json })
518 }
519}
520
521impl<G: core_traits::Global> Operation<G>
522 for tonic::Request<pb::scufflecloud::core::v1::CompleteCreateWebauthnCredentialRequest>
523{
524 type Principal = User;
525 type Resource = MfaWebauthnCredential;
526 type Response = pb::scufflecloud::core::v1::WebauthnCredential;
527
528 const ACTION: Action = Action::CompleteCreateWebauthnCredential;
529
530 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
531 let global = &self.global::<G>()?;
532 let session = self.session_or_err()?;
533 common::get_user_by_id(global, session.user_id).await
534 }
535
536 async fn load_resource(&mut self, driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
537 let global = &self.global::<G>()?;
538
539 let user_id: UserId = self
540 .get_ref()
541 .id
542 .parse()
543 .into_tonic_err_with_field_violation("id", "invalid ID")?;
544
545 let reg = serde_json::from_str(&self.get_ref().response_json)
546 .into_tonic_err_with_field_violation("response_json", "invalid register public key credential")?;
547
548 let conn = driver.conn().await?;
549 let state = diesel::delete(mfa_webauthn_reg_sessions::dsl::mfa_webauthn_reg_sessions)
550 .filter(
551 mfa_webauthn_reg_sessions::dsl::user_id
552 .eq(user_id)
553 .and(mfa_webauthn_reg_sessions::dsl::expires_at.gt(chrono::Utc::now())),
554 )
555 .returning(mfa_webauthn_reg_sessions::dsl::state)
556 .get_result::<serde_json::Value>(conn)
557 .await
558 .optional()
559 .into_tonic_internal_err("failed to query webauthn registration session")?
560 .into_tonic_err(
561 tonic::Code::FailedPrecondition,
562 "no webauthn registration session found",
563 ErrorDetails::new(),
564 )?;
565
566 let state: webauthn_rs::prelude::PasskeyRegistration =
567 serde_json::from_value(state).into_tonic_internal_err("failed to deserialize webauthn state")?;
568
569 let credential = global
570 .webauthn()
571 .finish_passkey_registration(®, &state)
572 .into_tonic_internal_err("failed to finish webauthn registration")?;
573
574 Ok(MfaWebauthnCredential {
575 id: MfaWebauthnCredentialId::new(),
576 user_id,
577 name: self.get_ref().name.clone(),
578 credential_id: credential.cred_id().to_vec(),
579 credential: serde_json::to_value(credential).into_tonic_internal_err("failed to serialize credential")?,
580 counter: None,
581 last_used_at: chrono::Utc::now(),
582 })
583 }
584
585 async fn execute(
586 self,
587 driver: &mut OperationDriver<'_, G>,
588 _principal: Self::Principal,
589 resource: Self::Resource,
590 ) -> Result<Self::Response, tonic::Status> {
591 let conn = driver.conn().await?;
592 diesel::insert_into(mfa_webauthn_credentials::dsl::mfa_webauthn_credentials)
593 .values(&resource)
594 .execute(conn)
595 .await
596 .into_tonic_internal_err("failed to insert webauthn credential")?;
597
598 Ok(resource.into())
599 }
600}
601
602impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::ListWebauthnCredentialsRequest> {
603 type Principal = User;
604 type Resource = User;
605 type Response = pb::scufflecloud::core::v1::WebauthnCredentialsList;
606
607 const ACTION: Action = Action::ListWebauthnCredentials;
608
609 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
610 let global = &self.global::<G>()?;
611 let session = self.session_or_err()?;
612 common::get_user_by_id(global, session.user_id).await
613 }
614
615 async fn load_resource(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
616 let global = &self.global::<G>()?;
617 let user_id: UserId = self
618 .get_ref()
619 .id
620 .parse()
621 .into_tonic_err_with_field_violation("id", "invalid ID")?;
622 common::get_user_by_id(global, user_id).await
623 }
624
625 async fn execute(
626 self,
627 _driver: &mut OperationDriver<'_, G>,
628 _principal: Self::Principal,
629 resource: Self::Resource,
630 ) -> Result<Self::Response, tonic::Status> {
631 let global = &self.global::<G>()?;
632 let mut db = global.db().await.into_tonic_internal_err("failed to connect to database")?;
633
634 let credentials = mfa_webauthn_credentials::dsl::mfa_webauthn_credentials
635 .filter(mfa_webauthn_credentials::dsl::user_id.eq(resource.id))
636 .select(MfaWebauthnCredential::as_select())
637 .load::<MfaWebauthnCredential>(&mut db)
638 .await
639 .into_tonic_internal_err("failed to query webauthn credentials")?;
640
641 Ok(pb::scufflecloud::core::v1::WebauthnCredentialsList {
642 credentials: credentials.into_iter().map(Into::into).collect(),
643 })
644 }
645}
646
647impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::UpdateWebauthnCredentialRequest> {
648 type Principal = User;
649 type Resource = MfaWebauthnCredential;
650 type Response = pb::scufflecloud::core::v1::WebauthnCredential;
651
652 const ACTION: Action = Action::UpdateWebauthnCredential;
653
654 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
655 let global = &self.global::<G>()?;
656 let session = self.session_or_err()?;
657 common::get_user_by_id(global, session.user_id).await
658 }
659
660 async fn load_resource(&mut self, driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
661 let user_id: UserId = self
662 .get_ref()
663 .user_id
664 .parse()
665 .into_tonic_err_with_field_violation("user_id", "invalid ID")?;
666
667 let credential_id: MfaWebauthnCredentialId = self
668 .get_ref()
669 .id
670 .parse()
671 .into_tonic_err_with_field_violation("id", "invalid ID")?;
672
673 let conn = driver.conn().await?;
674 let credential = mfa_webauthn_credentials::dsl::mfa_webauthn_credentials
675 .filter(
676 mfa_webauthn_credentials::dsl::id
677 .eq(credential_id)
678 .and(mfa_webauthn_credentials::dsl::user_id.eq(user_id)),
679 )
680 .select(MfaWebauthnCredential::as_select())
681 .first::<MfaWebauthnCredential>(conn)
682 .await
683 .into_tonic_internal_err("failed to find webauthn credential")?;
684
685 Ok(credential)
686 }
687
688 async fn execute(
689 self,
690 driver: &mut OperationDriver<'_, G>,
691 _principal: Self::Principal,
692 resource: Self::Resource,
693 ) -> Result<Self::Response, tonic::Status> {
694 let conn = driver.conn().await?;
695
696 let updated_credential = if let Some(name) = &self.get_ref().name {
697 diesel::update(mfa_webauthn_credentials::dsl::mfa_webauthn_credentials)
698 .filter(mfa_webauthn_credentials::dsl::id.eq(resource.id))
699 .set(mfa_webauthn_credentials::dsl::name.eq(name))
700 .returning(MfaWebauthnCredential::as_returning())
701 .get_result::<MfaWebauthnCredential>(conn)
702 .await
703 .into_tonic_internal_err("failed to update webauthn credential")?
704 } else {
705 resource
706 };
707
708 Ok(updated_credential.into())
709 }
710}
711
712impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::DeleteWebauthnCredentialRequest> {
713 type Principal = User;
714 type Resource = MfaWebauthnCredential;
715 type Response = pb::scufflecloud::core::v1::WebauthnCredential;
716
717 const ACTION: Action = Action::DeleteWebauthnCredential;
718
719 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
720 let global = &self.global::<G>()?;
721 let session = self.session_or_err()?;
722 common::get_user_by_id(global, session.user_id).await
723 }
724
725 async fn load_resource(&mut self, driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
726 let user_id: UserId = self
727 .get_ref()
728 .user_id
729 .parse()
730 .into_tonic_err_with_field_violation("user_id", "invalid ID")?;
731
732 let credential_id: MfaWebauthnCredentialId = self
733 .get_ref()
734 .id
735 .parse()
736 .into_tonic_err_with_field_violation("id", "invalid ID")?;
737
738 let conn = driver.conn().await?;
739 let credential = mfa_webauthn_credentials::dsl::mfa_webauthn_credentials
740 .filter(
741 mfa_webauthn_credentials::dsl::id
742 .eq(credential_id)
743 .and(mfa_webauthn_credentials::dsl::user_id.eq(user_id)),
744 )
745 .select(MfaWebauthnCredential::as_select())
746 .first::<MfaWebauthnCredential>(conn)
747 .await
748 .into_tonic_internal_err("failed to delete webauthn credential")?;
749
750 Ok(credential)
751 }
752
753 async fn execute(
754 self,
755 driver: &mut OperationDriver<'_, G>,
756 _principal: Self::Principal,
757 resource: Self::Resource,
758 ) -> Result<Self::Response, tonic::Status> {
759 let conn = driver.conn().await?;
760 diesel::delete(mfa_webauthn_credentials::dsl::mfa_webauthn_credentials)
761 .filter(mfa_webauthn_credentials::dsl::id.eq(resource.id))
762 .execute(conn)
763 .await
764 .into_tonic_internal_err("failed to delete webauthn credential")?;
765
766 Ok(resource.into())
767 }
768}
769
770impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::CreateWebauthnChallengeRequest> {
771 type Principal = User;
772 type Resource = User;
773 type Response = pb::scufflecloud::core::v1::WebauthnChallenge;
774
775 const ACTION: Action = Action::CreateWebauthnChallenge;
776
777 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
778 let global = &self.global::<G>()?;
779 let session = self.session_or_err()?;
780 common::get_user_by_id(global, session.user_id).await
781 }
782
783 async fn load_resource(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
784 let global = &self.global::<G>()?;
785 let user_id: UserId = self
786 .get_ref()
787 .id
788 .parse()
789 .into_tonic_err_with_field_violation("id", "invalid ID")?;
790 common::get_user_by_id(global, user_id).await
791 }
792
793 async fn execute(
794 self,
795 driver: &mut OperationDriver<'_, G>,
796 _principal: Self::Principal,
797 resource: Self::Resource,
798 ) -> Result<Self::Response, tonic::Status> {
799 let global = &self.global::<G>()?;
800
801 let conn = driver.conn().await?;
802 let credentials = mfa_webauthn_credentials::dsl::mfa_webauthn_credentials
803 .filter(mfa_webauthn_credentials::dsl::user_id.eq(resource.id))
804 .select(mfa_webauthn_credentials::dsl::credential)
805 .load::<serde_json::Value>(conn)
806 .await
807 .into_tonic_internal_err("failed to query webauthn credentials")?
808 .into_iter()
809 .map(serde_json::from_value)
810 .collect::<Result<Vec<webauthn_rs::prelude::Passkey>, _>>()
811 .into_tonic_internal_err("failed to deserialize webauthn credentials")?;
812
813 let (response, state) = global
814 .webauthn()
815 .start_passkey_authentication(&credentials)
816 .into_tonic_internal_err("failed to start webauthn authentication")?;
817
818 let auth_session = MfaWebauthnAuthenticationSession {
819 user_id: resource.id,
820 state: serde_json::to_value(&state).into_tonic_internal_err("failed to serialize webauthn state")?,
821 expires_at: chrono::Utc::now() + global.timeout_config().mfa,
822 };
823
824 let options_json =
825 serde_json::to_string(&response).into_tonic_internal_err("failed to serialize webauthn options")?;
826
827 diesel::insert_into(mfa_webauthn_auth_sessions::dsl::mfa_webauthn_auth_sessions)
828 .values(&auth_session)
829 .on_conflict(mfa_webauthn_auth_sessions::dsl::user_id)
830 .do_update()
831 .set((
832 mfa_webauthn_auth_sessions::dsl::state.eq(&auth_session.state),
833 mfa_webauthn_auth_sessions::dsl::expires_at.eq(&auth_session.expires_at),
834 ))
835 .execute(conn)
836 .await
837 .into_tonic_internal_err("failed to insert webauthn authentication session")?;
838
839 Ok(pb::scufflecloud::core::v1::WebauthnChallenge { options_json })
840 }
841}
842
843impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::CreateTotpCredentialRequest> {
844 type Principal = User;
845 type Resource = User;
846 type Response = pb::scufflecloud::core::v1::CreateTotpCredentialResponse;
847
848 const ACTION: Action = Action::CreateTotpCredential;
849
850 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
851 let global = &self.global::<G>()?;
852 let session = self.session_or_err()?;
853 common::get_user_by_id(global, session.user_id).await
854 }
855
856 async fn load_resource(&mut self, driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
857 let user_id: UserId = self
858 .get_ref()
859 .id
860 .parse()
861 .into_tonic_err_with_field_violation("id", "invalid ID")?;
862
863 let conn = driver.conn().await?;
864 common::get_user_by_id_in_tx(conn, user_id).await
865 }
866
867 async fn execute(
868 self,
869 driver: &mut OperationDriver<'_, G>,
870 _principal: Self::Principal,
871 resource: Self::Resource,
872 ) -> Result<Self::Response, tonic::Status> {
873 let global = &self.global::<G>()?;
874
875 let totp = totp::new_token(resource.primary_email.unwrap_or(resource.id.to_string()))
876 .into_tonic_internal_err("failed to generate TOTP token")?;
877
878 let response = pb::scufflecloud::core::v1::CreateTotpCredentialResponse {
879 secret_url: totp.get_url(),
880 secret_qrcode_png: totp.get_qr_png().into_tonic_internal_err("failed to generate TOTP QR code")?,
881 };
882
883 let reg_session = MfaTotpRegistrationSession {
884 user_id: resource.id,
885 secret: totp.secret,
886 expires_at: chrono::Utc::now() + global.timeout_config().mfa,
887 };
888
889 let conn = driver.conn().await?;
890 diesel::insert_into(mfa_totp_reg_sessions::dsl::mfa_totp_reg_sessions)
891 .values(®_session)
892 .on_conflict(mfa_totp_reg_sessions::dsl::user_id)
893 .do_update()
894 .set((
895 mfa_totp_reg_sessions::dsl::secret.eq(®_session.secret),
896 mfa_totp_reg_sessions::dsl::expires_at.eq(reg_session.expires_at),
897 ))
898 .execute(conn)
899 .await
900 .into_tonic_internal_err("failed to insert TOTP registration session")?;
901
902 Ok(response)
903 }
904}
905
906impl<G: core_traits::Global> Operation<G>
907 for tonic::Request<pb::scufflecloud::core::v1::CompleteCreateTotpCredentialRequest>
908{
909 type Principal = User;
910 type Resource = MfaTotpCredential;
911 type Response = pb::scufflecloud::core::v1::TotpCredential;
912
913 const ACTION: Action = Action::CompleteCreateTotpCredential;
914
915 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
916 let global = &self.global::<G>()?;
917 let session = self.session_or_err()?;
918 common::get_user_by_id(global, session.user_id).await
919 }
920
921 async fn load_resource(&mut self, driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
922 let user_id: UserId = self
923 .get_ref()
924 .id
925 .parse()
926 .into_tonic_err_with_field_violation("id", "invalid ID")?;
927
928 let conn = driver.conn().await?;
929 let secret = mfa_totp_reg_sessions::dsl::mfa_totp_reg_sessions
930 .find(user_id)
931 .filter(mfa_totp_reg_sessions::dsl::expires_at.gt(chrono::Utc::now()))
932 .select(mfa_totp_reg_sessions::dsl::secret)
933 .first::<Vec<u8>>(conn)
934 .await
935 .optional()
936 .into_tonic_internal_err("failed to query TOTP registration session")?
937 .into_tonic_err(
938 tonic::Code::FailedPrecondition,
939 "no TOTP registration session found",
940 ErrorDetails::new(),
941 )?;
942
943 match totp::verify_token(secret.clone(), &self.get_ref().code) {
944 Ok(()) => {}
945 Err(TotpError::InvalidToken) => {
946 return Err(TotpError::InvalidToken.into_tonic_err_with_field_violation("code", "invalid TOTP token"));
947 }
948 Err(e) => return Err(e.into_tonic_internal_err("failed to verify TOTP token")),
949 }
950
951 Ok(MfaTotpCredential {
952 id: MfaTotpCredentialId::new(),
953 user_id,
954 name: self.get_ref().name.clone(),
955 secret,
956 last_used_at: chrono::Utc::now(),
957 })
958 }
959
960 async fn execute(
961 self,
962 driver: &mut OperationDriver<'_, G>,
963 _principal: Self::Principal,
964 resource: Self::Resource,
965 ) -> Result<Self::Response, tonic::Status> {
966 let conn = driver.conn().await?;
967 diesel::insert_into(mfa_totp_credentials::dsl::mfa_totp_credentials)
968 .values(&resource)
969 .execute(conn)
970 .await
971 .into_tonic_internal_err("failed to insert TOTP credential")?;
972
973 Ok(resource.into())
974 }
975}
976
977impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::ListTotpCredentialsRequest> {
978 type Principal = User;
979 type Resource = User;
980 type Response = pb::scufflecloud::core::v1::TotpCredentialsList;
981
982 const ACTION: Action = Action::ListTotpCredentials;
983
984 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
985 let global = &self.global::<G>()?;
986 let session = self.session_or_err()?;
987 common::get_user_by_id(global, session.user_id).await
988 }
989
990 async fn load_resource(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
991 let global = &self.global::<G>()?;
992 let user_id: UserId = self
993 .get_ref()
994 .id
995 .parse()
996 .into_tonic_err_with_field_violation("id", "invalid ID")?;
997 common::get_user_by_id(global, user_id).await
998 }
999
1000 async fn execute(
1001 self,
1002 _driver: &mut OperationDriver<'_, G>,
1003 _principal: Self::Principal,
1004 resource: Self::Resource,
1005 ) -> Result<Self::Response, tonic::Status> {
1006 let global = &self.global::<G>()?;
1007 let mut db = global.db().await.into_tonic_internal_err("failed to connect to database")?;
1008
1009 let credentials = mfa_totp_credentials::dsl::mfa_totp_credentials
1010 .filter(mfa_totp_credentials::dsl::user_id.eq(resource.id))
1011 .select(MfaTotpCredential::as_select())
1012 .load::<MfaTotpCredential>(&mut db)
1013 .await
1014 .into_tonic_internal_err("failed to query TOTP credentials")?;
1015
1016 Ok(pb::scufflecloud::core::v1::TotpCredentialsList {
1017 credentials: credentials.into_iter().map(Into::into).collect(),
1018 })
1019 }
1020}
1021
1022impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::UpdateTotpCredentialRequest> {
1023 type Principal = User;
1024 type Resource = MfaTotpCredential;
1025 type Response = pb::scufflecloud::core::v1::TotpCredential;
1026
1027 const ACTION: Action = Action::UpdateTotpCredential;
1028
1029 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
1030 let global = &self.global::<G>()?;
1031 let session = self.session_or_err()?;
1032 common::get_user_by_id(global, session.user_id).await
1033 }
1034
1035 async fn load_resource(&mut self, driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
1036 let user_id: UserId = self
1037 .get_ref()
1038 .user_id
1039 .parse()
1040 .into_tonic_err_with_field_violation("user_id", "invalid ID")?;
1041
1042 let credential_id: MfaTotpCredentialId = self
1043 .get_ref()
1044 .id
1045 .parse()
1046 .into_tonic_err_with_field_violation("id", "invalid ID")?;
1047
1048 let conn = driver.conn().await?;
1049 let credential = mfa_totp_credentials::dsl::mfa_totp_credentials
1050 .filter(
1051 mfa_totp_credentials::dsl::id
1052 .eq(credential_id)
1053 .and(mfa_totp_credentials::dsl::user_id.eq(user_id)),
1054 )
1055 .select(MfaTotpCredential::as_select())
1056 .first::<MfaTotpCredential>(conn)
1057 .await
1058 .into_tonic_internal_err("failed to find webauthn credential")?;
1059
1060 Ok(credential)
1061 }
1062
1063 async fn execute(
1064 self,
1065 driver: &mut OperationDriver<'_, G>,
1066 _principal: Self::Principal,
1067 resource: Self::Resource,
1068 ) -> Result<Self::Response, tonic::Status> {
1069 let conn = driver.conn().await?;
1070
1071 let updated_credential = if let Some(name) = &self.get_ref().name {
1072 diesel::update(mfa_totp_credentials::dsl::mfa_totp_credentials)
1073 .filter(mfa_totp_credentials::dsl::id.eq(resource.id))
1074 .set(mfa_totp_credentials::dsl::name.eq(name))
1075 .returning(MfaTotpCredential::as_returning())
1076 .get_result::<MfaTotpCredential>(conn)
1077 .await
1078 .into_tonic_internal_err("failed to update webauthn credential")?
1079 } else {
1080 resource
1081 };
1082
1083 Ok(updated_credential.into())
1084 }
1085}
1086
1087impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::DeleteTotpCredentialRequest> {
1088 type Principal = User;
1089 type Resource = MfaTotpCredential;
1090 type Response = pb::scufflecloud::core::v1::TotpCredential;
1091
1092 const ACTION: Action = Action::DeleteTotpCredential;
1093
1094 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
1095 let global = &self.global::<G>()?;
1096 let session = self.session_or_err()?;
1097 common::get_user_by_id(global, session.user_id).await
1098 }
1099
1100 async fn load_resource(&mut self, driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
1101 let user_id: UserId = self
1102 .get_ref()
1103 .user_id
1104 .parse()
1105 .into_tonic_err_with_field_violation("user_id", "invalid ID")?;
1106
1107 let credential_id: MfaTotpCredentialId = self
1108 .get_ref()
1109 .id
1110 .parse()
1111 .into_tonic_err_with_field_violation("id", "invalid ID")?;
1112
1113 let conn = driver.conn().await?;
1114 let credential = mfa_totp_credentials::dsl::mfa_totp_credentials
1115 .filter(
1116 mfa_totp_credentials::dsl::id
1117 .eq(credential_id)
1118 .and(mfa_totp_credentials::dsl::user_id.eq(user_id)),
1119 )
1120 .select(MfaTotpCredential::as_select())
1121 .first::<MfaTotpCredential>(conn)
1122 .await
1123 .into_tonic_internal_err("failed to delete TOTP credential")?;
1124
1125 Ok(credential)
1126 }
1127
1128 async fn execute(
1129 self,
1130 driver: &mut OperationDriver<'_, G>,
1131 _principal: Self::Principal,
1132 resource: Self::Resource,
1133 ) -> Result<Self::Response, tonic::Status> {
1134 let conn = driver.conn().await?;
1135 diesel::delete(mfa_totp_credentials::dsl::mfa_totp_credentials)
1136 .filter(mfa_totp_credentials::dsl::id.eq(resource.id))
1137 .execute(conn)
1138 .await
1139 .into_tonic_internal_err("failed to delete TOTP credential")?;
1140
1141 Ok(resource.into())
1142 }
1143}
1144
1145impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::RegenerateRecoveryCodesRequest> {
1146 type Principal = User;
1147 type Resource = User;
1148 type Response = pb::scufflecloud::core::v1::RecoveryCodes;
1149
1150 const ACTION: Action = Action::RegenerateRecoveryCodes;
1151
1152 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
1153 let global = &self.global::<G>()?;
1154 let session = self.session_or_err()?;
1155 common::get_user_by_id(global, session.user_id).await
1156 }
1157
1158 async fn load_resource(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
1159 let global = &self.global::<G>()?;
1160 let user_id: UserId = self
1161 .get_ref()
1162 .id
1163 .parse()
1164 .into_tonic_err_with_field_violation("id", "invalid ID")?;
1165 common::get_user_by_id(global, user_id).await
1166 }
1167
1168 async fn execute(
1169 self,
1170 driver: &mut OperationDriver<'_, G>,
1171 _principal: Self::Principal,
1172 resource: Self::Resource,
1173 ) -> Result<Self::Response, tonic::Status> {
1174 let mut rng = rand::rngs::OsRng;
1175 let codes: Vec<_> = (0..12)
1176 .map(|_| rand::distributions::Alphanumeric.sample_string(&mut rng, 8))
1177 .collect();
1178
1179 let argon2 = Argon2::default();
1180 let recovery_codes = codes
1181 .iter()
1182 .map(|code| {
1183 let salt = SaltString::generate(&mut rng);
1184 argon2.hash_password(code.as_bytes(), &salt).map(|hash| hash.to_string())
1185 })
1186 .map(|code_hash| {
1187 code_hash.map(|code_hash| MfaRecoveryCode {
1188 id: MfaRecoveryCodeId::new(),
1189 user_id: resource.id,
1190 code_hash,
1191 })
1192 })
1193 .collect::<Result<Vec<_>, _>>()
1194 .into_tonic_internal_err("failed to generate recovery codes")?;
1195
1196 let conn = driver.conn().await?;
1197 diesel::delete(mfa_recovery_codes::dsl::mfa_recovery_codes)
1198 .filter(mfa_recovery_codes::dsl::user_id.eq(resource.id))
1199 .execute(conn)
1200 .await
1201 .into_tonic_internal_err("failed to delete existing recovery codes")?;
1202
1203 diesel::insert_into(mfa_recovery_codes::dsl::mfa_recovery_codes)
1204 .values(recovery_codes)
1205 .execute(conn)
1206 .await
1207 .into_tonic_internal_err("failed to insert new recovery codes")?;
1208
1209 Ok(pb::scufflecloud::core::v1::RecoveryCodes { codes })
1210 }
1211}
1212
1213impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::DeleteUserRequest> {
1214 type Principal = User;
1215 type Resource = User;
1216 type Response = pb::scufflecloud::core::v1::User;
1217
1218 const ACTION: Action = Action::DeleteUser;
1219
1220 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
1221 let global = &self.global::<G>()?;
1222 let session = self.session_or_err()?;
1223 common::get_user_by_id(global, session.user_id).await
1224 }
1225
1226 async fn load_resource(&mut self, driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
1227 let user_id: UserId = self
1228 .get_ref()
1229 .id
1230 .parse()
1231 .into_tonic_err_with_field_violation("id", "invalid ID")?;
1232
1233 let conn = driver.conn().await?;
1234 common::get_user_by_id_in_tx(conn, user_id).await
1235 }
1236
1237 async fn execute(
1238 self,
1239 driver: &mut OperationDriver<'_, G>,
1240 _principal: Self::Principal,
1241 resource: Self::Resource,
1242 ) -> Result<Self::Response, tonic::Status> {
1243 let conn = driver.conn().await?;
1244
1245 diesel::delete(users::dsl::users)
1246 .filter(users::dsl::id.eq(resource.id))
1247 .execute(conn)
1248 .await
1249 .into_tonic_internal_err("failed to delete webauthn credential")?;
1250
1251 Ok(resource.into())
1252 }
1253}