mirror of
https://github.com/PostHog/posthog.git
synced 2024-12-01 04:12:23 +01:00
3a67367ac7
* 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
* Restore `TopNavigation` changes
This reverts commit 05fd9e4ed6
.
* Try to add a story for `TopNavigation`
* minor clarifications
* Revert `TopNavigation` changes
* Restore `TopNavigation` changes
* Make new access control entirely project-based
* Update migrations
* Add `project_based_permissioning` to `TeamBasicSerializer`
* Update test_team.py
* Fix `isRestricted` in `ProjectRow`
* Disable project creation for non-admins
* Make project icon in top nav itself dynamic as well
* Fix story
* Delete 0169_project_based_permissioning.py
* Apply suggestions from code review
Co-authored-by: Paolo D'Amico <paolodamico@users.noreply.github.com>
* Update frontend/src/layout/navigation/TopNavigation.tsx
Co-authored-by: Paolo D'Amico <paolodamico@users.noreply.github.com>
* Address feedback
* Project-based permissioning full data access restrictions (#6068)
* Fix Access Control restriction tooltip
* Add `TeamMemberAccessPermission` and use it in viewsets
* Add `ErrorProjectUnavailable` scene
* Ignore mypy
* Update MainNavigation.tsx
* Update explicit_team_member.py
* Fix frontend detection of unavailable project
* Fix some tests and edge cases
* Fix basic permissions
* Add more tests
* Simplify `ExplicitTeamMemberViewSet` permissions
* Improve restrictions and add moar tests
* Update frontend
* Fix a couple of things
* Fix import
* Fix some edge cases
* Fix typing errors
* Use hedgehog instead of moth
Co-authored-by: Paolo D'Amico <paolodamico@users.noreply.github.com>
* Address feedback
* Add proper permissioning to dashboard views
* Update ee/api/test/test_dashboard.py
Co-authored-by: Paolo D'Amico <paolodamico@users.noreply.github.com>
Co-authored-by: Paolo D'Amico <paolodamico@users.noreply.github.com>
* Run prettier
* Remove debug code
Co-authored-by: Paolo D'Amico <paolodamico@users.noreply.github.com>
131 lines
5.7 KiB
Python
131 lines
5.7 KiB
Python
from typing import Any, Dict, Optional, cast
|
|
|
|
from django.db.utils import IntegrityError
|
|
from django.shortcuts import get_object_or_404
|
|
from rest_framework import exceptions, serializers, viewsets
|
|
from rest_framework.permissions import IsAuthenticated
|
|
|
|
from ee.models.explicit_team_membership import ExplicitTeamMembership
|
|
from posthog.api.routing import StructuredViewSetMixin
|
|
from posthog.api.shared import UserBasicSerializer
|
|
from posthog.models.organization import OrganizationMembership
|
|
from posthog.models.team import Team
|
|
from posthog.models.user import User
|
|
from posthog.permissions import TeamMemberStrictManagementPermission
|
|
|
|
|
|
class ExplicitTeamMemberSerializer(serializers.ModelSerializer):
|
|
user = UserBasicSerializer(source="parent_membership.user", read_only=True)
|
|
parent_level = serializers.IntegerField(source="parent_membership.level", read_only=True)
|
|
|
|
user_uuid = serializers.UUIDField(required=True, write_only=True)
|
|
|
|
class Meta:
|
|
model = ExplicitTeamMembership
|
|
fields = [
|
|
"id",
|
|
"level",
|
|
"parent_level",
|
|
"parent_membership_id",
|
|
"joined_at",
|
|
"updated_at",
|
|
"user",
|
|
"user_uuid", # write_only (see above)
|
|
"effective_level", # read_only (calculated)
|
|
]
|
|
read_only_fields = ["id", "parent_membership_id", "joined_at", "updated_at", "user", "effective_level"]
|
|
|
|
def create(self, validated_data):
|
|
team: Team = self.context["team"]
|
|
user_uuid = validated_data.pop("user_uuid")
|
|
validated_data["team"] = team
|
|
try:
|
|
requesting_parent_membership: OrganizationMembership = OrganizationMembership.objects.get(
|
|
organization_id=team.organization_id, user__uuid=user_uuid
|
|
)
|
|
except OrganizationMembership.DoesNotExist:
|
|
raise exceptions.PermissionDenied("You both need to belong to the same organization.")
|
|
validated_data["parent_membership"] = requesting_parent_membership
|
|
try:
|
|
return super().create(validated_data)
|
|
except IntegrityError:
|
|
raise exceptions.ValidationError("This user likely already is an explicit member of the project.")
|
|
|
|
def validate(self, attrs):
|
|
team: Team = self.context["team"]
|
|
if not team.access_control:
|
|
raise exceptions.ValidationError(
|
|
"Explicit members can only be accessed for projects with project-based permissioning enabled."
|
|
)
|
|
requesting_user: User = self.context["request"].user
|
|
membership_being_accessed = cast(Optional[ExplicitTeamMembership], self.instance)
|
|
try:
|
|
requesting_level = self.context["team"].get_effective_membership_level(requesting_user)
|
|
except OrganizationMembership.DoesNotExist:
|
|
# Requesting user does not belong to the project's organization, so we spoof a 404 for enhanced security
|
|
raise exceptions.NotFound("Project not found.")
|
|
|
|
new_level = attrs.get("level")
|
|
|
|
if requesting_level is None:
|
|
raise exceptions.PermissionDenied("You do not have the required access to this project.")
|
|
|
|
if attrs.get("user_uuid") == requesting_user.uuid:
|
|
# Create-only check
|
|
raise exceptions.PermissionDenied("You can't explicitly add yourself to projects.")
|
|
|
|
if new_level is not None and new_level > requesting_level:
|
|
raise exceptions.PermissionDenied("You can only set access level to lower or equal to your current one.")
|
|
|
|
if membership_being_accessed is not None:
|
|
# Update-only checks
|
|
if membership_being_accessed.parent_membership.user_id != requesting_user.id:
|
|
# Requesting user updating someone else
|
|
if membership_being_accessed.level > requesting_level:
|
|
raise exceptions.PermissionDenied("You can only edit others with level lower or equal to you.")
|
|
else:
|
|
# Requesting user updating themselves
|
|
if new_level is not None:
|
|
raise exceptions.PermissionDenied("You can't set your own access level.")
|
|
|
|
return attrs
|
|
|
|
|
|
class ExplicitTeamMemberViewSet(
|
|
StructuredViewSetMixin, viewsets.ModelViewSet,
|
|
):
|
|
permission_classes = [IsAuthenticated, TeamMemberStrictManagementPermission]
|
|
pagination_class = None
|
|
queryset = ExplicitTeamMembership.objects.select_related("team", "parent_membership", "parent_membership__user")
|
|
lookup_field = "parent_membership__user__uuid"
|
|
ordering = ["level", "-joined_at"]
|
|
serializer_class = ExplicitTeamMemberSerializer
|
|
|
|
def get_serializer_context(self) -> Dict[str, Any]:
|
|
serializer_context = super().get_serializer_context()
|
|
try:
|
|
serializer_context["team"] = Team.objects.get(id=serializer_context["team_id"])
|
|
except Team.DoesNotExist:
|
|
raise exceptions.NotFound("Project not found.")
|
|
return serializer_context
|
|
|
|
def get_permissions(self):
|
|
if (
|
|
self.action == "destroy"
|
|
and self.request.user.is_authenticated
|
|
and self.kwargs.get("parent_membership__user__uuid") == str(self.request.user.uuid)
|
|
):
|
|
# Special case: allow already authenticated users to leave projects
|
|
return []
|
|
return super().get_permissions()
|
|
|
|
def get_object(self) -> ExplicitTeamMembership:
|
|
queryset = self.filter_queryset(self.get_queryset())
|
|
lookup_value = self.kwargs[self.lookup_field]
|
|
if lookup_value == "@me":
|
|
return queryset.get(user=self.request.user)
|
|
filter_kwargs = {self.lookup_field: lookup_value}
|
|
obj = get_object_or_404(queryset, **filter_kwargs)
|
|
self.check_object_permissions(self.request, obj)
|
|
return obj
|