Organizations (#712)

* untested, unformatted, un-refactored

* minor simplification

* simplification fix

* refactoring, changes

* some fixes

* fixes, refactoring

* missed cache

* revs

* revs - more!

* removed donation links; added all org members to route

* renamed slug to title

---------

Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
This commit is contained in:
Wyatt Verchere
2023-10-02 10:56:57 -07:00
committed by GitHub
parent 58a61051b9
commit a1b59d4545
24 changed files with 3658 additions and 979 deletions

View File

@@ -1,10 +1,11 @@
use crate::auth::{get_user_from_headers, is_authorized};
use crate::database::models::notification_item::NotificationBuilder;
use crate::database::models::TeamMember;
use crate::models::ids::ProjectId;
use crate::database::models::team_item::TeamAssociationId;
use crate::database::models::{Organization, Team, TeamMember};
use crate::database::Project;
use crate::models::notifications::NotificationBody;
use crate::models::pats::Scopes;
use crate::models::teams::{Permissions, TeamId};
use crate::models::teams::{OrganizationPermissions, ProjectPermissions, TeamId};
use crate::models::users::UserId;
use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
@@ -27,6 +28,10 @@ pub fn config(cfg: &mut web::ServiceConfig) {
);
}
// Returns all members of a project,
// including the team members of the project's team, but
// also the members of the organization's team if the project is associated with an organization
// (Unlike team_members_get_project, which only returns the members of the project's team)
#[get("{id}/members")]
pub async fn team_members_get_project(
req: HttpRequest,
@@ -53,9 +58,85 @@ pub async fn team_members_get_project(
if !is_authorized(&project.inner, &current_user, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
}
let mut members_data =
TeamMember::get_from_team_full(project.inner.team_id, &**pool, &redis).await?;
let mut member_user_ids = members_data.iter().map(|x| x.user_id).collect::<Vec<_>>();
// Adds the organization's team members to the list of members, if the project is associated with an organization
if let Some(oid) = project.inner.organization_id {
let organization_data = Organization::get_id(oid, &**pool, &redis).await?;
if let Some(organization_data) = organization_data {
let org_team =
TeamMember::get_from_team_full(organization_data.team_id, &**pool, &redis)
.await?;
for member in org_team {
if !member_user_ids.contains(&member.user_id) {
member_user_ids.push(member.user_id);
members_data.push(member);
}
}
}
}
let users =
crate::database::models::User::get_many_ids(&member_user_ids, &**pool, &redis).await?;
let user_id = current_user.as_ref().map(|x| x.id.into());
let logged_in = current_user
.and_then(|user| {
members_data
.iter()
.find(|x| x.user_id == user.id.into() && x.accepted)
})
.is_some();
let team_members: Vec<_> = members_data
.into_iter()
.filter(|x| {
logged_in
|| x.accepted
|| user_id
.map(|y: crate::database::models::UserId| y == x.user_id)
.unwrap_or(false)
})
.flat_map(|data| {
users.iter().find(|x| x.id == data.user_id).map(|user| {
crate::models::teams::TeamMember::from(data, user.clone(), !logged_in)
})
})
.collect();
Ok(HttpResponse::Ok().json(team_members))
} else {
Ok(HttpResponse::NotFound().body(""))
}
}
#[get("{id}/members")]
pub async fn team_members_get_organization(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let string = info.into_inner().0;
let organization_data =
crate::database::models::Organization::get(&string, &**pool, &redis).await?;
if let Some(organization) = organization_data {
let current_user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::ORGANIZATION_READ]),
)
.await
.map(|x| x.1)
.ok();
let members_data =
TeamMember::get_from_team_full(project.inner.team_id, &**pool, &redis).await?;
TeamMember::get_from_team_full(organization.team_id, &**pool, &redis).await?;
let users = crate::database::models::User::get_many_ids(
&members_data.iter().map(|x| x.user_id).collect::<Vec<_>>(),
&**pool,
@@ -95,6 +176,7 @@ pub async fn team_members_get_project(
}
}
// Returns all members of a team, but not necessarily those of a project-team's organization (unlike team_members_get_project)
#[get("{id}/members")]
pub async fn team_members_get(
req: HttpRequest,
@@ -258,6 +340,7 @@ pub async fn join_team(
current_user.id.into(),
None,
None,
None,
Some(true),
None,
None,
@@ -290,8 +373,10 @@ pub struct NewTeamMember {
pub user_id: UserId,
#[serde(default = "default_role")]
pub role: String,
#[serde(default = "Permissions::default")]
pub permissions: Permissions,
#[serde(default)]
pub permissions: ProjectPermissions,
#[serde(default)]
pub organization_permissions: Option<OrganizationPermissions>,
#[serde(default)]
pub payouts_split: Decimal,
#[serde(default = "default_ordering")]
@@ -320,23 +405,76 @@ pub async fn add_team_member(
)
.await?
.1;
let member = TeamMember::get_from_user_id(team_id, current_user.id.into(), &**pool)
.await?
.ok_or_else(|| {
ApiError::CustomAuthentication(
"You don't have permission to edit members of this team".to_string(),
)
})?;
if !member.permissions.contains(Permissions::MANAGE_INVITES) {
return Err(ApiError::CustomAuthentication(
"You don't have permission to invite users to this team".to_string(),
));
}
if !member.permissions.contains(new_member.permissions) {
return Err(ApiError::InvalidInput(
"The new member has permissions that you don't have".to_string(),
));
let team_association = Team::get_association(team_id, &**pool)
.await?
.ok_or_else(|| ApiError::InvalidInput("The team specified does not exist".to_string()))?;
let member = TeamMember::get_from_user_id(team_id, current_user.id.into(), &**pool).await?;
match team_association {
// If team is associated with a project, check if they have permissions to invite users to that project
TeamAssociationId::Project(pid) => {
let organization =
Organization::get_associated_organization_project_id(pid, &**pool).await?;
let organization_team_member = if let Some(organization) = &organization {
TeamMember::get_from_user_id(organization.team_id, current_user.id.into(), &**pool)
.await?
} else {
None
};
let permissions = ProjectPermissions::get_permissions_by_role(
&current_user.role,
&member,
&organization_team_member,
)
.unwrap_or_default();
if !permissions.contains(ProjectPermissions::MANAGE_INVITES) {
return Err(ApiError::CustomAuthentication(
"You don't have permission to invite users to this team".to_string(),
));
}
if !permissions.contains(new_member.permissions) {
return Err(ApiError::InvalidInput(
"The new member has permissions that you don't have".to_string(),
));
}
if new_member.organization_permissions.is_some() {
return Err(ApiError::InvalidInput(
"The organization permissions of a project team member cannot be set"
.to_string(),
));
}
}
// If team is associated with an organization, check if they have permissions to invite users to that organization
TeamAssociationId::Organization(_) => {
let organization_permissions =
OrganizationPermissions::get_permissions_by_role(&current_user.role, &member)
.unwrap_or_default();
println!("{:?}", organization_permissions);
if !organization_permissions.contains(OrganizationPermissions::MANAGE_INVITES) {
return Err(ApiError::CustomAuthentication(
"You don't have permission to invite users to this organization".to_string(),
));
}
if !organization_permissions
.contains(new_member.organization_permissions.unwrap_or_default())
{
return Err(ApiError::InvalidInput(
"The new member has organization permissions that you don't have".to_string(),
));
}
if !organization_permissions
.contains(OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS)
&& !new_member.permissions.is_empty()
{
return Err(ApiError::InvalidInput(
"You do not have permission to give this user default project permissions."
.to_string(),
));
}
}
}
if new_member.role == crate::models::teams::OWNER_ROLE {
@@ -365,8 +503,7 @@ pub async fn add_team_member(
));
}
}
crate::database::models::User::get_id(member.user_id, &**pool, &redis)
crate::database::models::User::get_id(new_member.user_id.into(), &**pool, &redis)
.await?
.ok_or_else(|| ApiError::InvalidInput("An invalid User ID specified".to_string()))?;
@@ -377,6 +514,7 @@ pub async fn add_team_member(
user_id: new_member.user_id.into(),
role: new_member.role.clone(),
permissions: new_member.permissions,
organization_permissions: new_member.organization_permissions,
accepted: false,
payouts_split: new_member.payouts_split,
ordering: new_member.ordering,
@@ -384,27 +522,32 @@ pub async fn add_team_member(
.insert(&mut transaction)
.await?;
let result = sqlx::query!(
"
SELECT m.id
FROM mods m
WHERE m.team_id = $1
",
team_id as crate::database::models::ids::TeamId
)
.fetch_one(&**pool)
.await?;
NotificationBuilder {
body: NotificationBody::TeamInvite {
project_id: ProjectId(result.id as u64),
team_id: team_id.into(),
invited_by: current_user.id,
role: new_member.role.clone(),
},
match team_association {
TeamAssociationId::Project(pid) => {
NotificationBuilder {
body: NotificationBody::TeamInvite {
project_id: pid.into(),
team_id: team_id.into(),
invited_by: current_user.id,
role: new_member.role.clone(),
},
}
.insert(new_member.user_id.into(), &mut transaction)
.await?;
}
TeamAssociationId::Organization(oid) => {
NotificationBuilder {
body: NotificationBody::OrganizationInvite {
organization_id: oid.into(),
team_id: team_id.into(),
invited_by: current_user.id,
role: new_member.role.clone(),
},
}
.insert(new_member.user_id.into(), &mut transaction)
.await?;
}
}
.insert(new_member.user_id.into(), &mut transaction)
.await?;
TeamMember::clear_cache(team_id, &redis).await?;
@@ -415,7 +558,8 @@ pub async fn add_team_member(
#[derive(Serialize, Deserialize, Clone)]
pub struct EditTeamMember {
pub permissions: Option<Permissions>,
pub permissions: Option<ProjectPermissions>,
pub organization_permissions: Option<OrganizationPermissions>,
pub role: Option<String>,
pub payouts_split: Option<Decimal>,
pub ordering: Option<i64>,
@@ -443,13 +587,11 @@ pub async fn edit_team_member(
)
.await?
.1;
let member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool)
let team_association = Team::get_association(id, &**pool)
.await?
.ok_or_else(|| {
ApiError::CustomAuthentication(
"You don't have permission to edit members of this team".to_string(),
)
})?;
.ok_or_else(|| ApiError::InvalidInput("The team specified does not exist".to_string()))?;
let member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool).await?;
let edit_member_db = TeamMember::get_from_user_id_pending(id, user_id, &**pool)
.await?
.ok_or_else(|| {
@@ -468,17 +610,72 @@ pub async fn edit_team_member(
));
}
if !member.permissions.contains(Permissions::EDIT_MEMBER) {
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit members of this team".to_string(),
));
}
match team_association {
TeamAssociationId::Project(project_id) => {
let organization =
Organization::get_associated_organization_project_id(project_id, &**pool).await?;
let organization_team_member = if let Some(organization) = &organization {
TeamMember::get_from_user_id(organization.team_id, current_user.id.into(), &**pool)
.await?
} else {
None
};
let permissions = ProjectPermissions::get_permissions_by_role(
&current_user.role,
&member.clone(),
&organization_team_member,
)
.unwrap_or_default();
if !permissions.contains(ProjectPermissions::EDIT_MEMBER) {
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit members of this team".to_string(),
));
}
if let Some(new_permissions) = edit_member.permissions {
if !member.permissions.contains(new_permissions) {
return Err(ApiError::InvalidInput(
"The new permissions have permissions that you don't have".to_string(),
));
if let Some(new_permissions) = edit_member.permissions {
if !permissions.contains(new_permissions) {
return Err(ApiError::InvalidInput(
"The new permissions have permissions that you don't have".to_string(),
));
}
}
if edit_member.organization_permissions.is_some() {
return Err(ApiError::InvalidInput(
"The organization permissions of a project team member cannot be edited"
.to_string(),
));
}
}
TeamAssociationId::Organization(_) => {
let organization_permissions =
OrganizationPermissions::get_permissions_by_role(&current_user.role, &member)
.unwrap_or_default();
if !organization_permissions.contains(OrganizationPermissions::EDIT_MEMBER) {
return Err(ApiError::InvalidInput(
"You don't have permission to edit organization permissions".to_string(),
));
}
if let Some(new_permissions) = edit_member.organization_permissions {
if !organization_permissions.contains(new_permissions) {
return Err(ApiError::InvalidInput(
"The new organization permissions have permissions that you don't have"
.to_string(),
));
}
}
if edit_member.permissions.is_some()
&& !organization_permissions
.contains(OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS)
{
return Err(ApiError::InvalidInput(
"You do not have permission to give this user default project permissions."
.to_string(),
));
}
}
}
@@ -500,6 +697,7 @@ pub async fn edit_team_member(
id,
user_id,
edit_member.permissions,
edit_member.organization_permissions,
edit_member.role.clone(),
None,
edit_member.payouts_split,
@@ -541,6 +739,21 @@ pub async fn transfer_ownership(
.await?
.1;
// Forbid transferring ownership of a project team that is owned by an organization
// These are owned by the organization owner, and must be removed from the organization first
let pid = Team::get_association(id.into(), &**pool).await?;
if let Some(TeamAssociationId::Project(pid)) = pid {
let result = Project::get_id(pid, &**pool, &redis).await?;
if let Some(project_item) = result {
if project_item.inner.organization_id.is_some() {
return Err(ApiError::InvalidInput(
"You cannot transfer ownership of a project team that is owend by an organization"
.to_string(),
));
}
}
}
if !current_user.role.is_admin() {
let member = TeamMember::get_from_user_id(id.into(), current_user.id.into(), &**pool)
.await?
@@ -575,6 +788,7 @@ pub async fn transfer_ownership(
id.into(),
current_user.id.into(),
None,
None,
Some(crate::models::teams::DEFAULT_ROLE.to_string()),
None,
None,
@@ -586,7 +800,8 @@ pub async fn transfer_ownership(
TeamMember::edit_team_member(
id.into(),
new_owner.user_id.into(),
Some(Permissions::ALL),
Some(ProjectPermissions::all()),
Some(OrganizationPermissions::all()),
Some(crate::models::teams::OWNER_ROLE.to_string()),
None,
None,
@@ -623,13 +838,11 @@ pub async fn remove_team_member(
)
.await?
.1;
let member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool)
let team_association = Team::get_association(id, &**pool)
.await?
.ok_or_else(|| {
ApiError::CustomAuthentication(
"You don't have permission to edit members of this team".to_string(),
)
})?;
.ok_or_else(|| ApiError::InvalidInput("The team specified does not exist".to_string()))?;
let member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool).await?;
let delete_member = TeamMember::get_from_user_id_pending(id, user_id, &**pool).await?;
@@ -643,29 +856,101 @@ pub async fn remove_team_member(
let mut transaction = pool.begin().await?;
if delete_member.accepted {
// Members other than the owner can either leave the team, or be
// removed by a member with the REMOVE_MEMBER permission.
if delete_member.user_id == member.user_id
|| (member.permissions.contains(Permissions::REMOVE_MEMBER) && member.accepted)
{
TeamMember::delete(id, user_id, &mut transaction).await?;
} else {
return Err(ApiError::CustomAuthentication(
"You do not have permission to remove a member from this team".to_string(),
));
// Organization attached to a project this team is attached to
match team_association {
TeamAssociationId::Project(pid) => {
let organization =
Organization::get_associated_organization_project_id(pid, &**pool).await?;
let organization_team_member = if let Some(organization) = &organization {
TeamMember::get_from_user_id(
organization.team_id,
current_user.id.into(),
&**pool,
)
.await?
} else {
None
};
let permissions = ProjectPermissions::get_permissions_by_role(
&current_user.role,
&member,
&organization_team_member,
)
.unwrap_or_default();
if delete_member.accepted {
// Members other than the owner can either leave the team, or be
// removed by a member with the REMOVE_MEMBER permission.
if Some(delete_member.user_id) == member.as_ref().map(|m| m.user_id)
|| permissions.contains(ProjectPermissions::REMOVE_MEMBER)
&& member.as_ref().map(|m| m.accepted).unwrap_or(true)
// true as if the permission exists, but the member does not, they are part of an org
{
TeamMember::delete(id, user_id, &mut transaction).await?;
} else {
return Err(ApiError::CustomAuthentication(
"You do not have permission to remove a member from this team"
.to_string(),
));
}
} else if Some(delete_member.user_id) == member.as_ref().map(|m| m.user_id)
|| permissions.contains(ProjectPermissions::MANAGE_INVITES)
&& member.as_ref().map(|m| m.accepted).unwrap_or(true)
// true as if the permission exists, but the member does not, they are part of an org
{
// This is a pending invite rather than a member, so the
// user being invited or team members with the MANAGE_INVITES
// permission can remove it.
TeamMember::delete(id, user_id, &mut transaction).await?;
} else {
return Err(ApiError::CustomAuthentication(
"You do not have permission to cancel a team invite".to_string(),
));
}
}
TeamAssociationId::Organization(_) => {
let organization_permissions =
OrganizationPermissions::get_permissions_by_role(&current_user.role, &member)
.unwrap_or_default();
if let Some(member) = member {
// Organization teams requires a TeamMember, so we can 'unwrap'
if delete_member.accepted {
// Members other than the owner can either leave the team, or be
// removed by a member with the REMOVE_MEMBER permission.
if delete_member.user_id == member.user_id
|| organization_permissions
.contains(OrganizationPermissions::REMOVE_MEMBER)
&& member.accepted
{
TeamMember::delete(id, user_id, &mut transaction).await?;
} else {
return Err(ApiError::CustomAuthentication(
"You do not have permission to remove a member from this organization"
.to_string(),
));
}
} else if delete_member.user_id == member.user_id
|| organization_permissions
.contains(OrganizationPermissions::MANAGE_INVITES)
&& member.accepted
{
// This is a pending invite rather than a member, so the
// user being invited or team members with the MANAGE_INVITES
// permission can remove it.
TeamMember::delete(id, user_id, &mut transaction).await?;
} else {
return Err(ApiError::CustomAuthentication(
"You do not have permission to cancel an organization invite"
.to_string(),
));
}
} else {
return Err(ApiError::CustomAuthentication(
"You do not have permission to remove a member from this organization"
.to_string(),
));
}
}
} else if delete_member.user_id == member.user_id
|| (member.permissions.contains(Permissions::MANAGE_INVITES) && member.accepted)
{
// This is a pending invite rather than a member, so the
// user being invited or team members with the MANAGE_INVITES
// permission can remove it.
TeamMember::delete(id, user_id, &mut transaction).await?;
} else {
return Err(ApiError::CustomAuthentication(
"You do not have permission to cancel a team invite".to_string(),
));
}
TeamMember::clear_cache(id, &redis).await?;