scufflecloud_core/operations/
users.rs

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            // Verify password
95            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        // Generate random code
239        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        // Check if email is already registered
244        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        // Create email registration request
263        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        // Send email
278        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        // Delete email registration request
319        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        // Check if email is already registered
340        if user_emails::dsl::user_emails
341            .find(&registration_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(&reg, &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}