mirror of
https://github.com/PostHog/posthog.git
synced 2024-11-28 18:26:15 +01:00
eb6db7c075
* Refactor `AvailableFeature` from strings to an enum everywhere * Fix circular dependency and type * Add "Per-project access" feature flag, premium feature, and organization switch * Rename `OrganizationMembershipLevel` to `OrganizationAccessLevel` * Create `ExplicitTeamMembership` model * Show whether projects are restricted in the project switcher * Update organizations API code * Fix migrations * Move organization tests that require EE to `ee` * Revert `OrganizationMembershipLevel` rename * Fix organization tests * Update migration * Fix schema and add Members to Project Settings * Build out test memberships API with security tests * Update `TeamMembers` and `teamMembersLogic` * Move "Per-project access" description to tooltip * Add moar tests * Fix Project Members list logic * Add additional membership checks * Update migrations * Fix typing * Adjust explicit team memberships API similarly * Fix typo * Unify `ExplicitTeamMemberSerializer` * Remove old changes to `membersLogic` usage * Use `effective_membership_level` on `TeamBasicSerializer` * Clean up organization update tests * Explicitly disallow enabling per-project access for free * Fix circular import * Remove `id` from `UserSerializer` * Fix typing * Try to fix import * Fix fatal typing * Add more tests * Update permissioning.ts * Add clarifying comment to migration * Fix import * Revert `TopNavigation` changes * Add project member addition button+modal * minor clarifications * Revert `TopNavigation` changes * Make new access control entirely project-based * Update migrations * Add `project_based_permissioning` to `TeamBasicSerializer` * Update test_team.py * Fix Access Control restriction tooltip * minor improvements * fix frontend typing * Fix frontend typing a bit more * adjust copy & UI a bit * Address feedback on field comment * "Privacy settings" to "Access Control" * Make `FusedTeamMemberType` comment clearer * Remove useless `export` * Delete 0169_project_based_permissioning.py * Clean some code up a bit * Project-based permissioning member removal (#6067) * Fix `teamMembersLogic` loaders * Allow explicit project members to leave * Add member removal/leaving button to Members with Project Access * Restore error message * Fix error message * Correct things Co-authored-by: Paolo D'Amico <paolodamico@users.noreply.github.com>
457 lines
21 KiB
Python
457 lines
21 KiB
Python
from rest_framework import status
|
|
|
|
from ee.api.test.base import APILicensedTest
|
|
from ee.models.explicit_team_membership import ExplicitTeamMembership
|
|
from posthog.models import OrganizationMembership, Team, User
|
|
|
|
|
|
class TestTeamMembershipsAPI(APILicensedTest):
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.team.access_control = True
|
|
self.team.save()
|
|
|
|
def test_add_member_as_org_owner_allowed(self):
|
|
self.organization_membership.level = OrganizationMembership.Level.OWNER
|
|
self.organization_membership.save()
|
|
|
|
new_user: User = User.objects.create_and_join(self.organization, "rookie@posthog.com", None)
|
|
|
|
self.assertEqual(self.team.explicit_memberships.count(), 0)
|
|
|
|
response = self.client.post("/api/projects/@current/explicit_members/", {"user_uuid": new_user.uuid})
|
|
response_data = response.json()
|
|
|
|
self.assertDictContainsSubset(
|
|
{"effective_level": ExplicitTeamMembership.Level.MEMBER, "level": ExplicitTeamMembership.Level.MEMBER,},
|
|
response_data,
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
|
|
self.assertEqual(self.team.explicit_memberships.count(), 1)
|
|
|
|
def test_add_member_as_org_admin_allowed(self):
|
|
self.organization_membership.level = OrganizationMembership.Level.ADMIN
|
|
self.organization_membership.save()
|
|
|
|
new_user: User = User.objects.create_and_join(self.organization, "rookie@posthog.com", None)
|
|
|
|
self.assertEqual(self.team.explicit_memberships.count(), 0)
|
|
|
|
response = self.client.post("/api/projects/@current/explicit_members/", {"user_uuid": new_user.uuid})
|
|
response_data = response.json()
|
|
|
|
self.assertDictContainsSubset(
|
|
{"effective_level": ExplicitTeamMembership.Level.MEMBER, "level": ExplicitTeamMembership.Level.MEMBER,},
|
|
response_data,
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
|
|
self.assertEqual(self.team.explicit_memberships.count(), 1)
|
|
|
|
def test_add_member_as_org_member_forbidden(self):
|
|
self.organization_membership.level = OrganizationMembership.Level.MEMBER
|
|
self.organization_membership.save()
|
|
|
|
new_user: User = User.objects.create_and_join(self.organization, "rookie@posthog.com", None)
|
|
|
|
self.assertEqual(self.team.explicit_memberships.count(), 0)
|
|
|
|
response = self.client.post("/api/projects/@current/explicit_members/", {"user_uuid": new_user.uuid})
|
|
response_data = response.json()
|
|
|
|
self.assertDictEqual(
|
|
self.permission_denied_response("You don't have sufficient permissions in this project."), response_data
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
|
|
|
self.assertEqual(self.team.explicit_memberships.count(), 0)
|
|
|
|
def test_add_yourself_as_org_member_forbidden(self):
|
|
self.organization_membership.level = OrganizationMembership.Level.MEMBER
|
|
self.organization_membership.save()
|
|
|
|
self.assertEqual(self.team.explicit_memberships.count(), 0)
|
|
|
|
response = self.client.post("/api/projects/@current/explicit_members/", {"user_uuid": self.user.uuid})
|
|
response_data = response.json()
|
|
|
|
self.assertDictEqual(
|
|
self.permission_denied_response("You don't have sufficient permissions in this project."), response_data
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
|
|
|
self.assertEqual(self.team.explicit_memberships.count(), 0)
|
|
|
|
def test_add_yourself_as_org_admin_forbidden(self):
|
|
self.organization_membership.level = OrganizationMembership.Level.ADMIN
|
|
self.organization_membership.save()
|
|
|
|
self.assertEqual(self.team.explicit_memberships.count(), 0)
|
|
|
|
response = self.client.post("/api/projects/@current/explicit_members/", {"user_uuid": self.user.uuid})
|
|
response_data = response.json()
|
|
|
|
self.assertDictEqual(
|
|
self.permission_denied_response("You can't explicitly add yourself to projects."), response_data
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
|
|
|
self.assertEqual(self.team.explicit_memberships.count(), 0)
|
|
|
|
def test_add_member_as_org_member_and_project_member_forbidden(self):
|
|
self.organization_membership.level = OrganizationMembership.Level.MEMBER
|
|
self.organization_membership.save()
|
|
ExplicitTeamMembership.objects.create(
|
|
team=self.team, parent_membership=self.organization_membership, level=ExplicitTeamMembership.Level.MEMBER
|
|
)
|
|
|
|
new_user: User = User.objects.create_and_join(self.organization, "rookie@posthog.com", None)
|
|
|
|
self.assertEqual(self.team.explicit_memberships.count(), 1)
|
|
|
|
response = self.client.post("/api/projects/@current/explicit_members/", {"user_uuid": new_user.uuid})
|
|
response_data = response.json()
|
|
|
|
self.assertDictEqual(
|
|
self.permission_denied_response("You don't have sufficient permissions in this project."), response_data
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
|
|
|
self.assertEqual(self.team.explicit_memberships.count(), 1)
|
|
|
|
def test_add_member_as_org_member_but_project_admin_allowed(self):
|
|
self.organization_membership.level = OrganizationMembership.Level.MEMBER
|
|
self.organization_membership.save()
|
|
ExplicitTeamMembership.objects.create(
|
|
team=self.team, parent_membership=self.organization_membership, level=ExplicitTeamMembership.Level.ADMIN
|
|
)
|
|
|
|
self.assertEqual(self.team.explicit_memberships.count(), 1)
|
|
|
|
new_user: User = User.objects.create_and_join(self.organization, "rookie@posthog.com", None)
|
|
|
|
response = self.client.post("/api/projects/@current/explicit_members/", {"user_uuid": new_user.uuid})
|
|
response_data = response.json()
|
|
|
|
self.assertDictContainsSubset(
|
|
{"effective_level": ExplicitTeamMembership.Level.MEMBER, "level": ExplicitTeamMembership.Level.MEMBER,},
|
|
response_data,
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
|
|
self.assertEqual(self.team.explicit_memberships.count(), 2)
|
|
|
|
def test_add_member_as_org_admin_and_project_member_allowed(self):
|
|
self.organization_membership.level = OrganizationMembership.Level.ADMIN
|
|
self.organization_membership.save()
|
|
ExplicitTeamMembership.objects.create(
|
|
team=self.team, parent_membership=self.organization_membership, level=ExplicitTeamMembership.Level.MEMBER
|
|
)
|
|
|
|
new_user: User = User.objects.create_and_join(self.organization, "rookie@posthog.com", None)
|
|
|
|
response = self.client.post("/api/projects/@current/explicit_members/", {"user_uuid": new_user.uuid})
|
|
response_data = response.json()
|
|
|
|
self.assertDictContainsSubset(
|
|
{"effective_level": ExplicitTeamMembership.Level.MEMBER, "level": ExplicitTeamMembership.Level.MEMBER,},
|
|
response_data,
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
|
|
def test_add_admin_as_org_admin_allowed(self):
|
|
self.organization_membership.level = OrganizationMembership.Level.ADMIN
|
|
self.organization_membership.save()
|
|
|
|
new_user: User = User.objects.create_and_join(self.organization, "rookie@posthog.com", None)
|
|
|
|
response = self.client.post(
|
|
"/api/projects/@current/explicit_members/",
|
|
{"user_uuid": new_user.uuid, "level": ExplicitTeamMembership.Level.ADMIN},
|
|
)
|
|
response_data = response.json()
|
|
|
|
self.assertDictContainsSubset(
|
|
{"effective_level": ExplicitTeamMembership.Level.ADMIN, "level": ExplicitTeamMembership.Level.ADMIN,},
|
|
response_data,
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
|
|
def test_add_admin_as_project_member_forbidden(self):
|
|
self.organization_membership.level = OrganizationMembership.Level.MEMBER
|
|
self.organization_membership.save()
|
|
ExplicitTeamMembership.objects.create(
|
|
team=self.team, parent_membership=self.organization_membership, level=ExplicitTeamMembership.Level.MEMBER
|
|
)
|
|
|
|
new_user: User = User.objects.create_and_join(self.organization, "rookie@posthog.com", None)
|
|
|
|
response = self.client.post(
|
|
"/api/projects/@current/explicit_members/",
|
|
{"user_uuid": new_user.uuid, "level": ExplicitTeamMembership.Level.ADMIN},
|
|
)
|
|
response_data = response.json()
|
|
|
|
self.assertDictEqual(
|
|
self.permission_denied_response("You don't have sufficient permissions in this project."), response_data
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
|
|
|
def test_add_admin_as_project_admin_allowed(self):
|
|
self.organization_membership.level = OrganizationMembership.Level.ADMIN
|
|
self.organization_membership.save()
|
|
ExplicitTeamMembership.objects.create(
|
|
team=self.team, parent_membership=self.organization_membership, level=ExplicitTeamMembership.Level.ADMIN
|
|
)
|
|
|
|
new_user: User = User.objects.create_and_join(self.organization, "rookie@posthog.com", None)
|
|
|
|
response = self.client.post(
|
|
"/api/projects/@current/explicit_members/",
|
|
{"user_uuid": new_user.uuid, "level": ExplicitTeamMembership.Level.ADMIN},
|
|
)
|
|
response_data = response.json()
|
|
|
|
self.assertDictContainsSubset(
|
|
{"effective_level": ExplicitTeamMembership.Level.ADMIN, "level": ExplicitTeamMembership.Level.ADMIN,},
|
|
response_data,
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
|
|
def test_add_member_to_non_current_project_allowed(self):
|
|
self.organization_membership.level = OrganizationMembership.Level.ADMIN
|
|
self.organization_membership.save()
|
|
another_team = Team.objects.create(organization=self.organization, access_control=True)
|
|
|
|
new_user: User = User.objects.create_and_join(
|
|
self.organization, "rookie@posthog.com", None,
|
|
)
|
|
|
|
response = self.client.post(f"/api/projects/{another_team.id}/explicit_members/", {"user_uuid": new_user.uuid})
|
|
response_data = response.json()
|
|
|
|
self.assertDictContainsSubset(
|
|
{"effective_level": ExplicitTeamMembership.Level.MEMBER, "level": ExplicitTeamMembership.Level.MEMBER,},
|
|
response_data,
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
|
|
def test_add_member_to_project_in_outside_organization_forbidden(self):
|
|
self.organization_membership.level = OrganizationMembership.Level.ADMIN
|
|
self.organization_membership.save()
|
|
_, new_team, new_user = User.objects.bootstrap(
|
|
"Acme", "mallory@acme.com", None, team_fields={"access_control": True}
|
|
)
|
|
|
|
response = self.client.post(f"/api/projects/{new_team.id}/explicit_members/", {"user_uuid": new_user.uuid,})
|
|
response_data = response.json()
|
|
|
|
self.assertDictEqual(self.not_found_response("Project not found."), response_data)
|
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
|
|
|
def test_add_member_to_project_that_is_not_organization_member_forbidden(self):
|
|
self.organization_membership.level = OrganizationMembership.Level.ADMIN
|
|
self.organization_membership.save()
|
|
_, new_team, new_user = User.objects.bootstrap("Acme", "mallory@acme.com", None)
|
|
|
|
response = self.client.post(f"/api/projects/@current/explicit_members/", {"user_uuid": new_user.uuid,})
|
|
response_data = response.json()
|
|
|
|
self.assertDictEqual(
|
|
self.permission_denied_response("You both need to belong to the same organization."), response_data
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
|
|
|
def test_add_member_to_nonexistent_project_forbidden(self):
|
|
self.organization_membership.level = OrganizationMembership.Level.ADMIN
|
|
self.organization_membership.save()
|
|
new_user: User = User.objects.create_and_join(self.organization, "rookie@posthog.com", None)
|
|
|
|
response = self.client.post(f"/api/projects/2137/explicit_members/", {"user_uuid": new_user.uuid,})
|
|
response_data = response.json()
|
|
|
|
self.assertDictEqual(self.not_found_response("Project not found."), response_data)
|
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
|
|
|
def test_set_level_of_member_to_admin_as_org_owner_allowed(self):
|
|
self.organization_membership.level = OrganizationMembership.Level.OWNER
|
|
self.organization_membership.save()
|
|
|
|
new_user: User = User.objects.create_and_join(self.organization, "rookie@posthog.com", None)
|
|
new_org_membership: OrganizationMembership = OrganizationMembership.objects.get(
|
|
user=new_user, organization=self.organization
|
|
)
|
|
new_team_membership = ExplicitTeamMembership.objects.create(
|
|
team=self.team, parent_membership=new_org_membership
|
|
)
|
|
|
|
response = self.client.patch(
|
|
f"/api/projects/@current/explicit_members/{new_user.uuid}", {"level": ExplicitTeamMembership.Level.ADMIN}
|
|
)
|
|
response_data = response.json()
|
|
|
|
self.assertDictContainsSubset(
|
|
{"effective_level": ExplicitTeamMembership.Level.ADMIN, "level": ExplicitTeamMembership.Level.ADMIN,},
|
|
response_data,
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
|
|
def test_set_level_of_member_to_admin_as_org_member_forbidden(self):
|
|
self.organization_membership.level = OrganizationMembership.Level.MEMBER
|
|
self.organization_membership.save()
|
|
|
|
new_user: User = User.objects.create_and_join(self.organization, "rookie@posthog.com", None)
|
|
new_org_membership: OrganizationMembership = OrganizationMembership.objects.get(
|
|
user=new_user, organization=self.organization
|
|
)
|
|
new_team_membership = ExplicitTeamMembership.objects.create(
|
|
team=self.team, parent_membership=new_org_membership
|
|
)
|
|
|
|
response = self.client.patch(
|
|
f"/api/projects/@current/explicit_members/{new_user.uuid}", {"level": ExplicitTeamMembership.Level.ADMIN}
|
|
)
|
|
response_data = response.json()
|
|
|
|
self.assertDictEqual(
|
|
self.permission_denied_response("You don't have sufficient permissions in this project."), response_data,
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
|
|
|
def test_demote_yourself_as_org_member_and_project_admin_forbidden(self):
|
|
self.organization_membership.level = OrganizationMembership.Level.MEMBER
|
|
self.organization_membership.save()
|
|
self_team_membership = ExplicitTeamMembership.objects.create(
|
|
team=self.team, parent_membership=self.organization_membership, level=ExplicitTeamMembership.Level.ADMIN
|
|
)
|
|
|
|
response = self.client.patch(
|
|
f"/api/projects/@current/explicit_members/{self.user.uuid}", {"level": ExplicitTeamMembership.Level.MEMBER}
|
|
)
|
|
response_data = response.json()
|
|
|
|
self.assertDictEqual(
|
|
self.permission_denied_response("You can't set your own access level."), response_data,
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
|
|
|
def test_set_level_of_member_to_admin_as_org_member_but_project_admin_allowed(self):
|
|
self.organization_membership.level = OrganizationMembership.Level.MEMBER
|
|
self.organization_membership.save()
|
|
self_team_membership = ExplicitTeamMembership.objects.create(
|
|
team=self.team, parent_membership=self.organization_membership, level=ExplicitTeamMembership.Level.ADMIN
|
|
)
|
|
|
|
new_user: User = User.objects.create_and_join(self.organization, "rookie@posthog.com", None)
|
|
new_org_membership: OrganizationMembership = OrganizationMembership.objects.get(
|
|
user=new_user, organization=self.organization
|
|
)
|
|
new_team_membership = ExplicitTeamMembership.objects.create(
|
|
team=self.team, parent_membership=new_org_membership
|
|
)
|
|
|
|
response = self.client.patch(
|
|
f"/api/projects/@current/explicit_members/{new_user.uuid}", {"level": ExplicitTeamMembership.Level.ADMIN}
|
|
)
|
|
response_data = response.json()
|
|
|
|
self.assertDictContainsSubset(
|
|
{"effective_level": ExplicitTeamMembership.Level.ADMIN, "level": ExplicitTeamMembership.Level.ADMIN,},
|
|
response_data,
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
|
|
def test_remove_member_as_org_admin_allowed(self):
|
|
self.organization_membership.level = OrganizationMembership.Level.ADMIN
|
|
self.organization_membership.save()
|
|
|
|
new_user: User = User.objects.create_and_join(self.organization, "rookie@posthog.com", None)
|
|
new_org_membership: OrganizationMembership = OrganizationMembership.objects.get(
|
|
user=new_user, organization=self.organization
|
|
)
|
|
new_team_membership = ExplicitTeamMembership.objects.create(
|
|
team=self.team, parent_membership=new_org_membership
|
|
)
|
|
|
|
response = self.client.delete(f"/api/projects/@current/explicit_members/{new_user.uuid}")
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
|
|
|
def test_remove_member_as_org_member_allowed(self):
|
|
self.organization_membership.level = OrganizationMembership.Level.MEMBER
|
|
self.organization_membership.save()
|
|
|
|
new_user: User = User.objects.create_and_join(self.organization, "rookie@posthog.com", None)
|
|
new_org_membership: OrganizationMembership = OrganizationMembership.objects.get(
|
|
user=new_user, organization=self.organization
|
|
)
|
|
new_team_membership = ExplicitTeamMembership.objects.create(
|
|
team=self.team, parent_membership=new_org_membership
|
|
)
|
|
|
|
response = self.client.delete(f"/api/projects/@current/explicit_members/{new_user.uuid}")
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
|
|
|
def test_remove_member_as_org_member_but_project_admin_allowed(self):
|
|
self.organization_membership.level = OrganizationMembership.Level.MEMBER
|
|
self.organization_membership.save()
|
|
self_team_membership = ExplicitTeamMembership.objects.create(
|
|
team=self.team, parent_membership=self.organization_membership, level=ExplicitTeamMembership.Level.ADMIN
|
|
)
|
|
|
|
new_user: User = User.objects.create_and_join(self.organization, "rookie@posthog.com", None)
|
|
new_org_membership: OrganizationMembership = OrganizationMembership.objects.get(
|
|
user=new_user, organization=self.organization
|
|
)
|
|
new_team_membership = ExplicitTeamMembership.objects.create(
|
|
team=self.team, parent_membership=new_org_membership
|
|
)
|
|
|
|
response = self.client.delete(f"/api/projects/@current/explicit_members/{new_user.uuid}")
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
|
|
|
def test_add_member_to_non_private_project_forbidden(self):
|
|
self.organization_membership.level = OrganizationMembership.Level.OWNER
|
|
self.organization_membership.save()
|
|
self.team.access_control = False
|
|
self.team.save()
|
|
|
|
new_user: User = User.objects.create_and_join(self.organization, "rookie@posthog.com", None)
|
|
|
|
response = self.client.post("/api/projects/@current/explicit_members/", {"user_uuid": new_user.uuid})
|
|
response_data = response.json()
|
|
|
|
self.assertDictEqual(
|
|
self.validation_error_response(
|
|
"Explicit members can only be accessed for projects with project-based permissioning enabled.",
|
|
attr="non_field_errors",
|
|
),
|
|
response_data,
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
|
|
def test_leave_project_as_admin_allowed(self):
|
|
self.organization_membership.level = OrganizationMembership.Level.MEMBER
|
|
self.organization_membership.save()
|
|
|
|
explicit_team_membership = ExplicitTeamMembership.objects.create(
|
|
team=self.team, parent_membership=self.organization_membership, level=ExplicitTeamMembership.Level.ADMIN
|
|
)
|
|
|
|
response = self.client.delete(f"/api/projects/@current/explicit_members/{self.user.uuid}")
|
|
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
|
|
|
def test_leave_project_as_admin_member(self):
|
|
self.organization_membership.level = OrganizationMembership.Level.MEMBER
|
|
self.organization_membership.save()
|
|
|
|
explicit_team_membership = ExplicitTeamMembership.objects.create(
|
|
team=self.team, parent_membership=self.organization_membership, level=ExplicitTeamMembership.Level.MEMBER
|
|
)
|
|
|
|
response = self.client.delete(f"/api/projects/@current/explicit_members/{self.user.uuid}")
|
|
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|