From 36c17ed54610eef93523115b6c0f2b8714ce0eaa Mon Sep 17 00:00:00 2001 From: Raquel Smith Date: Wed, 23 Oct 2024 12:43:08 -0700 Subject: [PATCH 1/3] allow implicit team membership to invite to projects on org invites --- posthog/api/organization_invite.py | 18 ++++++--- posthog/api/test/test_organization_invites.py | 40 +++++++++++++++++-- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/posthog/api/organization_invite.py b/posthog/api/organization_invite.py index def55f6628c33..5f63439cf5d4e 100644 --- a/posthog/api/organization_invite.py +++ b/posthog/api/organization_invite.py @@ -78,15 +78,23 @@ def validate_private_project_access( if not is_private: continue try: - explicit_team_membership: ExplicitTeamMembership = ExplicitTeamMembership.objects.get( + team_membership: ExplicitTeamMembership | OrganizationMembership = ExplicitTeamMembership.objects.get( team_id=item["id"], parent_membership__user=self.context["request"].user, ) except ExplicitTeamMembership.DoesNotExist: - raise exceptions.ValidationError( - team_error, - ) - if explicit_team_membership.level < item["level"]: + try: + # No explicit team membership. Try getting the implicit team membership - any org owners and admins can invite to any team + team_membership = OrganizationMembership.objects.get( + organization_id=self.context["organization_id"], + user=self.context["request"].user, + level__in=[OrganizationMembership.Level.ADMIN, OrganizationMembership.Level.OWNER], + ) + except OrganizationMembership.DoesNotExist: + raise exceptions.ValidationError( + team_error, + ) + if team_membership.level < item["level"]: raise exceptions.ValidationError( "You cannot invite to a private project with a higher level than your own.", ) diff --git a/posthog/api/test/test_organization_invites.py b/posthog/api/test/test_organization_invites.py index 1e6499a37d3ac..94c59453d78b7 100644 --- a/posthog/api/test/test_organization_invites.py +++ b/posthog/api/test/test_organization_invites.py @@ -196,14 +196,14 @@ def test_can_specify_private_project_access_in_invite(self): { "target_email": email, "level": OrganizationMembership.Level.MEMBER, - "private_project_access": [{"id": self.team.id, "level": ExplicitTeamMembership.Level.ADMIN}], + "private_project_access": [{"id": private_team.id, "level": ExplicitTeamMembership.Level.ADMIN}], }, ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) obj = OrganizationInvite.objects.get(id=response.json()["id"]) self.assertEqual(obj.level, OrganizationMembership.Level.MEMBER) self.assertEqual( - obj.private_project_access, [{"id": self.team.id, "level": ExplicitTeamMembership.Level.ADMIN}] + obj.private_project_access, [{"id": private_team.id, "level": ExplicitTeamMembership.Level.ADMIN}] ) self.assertEqual(OrganizationInvite.objects.count(), count + 1) @@ -218,16 +218,48 @@ def test_can_specify_private_project_access_in_invite(self): { "target_email": email, "level": OrganizationMembership.Level.MEMBER, - "private_project_access": [{"id": self.team.id, "level": ExplicitTeamMembership.Level.ADMIN}], + "private_project_access": [{"id": private_team.id, "level": ExplicitTeamMembership.Level.ADMIN}], + }, + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + obj = OrganizationInvite.objects.get(id=response.json()["id"]) + self.assertEqual(obj.level, OrganizationMembership.Level.MEMBER) + self.assertEqual( + obj.private_project_access, [{"id": private_team.id, "level": ExplicitTeamMembership.Level.ADMIN}] + ) + self.assertEqual(OrganizationInvite.objects.count(), count + 1) + + def test_can_invite_to_private_project_if_user_has_implicit_access_to_team(self): + """ + Org admins and owners can invite to any private project, even if they're not an explicit admin of the team + because they have implicit access due to their org membership level. + """ + org_membership = OrganizationMembership.objects.get(user=self.user, organization=self.organization) + org_membership.level = OrganizationMembership.Level.ADMIN + org_membership.save() + + email = "x@posthog.com" + count = OrganizationInvite.objects.count() + private_team = Team.objects.create(organization=self.organization, name="Private Team", access_control=True) + response = self.client.post( + "/api/organizations/@current/invites/", + { + "target_email": email, + "level": OrganizationMembership.Level.MEMBER, + "private_project_access": [{"id": private_team.id, "level": ExplicitTeamMembership.Level.ADMIN}], }, ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) obj = OrganizationInvite.objects.get(id=response.json()["id"]) self.assertEqual(obj.level, OrganizationMembership.Level.MEMBER) self.assertEqual( - obj.private_project_access, [{"id": self.team.id, "level": ExplicitTeamMembership.Level.ADMIN}] + obj.private_project_access, [{"id": private_team.id, "level": ExplicitTeamMembership.Level.ADMIN}] ) self.assertEqual(OrganizationInvite.objects.count(), count + 1) + # reset the org membership level in case it's used in other tests + org_membership.level = OrganizationMembership.Level.MEMBER + org_membership.save() def test_invite_fails_if_team_in_private_project_access_not_in_org(self): email = "x@posthog.com" From 4bbab9cce9a90ed4768a0b38bf985b7bb4b60702 Mon Sep 17 00:00:00 2001 From: Raquel Smith Date: Wed, 23 Oct 2024 12:46:54 -0700 Subject: [PATCH 2/3] error should say project, not team --- posthog/api/organization_invite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/posthog/api/organization_invite.py b/posthog/api/organization_invite.py index 5f63439cf5d4e..99b6ddbf7bef0 100644 --- a/posthog/api/organization_invite.py +++ b/posthog/api/organization_invite.py @@ -59,7 +59,7 @@ def validate_target_email(self, email: str): def validate_private_project_access( self, private_project_access: Optional[list[dict[str, Any]]] ) -> Optional[list[dict[str, Any]]]: - team_error = "Team does not exist on this organization, or it is private and you do not have access to it." + team_error = "Project does not exist on this organization, or it is private and you do not have access to it." if not private_project_access: return None for item in private_project_access: From 21efa4c397283c6fe794c6c88acf18cd8f6e991d Mon Sep 17 00:00:00 2001 From: Raquel Smith Date: Fri, 25 Oct 2024 11:29:25 -0700 Subject: [PATCH 3/3] fix test --- posthog/api/test/test_organization_invites.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/posthog/api/test/test_organization_invites.py b/posthog/api/test/test_organization_invites.py index 94c59453d78b7..a486d796a9472 100644 --- a/posthog/api/test/test_organization_invites.py +++ b/posthog/api/test/test_organization_invites.py @@ -280,7 +280,7 @@ def test_invite_fails_if_team_in_private_project_access_not_in_org(self): { "type": "validation_error", "code": "invalid_input", - "detail": "Team does not exist on this organization, or it is private and you do not have access to it.", + "detail": "Project does not exist on this organization, or it is private and you do not have access to it.", "attr": "private_project_access", }, response_data, @@ -305,7 +305,7 @@ def test_invite_fails_if_inviter_does_not_have_access_to_team(self): { "type": "validation_error", "code": "invalid_input", - "detail": "Team does not exist on this organization, or it is private and you do not have access to it.", + "detail": "Project does not exist on this organization, or it is private and you do not have access to it.", "attr": "private_project_access", }, response_data,