scufflecloud_core/operations/
user_session_requests.rs1use diesel::{BoolExpressionMethods, ExpressionMethods, OptionalExtension, QueryDsl, SelectableHelper};
2use diesel_async::RunQueryDsl;
3use rand::Rng;
4use tonic::Code;
5use tonic_types::{ErrorDetails, StatusExt};
6
7use crate::cedar::{Action, Unauthenticated};
8use crate::http_ext::RequestExt;
9use crate::models::{User, UserSessionRequest, UserSessionRequestId};
10use crate::operations::Operation;
11use crate::schema::user_session_requests;
12use crate::std_ext::{OptionExt, ResultExt};
13use crate::{CoreConfig, common};
14
15impl<G: CoreConfig> 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 const TRANSACTION: bool = false;
22
23 async fn load_principal(
24 &mut self,
25 _conn: &mut diesel_async::AsyncPgConnection,
26 ) -> Result<Self::Principal, tonic::Status> {
27 Ok(Unauthenticated)
28 }
29
30 async fn load_resource(&mut self, _conn: &mut diesel_async::AsyncPgConnection) -> Result<Self::Resource, tonic::Status> {
31 let global = &self.global::<G>()?;
32 let ip_info = self.ip_address_info()?;
33 let code = format!("{:06}", rand::rngs::OsRng.gen_range(0..=999999));
34
35 Ok(UserSessionRequest {
36 id: UserSessionRequestId::new(),
37 device_name: self.get_ref().name.clone(),
38 device_ip: ip_info.to_network(),
39 code,
40 approved_by: None,
41 expires_at: chrono::Utc::now() + global.user_session_request_timeout(),
42 })
43 }
44
45 async fn execute(
46 self,
47 conn: &mut diesel_async::AsyncPgConnection,
48 _principal: Self::Principal,
49 resource: Self::Resource,
50 ) -> Result<Self::Response, tonic::Status> {
51 diesel::insert_into(user_session_requests::dsl::user_session_requests)
52 .values(&resource)
53 .execute(conn)
54 .await
55 .into_tonic_internal_err("failed to insert user session request")?;
56
57 Ok(resource.into())
58 }
59}
60
61impl<G: CoreConfig> 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 const TRANSACTION: bool = false;
68
69 async fn load_principal(
70 &mut self,
71 _conn: &mut diesel_async::AsyncPgConnection,
72 ) -> Result<Self::Principal, tonic::Status> {
73 Ok(Unauthenticated)
74 }
75
76 async fn load_resource(&mut self, conn: &mut diesel_async::AsyncPgConnection) -> Result<Self::Resource, tonic::Status> {
77 let id: UserSessionRequestId = self
78 .get_ref()
79 .id
80 .parse()
81 .into_tonic_err_with_field_violation("id", "invalid ID")?;
82
83 let Some(session_request) = user_session_requests::dsl::user_session_requests
84 .find(&id)
85 .filter(user_session_requests::dsl::expires_at.gt(chrono::Utc::now()))
86 .select(UserSessionRequest::as_select())
87 .first::<UserSessionRequest>(conn)
88 .await
89 .optional()
90 .into_tonic_internal_err("failed to query user session request")?
91 else {
92 return Err(tonic::Status::with_error_details(
93 tonic::Code::NotFound,
94 "user session request not found",
95 ErrorDetails::new(),
96 ));
97 };
98
99 Ok(session_request)
100 }
101
102 async fn execute(
103 self,
104 _conn: &mut diesel_async::AsyncPgConnection,
105 _principal: Self::Principal,
106 resource: Self::Resource,
107 ) -> Result<Self::Response, tonic::Status> {
108 Ok(resource.into())
109 }
110}
111
112impl<G: CoreConfig> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::GetUserSessionRequestByCodeRequest> {
113 type Principal = Unauthenticated;
114 type Resource = UserSessionRequest;
115 type Response = pb::scufflecloud::core::v1::UserSessionRequest;
116
117 const ACTION: Action = Action::GetUserSessionRequest;
118 const TRANSACTION: bool = false;
119
120 async fn load_principal(
121 &mut self,
122 _conn: &mut diesel_async::AsyncPgConnection,
123 ) -> Result<Self::Principal, tonic::Status> {
124 Ok(Unauthenticated)
125 }
126
127 async fn load_resource(&mut self, conn: &mut diesel_async::AsyncPgConnection) -> Result<Self::Resource, tonic::Status> {
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>(conn)
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 _conn: &mut diesel_async::AsyncPgConnection,
153 _principal: Self::Principal,
154 resource: Self::Resource,
155 ) -> Result<Self::Response, tonic::Status> {
156 Ok(resource.into())
157 }
158}
159
160impl<G: CoreConfig> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::ApproveUserSessionRequestByCodeRequest> {
161 type Principal = User;
162 type Resource = UserSessionRequest;
163 type Response = pb::scufflecloud::core::v1::UserSessionRequest;
164
165 const ACTION: Action = Action::ApproveUserSessionRequest;
166
167 async fn load_principal(
168 &mut self,
169 _conn: &mut diesel_async::AsyncPgConnection,
170 ) -> Result<Self::Principal, tonic::Status> {
171 let global = &self.global::<G>()?;
172 let session = self.session_or_err()?;
173 common::get_user_by_id(global, session.user_id).await
174 }
175
176 async fn load_resource(&mut self, conn: &mut diesel_async::AsyncPgConnection) -> Result<Self::Resource, tonic::Status> {
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 conn: &mut diesel_async::AsyncPgConnection,
203 principal: Self::Principal,
204 resource: Self::Resource,
205 ) -> Result<Self::Response, tonic::Status> {
206 let session_request = diesel::update(user_session_requests::dsl::user_session_requests)
207 .filter(user_session_requests::dsl::id.eq(resource.id))
208 .set(user_session_requests::dsl::approved_by.eq(&principal.id))
209 .returning(UserSessionRequest::as_select())
210 .get_result::<UserSessionRequest>(conn)
211 .await
212 .into_tonic_internal_err("failed to update user session request")?;
213
214 Ok(session_request.into())
215 }
216}
217
218impl<G: CoreConfig> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::CompleteUserSessionRequestRequest> {
219 type Principal = Unauthenticated;
220 type Resource = UserSessionRequest;
221 type Response = pb::scufflecloud::core::v1::NewUserSessionToken;
222
223 const ACTION: Action = Action::CompleteUserSessionRequest;
224
225 async fn load_principal(
226 &mut self,
227 _conn: &mut diesel_async::AsyncPgConnection,
228 ) -> Result<Self::Principal, tonic::Status> {
229 Ok(Unauthenticated)
230 }
231
232 async fn load_resource(&mut self, conn: &mut diesel_async::AsyncPgConnection) -> 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 Some(session_request) = diesel::delete(user_session_requests::dsl::user_session_requests)
241 .filter(user_session_requests::dsl::id.eq(id))
242 .returning(UserSessionRequest::as_select())
243 .get_result::<UserSessionRequest>(conn)
244 .await
245 .optional()
246 .into_tonic_internal_err("failed to delete user session request")?
247 else {
248 return Err(tonic::Status::with_error_details(
249 Code::NotFound,
250 "unknown id",
251 ErrorDetails::new(),
252 ));
253 };
254
255 Ok(session_request)
256 }
257
258 async fn execute(
259 self,
260 conn: &mut diesel_async::AsyncPgConnection,
261 _principal: Self::Principal,
262 resource: Self::Resource,
263 ) -> Result<Self::Response, tonic::Status> {
264 let global = &self.global::<G>()?;
265 let ip_info = self.ip_address_info()?;
266 let payload = self.into_inner();
267
268 let device = payload.device.require("device")?;
269
270 let Some(approved_by) = resource.approved_by else {
271 return Err(tonic::Status::with_error_details(
272 tonic::Code::FailedPrecondition,
273 "user session request is not approved yet",
274 ErrorDetails::new(),
275 ));
276 };
277
278 let new_token = common::create_session(global, conn, approved_by, device, &ip_info, false).await?;
279 Ok(new_token)
280 }
281}