scufflecloud_core/operations/
organizations.rs1use 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}