scufflecloud_core/operations/
user_session_requests.rs

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