0
0
mirror of https://github.com/PostHog/posthog.git synced 2024-11-28 09:16:49 +01:00
posthog/ee/api/test/test_team.py
Harry Waye b7c898d23e
feat(correlation): load exclude properties from team settings (#6715)
* feat(correlation): add team wide person property exclusion list

This change just adds the exclusion list to the `Team` model and checks
that it can be updated. Separately we can:

 1. add interface for the Project page to display
 2. pull and update this list from the funnels correlation page

NOTE: We don't perform any validation on the structure, :fingerscrossed:
this won't be an issue. Alternative would be to use ArrayField, but use
of JSONField is consistent with other fields.

* test(correlation): add test checking exclude properties pulled from team

* feat(correlation): load exclude properties from team settings

Previously we were loading exclude properties from local storage, so we
would not be sharing the exclusion list between users. This change
simply plugs the `excludePropertyNames` into the `teamLogic` for the
purpose of persisting and loading the values.

* use [teamLogic.actionTypes.loadCurrentTeamSuccess]

* get tests passing

* switch frontend to using `correlation_config`

* refactor: use Team.correlation_config for persistence

* add project settings for correlation

* fix lint

* ensure excluded properties are saved to project config

* Add default excluded properties

* format

* make propertyCorrelations not null

* ensure excluded property names config is unique

* rename excludeProperty to excludePropertyFromProject

* update var names

* change to targetProperties

* remove null special casing

* update to filter client side on exclude from project clicked

* update test name to reflect new functionality

* fix tests
2021-11-02 11:12:13 +00:00

410 lines
18 KiB
Python

from rest_framework.status import (
HTTP_200_OK,
HTTP_204_NO_CONTENT,
HTTP_400_BAD_REQUEST,
HTTP_403_FORBIDDEN,
HTTP_404_NOT_FOUND,
)
from ee.api.test.base import APILicensedTest
from ee.models.explicit_team_membership import ExplicitTeamMembership
from posthog.models import organization
from posthog.models.organization import Organization, OrganizationMembership
from posthog.models.team import Team
from posthog.models.user import User
class TestProjectEnterpriseAPI(APILicensedTest):
CLASS_DATA_LEVEL_SETUP = False
# Creating projects
def test_create_project(self):
self.organization_membership.level = OrganizationMembership.Level.ADMIN
self.organization_membership.save()
response = self.client.post("/api/projects/", {"name": "Test"})
self.assertEqual(response.status_code, 201)
self.assertEqual(Team.objects.count(), 2)
response_data = response.json()
self.assertDictContainsSubset(
{
"name": "Test",
"access_control": False,
"effective_membership_level": OrganizationMembership.Level.ADMIN,
},
response_data,
)
self.assertEqual(self.organization.teams.count(), 2)
def test_non_admin_cannot_create_project(self):
self.organization_membership.level = OrganizationMembership.Level.MEMBER
self.organization_membership.save()
count = Team.objects.count()
response = self.client.post("/api/projects/", {"name": "Test"})
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
self.assertEqual(Team.objects.count(), count)
self.assertEqual(
response.json(), self.permission_denied_response("Your organization access level is insufficient.")
)
def test_user_that_does_not_belong_to_an_org_cannot_create_a_project(self):
user = User.objects.create(email="no_org@posthog.com")
self.client.force_login(user)
response = self.client.post("/api/projects/", {"name": "Test"})
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST)
self.assertEqual(
response.json(),
{
"type": "validation_error",
"code": "invalid_input",
"detail": "You need to belong to an organization.",
"attr": None,
},
)
# Deleting projects
def test_delete_team_as_org_admin_allowed(self):
self.organization_membership.level = OrganizationMembership.Level.ADMIN
self.organization_membership.save()
response = self.client.delete(f"/api/projects/{self.team.id}")
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
self.assertEqual(Team.objects.filter(organization=self.organization).count(), 0)
def test_delete_team_as_org_member_forbidden(self):
self.organization_membership.level = OrganizationMembership.Level.MEMBER
self.organization_membership.save()
response = self.client.delete(f"/api/projects/{self.team.id}")
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
self.assertEqual(Team.objects.filter(organization=self.organization).count(), 1)
def test_delete_open_team_as_org_member_but_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.delete(f"/api/projects/{self.team.id}")
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
self.assertEqual(Team.objects.filter(organization=self.organization).count(), 1)
def test_delete_private_team_as_org_member_but_project_admin_allowed(self):
self.organization_membership.level = OrganizationMembership.Level.MEMBER
self.organization_membership.save()
self.team.access_control = True
self.team.save()
self_team_membership = ExplicitTeamMembership.objects.create(
team=self.team, parent_membership=self.organization_membership, level=ExplicitTeamMembership.Level.ADMIN
)
response = self.client.delete(f"/api/projects/{self.team.id}")
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
self.assertEqual(Team.objects.filter(organization=self.organization).count(), 0)
def test_delete_second_team_as_org_admin_allowed(self):
self.organization_membership.level = OrganizationMembership.Level.ADMIN
self.organization_membership.save()
team = Team.objects.create(organization=self.organization)
response = self.client.delete(f"/api/projects/{team.id}")
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
self.assertEqual(Team.objects.filter(organization=self.organization).count(), 1)
def test_no_delete_team_not_administrating_organization(self):
self.organization_membership.level = OrganizationMembership.Level.MEMBER
self.organization_membership.save()
team = Team.objects.create(organization=self.organization)
response = self.client.delete(f"/api/projects/{team.id}")
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
self.assertEqual(Team.objects.filter(organization=self.organization).count(), 2)
def test_no_delete_team_not_belonging_to_organization(self):
team_1 = Organization.objects.bootstrap(None)[2]
response = self.client.delete(f"/api/projects/{team_1.id}")
self.assertEqual(response.status_code, HTTP_404_NOT_FOUND)
self.assertTrue(Team.objects.filter(id=team_1.id).exists())
organization, _, _ = User.objects.bootstrap("X", "someone@x.com", "qwerty", "Someone")
team_2 = Team.objects.create(organization=organization)
response = self.client.delete(f"/api/projects/{team_2.id}")
self.assertEqual(response.status_code, HTTP_404_NOT_FOUND)
self.assertEqual(Team.objects.filter(organization=organization).count(), 2)
# Updating projects
def test_rename_project_as_org_member_allowed(self):
self.organization_membership.level = OrganizationMembership.Level.MEMBER
self.organization_membership.save()
response = self.client.patch(f"/api/projects/@current/", {"name": "Erinaceus europaeus"})
self.team.refresh_from_db()
self.assertEqual(response.status_code, HTTP_200_OK)
self.assertEqual(self.team.name, "Erinaceus europaeus")
def test_rename_private_project_as_org_member_forbidden(self):
self.organization_membership.level = OrganizationMembership.Level.MEMBER
self.organization_membership.save()
self.team.access_control = True
self.team.save()
response = self.client.patch(f"/api/projects/@current/", {"name": "Acherontia atropos"})
self.team.refresh_from_db()
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
self.assertEqual(self.team.name, "Default Project")
def test_rename_private_project_current_as_org_outsider_forbidden(self):
self.organization_membership.delete()
response = self.client.patch(f"/api/projects/@current/", {"name": "Acherontia atropos"})
self.team.refresh_from_db()
self.assertEqual(response.status_code, HTTP_404_NOT_FOUND)
def test_rename_private_project_id_as_org_outsider_forbidden(self):
self.organization_membership.delete()
response = self.client.patch(f"/api/projects/{self.team.id}/", {"name": "Acherontia atropos"})
self.team.refresh_from_db()
self.assertEqual(response.status_code, HTTP_404_NOT_FOUND)
def test_rename_private_project_as_org_member_and_project_member_allowed(self):
self.organization_membership.level = OrganizationMembership.Level.MEMBER
self.organization_membership.save()
self.team.access_control = True
self.team.save()
self_team_membership = ExplicitTeamMembership.objects.create(
team=self.team, parent_membership=self.organization_membership, level=ExplicitTeamMembership.Level.MEMBER
)
response = self.client.patch(f"/api/projects/@current/", {"name": "Acherontia atropos"})
self.team.refresh_from_db()
self.assertEqual(response.status_code, HTTP_200_OK)
self.assertEqual(self.team.name, "Acherontia atropos")
def test_enable_access_control_as_org_member_forbidden(self):
self.organization_membership.level = OrganizationMembership.Level.MEMBER
self.organization_membership.save()
response = self.client.patch(f"/api/projects/@current/", {"access_control": True})
self.team.refresh_from_db()
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
self.assertFalse(self.team.access_control)
def test_enable_access_control_as_org_admin_allowed(self):
self.organization_membership.level = OrganizationMembership.Level.ADMIN
self.organization_membership.save()
response = self.client.patch(f"/api/projects/@current/", {"access_control": True})
self.team.refresh_from_db()
self.assertEqual(response.status_code, HTTP_200_OK)
self.assertTrue(self.team.access_control)
def test_enable_access_control_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/", {"access_control": True})
self.team.refresh_from_db()
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
self.assertFalse(self.team.access_control)
def test_disable_access_control_as_org_member_forbidden(self):
self.organization_membership.level = OrganizationMembership.Level.MEMBER
self.organization_membership.save()
self.team.access_control = True
self.team.save()
response = self.client.patch(f"/api/projects/@current/", {"access_control": False})
self.team.refresh_from_db()
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
self.assertTrue(self.team.access_control)
def test_disable_access_control_as_org_member_and_project_admin_forbidden(self):
# Only org-wide admins+ should be allowed to make the project open,
# because if a project-specific admin who is only an org member did it, they wouldn't be able to reenable it
self.organization_membership.level = OrganizationMembership.Level.MEMBER
self.organization_membership.save()
self.team.access_control = True
self.team.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/", {"access_control": False})
self.team.refresh_from_db()
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
self.assertTrue(self.team.access_control)
def test_disable_access_control_as_org_admin_allowed(self):
self.organization_membership.level = OrganizationMembership.Level.ADMIN
self.organization_membership.save()
self.team.access_control = True
self.team.save()
response = self.client.patch(f"/api/projects/@current/", {"access_control": False})
self.team.refresh_from_db()
self.assertEqual(response.status_code, HTTP_200_OK)
self.assertFalse(self.team.access_control)
def test_can_update_and_retrieve_person_property_names_excluded_from_correlation(self):
response = self.client.patch(
f"/api/projects/@current/", {"correlation_config": {"excluded_person_property_names": ["$os"]}}
)
self.assertEqual(response.status_code, HTTP_200_OK)
response = self.client.get(f"/api/projects/@current/")
self.assertEqual(response.status_code, HTTP_200_OK)
response_data = response.json()
self.assertDictContainsSubset(
{"correlation_config": {"excluded_person_property_names": ["$os"]}}, response_data
)
# Fetching projects
def test_fetch_team_as_org_admin_works(self):
self.organization_membership.level = OrganizationMembership.Level.ADMIN
self.organization_membership.save()
response = self.client.get(f"/api/projects/@current/")
response_data = response.json()
self.assertEqual(response.status_code, HTTP_200_OK)
self.assertDictContainsSubset(
{
"name": "Default Project",
"access_control": False,
"effective_membership_level": OrganizationMembership.Level.ADMIN,
},
response_data,
)
def test_fetch_team_as_org_member_works(self):
self.organization_membership.level = OrganizationMembership.Level.MEMBER
self.organization_membership.save()
response = self.client.get(f"/api/projects/@current/")
response_data = response.json()
self.assertEqual(response.status_code, HTTP_200_OK)
self.assertDictContainsSubset(
{
"name": "Default Project",
"access_control": False,
"effective_membership_level": OrganizationMembership.Level.MEMBER,
},
response_data,
)
def test_fetch_private_team_as_org_member(self):
self.organization_membership.level = OrganizationMembership.Level.MEMBER
self.organization_membership.save()
self.team.access_control = True
self.team.save()
response = self.client.get(f"/api/projects/@current/")
response_data = response.json()
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
self.assertEqual(
self.permission_denied_response("You don't have sufficient permissions in the project."), response_data
)
def test_fetch_private_team_as_org_member_and_project_member(self):
self.organization_membership.level = OrganizationMembership.Level.MEMBER
self.organization_membership.save()
self.team.access_control = True
self.team.save()
self_team_membership = ExplicitTeamMembership.objects.create(
team=self.team, parent_membership=self.organization_membership, level=ExplicitTeamMembership.Level.MEMBER
)
response = self.client.get(f"/api/projects/@current/")
response_data = response.json()
self.assertEqual(response.status_code, HTTP_200_OK)
self.assertDictContainsSubset(
{
"name": "Default Project",
"access_control": True,
"effective_membership_level": OrganizationMembership.Level.MEMBER,
},
response_data,
)
def test_fetch_private_team_as_org_member_and_project_admin(self):
self.organization_membership.level = OrganizationMembership.Level.MEMBER
self.organization_membership.save()
self.team.access_control = True
self.team.save()
self_team_membership = ExplicitTeamMembership.objects.create(
team=self.team, parent_membership=self.organization_membership, level=ExplicitTeamMembership.Level.ADMIN
)
response = self.client.get(f"/api/projects/@current/")
response_data = response.json()
self.assertEqual(response.status_code, HTTP_200_OK)
self.assertDictContainsSubset(
{
"name": "Default Project",
"access_control": True,
"effective_membership_level": OrganizationMembership.Level.ADMIN,
},
response_data,
)
def test_fetch_team_as_org_outsider(self):
self.organization_membership.delete()
response = self.client.get(f"/api/projects/@current/")
response_data = response.json()
self.assertEqual(response.status_code, HTTP_404_NOT_FOUND)
self.assertEqual(self.not_found_response(), response_data)
def test_fetch_nonexistent_team(self):
response = self.client.get(f"/api/projects/234444/")
response_data = response.json()
self.assertEqual(response.status_code, HTTP_404_NOT_FOUND)
self.assertEqual(self.not_found_response(), response_data)
def test_list_teams_restricted_ones_hidden(self):
self.organization_membership.level = OrganizationMembership.Level.MEMBER
self.organization_membership.save()
other_team = Team.objects.create(organization=self.organization, name="Other", access_control=True)
# Other team should not be returned as it's restricted for the logged-in user
with self.assertNumQueries(9):
response = self.client.get(f"/api/projects/")
self.assertEqual(response.status_code, HTTP_200_OK)
self.assertEqual(
response.json().get("results"),
[
{
"id": self.team.id,
"uuid": str(self.team.uuid),
"organization": str(self.organization.id),
"api_token": self.team.api_token,
"name": self.team.name,
"completed_snippet_onboarding": False,
"ingested_event": False,
"is_demo": False,
"timezone": "UTC",
"access_control": False,
"effective_membership_level": int(OrganizationMembership.Level.MEMBER),
}
],
)