scufflecloud_core/operations/
user_session_requests.rs1use core_db_types::models::{User, UserSessionRequest, UserSessionRequestId};
2use core_db_types::schema::user_session_requests;
3use diesel::{BoolExpressionMethods, ExpressionMethods, OptionalExtension, QueryDsl, SelectableHelper};
4use diesel_async::RunQueryDsl;
5use ext_traits::{OptionExt, RequestExt, ResultExt};
6use geo_ip::GeoIpRequestExt;
7use rand::Rng;
8use tonic::Code;
9use tonic_types::{ErrorDetails, StatusExt};
10
11use crate::cedar::{Action, Unauthenticated};
12use crate::common;
13use crate::http_ext::CoreRequestExt;
14use crate::operations::{Operation, OperationDriver};
15
16impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::CreateUserSessionRequestRequest> {
17 type Principal = Unauthenticated;
18 type Resource = UserSessionRequest;
19 type Response = pb::scufflecloud::core::v1::UserSessionRequest;
20
21 const ACTION: Action = Action::CreateUserSessionRequest;
22
23 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
24 Ok(Unauthenticated)
25 }
26
27 async fn load_resource(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
28 let global = &self.global::<G>()?;
29 let ip_info = self.ip_address_info()?;
30 let code = format!("{:06}", rand::rngs::OsRng.gen_range(0..=999999));
31
32 Ok(UserSessionRequest {
33 id: UserSessionRequestId::new(),
34 device_name: self.get_ref().name.clone(),
35 device_ip: ip_info.to_network(),
36 code,
37 approved_by: None,
38 expires_at: chrono::Utc::now() + global.timeout_config().user_session_request,
39 })
40 }
41
42 async fn execute(
43 self,
44 _driver: &mut OperationDriver<'_, G>,
45 _principal: Self::Principal,
46 resource: Self::Resource,
47 ) -> Result<Self::Response, tonic::Status> {
48 let global = &self.global::<G>()?;
49 let mut db = global.db().await.into_tonic_internal_err("failed to connect to database")?;
50
51 diesel::insert_into(user_session_requests::dsl::user_session_requests)
52 .values(&resource)
53 .execute(&mut db)
54 .await
55 .into_tonic_internal_err("failed to insert user session request")?;
56
57 Ok(resource.into())
58 }
59}
60
61impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::GetUserSessionRequestRequest> {
62 type Principal = Unauthenticated;
63 type Resource = UserSessionRequest;
64 type Response = pb::scufflecloud::core::v1::UserSessionRequest;
65
66 const ACTION: Action = Action::GetUserSessionRequest;
67
68 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
69 Ok(Unauthenticated)
70 }
71
72 async fn load_resource(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
73 let global = &self.global::<G>()?;
74 let mut db = global.db().await.into_tonic_internal_err("failed to connect to database")?;
75
76 let id: UserSessionRequestId = self
77 .get_ref()
78 .id
79 .parse()
80 .into_tonic_err_with_field_violation("id", "invalid ID")?;
81
82 let Some(session_request) = user_session_requests::dsl::user_session_requests
83 .find(&id)
84 .filter(user_session_requests::dsl::expires_at.gt(chrono::Utc::now()))
85 .select(UserSessionRequest::as_select())
86 .first::<UserSessionRequest>(&mut db)
87 .await
88 .optional()
89 .into_tonic_internal_err("failed to query user session request")?
90 else {
91 return Err(tonic::Status::with_error_details(
92 tonic::Code::NotFound,
93 "user session request not found",
94 ErrorDetails::new(),
95 ));
96 };
97
98 Ok(session_request)
99 }
100
101 async fn execute(
102 self,
103 _driver: &mut OperationDriver<'_, G>,
104 _principal: Self::Principal,
105 resource: Self::Resource,
106 ) -> Result<Self::Response, tonic::Status> {
107 Ok(resource.into())
108 }
109}
110
111impl<G: core_traits::Global> Operation<G>
112 for tonic::Request<pb::scufflecloud::core::v1::GetUserSessionRequestByCodeRequest>
113{
114 type Principal = Unauthenticated;
115 type Resource = UserSessionRequest;
116 type Response = pb::scufflecloud::core::v1::UserSessionRequest;
117
118 const ACTION: Action = Action::GetUserSessionRequest;
119
120 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
121 Ok(Unauthenticated)
122 }
123
124 async fn load_resource(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
125 let global = &self.global::<G>()?;
126 let mut db = global.db().await.into_tonic_internal_err("failed to connect to database")?;
127
128 let Some(session_request) = user_session_requests::dsl::user_session_requests
129 .filter(
130 user_session_requests::dsl::code
131 .eq(&self.get_ref().code)
132 .and(user_session_requests::dsl::expires_at.gt(chrono::Utc::now())),
133 )
134 .select(UserSessionRequest::as_select())
135 .first::<UserSessionRequest>(&mut db)
136 .await
137 .optional()
138 .into_tonic_internal_err("failed to query user session request")?
139 else {
140 return Err(tonic::Status::with_error_details(
141 tonic::Code::NotFound,
142 "user session request not found",
143 ErrorDetails::new(),
144 ));
145 };
146
147 Ok(session_request)
148 }
149
150 async fn execute(
151 self,
152 _driver: &mut OperationDriver<'_, G>,
153 _principal: Self::Principal,
154 resource: Self::Resource,
155 ) -> Result<Self::Response, tonic::Status> {
156 Ok(resource.into())
157 }
158}
159
160impl<G: core_traits::Global> Operation<G>
161 for tonic::Request<pb::scufflecloud::core::v1::ApproveUserSessionRequestByCodeRequest>
162{
163 type Principal = User;
164 type Resource = UserSessionRequest;
165 type Response = pb::scufflecloud::core::v1::UserSessionRequest;
166
167 const ACTION: Action = Action::ApproveUserSessionRequest;
168
169 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
170 let global = &self.global::<G>()?;
171 let session = self.session_or_err()?;
172 common::get_user_by_id(global, session.user_id).await
173 }
174
175 async fn load_resource(&mut self, driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
176 let conn = driver.conn().await?;
177
178 let Some(session_request) = user_session_requests::dsl::user_session_requests
179 .filter(
180 user_session_requests::dsl::code
181 .eq(&self.get_ref().code)
182 .and(user_session_requests::dsl::approved_by.is_null())
183 .and(user_session_requests::dsl::expires_at.gt(chrono::Utc::now())),
184 )
185 .select(UserSessionRequest::as_select())
186 .first::<UserSessionRequest>(conn)
187 .await
188 .optional()
189 .into_tonic_internal_err("failed to query user session request")?
190 else {
191 return Err(tonic::Status::with_error_details(
192 tonic::Code::NotFound,
193 "user session request not found",
194 ErrorDetails::new(),
195 ));
196 };
197
198 Ok(session_request)
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 let conn = driver.conn().await?;
208
209 let session_request = diesel::update(user_session_requests::dsl::user_session_requests)
210 .filter(user_session_requests::dsl::id.eq(resource.id))
211 .set(user_session_requests::dsl::approved_by.eq(&principal.id))
212 .returning(UserSessionRequest::as_select())
213 .get_result::<UserSessionRequest>(conn)
214 .await
215 .into_tonic_internal_err("failed to update user session request")?;
216
217 Ok(session_request.into())
218 }
219}
220
221impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::CompleteUserSessionRequestRequest> {
222 type Principal = Unauthenticated;
223 type Resource = UserSessionRequest;
224 type Response = pb::scufflecloud::core::v1::NewUserSessionToken;
225
226 const ACTION: Action = Action::CompleteUserSessionRequest;
227
228 async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
229 Ok(Unauthenticated)
230 }
231
232 async fn load_resource(&mut self, driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
233 let id: UserSessionRequestId = self
234 .get_ref()
235 .id
236 .parse()
237 .into_tonic_err_with_field_violation("id", "invalid ID")?;
238
239 let conn = driver.conn().await?;
240
241 let Some(session_request) = diesel::delete(user_session_requests::dsl::user_session_requests)
243 .filter(user_session_requests::dsl::id.eq(id))
244 .returning(UserSessionRequest::as_select())
245 .get_result::<UserSessionRequest>(conn)
246 .await
247 .optional()
248 .into_tonic_internal_err("failed to delete user session request")?
249 else {
250 return Err(tonic::Status::with_error_details(
251 Code::NotFound,
252 "unknown id",
253 ErrorDetails::new(),
254 ));
255 };
256
257 Ok(session_request)
258 }
259
260 async fn execute(
261 self,
262 driver: &mut OperationDriver<'_, G>,
263 _principal: Self::Principal,
264 resource: Self::Resource,
265 ) -> Result<Self::Response, tonic::Status> {
266 let global = &self.global::<G>()?;
267 let ip_info = self.ip_address_info()?;
268 let payload = self.into_inner();
269
270 let device = payload.device.require("device")?;
271
272 let Some(approved_by) = resource.approved_by else {
273 return Err(tonic::Status::with_error_details(
274 tonic::Code::FailedPrecondition,
275 "user session request is not approved yet",
276 ErrorDetails::new(),
277 ));
278 };
279 let approved_by = common::get_user_by_id(global, approved_by).await?;
280
281 let conn = driver.conn().await?;
282
283 let new_token = common::create_session(global, conn, &approved_by, device, &ip_info, false).await?;
284 Ok(new_token)
285 }
286}