Skip to content

Commit 37d5f6f

Browse files
authored
feat: org members (#1728)
* feat: org members to permit * feat: org member routes to gw * misc: touch ups * refactor: functional loops * misc: missed merge update
1 parent 639b5ae commit 37d5f6f

File tree

7 files changed

+432
-59
lines changed

7 files changed

+432
-59
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backends/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ opentelemetry-appender-tracing = { workspace = true }
2424
opentelemetry-http = { workspace = true }
2525
opentelemetry-otlp = { workspace = true }
2626
pin-project = { workspace = true }
27-
permit-client-rs = { git = "https://github.com/shuttle-hq/permit-client-rs", rev = "27c7759" }
27+
permit-client-rs = { git = "https://github.com/shuttle-hq/permit-client-rs", rev = "19085ba" }
2828
permit-pdp-client-rs = { git = "https://github.com/shuttle-hq/permit-pdp-client-rs", rev = "37c7296" }
2929
portpicker = { workspace = true, optional = true }
3030
reqwest = { workspace = true, features = ["json"] }

backends/src/client/permit.rs

Lines changed: 133 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use std::fmt::{Debug, Display};
1+
use std::{
2+
fmt::{Debug, Display},
3+
str::FromStr,
4+
};
25

36
use async_trait::async_trait;
47
use http::StatusCode;
@@ -8,7 +11,7 @@ use permit_client_rs::{
811
create_relationship_tuple, delete_relationship_tuple, list_relationship_tuples,
912
},
1013
resource_instances_api::{create_resource_instance, delete_resource_instance},
11-
role_assignments_api::{assign_role, unassign_role},
14+
role_assignments_api::{assign_role, list_role_assignments, unassign_role},
1215
users_api::{create_user, delete_user, get_user},
1316
Error as PermitClientError,
1417
},
@@ -91,6 +94,29 @@ pub trait PermissionsDal {
9194
org_id: &str,
9295
) -> Result<()>;
9396

97+
/// Add a user as a normal member to an organization
98+
async fn add_organization_member(
99+
&self,
100+
admin_user: &str,
101+
org_id: &str,
102+
user_id: &str,
103+
) -> Result<()>;
104+
105+
/// Remove a user from an organization
106+
async fn remove_organization_member(
107+
&self,
108+
admin_user: &str,
109+
org_id: &str,
110+
user_id: &str,
111+
) -> Result<()>;
112+
113+
/// Get a list of all the members of an organization
114+
async fn get_organization_members(
115+
&self,
116+
user_id: &str,
117+
org_id: &str,
118+
) -> Result<Vec<organization::MemberResponse>>;
119+
94120
// Permissions queries
95121

96122
/// Get list of all projects user has permissions for
@@ -398,11 +424,10 @@ impl PermissionsDal for Client {
398424
)
399425
.await?;
400426

401-
let mut projects = Vec::with_capacity(relationships.len());
402-
403-
for rel in relationships {
404-
projects.push(rel.object_details.expect("to have object details").key);
405-
}
427+
let projects = relationships
428+
.into_iter()
429+
.map(|rel| rel.object_details.expect("to have object details").key)
430+
.collect();
406431

407432
Ok(projects)
408433
}
@@ -514,60 +539,111 @@ impl PermissionsDal for Client {
514539

515540
Ok(())
516541
}
542+
543+
async fn add_organization_member(
544+
&self,
545+
admin_user: &str,
546+
org_id: &str,
547+
user_id: &str,
548+
) -> Result<()> {
549+
if !self.allowed_org(admin_user, org_id, "manage").await? {
550+
return Err(Error::ResponseError(ResponseContent {
551+
status: StatusCode::FORBIDDEN,
552+
content: "User does not have permission to modify the organization".to_owned(),
553+
entity: "Organization".to_owned(),
554+
}));
555+
}
556+
557+
let user = self.get_user(user_id).await?;
558+
559+
if !user
560+
.roles
561+
.is_some_and(|roles| roles.iter().any(|r| r.role == AccountTier::Pro.to_string()))
562+
{
563+
return Err(Error::ResponseError(ResponseContent {
564+
status: StatusCode::BAD_REQUEST,
565+
content: "Only Pro users can be added to an organization".to_owned(),
566+
entity: "Organization".to_owned(),
567+
}));
568+
}
569+
570+
self.assign_resource_role(user_id, format!("Organization:{org_id}"), "member")
571+
.await?;
572+
573+
Ok(())
574+
}
575+
576+
async fn remove_organization_member(
577+
&self,
578+
admin_user: &str,
579+
org_id: &str,
580+
user_id: &str,
581+
) -> Result<()> {
582+
if admin_user == user_id {
583+
return Err(Error::ResponseError(ResponseContent {
584+
status: StatusCode::BAD_REQUEST,
585+
content: "Cannot remove yourself from an organization".to_owned(),
586+
entity: "Organization".to_owned(),
587+
}));
588+
}
589+
590+
if !self.allowed_org(admin_user, org_id, "manage").await? {
591+
return Err(Error::ResponseError(ResponseContent {
592+
status: StatusCode::FORBIDDEN,
593+
content: "User does not have permission to modify the organization".to_owned(),
594+
entity: "Organization".to_owned(),
595+
}));
596+
}
597+
598+
self.unassign_resource_role(user_id, format!("Organization:{org_id}"), "member")
599+
.await?;
600+
601+
Ok(())
602+
}
603+
604+
async fn get_organization_members(
605+
&self,
606+
user_id: &str,
607+
org_id: &str,
608+
) -> Result<Vec<organization::MemberResponse>> {
609+
if !self.allowed_org(user_id, org_id, "view").await? {
610+
return Err(Error::ResponseError(ResponseContent {
611+
status: StatusCode::FORBIDDEN,
612+
content: "User does not have permission to view the organization".to_owned(),
613+
entity: "Organization".to_owned(),
614+
}));
615+
}
616+
617+
let assignments = list_role_assignments(
618+
&self.api,
619+
&self.proj_id,
620+
&self.env_id,
621+
None,
622+
None,
623+
Some("default"),
624+
None,
625+
Some(&format!("Organization:{org_id}")),
626+
None,
627+
None,
628+
None,
629+
)
630+
.await?;
631+
632+
let members = assignments
633+
.into_iter()
634+
.map(|assignment| organization::MemberResponse {
635+
id: assignment.user,
636+
role: organization::MemberRole::from_str(&assignment.role)
637+
.unwrap_or(organization::MemberRole::Member),
638+
})
639+
.collect();
640+
641+
Ok(members)
642+
}
517643
}
518644

519645
// Helpers for trait methods
520646
impl Client {
521-
// pub async fn get_organization_members(&self, org_name: &str) -> Result<Vec<Value>> {
522-
// self.api
523-
// .get(
524-
// &format!(
525-
// "{}/role_assignments?resource_instance=Organization:{org_name}&role=member",
526-
// self.facts
527-
// ),
528-
// None,
529-
// )
530-
// .await
531-
// }
532-
533-
// pub async fn create_organization_member(
534-
// &self,
535-
// org_name: &str,
536-
// user_id: &str,
537-
// ) -> Result<()> {
538-
// self.api
539-
// .post(
540-
// &format!("{}/role_assignments", self.facts),
541-
// json!({
542-
// "role": "member",
543-
// "resource_instance": format!("Organization:{org_name}"),
544-
// "tenant": "default",
545-
// "user": user_id,
546-
// }),
547-
// None,
548-
// )
549-
// .await
550-
// }
551-
552-
// pub async fn delete_organization_member(
553-
// &self,
554-
// org_name: &str,
555-
// user_id: &str,
556-
// ) -> Result<()> {
557-
// self.api
558-
// .delete(
559-
// &format!("{}/role_assignments", self.facts),
560-
// json!({
561-
// "role": "member",
562-
// "resource_instance": format!("Organization:{org_name}"),
563-
// "tenant": "default",
564-
// "user": user_id,
565-
// }),
566-
// None,
567-
// )
568-
// .await
569-
// }
570-
571647
async fn create_user(&self, user_id: &str) -> Result<UserRead> {
572648
Ok(create_user(
573649
&self.api,

backends/src/test_utils/gateway.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,4 +232,40 @@ impl PermissionsDal for PermissionsMock {
232232
));
233233
Ok(())
234234
}
235+
236+
async fn add_organization_member(
237+
&self,
238+
admin_user: &str,
239+
org_id: &str,
240+
user_id: &str,
241+
) -> Result<()> {
242+
self.calls.lock().await.push(format!(
243+
"add_organization_member {admin_user} {org_id} {user_id}"
244+
));
245+
Ok(())
246+
}
247+
248+
async fn remove_organization_member(
249+
&self,
250+
admin_user: &str,
251+
org_id: &str,
252+
user_id: &str,
253+
) -> Result<()> {
254+
self.calls.lock().await.push(format!(
255+
"remove_organization_member {admin_user} {org_id} {user_id}"
256+
));
257+
Ok(())
258+
}
259+
260+
async fn get_organization_members(
261+
&self,
262+
user_id: &str,
263+
org_id: &str,
264+
) -> Result<Vec<organization::MemberResponse>> {
265+
self.calls
266+
.lock()
267+
.await
268+
.push(format!("get_organization_members {user_id} {org_id}"));
269+
Ok(Default::default())
270+
}
235271
}

0 commit comments

Comments
 (0)