scufflecloud_core/operations/
organizations.rs

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