scufflecloud_core/operations/
user_session_requests.rs

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