scufflecloud_core/operations/
user_sessions.rs1use core_db_types::models::{User, UserId, UserSession, UserSessionTokenId};
2use core_db_types::schema::user_sessions;
3use diesel::{BoolExpressionMethods, ExpressionMethods, QueryDsl, SelectableHelper};
4use diesel_async::RunQueryDsl;
5use ext_traits::{OptionExt, RequestExt, ResultExt};
6use tonic_types::{ErrorDetails, StatusExt};
7
8use crate::cedar::{Action, CoreApplication};
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: Some(session.into()),
153 mfa_options: mfa_options.into_iter().map(|o| o as i32).collect(),
154 };
155
156 Ok(new_token)
157 }
158}
159
160impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::InvalidateUserSessionRequest> {
161 type Principal = User;
162 type Resource = UserSession;
163 type Response = ();
164
165 const ACTION: Action = Action::InvalidateUserSession;
166
167 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
168 let global = &self.global::<G>()?;
169 let session = self.session_or_err()?;
170 common::get_user_by_id(global, session.user_id).await
171 }
172
173 async fn load_resource(&mut self, driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
174 let user_id: UserId = self
175 .get_ref()
176 .user_id
177 .parse()
178 .into_tonic_err_with_field_violation("id", "invalid ID")?;
179 let device_fingerprint = &self.get_ref().device_fingerprint;
180
181 let session = diesel::delete(user_sessions::dsl::user_sessions)
182 .filter(
183 user_sessions::dsl::user_id
184 .eq(&user_id)
185 .and(user_sessions::dsl::device_fingerprint.eq(device_fingerprint)),
186 )
187 .returning(UserSession::as_select())
188 .get_result(driver.conn().await?)
189 .await
190 .into_tonic_internal_err("failed to delete user session")?;
191
192 Ok(session)
193 }
194
195 async fn execute(
196 self,
197 _driver: &mut OperationDriver<'_, G>,
198 _principal: Self::Principal,
199 _resource: Self::Resource,
200 ) -> Result<Self::Response, tonic::Status> {
201 Ok(())
202 }
203}
204
205impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::ListUserSessionsRequest> {
206 type Principal = User;
207 type Resource = CoreApplication;
208 type Response = pb::scufflecloud::core::v1::ListUserSessionsResponse;
209
210 const ACTION: Action = Action::ListUserSessions;
211
212 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
213 let global = &self.global::<G>()?;
214 let session = self.session_or_err()?;
215 common::get_user_by_id(global, session.user_id).await
216 }
217
218 async fn load_resource(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
219 Ok(CoreApplication)
220 }
221
222 async fn execute(
223 self,
224 _driver: &mut OperationDriver<'_, G>,
225 principal: Self::Principal,
226 _resource: Self::Resource,
227 ) -> Result<Self::Response, tonic::Status> {
228 let global = &self.global::<G>()?;
229 let mut db = global.db().await.into_tonic_internal_err("failed to connect to database")?;
230
231 let sessions = user_sessions::dsl::user_sessions
232 .filter(user_sessions::dsl::user_id.eq(principal.id))
233 .load::<UserSession>(&mut db)
234 .await
235 .into_tonic_internal_err("failed to load user sessions")?;
236
237 Ok(pb::scufflecloud::core::v1::ListUserSessionsResponse {
238 sessions: sessions.into_iter().map(Into::into).collect(),
239 })
240 }
241}