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