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