diff --git a/ee/api/billing.py b/ee/api/billing.py index 08e0fc08a9719..2123ac0485b7f 100644 --- a/ee/api/billing.py +++ b/ee/api/billing.py @@ -7,7 +7,6 @@ from django.http import HttpResponse from django.shortcuts import redirect from rest_framework import serializers, status, viewsets -from rest_framework.authentication import BasicAuthentication, SessionAuthentication from rest_framework.decorators import action from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError from rest_framework.request import Request @@ -16,7 +15,7 @@ from ee.billing.billing_manager import BillingManager, build_billing_token from ee.models import License from ee.settings import BILLING_SERVICE_URL -from posthog.auth import PersonalAPIKeyAuthentication +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.cloud_utils import get_cached_instance_license from posthog.models import Organization @@ -34,14 +33,9 @@ class LicenseKeySerializer(serializers.Serializer): license = serializers.CharField() -class BillingViewset(viewsets.GenericViewSet): +class BillingViewset(TeamAndOrgViewSetMixin, viewsets.GenericViewSet): serializer_class = BillingSerializer - - authentication_classes = [ - PersonalAPIKeyAuthentication, - SessionAuthentication, - BasicAuthentication, - ] + derive_current_team_from_user_only = True def list(self, request: Request, *args: Any, **kwargs: Any) -> Response: license = get_cached_instance_license() diff --git a/ee/api/dashboard_collaborator.py b/ee/api/dashboard_collaborator.py index e9b339b921b0e..73f437e9bac2d 100644 --- a/ee/api/dashboard_collaborator.py +++ b/ee/api/dashboard_collaborator.py @@ -2,14 +2,13 @@ from django.db import IntegrityError from rest_framework import exceptions, mixins, serializers, viewsets -from rest_framework.permissions import SAFE_METHODS, BasePermission, IsAuthenticated +from rest_framework.permissions import SAFE_METHODS, BasePermission from rest_framework.request import Request from ee.models.dashboard_privilege import DashboardPrivilege -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer from posthog.models import Dashboard, User -from posthog.permissions import TeamMemberAccessPermission from posthog.user_permissions import UserPermissions, UserPermissionsSerializerMixin @@ -83,17 +82,13 @@ def create(self, validated_data): class DashboardCollaboratorViewSet( - StructuredViewSetMixin, + TeamAndOrgViewSetMixin, mixins.ListModelMixin, mixins.CreateModelMixin, mixins.DestroyModelMixin, viewsets.GenericViewSet, ): - permission_classes = [ - IsAuthenticated, - TeamMemberAccessPermission, - CanEditDashboardCollaborator, - ] + permission_classes = [CanEditDashboardCollaborator] pagination_class = None queryset = DashboardPrivilege.objects.select_related("dashboard").filter(user__is_active=True) lookup_field = "user__uuid" diff --git a/ee/api/explicit_team_member.py b/ee/api/explicit_team_member.py index 4f45bbb9fc418..9355e7afccb38 100644 --- a/ee/api/explicit_team_member.py +++ b/ee/api/explicit_team_member.py @@ -6,7 +6,7 @@ from rest_framework.permissions import IsAuthenticated from ee.models.explicit_team_membership import ExplicitTeamMembership -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer from posthog.models.organization import OrganizationMembership from posthog.models.team import Team @@ -101,8 +101,7 @@ def validate(self, attrs): return attrs -class ExplicitTeamMemberViewSet(StructuredViewSetMixin, viewsets.ModelViewSet): - permission_classes = [IsAuthenticated, TeamMemberStrictManagementPermission] +class ExplicitTeamMemberViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet): pagination_class = None queryset = ExplicitTeamMembership.objects.filter(parent_membership__user__is_active=True).select_related( "team", "parent_membership", "parent_membership__user" @@ -112,6 +111,8 @@ class ExplicitTeamMemberViewSet(StructuredViewSetMixin, viewsets.ModelViewSet): serializer_class = ExplicitTeamMemberSerializer include_in_docs = True + permission_classes = [IsAuthenticated, TeamMemberStrictManagementPermission] + def get_permissions(self): if ( self.action == "destroy" @@ -120,7 +121,7 @@ def get_permissions(self): ): # Special case: allow already authenticated users to leave projects return [] - return super().get_permissions() + return [permission() for permission in self.permission_classes] def get_object(self) -> ExplicitTeamMembership: queryset = self.filter_queryset(self.get_queryset()) diff --git a/ee/api/feature_flag_role_access.py b/ee/api/feature_flag_role_access.py index 3ce77dca89599..a2e22f2007ecf 100644 --- a/ee/api/feature_flag_role_access.py +++ b/ee/api/feature_flag_role_access.py @@ -1,12 +1,12 @@ from rest_framework import exceptions, mixins, serializers, viewsets -from rest_framework.permissions import SAFE_METHODS, BasePermission, IsAuthenticated +from rest_framework.permissions import SAFE_METHODS, BasePermission from ee.api.role import RoleSerializer from ee.models.feature_flag_role_access import FeatureFlagRoleAccess from ee.models.organization_resource_access import OrganizationResourceAccess from ee.models.role import Role from posthog.api.feature_flag import FeatureFlagSerializer -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.models import FeatureFlag from posthog.models.organization import OrganizationMembership @@ -66,14 +66,14 @@ def create(self, validated_data): class FeatureFlagRoleAccessViewSet( - StructuredViewSetMixin, + TeamAndOrgViewSetMixin, mixins.ListModelMixin, mixins.CreateModelMixin, mixins.DestroyModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet, ): - permission_classes = [IsAuthenticated, FeatureFlagRoleAccessPermissions] + permission_classes = [FeatureFlagRoleAccessPermissions] serializer_class = FeatureFlagRoleAccessSerializer queryset = FeatureFlagRoleAccess.objects.select_related("feature_flag") filter_rewrite_rules = {"team_id": "feature_flag__team_id"} diff --git a/ee/api/hooks.py b/ee/api/hooks.py index 96eba47f14378..6f4444d017ba5 100644 --- a/ee/api/hooks.py +++ b/ee/api/hooks.py @@ -3,15 +3,10 @@ from django.conf import settings from rest_framework import exceptions, serializers, viewsets -from rest_framework.permissions import IsAuthenticated from ee.models.hook import Hook -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.models.user import User -from posthog.permissions import ( - OrganizationMemberPermissions, - TeamMemberAccessPermission, -) class HookSerializer(serializers.ModelSerializer): @@ -31,18 +26,13 @@ def validate_target(self, target): return target -class HookViewSet(StructuredViewSetMixin, viewsets.ModelViewSet): +class HookViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet): """ Retrieve, create, update or destroy REST hooks. """ queryset = Hook.objects.all() ordering = "-created_at" - permission_classes = [ - IsAuthenticated, - OrganizationMemberPermissions, - TeamMemberAccessPermission, - ] serializer_class = HookSerializer def perform_create(self, serializer): diff --git a/ee/api/organization_resource_access.py b/ee/api/organization_resource_access.py index 618e59475a0be..77c71f6f5de64 100644 --- a/ee/api/organization_resource_access.py +++ b/ee/api/organization_resource_access.py @@ -1,10 +1,8 @@ from rest_framework import mixins, serializers, viewsets -from rest_framework.permissions import IsAuthenticated from ee.api.role import RolePermissions from ee.models.organization_resource_access import OrganizationResourceAccess -from posthog.api.routing import StructuredViewSetMixin -from posthog.permissions import OrganizationMemberPermissions +from posthog.api.routing import TeamAndOrgViewSetMixin class OrganizationResourceAccessSerializer(serializers.ModelSerializer): @@ -35,7 +33,7 @@ def create(self, validated_data): class OrganizationResourceAccessViewSet( - StructuredViewSetMixin, + TeamAndOrgViewSetMixin, mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin, @@ -43,10 +41,6 @@ class OrganizationResourceAccessViewSet( mixins.DestroyModelMixin, viewsets.GenericViewSet, ): - permission_classes = [ - IsAuthenticated, - OrganizationMemberPermissions, - RolePermissions, - ] + permission_classes = [RolePermissions] serializer_class = OrganizationResourceAccessSerializer queryset = OrganizationResourceAccess.objects.all() diff --git a/ee/api/role.py b/ee/api/role.py index 7e7c8714c537e..66f89133a8e68 100644 --- a/ee/api/role.py +++ b/ee/api/role.py @@ -2,17 +2,16 @@ from django.db import IntegrityError from rest_framework import mixins, serializers, viewsets -from rest_framework.permissions import SAFE_METHODS, BasePermission, IsAuthenticated +from rest_framework.permissions import SAFE_METHODS, BasePermission from ee.models.feature_flag_role_access import FeatureFlagRoleAccess from ee.models.organization_resource_access import OrganizationResourceAccess from ee.models.role import Role, RoleMembership -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer from posthog.models import OrganizationMembership from posthog.models.feature_flag import FeatureFlag from posthog.models.user import User -from posthog.permissions import OrganizationMemberPermissions class RolePermissions(BasePermission): @@ -86,7 +85,7 @@ def get_associated_flags(self, role: Role): class RoleViewSet( - StructuredViewSetMixin, + TeamAndOrgViewSetMixin, mixins.ListModelMixin, mixins.CreateModelMixin, mixins.RetrieveModelMixin, @@ -94,11 +93,7 @@ class RoleViewSet( mixins.DestroyModelMixin, viewsets.GenericViewSet, ): - permission_classes = [ - IsAuthenticated, - OrganizationMemberPermissions, - RolePermissions, - ] + permission_classes = [RolePermissions] serializer_class = RoleSerializer queryset = Role.objects.all() @@ -132,16 +127,13 @@ def create(self, validated_data): class RoleMembershipViewSet( - StructuredViewSetMixin, + TeamAndOrgViewSetMixin, mixins.ListModelMixin, mixins.CreateModelMixin, mixins.DestroyModelMixin, viewsets.GenericViewSet, ): - permission_classes = [ - IsAuthenticated, - RolePermissions, - ] + permission_classes = [RolePermissions] serializer_class = RoleMembershipSerializer queryset = RoleMembership.objects.select_related("role") filter_rewrite_rules = {"organization_id": "role__organization_id"} diff --git a/ee/api/subscription.py b/ee/api/subscription.py index 416371e45babe..35918914ea0e4 100644 --- a/ee/api/subscription.py +++ b/ee/api/subscription.py @@ -4,21 +4,15 @@ from django.db.models import QuerySet from django.http import HttpRequest, JsonResponse from rest_framework import serializers, viewsets -from rest_framework.authentication import BasicAuthentication, SessionAuthentication from rest_framework.exceptions import ValidationError -from rest_framework.permissions import IsAuthenticated from ee.tasks import subscriptions from posthog.api.forbid_destroy_model import ForbidDestroyModel -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer -from posthog.auth import PersonalAPIKeyAuthentication from posthog.constants import AvailableFeature from posthog.models.subscription import Subscription, unsubscribe_using_token -from posthog.permissions import ( - PremiumFeaturePermission, - TeamMemberAccessPermission, -) +from posthog.permissions import PremiumFeaturePermission from posthog.utils import str_to_bool @@ -95,20 +89,10 @@ def update(self, instance: Subscription, validated_data: dict, *args: Any, **kwa return instance -class SubscriptionViewSet(StructuredViewSetMixin, ForbidDestroyModel, viewsets.ModelViewSet): +class SubscriptionViewSet(TeamAndOrgViewSetMixin, ForbidDestroyModel, viewsets.ModelViewSet): queryset = Subscription.objects.all() serializer_class = SubscriptionSerializer - - authentication_classes = [ - PersonalAPIKeyAuthentication, - SessionAuthentication, - BasicAuthentication, - ] - permission_classes = [ - IsAuthenticated, - PremiumFeaturePermission, - TeamMemberAccessPermission, - ] + permission_classes = [PremiumFeaturePermission] premium_feature = AvailableFeature.SUBSCRIPTIONS def get_queryset(self) -> QuerySet: diff --git a/ee/clickhouse/views/experiments.py b/ee/clickhouse/views/experiments.py index 4bf181410df1d..05a73c4874ff8 100644 --- a/ee/clickhouse/views/experiments.py +++ b/ee/clickhouse/views/experiments.py @@ -4,7 +4,6 @@ from rest_framework import serializers, viewsets from rest_framework.decorators import action from rest_framework.exceptions import ValidationError -from rest_framework.permissions import IsAuthenticated from rest_framework.request import Request from rest_framework.response import Response from statshog.defaults.django import statsd @@ -20,17 +19,14 @@ ) from ee.clickhouse.queries.experiments.utils import requires_flag_warning from posthog.api.feature_flag import FeatureFlagSerializer, MinimalFeatureFlagSerializer -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer from posthog.caching.insight_cache import update_cached_state from posthog.clickhouse.query_tagging import tag_queries from posthog.constants import INSIGHT_TRENDS, AvailableFeature from posthog.models.experiment import Experiment from posthog.models.filters.filter import Filter -from posthog.permissions import ( - PremiumFeaturePermission, - TeamMemberAccessPermission, -) +from posthog.permissions import PremiumFeaturePermission from posthog.utils import generate_cache_key, get_safe_cache EXPERIMENT_RESULTS_CACHE_DEFAULT_TTL = 60 * 30 # 30 minutes @@ -285,14 +281,10 @@ def update(self, instance: Experiment, validated_data: dict, *args: Any, **kwarg return super().update(instance, validated_data) -class ClickhouseExperimentsViewSet(StructuredViewSetMixin, viewsets.ModelViewSet): +class ClickhouseExperimentsViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet): serializer_class = ExperimentSerializer queryset = Experiment.objects.all() - permission_classes = [ - IsAuthenticated, - PremiumFeaturePermission, - TeamMemberAccessPermission, - ] + permission_classes = [PremiumFeaturePermission] premium_feature = AvailableFeature.EXPERIMENTATION ordering = "-created_at" diff --git a/ee/clickhouse/views/groups.py b/ee/clickhouse/views/groups.py index 502ada5852f7b..06f3c5bbf09c7 100644 --- a/ee/clickhouse/views/groups.py +++ b/ee/clickhouse/views/groups.py @@ -8,11 +8,10 @@ from rest_framework.decorators import action from rest_framework.exceptions import NotFound, ValidationError from rest_framework.pagination import CursorPagination -from rest_framework.permissions import IsAuthenticated from ee.clickhouse.queries.related_actors_query import RelatedActorsQuery from posthog.api.documentation import extend_schema -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.auth import SharingAccessTokenAuthentication from posthog.clickhouse.kafka_engine import trim_quotes_expr from posthog.client import sync_execute @@ -20,7 +19,6 @@ from posthog.models.group_type_mapping import GroupTypeMapping from posthog.permissions import ( SharingTokenPermission, - TeamMemberAccessPermission, ) @@ -31,12 +29,10 @@ class Meta: read_only_fields = ["group_type", "group_type_index"] -class ClickhouseGroupsTypesView(StructuredViewSetMixin, mixins.ListModelMixin, viewsets.GenericViewSet): +class ClickhouseGroupsTypesView(TeamAndOrgViewSetMixin, mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = GroupTypeSerializer queryset = GroupTypeMapping.objects.all().order_by("group_type_index") pagination_class = None - permission_classes = [IsAuthenticated, TeamMemberAccessPermission] - sharing_enabled_actions = ["list"] def get_permissions(self): @@ -69,11 +65,10 @@ class Meta: fields = ["group_type_index", "group_key", "group_properties", "created_at"] -class ClickhouseGroupsView(StructuredViewSetMixin, mixins.ListModelMixin, viewsets.GenericViewSet): +class ClickhouseGroupsView(TeamAndOrgViewSetMixin, mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = GroupSerializer queryset = Group.objects.all() pagination_class = GroupCursorPagination - permission_classes = [IsAuthenticated, TeamMemberAccessPermission] def get_queryset(self): return ( diff --git a/ee/clickhouse/views/insights.py b/ee/clickhouse/views/insights.py index 8a284055229f0..ff772b71aaef8 100644 --- a/ee/clickhouse/views/insights.py +++ b/ee/clickhouse/views/insights.py @@ -27,8 +27,7 @@ def has_object_permission(self, request: Request, view, insight: Insight) -> boo class ClickhouseInsightsViewSet(InsightViewSet): - permission_classes = [*InsightViewSet.permission_classes, CanEditInsight] - + permission_classes = [CanEditInsight] retention_query_class = ClickhouseRetention stickiness_query_class = ClickhouseStickiness paths_query_class = ClickhousePaths diff --git a/ee/clickhouse/views/person.py b/ee/clickhouse/views/person.py index cbcd536126b55..d01dba65da928 100644 --- a/ee/clickhouse/views/person.py +++ b/ee/clickhouse/views/person.py @@ -62,4 +62,4 @@ def calculate_funnel_correlation_persons( class LegacyEnterprisePersonViewSet(EnterprisePersonViewSet): - legacy_team_compatibility = True + derive_current_team_from_user_only = True diff --git a/ee/session_recordings/session_recording_playlist.py b/ee/session_recordings/session_recording_playlist.py index 5b179c66dd178..5a7b2838cfff1 100644 --- a/ee/session_recordings/session_recording_playlist.py +++ b/ee/session_recordings/session_recording_playlist.py @@ -8,10 +8,9 @@ from rest_framework import request, response, serializers, viewsets from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied -from rest_framework.permissions import IsAuthenticated from posthog.api.forbid_destroy_model import ForbidDestroyModel -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer from posthog.constants import SESSION_RECORDINGS_FILTER_IDS, AvailableFeature from posthog.models import ( @@ -30,9 +29,6 @@ from posthog.models.filters.session_recordings_filter import SessionRecordingsFilter from posthog.models.team.team import check_is_feature_available_for_team from posthog.models.utils import UUIDT -from posthog.permissions import ( - TeamMemberAccessPermission, -) from posthog.rate_limit import ( ClickHouseBurstRateThrottle, ClickHouseSustainedRateThrottle, @@ -166,10 +162,9 @@ def _check_can_create_playlist(self, team: Team) -> bool: return True -class SessionRecordingPlaylistViewSet(StructuredViewSetMixin, ForbidDestroyModel, viewsets.ModelViewSet): +class SessionRecordingPlaylistViewSet(TeamAndOrgViewSetMixin, ForbidDestroyModel, viewsets.ModelViewSet): queryset = SessionRecordingPlaylist.objects.all() serializer_class = SessionRecordingPlaylistSerializer - permission_classes = [IsAuthenticated, TeamMemberAccessPermission] throttle_classes = [ClickHouseBurstRateThrottle, ClickHouseSustainedRateThrottle] filter_backends = [DjangoFilterBackend] filterset_fields = ["short_id", "created_by"] diff --git a/posthog/api/action.py b/posthog/api/action.py index c68796013af63..bcda749bc61d5 100644 --- a/posthog/api/action.py +++ b/posthog/api/action.py @@ -3,19 +3,16 @@ from dateutil.relativedelta import relativedelta from django.db.models import Count, Prefetch from django.utils.timezone import now -from rest_framework import authentication, request, serializers, viewsets +from rest_framework import request, serializers, viewsets from rest_framework.decorators import action -from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework_csv import renderers as csvrenderers -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer from posthog.api.utils import get_target_entity from posthog.auth import ( - JwtAuthentication, - PersonalAPIKeyAuthentication, TemporaryTokenAuthentication, ) from posthog.client import sync_execute @@ -24,9 +21,6 @@ from posthog.hogql.hogql import HogQLContext from posthog.models import Action, ActionStep, Filter, Person from posthog.models.action.util import format_action_filter -from posthog.permissions import ( - TeamMemberAccessPermission, -) from posthog.queries.trends.trends_actors import TrendsActors from .forbid_destroy_model import ForbidDestroyModel @@ -175,21 +169,14 @@ def update(self, instance: Any, validated_data: Dict[str, Any]) -> Any: class ActionViewSet( TaggedItemViewSetMixin, - StructuredViewSetMixin, + TeamAndOrgViewSetMixin, ForbidDestroyModel, viewsets.ModelViewSet, ): renderer_classes = tuple(api_settings.DEFAULT_RENDERER_CLASSES) + (csvrenderers.PaginatedCSVRenderer,) queryset = Action.objects.all() serializer_class = ActionSerializer - authentication_classes = [ - TemporaryTokenAuthentication, - JwtAuthentication, - PersonalAPIKeyAuthentication, - authentication.SessionAuthentication, - authentication.BasicAuthentication, - ] - permission_classes = [IsAuthenticated, TeamMemberAccessPermission] + authentication_classes = [TemporaryTokenAuthentication] ordering = ["-last_calculated_at", "name"] def get_queryset(self): diff --git a/posthog/api/activity_log.py b/posthog/api/activity_log.py index 5408b0b6fbfc2..dfb4d18b0b72d 100644 --- a/posthog/api/activity_log.py +++ b/posthog/api/activity_log.py @@ -9,7 +9,7 @@ from rest_framework.request import Request from rest_framework.response import Response -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer from posthog.models import ActivityLog, FeatureFlag, Insight, NotificationViewed, User from posthog.models.comment import Comment @@ -71,7 +71,7 @@ def get_all_timings(cls): return cls.timings_dict -class ActivityLogViewSet(StructuredViewSetMixin, viewsets.GenericViewSet, mixins.ListModelMixin): +class ActivityLogViewSet(TeamAndOrgViewSetMixin, viewsets.GenericViewSet, mixins.ListModelMixin): queryset = ActivityLog.objects.all() serializer_class = ActivityLogSerializer pagination_class = ActivityLogPagination diff --git a/posthog/api/annotation.py b/posthog/api/annotation.py index 300e75de73e11..c98a13cefd0f7 100644 --- a/posthog/api/annotation.py +++ b/posthog/api/annotation.py @@ -4,16 +4,12 @@ from django.db.models.signals import post_save from django.dispatch import receiver from rest_framework import filters, serializers, viewsets, pagination -from rest_framework.permissions import IsAuthenticated from posthog.api.forbid_destroy_model import ForbidDestroyModel -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer from posthog.event_usage import report_user_action from posthog.models import Annotation -from posthog.permissions import ( - TeamMemberAccessPermission, -) class AnnotationSerializer(serializers.ModelSerializer): @@ -64,14 +60,13 @@ class AnnotationsLimitOffsetPagination(pagination.LimitOffsetPagination): default_limit = 1000 -class AnnotationsViewSet(StructuredViewSetMixin, ForbidDestroyModel, viewsets.ModelViewSet): +class AnnotationsViewSet(TeamAndOrgViewSetMixin, ForbidDestroyModel, viewsets.ModelViewSet): """ Create, Read, Update and Delete annotations. [See docs](https://posthog.com/docs/user-guides/annotations) for more information on annotations. """ queryset = Annotation.objects.select_related("dashboard_item") serializer_class = AnnotationSerializer - permission_classes = [IsAuthenticated, TeamMemberAccessPermission] filter_backends = [filters.SearchFilter] pagination_class = AnnotationsLimitOffsetPagination search_fields = ["content"] diff --git a/posthog/api/app_metrics.py b/posthog/api/app_metrics.py index a92cc6a6efbd2..b91de61540535 100644 --- a/posthog/api/app_metrics.py +++ b/posthog/api/app_metrics.py @@ -4,7 +4,7 @@ from rest_framework import mixins, request, response, viewsets from rest_framework.decorators import action -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.models.plugin import PluginConfig from posthog.queries.app_metrics.app_metrics import ( AppMetricsErrorDetailsQuery, @@ -21,7 +21,7 @@ ) -class AppMetricsViewSet(StructuredViewSetMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet): +class AppMetricsViewSet(TeamAndOrgViewSetMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet): queryset = PluginConfig.objects.all() def retrieve(self, request: request.Request, *args: Any, **kwargs: Any) -> response.Response: @@ -75,7 +75,7 @@ def error_details(self, request: request.Request, *args: Any, **kwargs: Any) -> class HistoricalExportsAppMetricsViewSet( - StructuredViewSetMixin, + TeamAndOrgViewSetMixin, mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.ViewSet, diff --git a/posthog/api/async_migration.py b/posthog/api/async_migration.py index 5892151ca98a3..11a8daaf96a01 100644 --- a/posthog/api/async_migration.py +++ b/posthog/api/async_migration.py @@ -1,9 +1,8 @@ import structlog -from rest_framework import permissions, response, serializers, viewsets +from rest_framework import response, serializers, viewsets from rest_framework.decorators import action from semantic_version.base import Version -from posthog.api.routing import StructuredViewSetMixin from posthog.async_migrations.runner import ( MAX_CONCURRENT_ASYNC_MIGRATIONS, is_posthog_version_compatible, @@ -23,6 +22,7 @@ ) from posthog.models.instance_setting import get_instance_setting from posthog.permissions import IsStaffUser +from rest_framework.permissions import IsAuthenticated logger = structlog.get_logger(__name__) @@ -96,9 +96,9 @@ def get_is_available(self, async_migration: AsyncMigration): ) -class AsyncMigrationsViewset(StructuredViewSetMixin, viewsets.ModelViewSet): +class AsyncMigrationsViewset(viewsets.ModelViewSet): queryset = AsyncMigration.objects.all().order_by("name") - permission_classes = [permissions.IsAuthenticated, IsStaffUser] + permission_classes = [IsAuthenticated, IsStaffUser] serializer_class = AsyncMigrationSerializer include_in_docs = False diff --git a/posthog/api/cohort.py b/posthog/api/cohort.py index 8be3c0e528cce..86abf8c4f37a0 100644 --- a/posthog/api/cohort.py +++ b/posthog/api/cohort.py @@ -27,7 +27,6 @@ from rest_framework import serializers, viewsets from rest_framework.decorators import action from rest_framework.exceptions import ValidationError -from rest_framework.permissions import IsAuthenticated from rest_framework.request import Request from rest_framework.response import Response from rest_framework.settings import api_settings @@ -36,7 +35,7 @@ from posthog.api.forbid_destroy_model import ForbidDestroyModel from posthog.api.person import get_funnel_actor_class -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer from posthog.api.utils import get_target_entity from posthog.client import sync_execute @@ -65,7 +64,6 @@ INSERT_COHORT_ALL_PEOPLE_THROUGH_PERSON_ID, PERSON_STATIC_COHORT_TABLE, ) -from posthog.permissions import TeamMemberAccessPermission from posthog.queries.actor_base_query import ( ActorBaseQuery, get_people, @@ -284,10 +282,9 @@ def to_representation(self, instance): return representation -class CohortViewSet(StructuredViewSetMixin, ForbidDestroyModel, viewsets.ModelViewSet): +class CohortViewSet(TeamAndOrgViewSetMixin, ForbidDestroyModel, viewsets.ModelViewSet): queryset = Cohort.objects.all() serializer_class = CohortSerializer - permission_classes = [IsAuthenticated, TeamMemberAccessPermission] def get_queryset(self) -> QuerySet: queryset = super().get_queryset() @@ -436,7 +433,7 @@ def persons(self, request: Request, **kwargs) -> Response: class LegacyCohortViewSet(CohortViewSet): - legacy_team_compatibility = True + derive_current_team_from_user_only = True def will_create_loops(cohort: Cohort) -> bool: diff --git a/posthog/api/comments.py b/posthog/api/comments.py index 9b8a4f280142c..977dfdd9101cf 100644 --- a/posthog/api/comments.py +++ b/posthog/api/comments.py @@ -9,7 +9,7 @@ from posthog.api.forbid_destroy_model import ForbidDestroyModel -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer from posthog.models.comment import Comment @@ -64,7 +64,7 @@ class CommentPagination(pagination.CursorPagination): page_size = 100 -class CommentViewSet(StructuredViewSetMixin, ForbidDestroyModel, viewsets.ModelViewSet): +class CommentViewSet(TeamAndOrgViewSetMixin, ForbidDestroyModel, viewsets.ModelViewSet): queryset = Comment.objects.all() serializer_class = CommentSerializer pagination_class = CommentPagination diff --git a/posthog/api/dashboards/dashboard.py b/posthog/api/dashboards/dashboard.py index 4101ce3559d7f..70b4b9c6a60ca 100644 --- a/posthog/api/dashboards/dashboard.py +++ b/posthog/api/dashboards/dashboard.py @@ -9,7 +9,7 @@ from rest_framework import exceptions, serializers, viewsets from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied -from rest_framework.permissions import SAFE_METHODS, BasePermission, IsAuthenticated +from rest_framework.permissions import SAFE_METHODS, BasePermission from rest_framework.request import Request from rest_framework.response import Response from rest_framework.utils.serializer_helpers import ReturnDict @@ -19,7 +19,7 @@ ) from posthog.api.forbid_destroy_model import ForbidDestroyModel from posthog.api.insight import InsightSerializer, InsightViewSet -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer from posthog.api.tagged_item import TaggedItemSerializerMixin, TaggedItemViewSetMixin from posthog.constants import AvailableFeature @@ -31,7 +31,6 @@ from posthog.models.tagged_item import TaggedItem from posthog.models.team.team import check_is_feature_available_for_team from posthog.models.user import User -from posthog.permissions import TeamMemberAccessPermission from posthog.user_permissions import UserPermissionsSerializerMixin logger = structlog.get_logger(__name__) @@ -404,16 +403,12 @@ def _update_creation_mode(self, validated_data, use_template: str, use_dashboard class DashboardsViewSet( TaggedItemViewSetMixin, - StructuredViewSetMixin, + TeamAndOrgViewSetMixin, ForbidDestroyModel, viewsets.ModelViewSet, ): queryset = Dashboard.objects.order_by("name") - permission_classes = [ - IsAuthenticated, - TeamMemberAccessPermission, - CanEditDashboard, - ] + permission_classes = [CanEditDashboard] def get_serializer_class(self) -> Type[BaseSerializer]: return DashboardBasicSerializer if self.action == "list" else DashboardSerializer @@ -522,7 +517,7 @@ def create_from_template_json(self, request: Request, *args: Any, **kwargs: Any) class LegacyDashboardsViewSet(DashboardsViewSet): - legacy_team_compatibility = True + derive_current_team_from_user_only = True def get_parents_query_dict(self) -> Dict[str, Any]: if not self.request.user.is_authenticated or "share_token" in self.request.GET: @@ -531,4 +526,4 @@ def get_parents_query_dict(self) -> Dict[str, Any]: class LegacyInsightViewSet(InsightViewSet): - legacy_team_compatibility = True + derive_current_team_from_user_only = True diff --git a/posthog/api/dashboards/dashboard_templates.py b/posthog/api/dashboards/dashboard_templates.py index 70f8985e41cc4..330fb10565171 100644 --- a/posthog/api/dashboards/dashboard_templates.py +++ b/posthog/api/dashboards/dashboard_templates.py @@ -9,13 +9,12 @@ from rest_framework import request, response, serializers, viewsets from rest_framework.decorators import action from rest_framework.exceptions import ValidationError -from rest_framework.permissions import SAFE_METHODS, BasePermission, IsAuthenticated +from rest_framework.permissions import SAFE_METHODS, BasePermission from rest_framework.request import Request from posthog.api.forbid_destroy_model import ForbidDestroyModel -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.models.dashboard_templates import DashboardTemplate -from posthog.permissions import TeamMemberAccessPermission logger = structlog.get_logger(__name__) @@ -70,12 +69,8 @@ def update(self, instance: DashboardTemplate, validated_data: Dict, *args, **kwa return super().update(instance, validated_data, *args, **kwargs) -class DashboardTemplateViewSet(StructuredViewSetMixin, ForbidDestroyModel, viewsets.ModelViewSet): - permission_classes = [ - IsAuthenticated, - TeamMemberAccessPermission, - OnlyStaffCanEditDashboardTemplate, - ] +class DashboardTemplateViewSet(TeamAndOrgViewSetMixin, ForbidDestroyModel, viewsets.ModelViewSet): + permission_classes = [OnlyStaffCanEditDashboardTemplate] serializer_class = DashboardTemplateSerializer @method_decorator(cache_page(60 * 2)) # cache for 2 minutes diff --git a/posthog/api/data_management.py b/posthog/api/data_management.py index 186f2f83021f7..b2782a985ab1c 100644 --- a/posthog/api/data_management.py +++ b/posthog/api/data_management.py @@ -1,7 +1,7 @@ from rest_framework import viewsets from rest_framework.decorators import action -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.models.activity_logging.activity_page import activity_page_response from posthog.models.activity_logging.activity_log import ( load_all_activity, @@ -9,7 +9,7 @@ from rest_framework import request -class DataManagementViewSet(StructuredViewSetMixin, viewsets.GenericViewSet): +class DataManagementViewSet(TeamAndOrgViewSetMixin, viewsets.GenericViewSet): @action(methods=["GET"], url_path="activity", detail=False) def all_activity(self, request: request.Request, **kwargs): limit = int(request.query_params.get("limit", "10")) diff --git a/posthog/api/documentation.py b/posthog/api/documentation.py index 8bb076235cdb3..b221fa18936c8 100644 --- a/posthog/api/documentation.py +++ b/posthog/api/documentation.py @@ -170,7 +170,10 @@ def preprocess_exclude_path_format(endpoints, **kwargs): """ result = [] for path, path_regex, method, callback in endpoints: - if hasattr(callback.cls, "legacy_team_compatibility") and callback.cls.legacy_team_compatibility: + if ( + hasattr(callback.cls, "derive_current_team_from_user_only") + and callback.cls.derive_current_team_from_user_only + ): pass elif hasattr(callback.cls, "include_in_docs") and callback.cls.include_in_docs: path = path.replace("{parent_lookup_team_id}", "{project_id}") diff --git a/posthog/api/early_access_feature.py b/posthog/api/early_access_feature.py index 2827243b9029c..9886e2e534cac 100644 --- a/posthog/api/early_access_feature.py +++ b/posthog/api/early_access_feature.py @@ -3,21 +3,17 @@ from django.http import JsonResponse from rest_framework.response import Response from posthog.api.feature_flag import FeatureFlagSerializer, MinimalFeatureFlagSerializer -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.utils import get_token from posthog.exceptions import generate_exception_response from posthog.models.early_access_feature import EarlyAccessFeature from rest_framework import serializers, viewsets -from rest_framework.permissions import IsAuthenticated from rest_framework.request import Request from rest_framework import status from posthog.models.feature_flag.feature_flag import FeatureFlag from posthog.models.team.team import Team -from posthog.permissions import ( - TeamMemberAccessPermission, -) from django.utils.text import slugify from django.views.decorators.csrf import csrf_exempt @@ -221,9 +217,8 @@ def create(self, validated_data): return feature -class EarlyAccessFeatureViewSet(StructuredViewSetMixin, viewsets.ModelViewSet): +class EarlyAccessFeatureViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet): queryset = EarlyAccessFeature.objects.select_related("feature_flag").all() - permission_classes = [IsAuthenticated, TeamMemberAccessPermission] def get_serializer_class(self) -> Type[serializers.Serializer]: if self.request.method == "POST": diff --git a/posthog/api/element.py b/posthog/api/element.py index be87cb90f51dc..32155b24c4264 100644 --- a/posthog/api/element.py +++ b/posthog/api/element.py @@ -1,20 +1,18 @@ from typing import Literal, Tuple -from rest_framework import authentication, request, response, serializers, viewsets +from rest_framework import request, response, serializers, viewsets from rest_framework.decorators import action from rest_framework.exceptions import ValidationError -from rest_framework.permissions import IsAuthenticated from statshog.defaults.django import statsd -from posthog.api.routing import StructuredViewSetMixin -from posthog.auth import PersonalAPIKeyAuthentication, TemporaryTokenAuthentication +from posthog.api.routing import TeamAndOrgViewSetMixin +from posthog.auth import TemporaryTokenAuthentication from posthog.client import sync_execute from posthog.models import Element, Filter from posthog.models.element.element import chain_to_elements from posthog.models.element.sql import GET_ELEMENTS, GET_VALUES from posthog.models.instance_setting import get_instance_setting from posthog.models.property.util import parse_prop_grouped_clauses -from posthog.permissions import TeamMemberAccessPermission from posthog.queries.query_date_range import QueryDateRange from posthog.utils import format_query_params_absolute_url @@ -35,18 +33,12 @@ class Meta: ] -class ElementViewSet(StructuredViewSetMixin, viewsets.ModelViewSet): +class ElementViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet): filter_rewrite_rules = {"team_id": "group__team_id"} queryset = Element.objects.all() serializer_class = ElementSerializer - authentication_classes = [ - TemporaryTokenAuthentication, - PersonalAPIKeyAuthentication, - authentication.SessionAuthentication, - authentication.BasicAuthentication, - ] - permission_classes = [IsAuthenticated, TeamMemberAccessPermission] + authentication_classes = [TemporaryTokenAuthentication] include_in_docs = False @action(methods=["GET"], detail=False) @@ -187,4 +179,4 @@ def values(self, request: request.Request, **kwargs) -> response.Response: class LegacyElementViewSet(ElementViewSet): - legacy_team_compatibility = True + derive_current_team_from_user_only = True diff --git a/posthog/api/event.py b/posthog/api/event.py index 323229a64af22..e6aeabc6a4678 100644 --- a/posthog/api/event.py +++ b/posthog/api/event.py @@ -10,13 +10,12 @@ from rest_framework.decorators import action from rest_framework.exceptions import NotFound from rest_framework.pagination import LimitOffsetPagination -from rest_framework.permissions import IsAuthenticated from rest_framework.settings import api_settings from rest_framework_csv import renderers as csvrenderers from sentry_sdk import capture_exception from posthog.api.documentation import PropertiesSerializer, extend_schema -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.client import query_with_columns, sync_execute from posthog.hogql.constants import DEFAULT_RETURNED_ROWS, MAX_SELECT_RETURNED_ROWS from posthog.models import Element, Filter, Person @@ -26,9 +25,6 @@ from posthog.models.person.util import get_persons_by_distinct_ids from posthog.models.team import Team from posthog.models.utils import UUIDT -from posthog.permissions import ( - TeamMemberAccessPermission, -) from posthog.queries.property_values import get_property_values_for_key from posthog.rate_limit import ( ClickHouseBurstRateThrottle, @@ -83,14 +79,13 @@ def get_paginated_response_schema(self, schema): class EventViewSet( - StructuredViewSetMixin, + TeamAndOrgViewSetMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet, ): renderer_classes = tuple(api_settings.DEFAULT_RENDERER_CLASSES) + (csvrenderers.PaginatedCSVRenderer,) serializer_class = ClickhouseEventSerializer - permission_classes = [IsAuthenticated, TeamMemberAccessPermission] throttle_classes = [ClickHouseBurstRateThrottle, ClickHouseSustainedRateThrottle] pagination_class = UncountedLimitOffsetPagination @@ -286,4 +281,4 @@ def values(self, request: request.Request, **kwargs) -> response.Response: class LegacyEventViewSet(EventViewSet): - legacy_team_compatibility = True + derive_current_team_from_user_only = True diff --git a/posthog/api/event_definition.py b/posthog/api/event_definition.py index c1c8bcfe5b808..a5510d0d02abc 100644 --- a/posthog/api/event_definition.py +++ b/posthog/api/event_definition.py @@ -3,7 +3,6 @@ from django.db.models import Manager, Prefetch from rest_framework import ( mixins, - permissions, serializers, viewsets, request, @@ -11,7 +10,7 @@ response, ) -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer from posthog.api.tagged_item import TaggedItemSerializerMixin, TaggedItemViewSetMixin from posthog.api.utils import create_event_definitions_sql @@ -23,10 +22,6 @@ from posthog.models.activity_logging.activity_log import Detail, log_activity from posthog.models.user import User from posthog.models.utils import UUIDT -from posthog.permissions import ( - OrganizationMemberPermissions, - TeamMemberAccessPermission, -) from posthog.settings import EE_AVAILABLE from loginas.utils import is_impersonated_session @@ -69,7 +64,7 @@ def get_is_action(self, obj): class EventDefinitionViewSet( TaggedItemViewSetMixin, - StructuredViewSetMixin, + TeamAndOrgViewSetMixin, mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, @@ -77,11 +72,6 @@ class EventDefinitionViewSet( viewsets.GenericViewSet, ): serializer_class = EventDefinitionSerializer - permission_classes = [ - permissions.IsAuthenticated, - OrganizationMemberPermissions, - TeamMemberAccessPermission, - ] lookup_field = "id" filter_backends = [TermSearchFilterBackend] diff --git a/posthog/api/exports.py b/posthog/api/exports.py index e5fbc85fbb219..ee6d96ab09d14 100644 --- a/posthog/api/exports.py +++ b/posthog/api/exports.py @@ -5,21 +5,15 @@ from django.http import HttpResponse from django.utils.timezone import now from rest_framework import mixins, serializers, viewsets -from rest_framework.authentication import BasicAuthentication, SessionAuthentication from rest_framework.decorators import action from rest_framework.exceptions import ValidationError -from rest_framework.permissions import IsAuthenticated from rest_framework.request import Request -from posthog.api.routing import StructuredViewSetMixin -from posthog.auth import PersonalAPIKeyAuthentication +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.event_usage import report_user_action from posthog.models import Insight, User from posthog.models.activity_logging.activity_log import Change, Detail, log_activity from posthog.models.exported_asset import ExportedAsset, get_content_response -from posthog.permissions import ( - TeamMemberAccessPermission, -) from posthog.tasks import exporter from loginas.utils import is_impersonated_session @@ -132,21 +126,14 @@ def _create_asset( class ExportedAssetViewSet( + TeamAndOrgViewSetMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin, - StructuredViewSetMixin, viewsets.GenericViewSet, ): queryset = ExportedAsset.objects.order_by("-created_at") serializer_class = ExportedAssetSerializer - authentication_classes = [ - PersonalAPIKeyAuthentication, - SessionAuthentication, - BasicAuthentication, - ] - permission_classes = [IsAuthenticated, TeamMemberAccessPermission] - # TODO: This should be removed as it is only used by frontend exporter and can instead use the api/sharing.py endpoint @action(methods=["GET"], detail=True) def content(self, request: Request, *args: Any, **kwargs: Any) -> HttpResponse: diff --git a/posthog/api/feature_flag.py b/posthog/api/feature_flag.py index dc97ed65980bc..4a48cf43eadc9 100644 --- a/posthog/api/feature_flag.py +++ b/posthog/api/feature_flag.py @@ -5,7 +5,6 @@ from django.db.models import QuerySet, Q, deletion from django.conf import settings from rest_framework import ( - authentication, exceptions, request, serializers, @@ -13,18 +12,18 @@ viewsets, ) from rest_framework.decorators import action -from rest_framework.permissions import SAFE_METHODS, BasePermission, IsAuthenticated +from rest_framework.permissions import SAFE_METHODS, BasePermission from rest_framework.request import Request from rest_framework.response import Response from sentry_sdk import capture_exception from posthog.api.cohort import CohortSerializer from posthog.api.forbid_destroy_model import ForbidDestroyModel -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer from posthog.api.tagged_item import TaggedItemSerializerMixin, TaggedItemViewSetMixin from posthog.api.dashboards.dashboard import Dashboard -from posthog.auth import PersonalAPIKeyAuthentication, TemporaryTokenAuthentication +from posthog.auth import TemporaryTokenAuthentication from posthog.constants import FlagRequestType from posthog.event_usage import report_user_action from posthog.helpers.dashboard_templates import ( @@ -50,9 +49,6 @@ from posthog.models.feedback.survey import Survey from posthog.models.group_type_mapping import GroupTypeMapping from posthog.models.property import Property -from posthog.permissions import ( - TeamMemberAccessPermission, -) from posthog.queries.base import ( determine_parsed_date_for_property_matching, ) @@ -362,8 +358,8 @@ class Meta: class FeatureFlagViewSet( + TeamAndOrgViewSetMixin, TaggedItemViewSetMixin, - StructuredViewSetMixin, ForbidDestroyModel, viewsets.ModelViewSet, ): @@ -375,16 +371,9 @@ class FeatureFlagViewSet( queryset = FeatureFlag.objects.all() serializer_class = FeatureFlagSerializer - permission_classes = [ - IsAuthenticated, - TeamMemberAccessPermission, - CanEditFeatureFlag, - ] + permission_classes = [CanEditFeatureFlag] authentication_classes = [ - PersonalAPIKeyAuthentication, TemporaryTokenAuthentication, # Allows endpoint to be called from the Toolbar - authentication.SessionAuthentication, - authentication.BasicAuthentication, ] def get_queryset(self) -> QuerySet: @@ -736,4 +725,4 @@ def perform_update(self, serializer): class LegacyFeatureFlagViewSet(FeatureFlagViewSet): - legacy_team_compatibility = True + derive_current_team_from_user_only = True diff --git a/posthog/api/ingestion_warnings.py b/posthog/api/ingestion_warnings.py index 827567e5c8cb8..dc82c0b941812 100644 --- a/posthog/api/ingestion_warnings.py +++ b/posthog/api/ingestion_warnings.py @@ -6,11 +6,11 @@ from rest_framework.request import Request from rest_framework.response import Response -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.client import sync_execute -class IngestionWarningsViewSet(StructuredViewSetMixin, viewsets.ViewSet): +class IngestionWarningsViewSet(TeamAndOrgViewSetMixin, viewsets.ViewSet): def list(self, request: Request, **kw) -> Response: start_date = now() - timedelta(days=30) warning_events = sync_execute( diff --git a/posthog/api/insight.py b/posthog/api/insight.py index 8b28bab7b3fce..aa114aa23b9e3 100644 --- a/posthog/api/insight.py +++ b/posthog/api/insight.py @@ -16,7 +16,6 @@ from rest_framework.decorators import action from rest_framework.exceptions import ParseError, PermissionDenied, ValidationError from rest_framework.parsers import JSONParser -from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework_csv import renderers as csvrenderers @@ -32,7 +31,7 @@ TrendSerializer, ) from posthog.clickhouse.cancel import cancel_query_on_cluster -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer from posthog.api.tagged_item import TaggedItemSerializerMixin, TaggedItemViewSetMixin from posthog.api.utils import format_paginated_url @@ -78,7 +77,6 @@ from posthog.models.filters.stickiness_filter import StickinessFilter from posthog.models.insight import InsightViewed from posthog.models.utils import UUIDT -from posthog.permissions import TeamMemberAccessPermission from posthog.queries.funnels import ( ClickhouseFunnelTimeToConvert, ClickhouseFunnelTrends, @@ -564,12 +562,11 @@ def dashboard_tile_from_context(self, insight: Insight, dashboard: Optional[Dash class InsightViewSet( TaggedItemViewSetMixin, - StructuredViewSetMixin, + TeamAndOrgViewSetMixin, ForbidDestroyModel, viewsets.ModelViewSet, ): serializer_class = InsightSerializer - permission_classes = [IsAuthenticated, TeamMemberAccessPermission] throttle_classes = [ ClickHouseBurstRateThrottle, ClickHouseSustainedRateThrottle, @@ -1083,4 +1080,4 @@ def timing(self, request: request.Request, **kwargs): class LegacyInsightViewSet(InsightViewSet): - legacy_team_compatibility = True + derive_current_team_from_user_only = True diff --git a/posthog/api/integration.py b/posthog/api/integration.py index 5d513b82bbd84..a6319eed10312 100644 --- a/posthog/api/integration.py +++ b/posthog/api/integration.py @@ -1,18 +1,14 @@ from typing import Any from rest_framework import mixins, serializers, viewsets -from rest_framework.authentication import BasicAuthentication, SessionAuthentication from rest_framework.decorators import action from rest_framework.exceptions import ValidationError -from rest_framework.permissions import IsAuthenticated from rest_framework.request import Request from rest_framework.response import Response -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer -from posthog.auth import PersonalAPIKeyAuthentication from posthog.models.integration import Integration, SlackIntegration -from posthog.permissions import TeamMemberAccessPermission class IntegrationSerializer(serializers.ModelSerializer): @@ -49,19 +45,12 @@ class IntegrationViewSet( mixins.RetrieveModelMixin, mixins.ListModelMixin, mixins.DestroyModelMixin, - StructuredViewSetMixin, + TeamAndOrgViewSetMixin, viewsets.GenericViewSet, ): queryset = Integration.objects.all() serializer_class = IntegrationSerializer - authentication_classes = [ - PersonalAPIKeyAuthentication, - SessionAuthentication, - BasicAuthentication, - ] - permission_classes = [IsAuthenticated, TeamMemberAccessPermission] - @action(methods=["GET"], detail=True, url_path="channels") def content(self, request: Request, *args: Any, **kwargs: Any) -> Response: instance = self.get_object() diff --git a/posthog/api/notebook.py b/posthog/api/notebook.py index 1b0a5045a6728..3bc04cf46aa00 100644 --- a/posthog/api/notebook.py +++ b/posthog/api/notebook.py @@ -16,11 +16,10 @@ from rest_framework.request import Request from rest_framework.response import Response from rest_framework.decorators import action -from rest_framework.permissions import IsAuthenticated from rest_framework.serializers import BaseSerializer from posthog.api.forbid_destroy_model import ForbidDestroyModel -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer from posthog.exceptions import Conflict from posthog.models import User @@ -34,7 +33,6 @@ from posthog.models.activity_logging.activity_page import activity_page_response from posthog.models.notebook.notebook import Notebook from posthog.models.utils import UUIDT -from posthog.permissions import TeamMemberAccessPermission from posthog.settings import DEBUG from posthog.utils import relative_date_parse from loginas.utils import is_impersonated_session @@ -236,9 +234,8 @@ def update(self, instance: Notebook, validated_data: Dict, **kwargs) -> Notebook ], ) ) -class NotebookViewSet(StructuredViewSetMixin, ForbidDestroyModel, viewsets.ModelViewSet): +class NotebookViewSet(TeamAndOrgViewSetMixin, ForbidDestroyModel, viewsets.ModelViewSet): queryset = Notebook.objects.all() - permission_classes = [IsAuthenticated, TeamMemberAccessPermission] filter_backends = [DjangoFilterBackend] filterset_fields = ["short_id"] # TODO: Remove this once we have released notebooks diff --git a/posthog/api/organization.py b/posthog/api/organization.py index 8ee366c906014..753969de61e69 100644 --- a/posthog/api/organization.py +++ b/posthog/api/organization.py @@ -6,6 +6,7 @@ from rest_framework.request import Request from posthog import settings +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import TeamBasicSerializer from posthog.cloud_utils import is_cloud from posthog.constants import AvailableFeature @@ -18,7 +19,6 @@ from posthog.permissions import ( CREATE_METHODS, OrganizationAdminWritePermissions, - OrganizationMemberPermissions, extract_organization, ) from posthog.user_permissions import UserPermissions, UserPermissionsSerializerMixin @@ -123,13 +123,9 @@ def get_metadata(self, instance: Organization) -> Dict[str, Union[str, int, obje } -class OrganizationViewSet(viewsets.ModelViewSet): +class OrganizationViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet): serializer_class = OrganizationSerializer - permission_classes = [ - permissions.IsAuthenticated, - OrganizationMemberPermissions, - OrganizationPermissionsWithDelete, - ] + permission_classes = [OrganizationPermissionsWithDelete] queryset = Organization.objects.none() lookup_field = "id" ordering = "-created_by" @@ -137,7 +133,7 @@ class OrganizationViewSet(viewsets.ModelViewSet): def get_permissions(self): if self.request.method == "POST": # Cannot use `OrganizationMemberPermissions` or `OrganizationAdminWritePermissions` - # because they require an existing org, unneeded anyway because permissions are organization-based + # because they require an existing org, unneeded anyways because permissions are organization-based return [ permission() for permission in [ diff --git a/posthog/api/organization_domain.py b/posthog/api/organization_domain.py index bcf30b4eac798..c67c0317c7922 100644 --- a/posthog/api/organization_domain.py +++ b/posthog/api/organization_domain.py @@ -3,16 +3,12 @@ from rest_framework import exceptions, request, response, serializers from rest_framework.decorators import action -from rest_framework.permissions import IsAuthenticated from rest_framework.viewsets import ModelViewSet -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.cloud_utils import is_cloud from posthog.models import OrganizationDomain -from posthog.permissions import ( - OrganizationAdminWritePermissions, - OrganizationMemberPermissions, -) +from posthog.permissions import OrganizationAdminWritePermissions DOMAIN_REGEX = r"^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$" @@ -74,13 +70,9 @@ def validate(self, attrs: Dict[str, Any]) -> Dict[str, Any]: return attrs -class OrganizationDomainViewset(StructuredViewSetMixin, ModelViewSet): +class OrganizationDomainViewset(TeamAndOrgViewSetMixin, ModelViewSet): serializer_class = OrganizationDomainSerializer - permission_classes = [ - IsAuthenticated, - OrganizationMemberPermissions, - OrganizationAdminWritePermissions, - ] + permission_classes = [OrganizationAdminWritePermissions] queryset = OrganizationDomain.objects.all() def get_queryset(self): diff --git a/posthog/api/organization_feature_flag.py b/posthog/api/organization_feature_flag.py index f2cdf2098620e..543ed6c692c27 100644 --- a/posthog/api/organization_feature_flag.py +++ b/posthog/api/organization_feature_flag.py @@ -1,7 +1,6 @@ from typing import Dict from django.core.exceptions import ObjectDoesNotExist from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated from rest_framework.decorators import action from rest_framework import ( mixins, @@ -9,26 +8,24 @@ status, ) from posthog.api.cohort import CohortSerializer -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.feature_flag import FeatureFlagSerializer from posthog.api.feature_flag import CanEditFeatureFlag from posthog.api.shared import UserBasicSerializer from posthog.models import FeatureFlag, Team from posthog.models.cohort import Cohort, CohortOrEmpty from posthog.models.filters.filter import Filter -from posthog.permissions import OrganizationMemberPermissions class OrganizationFeatureFlagView( + TeamAndOrgViewSetMixin, viewsets.ViewSet, - StructuredViewSetMixin, mixins.RetrieveModelMixin, ): """ Retrieves all feature flags for a given organization and key. """ - permission_classes = [IsAuthenticated, OrganizationMemberPermissions] lookup_field = "feature_flag_key" def retrieve(self, request, *args, **kwargs): diff --git a/posthog/api/organization_invite.py b/posthog/api/organization_invite.py index 17452b3e994e3..6860c5f875104 100644 --- a/posthog/api/organization_invite.py +++ b/posthog/api/organization_invite.py @@ -10,16 +10,14 @@ viewsets, ) from rest_framework.decorators import action -from rest_framework.permissions import IsAuthenticated -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer from posthog.email import is_email_available from posthog.event_usage import report_bulk_invited, report_team_member_invited from posthog.models import OrganizationInvite, OrganizationMembership from posthog.models.organization import Organization from posthog.models.user import User -from posthog.permissions import OrganizationMemberPermissions from posthog.tasks.email import send_invite @@ -79,14 +77,13 @@ def create(self, validated_data: Dict[str, Any], *args: Any, **kwargs: Any) -> O class OrganizationInviteViewSet( - StructuredViewSetMixin, + TeamAndOrgViewSetMixin, mixins.DestroyModelMixin, mixins.CreateModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet, ): serializer_class = OrganizationInviteSerializer - permission_classes = [IsAuthenticated, OrganizationMemberPermissions] queryset = OrganizationInvite.objects.all() lookup_field = "id" ordering = "-created_at" diff --git a/posthog/api/organization_member.py b/posthog/api/organization_member.py index 11f3c08e30411..94fd64aa73d60 100644 --- a/posthog/api/organization_member.py +++ b/posthog/api/organization_member.py @@ -4,17 +4,17 @@ from django.shortcuts import get_object_or_404 from django_otp.plugins.otp_totp.models import TOTPDevice from rest_framework import exceptions, mixins, serializers, viewsets -from rest_framework.permissions import SAFE_METHODS, BasePermission, IsAuthenticated +from rest_framework.permissions import SAFE_METHODS, BasePermission from rest_framework.request import Request from rest_framework.serializers import raise_errors_on_nested_writes from social_django.admin import UserSocialAuth -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer from posthog.constants import INTERNAL_BOT_EMAIL_SUFFIX from posthog.models import OrganizationMembership from posthog.models.user import User -from posthog.permissions import OrganizationMemberPermissions, extract_organization +from posthog.permissions import extract_organization class OrganizationMemberObjectPermissions(BasePermission): @@ -79,18 +79,14 @@ def update(self, updated_membership, validated_data, **kwargs): class OrganizationMemberViewSet( - StructuredViewSetMixin, + TeamAndOrgViewSetMixin, mixins.DestroyModelMixin, mixins.UpdateModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet, ): serializer_class = OrganizationMemberSerializer - permission_classes = [ - IsAuthenticated, - OrganizationMemberPermissions, - OrganizationMemberObjectPermissions, - ] + permission_classes = [OrganizationMemberObjectPermissions] queryset = ( OrganizationMembership.objects.order_by("user__first_name", "-joined_at") .exclude(user__email__endswith=INTERNAL_BOT_EMAIL_SUFFIX) diff --git a/posthog/api/person.py b/posthog/api/person.py index 7ced939f0f621..1e02714d90dbf 100644 --- a/posthog/api/person.py +++ b/posthog/api/person.py @@ -22,7 +22,6 @@ from rest_framework.decorators import action from rest_framework.exceptions import MethodNotAllowed, NotFound, ValidationError from rest_framework.pagination import LimitOffsetPagination -from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework_csv import renderers as csvrenderers @@ -30,7 +29,7 @@ from posthog.api.capture import capture_internal from posthog.api.documentation import PersonPropertiesSerializer, extend_schema -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.utils import format_paginated_url, get_pk_or_uuid, get_target_entity from posthog.constants import ( CSV_EXPORT_LIMIT, @@ -58,7 +57,6 @@ from posthog.models.filters.retention_filter import RetentionFilter from posthog.models.filters.stickiness_filter import StickinessFilter from posthog.models.person.util import delete_person -from posthog.permissions import TeamMemberAccessPermission from posthog.queries.actor_base_query import ( ActorBaseQuery, get_people, @@ -220,7 +218,7 @@ def get_funnel_actor_class(filter: Filter) -> Callable: return funnel_actor_class -class PersonViewSet(StructuredViewSetMixin, viewsets.ModelViewSet): +class PersonViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet): """ To create or update persons, use a PostHog library of your choice and [use an identify call](/docs/integrate/identifying-users). This API endpoint is only for reading and deleting. """ @@ -229,7 +227,6 @@ class PersonViewSet(StructuredViewSetMixin, viewsets.ModelViewSet): queryset = Person.objects.all() serializer_class = PersonSerializer pagination_class = PersonLimitOffsetPagination - permission_classes = [IsAuthenticated, TeamMemberAccessPermission] throttle_classes = [ClickHouseBurstRateThrottle, PersonsThrottle] lifecycle_class = Lifecycle retention_class = Retention @@ -960,4 +957,4 @@ def prepare_actor_query_filter(filter: T) -> T: class LegacyPersonViewSet(PersonViewSet): - legacy_team_compatibility = True + derive_current_team_from_user_only = True diff --git a/posthog/api/plugin.py b/posthog/api/plugin.py index 3eabe8ee187a8..f299091562b71 100644 --- a/posthog/api/plugin.py +++ b/posthog/api/plugin.py @@ -17,10 +17,10 @@ from rest_framework import renderers, request, serializers, status, viewsets from rest_framework.decorators import action, renderer_classes from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError -from rest_framework.permissions import SAFE_METHODS, BasePermission, IsAuthenticated +from rest_framework.permissions import SAFE_METHODS, BasePermission from rest_framework.response import Response -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.models import Plugin, PluginAttachment, PluginConfig, User from posthog.models.activity_logging.activity_log import ( ActivityPage, @@ -40,10 +40,6 @@ validate_plugin_job_payload, ) from posthog.models.utils import UUIDT, generate_random_token -from posthog.permissions import ( - OrganizationMemberPermissions, - TeamMemberAccessPermission, -) from posthog.plugins import can_configure_plugins, can_install_plugins, parse_url from posthog.plugins.access import can_globally_manage_plugins from posthog.queries.app_metrics.app_metrics import TeamPluginsDeliveryRateQuery @@ -300,12 +296,10 @@ def update(self, plugin: Plugin, validated_data: Dict, *args: Any, **kwargs: Any return super().update(plugin, validated_data) -class PluginViewSet(StructuredViewSetMixin, viewsets.ModelViewSet): +class PluginViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet): queryset = Plugin.objects.all() serializer_class = PluginSerializer permission_classes = [ - IsAuthenticated, - OrganizationMemberPermissions, PluginsAccessLevelPermission, PluginOwnershipPermission, ] @@ -713,14 +707,9 @@ def update( # type: ignore return response -class PluginConfigViewSet(StructuredViewSetMixin, viewsets.ModelViewSet): +class PluginConfigViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet): queryset = PluginConfig.objects.all() serializer_class = PluginConfigSerializer - permission_classes = [ - IsAuthenticated, - OrganizationMemberPermissions, - TeamMemberAccessPermission, - ] def get_queryset(self): if not can_configure_plugins(self.team.organization_id): @@ -874,7 +863,7 @@ def _get_secret_fields_for_plugin(plugin: Plugin) -> Set[str]: class LegacyPluginConfigViewSet(PluginConfigViewSet): - legacy_team_compatibility = True + derive_current_team_from_user_only = True class PipelineTransformationsViewSet(PluginViewSet): diff --git a/posthog/api/plugin_log_entry.py b/posthog/api/plugin_log_entry.py index 4497f75c3f0f2..a25be40ea56ae 100644 --- a/posthog/api/plugin_log_entry.py +++ b/posthog/api/plugin_log_entry.py @@ -2,17 +2,15 @@ from django.utils import timezone from rest_framework import exceptions, mixins, viewsets -from rest_framework.permissions import IsAuthenticated from rest_framework_dataclasses.serializers import DataclassSerializer from posthog.api.plugin import PluginOwnershipPermission, PluginsAccessLevelPermission -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.models.plugin import ( PluginLogEntry, PluginLogEntryType, fetch_plugin_log_entries, ) -from posthog.permissions import TeamMemberAccessPermission class PluginLogEntrySerializer(DataclassSerializer): @@ -20,14 +18,9 @@ class Meta: dataclass = PluginLogEntry -class PluginLogEntryViewSet(StructuredViewSetMixin, mixins.ListModelMixin, viewsets.GenericViewSet): +class PluginLogEntryViewSet(TeamAndOrgViewSetMixin, mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = PluginLogEntrySerializer - permission_classes = [ - IsAuthenticated, - PluginsAccessLevelPermission, - PluginOwnershipPermission, - TeamMemberAccessPermission, - ] + permission_classes = [PluginsAccessLevelPermission, PluginOwnershipPermission] def get_queryset(self): limit_raw = self.request.GET.get("limit") diff --git a/posthog/api/property_definition.py b/posthog/api/property_definition.py index d720f28917402..1e8678fd79317 100644 --- a/posthog/api/property_definition.py +++ b/posthog/api/property_definition.py @@ -6,7 +6,6 @@ from django.db.models import Prefetch from rest_framework import ( mixins, - permissions, serializers, viewsets, status, @@ -18,7 +17,7 @@ from rest_framework.pagination import LimitOffsetPagination from posthog.api.documentation import extend_schema -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.tagged_item import TaggedItemSerializerMixin, TaggedItemViewSetMixin from posthog.constants import GROUP_TYPES_LIMIT, AvailableFeature from posthog.event_usage import report_user_action @@ -27,10 +26,6 @@ from posthog.models import PropertyDefinition, TaggedItem, User, EventProperty from posthog.models.activity_logging.activity_log import log_activity, Detail from posthog.models.utils import UUIDT -from posthog.permissions import ( - OrganizationMemberPermissions, - TeamMemberAccessPermission, -) from loginas.utils import is_impersonated_session @@ -444,8 +439,8 @@ def paginate_queryset(self, queryset, request, view=None) -> Optional[List[Any]] class PropertyDefinitionViewSet( + TeamAndOrgViewSetMixin, TaggedItemViewSetMixin, - StructuredViewSetMixin, mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, @@ -453,11 +448,6 @@ class PropertyDefinitionViewSet( viewsets.GenericViewSet, ): serializer_class = PropertyDefinitionSerializer - permission_classes = [ - permissions.IsAuthenticated, - OrganizationMemberPermissions, - TeamMemberAccessPermission, - ] lookup_field = "id" filter_backends = [TermSearchFilterBackend] ordering = "name" diff --git a/posthog/api/query.py b/posthog/api/query.py index 89dfb77b3f4ce..58309b111a07f 100644 --- a/posthog/api/query.py +++ b/posthog/api/query.py @@ -7,14 +7,13 @@ from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.exceptions import ValidationError, NotAuthenticated -from rest_framework.permissions import IsAuthenticated from rest_framework.request import Request from rest_framework.response import Response from sentry_sdk import capture_exception from posthog.api.documentation import extend_schema from posthog.api.mixins import PydanticModelMixin -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.services.query import process_query_model from posthog.clickhouse.client.execute_async import ( cancel_query, @@ -26,7 +25,6 @@ from posthog.hogql.ai import PromptUnclear, write_sql_from_prompt from posthog.hogql.errors import HogQLException from posthog.models.user import User -from posthog.permissions import TeamMemberAccessPermission from posthog.rate_limit import ( AIBurstRateThrottle, AISustainedRateThrottle, @@ -40,12 +38,7 @@ class QueryThrottle(TeamRateThrottle): rate = "120/hour" -class QueryViewSet(PydanticModelMixin, StructuredViewSetMixin, viewsets.ViewSet): - permission_classes = [ - IsAuthenticated, - TeamMemberAccessPermission, - ] - +class QueryViewSet(PydanticModelMixin, TeamAndOrgViewSetMixin, viewsets.ViewSet): def get_throttles(self): if self.action == "draft_sql": return [AIBurstRateThrottle(), AISustainedRateThrottle()] diff --git a/posthog/api/routing.py b/posthog/api/routing.py index 0f61d093a03a7..2a00ea53fcfa4 100644 --- a/posthog/api/routing.py +++ b/posthog/api/routing.py @@ -3,6 +3,7 @@ from rest_framework import authentication from rest_framework.exceptions import AuthenticationFailed, NotFound, ValidationError +from rest_framework.permissions import IsAuthenticated from rest_framework.viewsets import GenericViewSet from rest_framework_extensions.routers import ExtendedDefaultRouter from rest_framework_extensions.settings import extensions_api_settings @@ -12,6 +13,7 @@ from posthog.models.organization import Organization from posthog.models.team import Team from posthog.models.user import User +from posthog.permissions import OrganizationMemberPermissions, TeamMemberAccessPermission from posthog.user_permissions import UserPermissions if TYPE_CHECKING: @@ -28,10 +30,11 @@ def __init__(self, *args, **kwargs): self.trailing_slash = r"/?" -class StructuredViewSetMixin(_GenericViewSet): +# NOTE: Previously known as the StructuredViewSetMixin +class TeamAndOrgViewSetMixin(_GenericViewSet): # This flag disables nested routing handling, reverting to the old request.user.team behavior # Allows for a smoother transition from the old flat API structure to the newer nested one - legacy_team_compatibility: bool = False + derive_current_team_from_user_only: bool = False # Rewrite filter queries, so that for example foreign keys can be accessed # Example: {"team_id": "foo__team_id"} will make the viewset filtered by obj.foo.team_id instead of obj.team_id @@ -39,24 +42,51 @@ class StructuredViewSetMixin(_GenericViewSet): include_in_docs = True - authentication_classes = [ - JwtAuthentication, - PersonalAPIKeyAuthentication, - authentication.SessionAuthentication, - authentication.BasicAuthentication, - ] + authentication_classes = [] + permission_classes = [] + + # We want to try and ensure that the base permission and authentication are always used + # so we offer a way to add additional classes + def get_permissions(self): + # NOTE: We define these here to make it hard _not_ to use them. If you want to override them, you have to + # override the entire method. + permission_classes: list = [IsAuthenticated] + + if self.is_team_view: + permission_classes.append(TeamMemberAccessPermission) + else: + permission_classes.append(OrganizationMemberPermissions) + + permission_classes.extend(self.permission_classes) + return [permission() for permission in permission_classes] + + def get_authenticators(self): + # NOTE: Custom authentication_classes go first as these typically have extra initial checks + authentication_classes: list = [ + *self.authentication_classes, + JwtAuthentication, + PersonalAPIKeyAuthentication, + authentication.SessionAuthentication, + ] + + return [auth() for auth in authentication_classes] def get_queryset(self): queryset = super().get_queryset() return self.filter_queryset_by_parents_lookups(queryset) + @property + def is_team_view(self): + # NOTE: We check the property first as it avoids any potential DB lookups via the parents_query_dict + return self.derive_current_team_from_user_only or "team_id" in self.parents_query_dict + @property def team_id(self) -> int: team_from_token = self._get_team_from_request() if team_from_token: return team_from_token.id - if self.legacy_team_compatibility: + if self.derive_current_team_from_user_only: user = cast(User, self.request.user) team = user.team assert team is not None @@ -69,7 +99,7 @@ def team(self) -> Team: if team_from_token: return team_from_token - if self.legacy_team_compatibility: + if self.derive_current_team_from_user_only: user = cast(User, self.request.user) team = user.team assert team is not None @@ -112,7 +142,7 @@ def parents_query_dict(self) -> Dict[str, Any]: # used to override the last visited project if there's a token in the request team_from_request = self._get_team_from_request() - if self.legacy_team_compatibility: + if self.derive_current_team_from_user_only: if not self.request.user.is_authenticated: raise AuthenticationFailed() project = team_from_request or self.request.user.team @@ -120,7 +150,7 @@ def parents_query_dict(self) -> Dict[str, Any]: raise ValidationError("This endpoint requires a project.") return {"team_id": project.id} result = {} - # process URL paremetrs (here called kwargs), such as organization_id in /api/organizations/:organization_id/ + # process URL parameters (here called kwargs), such as organization_id in /api/organizations/:organization_id/ for kwarg_name, kwarg_value in self.kwargs.items(): # drf-extensions nested parameters are prefixed if kwarg_name.startswith(extensions_api_settings.DEFAULT_PARENT_LOOKUP_KWARG_NAME_PREFIX): @@ -182,30 +212,30 @@ def user_permissions(self) -> "UserPermissions": # def create(self, *args, **kwargs): # super_cls = super() - # if self.legacy_team_compatibility: + # if self.derive_current_team_from_user_only: # print(f"Legacy endpoint called – {super_cls.get_view_name()} (create)") # return super_cls.create(*args, **kwargs) # def retrieve(self, *args, **kwargs): # super_cls = super() - # if self.legacy_team_compatibility: + # if self.derive_current_team_from_user_only: # print(f"Legacy endpoint called – {super_cls.get_view_name()} (retrieve)") # return super_cls.retrieve(*args, **kwargs) # def list(self, *args, **kwargs): # super_cls = super() - # if self.legacy_team_compatibility: + # if self.derive_current_team_from_user_only: # print(f"Legacy endpoint called – {super_cls.get_view_name()} (list)") # return super_cls.list(*args, **kwargs) # def update(self, *args, **kwargs): # super_cls = super() - # if self.legacy_team_compatibility: + # if self.derive_current_team_from_user_only: # print(f"Legacy endpoint called – {super_cls.get_view_name()} (update)") # return super_cls.update(*args, **kwargs) # def delete(self, *args, **kwargs): # super_cls = super() - # if self.legacy_team_compatibility: + # if self.derive_current_team_from_user_only: # print(f"Legacy endpoint called – {super_cls.get_view_name()} (delete)") # return super_cls.delete(*args, **kwargs) diff --git a/posthog/api/scheduled_change.py b/posthog/api/scheduled_change.py index c2214970887f1..1b48c28ae7e71 100644 --- a/posthog/api/scheduled_change.py +++ b/posthog/api/scheduled_change.py @@ -1,16 +1,12 @@ from typing import Any, Dict from rest_framework import ( - authentication, serializers, viewsets, ) -from rest_framework.permissions import IsAuthenticated -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer -from posthog.auth import PersonalAPIKeyAuthentication from posthog.models import ScheduledChange -from posthog.permissions import TeamMemberAccessPermission class ScheduledChangeSerializer(serializers.ModelSerializer): @@ -41,11 +37,12 @@ def create(self, validated_data: Dict, *args: Any, **kwargs: Any) -> ScheduledCh return super().create(validated_data) -class ScheduledChangeViewSet(StructuredViewSetMixin, viewsets.ModelViewSet): +class ScheduledChangeViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet): """ Create, read, update and delete scheduled changes. """ + serializer_class = ScheduledChangeSerializer queryset = ScheduledChange.objects.all() def get_queryset(self): @@ -60,14 +57,3 @@ def get_queryset(self): queryset = queryset.filter(record_id=record_id) return queryset - - serializer_class = ScheduledChangeSerializer - permission_classes = [ - IsAuthenticated, - TeamMemberAccessPermission, - ] - authentication_classes = [ - PersonalAPIKeyAuthentication, - authentication.SessionAuthentication, - authentication.BasicAuthentication, - ] diff --git a/posthog/api/search.py b/posthog/api/search.py index 521536c728c36..a66432e022f39 100644 --- a/posthog/api/search.py +++ b/posthog/api/search.py @@ -6,12 +6,10 @@ from django.db.models.functions import Cast, JSONObject # type: ignore from django.http import HttpResponse from rest_framework import viewsets, serializers -from rest_framework.permissions import IsAuthenticated from rest_framework.request import Request from rest_framework.response import Response -from posthog.api.routing import StructuredViewSetMixin -from posthog.permissions import TeamMemberAccessPermission +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.models import Action, Cohort, Insight, Dashboard, FeatureFlag, Experiment, Team, EventDefinition, Survey from posthog.models.notebook.notebook import Notebook @@ -78,9 +76,7 @@ def validate_q(self, value: str): return process_query(value) -class SearchViewSet(StructuredViewSetMixin, viewsets.ViewSet): - permission_classes = [IsAuthenticated, TeamMemberAccessPermission] - +class SearchViewSet(TeamAndOrgViewSetMixin, viewsets.ViewSet): def list(self, request: Request, **kw) -> HttpResponse: # parse query params query_serializer = QuerySerializer(data=self.request.query_params) diff --git a/posthog/api/sharing.py b/posthog/api/sharing.py index 6ece14d0e6dd1..d0909ec2d9029 100644 --- a/posthog/api/sharing.py +++ b/posthog/api/sharing.py @@ -8,13 +8,13 @@ from django.views.decorators.clickjacking import xframe_options_exempt from rest_framework import mixins, response, serializers, viewsets from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError -from rest_framework.permissions import SAFE_METHODS, IsAuthenticated +from rest_framework.permissions import SAFE_METHODS from rest_framework.request import Request from posthog.api.dashboards.dashboard import DashboardSerializer from posthog.api.exports import ExportedAssetSerializer from posthog.api.insight import InsightSerializer -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.models import SharingConfiguration, Team from posthog.models.activity_logging.activity_log import log_activity, Detail, Change from posthog.models.dashboard import Dashboard @@ -26,7 +26,6 @@ from posthog.models.insight import Insight from posthog.models import SessionRecording from posthog.models.user import User -from posthog.permissions import TeamMemberAccessPermission from posthog.session_recordings.session_recording_api import SessionRecordingSerializer from posthog.user_permissions import UserPermissions from posthog.utils import render_template @@ -80,11 +79,7 @@ class Meta: read_only_fields = ["created_at", "access_token"] -class SharingConfigurationViewSet(StructuredViewSetMixin, mixins.ListModelMixin, viewsets.GenericViewSet): - permission_classes = [ - IsAuthenticated, - TeamMemberAccessPermission, - ] +class SharingConfigurationViewSet(TeamAndOrgViewSetMixin, mixins.ListModelMixin, viewsets.GenericViewSet): pagination_class = None queryset = SharingConfiguration.objects.select_related("dashboard", "insight", "recording") serializer_class = SharingConfigurationSerializer @@ -202,7 +197,7 @@ def patch(self, request: Request, *args: Any, **kwargs: Any) -> response.Respons return response.Response(serializer.data) -class SharingViewerPageViewSet(mixins.RetrieveModelMixin, StructuredViewSetMixin, viewsets.GenericViewSet): +class SharingViewerPageViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet): """ NOTE: This ViewSet takes care of multiple rendering cases: 1. Shared Resources like Shared Dashboard or Insight diff --git a/posthog/api/survey.py b/posthog/api/survey.py index 22e9f6aa9cb99..db07f6db2dba7 100644 --- a/posthog/api/survey.py +++ b/posthog/api/survey.py @@ -10,15 +10,13 @@ from rest_framework.response import Response from rest_framework.decorators import action from posthog.api.feature_flag import FeatureFlagSerializer, MinimalFeatureFlagSerializer -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from rest_framework import serializers, viewsets, request -from rest_framework.permissions import IsAuthenticated from rest_framework.request import Request from rest_framework import status from posthog.models.feature_flag.feature_flag import FeatureFlag from posthog.models.team.team import Team -from posthog.permissions import TeamMemberAccessPermission from django.utils.text import slugify from django.views.decorators.csrf import csrf_exempt @@ -254,12 +252,8 @@ def _create_new_targeting_flag(self, name, filters, active=False): return targeting_feature_flag -class SurveyViewSet(StructuredViewSetMixin, viewsets.ModelViewSet): +class SurveyViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet): queryset = Survey.objects.select_related("linked_flag", "targeting_flag").all() - permission_classes = [ - IsAuthenticated, - TeamMemberAccessPermission, - ] def get_serializer_class(self) -> Type[serializers.Serializer]: if self.request.method == "POST" or self.request.method == "PATCH": diff --git a/posthog/api/tagged_item.py b/posthog/api/tagged_item.py index d756b3411655e..bec8cfef025e5 100644 --- a/posthog/api/tagged_item.py +++ b/posthog/api/tagged_item.py @@ -2,7 +2,7 @@ from rest_framework import response, serializers, status, viewsets from rest_framework.viewsets import GenericViewSet -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.constants import AvailableFeature from posthog.models import Tag, TaggedItem, User from posthog.models.tag import tagify @@ -107,7 +107,7 @@ def get_tag(self, obj: TaggedItem) -> str: return obj.tag.name -class TaggedItemViewSet(StructuredViewSetMixin, GenericViewSet): +class TaggedItemViewSet(TeamAndOrgViewSetMixin, GenericViewSet): serializer_class = TaggedItemSerializer queryset = Tag.objects.none() diff --git a/posthog/api/team.py b/posthog/api/team.py index 2ca4a00ce575d..75bd46e7d486a 100644 --- a/posthog/api/team.py +++ b/posthog/api/team.py @@ -7,19 +7,20 @@ from loginas.utils import is_impersonated_session from rest_framework import ( exceptions, - permissions, request, response, serializers, viewsets, ) +from rest_framework.permissions import BasePermission, IsAuthenticated from rest_framework.decorators import action from posthog.api.geoip import get_geoip_properties +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import TeamBasicSerializer from posthog.constants import AvailableFeature from posthog.event_usage import report_user_action -from posthog.models import InsightCachingState, Organization, Team, User +from posthog.models import InsightCachingState, Team, User from posthog.models.activity_logging.activity_log import ( log_activity, Detail, @@ -32,10 +33,7 @@ from posthog.models.group_type_mapping import GroupTypeMapping from posthog.models.organization import OrganizationMembership from posthog.models.signals import mute_selected_signals -from posthog.models.team.team import ( - groups_on_events_querying_enabled, - set_team_in_cache, -) +from posthog.models.team.team import groups_on_events_querying_enabled, set_team_in_cache from posthog.models.team.util import delete_batch_exports, delete_bulky_postgres_data from posthog.models.utils import generate_random_token_project, UUIDT from posthog.permissions import ( @@ -50,7 +48,7 @@ from posthog.utils import get_ip_address, get_week_start_for_country_code -class PremiumMultiProjectPermissions(permissions.BasePermission): +class PremiumMultiProjectPermissions(BasePermission): """Require user to have all necessary premium features on their plan for create access to the endpoint.""" message = "You must upgrade your PostHog plan to be able to create and manage multiple projects." @@ -334,17 +332,16 @@ def update(self, instance: Team, validated_data: Dict[str, Any]) -> Team: return updated_team -class TeamViewSet(viewsets.ModelViewSet): +class TeamViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet): """ Projects for the current organization. """ serializer_class = TeamSerializer queryset = Team.objects.all().select_related("organization") - permission_classes = [permissions.IsAuthenticated, PremiumMultiProjectPermissions] + permission_classes = [IsAuthenticated, PremiumMultiProjectPermissions] lookup_field = "id" ordering = "-created_by" - organization: Optional[Organization] = None include_in_docs = True def get_queryset(self): @@ -357,16 +354,6 @@ def get_serializer_class(self) -> Type[serializers.BaseSerializer]: return TeamBasicSerializer return super().get_serializer_class() - def check_permissions(self, request): - if self.action and self.action == "create": - organization = getattr(self.request.user, "organization", None) - if not organization: - raise exceptions.ValidationError("You need to belong to an organization.") - # To be used later by OrganizationAdminWritePermissions and TeamSerializer - self.organization = organization - - return super().check_permissions(request) - def get_permissions(self) -> List: """ Special permissions handling for create requests as the organization is inferred from the current user. @@ -386,6 +373,16 @@ def get_permissions(self) -> List: base_permissions.append(TeamMemberLightManagementPermission()) return base_permissions + def check_permissions(self, request): + if self.action and self.action == "create": + organization = getattr(self.request.user, "organization", None) + if not organization: + raise exceptions.ValidationError("You need to belong to an organization.") + # To be used later by OrganizationAdminWritePermissions and TeamSerializer + self.organization = organization + + return super().check_permissions(request) + def get_object(self): lookup_value = self.kwargs[self.lookup_field] if lookup_value == "@current": @@ -451,7 +448,7 @@ def perform_destroy(self, team: Team): detail=True, # Only ADMIN or higher users are allowed to access this project permission_classes=[ - permissions.IsAuthenticated, + IsAuthenticated, TeamMemberStrictManagementPermission, ], ) @@ -487,7 +484,7 @@ def reset_token(self, request: request.Request, id: str, **kwargs) -> response.R @action( methods=["GET"], detail=True, - permission_classes=[permissions.IsAuthenticated], + permission_classes=[IsAuthenticated], ) def is_generating_demo_data(self, request: request.Request, id: str, **kwargs) -> response.Response: team = self.get_object() diff --git a/posthog/api/test/__snapshots__/test_api_docs.ambr b/posthog/api/test/__snapshots__/test_api_docs.ambr index 5a47d45cd3567..b1ffdb2329c1d 100644 --- a/posthog/api/test/__snapshots__/test_api_docs.ambr +++ b/posthog/api/test/__snapshots__/test_api_docs.ambr @@ -1,6 +1,8 @@ # serializer version: 1 # name: TestAPIDocsSchema.test_api_docs_generation_warnings_snapshot list([ + "/home/runner/work/posthog/posthog/posthog/api/organization.py: Warning [OrganizationViewSet]: could not resolve authenticator . There was no OpenApiAuthenticationExtension registered for that class. Try creating one by subclassing it. Ignoring for now.", + '/home/runner/work/posthog/posthog/posthog/api/organization.py: Warning [OrganizationViewSet > OrganizationSerializer]: unable to resolve type hint for function "get_metadata". Consider using a type hint or @extend_schema_field. Defaulting to string.', '/home/runner/work/posthog/posthog/posthog/batch_exports/http.py: Warning [BatchExportOrganizationViewSet]: could not derive type of path parameter "parent_lookup_organization_id" because model "posthog.batch_exports.models.BatchExport" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', "/home/runner/work/posthog/posthog/posthog/batch_exports/http.py: Warning [BatchExportOrganizationViewSet]: could not resolve authenticator . There was no OpenApiAuthenticationExtension registered for that class. Try creating one by subclassing it. Ignoring for now.", '/home/runner/work/posthog/posthog/posthog/batch_exports/http.py: Warning [BatchExportOrganizationViewSet > BatchExportSerializer]: could not resolve serializer field "HogQLSelectQueryField(required=False)". Defaulting to "string"', @@ -176,6 +178,12 @@ '/home/runner/work/posthog/posthog/posthog/warehouse/api/table.py: Warning [TableViewSet > TableSerializer]: unable to resolve type hint for function "get_external_schema". Consider using a type hint or @extend_schema_field. Defaulting to string.', '/home/runner/work/posthog/posthog/posthog/warehouse/api/view_link.py: Warning [ViewLinkViewSet]: could not derive type of path parameter "project_id" because model "posthog.warehouse.models.view_link.DataWarehouseViewLink" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', "/home/runner/work/posthog/posthog/posthog/warehouse/api/view_link.py: Warning [ViewLinkViewSet]: could not resolve authenticator . There was no OpenApiAuthenticationExtension registered for that class. Try creating one by subclassing it. Ignoring for now.", + 'Warning: operationId "list" has collisions [(\'/api/organizations/\', \'get\'), (\'/api/projects/\', \'get\')]. resolving with numeral suffixes.', + 'Warning: operationId "create" has collisions [(\'/api/organizations/\', \'post\'), (\'/api/projects/\', \'post\')]. resolving with numeral suffixes.', + 'Warning: operationId "retrieve" has collisions [(\'/api/organizations/{id}/\', \'get\'), (\'/api/projects/{id}/\', \'get\')]. resolving with numeral suffixes.', + 'Warning: operationId "update" has collisions [(\'/api/organizations/{id}/\', \'put\'), (\'/api/projects/{id}/\', \'put\')]. resolving with numeral suffixes.', + 'Warning: operationId "partial_update" has collisions [(\'/api/organizations/{id}/\', \'patch\'), (\'/api/projects/{id}/\', \'patch\')]. resolving with numeral suffixes.', + 'Warning: operationId "destroy" has collisions [(\'/api/organizations/{id}/\', \'delete\'), (\'/api/projects/{id}/\', \'delete\')]. resolving with numeral suffixes.', 'Warning: operationId "batch_exports_list" has collisions [(\'/api/organizations/{parent_lookup_organization_id}/batch_exports/\', \'get\'), (\'/api/projects/{project_id}/batch_exports/\', \'get\')]. resolving with numeral suffixes.', 'Warning: operationId "batch_exports_create" has collisions [(\'/api/organizations/{parent_lookup_organization_id}/batch_exports/\', \'post\'), (\'/api/projects/{project_id}/batch_exports/\', \'post\')]. resolving with numeral suffixes.', 'Warning: operationId "batch_exports_retrieve" has collisions [(\'/api/organizations/{parent_lookup_organization_id}/batch_exports/{id}/\', \'get\'), (\'/api/projects/{project_id}/batch_exports/{id}/\', \'get\')]. resolving with numeral suffixes.', diff --git a/posthog/api/test/test_action.py b/posthog/api/test/test_action.py index fc21c9f12f270..c86d14d4156b1 100644 --- a/posthog/api/test/test_action.py +++ b/posthog/api/test/test_action.py @@ -183,14 +183,14 @@ def test_update_action_remove_all_steps(self, *args): # otherwise evil sites could create actions with a users' session. # NOTE: Origin header is only set on cross domain request def test_create_from_other_domain(self, *args): - # FIXME: BaseTest is using Django client to performe calls to a DRF endpoint. + # FIXME: BaseTest is using Django client to perform calls to a DRF endpoint. # Django HttpResponse does not have an attribute `data`. Better use rest_framework.test.APIClient. response = self.client.post( f"/api/projects/{self.team.id}/actions/", data={"name": "user signed up"}, HTTP_ORIGIN="https://evilwebsite.com", ) - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 401) self.user.temporary_token = "token123" self.user.save() @@ -214,13 +214,13 @@ def test_create_from_other_domain(self, *args): f"/api/projects/{self.team.id}/actions/", HTTP_ORIGIN="https://evilwebsite.com", ) - self.assertEqual(list_response.status_code, 403) + self.assertEqual(list_response.status_code, 401) detail_response = self.client.get( f"/api/projects/{self.team.id}/actions/{response.json()['id']}/", HTTP_ORIGIN="https://evilwebsite.com", ) - self.assertEqual(detail_response.status_code, 403) + self.assertEqual(detail_response.status_code, 401) self.client.logout() list_response = self.client.get( diff --git a/posthog/api/test/test_event_definition.py b/posthog/api/test/test_event_definition.py index 9b978490cda7e..aa2a2c05a2428 100644 --- a/posthog/api/test/test_event_definition.py +++ b/posthog/api/test/test_event_definition.py @@ -135,7 +135,7 @@ def test_cant_see_event_definitions_for_another_team(self): # Also can't fetch for a team to which the user doesn't have permissions response = self.client.get(f"/api/projects/{team.pk}/event_definitions/") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(response.json(), self.permission_denied_response()) + self.assertEqual(response.json(), self.permission_denied_response("You don't have access to the project.")) def test_query_event_definitions(self): # Regular search diff --git a/posthog/api/test/test_property_definition.py b/posthog/api/test/test_property_definition.py index 512f43ce92d0c..3d68b73facd59 100644 --- a/posthog/api/test/test_property_definition.py +++ b/posthog/api/test/test_property_definition.py @@ -135,7 +135,7 @@ def test_cant_see_property_definitions_for_another_team(self): # Also can't fetch for a team to which the user doesn't have permissions response = self.client.get(f"/api/projects/{team.pk}/property_definitions/") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(response.json(), self.permission_denied_response()) + self.assertEqual(response.json(), self.permission_denied_response("You don't have access to the project.")) def test_query_property_definitions(self): # no search at all diff --git a/posthog/api/uploaded_media.py b/posthog/api/uploaded_media.py index b8d4610f201cb..ccf0efcf441bd 100644 --- a/posthog/api/uploaded_media.py +++ b/posthog/api/uploaded_media.py @@ -13,14 +13,12 @@ ValidationError, ) from rest_framework.parsers import FormParser, MultiPartParser -from rest_framework.permissions import IsAuthenticatedOrReadOnly from rest_framework.response import Response from statshog.defaults.django import statsd -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.models import UploadedMedia from posthog.models.uploaded_media import ObjectStorageUnavailable -from posthog.permissions import TeamMemberAccessPermission from posthog.storage import object_storage FOUR_MEGABYTES = 4 * 1024 * 1024 @@ -82,10 +80,9 @@ def download(request, *args, **kwargs) -> HttpResponse: ) -class MediaViewSet(StructuredViewSetMixin, viewsets.GenericViewSet): +class MediaViewSet(TeamAndOrgViewSetMixin, viewsets.GenericViewSet): queryset = UploadedMedia.objects.all() parser_classes = (MultiPartParser, FormParser) - permission_classes = [IsAuthenticatedOrReadOnly, TeamMemberAccessPermission] @extend_schema( description=""" diff --git a/posthog/auth.py b/posthog/auth.py index 062e7e48bb5c5..c9b87107b7104 100644 --- a/posthog/auth.py +++ b/posthog/auth.py @@ -143,8 +143,13 @@ def authenticate(self, request: Request): if not user.exists(): raise AuthenticationFailed(detail="User doesn't exist") return (user.first(), None) + return None + # NOTE: This appears first in the authentication chain often so we want to define an authenticate_header to ensure 401 and not 403 + def authenticate_header(self, request: Request): + return "Bearer" + class JwtAuthentication(authentication.BaseAuthentication): """ diff --git a/posthog/batch_exports/http.py b/posthog/batch_exports/http.py index ad98129126cc6..fc6b6319631a3 100644 --- a/posthog/batch_exports/http.py +++ b/posthog/batch_exports/http.py @@ -14,10 +14,9 @@ ValidationError, ) from rest_framework.pagination import CursorPagination -from rest_framework.permissions import IsAuthenticated from rest_framework_dataclasses.serializers import DataclassSerializer -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.batch_exports.models import ( BATCH_EXPORT_INTERVALS, BatchExportLogEntry, @@ -49,7 +48,6 @@ Team, User, ) -from posthog.permissions import OrganizationMemberPermissions, TeamMemberAccessPermission from posthog.temporal.common.client import sync_connect from posthog.utils import relative_date_parse @@ -94,9 +92,8 @@ class RunsCursorPagination(CursorPagination): page_size = 100 -class BatchExportRunViewSet(StructuredViewSetMixin, viewsets.ReadOnlyModelViewSet): +class BatchExportRunViewSet(TeamAndOrgViewSetMixin, viewsets.ReadOnlyModelViewSet): queryset = BatchExportRun.objects.all() - permission_classes = [IsAuthenticated, TeamMemberAccessPermission] serializer_class = BatchExportRunSerializer pagination_class = RunsCursorPagination @@ -340,9 +337,8 @@ def update(self, batch_export: BatchExport, validated_data: dict) -> BatchExport return batch_export -class BatchExportViewSet(StructuredViewSetMixin, viewsets.ModelViewSet): +class BatchExportViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet): queryset = BatchExport.objects.all() - permission_classes = [IsAuthenticated, TeamMemberAccessPermission] serializer_class = BatchExportSerializer def get_queryset(self): @@ -454,7 +450,6 @@ def perform_destroy(self, instance: BatchExport): class BatchExportOrganizationViewSet(BatchExportViewSet): - permission_classes = [IsAuthenticated, OrganizationMemberPermissions] filter_rewrite_rules = {"organization_id": "team__organization_id"} @@ -463,8 +458,7 @@ class Meta: dataclass = BatchExportLogEntry -class BatchExportLogViewSet(StructuredViewSetMixin, mixins.ListModelMixin, viewsets.GenericViewSet): - permission_classes = [IsAuthenticated, TeamMemberAccessPermission] +class BatchExportLogViewSet(TeamAndOrgViewSetMixin, mixins.ListModelMixin, viewsets.GenericViewSet): serializer_class = BatchExportLogEntrySerializer def get_queryset(self): diff --git a/posthog/permissions.py b/posthog/permissions.py index 5d6f713677551..d1357d46f2959 100644 --- a/posthog/permissions.py +++ b/posthog/permissions.py @@ -137,6 +137,9 @@ def has_permission(self, request, view) -> bool: view.team # noqa: B018 except Team.DoesNotExist: return True # This will be handled as a 404 in the viewset + + # NOTE: The naming here is confusing - "current_team" refers to the team that the user_permissions was initialized with + # - not the "current_team" property of the user requesting_level = view.user_permissions.current_team.effective_membership_level return requesting_level is not None diff --git a/posthog/session_recordings/session_recording_api.py b/posthog/session_recordings/session_recording_api.py index 3f22408b22aa4..5cbff80d8c944 100644 --- a/posthog/session_recordings/session_recording_api.py +++ b/posthog/session_recordings/session_recording_api.py @@ -16,11 +16,10 @@ from loginas.utils import is_impersonated_session from rest_framework import exceptions, request, serializers, viewsets from rest_framework.decorators import action -from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from posthog.api.person import MinimalPersonSerializer -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.auth import SharingAccessTokenAuthentication from posthog.cloud_utils import is_cloud from posthog.constants import SESSION_RECORDINGS_FILTER_IDS @@ -30,7 +29,6 @@ from posthog.session_recordings.models.session_recording import SessionRecording from posthog.permissions import ( SharingTokenPermission, - TeamMemberAccessPermission, ) from posthog.session_recordings.models.session_recording_event import ( SessionRecordingViewed, @@ -179,8 +177,8 @@ def list_recordings_response( return response -class SessionRecordingViewSet(StructuredViewSetMixin, viewsets.GenericViewSet): - permission_classes = [IsAuthenticated, TeamMemberAccessPermission] +# NOTE: Could we put the sharing stuff in the shared mixin :thinking: +class SessionRecordingViewSet(TeamAndOrgViewSetMixin, viewsets.GenericViewSet): throttle_classes = [ClickHouseBurstRateThrottle, ClickHouseSustainedRateThrottle] serializer_class = SessionRecordingSerializer # We don't use this diff --git a/posthog/session_recordings/test/__snapshots__/test_session_recordings.ambr b/posthog/session_recordings/test/__snapshots__/test_session_recordings.ambr index affde7090e435..02915aeed7207 100644 --- a/posthog/session_recordings/test/__snapshots__/test_session_recordings.ambr +++ b/posthog/session_recordings/test/__snapshots__/test_session_recordings.ambr @@ -706,6 +706,30 @@ LIMIT 21 ''' # --- +# name: TestSessionRecordings.test_get_session_recordings.30 + ''' + SELECT "posthog_persondistinctid"."id", + "posthog_persondistinctid"."team_id", + "posthog_persondistinctid"."person_id", + "posthog_persondistinctid"."distinct_id", + "posthog_persondistinctid"."version", + "posthog_person"."id", + "posthog_person"."created_at", + "posthog_person"."properties_last_updated_at", + "posthog_person"."properties_last_operation", + "posthog_person"."team_id", + "posthog_person"."properties", + "posthog_person"."is_user_id", + "posthog_person"."is_identified", + "posthog_person"."uuid", + "posthog_person"."version" + FROM "posthog_persondistinctid" + INNER JOIN "posthog_person" ON ("posthog_persondistinctid"."person_id" = "posthog_person"."id") + WHERE ("posthog_persondistinctid"."distinct_id" IN ('user2', + 'user_one_2') + AND "posthog_persondistinctid"."team_id" = 2) /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ + ''' +# --- # name: TestSessionRecordings.test_get_session_recordings.4 ''' SELECT "posthog_team"."id", @@ -3137,6 +3161,61 @@ AND "posthog_persondistinctid"."team_id" = 2) /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ ''' # --- +# name: TestSessionRecordings.test_listing_recordings_is_not_nplus1_for_persons.185 + ''' + SELECT "posthog_instancesetting"."id", + "posthog_instancesetting"."key", + "posthog_instancesetting"."raw_value" + FROM "posthog_instancesetting" + WHERE "posthog_instancesetting"."key" = 'constance:posthog:PERSON_ON_EVENTS_V2_ENABLED' + ORDER BY "posthog_instancesetting"."id" ASC + LIMIT 1 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ + ''' +# --- +# name: TestSessionRecordings.test_listing_recordings_is_not_nplus1_for_persons.186 + ''' + SELECT "posthog_instancesetting"."id", + "posthog_instancesetting"."key", + "posthog_instancesetting"."raw_value" + FROM "posthog_instancesetting" + WHERE "posthog_instancesetting"."key" = 'constance:posthog:PERSON_ON_EVENTS_ENABLED' + ORDER BY "posthog_instancesetting"."id" ASC + LIMIT 1 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ + ''' +# --- +# name: TestSessionRecordings.test_listing_recordings_is_not_nplus1_for_persons.187 + ''' + SELECT "posthog_instancesetting"."id", + "posthog_instancesetting"."key", + "posthog_instancesetting"."raw_value" + FROM "posthog_instancesetting" + WHERE "posthog_instancesetting"."key" = 'constance:posthog:PERSON_ON_EVENTS_V2_ENABLED' + ORDER BY "posthog_instancesetting"."id" ASC + LIMIT 1 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ + ''' +# --- +# name: TestSessionRecordings.test_listing_recordings_is_not_nplus1_for_persons.188 + ''' + SELECT "posthog_instancesetting"."id", + "posthog_instancesetting"."key", + "posthog_instancesetting"."raw_value" + FROM "posthog_instancesetting" + WHERE "posthog_instancesetting"."key" = 'constance:posthog:PERSON_ON_EVENTS_ENABLED' + ORDER BY "posthog_instancesetting"."id" ASC + LIMIT 1 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ + ''' +# --- +# name: TestSessionRecordings.test_listing_recordings_is_not_nplus1_for_persons.189 + ''' + SELECT "posthog_instancesetting"."id", + "posthog_instancesetting"."key", + "posthog_instancesetting"."raw_value" + FROM "posthog_instancesetting" + WHERE "posthog_instancesetting"."key" = 'constance:posthog:AGGREGATE_BY_DISTINCT_IDS_TEAMS' + ORDER BY "posthog_instancesetting"."id" ASC + LIMIT 1 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ + ''' +# --- # name: TestSessionRecordings.test_listing_recordings_is_not_nplus1_for_persons.19 ''' SELECT "posthog_instancesetting"."id", @@ -3148,6 +3227,115 @@ LIMIT 1 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ ''' # --- +# name: TestSessionRecordings.test_listing_recordings_is_not_nplus1_for_persons.190 + ''' + SELECT "posthog_instancesetting"."id", + "posthog_instancesetting"."key", + "posthog_instancesetting"."raw_value" + FROM "posthog_instancesetting" + WHERE "posthog_instancesetting"."key" = 'constance:posthog:RECORDINGS_TTL_WEEKS' + ORDER BY "posthog_instancesetting"."id" ASC + LIMIT 1 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ + ''' +# --- +# name: TestSessionRecordings.test_listing_recordings_is_not_nplus1_for_persons.191 + ''' + SELECT "posthog_instancesetting"."id", + "posthog_instancesetting"."key", + "posthog_instancesetting"."raw_value" + FROM "posthog_instancesetting" + WHERE "posthog_instancesetting"."key" = 'constance:posthog:AGGREGATE_BY_DISTINCT_IDS_TEAMS' + ORDER BY "posthog_instancesetting"."id" ASC + LIMIT 1 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ + ''' +# --- +# name: TestSessionRecordings.test_listing_recordings_is_not_nplus1_for_persons.192 + ''' + SELECT "posthog_instancesetting"."id", + "posthog_instancesetting"."key", + "posthog_instancesetting"."raw_value" + FROM "posthog_instancesetting" + WHERE "posthog_instancesetting"."key" = 'constance:posthog:AGGREGATE_BY_DISTINCT_IDS_TEAMS' + ORDER BY "posthog_instancesetting"."id" ASC + LIMIT 1 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ + ''' +# --- +# name: TestSessionRecordings.test_listing_recordings_is_not_nplus1_for_persons.193 + ''' + SELECT "posthog_sessionrecording"."id", + "posthog_sessionrecording"."session_id", + "posthog_sessionrecording"."team_id", + "posthog_sessionrecording"."created_at", + "posthog_sessionrecording"."deleted", + "posthog_sessionrecording"."object_storage_path", + "posthog_sessionrecording"."distinct_id", + "posthog_sessionrecording"."duration", + "posthog_sessionrecording"."active_seconds", + "posthog_sessionrecording"."inactive_seconds", + "posthog_sessionrecording"."start_time", + "posthog_sessionrecording"."end_time", + "posthog_sessionrecording"."click_count", + "posthog_sessionrecording"."keypress_count", + "posthog_sessionrecording"."mouse_activity_count", + "posthog_sessionrecording"."console_log_count", + "posthog_sessionrecording"."console_warn_count", + "posthog_sessionrecording"."console_error_count", + "posthog_sessionrecording"."start_url", + "posthog_sessionrecording"."storage_version" + FROM "posthog_sessionrecording" + WHERE ("posthog_sessionrecording"."session_id" IN ('1', + '10', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9') + AND "posthog_sessionrecording"."team_id" = 2) /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ + ''' +# --- +# name: TestSessionRecordings.test_listing_recordings_is_not_nplus1_for_persons.194 + ''' + SELECT "posthog_sessionrecordingviewed"."session_id" + FROM "posthog_sessionrecordingviewed" + WHERE ("posthog_sessionrecordingviewed"."team_id" = 2 + AND "posthog_sessionrecordingviewed"."user_id" = 2) /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ + ''' +# --- +# name: TestSessionRecordings.test_listing_recordings_is_not_nplus1_for_persons.195 + ''' + SELECT "posthog_persondistinctid"."id", + "posthog_persondistinctid"."team_id", + "posthog_persondistinctid"."person_id", + "posthog_persondistinctid"."distinct_id", + "posthog_persondistinctid"."version", + "posthog_person"."id", + "posthog_person"."created_at", + "posthog_person"."properties_last_updated_at", + "posthog_person"."properties_last_operation", + "posthog_person"."team_id", + "posthog_person"."properties", + "posthog_person"."is_user_id", + "posthog_person"."is_identified", + "posthog_person"."uuid", + "posthog_person"."version" + FROM "posthog_persondistinctid" + INNER JOIN "posthog_person" ON ("posthog_persondistinctid"."person_id" = "posthog_person"."id") + WHERE ("posthog_persondistinctid"."distinct_id" IN ('user1', + 'user10', + 'user2', + 'user3', + 'user4', + 'user5', + 'user6', + 'user7', + 'user8', + 'user9') + AND "posthog_persondistinctid"."team_id" = 2) /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ + ''' +# --- # name: TestSessionRecordings.test_listing_recordings_is_not_nplus1_for_persons.2 ''' SELECT "posthog_organizationmembership"."id", diff --git a/posthog/settings/web.py b/posthog/settings/web.py index 08c7f00769619..8514e81365f5b 100644 --- a/posthog/settings/web.py +++ b/posthog/settings/web.py @@ -241,7 +241,6 @@ REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": [ "posthog.auth.PersonalAPIKeyAuthentication", - "rest_framework.authentication.BasicAuthentication", "rest_framework.authentication.SessionAuthentication", ], "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", diff --git a/posthog/warehouse/api/external_data_schema.py b/posthog/warehouse/api/external_data_schema.py index f69a34efcc4b7..a2f5bb9e1bc86 100644 --- a/posthog/warehouse/api/external_data_schema.py +++ b/posthog/warehouse/api/external_data_schema.py @@ -1,10 +1,8 @@ from rest_framework import serializers from posthog.warehouse.models import ExternalDataSchema from typing import Optional -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from rest_framework import viewsets, filters -from rest_framework.permissions import IsAuthenticated -from posthog.permissions import OrganizationMemberPermissions from rest_framework.exceptions import NotAuthenticated from posthog.models import User @@ -29,10 +27,9 @@ class Meta: fields = ["id", "name", "should_sync", "last_synced_at"] -class ExternalDataSchemaViewset(StructuredViewSetMixin, viewsets.ModelViewSet): +class ExternalDataSchemaViewset(TeamAndOrgViewSetMixin, viewsets.ModelViewSet): queryset = ExternalDataSchema.objects.all() serializer_class = ExternalDataSchemaSerializer - permission_classes = [IsAuthenticated, OrganizationMemberPermissions] filter_backends = [filters.SearchFilter] search_fields = ["name"] ordering = "-created_at" diff --git a/posthog/warehouse/api/external_data_source.py b/posthog/warehouse/api/external_data_source.py index faf884d6c8b3b..6b59b2f01637a 100644 --- a/posthog/warehouse/api/external_data_source.py +++ b/posthog/warehouse/api/external_data_source.py @@ -5,13 +5,11 @@ from rest_framework import filters, serializers, status, viewsets from rest_framework.decorators import action from rest_framework.exceptions import NotAuthenticated -from rest_framework.permissions import IsAuthenticated from rest_framework.request import Request from rest_framework.response import Response -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.models import User -from posthog.permissions import OrganizationMemberPermissions from posthog.warehouse.data_load.service import ( sync_external_data_job_workflow, trigger_external_data_workflow, @@ -87,14 +85,13 @@ class Meta: read_only_fields = ["id", "created_by", "created_at", "status", "source_type"] -class ExternalDataSourceViewSet(StructuredViewSetMixin, viewsets.ModelViewSet): +class ExternalDataSourceViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet): """ Create, Read, Update and Delete External data Sources. """ queryset = ExternalDataSource.objects.all() serializer_class = ExternalDataSourceSerializers - permission_classes = [IsAuthenticated, OrganizationMemberPermissions] filter_backends = [filters.SearchFilter] search_fields = ["source_id"] ordering = "-created_at" diff --git a/posthog/warehouse/api/saved_query.py b/posthog/warehouse/api/saved_query.py index 1b624d44159a6..02182f9b3a6b7 100644 --- a/posthog/warehouse/api/saved_query.py +++ b/posthog/warehouse/api/saved_query.py @@ -3,9 +3,8 @@ from django.conf import settings from rest_framework import exceptions, filters, serializers, viewsets from rest_framework.exceptions import NotAuthenticated -from rest_framework.permissions import IsAuthenticated -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer from posthog.hogql.context import HogQLContext from posthog.hogql.database.database import SerializedField, serialize_fields @@ -14,7 +13,6 @@ from posthog.hogql.parser import parse_select from posthog.hogql.printer import print_ast from posthog.models import User -from posthog.permissions import OrganizationMemberPermissions from posthog.warehouse.models import DataWarehouseSavedQuery @@ -92,14 +90,13 @@ def validate_query(self, query): return query -class DataWarehouseSavedQueryViewSet(StructuredViewSetMixin, viewsets.ModelViewSet): +class DataWarehouseSavedQueryViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet): """ Create, Read, Update and Delete Warehouse Tables. """ queryset = DataWarehouseSavedQuery.objects.all() serializer_class = DataWarehouseSavedQuerySerializer - permission_classes = [IsAuthenticated, OrganizationMemberPermissions] filter_backends = [filters.SearchFilter] search_fields = ["name"] ordering = "-created_at" diff --git a/posthog/warehouse/api/table.py b/posthog/warehouse/api/table.py index aa4d23e2ecd01..1fbb36f05b564 100644 --- a/posthog/warehouse/api/table.py +++ b/posthog/warehouse/api/table.py @@ -2,13 +2,11 @@ from rest_framework import filters, request, response, serializers, status, viewsets from rest_framework.exceptions import NotAuthenticated -from rest_framework.permissions import IsAuthenticated -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer from posthog.hogql.database.database import SerializedField, serialize_fields from posthog.models import User -from posthog.permissions import OrganizationMemberPermissions from posthog.warehouse.models import ( DataWarehouseCredential, DataWarehouseSavedQuery, @@ -93,14 +91,13 @@ def get_columns(self, table: DataWarehouseTable) -> List[SerializedField]: return serialize_fields(table.hogql_definition().fields) -class TableViewSet(StructuredViewSetMixin, viewsets.ModelViewSet): +class TableViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet): """ Create, Read, Update and Delete Warehouse Tables. """ queryset = DataWarehouseTable.objects.all() serializer_class = TableSerializer - permission_classes = [IsAuthenticated, OrganizationMemberPermissions] filter_backends = [filters.SearchFilter] search_fields = ["name"] ordering = "-created_at" diff --git a/posthog/warehouse/api/view_link.py b/posthog/warehouse/api/view_link.py index dcda8aeda6ede..82edd17f87e1d 100644 --- a/posthog/warehouse/api/view_link.py +++ b/posthog/warehouse/api/view_link.py @@ -2,13 +2,11 @@ from rest_framework import filters, serializers, viewsets from rest_framework.exceptions import NotAuthenticated -from rest_framework.permissions import IsAuthenticated -from posthog.api.routing import StructuredViewSetMixin +from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.api.shared import UserBasicSerializer from posthog.hogql.database.database import create_hogql_database from posthog.models import User -from posthog.permissions import OrganizationMemberPermissions from posthog.warehouse.models import DataWarehouseSavedQuery, DataWarehouseViewLink @@ -84,14 +82,13 @@ def _validate_join_key(self, join_key: Optional[str], table: Optional[str], team return -class ViewLinkViewSet(StructuredViewSetMixin, viewsets.ModelViewSet): +class ViewLinkViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet): """ Create, Read, Update and Delete View Columns. """ queryset = DataWarehouseViewLink.objects.all() serializer_class = ViewLinkSerializer - permission_classes = [IsAuthenticated, OrganizationMemberPermissions] filter_backends = [filters.SearchFilter] search_fields = ["name"] ordering = "-created_at"