scufflecloud_core/operations/
user_session_requests.rs

1use 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        // Delete the session request
240        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}