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 let ip_info = common::ip_to_pb(global, session.last_ip.ip())?;
81 let ua_info = session.last_user_agent.clone().map(common::ua_to_pb);
82
83 Ok(session.into_pb(ip_info, ua_info))
84 }
85}
86
87pub(crate) struct RefreshUserSessionRequest;
88
89impl<G: core_traits::Global> Operation<G> for tonic::Request<RefreshUserSessionRequest> {
90 type Principal = User;
91 type Resource = UserSession;
92 type Response = pb::scufflecloud::core::v1::NewUserSessionToken;
93
94 const ACTION: Action = Action::RefreshUserSession;
95
96 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
97 let global = &self.global::<G>()?;
98 let session = self.expired_session_or_err()?;
99 common::get_user_by_id(global, session.user_id).await
100 }
101
102 async fn load_resource(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
103 let session = self.expired_session_or_err()?;
104 Ok(session.clone())
105 }
106
107 async fn execute(
108 self,
109 driver: &mut OperationDriver<'_, G>,
110 _principal: Self::Principal,
111 resource: Self::Resource,
112 ) -> Result<Self::Response, tonic::Status> {
113 let global = &self.global::<G>()?;
114
115 let token_id = UserSessionTokenId::new();
116 let token = common::generate_random_bytes().into_tonic_internal_err("failed to generate token")?;
117 let encrypted_token = common::encrypt_token(resource.device_algorithm.into(), &token, &resource.device_pk_data)?;
118 let conn = driver.conn().await?;
119
120 let session = diesel::update(user_sessions::dsl::user_sessions)
121 .filter(
122 user_sessions::dsl::user_id
123 .eq(&resource.user_id)
124 .and(user_sessions::dsl::device_fingerprint.eq(&resource.device_fingerprint)),
125 )
126 .set((
127 user_sessions::dsl::token_id.eq(token_id),
128 user_sessions::dsl::token.eq(token),
129 user_sessions::dsl::token_expires_at.eq(chrono::Utc::now() + global.timeout_config().user_session_token),
130 ))
131 .returning(UserSession::as_select())
132 .get_result::<UserSession>(conn)
133 .await
134 .into_tonic_internal_err("failed to update user session")?;
135
136 let (Some(token_id), Some(token_expires_at)) = (session.token_id, session.token_expires_at) else {
137 return Err(tonic::Status::with_error_details(
138 tonic::Code::Internal,
139 "user session does not have a token",
140 ErrorDetails::new(),
141 ));
142 };
143
144 let mfa_options = if session.mfa_pending {
145 common::mfa_options(conn, session.user_id).await?
146 } else {
147 vec![]
148 };
149
150 let session_ip_info = common::ip_to_pb(global, session.last_ip.ip())?;
151 let session_ua_info = session.last_user_agent.clone().map(common::ua_to_pb);
152
153 let new_token = pb::scufflecloud::core::v1::NewUserSessionToken {
154 id: token_id.to_string(),
155 encrypted_token,
156 user_id: session.user_id.to_string(),
157 expires_at: Some(token_expires_at.to_prost_timestamp_utc()),
158 session: Some(session.into_pb(session_ip_info, session_ua_info)),
159 mfa_options: mfa_options.into_iter().map(|o| o as i32).collect(),
160 };
161
162 Ok(new_token)
163 }
164}
165
166impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::InvalidateUserSessionRequest> {
167 type Principal = User;
168 type Resource = UserSession;
169 type Response = ();
170
171 const ACTION: Action = Action::InvalidateUserSession;
172
173 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
174 let global = &self.global::<G>()?;
175 let session = self.session_or_err()?;
176 common::get_user_by_id(global, session.user_id).await
177 }
178
179 async fn load_resource(&mut self, driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
180 let user_id: UserId = self
181 .get_ref()
182 .user_id
183 .parse()
184 .into_tonic_err_with_field_violation("id", "invalid ID")?;
185 let device_fingerprint = &self.get_ref().device_fingerprint;
186
187 let session = diesel::delete(user_sessions::dsl::user_sessions)
188 .filter(
189 user_sessions::dsl::user_id
190 .eq(&user_id)
191 .and(user_sessions::dsl::device_fingerprint.eq(device_fingerprint)),
192 )
193 .returning(UserSession::as_select())
194 .get_result(driver.conn().await?)
195 .await
196 .into_tonic_internal_err("failed to delete user session")?;
197
198 Ok(session)
199 }
200
201 async fn execute(
202 self,
203 _driver: &mut OperationDriver<'_, G>,
204 _principal: Self::Principal,
205 _resource: Self::Resource,
206 ) -> Result<Self::Response, tonic::Status> {
207 Ok(())
208 }
209}
210
211impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::ListUserSessionsRequest> {
212 type Principal = User;
213 type Resource = CoreApplication;
214 type Response = pb::scufflecloud::core::v1::ListUserSessionsResponse;
215
216 const ACTION: Action = Action::ListUserSessions;
217
218 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
219 let global = &self.global::<G>()?;
220 let session = self.session_or_err()?;
221 common::get_user_by_id(global, session.user_id).await
222 }
223
224 async fn load_resource(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
225 Ok(CoreApplication)
226 }
227
228 async fn execute(
229 self,
230 _driver: &mut OperationDriver<'_, G>,
231 principal: Self::Principal,
232 _resource: Self::Resource,
233 ) -> Result<Self::Response, tonic::Status> {
234 let global = &self.global::<G>()?;
235 let mut db = global.db().await.into_tonic_internal_err("failed to connect to database")?;
236
237 let sessions = user_sessions::dsl::user_sessions
238 .filter(user_sessions::dsl::user_id.eq(principal.id))
239 .load::<UserSession>(&mut db)
240 .await
241 .into_tonic_internal_err("failed to load user sessions")?;
242
243 let sessions = sessions
244 .into_iter()
245 .map(|s| {
246 let last_ip = common::ip_to_pb(global, s.last_ip.ip())?;
247 let ua_info = s.last_user_agent.clone().map(common::ua_to_pb);
248 Ok(s.into_pb(last_ip, ua_info))
249 })
250 .collect::<Result<_, tonic::Status>>()?;
251
252 Ok(pb::scufflecloud::core::v1::ListUserSessionsResponse { sessions })
253 }
254}