scufflecloud_core/operations/
organizations.rs

1use core_db_types::models::{Organization, OrganizationId, OrganizationMember, Project, ProjectId, User, UserId};
2use core_db_types::schema::{organization_members, organizations, projects};
3use diesel::{ExpressionMethods, QueryDsl, SelectableHelper};
4use diesel_async::RunQueryDsl;
5use ext_traits::{RequestExt, ResultExt};
6
7use crate::cedar::Action;
8use crate::common;
9use crate::http_ext::CoreRequestExt;
10use crate::operations::{Operation, OperationDriver};
11
12impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::CreateOrganizationRequest> {
13    type Principal = User;
14    type Resource = Organization;
15    type Response = pb::scufflecloud::core::v1::Organization;
16
17    const ACTION: Action = Action::CreateOrganization;
18
19    async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
20        let global = &self.global::<G>()?;
21        let session = self.session_or_err()?;
22
23        common::get_user_by_id(global, session.user_id).await
24    }
25
26    async fn load_resource(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
27        let session = self.session_or_err()?;
28
29        Ok(Organization {
30            id: OrganizationId::new(),
31            google_customer_id: None,
32            google_hosted_domain: None,
33            name: self.get_ref().name.clone(),
34            owner_id: session.user_id,
35        })
36    }
37
38    async fn execute(
39        self,
40        _driver: &mut OperationDriver<'_, G>,
41        _principal: Self::Principal,
42        resource: Self::Resource,
43    ) -> Result<Self::Response, tonic::Status> {
44        let global = &self.global::<G>()?;
45        let mut db = global.db().await.into_tonic_internal_err("failed to connect to database")?;
46
47        diesel::insert_into(organizations::dsl::organizations)
48            .values(&resource)
49            .execute(&mut db)
50            .await
51            .into_tonic_internal_err("failed to create organization")?;
52
53        Ok(resource.into())
54    }
55}
56
57impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::GetOrganizationRequest> {
58    type Principal = User;
59    type Resource = Organization;
60    type Response = pb::scufflecloud::core::v1::Organization;
61
62    const ACTION: Action = Action::GetOrganization;
63
64    async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
65        let global = &self.global::<G>()?;
66        let session = self.session_or_err()?;
67        common::get_user_by_id(global, session.user_id).await
68    }
69
70    async fn load_resource(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
71        let global = &self.global::<G>()?;
72        let id: OrganizationId = self
73            .get_ref()
74            .id
75            .parse()
76            .into_tonic_err_with_field_violation("id", "invalid ID")?;
77        common::get_organization_by_id(global, id).await
78    }
79
80    async fn execute(
81        self,
82        _driver: &mut OperationDriver<'_, G>,
83        _principal: Self::Principal,
84        resource: Self::Resource,
85    ) -> Result<Self::Response, tonic::Status> {
86        Ok(resource.into())
87    }
88}
89
90impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::UpdateOrganizationRequest> {
91    type Principal = User;
92    type Resource = Organization;
93    type Response = pb::scufflecloud::core::v1::Organization;
94
95    const ACTION: Action = Action::UpdateOrganization;
96
97    async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
98        let global = &self.global::<G>()?;
99        let session = self.session_or_err()?;
100        common::get_user_by_id(global, session.user_id).await
101    }
102
103    async fn load_resource(&mut self, driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
104        let id: OrganizationId = self
105            .get_ref()
106            .id
107            .parse()
108            .into_tonic_err_with_field_violation("id", "invalid ID")?;
109
110        let conn = driver.conn().await?;
111
112        common::get_organization_by_id_in_tx(conn, id).await
113    }
114
115    async fn execute(
116        self,
117        driver: &mut OperationDriver<'_, G>,
118        _principal: Self::Principal,
119        mut resource: Self::Resource,
120    ) -> Result<Self::Response, tonic::Status> {
121        let payload = self.into_inner();
122
123        let owner_update_id = payload
124            .owner
125            .map(|owner| {
126                owner
127                    .owner_id
128                    .parse::<UserId>()
129                    .into_tonic_err_with_field_violation("owner_id", "invalid ID")
130            })
131            .transpose()?;
132
133        let conn = driver.conn().await?;
134
135        if let Some(owner_update_id) = owner_update_id {
136            resource = diesel::update(organizations::dsl::organizations)
137                .filter(organizations::dsl::id.eq(resource.id))
138                .set(organizations::dsl::owner_id.eq(&owner_update_id))
139                .returning(Organization::as_returning())
140                .get_result::<Organization>(conn)
141                .await
142                .into_tonic_internal_err("failed to update organization owner")?;
143        }
144
145        if let Some(name) = &payload.name {
146            resource = diesel::update(organizations::dsl::organizations)
147                .filter(organizations::dsl::id.eq(resource.id))
148                .set(organizations::dsl::name.eq(&name.name))
149                .returning(Organization::as_returning())
150                .get_result::<Organization>(conn)
151                .await
152                .into_tonic_internal_err("failed to update organization name")?;
153        }
154
155        Ok(resource.into())
156    }
157}
158
159impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::ListOrganizationMembersRequest> {
160    type Principal = User;
161    type Resource = Organization;
162    type Response = pb::scufflecloud::core::v1::OrganizationMembersList;
163
164    const ACTION: Action = Action::ListOrganizationMembers;
165
166    async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
167        let global = &self.global::<G>()?;
168        let session = self.session_or_err()?;
169        common::get_user_by_id(global, session.user_id).await
170    }
171
172    async fn load_resource(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
173        let global = &self.global::<G>()?;
174        let id: OrganizationId = self
175            .get_ref()
176            .id
177            .parse()
178            .into_tonic_err_with_field_violation("id", "invalid ID")?;
179        common::get_organization_by_id(global, id).await
180    }
181
182    async fn execute(
183        self,
184        _driver: &mut OperationDriver<'_, G>,
185        _principal: Self::Principal,
186        resource: Self::Resource,
187    ) -> Result<Self::Response, tonic::Status> {
188        let global = &self.global::<G>()?;
189        let mut db = global.db().await.into_tonic_internal_err("failed to connect to database")?;
190
191        let members = organization_members::dsl::organization_members
192            .filter(organization_members::dsl::organization_id.eq(resource.id))
193            .load::<OrganizationMember>(&mut db)
194            .await
195            .into_tonic_internal_err("failed to load organization members")?;
196
197        Ok(pb::scufflecloud::core::v1::OrganizationMembersList {
198            members: members.into_iter().map(Into::into).collect(),
199        })
200    }
201}
202
203impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::ListOrganizationsByUserRequest> {
204    type Principal = User;
205    type Resource = User;
206    type Response = pb::scufflecloud::core::v1::OrganizationsList;
207
208    const ACTION: Action = Action::ListOrganizationsByUser;
209
210    async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
211        let global = &self.global::<G>()?;
212        let session = self.session_or_err()?;
213        common::get_user_by_id(global, session.user_id).await
214    }
215
216    async fn load_resource(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
217        let global = &self.global::<G>()?;
218        let id: UserId = self
219            .get_ref()
220            .id
221            .parse()
222            .into_tonic_err_with_field_violation("id", "invalid ID")?;
223        common::get_user_by_id(global, id).await
224    }
225
226    async fn execute(
227        self,
228        _driver: &mut OperationDriver<'_, G>,
229        _principal: Self::Principal,
230        resource: Self::Resource,
231    ) -> Result<Self::Response, tonic::Status> {
232        let global = &self.global::<G>()?;
233        let mut db = global.db().await.into_tonic_internal_err("failed to connect to database")?;
234
235        let organizations = organization_members::dsl::organization_members
236            .filter(organization_members::dsl::user_id.eq(resource.id))
237            .inner_join(organizations::dsl::organizations)
238            .select(Organization::as_select())
239            .load::<Organization>(&mut db)
240            .await
241            .into_tonic_internal_err("failed to load organizations")?;
242
243        Ok(pb::scufflecloud::core::v1::OrganizationsList {
244            organizations: organizations.into_iter().map(Into::into).collect(),
245        })
246    }
247}
248
249impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::CreateProjectRequest> {
250    type Principal = User;
251    type Resource = Project;
252    type Response = pb::scufflecloud::core::v1::Project;
253
254    const ACTION: Action = Action::CreateProject;
255
256    async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
257        let global = &self.global::<G>()?;
258        let session = self.session_or_err()?;
259        common::get_user_by_id(global, session.user_id).await
260    }
261
262    async fn load_resource(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
263        let organization_id: OrganizationId = self
264            .get_ref()
265            .id
266            .parse()
267            .into_tonic_err_with_field_violation("id", "invalid ID")?;
268
269        Ok(Project {
270            id: ProjectId::new(),
271            name: self.get_ref().name.clone(),
272            organization_id,
273        })
274    }
275
276    async fn execute(
277        self,
278        driver: &mut OperationDriver<'_, G>,
279        _principal: Self::Principal,
280        resource: Self::Resource,
281    ) -> Result<Self::Response, tonic::Status> {
282        let conn = driver.conn().await?;
283
284        diesel::insert_into(projects::dsl::projects)
285            .values(&resource)
286            .execute(conn)
287            .await
288            .into_tonic_internal_err("failed to create project")?;
289
290        Ok(resource.into())
291    }
292}
293
294impl<G: core_traits::Global> Operation<G> for tonic::Request<pb::scufflecloud::core::v1::ListProjectsRequest> {
295    type Principal = User;
296    type Resource = Organization;
297    type Response = pb::scufflecloud::core::v1::ProjectsList;
298
299    const ACTION: Action = Action::ListProjects;
300
301    async fn load_principal(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Principal, tonic::Status> {
302        let global = &self.global::<G>()?;
303        let session = self.session_or_err()?;
304        common::get_user_by_id(global, session.user_id).await
305    }
306
307    async fn load_resource(&mut self, _driver: &mut OperationDriver<'_, G>) -> Result<Self::Resource, tonic::Status> {
308        let global = &self.global::<G>()?;
309        let id: OrganizationId = self
310            .get_ref()
311            .id
312            .parse()
313            .into_tonic_err_with_field_violation("id", "invalid ID")?;
314        common::get_organization_by_id(global, id).await
315    }
316
317    async fn execute(
318        self,
319        driver: &mut OperationDriver<'_, G>,
320        _principal: Self::Principal,
321        resource: Self::Resource,
322    ) -> Result<Self::Response, tonic::Status> {
323        let conn = driver.conn().await?;
324
325        let projects = projects::dsl::projects
326            .filter(projects::dsl::organization_id.eq(resource.id))
327            .load::<Project>(conn)
328            .await
329            .into_tonic_internal_err("failed to load projects")?;
330
331        Ok(pb::scufflecloud::core::v1::ProjectsList {
332            projects: projects.into_iter().map(Into::into).collect(),
333        })
334    }
335}