scufflecloud_core/operations/
user_sessions.rs1use diesel::{BoolExpressionMethods, ExpressionMethods, SelectableHelper};
2use diesel_async::RunQueryDsl;
3use tonic_types::{ErrorDetails, StatusExt};
4
5use crate::cedar::Action;
6use crate::chrono_ext::ChronoDateTimeExt;
7use crate::http_ext::RequestExt;
8use crate::models::{User, UserSession, UserSessionTokenId};
9use crate::operations::Operation;
10use crate::schema::user_sessions;
11use crate::std_ext::{OptionExt, ResultExt};
12use crate::{CoreConfig, common, totp};
13
14impl<G: CoreConfig> 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(
22 &mut self,
23 _conn: &mut diesel_async::AsyncPgConnection,
24 ) -> Result<Self::Principal, tonic::Status> {
25 let global = &self.global::<G>()?;
26 let session = self.session_or_err()?;
27 common::get_user_by_id(global, session.user_id).await
28 }
29
30 async fn load_resource(&mut self, _conn: &mut diesel_async::AsyncPgConnection) -> Result<Self::Resource, tonic::Status> {
31 let session = self.session_or_err()?;
32 Ok(session.clone())
33 }
34
35 async fn execute(
36 self,
37 conn: &mut diesel_async::AsyncPgConnection,
38 _principal: Self::Principal,
39 resource: Self::Resource,
40 ) -> Result<Self::Response, tonic::Status> {
41 let global = &self.global::<G>()?;
42 let payload = self.into_inner();
43
44 match payload.response.require("response")? {
46 pb::scufflecloud::core::v1::validate_mfa_for_user_session_request::Response::Totp(
47 pb::scufflecloud::core::v1::ValidateMfaForUserSessionTotp { code },
48 ) => {
49 totp::process_token(conn, resource.user_id, &code).await?;
50 }
51 pb::scufflecloud::core::v1::validate_mfa_for_user_session_request::Response::Webauthn(
52 pb::scufflecloud::core::v1::ValidateMfaForUserSessionWebauthn { response_json },
53 ) => {
54 let pk_cred: webauthn_rs::prelude::PublicKeyCredential = serde_json::from_str(&response_json)
55 .into_tonic_err_with_field_violation("response_json", "invalid public key credential")?;
56 common::finish_webauthn_authentication(global, conn, resource.user_id, &pk_cred).await?;
57 }
58 pb::scufflecloud::core::v1::validate_mfa_for_user_session_request::Response::RecoveryCode(
59 pb::scufflecloud::core::v1::ValidateMfaForUserSessionRecoveryCode { code },
60 ) => {
61 common::process_recovery_code(conn, resource.user_id, &code).await?;
62 }
63 }
64
65 let session = diesel::update(user_sessions::dsl::user_sessions)
67 .filter(
68 user_sessions::dsl::user_id
69 .eq(&resource.user_id)
70 .and(user_sessions::dsl::device_fingerprint.eq(&resource.device_fingerprint)),
71 )
72 .set((
73 user_sessions::dsl::mfa_pending.eq(false),
74 user_sessions::dsl::expires_at.eq(chrono::Utc::now() + global.user_session_timeout()),
75 ))
76 .returning(UserSession::as_select())
77 .get_result::<UserSession>(conn)
78 .await
79 .into_tonic_internal_err("failed to update user session")?;
80
81 Ok(session.into())
82 }
83}
84
85pub(crate) struct RefreshUserSessionRequest;
86
87impl<G: CoreConfig> Operation<G> for tonic::Request<RefreshUserSessionRequest> {
88 type Principal = User;
89 type Resource = UserSession;
90 type Response = pb::scufflecloud::core::v1::NewUserSessionToken;
91
92 const ACTION: Action = Action::RefreshUserSession;
93
94 async fn load_principal(
95 &mut self,
96 _conn: &mut diesel_async::AsyncPgConnection,
97 ) -> Result<Self::Principal, tonic::Status> {
98 let global = &self.global::<G>()?;
99 let session = self.session_or_err()?;
100 common::get_user_by_id(global, session.user_id).await
101 }
102
103 async fn load_resource(&mut self, _conn: &mut diesel_async::AsyncPgConnection) -> Result<Self::Resource, tonic::Status> {
104 let session = self.expired_session_or_err()?;
105 Ok(session.clone())
106 }
107
108 async fn execute(
109 self,
110 conn: &mut diesel_async::AsyncPgConnection,
111 _principal: Self::Principal,
112 resource: Self::Resource,
113 ) -> Result<Self::Response, tonic::Status> {
114 let global = &self.global::<G>()?;
115
116 let token_id = UserSessionTokenId::new();
117 let token = common::generate_random_bytes().into_tonic_internal_err("failed to generate token")?;
118 let encrypted_token = common::encrypt_token(resource.device_algorithm.into(), &token, &resource.device_pk_data)?;
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.user_session_token_timeout()),
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 new_token = pb::scufflecloud::core::v1::NewUserSessionToken {
151 id: token_id.to_string(),
152 encrypted_token,
153 expires_at: Some(token_expires_at.to_prost_timestamp_utc()),
154 session_mfa_pending: session.mfa_pending,
155 mfa_options: mfa_options.into_iter().map(|o| o as i32).collect(),
156 };
157
158 Ok(new_token)
159 }
160}
161
162pub(crate) struct InvalidateUserSessionRequest;
163
164impl<G: CoreConfig> Operation<G> for tonic::Request<InvalidateUserSessionRequest> {
165 type Principal = User;
166 type Resource = UserSession;
167 type Response = ();
168
169 const ACTION: Action = Action::InvalidateUserSession;
170 const TRANSACTION: bool = false;
171
172 async fn load_principal(
173 &mut self,
174 _conn: &mut diesel_async::AsyncPgConnection,
175 ) -> Result<Self::Principal, tonic::Status> {
176 let global = &self.global::<G>()?;
177 let session = self.session_or_err()?;
178 common::get_user_by_id(global, session.user_id).await
179 }
180
181 async fn load_resource(&mut self, _conn: &mut diesel_async::AsyncPgConnection) -> Result<Self::Resource, tonic::Status> {
182 let session = self.session_or_err()?;
183 Ok(session.clone())
184 }
185
186 async fn execute(
187 self,
188 conn: &mut diesel_async::AsyncPgConnection,
189 _principal: Self::Principal,
190 resource: Self::Resource,
191 ) -> Result<Self::Response, tonic::Status> {
192 diesel::delete(user_sessions::dsl::user_sessions)
193 .filter(
194 user_sessions::dsl::user_id
195 .eq(&resource.user_id)
196 .and(user_sessions::dsl::device_fingerprint.eq(&resource.device_fingerprint)),
197 )
198 .execute(conn)
199 .await
200 .into_tonic_internal_err("failed to delete user session")?;
201
202 Ok(())
203 }
204}