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