scufflecloud_core/operations/
user_sessions.rs1use core_db_types::models::{User, UserSession, UserSessionTokenId};
2use core_db_types::schema::user_sessions;
3use diesel::{BoolExpressionMethods, ExpressionMethods, SelectableHelper};
4use diesel_async::RunQueryDsl;
5use ext_traits::{OptionExt, RequestExt, ResultExt};
6use tonic_types::{ErrorDetails, StatusExt};
7
8use crate::cedar::Action;
9use crate::chrono_ext::ChronoDateTimeExt;
10use crate::http_ext::CoreRequestExt;
11use crate::operations::{Operation, OperationDriver};
12use crate::{common, totp};
13
14impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::ValidateMfaForUserSessionRequest> {
15 type Principal = User;
16 type Resource = UserSession;
17 type Response = pb::scufflecloud::core::v1::UserSession;
18
19 const ACTION: Action = Action::ValidateMfaForUserSession;
20
21 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
22 let global = &self.global::<G>()?;
23 let session = self.session_or_err()?;
24 common::get_user_by_id(global, session.user_id).await
25 }
26
27 async fn load_resource(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
28 let session = self.session_or_err()?;
29 Ok(session.clone())
30 }
31
32 async fn execute(
33 self,
34 driver: &mut OperationDriver<'_, G>,
35 _principal: Self::Principal,
36 resource: Self::Resource,
37 ) -> Result<Self::Response, tonic::Status> {
38 let global = &self.global::<G>()?;
39 let payload = self.into_inner();
40
41 let conn = driver.conn().await?;
42
43 match payload.response.require("response")? {
45 pb::scufflecloud::core::v1::validate_mfa_for_user_session_request::Response::Totp(
46 pb::scufflecloud::core::v1::ValidateMfaForUserSessionTotp { code },
47 ) => {
48 totp::process_token(conn, resource.user_id, &code).await?;
49 }
50 pb::scufflecloud::core::v1::validate_mfa_for_user_session_request::Response::Webauthn(
51 pb::scufflecloud::core::v1::ValidateMfaForUserSessionWebauthn { response_json },
52 ) => {
53 let pk_cred: webauthn_rs::prelude::PublicKeyCredential = serde_json::from_str(&response_json)
54 .into_tonic_err_with_field_violation("response_json", "invalid public key credential")?;
55 common::finish_webauthn_authentication(global, conn, resource.user_id, &pk_cred).await?;
56 }
57 pb::scufflecloud::core::v1::validate_mfa_for_user_session_request::Response::RecoveryCode(
58 pb::scufflecloud::core::v1::ValidateMfaForUserSessionRecoveryCode { code },
59 ) => {
60 common::process_recovery_code(conn, resource.user_id, &code).await?;
61 }
62 }
63
64 let session = diesel::update(user_sessions::dsl::user_sessions)
66 .filter(
67 user_sessions::dsl::user_id
68 .eq(&resource.user_id)
69 .and(user_sessions::dsl::device_fingerprint.eq(&resource.device_fingerprint)),
70 )
71 .set((
72 user_sessions::dsl::mfa_pending.eq(false),
73 user_sessions::dsl::expires_at.eq(chrono::Utc::now() + global.timeout_config().user_session_token),
74 ))
75 .returning(UserSession::as_select())
76 .get_result::<UserSession>(conn)
77 .await
78 .into_tonic_internal_err("failed to update user session")?;
79
80 Ok(session.into())
81 }
82}
83
84pub(crate) struct RefreshUserSessionRequest;
85
86impl<G: core_traits::Global> Operation<G> for tonic::Request<RefreshUserSessionRequest> {
87 type Principal = User;
88 type Resource = UserSession;
89 type Response = pb::scufflecloud::core::v1::NewUserSessionToken;
90
91 const ACTION: Action = Action::RefreshUserSession;
92
93 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
94 let global = &self.global::<G>()?;
95 let session = self.expired_session_or_err()?;
96 common::get_user_by_id(global, session.user_id).await
97 }
98
99 async fn load_resource(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
100 let session = self.expired_session_or_err()?;
101 Ok(session.clone())
102 }
103
104 async fn execute(
105 self,
106 driver: &mut OperationDriver<'_, G>,
107 _principal: Self::Principal,
108 resource: Self::Resource,
109 ) -> Result<Self::Response, tonic::Status> {
110 let global = &self.global::<G>()?;
111
112 let token_id = UserSessionTokenId::new();
113 let token = common::generate_random_bytes().into_tonic_internal_err("failed to generate token")?;
114 let encrypted_token = common::encrypt_token(resource.device_algorithm.into(), &token, &resource.device_pk_data)?;
115 let conn = driver.conn().await?;
116
117 let session = diesel::update(user_sessions::dsl::user_sessions)
118 .filter(
119 user_sessions::dsl::user_id
120 .eq(&resource.user_id)
121 .and(user_sessions::dsl::device_fingerprint.eq(&resource.device_fingerprint)),
122 )
123 .set((
124 user_sessions::dsl::token_id.eq(token_id),
125 user_sessions::dsl::token.eq(token),
126 user_sessions::dsl::token_expires_at.eq(chrono::Utc::now() + global.timeout_config().user_session_token),
127 ))
128 .returning(UserSession::as_select())
129 .get_result::<UserSession>(conn)
130 .await
131 .into_tonic_internal_err("failed to update user session")?;
132
133 let (Some(token_id), Some(token_expires_at)) = (session.token_id, session.token_expires_at) else {
134 return Err(tonic::Status::with_error_details(
135 tonic::Code::Internal,
136 "user session does not have a token",
137 ErrorDetails::new(),
138 ));
139 };
140
141 let mfa_options = if session.mfa_pending {
142 common::mfa_options(conn, session.user_id).await?
143 } else {
144 vec![]
145 };
146
147 let new_token = pb::scufflecloud::core::v1::NewUserSessionToken {
148 id: token_id.to_string(),
149 encrypted_token,
150 user_id: session.user_id.to_string(),
151 expires_at: Some(token_expires_at.to_prost_timestamp_utc()),
152 session_expires_at: Some(session.expires_at.to_prost_timestamp_utc()),
153 session_mfa_pending: session.mfa_pending,
154 mfa_options: mfa_options.into_iter().map(|o| o as i32).collect(),
155 };
156
157 Ok(new_token)
158 }
159}
160
161pub(crate) struct InvalidateUserSessionRequest;
162
163impl<G: core_traits::Global> Operation<G> for tonic::Request<InvalidateUserSessionRequest> {
164 type Principal = User;
165 type Resource = UserSession;
166 type Response = ();
167
168 const ACTION: Action = Action::InvalidateUserSession;
169
170 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
171 let global = &self.global::<G>()?;
172 let session = self.session_or_err()?;
173 common::get_user_by_id(global, session.user_id).await
174 }
175
176 async fn load_resource(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
177 let session = self.session_or_err()?;
178 Ok(session.clone())
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 diesel::delete(user_sessions::dsl::user_sessions)
191 .filter(
192 user_sessions::dsl::user_id
193 .eq(&resource.user_id)
194 .and(user_sessions::dsl::device_fingerprint.eq(&resource.device_fingerprint)),
195 )
196 .execute(&mut db)
197 .await
198 .into_tonic_internal_err("failed to delete user session")?;
199
200 Ok(())
201 }
202}