0
0
mirror of https://github.com/PostHog/posthog.git synced 2024-11-28 18:26:15 +01:00
posthog/ee/api/test/test_team_memberships.py
Michael Matloka bc3e223265
Project-based permissioning framework (#5976)
* 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

* 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

* adjust copy & UI a bit

* Address feedback on field comment

* "Privacy settings" to "Access Control"

* Ignore mypy

* Rename `Team` field `project_based_permissioning` to `access_control`

* Update migrations

Co-authored-by: Paolo D'Amico <paolodamico@users.noreply.github.com>
2021-09-22 18:29:59 +02:00

435 lines
20 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)