Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ee/api/max_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from rest_framework.viewsets import GenericViewSet

from posthog.api.routing import TeamAndOrgViewSetMixin
from posthog.auth import OAuthAccessTokenAuthentication, PersonalAPIKeyAuthentication
from posthog.auth import PersonalAPIKeyAuthentication
from posthog.models.user import User
from posthog.rate_limit import AIBurstRateThrottle, AISustainedRateThrottle
from posthog.renderers import SafeJSONRenderer
Expand Down Expand Up @@ -44,7 +44,7 @@ class MaxToolsViewSet(TeamAndOrgViewSetMixin, GenericViewSet):
permission_classes = [IsAuthenticated]
renderer_classes = [SafeJSONRenderer]
throttle_classes = [AIBurstRateThrottle, AISustainedRateThrottle]
authentication_classes = [PersonalAPIKeyAuthentication, OAuthAccessTokenAuthentication]
authentication_classes = [PersonalAPIKeyAuthentication]

@action(
detail=False,
Expand Down
4 changes: 2 additions & 2 deletions posthog/api/external_web_analytics/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from posthog.api.mixins import PydanticModelMixin
from posthog.api.routing import TeamAndOrgViewSetMixin
from posthog.api.utils import action
from posthog.auth import OAuthAccessTokenAuthentication, PersonalAPIKeyAuthentication, SessionAuthentication
from posthog.auth import PersonalAPIKeyAuthentication, SessionAuthentication
from posthog.clickhouse.client.limit import get_web_analytics_api_rate_limiter
from posthog.models.user import User
from posthog.rate_limit import WebAnalyticsAPIBurstThrottle, WebAnalyticsAPISustainedThrottle
Expand All @@ -36,7 +36,7 @@ class ExternalWebAnalyticsViewSet(TeamAndOrgViewSetMixin, PydanticModelMixin, vi

scope_object = "query"
scope_object_read_actions = ["summary", "overview", "trend", "breakdown"]
authentication_classes = [SessionAuthentication, PersonalAPIKeyAuthentication, OAuthAccessTokenAuthentication]
authentication_classes = [SessionAuthentication, PersonalAPIKeyAuthentication]
scope_object_write_actions: list[str] = []
throttle_classes = [WebAnalyticsAPIBurstThrottle, WebAnalyticsAPISustainedThrottle]

Expand Down
6 changes: 2 additions & 4 deletions posthog/api/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from posthog.api.routing import TeamAndOrgViewSetMixin
from posthog.api.shared import UserBasicSerializer
from posthog.api.utils import action
from posthog.auth import OAuthAccessTokenAuthentication, PersonalAPIKeyAuthentication
from posthog.auth import PersonalAPIKeyAuthentication
from posthog.models.instance_setting import get_instance_setting
from posthog.models.integration import (
ClickUpIntegration,
Expand Down Expand Up @@ -165,9 +165,7 @@ class IntegrationViewSet(
serializer_class = IntegrationSerializer

def safely_get_queryset(self, queryset):
if isinstance(self.request.successful_authenticator, PersonalAPIKeyAuthentication) or isinstance(
self.request.successful_authenticator, OAuthAccessTokenAuthentication
):
if isinstance(self.request.successful_authenticator, PersonalAPIKeyAuthentication):
return queryset.filter(kind="github")
return queryset

Expand Down
4 changes: 2 additions & 2 deletions posthog/api/llm_gateway/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from rest_framework.response import Response

from posthog.api.routing import TeamAndOrgViewSetMixin
from posthog.auth import OAuthAccessTokenAuthentication, PersonalAPIKeyAuthentication
from posthog.auth import PersonalAPIKeyAuthentication
from posthog.permissions import APIScopePermission
from posthog.rate_limit import LLMGatewayBurstRateThrottle, LLMGatewaySustainedRateThrottle
from posthog.renderers import SafeJSONRenderer, ServerSentEventRenderer
Expand All @@ -35,7 +35,7 @@


class LLMGatewayViewSet(TeamAndOrgViewSetMixin, viewsets.ViewSet):
authentication_classes = [SessionAuthentication, PersonalAPIKeyAuthentication, OAuthAccessTokenAuthentication]
authentication_classes = [SessionAuthentication, PersonalAPIKeyAuthentication]
permission_classes = [IsAuthenticated, APIScopePermission]
scope_object = "task"
renderer_classes = [SafeJSONRenderer, ServerSentEventRenderer]
Expand Down
6 changes: 1 addition & 5 deletions posthog/api/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from posthog import settings
from posthog.api.routing import TeamAndOrgViewSetMixin
from posthog.api.shared import ProjectBasicSerializer, TeamBasicSerializer
from posthog.auth import OAuthAccessTokenAuthentication, PersonalAPIKeyAuthentication
from posthog.auth import PersonalAPIKeyAuthentication
from posthog.cloud_utils import is_cloud
from posthog.constants import INTERNAL_BOT_EMAIL_SUFFIX, AvailableFeature
from posthog.event_usage import groups, report_organization_action, report_organization_deleted
Expand Down Expand Up @@ -245,10 +245,6 @@ def safely_get_queryset(self, queryset) -> QuerySet:
if isinstance(self.request.successful_authenticator, PersonalAPIKeyAuthentication):
if scoped_organizations := self.request.successful_authenticator.personal_api_key.scoped_organizations:
queryset = queryset.filter(id__in=scoped_organizations)
if isinstance(self.request.successful_authenticator, OAuthAccessTokenAuthentication):
if scoped_organizations := self.request.successful_authenticator.access_token.scoped_organizations:
queryset = queryset.filter(id__in=scoped_organizations)

return queryset

def safely_get_object(self, queryset):
Expand Down
5 changes: 1 addition & 4 deletions posthog/api/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from posthog.api.routing import TeamAndOrgViewSetMixin
from posthog.api.shared import ProjectBackwardCompatBasicSerializer
from posthog.api.team import TEAM_CONFIG_FIELDS_SET, TeamSerializer, validate_team_attrs
from posthog.auth import OAuthAccessTokenAuthentication, PersonalAPIKeyAuthentication
from posthog.auth import PersonalAPIKeyAuthentication
from posthog.constants import AvailableFeature
from posthog.event_usage import report_user_action
from posthog.geoip import get_geoip_properties
Expand Down Expand Up @@ -451,9 +451,6 @@ def safely_get_queryset(self, queryset):
if isinstance(self.request.successful_authenticator, PersonalAPIKeyAuthentication):
if scoped_organizations := self.request.successful_authenticator.personal_api_key.scoped_organizations:
queryset = queryset.filter(organization_id__in=scoped_organizations)
if isinstance(self.request.successful_authenticator, OAuthAccessTokenAuthentication):
if scoped_organizations := self.request.successful_authenticator.access_token.scoped_organizations:
queryset = queryset.filter(organization_id__in=scoped_organizations)
return queryset.filter(id__in=visible_teams_ids)

def get_serializer_class(self) -> type[serializers.BaseSerializer]:
Expand Down
7 changes: 1 addition & 6 deletions posthog/api/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from posthog.api.routing import TeamAndOrgViewSetMixin
from posthog.api.shared import TeamBasicSerializer
from posthog.api.utils import action
from posthog.auth import OAuthAccessTokenAuthentication, PersonalAPIKeyAuthentication
from posthog.auth import PersonalAPIKeyAuthentication
from posthog.constants import AvailableFeature
from posthog.event_usage import report_user_action
from posthog.geoip import get_geoip_properties
Expand Down Expand Up @@ -807,11 +807,6 @@ def safely_get_queryset(self, queryset):
queryset = queryset.filter(project__organization_id__in=scoped_organizations)
if scoped_teams := self.request.successful_authenticator.personal_api_key.scoped_teams:
queryset = queryset.filter(id__in=scoped_teams)
if isinstance(self.request.successful_authenticator, OAuthAccessTokenAuthentication):
if scoped_organizations := self.request.successful_authenticator.access_token.scoped_organizations:
queryset = queryset.filter(project__organization_id__in=scoped_organizations)
if scoped_teams := self.request.successful_authenticator.access_token.scoped_teams:
queryset = queryset.filter(id__in=scoped_teams)
return queryset

def get_serializer_class(self) -> type[serializers.BaseSerializer]:
Expand Down
40 changes: 9 additions & 31 deletions posthog/api/test/test_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from django.contrib.sessions.middleware import SessionMiddleware
from django.core import mail
from django.core.cache import cache
from django.test import RequestFactory, override_settings
from django.test import RequestFactory
from django.utils import timezone

from django_otp.oath import totp
Expand All @@ -26,7 +26,6 @@
from two_factor.utils import totp_digits

from posthog.api.authentication import password_reset_token_generator, post_login, social_login_notification
from posthog.api.test.test_oauth import generate_rsa_key
from posthog.auth import OAuthAccessTokenAuthentication, ProjectSecretAPIKeyAuthentication, ProjectSecretAPIKeyUser
from posthog.models import User
from posthog.models.instance_setting import set_instance_setting
Expand Down Expand Up @@ -1131,12 +1130,6 @@ def test_authenticate_with_no_project_api_key_in_body_passes(self):
self.assertEqual(user.team, self.team)


@override_settings(
OAUTH2_PROVIDER={
**settings.OAUTH2_PROVIDER,
"OIDC_RSA_PRIVATE_KEY": generate_rsa_key(),
}
)
class TestOAuthAccessTokenAuthentication(APIBaseTest):
def setUp(self):
super().setUp()
Expand All @@ -1156,7 +1149,7 @@ def setUp(self):
self.access_token = OAuthAccessToken.objects.create(
user=self.user,
application=self.oauth_app,
token="pha_test_access_token_123",
token="test_access_token_123",
expires=timezone.now() + timedelta(hours=1),
scope="openid profile",
)
Expand All @@ -1180,22 +1173,21 @@ def test_authenticate_with_valid_oauth_token(self):
def test_authenticate_with_invalid_oauth_token(self):
wsgi_request = self.factory.get(
"/",
headers={"AUTHORIZATION": "Bearer pha_invalid_token_123"},
headers={"AUTHORIZATION": "Bearer invalid_token_123"},
)
request = Request(wsgi_request)

authenticator = OAuthAccessTokenAuthentication()
result = authenticator.authenticate(request)

with self.assertRaises(AuthenticationFailed) as context:
authenticator.authenticate(request)

self.assertEqual(str(context.exception.detail), "Invalid access token.")
# Should return None for nonexistent tokens to allow next auth method
self.assertIsNone(result)

def test_authenticate_with_expired_oauth_token(self):
expired_token = OAuthAccessToken.objects.create(
user=self.user,
application=self.oauth_app,
token="pha_expired_token_123",
token="expired_token_123",
expires=timezone.now() - timedelta(hours=1),
scope="openid profile",
)
Expand Down Expand Up @@ -1289,7 +1281,7 @@ def test_authenticate_with_token_validation_error_raises_exception(self):
invalid_token = OAuthAccessToken.objects.create(
user=self.user,
application=None, # This will cause a validation error
token="pha_invalid_app_token_123",
token="invalid_app_token_123",
expires=timezone.now() + timedelta(hours=1),
scope="openid profile",
)
Expand All @@ -1314,7 +1306,7 @@ def test_authenticate_with_user_not_found_raises_exception(self):
token_without_user = OAuthAccessToken.objects.create(
user=None,
application=self.oauth_app,
token="pha_no_user_token_123",
token="no_user_token_123",
expires=timezone.now() + timedelta(hours=1),
scope="openid profile",
)
Expand Down Expand Up @@ -1367,20 +1359,6 @@ def test_oauth_access_token_calls_tag_queries_with_correct_parameters(self):
access_method="oauth",
)

def test_authenticate_without_pha_prefix_returns_none(self):
"""Test that tokens without the pha_ prefix are skipped by OAuth authentication,
allowing PersonalAPIKeyAuthentication to handle them."""
wsgi_request = self.factory.get(
"/",
headers={"AUTHORIZATION": "Bearer random_token_without_prefix"},
)
request = Request(wsgi_request)

authenticator = OAuthAccessTokenAuthentication()
result = authenticator.authenticate(request)

self.assertIsNone(result)


class TestOAuthLoginNotification(APIBaseTest):
CONFIG_AUTO_LOGIN = False
Expand Down
40 changes: 0 additions & 40 deletions posthog/api/test/test_organization.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
from datetime import timedelta
from typing import cast

from posthog.test.base import APIBaseTest
from unittest.mock import ANY, patch

from django.conf import settings
from django.test import override_settings
from django.utils import timezone

from rest_framework import status
from rest_framework.test import APIRequestFactory

from posthog.api.organization import OrganizationSerializer
from posthog.api.test.test_oauth import generate_rsa_key
from posthog.models import FeatureFlag, Organization, OrganizationMembership, Team
from posthog.models.oauth import OAuthAccessToken, OAuthApplication
from posthog.models.personal_api_key import PersonalAPIKey, hash_key_value
from posthog.models.utils import generate_random_token_personal
from posthog.user_permissions import UserPermissions
Expand Down Expand Up @@ -192,39 +185,6 @@ def test_projects_outside_personal_api_key_scoped_organizations_not_listed(self)
"Only the scoped organization should be listed, the other one should be excluded",
)

@override_settings(
OAUTH2_PROVIDER={
**settings.OAUTH2_PROVIDER,
"OIDC_RSA_PRIVATE_KEY": generate_rsa_key(),
}
)
def test_projects_outside_oauth_scoped_organizations_causes_401(self):
# TODO: This should filter out the organizations to the scoped organizations, but it causes a 401 due to a bug in APIScopePermission for list endpoints.
other_org, _, _ = Organization.objects.bootstrap(self.user)

oauth_app = OAuthApplication.objects.create(
name="Test OAuth App",
client_id="test_client_id",
client_type=OAuthApplication.CLIENT_CONFIDENTIAL,
authorization_grant_type=OAuthApplication.GRANT_AUTHORIZATION_CODE,
redirect_uris="https://example.com/callback",
algorithm="RS256",
user=self.user,
)

access_token = OAuthAccessToken.objects.create(
application=oauth_app,
user=self.user,
token="test_oauth_token",
scope="organization:read",
expires=timezone.now() + timedelta(hours=1),
scoped_organizations=[str(other_org.id)],
)

response = self.client.get("/api/organizations/", HTTP_AUTHORIZATION=f"Bearer {access_token.token}")

self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

def test_delete_organizations_and_verify_list(self):
self.organization_membership.level = OrganizationMembership.Level.OWNER
self.organization_membership.save()
Expand Down
4 changes: 2 additions & 2 deletions posthog/api/test/test_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ def setUp(self):
self.access_token = OAuthAccessToken.objects.create(
user=self.user,
application=self.oauth_app,
token="pha_test_oauth_access_token_123",
token="test_oauth_access_token_123",
expires=timezone.now() + timedelta(hours=1),
scope="annotation:read",
)
Expand Down Expand Up @@ -218,7 +218,7 @@ def test_oauth_token_fails_with_expired_token(self):
expired_token = OAuthAccessToken.objects.create(
user=self.user,
application=self.oauth_app,
token="pha_expired_oauth_token_123",
token="expired_oauth_token_123",
expires=timezone.now() - timedelta(hours=1),
scope="annotation:read",
)
Expand Down
Loading