0
0
mirror of https://github.com/PostHog/posthog.git synced 2024-11-24 00:47:50 +01:00

Merge branch 'master' into experiments/data-warehouse-support-v2

This commit is contained in:
Daniel Bachhuber 2024-11-21 12:52:59 -08:00
commit 80d42d0527
73 changed files with 19411 additions and 12125 deletions

View File

@ -0,0 +1,194 @@
from typing import TYPE_CHECKING, cast
from rest_framework import exceptions, serializers, status
from rest_framework.decorators import action
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from ee.models.rbac.access_control import AccessControl
from posthog.models.scopes import API_SCOPE_OBJECTS, APIScopeObjectOrNotSupported
from posthog.models.team.team import Team
from posthog.rbac.user_access_control import (
ACCESS_CONTROL_LEVELS_RESOURCE,
UserAccessControl,
default_access_level,
highest_access_level,
ordered_access_levels,
)
if TYPE_CHECKING:
_GenericViewSet = GenericViewSet
else:
_GenericViewSet = object
class AccessControlSerializer(serializers.ModelSerializer):
access_level = serializers.CharField(allow_null=True)
class Meta:
model = AccessControl
fields = [
"access_level",
"resource",
"resource_id",
"organization_member",
"role",
"created_by",
"created_at",
"updated_at",
]
read_only_fields = ["id", "created_at", "created_by"]
# Validate that resource is a valid option from the API_SCOPE_OBJECTS
def validate_resource(self, resource):
if resource not in API_SCOPE_OBJECTS:
raise serializers.ValidationError("Invalid resource. Must be one of: {}".format(API_SCOPE_OBJECTS))
return resource
# Validate that access control is a valid option
def validate_access_level(self, access_level):
if access_level and access_level not in ordered_access_levels(self.initial_data["resource"]):
raise serializers.ValidationError(
f"Invalid access level. Must be one of: {', '.join(ordered_access_levels(self.initial_data['resource']))}"
)
return access_level
def validate(self, data):
context = self.context
# Ensure that only one of organization_member or role is set
if data.get("organization_member") and data.get("role"):
raise serializers.ValidationError("You can not scope an access control to both a member and a role.")
access_control = cast(UserAccessControl, self.context["view"].user_access_control)
resource = data["resource"]
resource_id = data.get("resource_id")
# We assume the highest level is required for the given resource to edit access controls
required_level = highest_access_level(resource)
team = context["view"].team
the_object = context["view"].get_object()
if resource_id:
# Check that they have the right access level for this specific resource object
if not access_control.check_can_modify_access_levels_for_object(the_object):
raise exceptions.PermissionDenied(f"Must be {required_level} to modify {resource} permissions.")
else:
# If modifying the base resource rules then we are checking the parent membership (project or organization)
# NOTE: Currently we only support org level in the UI so its simply an org level check
if not access_control.check_can_modify_access_levels_for_object(team):
raise exceptions.PermissionDenied("Must be an Organization admin to modify project-wide permissions.")
return data
class AccessControlViewSetMixin(_GenericViewSet):
"""
Adds an "access_controls" action to the viewset that handles access control for the given resource
Why a mixin? We want to easily add this to any existing resource, including providing easy helpers for adding access control info such
as the current users access level to any response.
"""
# 1. Know that the project level access is covered by the Permission check
# 2. Get the actual object which we can pass to the serializer to check if the user created it
# 3. We can also use the serializer to check the access level for the object
def _get_access_control_serializer(self, *args, **kwargs):
kwargs.setdefault("context", self.get_serializer_context())
return AccessControlSerializer(*args, **kwargs)
def _get_access_controls(self, request: Request, is_global=False):
resource = cast(APIScopeObjectOrNotSupported, getattr(self, "scope_object", None))
user_access_control = cast(UserAccessControl, self.user_access_control) # type: ignore
team = cast(Team, self.team) # type: ignore
if is_global and resource != "project" or not resource or resource == "INTERNAL":
raise exceptions.NotFound("Role based access controls are only available for projects.")
obj = self.get_object()
resource_id = obj.id
if is_global:
# If role based then we are getting all controls for the project that aren't specific to a resource
access_controls = AccessControl.objects.filter(team=team, resource_id=None).all()
else:
# Otherwise we are getting all controls for the specific resource
access_controls = AccessControl.objects.filter(team=team, resource=resource, resource_id=resource_id).all()
serializer = self._get_access_control_serializer(instance=access_controls, many=True)
user_access_level = user_access_control.access_level_for_object(obj, resource)
return Response(
{
"access_controls": serializer.data,
# NOTE: For Role based controls we are always configuring resource level items
"available_access_levels": ACCESS_CONTROL_LEVELS_RESOURCE
if is_global
else ordered_access_levels(resource),
"default_access_level": "editor" if is_global else default_access_level(resource),
"user_access_level": user_access_level,
"user_can_edit_access_levels": user_access_control.check_can_modify_access_levels_for_object(obj),
}
)
def _update_access_controls(self, request: Request, is_global=False):
resource = getattr(self, "scope_object", None)
obj = self.get_object()
resource_id = str(obj.id)
team = cast(Team, self.team) # type: ignore
# Generically validate the incoming data
if not is_global:
# If not role based we are deriving from the viewset
data = request.data
data["resource"] = resource
data["resource_id"] = resource_id
partial_serializer = self._get_access_control_serializer(data=request.data)
partial_serializer.is_valid(raise_exception=True)
params = partial_serializer.validated_data
instance = AccessControl.objects.filter(
team=team,
resource=params["resource"],
resource_id=params.get("resource_id"),
organization_member=params.get("organization_member"),
role=params.get("role"),
).first()
if params["access_level"] is None:
if instance:
instance.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
# Perform the upsert
if instance:
serializer = self._get_access_control_serializer(instance, data=request.data)
else:
serializer = self._get_access_control_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.validated_data["team"] = team
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
@action(methods=["GET", "PUT"], detail=True)
def access_controls(self, request: Request, *args, **kwargs):
if request.method == "PUT":
return self._update_access_controls(request)
return self._get_access_controls(request)
@action(methods=["GET", "PUT"], detail=True)
def global_access_controls(self, request: Request, *args, **kwargs):
if request.method == "PUT":
return self._update_access_controls(request, is_global=True)
return self._get_access_controls(request, is_global=True)

View File

@ -4,14 +4,12 @@ from django.db import IntegrityError
from rest_framework import mixins, serializers, viewsets
from rest_framework.permissions import SAFE_METHODS, BasePermission
from ee.models.feature_flag_role_access import FeatureFlagRoleAccess
from ee.models.rbac.organization_resource_access import OrganizationResourceAccess
from ee.models.rbac.role import Role, RoleMembership
from posthog.api.organization_member import OrganizationMemberSerializer
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
@ -38,7 +36,6 @@ class RolePermissions(BasePermission):
class RoleSerializer(serializers.ModelSerializer):
created_by = UserBasicSerializer(read_only=True)
members = serializers.SerializerMethodField()
associated_flags = serializers.SerializerMethodField()
class Meta:
model = Role
@ -49,7 +46,6 @@ class RoleSerializer(serializers.ModelSerializer):
"created_at",
"created_by",
"members",
"associated_flags",
]
read_only_fields = ["id", "created_at", "created_by"]
@ -75,29 +71,12 @@ class RoleSerializer(serializers.ModelSerializer):
members = RoleMembership.objects.filter(role=role)
return RoleMembershipSerializer(members, many=True).data
def get_associated_flags(self, role: Role):
associated_flags: list[dict] = []
role_access_objects = FeatureFlagRoleAccess.objects.filter(role=role).values_list("feature_flag_id")
flags = FeatureFlag.objects.filter(id__in=role_access_objects)
for flag in flags:
associated_flags.append({"id": flag.id, "key": flag.key})
return associated_flags
class RoleViewSet(
TeamAndOrgViewSetMixin,
mixins.ListModelMixin,
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
viewsets.GenericViewSet,
):
class RoleViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet):
scope_object = "organization"
permission_classes = [RolePermissions]
serializer_class = RoleSerializer
queryset = Role.objects.all()
permission_classes = [RolePermissions]
def safely_get_queryset(self, queryset):
return queryset.filter(**self.request.GET.dict())

View File

@ -0,0 +1,598 @@
import json
from unittest.mock import MagicMock, patch
from rest_framework import status
from ee.api.test.base import APILicensedTest
from ee.models.rbac.role import Role, RoleMembership
from posthog.constants import AvailableFeature
from posthog.models.dashboard import Dashboard
from posthog.models.feature_flag.feature_flag import FeatureFlag
from posthog.models.notebook.notebook import Notebook
from posthog.models.organization import OrganizationMembership
from posthog.models.team.team import Team
from posthog.utils import render_template
class BaseAccessControlTest(APILicensedTest):
def setUp(self):
super().setUp()
self.organization.available_features = [
AvailableFeature.PROJECT_BASED_PERMISSIONING,
AvailableFeature.ROLE_BASED_ACCESS,
]
self.organization.save()
def _put_project_access_control(self, data=None):
payload = {"access_level": "admin"}
if data:
payload.update(data)
return self.client.put(
"/api/projects/@current/access_controls",
payload,
)
def _put_global_access_control(self, data=None):
payload = {"access_level": "editor"}
if data:
payload.update(data)
return self.client.put(
"/api/projects/@current/global_access_controls",
payload,
)
def _org_membership(self, level: OrganizationMembership.Level = OrganizationMembership.Level.ADMIN):
self.organization_membership.level = level
self.organization_membership.save()
class TestAccessControlProjectLevelAPI(BaseAccessControlTest):
def test_project_change_rejected_if_not_org_admin(self):
self._org_membership(OrganizationMembership.Level.MEMBER)
res = self._put_project_access_control()
assert res.status_code == status.HTTP_403_FORBIDDEN, res.json()
def test_project_change_accepted_if_org_admin(self):
self._org_membership(OrganizationMembership.Level.ADMIN)
res = self._put_project_access_control()
assert res.status_code == status.HTTP_200_OK, res.json()
def test_project_change_accepted_if_org_owner(self):
self._org_membership(OrganizationMembership.Level.OWNER)
res = self._put_project_access_control()
assert res.status_code == status.HTTP_200_OK, res.json()
def test_project_removed_with_null(self):
self._org_membership(OrganizationMembership.Level.OWNER)
res = self._put_project_access_control()
res = self._put_project_access_control({"access_level": None})
assert res.status_code == status.HTTP_204_NO_CONTENT
def test_project_change_if_in_access_control(self):
self._org_membership(OrganizationMembership.Level.ADMIN)
# Add ourselves to access
res = self._put_project_access_control(
{"organization_member": str(self.organization_membership.id), "access_level": "admin"}
)
assert res.status_code == status.HTTP_200_OK, res.json()
self._org_membership(OrganizationMembership.Level.MEMBER)
# Now change ourselves to a member
res = self._put_project_access_control(
{"organization_member": str(self.organization_membership.id), "access_level": "member"}
)
assert res.status_code == status.HTTP_200_OK, res.json()
assert res.json()["access_level"] == "member"
# Now try and change our own membership and fail!
res = self._put_project_access_control(
{"organization_member": str(self.organization_membership.id), "access_level": "admin"}
)
assert res.status_code == status.HTTP_403_FORBIDDEN
assert res.json()["detail"] == "Must be admin to modify project permissions."
def test_project_change_rejected_if_not_in_organization(self):
self.organization_membership.delete()
res = self._put_project_access_control(
{"organization_member": str(self.organization_membership.id), "access_level": "admin"}
)
assert res.status_code == status.HTTP_404_NOT_FOUND, res.json()
def test_project_change_rejected_if_bad_access_level(self):
self._org_membership(OrganizationMembership.Level.ADMIN)
res = self._put_project_access_control({"access_level": "bad"})
assert res.status_code == status.HTTP_400_BAD_REQUEST, res.json()
assert res.json()["detail"] == "Invalid access level. Must be one of: none, member, admin", res.json()
class TestAccessControlResourceLevelAPI(BaseAccessControlTest):
def setUp(self):
super().setUp()
self.notebook = Notebook.objects.create(
team=self.team, created_by=self.user, short_id="0", title="first notebook"
)
self.other_user = self._create_user("other_user")
self.other_user_notebook = Notebook.objects.create(
team=self.team, created_by=self.other_user, short_id="1", title="first notebook"
)
def _get_access_controls(self):
return self.client.get(f"/api/projects/@current/notebooks/{self.notebook.short_id}/access_controls")
def _put_access_control(self, data=None, notebook_id=None):
payload = {
"access_level": "editor",
}
if data:
payload.update(data)
return self.client.put(
f"/api/projects/@current/notebooks/{notebook_id or self.notebook.short_id}/access_controls",
payload,
)
def test_get_access_controls(self):
self._org_membership(OrganizationMembership.Level.MEMBER)
res = self._get_access_controls()
assert res.status_code == status.HTTP_200_OK, res.json()
assert res.json() == {
"access_controls": [],
"available_access_levels": ["none", "viewer", "editor"],
"user_access_level": "editor",
"default_access_level": "editor",
"user_can_edit_access_levels": True,
}
def test_change_rejected_if_not_org_admin(self):
self._org_membership(OrganizationMembership.Level.MEMBER)
res = self._put_access_control(notebook_id=self.other_user_notebook.short_id)
assert res.status_code == status.HTTP_403_FORBIDDEN, res.json()
def test_change_accepted_if_org_admin(self):
self._org_membership(OrganizationMembership.Level.ADMIN)
res = self._put_access_control(notebook_id=self.other_user_notebook.short_id)
assert res.status_code == status.HTTP_200_OK, res.json()
def test_change_accepted_if_creator_of_the_resource(self):
self._org_membership(OrganizationMembership.Level.MEMBER)
res = self._put_access_control(notebook_id=self.notebook.short_id)
assert res.status_code == status.HTTP_200_OK, res.json()
class TestGlobalAccessControlsPermissions(BaseAccessControlTest):
def setUp(self):
super().setUp()
self.role = Role.objects.create(name="Engineers", organization=self.organization)
self.role_membership = RoleMembership.objects.create(user=self.user, role=self.role)
def test_admin_can_always_access(self):
self._org_membership(OrganizationMembership.Level.ADMIN)
assert (
self._put_global_access_control({"resource": "feature_flag", "access_level": "none"}).status_code
== status.HTTP_200_OK
)
assert self.client.get("/api/projects/@current/feature_flags").status_code == status.HTTP_200_OK
def test_forbidden_access_if_resource_wide_control_in_place(self):
self._org_membership(OrganizationMembership.Level.ADMIN)
assert (
self._put_global_access_control({"resource": "feature_flag", "access_level": "none"}).status_code
== status.HTTP_200_OK
)
self._org_membership(OrganizationMembership.Level.MEMBER)
assert self.client.get("/api/projects/@current/feature_flags").status_code == status.HTTP_403_FORBIDDEN
assert self.client.post("/api/projects/@current/feature_flags").status_code == status.HTTP_403_FORBIDDEN
def test_forbidden_write_access_if_resource_wide_control_in_place(self):
self._org_membership(OrganizationMembership.Level.ADMIN)
assert (
self._put_global_access_control({"resource": "feature_flag", "access_level": "viewer"}).status_code
== status.HTTP_200_OK
)
self._org_membership(OrganizationMembership.Level.MEMBER)
assert self.client.get("/api/projects/@current/feature_flags").status_code == status.HTTP_200_OK
assert self.client.post("/api/projects/@current/feature_flags").status_code == status.HTTP_403_FORBIDDEN
def test_access_granted_with_granted_role(self):
self._org_membership(OrganizationMembership.Level.ADMIN)
assert (
self._put_global_access_control({"resource": "feature_flag", "access_level": "none"}).status_code
== status.HTTP_200_OK
)
assert (
self._put_global_access_control(
{"resource": "feature_flag", "access_level": "viewer", "role": self.role.id}
).status_code
== status.HTTP_200_OK
)
self._org_membership(OrganizationMembership.Level.MEMBER)
assert self.client.get("/api/projects/@current/feature_flags").status_code == status.HTTP_200_OK
assert self.client.post("/api/projects/@current/feature_flags").status_code == status.HTTP_403_FORBIDDEN
self.role_membership.delete()
assert self.client.get("/api/projects/@current/feature_flags").status_code == status.HTTP_403_FORBIDDEN
class TestAccessControlPermissions(BaseAccessControlTest):
"""
Test actual permissions being applied for a resource (notebooks as an example)
"""
def setUp(self):
super().setUp()
self.other_user = self._create_user("other_user")
self.other_user_notebook = Notebook.objects.create(
team=self.team, created_by=self.other_user, title="not my notebook"
)
self.notebook = Notebook.objects.create(team=self.team, created_by=self.user, title="my notebook")
def _post_notebook(self):
return self.client.post("/api/projects/@current/notebooks/", {"title": "notebook"})
def _patch_notebook(self, id: str):
return self.client.patch(f"/api/projects/@current/notebooks/{id}", {"title": "new-title"})
def _get_notebook(self, id: str):
return self.client.get(f"/api/projects/@current/notebooks/{id}")
def _put_notebook_access_control(self, notebook_id: str, data=None):
payload = {
"access_level": "editor",
}
if data:
payload.update(data)
return self.client.put(
f"/api/projects/@current/notebooks/{notebook_id}/access_controls",
payload,
)
def test_default_allows_all_access(self):
self._org_membership(OrganizationMembership.Level.MEMBER)
assert self._get_notebook(self.other_user_notebook.short_id).status_code == status.HTTP_200_OK
assert self._patch_notebook(id=self.other_user_notebook.short_id).status_code == status.HTTP_200_OK
res = self._post_notebook()
assert res.status_code == status.HTTP_201_CREATED
assert self._patch_notebook(id=res.json()["short_id"]).status_code == status.HTTP_200_OK
def test_rejects_all_access_without_project_access(self):
self._org_membership(OrganizationMembership.Level.ADMIN)
assert self._put_project_access_control({"access_level": "none"}).status_code == status.HTTP_200_OK
self._org_membership(OrganizationMembership.Level.MEMBER)
assert self._get_notebook(self.other_user_notebook.short_id).status_code == status.HTTP_403_FORBIDDEN
assert self._patch_notebook(id=self.other_user_notebook.short_id).status_code == status.HTTP_403_FORBIDDEN
assert self._post_notebook().status_code == status.HTTP_403_FORBIDDEN
def test_permits_access_with_member_control(self):
self._org_membership(OrganizationMembership.Level.ADMIN)
assert self._put_project_access_control({"access_level": "none"}).status_code == status.HTTP_200_OK
assert (
self._put_project_access_control(
{"access_level": "member", "organization_member": str(self.organization_membership.id)}
).status_code
== status.HTTP_200_OK
)
self._org_membership(OrganizationMembership.Level.MEMBER)
assert self._get_notebook(self.other_user_notebook.short_id).status_code == status.HTTP_200_OK
assert self._patch_notebook(id=self.other_user_notebook.short_id).status_code == status.HTTP_200_OK
assert self._post_notebook().status_code == status.HTTP_201_CREATED
def test_rejects_edit_access_with_resource_control(self):
self._org_membership(OrganizationMembership.Level.ADMIN)
# Set other notebook to only allow view access by default
assert (
self._put_notebook_access_control(self.other_user_notebook.short_id, {"access_level": "viewer"}).status_code
== status.HTTP_200_OK
)
self._org_membership(OrganizationMembership.Level.MEMBER)
assert self._get_notebook(self.other_user_notebook.short_id).status_code == status.HTTP_200_OK
assert self._patch_notebook(id=self.other_user_notebook.short_id).status_code == status.HTTP_403_FORBIDDEN
assert self._post_notebook().status_code == status.HTTP_201_CREATED
def test_rejects_view_access_if_not_creator(self):
self._org_membership(OrganizationMembership.Level.ADMIN)
# Set other notebook to only allow view access by default
assert (
self._put_notebook_access_control(self.other_user_notebook.short_id, {"access_level": "none"}).status_code
== status.HTTP_200_OK
)
assert (
self._put_notebook_access_control(self.notebook.short_id, {"access_level": "none"}).status_code
== status.HTTP_200_OK
)
self._org_membership(OrganizationMembership.Level.MEMBER)
# Access to other notebook is denied
assert self._get_notebook(self.other_user_notebook.short_id).status_code == status.HTTP_403_FORBIDDEN
assert self._patch_notebook(id=self.other_user_notebook.short_id).status_code == status.HTTP_403_FORBIDDEN
# As creator, access to my notebook is still permitted
assert self._get_notebook(self.notebook.short_id).status_code == status.HTTP_200_OK
assert self._patch_notebook(id=self.notebook.short_id).status_code == status.HTTP_200_OK
def test_org_level_endpoints_work(self):
assert self.client.get("/api/organizations/@current/plugins").status_code == status.HTTP_200_OK
class TestAccessControlQueryCounts(BaseAccessControlTest):
def setUp(self):
super().setUp()
self.other_user = self._create_user("other_user")
self.other_user_notebook = Notebook.objects.create(
team=self.team, created_by=self.other_user, title="not my notebook"
)
self.notebook = Notebook.objects.create(team=self.team, created_by=self.user, title="my notebook")
# Baseline call to trigger caching of one off things like instance settings
self.client.get(f"/api/projects/@current/notebooks/{self.notebook.short_id}")
def test_query_counts(self):
self._org_membership(OrganizationMembership.Level.MEMBER)
my_dashboard = Dashboard.objects.create(team=self.team, created_by=self.user)
other_user_dashboard = Dashboard.objects.create(team=self.team, created_by=self.other_user)
# Baseline query (triggers any first time cache things)
self.client.get(f"/api/projects/@current/notebooks/{self.notebook.short_id}")
baseline = 11
# Access controls total 2 extra queries - 1 for org membership, 1 for the user roles, 1 for the preloaded access controls
with self.assertNumQueries(baseline + 4):
self.client.get(f"/api/projects/@current/dashboards/{my_dashboard.id}?no_items_field=true")
# Accessing a different users dashboard doesn't +1 as the preload works using the pk
with self.assertNumQueries(baseline + 4):
self.client.get(f"/api/projects/@current/dashboards/{other_user_dashboard.id}?no_items_field=true")
baseline = 6
# Getting my own notebook is the same as a dashboard - 2 extra queries
with self.assertNumQueries(baseline + 4):
self.client.get(f"/api/projects/@current/notebooks/{self.notebook.short_id}")
# Except when accessing a different notebook where we _also_ need to check as we are not the creator and the pk is not the same (short_id)
with self.assertNumQueries(baseline + 5):
self.client.get(f"/api/projects/@current/notebooks/{self.other_user_notebook.short_id}")
baseline = 4
# Project access doesn't double query the object
with self.assertNumQueries(baseline + 7):
# We call this endpoint as we don't want to include all the extra queries that rendering the project uses
self.client.get("/api/projects/@current/is_generating_demo_data")
# When accessing the list of notebooks we have extra queries due to checking for role based access and filtering out items
baseline = 7
with self.assertNumQueries(baseline + 4): # org, roles, preloaded access controls
self.client.get("/api/projects/@current/notebooks/")
def test_query_counts_with_preload_optimization(self):
self._org_membership(OrganizationMembership.Level.MEMBER)
my_dashboard = Dashboard.objects.create(team=self.team, created_by=self.user)
other_user_dashboard = Dashboard.objects.create(team=self.team, created_by=self.other_user)
# Baseline query (triggers any first time cache things)
self.client.get(f"/api/projects/@current/notebooks/{self.notebook.short_id}")
baseline = 11
# Access controls total 2 extra queries - 1 for org membership, 1 for the user roles, 1 for the preloaded access controls
with self.assertNumQueries(baseline + 4):
self.client.get(f"/api/projects/@current/dashboards/{my_dashboard.id}?no_items_field=true")
# Accessing a different users dashboard doesn't +1 as the preload works using the pk
with self.assertNumQueries(baseline + 4):
self.client.get(f"/api/projects/@current/dashboards/{other_user_dashboard.id}?no_items_field=true")
def test_query_counts_only_adds_1_for_non_pk_resources(self):
self._org_membership(OrganizationMembership.Level.MEMBER)
# Baseline query (triggers any first time cache things)
self.client.get(f"/api/projects/@current/notebooks/{self.notebook.short_id}")
baseline = 11
baseline = 6
# Getting my own notebook is the same as a dashboard - 2 extra queries
with self.assertNumQueries(baseline + 4):
self.client.get(f"/api/projects/@current/notebooks/{self.notebook.short_id}")
# Except when accessing a different notebook where we _also_ need to check as we are not the creator and the pk is not the same (short_id)
with self.assertNumQueries(baseline + 5):
self.client.get(f"/api/projects/@current/notebooks/{self.other_user_notebook.short_id}")
def test_query_counts_stable_for_project_access(self):
self._org_membership(OrganizationMembership.Level.MEMBER)
baseline = 4
# Project access doesn't double query the object
with self.assertNumQueries(baseline + 7):
# We call this endpoint as we don't want to include all the extra queries that rendering the project uses
self.client.get("/api/projects/@current/is_generating_demo_data")
# When accessing the list of notebooks we have extra queries due to checking for role based access and filtering out items
baseline = 7
with self.assertNumQueries(baseline + 4): # org, roles, preloaded access controls
self.client.get("/api/projects/@current/notebooks/")
def test_query_counts_stable_when_listing_resources(self):
# When accessing the list of notebooks we have extra queries due to checking for role based access and filtering out items
baseline = 7
with self.assertNumQueries(baseline + 4): # org, roles, preloaded access controls
self.client.get("/api/projects/@current/notebooks/")
def test_query_counts_stable_when_listing_resources_including_access_control_info(self):
for i in range(10):
FeatureFlag.objects.create(team=self.team, created_by=self.other_user, key=f"flag-{i}")
baseline = 42 # This is a lot! There is currently an n+1 issue with the legacy access control system
with self.assertNumQueries(baseline + 6): # org, roles, preloaded permissions acs, preloaded acs for the list
self.client.get("/api/projects/@current/feature_flags/")
for i in range(10):
FeatureFlag.objects.create(team=self.team, created_by=self.other_user, key=f"flag-{10 + i}")
baseline = baseline + (10 * 3) # The existing access control adds 3 queries per item :(
with self.assertNumQueries(baseline + 6): # org, roles, preloaded permissions acs, preloaded acs for the list
self.client.get("/api/projects/@current/feature_flags/")
class TestAccessControlFiltering(BaseAccessControlTest):
def setUp(self):
super().setUp()
self.other_user = self._create_user("other_user")
self.other_user_notebook = Notebook.objects.create(
team=self.team, created_by=self.other_user, title="not my notebook"
)
self.notebook = Notebook.objects.create(team=self.team, created_by=self.user, title="my notebook")
def _put_notebook_access_control(self, notebook_id: str, data=None):
payload = {
"access_level": "editor",
}
if data:
payload.update(data)
return self.client.put(
f"/api/projects/@current/notebooks/{notebook_id}/access_controls",
payload,
)
def _get_notebooks(self):
return self.client.get("/api/projects/@current/notebooks/")
def test_default_allows_all_access(self):
self._org_membership(OrganizationMembership.Level.MEMBER)
assert len(self._get_notebooks().json()["results"]) == 2
def test_does_not_list_notebooks_without_access(self):
self._org_membership(OrganizationMembership.Level.ADMIN)
assert (
self._put_notebook_access_control(self.other_user_notebook.short_id, {"access_level": "none"}).status_code
== status.HTTP_200_OK
)
assert (
self._put_notebook_access_control(self.notebook.short_id, {"access_level": "none"}).status_code
== status.HTTP_200_OK
)
self._org_membership(OrganizationMembership.Level.MEMBER)
res = self._get_notebooks()
assert len(res.json()["results"]) == 1
assert res.json()["results"][0]["id"] == str(self.notebook.id)
def test_list_notebooks_with_explicit_access(self):
self._org_membership(OrganizationMembership.Level.ADMIN)
assert (
self._put_notebook_access_control(self.other_user_notebook.short_id, {"access_level": "none"}).status_code
== status.HTTP_200_OK
)
assert (
self._put_notebook_access_control(
self.other_user_notebook.short_id,
{"organization_member": str(self.organization_membership.id), "access_level": "viewer"},
).status_code
== status.HTTP_200_OK
)
self._org_membership(OrganizationMembership.Level.MEMBER)
res = self._get_notebooks()
assert len(res.json()["results"]) == 2
def test_search_results_exclude_restricted_objects(self):
res = self.client.get("/api/projects/@current/search?q=my notebook")
assert len(res.json()["results"]) == 2
self._org_membership(OrganizationMembership.Level.ADMIN)
assert (
self._put_notebook_access_control(self.other_user_notebook.short_id, {"access_level": "none"}).status_code
== status.HTTP_200_OK
)
self._org_membership(OrganizationMembership.Level.MEMBER)
res = self.client.get("/api/projects/@current/search?q=my notebook")
assert len(res.json()["results"]) == 1
class TestAccessControlProjectFiltering(BaseAccessControlTest):
"""
Projects are listed in multiple places and ways so we need to test all of them here
"""
def setUp(self):
super().setUp()
self.other_team = Team.objects.create(organization=self.organization, name="other team")
self.other_team_2 = Team.objects.create(organization=self.organization, name="other team 2")
def _put_project_access_control_as_admin(self, team_id: int, data=None):
self._org_membership(OrganizationMembership.Level.ADMIN)
payload = {
"access_level": "editor",
}
if data:
payload.update(data)
res = self.client.put(
f"/api/projects/{team_id}/access_controls",
payload,
)
self._org_membership(OrganizationMembership.Level.MEMBER)
assert res.status_code == status.HTTP_200_OK, res.json()
return res
def _get_posthog_app_context(self):
mock_template = MagicMock()
with patch("posthog.utils.get_template", return_value=mock_template):
mock_request = MagicMock()
mock_request.user = self.user
mock_request.GET = {}
render_template("index.html", request=mock_request, context={})
# Get the context passed to the template
return json.loads(mock_template.render.call_args[0][0]["posthog_app_context"])
def test_default_lists_all_projects(self):
assert len(self.client.get("/api/projects").json()["results"]) == 3
me_response = self.client.get("/api/users/@me").json()
assert len(me_response["organization"]["teams"]) == 3
def test_does_not_list_projects_without_access(self):
self._put_project_access_control_as_admin(self.other_team.id, {"access_level": "none"})
assert len(self.client.get("/api/projects").json()["results"]) == 2
me_response = self.client.get("/api/users/@me").json()
assert len(me_response["organization"]["teams"]) == 2
def test_always_lists_all_projects_if_org_admin(self):
self._put_project_access_control_as_admin(self.other_team.id, {"access_level": "none"})
self._org_membership(OrganizationMembership.Level.ADMIN)
assert len(self.client.get("/api/projects").json()["results"]) == 3
me_response = self.client.get("/api/users/@me").json()
assert len(me_response["organization"]["teams"]) == 3
def test_template_render_filters_teams(self):
app_context = self._get_posthog_app_context()
assert len(app_context["current_user"]["organization"]["teams"]) == 3
assert app_context["current_team"]["id"] == self.team.id
assert app_context["current_team"]["user_access_level"] == "admin"
self._put_project_access_control_as_admin(self.team.id, {"access_level": "none"})
app_context = self._get_posthog_app_context()
assert len(app_context["current_user"]["organization"]["teams"]) == 2
assert app_context["current_team"]["id"] == self.team.id
assert app_context["current_team"]["user_access_level"] == "none"
# TODO: Add tests to check that a dashboard can't be edited if the user doesn't have access

View File

@ -5,9 +5,3 @@
ALTER TABLE sharded_performance_events ON CLUSTER 'posthog' MODIFY TTL toDate(timestamp) + toIntervalWeek(5)
'''
# ---
# name: TestInstanceSettings.test_update_recordings_ttl_setting
'''
/* user_id:0 request:_snapshot_ */
ALTER TABLE sharded_session_recording_events ON CLUSTER 'posthog' MODIFY TTL toDate(created_at) + toIntervalWeek(5)
'''
# ---

View File

@ -101,20 +101,6 @@
LIMIT 100
'''
# ---
# name: TestOrganizationResourceAccessAPI.test_list_organization_resource_access_is_not_nplus1.14
'''
SELECT "ee_organizationresourceaccess"."id",
"ee_organizationresourceaccess"."resource",
"ee_organizationresourceaccess"."access_level",
"ee_organizationresourceaccess"."organization_id",
"ee_organizationresourceaccess"."created_by_id",
"ee_organizationresourceaccess"."created_at",
"ee_organizationresourceaccess"."updated_at"
FROM "ee_organizationresourceaccess"
WHERE "ee_organizationresourceaccess"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
LIMIT 100
'''
# ---
# name: TestOrganizationResourceAccessAPI.test_list_organization_resource_access_is_not_nplus1.2
'''
SELECT "posthog_organization"."id",

View File

@ -79,7 +79,8 @@ class TestActionApi(APIBaseTest):
# django_session + user + team + look up if rate limit is enabled (cached after first lookup)
# + organizationmembership + organization + action + taggeditem
with self.assertNumQueries(8):
# + access control queries
with self.assertNumQueries(12):
response = self.client.get(f"/api/projects/{self.team.id}/actions")
self.assertEqual(response.json()["results"][0]["tags"][0], "tag")
self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@ -133,7 +133,9 @@ class TestOrganizationEnterpriseAPI(APILicensedTest):
response.json(),
{
"attr": None,
"detail": "Your organization access level is insufficient.",
"detail": "You do not have admin access to this resource."
if level == OrganizationMembership.Level.MEMBER
else "Your organization access level is insufficient.",
"code": "permission_denied",
"type": "authentication_error",
},
@ -196,7 +198,7 @@ class TestOrganizationEnterpriseAPI(APILicensedTest):
expected_response = {
"attr": None,
"detail": "Your organization access level is insufficient.",
"detail": "You do not have admin access to this resource.",
"code": "permission_denied",
"type": "authentication_error",
}

View File

@ -118,7 +118,7 @@ class TestProjectEnterpriseAPI(team_enterprise_api_test_factory()):
projects_response = self.client.get(f"/api/environments/")
# 9 (above):
with self.assertNumQueries(FuzzyInt(10, 11)):
with self.assertNumQueries(FuzzyInt(13, 14)):
current_org_response = self.client.get(f"/api/organizations/{self.organization.id}/")
self.assertEqual(projects_response.status_code, 200)

View File

@ -621,7 +621,7 @@ class TestTeamEnterpriseAPI(team_enterprise_api_test_factory()):
projects_response = self.client.get(f"/api/environments/")
# 9 (above):
with self.assertNumQueries(FuzzyInt(10, 11)):
with self.assertNumQueries(FuzzyInt(13, 14)):
current_org_response = self.client.get(f"/api/organizations/{self.organization.id}/")
self.assertEqual(projects_response.status_code, HTTP_200_OK)

View File

@ -57,7 +57,7 @@ class TestExperimentCRUD(APILicensedTest):
format="json",
).json()
with self.assertNumQueries(FuzzyInt(9, 10)):
with self.assertNumQueries(FuzzyInt(13, 14)):
response = self.client.get(f"/api/projects/{self.team.id}/experiments")
self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -74,7 +74,7 @@ class TestExperimentCRUD(APILicensedTest):
format="json",
).json()
with self.assertNumQueries(FuzzyInt(9, 10)):
with self.assertNumQueries(FuzzyInt(13, 14)):
response = self.client.get(f"/api/projects/{self.team.id}/experiments")
self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -1452,7 +1452,7 @@ class TestExperimentCRUD(APILicensedTest):
).json()
# TODO: Make sure permission bool doesn't cause n + 1
with self.assertNumQueries(12):
with self.assertNumQueries(17):
response = self.client.get(f"/api/projects/{self.team.id}/feature_flags")
self.assertEqual(response.status_code, status.HTTP_200_OK)
result = response.json()

View File

@ -14,7 +14,7 @@ export const stackFrameLogic = kea<stackFrameLogicType>([
loadFrameContexts: async ({ frames }: { frames: ErrorTrackingStackFrame[] }) => {
const loadedFrameIds = Object.keys(values.frameContexts)
const ids = frames
.filter(({ raw_id }) => loadedFrameIds.includes(raw_id))
.filter(({ raw_id }) => !loadedFrameIds.includes(raw_id))
.map(({ raw_id }) => raw_id)
const response = await api.errorTracking.fetchStackFrames(ids)
return { ...values.frameContexts, ...response }

View File

@ -15,6 +15,8 @@ from rest_framework.utils.serializer_helpers import ReturnDict
from posthog.api.dashboards.dashboard_template_json_schema_parser import (
DashboardTemplateCreationJSONSchemaParser,
)
from posthog.rbac.access_control_api_mixin import AccessControlViewSetMixin
from posthog.rbac.user_access_control import UserAccessControlSerializerMixin
from posthog.api.forbid_destroy_model import ForbidDestroyModel
from posthog.api.insight import InsightSerializer, InsightViewSet
from posthog.api.monitoring import Feature, monitor
@ -87,6 +89,7 @@ class DashboardBasicSerializer(
TaggedItemSerializerMixin,
serializers.ModelSerializer,
UserPermissionsSerializerMixin,
UserAccessControlSerializerMixin,
):
created_by = UserBasicSerializer(read_only=True)
effective_privilege_level = serializers.SerializerMethodField()
@ -109,6 +112,7 @@ class DashboardBasicSerializer(
"restriction_level",
"effective_restriction_level",
"effective_privilege_level",
"user_access_level",
]
read_only_fields = fields
@ -157,8 +161,9 @@ class DashboardSerializer(DashboardBasicSerializer):
"restriction_level",
"effective_restriction_level",
"effective_privilege_level",
"user_access_level",
]
read_only_fields = ["creation_mode", "effective_restriction_level", "is_shared"]
read_only_fields = ["creation_mode", "effective_restriction_level", "is_shared", "user_access_level"]
def validate_filters(self, value) -> dict:
if not isinstance(value, dict):
@ -450,6 +455,7 @@ class DashboardSerializer(DashboardBasicSerializer):
class DashboardsViewSet(
TeamAndOrgViewSetMixin,
AccessControlViewSetMixin,
TaggedItemViewSetMixin,
ForbidDestroyModel,
viewsets.ModelViewSet,

View File

@ -37,7 +37,7 @@ class PersonalAPIKeyScheme(OpenApiAuthenticationExtension):
for permission in auto_schema.view.get_permissions():
if isinstance(permission, APIScopePermission):
try:
scopes = permission.get_required_scopes(request, view)
scopes = permission._get_required_scopes(request, view)
return [{self.name: scopes}]
except (PermissionDenied, ImproperlyConfigured):
# NOTE: This should never happen - it indicates that we shouldn't be including it in the docs

View File

@ -19,6 +19,8 @@ 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.rbac.access_control_api_mixin import AccessControlViewSetMixin
from posthog.rbac.user_access_control import UserAccessControlSerializerMixin
from posthog.api.documentation import extend_schema
from posthog.api.forbid_destroy_model import ForbidDestroyModel
@ -88,7 +90,9 @@ class CanEditFeatureFlag(BasePermission):
return can_user_edit_feature_flag(request, feature_flag)
class FeatureFlagSerializer(TaggedItemSerializerMixin, serializers.HyperlinkedModelSerializer):
class FeatureFlagSerializer(
TaggedItemSerializerMixin, UserAccessControlSerializerMixin, serializers.HyperlinkedModelSerializer
):
created_by = UserBasicSerializer(read_only=True)
# :TRICKY: Needed for backwards compatibility
filters = serializers.DictField(source="get_filters", required=False)
@ -147,12 +151,16 @@ class FeatureFlagSerializer(TaggedItemSerializerMixin, serializers.HyperlinkedMo
"usage_dashboard",
"analytics_dashboards",
"has_enriched_analytics",
"user_access_level",
"creation_context",
]
def get_can_edit(self, feature_flag: FeatureFlag) -> bool:
# TODO: make sure this isn't n+1
return can_user_edit_feature_flag(self.context["request"], feature_flag)
return (
can_user_edit_feature_flag(self.context["request"], feature_flag)
and self.get_user_access_level(feature_flag) == "editor"
)
# Simple flags are ones that only have rollout_percentage
#  That means server side libraries are able to gate these flags without calling to the server
@ -435,6 +443,7 @@ class MinimalFeatureFlagSerializer(serializers.ModelSerializer):
class FeatureFlagViewSet(
TeamAndOrgViewSetMixin,
AccessControlViewSetMixin,
TaggedItemViewSetMixin,
ForbidDestroyModel,
viewsets.ModelViewSet,

View File

@ -107,6 +107,8 @@ from posthog.rate_limit import (
ClickHouseBurstRateThrottle,
ClickHouseSustainedRateThrottle,
)
from posthog.rbac.access_control_api_mixin import AccessControlViewSetMixin
from posthog.rbac.user_access_control import UserAccessControlSerializerMixin
from posthog.settings import CAPTURE_TIME_TO_SEE_DATA, SITE_URL
from posthog.user_permissions import UserPermissionsSerializerMixin
from posthog.utils import (
@ -250,7 +252,7 @@ class InsightBasicSerializer(TaggedItemSerializerMixin, serializers.ModelSeriali
return [tile.dashboard_id for tile in instance.dashboard_tiles.all()]
class InsightSerializer(InsightBasicSerializer, UserPermissionsSerializerMixin):
class InsightSerializer(InsightBasicSerializer, UserPermissionsSerializerMixin, UserAccessControlSerializerMixin):
result = serializers.SerializerMethodField()
hasMore = serializers.SerializerMethodField()
columns = serializers.SerializerMethodField()
@ -332,6 +334,7 @@ class InsightSerializer(InsightBasicSerializer, UserPermissionsSerializerMixin):
"is_sample",
"effective_restriction_level",
"effective_privilege_level",
"user_access_level",
"timezone",
"is_cached",
"query_status",
@ -348,6 +351,7 @@ class InsightSerializer(InsightBasicSerializer, UserPermissionsSerializerMixin):
"is_sample",
"effective_restriction_level",
"effective_privilege_level",
"user_access_level",
"timezone",
"refreshing",
"is_cached",
@ -710,6 +714,7 @@ Background calculation can be tracked using the `query_status` response field.""
)
class InsightViewSet(
TeamAndOrgViewSetMixin,
AccessControlViewSetMixin,
TaggedItemViewSetMixin,
ForbidDestroyModel,
viewsets.ModelViewSet,

View File

@ -14,6 +14,8 @@ from drf_spectacular.utils import (
extend_schema_view,
OpenApiExample,
)
from posthog.rbac.access_control_api_mixin import AccessControlViewSetMixin
from posthog.rbac.user_access_control import UserAccessControlSerializerMixin
from rest_framework import serializers, viewsets
from rest_framework.request import Request
from rest_framework.response import Response
@ -95,7 +97,7 @@ class NotebookMinimalSerializer(serializers.ModelSerializer):
read_only_fields = fields
class NotebookSerializer(NotebookMinimalSerializer):
class NotebookSerializer(NotebookMinimalSerializer, UserAccessControlSerializerMixin):
class Meta:
model = Notebook
fields = [
@ -110,6 +112,7 @@ class NotebookSerializer(NotebookMinimalSerializer):
"created_by",
"last_modified_at",
"last_modified_by",
"user_access_level",
]
read_only_fields = [
"id",
@ -118,6 +121,7 @@ class NotebookSerializer(NotebookMinimalSerializer):
"created_by",
"last_modified_at",
"last_modified_by",
"user_access_level",
]
def create(self, validated_data: dict, *args, **kwargs) -> Notebook:
@ -235,7 +239,7 @@ class NotebookSerializer(NotebookMinimalSerializer):
],
)
)
class NotebookViewSet(TeamAndOrgViewSetMixin, ForbidDestroyModel, viewsets.ModelViewSet):
class NotebookViewSet(TeamAndOrgViewSetMixin, AccessControlViewSetMixin, ForbidDestroyModel, viewsets.ModelViewSet):
scope_object = "notebook"
queryset = Notebook.objects.all()
filter_backends = [DjangoFilterBackend]

View File

@ -3,7 +3,6 @@ from typing import Any, Optional, Union, cast
from django.db.models import Model, QuerySet
from django.shortcuts import get_object_or_404
from django.views import View
from rest_framework import exceptions, permissions, serializers, viewsets
from rest_framework.request import Request
@ -16,12 +15,13 @@ from posthog.constants import INTERNAL_BOT_EMAIL_SUFFIX, AvailableFeature
from posthog.event_usage import report_organization_deleted
from posthog.models import Organization, User
from posthog.models.async_deletion import AsyncDeletion, DeletionType
from posthog.rbac.user_access_control import UserAccessControlSerializerMixin
from posthog.models.organization import OrganizationMembership
from posthog.models.signals import mute_selected_signals
from posthog.models.team.util import delete_bulky_postgres_data
from posthog.models.uploaded_media import UploadedMedia
from posthog.permissions import (
CREATE_METHODS,
CREATE_ACTIONS,
APIScopePermission,
OrganizationAdminWritePermissions,
TimeSensitiveActionPermission,
@ -40,7 +40,7 @@ class PremiumMultiorganizationPermissions(permissions.BasePermission):
if (
# Make multiple orgs only premium on self-hosted, since enforcement of this wouldn't make sense on Cloud
not is_cloud()
and request.method in CREATE_METHODS
and view.action in CREATE_ACTIONS
and (
user.organization is None
or not user.organization.is_feature_available(AvailableFeature.ORGANIZATIONS_PROJECTS)
@ -52,7 +52,7 @@ class PremiumMultiorganizationPermissions(permissions.BasePermission):
class OrganizationPermissionsWithDelete(OrganizationAdminWritePermissions):
def has_object_permission(self, request: Request, view: View, object: Model) -> bool:
def has_object_permission(self, request: Request, view, object: Model) -> bool:
if request.method in permissions.SAFE_METHODS:
return True
# TODO: Optimize so that this computation is only done once, on `OrganizationMemberPermissions`
@ -66,7 +66,9 @@ class OrganizationPermissionsWithDelete(OrganizationAdminWritePermissions):
)
class OrganizationSerializer(serializers.ModelSerializer, UserPermissionsSerializerMixin):
class OrganizationSerializer(
serializers.ModelSerializer, UserPermissionsSerializerMixin, UserAccessControlSerializerMixin
):
membership_level = serializers.SerializerMethodField()
teams = serializers.SerializerMethodField()
projects = serializers.SerializerMethodField()
@ -127,7 +129,14 @@ class OrganizationSerializer(serializers.ModelSerializer, UserPermissionsSeriali
return membership.level if membership is not None else None
def get_teams(self, instance: Organization) -> list[dict[str, Any]]:
visible_teams = instance.teams.filter(id__in=self.user_permissions.team_ids_visible_for_user)
# Support new access control system
visible_teams = (
self.user_access_control.filter_queryset_by_access_level(instance.teams.all(), include_all_if_admin=True)
if self.user_access_control
else instance.teams.none()
)
# Support old access control system
visible_teams = visible_teams.filter(id__in=self.user_permissions.team_ids_visible_for_user)
return TeamBasicSerializer(visible_teams, context=self.context, many=True).data # type: ignore
def get_projects(self, instance: Organization) -> list[dict[str, Any]]:

View File

@ -2,7 +2,6 @@ from typing import cast
from django.db.models import Model, Prefetch, QuerySet, F
from django.shortcuts import get_object_or_404
from django.views import View
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
@ -23,7 +22,7 @@ class OrganizationMemberObjectPermissions(BasePermission):
message = "Your cannot edit other organization members."
def has_object_permission(self, request: Request, view: View, membership: OrganizationMembership) -> bool:
def has_object_permission(self, request: Request, view, membership: OrganizationMembership) -> bool:
if request.method in SAFE_METHODS:
return True
organization = extract_organization(membership, view)

View File

@ -15,6 +15,7 @@ from posthog.api.team import (
TeamSerializer,
validate_team_attrs,
)
from ee.api.rbac.access_control import AccessControlViewSetMixin
from posthog.auth import PersonalAPIKeyAuthentication
from posthog.event_usage import report_user_action
from posthog.geoip import get_geoip_properties
@ -395,7 +396,7 @@ class ProjectBackwardCompatSerializer(ProjectBackwardCompatBasicSerializer, User
return instance
class ProjectViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet):
class ProjectViewSet(TeamAndOrgViewSetMixin, AccessControlViewSetMixin, viewsets.ModelViewSet):
"""
Projects for the current organization.
"""

View File

@ -24,10 +24,12 @@ from posthog.models.team import Team
from posthog.models.user import User
from posthog.permissions import (
APIScopePermission,
AccessControlPermission,
OrganizationMemberPermissions,
SharingTokenPermission,
TeamMemberAccessPermission,
)
from posthog.rbac.user_access_control import UserAccessControl
from posthog.user_permissions import UserPermissions
if TYPE_CHECKING:
@ -101,7 +103,7 @@ class TeamAndOrgViewSetMixin(_GenericViewSet): # TODO: Rename to include "Env"
# 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, APIScopePermission]
permission_classes: list = [IsAuthenticated, APIScopePermission, AccessControlPermission]
if self._is_team_view or self._is_project_view:
permission_classes.append(TeamMemberAccessPermission)
@ -145,19 +147,47 @@ class TeamAndOrgViewSetMixin(_GenericViewSet): # TODO: Rename to include "Env"
raise NotImplementedError()
def get_queryset(self) -> QuerySet:
try:
return self.dangerously_get_queryset()
except NotImplementedError:
pass
# Add a recursion guard
if getattr(self, "_in_get_queryset", False):
return super().get_queryset()
queryset = super().get_queryset()
# First of all make sure we do the custom filters before applying our own
try:
queryset = self.safely_get_queryset(queryset)
except NotImplementedError:
pass
self._in_get_queryset = True
return self._filter_queryset_by_parents_lookups(queryset)
try:
return self.dangerously_get_queryset()
except NotImplementedError:
pass
queryset = super().get_queryset()
# First of all make sure we do the custom filters before applying our own
try:
queryset = self.safely_get_queryset(queryset)
except NotImplementedError:
pass
queryset = self._filter_queryset_by_parents_lookups(queryset)
if self.action != "list":
# NOTE: If we are getting an individual object then we don't filter it out here - this is handled by the permission logic
# The reason being, that if we filter out here already, we can't load the object which is required for checking access controls for it
return queryset
# NOTE: Half implemented - for admins, they may want to include listing of results that are not accessible (like private resources)
include_all_if_admin = self.request.GET.get("admin_include_all") == "true"
# Additionally "projects" is a special one where we always want to include all projects if you're an org admin
if self.scope_object == "project":
include_all_if_admin = True
# Only apply access control filter if we're not already in a recursive call
queryset = self.user_access_control.filter_queryset_by_access_level(
queryset, include_all_if_admin=include_all_if_admin
)
return queryset
finally:
self._in_get_queryset = False
def dangerously_get_object(self) -> Any:
"""
@ -408,3 +438,13 @@ class TeamAndOrgViewSetMixin(_GenericViewSet): # TODO: Rename to include "Env"
@cached_property
def user_permissions(self) -> "UserPermissions":
return UserPermissions(user=cast(User, self.request.user), team=self.team)
@cached_property
def user_access_control(self) -> "UserAccessControl":
team: Optional[Team] = None
try:
team = self.team
except (Team.DoesNotExist, KeyError):
pass
return UserAccessControl(user=cast(User, self.request.user), team=team, organization_id=self.organization_id)

View File

@ -100,6 +100,7 @@ class SearchViewSet(TeamAndOrgViewSetMixin, viewsets.ViewSet):
# add entities
for entity_meta in [ENTITY_MAP[entity] for entity in entities]:
klass_qs, entity_name = class_queryset(
view=self,
klass=entity_meta.get("klass"),
project_id=self.project_id,
query=query,
@ -134,6 +135,7 @@ def process_query(query: str):
def class_queryset(
view: TeamAndOrgViewSetMixin,
klass: type[Model],
project_id: int,
query: str | None,
@ -145,6 +147,7 @@ def class_queryset(
values = ["type", "result_id", "extra_fields"]
qs: QuerySet[Any] = klass.objects.filter(team__project_id=project_id) # filter team
qs = view.user_access_control.filter_queryset_by_access_level(qs) # filter access level
# :TRICKY: can't use an annotation here as `type` conflicts with a field on some models
qs = qs.extra(select={"type": f"'{entity_type}'"}) # entity type

View File

@ -24,6 +24,8 @@ from posthog.models.activity_logging.activity_log import (
load_activity,
log_activity,
)
from posthog.rbac.access_control_api_mixin import AccessControlViewSetMixin
from posthog.rbac.user_access_control import UserAccessControlSerializerMixin
from posthog.models.activity_logging.activity_page import activity_page_response
from posthog.models.async_deletion import AsyncDeletion, DeletionType
from posthog.models.group_type_mapping import GroupTypeMapping
@ -35,8 +37,9 @@ from posthog.models.signals import mute_selected_signals
from posthog.models.team.util import delete_batch_exports, delete_bulky_postgres_data
from posthog.models.utils import UUIDT
from posthog.permissions import (
CREATE_METHODS,
CREATE_ACTIONS,
APIScopePermission,
AccessControlPermission,
OrganizationAdminWritePermissions,
OrganizationMemberPermissions,
TeamMemberLightManagementPermission,
@ -57,7 +60,7 @@ class PremiumMultiProjectPermissions(BasePermission): # TODO: Rename to include
message = "You must upgrade your PostHog plan to be able to create and manage multiple projects or environments."
def has_permission(self, request: request.Request, view) -> bool:
if request.method in CREATE_METHODS:
if view.action in CREATE_ACTIONS:
try:
organization = get_organization_from_view(view)
except ValueError:
@ -140,7 +143,7 @@ class CachingTeamSerializer(serializers.ModelSerializer):
]
class TeamSerializer(serializers.ModelSerializer, UserPermissionsSerializerMixin):
class TeamSerializer(serializers.ModelSerializer, UserPermissionsSerializerMixin, UserAccessControlSerializerMixin):
instance: Optional[Team]
effective_membership_level = serializers.SerializerMethodField()
@ -207,6 +210,7 @@ class TeamSerializer(serializers.ModelSerializer, UserPermissionsSerializerMixin
"live_events_token",
"product_intents",
"capture_dead_clicks",
"user_access_level",
)
read_only_fields = (
"id",
@ -222,9 +226,11 @@ class TeamSerializer(serializers.ModelSerializer, UserPermissionsSerializerMixin
"default_modifiers",
"person_on_events_querying_enabled",
"live_events_token",
"user_access_level",
)
def get_effective_membership_level(self, team: Team) -> Optional[OrganizationMembership.Level]:
# TODO: Map from user_access_controls
return self.user_permissions.team(team).effective_membership_level
def get_has_group_types(self, team: Team) -> bool:
@ -444,7 +450,7 @@ class TeamSerializer(serializers.ModelSerializer, UserPermissionsSerializerMixin
return updated_team
class TeamViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet):
class TeamViewSet(TeamAndOrgViewSetMixin, AccessControlViewSetMixin, viewsets.ModelViewSet):
"""
Projects for the current organization.
"""
@ -481,6 +487,7 @@ class TeamViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet):
permissions: list = [
IsAuthenticated,
APIScopePermission,
AccessControlPermission,
PremiumMultiProjectPermissions,
*self.permission_classes,
]

View File

@ -96,216 +96,97 @@
# ---
# name: TestActionApi.test_listing_actions_is_not_nplus1.10
'''
SELECT "posthog_user"."id",
"posthog_user"."password",
"posthog_user"."last_login",
"posthog_user"."first_name",
"posthog_user"."last_name",
"posthog_user"."is_staff",
"posthog_user"."date_joined",
"posthog_user"."uuid",
"posthog_user"."current_organization_id",
"posthog_user"."current_team_id",
"posthog_user"."email",
"posthog_user"."pending_email",
"posthog_user"."temporary_token",
"posthog_user"."distinct_id",
"posthog_user"."is_email_verified",
"posthog_user"."has_seen_product_intro_for",
"posthog_user"."strapi_id",
"posthog_user"."is_active",
"posthog_user"."theme_mode",
"posthog_user"."partial_notification_settings",
"posthog_user"."anonymize_data",
"posthog_user"."toolbar_mode",
"posthog_user"."hedgehog_config",
"posthog_user"."events_column_config",
"posthog_user"."email_opt_in"
FROM "posthog_user"
WHERE "posthog_user"."id" = 99999
SELECT "posthog_project"."id",
"posthog_project"."organization_id",
"posthog_project"."name",
"posthog_project"."created_at",
"posthog_project"."product_description"
FROM "posthog_project"
WHERE "posthog_project"."id" = 99999
LIMIT 21
'''
# ---
# name: TestActionApi.test_listing_actions_is_not_nplus1.11
'''
SELECT "posthog_team"."id",
"posthog_team"."uuid",
"posthog_team"."organization_id",
"posthog_team"."project_id",
"posthog_team"."api_token",
"posthog_team"."app_urls",
"posthog_team"."name",
"posthog_team"."slack_incoming_webhook",
"posthog_team"."created_at",
"posthog_team"."updated_at",
"posthog_team"."anonymize_ips",
"posthog_team"."completed_snippet_onboarding",
"posthog_team"."has_completed_onboarding_for",
"posthog_team"."ingested_event",
"posthog_team"."autocapture_opt_out",
"posthog_team"."autocapture_web_vitals_opt_in",
"posthog_team"."autocapture_web_vitals_allowed_metrics",
"posthog_team"."autocapture_exceptions_opt_in",
"posthog_team"."autocapture_exceptions_errors_to_ignore",
"posthog_team"."person_processing_opt_out",
"posthog_team"."session_recording_opt_in",
"posthog_team"."session_recording_sample_rate",
"posthog_team"."session_recording_minimum_duration_milliseconds",
"posthog_team"."session_recording_linked_flag",
"posthog_team"."session_recording_network_payload_capture_config",
"posthog_team"."session_recording_url_trigger_config",
"posthog_team"."session_recording_url_blocklist_config",
"posthog_team"."session_recording_event_trigger_config",
"posthog_team"."session_replay_config",
"posthog_team"."survey_config",
"posthog_team"."capture_console_log_opt_in",
"posthog_team"."capture_performance_opt_in",
"posthog_team"."capture_dead_clicks",
"posthog_team"."surveys_opt_in",
"posthog_team"."heatmaps_opt_in",
"posthog_team"."session_recording_version",
"posthog_team"."signup_token",
"posthog_team"."is_demo",
"posthog_team"."access_control",
"posthog_team"."week_start_day",
"posthog_team"."inject_web_apps",
"posthog_team"."test_account_filters",
"posthog_team"."test_account_filters_default_checked",
"posthog_team"."path_cleaning_filters",
"posthog_team"."timezone",
"posthog_team"."data_attributes",
"posthog_team"."person_display_name_properties",
"posthog_team"."live_events_columns",
"posthog_team"."recording_domains",
"posthog_team"."primary_dashboard_id",
"posthog_team"."extra_settings",
"posthog_team"."modifiers",
"posthog_team"."correlation_config",
"posthog_team"."session_recording_retention_period_days",
"posthog_team"."external_data_workspace_id",
"posthog_team"."external_data_workspace_last_synced_at"
FROM "posthog_team"
WHERE "posthog_team"."id" = 99999
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
"posthog_organizationmembership"."user_id",
"posthog_organizationmembership"."level",
"posthog_organizationmembership"."joined_at",
"posthog_organizationmembership"."updated_at",
"posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organizationmembership"
INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
WHERE ("posthog_organizationmembership"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
AND "posthog_organizationmembership"."user_id" = 99999)
LIMIT 21
'''
# ---
# name: TestActionApi.test_listing_actions_is_not_nplus1.12
'''
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
"posthog_organizationmembership"."user_id",
"posthog_organizationmembership"."level",
"posthog_organizationmembership"."joined_at",
"posthog_organizationmembership"."updated_at",
"posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organizationmembership"
INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
WHERE "posthog_organizationmembership"."user_id" = 99999
SELECT "ee_accesscontrol"."id",
"ee_accesscontrol"."team_id",
"ee_accesscontrol"."access_level",
"ee_accesscontrol"."resource",
"ee_accesscontrol"."resource_id",
"ee_accesscontrol"."organization_member_id",
"ee_accesscontrol"."role_id",
"ee_accesscontrol"."created_by_id",
"ee_accesscontrol"."created_at",
"ee_accesscontrol"."updated_at"
FROM "ee_accesscontrol"
LEFT OUTER JOIN "posthog_organizationmembership" ON ("ee_accesscontrol"."organization_member_id" = "posthog_organizationmembership"."id")
WHERE (("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'project'
AND "ee_accesscontrol"."resource_id" = '99'
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'project'
AND "ee_accesscontrol"."resource_id" = '99'
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'action'
AND "ee_accesscontrol"."resource_id" IS NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'action'
AND "ee_accesscontrol"."resource_id" IS NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'action'
AND "ee_accesscontrol"."resource_id" IS NOT NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'action'
AND "ee_accesscontrol"."resource_id" IS NOT NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999))
'''
# ---
# name: TestActionApi.test_listing_actions_is_not_nplus1.13
'''
SELECT "posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organization"
WHERE "posthog_organization"."id" = '00000000-0000-0000-0000-000000000000'::uuid
LIMIT 21
'''
# ---
# name: TestActionApi.test_listing_actions_is_not_nplus1.14
'''
SELECT "posthog_action"."id",
"posthog_action"."name",
"posthog_action"."team_id",
"posthog_action"."description",
"posthog_action"."created_at",
"posthog_action"."created_by_id",
"posthog_action"."deleted",
"posthog_action"."post_to_slack",
"posthog_action"."slack_message_format",
"posthog_action"."updated_at",
"posthog_action"."bytecode",
"posthog_action"."bytecode_error",
"posthog_action"."steps_json",
"posthog_action"."pinned_at",
"posthog_action"."is_calculating",
"posthog_action"."last_calculated_at",
COUNT("posthog_action_events"."event_id") AS "count",
"posthog_user"."id",
"posthog_user"."password",
"posthog_user"."last_login",
"posthog_user"."first_name",
"posthog_user"."last_name",
"posthog_user"."is_staff",
"posthog_user"."date_joined",
"posthog_user"."uuid",
"posthog_user"."current_organization_id",
"posthog_user"."current_team_id",
"posthog_user"."email",
"posthog_user"."pending_email",
"posthog_user"."temporary_token",
"posthog_user"."distinct_id",
"posthog_user"."is_email_verified",
"posthog_user"."requested_password_reset_at",
"posthog_user"."has_seen_product_intro_for",
"posthog_user"."strapi_id",
"posthog_user"."is_active",
"posthog_user"."theme_mode",
"posthog_user"."partial_notification_settings",
"posthog_user"."anonymize_data",
"posthog_user"."toolbar_mode",
"posthog_user"."hedgehog_config",
"posthog_user"."events_column_config",
"posthog_user"."email_opt_in"
FROM "posthog_action"
LEFT OUTER JOIN "posthog_action_events" ON ("posthog_action"."id" = "posthog_action_events"."action_id")
INNER JOIN "posthog_team" ON ("posthog_action"."team_id" = "posthog_team"."id")
LEFT OUTER JOIN "posthog_user" ON ("posthog_action"."created_by_id" = "posthog_user"."id")
WHERE (NOT "posthog_action"."deleted"
AND "posthog_action"."team_id" = 99999
AND "posthog_team"."project_id" = 99999)
GROUP BY "posthog_action"."id",
"posthog_user"."id"
ORDER BY "posthog_action"."last_calculated_at" DESC,
"posthog_action"."name" ASC
'''
# ---
# name: TestActionApi.test_listing_actions_is_not_nplus1.2
'''
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
@ -337,7 +218,7 @@
WHERE "posthog_organizationmembership"."user_id" = 99999
'''
# ---
# name: TestActionApi.test_listing_actions_is_not_nplus1.3
# name: TestActionApi.test_listing_actions_is_not_nplus1.14
'''
SELECT "posthog_organization"."id",
"posthog_organization"."name",
@ -363,7 +244,7 @@
LIMIT 21
'''
# ---
# name: TestActionApi.test_listing_actions_is_not_nplus1.4
# name: TestActionApi.test_listing_actions_is_not_nplus1.15
'''
SELECT "posthog_action"."id",
"posthog_action"."name",
@ -421,7 +302,7 @@
"posthog_action"."name" ASC
'''
# ---
# name: TestActionApi.test_listing_actions_is_not_nplus1.5
# name: TestActionApi.test_listing_actions_is_not_nplus1.16
'''
SELECT "posthog_user"."id",
"posthog_user"."password",
@ -453,7 +334,7 @@
LIMIT 21
'''
# ---
# name: TestActionApi.test_listing_actions_is_not_nplus1.6
# name: TestActionApi.test_listing_actions_is_not_nplus1.17
'''
SELECT "posthog_team"."id",
"posthog_team"."uuid",
@ -516,7 +397,111 @@
LIMIT 21
'''
# ---
# name: TestActionApi.test_listing_actions_is_not_nplus1.7
# name: TestActionApi.test_listing_actions_is_not_nplus1.18
'''
SELECT "posthog_project"."id",
"posthog_project"."organization_id",
"posthog_project"."name",
"posthog_project"."created_at",
"posthog_project"."product_description"
FROM "posthog_project"
WHERE "posthog_project"."id" = 99999
LIMIT 21
'''
# ---
# name: TestActionApi.test_listing_actions_is_not_nplus1.19
'''
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
"posthog_organizationmembership"."user_id",
"posthog_organizationmembership"."level",
"posthog_organizationmembership"."joined_at",
"posthog_organizationmembership"."updated_at",
"posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organizationmembership"
INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
WHERE ("posthog_organizationmembership"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
AND "posthog_organizationmembership"."user_id" = 99999)
LIMIT 21
'''
# ---
# name: TestActionApi.test_listing_actions_is_not_nplus1.2
'''
SELECT "posthog_project"."id",
"posthog_project"."organization_id",
"posthog_project"."name",
"posthog_project"."created_at",
"posthog_project"."product_description"
FROM "posthog_project"
WHERE "posthog_project"."id" = 99999
LIMIT 21
'''
# ---
# name: TestActionApi.test_listing_actions_is_not_nplus1.20
'''
SELECT "ee_accesscontrol"."id",
"ee_accesscontrol"."team_id",
"ee_accesscontrol"."access_level",
"ee_accesscontrol"."resource",
"ee_accesscontrol"."resource_id",
"ee_accesscontrol"."organization_member_id",
"ee_accesscontrol"."role_id",
"ee_accesscontrol"."created_by_id",
"ee_accesscontrol"."created_at",
"ee_accesscontrol"."updated_at"
FROM "ee_accesscontrol"
LEFT OUTER JOIN "posthog_organizationmembership" ON ("ee_accesscontrol"."organization_member_id" = "posthog_organizationmembership"."id")
WHERE (("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'project'
AND "ee_accesscontrol"."resource_id" = '99'
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'project'
AND "ee_accesscontrol"."resource_id" = '99'
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'action'
AND "ee_accesscontrol"."resource_id" IS NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'action'
AND "ee_accesscontrol"."resource_id" IS NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'action'
AND "ee_accesscontrol"."resource_id" IS NOT NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'action'
AND "ee_accesscontrol"."resource_id" IS NOT NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999))
'''
# ---
# name: TestActionApi.test_listing_actions_is_not_nplus1.21
'''
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
@ -548,7 +533,7 @@
WHERE "posthog_organizationmembership"."user_id" = 99999
'''
# ---
# name: TestActionApi.test_listing_actions_is_not_nplus1.8
# name: TestActionApi.test_listing_actions_is_not_nplus1.22
'''
SELECT "posthog_organization"."id",
"posthog_organization"."name",
@ -574,7 +559,7 @@
LIMIT 21
'''
# ---
# name: TestActionApi.test_listing_actions_is_not_nplus1.9
# name: TestActionApi.test_listing_actions_is_not_nplus1.23
'''
SELECT "posthog_action"."id",
"posthog_action"."name",
@ -632,3 +617,294 @@
"posthog_action"."name" ASC
'''
# ---
# name: TestActionApi.test_listing_actions_is_not_nplus1.3
'''
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
"posthog_organizationmembership"."user_id",
"posthog_organizationmembership"."level",
"posthog_organizationmembership"."joined_at",
"posthog_organizationmembership"."updated_at",
"posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organizationmembership"
INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
WHERE ("posthog_organizationmembership"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
AND "posthog_organizationmembership"."user_id" = 99999)
LIMIT 21
'''
# ---
# name: TestActionApi.test_listing_actions_is_not_nplus1.4
'''
SELECT "ee_accesscontrol"."id",
"ee_accesscontrol"."team_id",
"ee_accesscontrol"."access_level",
"ee_accesscontrol"."resource",
"ee_accesscontrol"."resource_id",
"ee_accesscontrol"."organization_member_id",
"ee_accesscontrol"."role_id",
"ee_accesscontrol"."created_by_id",
"ee_accesscontrol"."created_at",
"ee_accesscontrol"."updated_at"
FROM "ee_accesscontrol"
LEFT OUTER JOIN "posthog_organizationmembership" ON ("ee_accesscontrol"."organization_member_id" = "posthog_organizationmembership"."id")
WHERE (("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'project'
AND "ee_accesscontrol"."resource_id" = '99'
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'project'
AND "ee_accesscontrol"."resource_id" = '99'
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'action'
AND "ee_accesscontrol"."resource_id" IS NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'action'
AND "ee_accesscontrol"."resource_id" IS NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'action'
AND "ee_accesscontrol"."resource_id" IS NOT NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'action'
AND "ee_accesscontrol"."resource_id" IS NOT NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999))
'''
# ---
# name: TestActionApi.test_listing_actions_is_not_nplus1.5
'''
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
"posthog_organizationmembership"."user_id",
"posthog_organizationmembership"."level",
"posthog_organizationmembership"."joined_at",
"posthog_organizationmembership"."updated_at",
"posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organizationmembership"
INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
WHERE "posthog_organizationmembership"."user_id" = 99999
'''
# ---
# name: TestActionApi.test_listing_actions_is_not_nplus1.6
'''
SELECT "posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organization"
WHERE "posthog_organization"."id" = '00000000-0000-0000-0000-000000000000'::uuid
LIMIT 21
'''
# ---
# name: TestActionApi.test_listing_actions_is_not_nplus1.7
'''
SELECT "posthog_action"."id",
"posthog_action"."name",
"posthog_action"."team_id",
"posthog_action"."description",
"posthog_action"."created_at",
"posthog_action"."created_by_id",
"posthog_action"."deleted",
"posthog_action"."post_to_slack",
"posthog_action"."slack_message_format",
"posthog_action"."updated_at",
"posthog_action"."bytecode",
"posthog_action"."bytecode_error",
"posthog_action"."steps_json",
"posthog_action"."pinned_at",
"posthog_action"."is_calculating",
"posthog_action"."last_calculated_at",
COUNT("posthog_action_events"."event_id") AS "count",
"posthog_user"."id",
"posthog_user"."password",
"posthog_user"."last_login",
"posthog_user"."first_name",
"posthog_user"."last_name",
"posthog_user"."is_staff",
"posthog_user"."date_joined",
"posthog_user"."uuid",
"posthog_user"."current_organization_id",
"posthog_user"."current_team_id",
"posthog_user"."email",
"posthog_user"."pending_email",
"posthog_user"."temporary_token",
"posthog_user"."distinct_id",
"posthog_user"."is_email_verified",
"posthog_user"."requested_password_reset_at",
"posthog_user"."has_seen_product_intro_for",
"posthog_user"."strapi_id",
"posthog_user"."is_active",
"posthog_user"."theme_mode",
"posthog_user"."partial_notification_settings",
"posthog_user"."anonymize_data",
"posthog_user"."toolbar_mode",
"posthog_user"."hedgehog_config",
"posthog_user"."events_column_config",
"posthog_user"."email_opt_in"
FROM "posthog_action"
LEFT OUTER JOIN "posthog_action_events" ON ("posthog_action"."id" = "posthog_action_events"."action_id")
INNER JOIN "posthog_team" ON ("posthog_action"."team_id" = "posthog_team"."id")
LEFT OUTER JOIN "posthog_user" ON ("posthog_action"."created_by_id" = "posthog_user"."id")
WHERE (NOT "posthog_action"."deleted"
AND "posthog_action"."team_id" = 99999
AND "posthog_team"."project_id" = 99999)
GROUP BY "posthog_action"."id",
"posthog_user"."id"
ORDER BY "posthog_action"."last_calculated_at" DESC,
"posthog_action"."name" ASC
'''
# ---
# name: TestActionApi.test_listing_actions_is_not_nplus1.8
'''
SELECT "posthog_user"."id",
"posthog_user"."password",
"posthog_user"."last_login",
"posthog_user"."first_name",
"posthog_user"."last_name",
"posthog_user"."is_staff",
"posthog_user"."date_joined",
"posthog_user"."uuid",
"posthog_user"."current_organization_id",
"posthog_user"."current_team_id",
"posthog_user"."email",
"posthog_user"."pending_email",
"posthog_user"."temporary_token",
"posthog_user"."distinct_id",
"posthog_user"."is_email_verified",
"posthog_user"."has_seen_product_intro_for",
"posthog_user"."strapi_id",
"posthog_user"."is_active",
"posthog_user"."theme_mode",
"posthog_user"."partial_notification_settings",
"posthog_user"."anonymize_data",
"posthog_user"."toolbar_mode",
"posthog_user"."hedgehog_config",
"posthog_user"."events_column_config",
"posthog_user"."email_opt_in"
FROM "posthog_user"
WHERE "posthog_user"."id" = 99999
LIMIT 21
'''
# ---
# name: TestActionApi.test_listing_actions_is_not_nplus1.9
'''
SELECT "posthog_team"."id",
"posthog_team"."uuid",
"posthog_team"."organization_id",
"posthog_team"."project_id",
"posthog_team"."api_token",
"posthog_team"."app_urls",
"posthog_team"."name",
"posthog_team"."slack_incoming_webhook",
"posthog_team"."created_at",
"posthog_team"."updated_at",
"posthog_team"."anonymize_ips",
"posthog_team"."completed_snippet_onboarding",
"posthog_team"."has_completed_onboarding_for",
"posthog_team"."ingested_event",
"posthog_team"."autocapture_opt_out",
"posthog_team"."autocapture_web_vitals_opt_in",
"posthog_team"."autocapture_web_vitals_allowed_metrics",
"posthog_team"."autocapture_exceptions_opt_in",
"posthog_team"."autocapture_exceptions_errors_to_ignore",
"posthog_team"."person_processing_opt_out",
"posthog_team"."session_recording_opt_in",
"posthog_team"."session_recording_sample_rate",
"posthog_team"."session_recording_minimum_duration_milliseconds",
"posthog_team"."session_recording_linked_flag",
"posthog_team"."session_recording_network_payload_capture_config",
"posthog_team"."session_recording_url_trigger_config",
"posthog_team"."session_recording_url_blocklist_config",
"posthog_team"."session_recording_event_trigger_config",
"posthog_team"."session_replay_config",
"posthog_team"."survey_config",
"posthog_team"."capture_console_log_opt_in",
"posthog_team"."capture_performance_opt_in",
"posthog_team"."capture_dead_clicks",
"posthog_team"."surveys_opt_in",
"posthog_team"."heatmaps_opt_in",
"posthog_team"."session_recording_version",
"posthog_team"."signup_token",
"posthog_team"."is_demo",
"posthog_team"."access_control",
"posthog_team"."week_start_day",
"posthog_team"."inject_web_apps",
"posthog_team"."test_account_filters",
"posthog_team"."test_account_filters_default_checked",
"posthog_team"."path_cleaning_filters",
"posthog_team"."timezone",
"posthog_team"."data_attributes",
"posthog_team"."person_display_name_properties",
"posthog_team"."live_events_columns",
"posthog_team"."recording_domains",
"posthog_team"."primary_dashboard_id",
"posthog_team"."extra_settings",
"posthog_team"."modifiers",
"posthog_team"."correlation_config",
"posthog_team"."session_recording_retention_period_days",
"posthog_team"."external_data_workspace_id",
"posthog_team"."external_data_workspace_last_synced_at"
FROM "posthog_team"
WHERE "posthog_team"."id" = 99999
LIMIT 21
'''
# ---

View File

@ -96,68 +96,85 @@
# ---
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.10
'''
SELECT "posthog_team"."id",
"posthog_team"."uuid",
"posthog_team"."organization_id",
"posthog_team"."project_id",
"posthog_team"."api_token",
"posthog_team"."app_urls",
"posthog_team"."name",
"posthog_team"."slack_incoming_webhook",
"posthog_team"."created_at",
"posthog_team"."updated_at",
"posthog_team"."anonymize_ips",
"posthog_team"."completed_snippet_onboarding",
"posthog_team"."has_completed_onboarding_for",
"posthog_team"."ingested_event",
"posthog_team"."autocapture_opt_out",
"posthog_team"."autocapture_web_vitals_opt_in",
"posthog_team"."autocapture_web_vitals_allowed_metrics",
"posthog_team"."autocapture_exceptions_opt_in",
"posthog_team"."autocapture_exceptions_errors_to_ignore",
"posthog_team"."person_processing_opt_out",
"posthog_team"."session_recording_opt_in",
"posthog_team"."session_recording_sample_rate",
"posthog_team"."session_recording_minimum_duration_milliseconds",
"posthog_team"."session_recording_linked_flag",
"posthog_team"."session_recording_network_payload_capture_config",
"posthog_team"."session_recording_url_trigger_config",
"posthog_team"."session_recording_url_blocklist_config",
"posthog_team"."session_recording_event_trigger_config",
"posthog_team"."session_replay_config",
"posthog_team"."survey_config",
"posthog_team"."capture_console_log_opt_in",
"posthog_team"."capture_performance_opt_in",
"posthog_team"."capture_dead_clicks",
"posthog_team"."surveys_opt_in",
"posthog_team"."heatmaps_opt_in",
"posthog_team"."session_recording_version",
"posthog_team"."signup_token",
"posthog_team"."is_demo",
"posthog_team"."access_control",
"posthog_team"."week_start_day",
"posthog_team"."inject_web_apps",
"posthog_team"."test_account_filters",
"posthog_team"."test_account_filters_default_checked",
"posthog_team"."path_cleaning_filters",
"posthog_team"."timezone",
"posthog_team"."data_attributes",
"posthog_team"."person_display_name_properties",
"posthog_team"."live_events_columns",
"posthog_team"."recording_domains",
"posthog_team"."primary_dashboard_id",
"posthog_team"."extra_settings",
"posthog_team"."modifiers",
"posthog_team"."correlation_config",
"posthog_team"."session_recording_retention_period_days",
"posthog_team"."external_data_workspace_id",
"posthog_team"."external_data_workspace_last_synced_at"
FROM "posthog_team"
WHERE "posthog_team"."id" = 99999
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
"posthog_organizationmembership"."user_id",
"posthog_organizationmembership"."level",
"posthog_organizationmembership"."joined_at",
"posthog_organizationmembership"."updated_at",
"posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organizationmembership"
INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
WHERE ("posthog_organizationmembership"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
AND "posthog_organizationmembership"."user_id" = 99999)
LIMIT 21
'''
# ---
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.11
'''
SELECT "ee_accesscontrol"."id",
"ee_accesscontrol"."team_id",
"ee_accesscontrol"."access_level",
"ee_accesscontrol"."resource",
"ee_accesscontrol"."resource_id",
"ee_accesscontrol"."organization_member_id",
"ee_accesscontrol"."role_id",
"ee_accesscontrol"."created_by_id",
"ee_accesscontrol"."created_at",
"ee_accesscontrol"."updated_at"
FROM "ee_accesscontrol"
LEFT OUTER JOIN "posthog_organizationmembership" ON ("ee_accesscontrol"."organization_member_id" = "posthog_organizationmembership"."id")
WHERE (("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'project'
AND "ee_accesscontrol"."resource_id" = '107'
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'project'
AND "ee_accesscontrol"."resource_id" = '107'
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'annotation'
AND "ee_accesscontrol"."resource_id" IS NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'annotation'
AND "ee_accesscontrol"."resource_id" IS NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'annotation'
AND "ee_accesscontrol"."resource_id" IS NOT NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'annotation'
AND "ee_accesscontrol"."resource_id" IS NOT NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999))
'''
# ---
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.12
'''
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
@ -189,7 +206,7 @@
WHERE "posthog_organizationmembership"."user_id" = 99999
'''
# ---
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.12
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.13
'''
SELECT COUNT(*) AS "__count"
FROM "posthog_annotation"
@ -199,7 +216,7 @@
OR "posthog_annotation"."team_id" = 99999))
'''
# ---
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.13
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.14
'''
SELECT "posthog_annotation"."id",
"posthog_annotation"."content",
@ -280,49 +297,7 @@
LIMIT 1000
'''
# ---
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.2
'''
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
"posthog_organizationmembership"."user_id",
"posthog_organizationmembership"."level",
"posthog_organizationmembership"."joined_at",
"posthog_organizationmembership"."updated_at",
"posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organizationmembership"
INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
WHERE "posthog_organizationmembership"."user_id" = 99999
'''
# ---
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.3
'''
SELECT COUNT(*) AS "__count"
FROM "posthog_annotation"
WHERE (NOT "posthog_annotation"."deleted"
AND (("posthog_annotation"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
AND "posthog_annotation"."scope" = 'organization')
OR "posthog_annotation"."team_id" = 99999))
'''
# ---
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.4
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.15
'''
SELECT "posthog_user"."id",
"posthog_user"."password",
@ -354,7 +329,7 @@
LIMIT 21
'''
# ---
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.5
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.16
'''
SELECT "posthog_team"."id",
"posthog_team"."uuid",
@ -417,7 +392,111 @@
LIMIT 21
'''
# ---
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.6
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.17
'''
SELECT "posthog_project"."id",
"posthog_project"."organization_id",
"posthog_project"."name",
"posthog_project"."created_at",
"posthog_project"."product_description"
FROM "posthog_project"
WHERE "posthog_project"."id" = 99999
LIMIT 21
'''
# ---
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.18
'''
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
"posthog_organizationmembership"."user_id",
"posthog_organizationmembership"."level",
"posthog_organizationmembership"."joined_at",
"posthog_organizationmembership"."updated_at",
"posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organizationmembership"
INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
WHERE ("posthog_organizationmembership"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
AND "posthog_organizationmembership"."user_id" = 99999)
LIMIT 21
'''
# ---
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.19
'''
SELECT "ee_accesscontrol"."id",
"ee_accesscontrol"."team_id",
"ee_accesscontrol"."access_level",
"ee_accesscontrol"."resource",
"ee_accesscontrol"."resource_id",
"ee_accesscontrol"."organization_member_id",
"ee_accesscontrol"."role_id",
"ee_accesscontrol"."created_by_id",
"ee_accesscontrol"."created_at",
"ee_accesscontrol"."updated_at"
FROM "ee_accesscontrol"
LEFT OUTER JOIN "posthog_organizationmembership" ON ("ee_accesscontrol"."organization_member_id" = "posthog_organizationmembership"."id")
WHERE (("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'project'
AND "ee_accesscontrol"."resource_id" = '107'
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'project'
AND "ee_accesscontrol"."resource_id" = '107'
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'annotation'
AND "ee_accesscontrol"."resource_id" IS NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'annotation'
AND "ee_accesscontrol"."resource_id" IS NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'annotation'
AND "ee_accesscontrol"."resource_id" IS NOT NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'annotation'
AND "ee_accesscontrol"."resource_id" IS NOT NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999))
'''
# ---
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.2
'''
SELECT "posthog_project"."id",
"posthog_project"."organization_id",
"posthog_project"."name",
"posthog_project"."created_at",
"posthog_project"."product_description"
FROM "posthog_project"
WHERE "posthog_project"."id" = 99999
LIMIT 21
'''
# ---
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.20
'''
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
@ -449,7 +528,7 @@
WHERE "posthog_organizationmembership"."user_id" = 99999
'''
# ---
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.7
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.21
'''
SELECT COUNT(*) AS "__count"
FROM "posthog_annotation"
@ -459,7 +538,7 @@
OR "posthog_annotation"."team_id" = 99999))
'''
# ---
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.8
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.22
'''
SELECT "posthog_annotation"."id",
"posthog_annotation"."content",
@ -540,7 +619,129 @@
LIMIT 1000
'''
# ---
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.9
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.3
'''
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
"posthog_organizationmembership"."user_id",
"posthog_organizationmembership"."level",
"posthog_organizationmembership"."joined_at",
"posthog_organizationmembership"."updated_at",
"posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organizationmembership"
INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
WHERE ("posthog_organizationmembership"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
AND "posthog_organizationmembership"."user_id" = 99999)
LIMIT 21
'''
# ---
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.4
'''
SELECT "ee_accesscontrol"."id",
"ee_accesscontrol"."team_id",
"ee_accesscontrol"."access_level",
"ee_accesscontrol"."resource",
"ee_accesscontrol"."resource_id",
"ee_accesscontrol"."organization_member_id",
"ee_accesscontrol"."role_id",
"ee_accesscontrol"."created_by_id",
"ee_accesscontrol"."created_at",
"ee_accesscontrol"."updated_at"
FROM "ee_accesscontrol"
LEFT OUTER JOIN "posthog_organizationmembership" ON ("ee_accesscontrol"."organization_member_id" = "posthog_organizationmembership"."id")
WHERE (("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'project'
AND "ee_accesscontrol"."resource_id" = '107'
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'project'
AND "ee_accesscontrol"."resource_id" = '107'
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'annotation'
AND "ee_accesscontrol"."resource_id" IS NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'annotation'
AND "ee_accesscontrol"."resource_id" IS NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'annotation'
AND "ee_accesscontrol"."resource_id" IS NOT NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'annotation'
AND "ee_accesscontrol"."resource_id" IS NOT NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999))
'''
# ---
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.5
'''
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
"posthog_organizationmembership"."user_id",
"posthog_organizationmembership"."level",
"posthog_organizationmembership"."joined_at",
"posthog_organizationmembership"."updated_at",
"posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organizationmembership"
INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
WHERE "posthog_organizationmembership"."user_id" = 99999
'''
# ---
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.6
'''
SELECT COUNT(*) AS "__count"
FROM "posthog_annotation"
WHERE (NOT "posthog_annotation"."deleted"
AND (("posthog_annotation"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
AND "posthog_annotation"."scope" = 'organization')
OR "posthog_annotation"."team_id" = 99999))
'''
# ---
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.7
'''
SELECT "posthog_user"."id",
"posthog_user"."password",
@ -572,3 +773,78 @@
LIMIT 21
'''
# ---
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.8
'''
SELECT "posthog_team"."id",
"posthog_team"."uuid",
"posthog_team"."organization_id",
"posthog_team"."project_id",
"posthog_team"."api_token",
"posthog_team"."app_urls",
"posthog_team"."name",
"posthog_team"."slack_incoming_webhook",
"posthog_team"."created_at",
"posthog_team"."updated_at",
"posthog_team"."anonymize_ips",
"posthog_team"."completed_snippet_onboarding",
"posthog_team"."has_completed_onboarding_for",
"posthog_team"."ingested_event",
"posthog_team"."autocapture_opt_out",
"posthog_team"."autocapture_web_vitals_opt_in",
"posthog_team"."autocapture_web_vitals_allowed_metrics",
"posthog_team"."autocapture_exceptions_opt_in",
"posthog_team"."autocapture_exceptions_errors_to_ignore",
"posthog_team"."person_processing_opt_out",
"posthog_team"."session_recording_opt_in",
"posthog_team"."session_recording_sample_rate",
"posthog_team"."session_recording_minimum_duration_milliseconds",
"posthog_team"."session_recording_linked_flag",
"posthog_team"."session_recording_network_payload_capture_config",
"posthog_team"."session_recording_url_trigger_config",
"posthog_team"."session_recording_url_blocklist_config",
"posthog_team"."session_recording_event_trigger_config",
"posthog_team"."session_replay_config",
"posthog_team"."survey_config",
"posthog_team"."capture_console_log_opt_in",
"posthog_team"."capture_performance_opt_in",
"posthog_team"."capture_dead_clicks",
"posthog_team"."surveys_opt_in",
"posthog_team"."heatmaps_opt_in",
"posthog_team"."session_recording_version",
"posthog_team"."signup_token",
"posthog_team"."is_demo",
"posthog_team"."access_control",
"posthog_team"."week_start_day",
"posthog_team"."inject_web_apps",
"posthog_team"."test_account_filters",
"posthog_team"."test_account_filters_default_checked",
"posthog_team"."path_cleaning_filters",
"posthog_team"."timezone",
"posthog_team"."data_attributes",
"posthog_team"."person_display_name_properties",
"posthog_team"."live_events_columns",
"posthog_team"."recording_domains",
"posthog_team"."primary_dashboard_id",
"posthog_team"."extra_settings",
"posthog_team"."modifiers",
"posthog_team"."correlation_config",
"posthog_team"."session_recording_retention_period_days",
"posthog_team"."external_data_workspace_id",
"posthog_team"."external_data_workspace_last_synced_at"
FROM "posthog_team"
WHERE "posthog_team"."id" = 99999
LIMIT 21
'''
# ---
# name: TestAnnotation.test_retrieving_annotation_is_not_n_plus_1.9
'''
SELECT "posthog_project"."id",
"posthog_project"."organization_id",
"posthog_project"."name",
"posthog_project"."created_at",
"posthog_project"."product_description"
FROM "posthog_project"
WHERE "posthog_project"."id" = 99999
LIMIT 21
'''
# ---

View File

@ -6,7 +6,6 @@
'/home/runner/work/posthog/posthog/ee/api/explicit_team_member.py: Warning [ExplicitTeamMemberViewSet]: could not derive type of path parameter "project_id" because model "ee.models.explicit_team_membership.ExplicitTeamMembership" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".',
'/home/runner/work/posthog/posthog/ee/api/feature_flag_role_access.py: Warning [FeatureFlagRoleAccessViewSet]: could not derive type of path parameter "project_id" because model "ee.models.feature_flag_role_access.FeatureFlagRoleAccess" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".',
'/home/runner/work/posthog/posthog/ee/api/rbac/role.py: Warning [RoleMembershipViewSet]: could not derive type of path parameter "organization_id" because model "ee.models.rbac.role.RoleMembership" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".',
'/home/runner/work/posthog/posthog/ee/api/rbac/role.py: Warning [RoleViewSet > RoleSerializer]: unable to resolve type hint for function "get_associated_flags". Consider using a type hint or @extend_schema_field. Defaulting to string.',
'/home/runner/work/posthog/posthog/ee/api/rbac/role.py: Warning [RoleViewSet > RoleSerializer]: unable to resolve type hint for function "get_members". Consider using a type hint or @extend_schema_field. Defaulting to string.',
'/home/runner/work/posthog/posthog/ee/api/subscription.py: Warning [SubscriptionViewSet > SubscriptionSerializer]: unable to resolve type hint for function "summary". Consider using a type hint or @extend_schema_field. Defaulting to string.',
'/home/runner/work/posthog/posthog/ee/api/subscription.py: Warning [SubscriptionViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.subscription.Subscription" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".',

View File

@ -64,6 +64,358 @@
'''
# ---
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.10
'''
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
"posthog_organizationmembership"."user_id",
"posthog_organizationmembership"."level",
"posthog_organizationmembership"."joined_at",
"posthog_organizationmembership"."updated_at",
"posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organizationmembership"
INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
WHERE "posthog_organizationmembership"."user_id" = 99999
'''
# ---
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.11
'''
SELECT "posthog_team"."id",
"posthog_team"."organization_id",
"posthog_team"."access_control"
FROM "posthog_team"
WHERE "posthog_team"."organization_id" IN ('00000000-0000-0000-0000-000000000000'::uuid)
'''
# ---
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.12
'''
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
"posthog_organizationmembership"."user_id",
"posthog_organizationmembership"."level",
"posthog_organizationmembership"."joined_at",
"posthog_organizationmembership"."updated_at",
"posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organizationmembership"
INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
WHERE ("posthog_organizationmembership"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
AND "posthog_organizationmembership"."user_id" = 99999)
LIMIT 21
'''
# ---
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.13
'''
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
"posthog_organizationmembership"."user_id",
"posthog_organizationmembership"."level",
"posthog_organizationmembership"."joined_at",
"posthog_organizationmembership"."updated_at",
"posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organizationmembership"
INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
WHERE "posthog_organizationmembership"."user_id" = 99999
'''
# ---
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.14
'''
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
"posthog_organizationmembership"."user_id",
"posthog_organizationmembership"."level",
"posthog_organizationmembership"."joined_at",
"posthog_organizationmembership"."updated_at",
"posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organizationmembership"
INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
WHERE "posthog_organizationmembership"."user_id" = 99999
'''
# ---
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.15
'''
SELECT "posthog_team"."id",
"posthog_team"."organization_id",
"posthog_team"."access_control"
FROM "posthog_team"
WHERE "posthog_team"."organization_id" IN ('00000000-0000-0000-0000-000000000000'::uuid)
'''
# ---
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.16
'''
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
"posthog_organizationmembership"."user_id",
"posthog_organizationmembership"."level",
"posthog_organizationmembership"."joined_at",
"posthog_organizationmembership"."updated_at",
"posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organizationmembership"
INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
WHERE ("posthog_organizationmembership"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
AND "posthog_organizationmembership"."user_id" = 99999)
LIMIT 21
'''
# ---
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.17
'''
SELECT "posthog_hogfunction"."id",
"posthog_hogfunction"."team_id",
"posthog_hogfunction"."name",
"posthog_hogfunction"."description",
"posthog_hogfunction"."created_at",
"posthog_hogfunction"."created_by_id",
"posthog_hogfunction"."deleted",
"posthog_hogfunction"."updated_at",
"posthog_hogfunction"."enabled",
"posthog_hogfunction"."type",
"posthog_hogfunction"."icon_url",
"posthog_hogfunction"."hog",
"posthog_hogfunction"."bytecode",
"posthog_hogfunction"."inputs_schema",
"posthog_hogfunction"."inputs",
"posthog_hogfunction"."encrypted_inputs",
"posthog_hogfunction"."filters",
"posthog_hogfunction"."masking",
"posthog_hogfunction"."template_id",
"posthog_team"."id",
"posthog_team"."uuid",
"posthog_team"."organization_id",
"posthog_team"."project_id",
"posthog_team"."api_token",
"posthog_team"."app_urls",
"posthog_team"."name",
"posthog_team"."slack_incoming_webhook",
"posthog_team"."created_at",
"posthog_team"."updated_at",
"posthog_team"."anonymize_ips",
"posthog_team"."completed_snippet_onboarding",
"posthog_team"."has_completed_onboarding_for",
"posthog_team"."ingested_event",
"posthog_team"."autocapture_opt_out",
"posthog_team"."autocapture_web_vitals_opt_in",
"posthog_team"."autocapture_web_vitals_allowed_metrics",
"posthog_team"."autocapture_exceptions_opt_in",
"posthog_team"."autocapture_exceptions_errors_to_ignore",
"posthog_team"."person_processing_opt_out",
"posthog_team"."session_recording_opt_in",
"posthog_team"."session_recording_sample_rate",
"posthog_team"."session_recording_minimum_duration_milliseconds",
"posthog_team"."session_recording_linked_flag",
"posthog_team"."session_recording_network_payload_capture_config",
"posthog_team"."session_recording_url_trigger_config",
"posthog_team"."session_recording_url_blocklist_config",
"posthog_team"."session_recording_event_trigger_config",
"posthog_team"."session_replay_config",
"posthog_team"."survey_config",
"posthog_team"."capture_console_log_opt_in",
"posthog_team"."capture_performance_opt_in",
"posthog_team"."capture_dead_clicks",
"posthog_team"."surveys_opt_in",
"posthog_team"."heatmaps_opt_in",
"posthog_team"."session_recording_version",
"posthog_team"."signup_token",
"posthog_team"."is_demo",
"posthog_team"."access_control",
"posthog_team"."week_start_day",
"posthog_team"."inject_web_apps",
"posthog_team"."test_account_filters",
"posthog_team"."test_account_filters_default_checked",
"posthog_team"."path_cleaning_filters",
"posthog_team"."timezone",
"posthog_team"."data_attributes",
"posthog_team"."person_display_name_properties",
"posthog_team"."live_events_columns",
"posthog_team"."recording_domains",
"posthog_team"."primary_dashboard_id",
"posthog_team"."extra_settings",
"posthog_team"."modifiers",
"posthog_team"."correlation_config",
"posthog_team"."session_recording_retention_period_days",
"posthog_team"."plugins_opt_in",
"posthog_team"."opt_out_capture",
"posthog_team"."event_names",
"posthog_team"."event_names_with_usage",
"posthog_team"."event_properties",
"posthog_team"."event_properties_with_usage",
"posthog_team"."event_properties_numerical",
"posthog_team"."external_data_workspace_id",
"posthog_team"."external_data_workspace_last_synced_at"
FROM "posthog_hogfunction"
INNER JOIN "posthog_team" ON ("posthog_hogfunction"."team_id" = "posthog_team"."id")
WHERE ("posthog_hogfunction"."team_id" = 99999
AND "posthog_hogfunction"."filters" @> '{"filter_test_accounts": true}'::jsonb)
'''
# ---
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.18
'''
SELECT 1 AS "a"
FROM "posthog_grouptypemapping"
WHERE "posthog_grouptypemapping"."team_id" = 99999
LIMIT 1
'''
# ---
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.19
'''
SELECT "posthog_team"."id",
"posthog_team"."uuid",
"posthog_team"."organization_id",
"posthog_team"."project_id",
"posthog_team"."api_token",
"posthog_team"."app_urls",
"posthog_team"."name",
"posthog_team"."slack_incoming_webhook",
"posthog_team"."created_at",
"posthog_team"."updated_at",
"posthog_team"."anonymize_ips",
"posthog_team"."completed_snippet_onboarding",
"posthog_team"."has_completed_onboarding_for",
"posthog_team"."ingested_event",
"posthog_team"."autocapture_opt_out",
"posthog_team"."autocapture_web_vitals_opt_in",
"posthog_team"."autocapture_web_vitals_allowed_metrics",
"posthog_team"."autocapture_exceptions_opt_in",
"posthog_team"."autocapture_exceptions_errors_to_ignore",
"posthog_team"."person_processing_opt_out",
"posthog_team"."session_recording_opt_in",
"posthog_team"."session_recording_sample_rate",
"posthog_team"."session_recording_minimum_duration_milliseconds",
"posthog_team"."session_recording_linked_flag",
"posthog_team"."session_recording_network_payload_capture_config",
"posthog_team"."session_recording_url_trigger_config",
"posthog_team"."session_recording_url_blocklist_config",
"posthog_team"."session_recording_event_trigger_config",
"posthog_team"."session_replay_config",
"posthog_team"."survey_config",
"posthog_team"."capture_console_log_opt_in",
"posthog_team"."capture_performance_opt_in",
"posthog_team"."capture_dead_clicks",
"posthog_team"."surveys_opt_in",
"posthog_team"."heatmaps_opt_in",
"posthog_team"."session_recording_version",
"posthog_team"."signup_token",
"posthog_team"."is_demo",
"posthog_team"."access_control",
"posthog_team"."week_start_day",
"posthog_team"."inject_web_apps",
"posthog_team"."test_account_filters",
"posthog_team"."test_account_filters_default_checked",
"posthog_team"."path_cleaning_filters",
"posthog_team"."timezone",
"posthog_team"."data_attributes",
"posthog_team"."person_display_name_properties",
"posthog_team"."live_events_columns",
"posthog_team"."recording_domains",
"posthog_team"."primary_dashboard_id",
"posthog_team"."extra_settings",
"posthog_team"."modifiers",
"posthog_team"."correlation_config",
"posthog_team"."session_recording_retention_period_days",
"posthog_team"."external_data_workspace_id",
"posthog_team"."external_data_workspace_last_synced_at"
FROM "posthog_team"
WHERE "posthog_team"."id" = 99999
LIMIT 21
'''
# ---
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.2
'''
SELECT "posthog_team"."id",
"posthog_team"."organization_id",
"posthog_team"."access_control"
FROM "posthog_team"
WHERE "posthog_team"."organization_id" IN ('00000000-0000-0000-0000-000000000000'::uuid)
'''
# ---
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.20
'''
SELECT "posthog_productintent"."id",
"posthog_productintent"."team_id",
@ -77,7 +429,7 @@
WHERE "posthog_productintent"."team_id" = 99999
'''
# ---
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.11
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.21
'''
SELECT "posthog_productintent"."product_type",
"posthog_productintent"."created_at",
@ -87,7 +439,7 @@
WHERE "posthog_productintent"."team_id" = 99999
'''
# ---
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.12
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.22
'''
SELECT "posthog_user"."id",
"posthog_user"."password",
@ -119,7 +471,7 @@
LIMIT 21
'''
# ---
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.13
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.23
'''
SELECT "posthog_featureflag"."id",
"posthog_featureflag"."key",
@ -142,7 +494,7 @@
AND "posthog_featureflag"."team_id" = 99999)
'''
# ---
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.14
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.24
'''
SELECT "posthog_pluginconfig"."id",
"posthog_pluginconfig"."web_token",
@ -158,15 +510,6 @@
AND "posthog_pluginconfig"."team_id" = 99999)
'''
# ---
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.2
'''
SELECT "posthog_team"."id",
"posthog_team"."organization_id",
"posthog_team"."access_control"
FROM "posthog_team"
WHERE "posthog_team"."organization_id" IN ('00000000-0000-0000-0000-000000000000'::uuid)
'''
# ---
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.3
'''
SELECT "posthog_team"."id",
@ -266,7 +609,9 @@
"posthog_organization"."domain_whitelist"
FROM "posthog_organizationmembership"
INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
WHERE "posthog_organizationmembership"."user_id" = 99999
WHERE ("posthog_organizationmembership"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
AND "posthog_organizationmembership"."user_id" = 99999)
LIMIT 21
'''
# ---
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.5
@ -312,163 +657,117 @@
# ---
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.7
'''
SELECT "posthog_hogfunction"."id",
"posthog_hogfunction"."team_id",
"posthog_hogfunction"."name",
"posthog_hogfunction"."description",
"posthog_hogfunction"."created_at",
"posthog_hogfunction"."created_by_id",
"posthog_hogfunction"."deleted",
"posthog_hogfunction"."updated_at",
"posthog_hogfunction"."enabled",
"posthog_hogfunction"."type",
"posthog_hogfunction"."icon_url",
"posthog_hogfunction"."hog",
"posthog_hogfunction"."bytecode",
"posthog_hogfunction"."inputs_schema",
"posthog_hogfunction"."inputs",
"posthog_hogfunction"."encrypted_inputs",
"posthog_hogfunction"."filters",
"posthog_hogfunction"."masking",
"posthog_hogfunction"."template_id",
"posthog_team"."id",
"posthog_team"."uuid",
"posthog_team"."organization_id",
"posthog_team"."project_id",
"posthog_team"."api_token",
"posthog_team"."app_urls",
"posthog_team"."name",
"posthog_team"."slack_incoming_webhook",
"posthog_team"."created_at",
"posthog_team"."updated_at",
"posthog_team"."anonymize_ips",
"posthog_team"."completed_snippet_onboarding",
"posthog_team"."has_completed_onboarding_for",
"posthog_team"."ingested_event",
"posthog_team"."autocapture_opt_out",
"posthog_team"."autocapture_web_vitals_opt_in",
"posthog_team"."autocapture_web_vitals_allowed_metrics",
"posthog_team"."autocapture_exceptions_opt_in",
"posthog_team"."autocapture_exceptions_errors_to_ignore",
"posthog_team"."person_processing_opt_out",
"posthog_team"."session_recording_opt_in",
"posthog_team"."session_recording_sample_rate",
"posthog_team"."session_recording_minimum_duration_milliseconds",
"posthog_team"."session_recording_linked_flag",
"posthog_team"."session_recording_network_payload_capture_config",
"posthog_team"."session_recording_url_trigger_config",
"posthog_team"."session_recording_url_blocklist_config",
"posthog_team"."session_recording_event_trigger_config",
"posthog_team"."session_replay_config",
"posthog_team"."survey_config",
"posthog_team"."capture_console_log_opt_in",
"posthog_team"."capture_performance_opt_in",
"posthog_team"."capture_dead_clicks",
"posthog_team"."surveys_opt_in",
"posthog_team"."heatmaps_opt_in",
"posthog_team"."session_recording_version",
"posthog_team"."signup_token",
"posthog_team"."is_demo",
"posthog_team"."access_control",
"posthog_team"."week_start_day",
"posthog_team"."inject_web_apps",
"posthog_team"."test_account_filters",
"posthog_team"."test_account_filters_default_checked",
"posthog_team"."path_cleaning_filters",
"posthog_team"."timezone",
"posthog_team"."data_attributes",
"posthog_team"."person_display_name_properties",
"posthog_team"."live_events_columns",
"posthog_team"."recording_domains",
"posthog_team"."primary_dashboard_id",
"posthog_team"."extra_settings",
"posthog_team"."modifiers",
"posthog_team"."correlation_config",
"posthog_team"."session_recording_retention_period_days",
"posthog_team"."plugins_opt_in",
"posthog_team"."opt_out_capture",
"posthog_team"."event_names",
"posthog_team"."event_names_with_usage",
"posthog_team"."event_properties",
"posthog_team"."event_properties_with_usage",
"posthog_team"."event_properties_numerical",
"posthog_team"."external_data_workspace_id",
"posthog_team"."external_data_workspace_last_synced_at"
FROM "posthog_hogfunction"
INNER JOIN "posthog_team" ON ("posthog_hogfunction"."team_id" = "posthog_team"."id")
WHERE ("posthog_hogfunction"."team_id" = 99999
AND "posthog_hogfunction"."filters" @> '{"filter_test_accounts": true}'::jsonb)
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
"posthog_organizationmembership"."user_id",
"posthog_organizationmembership"."level",
"posthog_organizationmembership"."joined_at",
"posthog_organizationmembership"."updated_at",
"posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organizationmembership"
INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
WHERE ("posthog_organizationmembership"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
AND "posthog_organizationmembership"."user_id" = 99999)
LIMIT 21
'''
# ---
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.8
'''
SELECT 1 AS "a"
FROM "posthog_grouptypemapping"
WHERE "posthog_grouptypemapping"."team_id" = 99999
LIMIT 1
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
"posthog_organizationmembership"."user_id",
"posthog_organizationmembership"."level",
"posthog_organizationmembership"."joined_at",
"posthog_organizationmembership"."updated_at",
"posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organizationmembership"
INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
WHERE ("posthog_organizationmembership"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
AND "posthog_organizationmembership"."user_id" = 99999)
LIMIT 21
'''
# ---
# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.9
'''
SELECT "posthog_team"."id",
"posthog_team"."uuid",
"posthog_team"."organization_id",
"posthog_team"."project_id",
"posthog_team"."api_token",
"posthog_team"."app_urls",
"posthog_team"."name",
"posthog_team"."slack_incoming_webhook",
"posthog_team"."created_at",
"posthog_team"."updated_at",
"posthog_team"."anonymize_ips",
"posthog_team"."completed_snippet_onboarding",
"posthog_team"."has_completed_onboarding_for",
"posthog_team"."ingested_event",
"posthog_team"."autocapture_opt_out",
"posthog_team"."autocapture_web_vitals_opt_in",
"posthog_team"."autocapture_web_vitals_allowed_metrics",
"posthog_team"."autocapture_exceptions_opt_in",
"posthog_team"."autocapture_exceptions_errors_to_ignore",
"posthog_team"."person_processing_opt_out",
"posthog_team"."session_recording_opt_in",
"posthog_team"."session_recording_sample_rate",
"posthog_team"."session_recording_minimum_duration_milliseconds",
"posthog_team"."session_recording_linked_flag",
"posthog_team"."session_recording_network_payload_capture_config",
"posthog_team"."session_recording_url_trigger_config",
"posthog_team"."session_recording_url_blocklist_config",
"posthog_team"."session_recording_event_trigger_config",
"posthog_team"."session_replay_config",
"posthog_team"."survey_config",
"posthog_team"."capture_console_log_opt_in",
"posthog_team"."capture_performance_opt_in",
"posthog_team"."capture_dead_clicks",
"posthog_team"."surveys_opt_in",
"posthog_team"."heatmaps_opt_in",
"posthog_team"."session_recording_version",
"posthog_team"."signup_token",
"posthog_team"."is_demo",
"posthog_team"."access_control",
"posthog_team"."week_start_day",
"posthog_team"."inject_web_apps",
"posthog_team"."test_account_filters",
"posthog_team"."test_account_filters_default_checked",
"posthog_team"."path_cleaning_filters",
"posthog_team"."timezone",
"posthog_team"."data_attributes",
"posthog_team"."person_display_name_properties",
"posthog_team"."live_events_columns",
"posthog_team"."recording_domains",
"posthog_team"."primary_dashboard_id",
"posthog_team"."extra_settings",
"posthog_team"."modifiers",
"posthog_team"."correlation_config",
"posthog_team"."session_recording_retention_period_days",
"posthog_team"."external_data_workspace_id",
"posthog_team"."external_data_workspace_last_synced_at"
FROM "posthog_team"
WHERE "posthog_team"."id" = 99999
LIMIT 21
SELECT "ee_accesscontrol"."id",
"ee_accesscontrol"."team_id",
"ee_accesscontrol"."access_level",
"ee_accesscontrol"."resource",
"ee_accesscontrol"."resource_id",
"ee_accesscontrol"."organization_member_id",
"ee_accesscontrol"."role_id",
"ee_accesscontrol"."created_by_id",
"ee_accesscontrol"."created_at",
"ee_accesscontrol"."updated_at"
FROM "ee_accesscontrol"
LEFT OUTER JOIN "posthog_organizationmembership" ON ("ee_accesscontrol"."organization_member_id" = "posthog_organizationmembership"."id")
INNER JOIN "posthog_team" ON ("ee_accesscontrol"."team_id" = "posthog_team"."id")
WHERE (("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'project'
AND "ee_accesscontrol"."resource_id" = '253'
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'project'
AND "ee_accesscontrol"."resource_id" = '253'
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'project'
AND "ee_accesscontrol"."resource_id" IS NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'project'
AND "ee_accesscontrol"."resource_id" IS NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'project'
AND "ee_accesscontrol"."resource_id" IS NOT NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "posthog_team"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'project'
AND "ee_accesscontrol"."resource_id" IS NOT NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "posthog_team"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid))
'''
# ---
# name: TestDecide.test_flag_with_behavioural_cohorts

View File

@ -130,28 +130,86 @@
"posthog_organization"."domain_whitelist"
FROM "posthog_organizationmembership"
INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
WHERE "posthog_organizationmembership"."user_id" = 99999
WHERE ("posthog_organizationmembership"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
AND "posthog_organizationmembership"."user_id" = 99999)
LIMIT 21
'''
# ---
# name: TestElement.test_element_stats_postgres_queries_are_as_expected.3
'''
SELECT "posthog_instancesetting"."id",
"posthog_instancesetting"."key",
"posthog_instancesetting"."raw_value"
FROM "posthog_instancesetting"
WHERE "posthog_instancesetting"."key" = 'constance:posthog:RATE_LIMIT_ENABLED'
ORDER BY "posthog_instancesetting"."id" ASC
LIMIT 1
SELECT "ee_accesscontrol"."id",
"ee_accesscontrol"."team_id",
"ee_accesscontrol"."access_level",
"ee_accesscontrol"."resource",
"ee_accesscontrol"."resource_id",
"ee_accesscontrol"."organization_member_id",
"ee_accesscontrol"."role_id",
"ee_accesscontrol"."created_by_id",
"ee_accesscontrol"."created_at",
"ee_accesscontrol"."updated_at"
FROM "ee_accesscontrol"
LEFT OUTER JOIN "posthog_organizationmembership" ON ("ee_accesscontrol"."organization_member_id" = "posthog_organizationmembership"."id")
WHERE (("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'project'
AND "ee_accesscontrol"."resource_id" = '272'
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'project'
AND "ee_accesscontrol"."resource_id" = '272'
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'INTERNAL'
AND "ee_accesscontrol"."resource_id" IS NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'INTERNAL'
AND "ee_accesscontrol"."resource_id" IS NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'INTERNAL'
AND "ee_accesscontrol"."resource_id" IS NOT NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'INTERNAL'
AND "ee_accesscontrol"."resource_id" IS NOT NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999))
'''
# ---
# name: TestElement.test_element_stats_postgres_queries_are_as_expected.4
'''
SELECT "posthog_instancesetting"."id",
"posthog_instancesetting"."key",
"posthog_instancesetting"."raw_value"
FROM "posthog_instancesetting"
WHERE "posthog_instancesetting"."key" = 'constance:posthog:HEATMAP_SAMPLE_N'
ORDER BY "posthog_instancesetting"."id" ASC
LIMIT 1
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
"posthog_organizationmembership"."user_id",
"posthog_organizationmembership"."level",
"posthog_organizationmembership"."joined_at",
"posthog_organizationmembership"."updated_at",
"posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organizationmembership"
INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
WHERE "posthog_organizationmembership"."user_id" = 99999
'''
# ---

View File

@ -1668,6 +1668,69 @@
'''
# ---
# name: TestFeatureFlag.test_creating_static_cohort.10
'''
SELECT "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_person"
WHERE ("posthog_person"."team_id" = 99999
AND ("posthog_person"."properties" -> 'key') = '"value"'::jsonb
AND "posthog_person"."properties" ? 'key'
AND NOT (("posthog_person"."properties" -> 'key') = 'null'::jsonb))
ORDER BY "posthog_person"."id" ASC
LIMIT 10000
'''
# ---
# name: TestFeatureFlag.test_creating_static_cohort.11
'''
SELECT "posthog_persondistinctid"."id",
"posthog_persondistinctid"."team_id",
"posthog_persondistinctid"."person_id",
"posthog_persondistinctid"."distinct_id",
"posthog_persondistinctid"."version"
FROM "posthog_persondistinctid"
WHERE ("posthog_persondistinctid"."id" IN
(SELECT U0."id"
FROM "posthog_persondistinctid" U0
WHERE U0."person_id" = ("posthog_persondistinctid"."person_id")
LIMIT 3)
AND "posthog_persondistinctid"."person_id" IN (1,
2,
3,
4,
5 /* ... */))
'''
# ---
# name: TestFeatureFlag.test_creating_static_cohort.12
'''
SELECT "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_person"
WHERE ("posthog_person"."team_id" = 99999
AND ("posthog_person"."properties" -> 'key') = '"value"'::jsonb
AND "posthog_person"."properties" ? 'key'
AND NOT (("posthog_person"."properties" -> 'key') = 'null'::jsonb))
ORDER BY "posthog_person"."id" ASC
LIMIT 10000
OFFSET 10000
'''
# ---
# name: TestFeatureFlag.test_creating_static_cohort.13
'''
SELECT "posthog_person"."uuid"
FROM "posthog_person"
@ -1681,7 +1744,7 @@
LIMIT 1)))
'''
# ---
# name: TestFeatureFlag.test_creating_static_cohort.11
# name: TestFeatureFlag.test_creating_static_cohort.14
'''
SELECT "posthog_team"."id",
"posthog_team"."uuid",
@ -1751,7 +1814,7 @@
LIMIT 21
'''
# ---
# name: TestFeatureFlag.test_creating_static_cohort.12
# name: TestFeatureFlag.test_creating_static_cohort.15
'''
SELECT "posthog_team"."id",
"posthog_team"."uuid",
@ -1821,7 +1884,7 @@
LIMIT 21
'''
# ---
# name: TestFeatureFlag.test_creating_static_cohort.13
# name: TestFeatureFlag.test_creating_static_cohort.16
'''
SELECT "posthog_experiment"."id",
"posthog_experiment"."name",
@ -1847,7 +1910,7 @@
WHERE "posthog_experiment"."exposure_cohort_id" = 99999
'''
# ---
# name: TestFeatureFlag.test_creating_static_cohort.14
# name: TestFeatureFlag.test_creating_static_cohort.17
'''
/* user_id:0 celery:posthog.tasks.calculate_cohort.insert_cohort_from_feature_flag */
SELECT count(DISTINCT person_id)
@ -1856,7 +1919,7 @@
AND cohort_id = 99999
'''
# ---
# name: TestFeatureFlag.test_creating_static_cohort.15
# name: TestFeatureFlag.test_creating_static_cohort.18
'''
/* user_id:0 request:_snapshot_ */
SELECT id
@ -1877,6 +1940,98 @@
'''
# ---
# name: TestFeatureFlag.test_creating_static_cohort.2
'''
SELECT "posthog_project"."id",
"posthog_project"."organization_id",
"posthog_project"."name",
"posthog_project"."created_at",
"posthog_project"."product_description"
FROM "posthog_project"
WHERE "posthog_project"."id" = 99999
LIMIT 21
'''
# ---
# name: TestFeatureFlag.test_creating_static_cohort.3
'''
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
"posthog_organizationmembership"."user_id",
"posthog_organizationmembership"."level",
"posthog_organizationmembership"."joined_at",
"posthog_organizationmembership"."updated_at",
"posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organizationmembership"
INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
WHERE ("posthog_organizationmembership"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
AND "posthog_organizationmembership"."user_id" = 99999)
LIMIT 21
'''
# ---
# name: TestFeatureFlag.test_creating_static_cohort.4
'''
SELECT "ee_accesscontrol"."id",
"ee_accesscontrol"."team_id",
"ee_accesscontrol"."access_level",
"ee_accesscontrol"."resource",
"ee_accesscontrol"."resource_id",
"ee_accesscontrol"."organization_member_id",
"ee_accesscontrol"."role_id",
"ee_accesscontrol"."created_by_id",
"ee_accesscontrol"."created_at",
"ee_accesscontrol"."updated_at"
FROM "ee_accesscontrol"
LEFT OUTER JOIN "posthog_organizationmembership" ON ("ee_accesscontrol"."organization_member_id" = "posthog_organizationmembership"."id")
WHERE (("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'project'
AND "ee_accesscontrol"."resource_id" = '310'
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'project'
AND "ee_accesscontrol"."resource_id" = '310'
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'feature_flag'
AND "ee_accesscontrol"."resource_id" IS NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'feature_flag'
AND "ee_accesscontrol"."resource_id" IS NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'feature_flag'
AND "ee_accesscontrol"."resource_id" = '130'
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'feature_flag'
AND "ee_accesscontrol"."resource_id" = '130'
AND "ee_accesscontrol"."role_id" IS NULL
AND "ee_accesscontrol"."team_id" = 99999))
'''
# ---
# name: TestFeatureFlag.test_creating_static_cohort.5
'''
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
@ -1908,7 +2063,7 @@
WHERE "posthog_organizationmembership"."user_id" = 99999
'''
# ---
# name: TestFeatureFlag.test_creating_static_cohort.3
# name: TestFeatureFlag.test_creating_static_cohort.6
'''
SELECT "posthog_organization"."id",
"posthog_organization"."name",
@ -1934,7 +2089,7 @@
LIMIT 21
'''
# ---
# name: TestFeatureFlag.test_creating_static_cohort.4
# name: TestFeatureFlag.test_creating_static_cohort.7
'''
SELECT "posthog_featureflag"."id",
"posthog_featureflag"."key",
@ -1985,7 +2140,7 @@
LIMIT 21
'''
# ---
# name: TestFeatureFlag.test_creating_static_cohort.5
# name: TestFeatureFlag.test_creating_static_cohort.8
'''
SELECT "posthog_featureflag"."id",
"posthog_featureflag"."key",
@ -2008,7 +2163,7 @@
LIMIT 21
'''
# ---
# name: TestFeatureFlag.test_creating_static_cohort.6
# name: TestFeatureFlag.test_creating_static_cohort.9
'''
SELECT "posthog_cohort"."id",
"posthog_cohort"."name",
@ -2034,69 +2189,6 @@
LIMIT 21
'''
# ---
# name: TestFeatureFlag.test_creating_static_cohort.7
'''
SELECT "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_person"
WHERE ("posthog_person"."team_id" = 99999
AND ("posthog_person"."properties" -> 'key') = '"value"'::jsonb
AND "posthog_person"."properties" ? 'key'
AND NOT (("posthog_person"."properties" -> 'key') = 'null'::jsonb))
ORDER BY "posthog_person"."id" ASC
LIMIT 10000
'''
# ---
# name: TestFeatureFlag.test_creating_static_cohort.8
'''
SELECT "posthog_persondistinctid"."id",
"posthog_persondistinctid"."team_id",
"posthog_persondistinctid"."person_id",
"posthog_persondistinctid"."distinct_id",
"posthog_persondistinctid"."version"
FROM "posthog_persondistinctid"
WHERE ("posthog_persondistinctid"."id" IN
(SELECT U0."id"
FROM "posthog_persondistinctid" U0
WHERE U0."person_id" = ("posthog_persondistinctid"."person_id")
LIMIT 3)
AND "posthog_persondistinctid"."person_id" IN (1,
2,
3,
4,
5 /* ... */))
'''
# ---
# name: TestFeatureFlag.test_creating_static_cohort.9
'''
SELECT "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_person"
WHERE ("posthog_person"."team_id" = 99999
AND ("posthog_person"."properties" -> 'key') = '"value"'::jsonb
AND "posthog_person"."properties" ? 'key'
AND NOT (("posthog_person"."properties" -> 'key') = 'null'::jsonb))
ORDER BY "posthog_person"."id" ASC
LIMIT 10000
OFFSET 10000
'''
# ---
# name: TestResiliency.test_feature_flags_v3_with_experience_continuity_working_slow_db
'''
WITH target_person_ids AS

File diff suppressed because it is too large Load Diff

View File

@ -1155,6 +1155,40 @@
'''
# ---
# name: TestOrganizationFeatureFlagCopy.test_copy_feature_flag_create_new.36
'''
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
"posthog_organizationmembership"."user_id",
"posthog_organizationmembership"."level",
"posthog_organizationmembership"."joined_at",
"posthog_organizationmembership"."updated_at",
"posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organizationmembership"
INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
WHERE ("posthog_organizationmembership"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
AND "posthog_organizationmembership"."user_id" = 99999)
LIMIT 21
'''
# ---
# name: TestOrganizationFeatureFlagCopy.test_copy_feature_flag_create_new.37
'''
SELECT "posthog_dashboard"."id",
"posthog_dashboard"."name",
@ -1179,26 +1213,38 @@
AND "posthog_featureflagdashboards"."feature_flag_id" = 99999)
'''
# ---
# name: TestOrganizationFeatureFlagCopy.test_copy_feature_flag_create_new.37
'''
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
'''
# ---
# name: TestOrganizationFeatureFlagCopy.test_copy_feature_flag_create_new.38
'''
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
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
"posthog_organizationmembership"."user_id",
"posthog_organizationmembership"."level",
"posthog_organizationmembership"."joined_at",
"posthog_organizationmembership"."updated_at",
"posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organizationmembership"
INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
WHERE ("posthog_organizationmembership"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
AND "posthog_organizationmembership"."user_id" = 99999)
LIMIT 21
'''
# ---
# name: TestOrganizationFeatureFlagCopy.test_copy_feature_flag_create_new.39

View File

@ -84,6 +84,102 @@
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.11
'''
SELECT 1 AS "a"
FROM "posthog_organizationmembership"
WHERE ("posthog_organizationmembership"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
AND "posthog_organizationmembership"."user_id" = 99999)
LIMIT 1
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.12
'''
SELECT "posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organization"
WHERE "posthog_organization"."id" = '00000000-0000-0000-0000-000000000000'::uuid
LIMIT 21
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.13
'''
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
"posthog_organizationmembership"."user_id",
"posthog_organizationmembership"."level",
"posthog_organizationmembership"."joined_at",
"posthog_organizationmembership"."updated_at",
"posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organizationmembership"
INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
WHERE ("posthog_organizationmembership"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
AND "posthog_organizationmembership"."user_id" = 99999)
LIMIT 21
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.14
'''
SELECT "ee_accesscontrol"."id",
"ee_accesscontrol"."team_id",
"ee_accesscontrol"."access_level",
"ee_accesscontrol"."resource",
"ee_accesscontrol"."resource_id",
"ee_accesscontrol"."organization_member_id",
"ee_accesscontrol"."role_id",
"ee_accesscontrol"."created_by_id",
"ee_accesscontrol"."created_at",
"ee_accesscontrol"."updated_at"
FROM "ee_accesscontrol"
LEFT OUTER JOIN "posthog_organizationmembership" ON ("ee_accesscontrol"."organization_member_id" = "posthog_organizationmembership"."id")
INNER JOIN "posthog_team" ON ("ee_accesscontrol"."team_id" = "posthog_team"."id")
WHERE (("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'plugin'
AND "ee_accesscontrol"."resource_id" IS NOT NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "posthog_team"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'plugin'
AND "ee_accesscontrol"."resource_id" IS NOT NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "posthog_team"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid))
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.15
'''
SELECT COUNT(*) AS "__count"
FROM "posthog_plugin"
@ -97,7 +193,7 @@
AND U1."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid)))
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.12
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.16
'''
SELECT "posthog_plugin"."id",
"posthog_plugin"."organization_id",
@ -156,7 +252,7 @@
LIMIT 100
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.13
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.17
'''
SELECT "posthog_user"."id",
"posthog_user"."password",
@ -188,135 +284,35 @@
LIMIT 21
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.14
'''
SELECT "posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organization"
WHERE "posthog_organization"."id" = '00000000-0000-0000-0000-000000000000'::uuid
LIMIT 21
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.15
'''
SELECT "posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organization"
WHERE "posthog_organization"."id" = '00000000-0000-0000-0000-000000000000'::uuid
LIMIT 21
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.16
'''
SELECT 1 AS "a"
FROM "posthog_organizationmembership"
WHERE ("posthog_organizationmembership"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
AND "posthog_organizationmembership"."user_id" = 99999)
LIMIT 1
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.17
'''
SELECT "posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organization"
WHERE "posthog_organization"."id" = '00000000-0000-0000-0000-000000000000'::uuid
LIMIT 21
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.18
'''
SELECT COUNT(*) AS "__count"
FROM "posthog_plugin"
WHERE ("posthog_plugin"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
OR "posthog_plugin"."is_global"
OR "posthog_plugin"."id" IN
(SELECT U0."plugin_id"
FROM "posthog_pluginconfig" U0
INNER JOIN "posthog_team" U1 ON (U0."team_id" = U1."id")
WHERE (NOT U0."deleted"
AND U1."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid)))
SELECT "posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organization"
WHERE "posthog_organization"."id" = '00000000-0000-0000-0000-000000000000'::uuid
LIMIT 21
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.19
'''
SELECT "posthog_plugin"."id",
"posthog_plugin"."organization_id",
"posthog_plugin"."plugin_type",
"posthog_plugin"."is_global",
"posthog_plugin"."is_preinstalled",
"posthog_plugin"."is_stateless",
"posthog_plugin"."name",
"posthog_plugin"."description",
"posthog_plugin"."url",
"posthog_plugin"."icon",
"posthog_plugin"."config_schema",
"posthog_plugin"."tag",
"posthog_plugin"."archive",
"posthog_plugin"."latest_tag",
"posthog_plugin"."latest_tag_checked_at",
"posthog_plugin"."capabilities",
"posthog_plugin"."metrics",
"posthog_plugin"."public_jobs",
"posthog_plugin"."error",
"posthog_plugin"."from_json",
"posthog_plugin"."from_web",
"posthog_plugin"."source",
"posthog_plugin"."created_at",
"posthog_plugin"."updated_at",
"posthog_plugin"."log_level",
"posthog_organization"."id",
SELECT "posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
@ -335,17 +331,9 @@
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_plugin"
LEFT OUTER JOIN "posthog_organization" ON ("posthog_plugin"."organization_id" = "posthog_organization"."id")
WHERE ("posthog_plugin"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
OR "posthog_plugin"."is_global"
OR "posthog_plugin"."id" IN
(SELECT U0."plugin_id"
FROM "posthog_pluginconfig" U0
INNER JOIN "posthog_team" U1 ON (U0."team_id" = U1."id")
WHERE (NOT U0."deleted"
AND U1."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid)))
LIMIT 100
FROM "posthog_organization"
WHERE "posthog_organization"."id" = '00000000-0000-0000-0000-000000000000'::uuid
LIMIT 21
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.2
@ -376,34 +364,11 @@
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.20
'''
SELECT "posthog_user"."id",
"posthog_user"."password",
"posthog_user"."last_login",
"posthog_user"."first_name",
"posthog_user"."last_name",
"posthog_user"."is_staff",
"posthog_user"."date_joined",
"posthog_user"."uuid",
"posthog_user"."current_organization_id",
"posthog_user"."current_team_id",
"posthog_user"."email",
"posthog_user"."pending_email",
"posthog_user"."temporary_token",
"posthog_user"."distinct_id",
"posthog_user"."is_email_verified",
"posthog_user"."has_seen_product_intro_for",
"posthog_user"."strapi_id",
"posthog_user"."is_active",
"posthog_user"."theme_mode",
"posthog_user"."partial_notification_settings",
"posthog_user"."anonymize_data",
"posthog_user"."toolbar_mode",
"posthog_user"."hedgehog_config",
"posthog_user"."events_column_config",
"posthog_user"."email_opt_in"
FROM "posthog_user"
WHERE "posthog_user"."id" = 99999
LIMIT 21
SELECT 1 AS "a"
FROM "posthog_organizationmembership"
WHERE ("posthog_organizationmembership"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
AND "posthog_organizationmembership"."user_id" = 99999)
LIMIT 1
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.21
@ -434,7 +399,13 @@
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.22
'''
SELECT "posthog_organization"."id",
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
"posthog_organizationmembership"."user_id",
"posthog_organizationmembership"."level",
"posthog_organizationmembership"."joined_at",
"posthog_organizationmembership"."updated_at",
"posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
@ -453,47 +424,41 @@
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organization"
WHERE "posthog_organization"."id" = '00000000-0000-0000-0000-000000000000'::uuid
FROM "posthog_organizationmembership"
INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
WHERE ("posthog_organizationmembership"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
AND "posthog_organizationmembership"."user_id" = 99999)
LIMIT 21
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.23
'''
SELECT 1 AS "a"
FROM "posthog_organizationmembership"
WHERE ("posthog_organizationmembership"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
AND "posthog_organizationmembership"."user_id" = 99999)
LIMIT 1
SELECT "ee_accesscontrol"."id",
"ee_accesscontrol"."team_id",
"ee_accesscontrol"."access_level",
"ee_accesscontrol"."resource",
"ee_accesscontrol"."resource_id",
"ee_accesscontrol"."organization_member_id",
"ee_accesscontrol"."role_id",
"ee_accesscontrol"."created_by_id",
"ee_accesscontrol"."created_at",
"ee_accesscontrol"."updated_at"
FROM "ee_accesscontrol"
LEFT OUTER JOIN "posthog_organizationmembership" ON ("ee_accesscontrol"."organization_member_id" = "posthog_organizationmembership"."id")
INNER JOIN "posthog_team" ON ("ee_accesscontrol"."team_id" = "posthog_team"."id")
WHERE (("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'plugin'
AND "ee_accesscontrol"."resource_id" IS NOT NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "posthog_team"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'plugin'
AND "ee_accesscontrol"."resource_id" IS NOT NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "posthog_team"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid))
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.24
'''
SELECT "posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organization"
WHERE "posthog_organization"."id" = '00000000-0000-0000-0000-000000000000'::uuid
LIMIT 21
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.25
'''
SELECT COUNT(*) AS "__count"
FROM "posthog_plugin"
@ -507,7 +472,7 @@
AND U1."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid)))
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.26
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.25
'''
SELECT "posthog_plugin"."id",
"posthog_plugin"."organization_id",
@ -566,56 +531,7 @@
LIMIT 100
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.3
'''
SELECT 1 AS "a"
FROM "posthog_organizationmembership"
WHERE ("posthog_organizationmembership"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
AND "posthog_organizationmembership"."user_id" = 99999)
LIMIT 1
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.4
'''
SELECT "posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organization"
WHERE "posthog_organization"."id" = '00000000-0000-0000-0000-000000000000'::uuid
LIMIT 21
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.5
'''
SELECT COUNT(*) AS "__count"
FROM "posthog_plugin"
WHERE ("posthog_plugin"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
OR "posthog_plugin"."is_global"
OR "posthog_plugin"."id" IN
(SELECT U0."plugin_id"
FROM "posthog_pluginconfig" U0
INNER JOIN "posthog_team" U1 ON (U0."team_id" = U1."id")
WHERE (NOT U0."deleted"
AND U1."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid)))
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.6
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.26
'''
SELECT "posthog_user"."id",
"posthog_user"."password",
@ -647,7 +563,7 @@
LIMIT 21
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.7
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.27
'''
SELECT "posthog_organization"."id",
"posthog_organization"."name",
@ -673,7 +589,7 @@
LIMIT 21
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.8
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.28
'''
SELECT "posthog_organization"."id",
"posthog_organization"."name",
@ -699,7 +615,7 @@
LIMIT 21
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.9
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.29
'''
SELECT 1 AS "a"
FROM "posthog_organizationmembership"
@ -708,3 +624,331 @@
LIMIT 1
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.3
'''
SELECT 1 AS "a"
FROM "posthog_organizationmembership"
WHERE ("posthog_organizationmembership"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
AND "posthog_organizationmembership"."user_id" = 99999)
LIMIT 1
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.30
'''
SELECT "posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organization"
WHERE "posthog_organization"."id" = '00000000-0000-0000-0000-000000000000'::uuid
LIMIT 21
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.31
'''
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
"posthog_organizationmembership"."user_id",
"posthog_organizationmembership"."level",
"posthog_organizationmembership"."joined_at",
"posthog_organizationmembership"."updated_at",
"posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organizationmembership"
INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
WHERE ("posthog_organizationmembership"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
AND "posthog_organizationmembership"."user_id" = 99999)
LIMIT 21
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.32
'''
SELECT "ee_accesscontrol"."id",
"ee_accesscontrol"."team_id",
"ee_accesscontrol"."access_level",
"ee_accesscontrol"."resource",
"ee_accesscontrol"."resource_id",
"ee_accesscontrol"."organization_member_id",
"ee_accesscontrol"."role_id",
"ee_accesscontrol"."created_by_id",
"ee_accesscontrol"."created_at",
"ee_accesscontrol"."updated_at"
FROM "ee_accesscontrol"
LEFT OUTER JOIN "posthog_organizationmembership" ON ("ee_accesscontrol"."organization_member_id" = "posthog_organizationmembership"."id")
INNER JOIN "posthog_team" ON ("ee_accesscontrol"."team_id" = "posthog_team"."id")
WHERE (("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'plugin'
AND "ee_accesscontrol"."resource_id" IS NOT NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "posthog_team"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'plugin'
AND "ee_accesscontrol"."resource_id" IS NOT NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "posthog_team"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid))
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.33
'''
SELECT COUNT(*) AS "__count"
FROM "posthog_plugin"
WHERE ("posthog_plugin"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
OR "posthog_plugin"."is_global"
OR "posthog_plugin"."id" IN
(SELECT U0."plugin_id"
FROM "posthog_pluginconfig" U0
INNER JOIN "posthog_team" U1 ON (U0."team_id" = U1."id")
WHERE (NOT U0."deleted"
AND U1."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid)))
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.34
'''
SELECT "posthog_plugin"."id",
"posthog_plugin"."organization_id",
"posthog_plugin"."plugin_type",
"posthog_plugin"."is_global",
"posthog_plugin"."is_preinstalled",
"posthog_plugin"."is_stateless",
"posthog_plugin"."name",
"posthog_plugin"."description",
"posthog_plugin"."url",
"posthog_plugin"."icon",
"posthog_plugin"."config_schema",
"posthog_plugin"."tag",
"posthog_plugin"."archive",
"posthog_plugin"."latest_tag",
"posthog_plugin"."latest_tag_checked_at",
"posthog_plugin"."capabilities",
"posthog_plugin"."metrics",
"posthog_plugin"."public_jobs",
"posthog_plugin"."error",
"posthog_plugin"."from_json",
"posthog_plugin"."from_web",
"posthog_plugin"."source",
"posthog_plugin"."created_at",
"posthog_plugin"."updated_at",
"posthog_plugin"."log_level",
"posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_plugin"
LEFT OUTER JOIN "posthog_organization" ON ("posthog_plugin"."organization_id" = "posthog_organization"."id")
WHERE ("posthog_plugin"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
OR "posthog_plugin"."is_global"
OR "posthog_plugin"."id" IN
(SELECT U0."plugin_id"
FROM "posthog_pluginconfig" U0
INNER JOIN "posthog_team" U1 ON (U0."team_id" = U1."id")
WHERE (NOT U0."deleted"
AND U1."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid)))
LIMIT 100
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.4
'''
SELECT "posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organization"
WHERE "posthog_organization"."id" = '00000000-0000-0000-0000-000000000000'::uuid
LIMIT 21
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.5
'''
SELECT "posthog_organizationmembership"."id",
"posthog_organizationmembership"."organization_id",
"posthog_organizationmembership"."user_id",
"posthog_organizationmembership"."level",
"posthog_organizationmembership"."joined_at",
"posthog_organizationmembership"."updated_at",
"posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organizationmembership"
INNER JOIN "posthog_organization" ON ("posthog_organizationmembership"."organization_id" = "posthog_organization"."id")
WHERE ("posthog_organizationmembership"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
AND "posthog_organizationmembership"."user_id" = 99999)
LIMIT 21
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.6
'''
SELECT "ee_accesscontrol"."id",
"ee_accesscontrol"."team_id",
"ee_accesscontrol"."access_level",
"ee_accesscontrol"."resource",
"ee_accesscontrol"."resource_id",
"ee_accesscontrol"."organization_member_id",
"ee_accesscontrol"."role_id",
"ee_accesscontrol"."created_by_id",
"ee_accesscontrol"."created_at",
"ee_accesscontrol"."updated_at"
FROM "ee_accesscontrol"
LEFT OUTER JOIN "posthog_organizationmembership" ON ("ee_accesscontrol"."organization_member_id" = "posthog_organizationmembership"."id")
INNER JOIN "posthog_team" ON ("ee_accesscontrol"."team_id" = "posthog_team"."id")
WHERE (("ee_accesscontrol"."organization_member_id" IS NULL
AND "ee_accesscontrol"."resource" = 'plugin'
AND "ee_accesscontrol"."resource_id" IS NOT NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "posthog_team"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid)
OR ("posthog_organizationmembership"."user_id" = 99999
AND "ee_accesscontrol"."resource" = 'plugin'
AND "ee_accesscontrol"."resource_id" IS NOT NULL
AND "ee_accesscontrol"."role_id" IS NULL
AND "posthog_team"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid))
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.7
'''
SELECT COUNT(*) AS "__count"
FROM "posthog_plugin"
WHERE ("posthog_plugin"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid
OR "posthog_plugin"."is_global"
OR "posthog_plugin"."id" IN
(SELECT U0."plugin_id"
FROM "posthog_pluginconfig" U0
INNER JOIN "posthog_team" U1 ON (U0."team_id" = U1."id")
WHERE (NOT U0."deleted"
AND U1."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid)))
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.8
'''
SELECT "posthog_user"."id",
"posthog_user"."password",
"posthog_user"."last_login",
"posthog_user"."first_name",
"posthog_user"."last_name",
"posthog_user"."is_staff",
"posthog_user"."date_joined",
"posthog_user"."uuid",
"posthog_user"."current_organization_id",
"posthog_user"."current_team_id",
"posthog_user"."email",
"posthog_user"."pending_email",
"posthog_user"."temporary_token",
"posthog_user"."distinct_id",
"posthog_user"."is_email_verified",
"posthog_user"."has_seen_product_intro_for",
"posthog_user"."strapi_id",
"posthog_user"."is_active",
"posthog_user"."theme_mode",
"posthog_user"."partial_notification_settings",
"posthog_user"."anonymize_data",
"posthog_user"."toolbar_mode",
"posthog_user"."hedgehog_config",
"posthog_user"."events_column_config",
"posthog_user"."email_opt_in"
FROM "posthog_user"
WHERE "posthog_user"."id" = 99999
LIMIT 21
'''
# ---
# name: TestPluginAPI.test_listing_plugins_is_not_nplus1.9
'''
SELECT "posthog_organization"."id",
"posthog_organization"."name",
"posthog_organization"."slug",
"posthog_organization"."logo_media_id",
"posthog_organization"."created_at",
"posthog_organization"."updated_at",
"posthog_organization"."plugins_access_level",
"posthog_organization"."for_internal_metrics",
"posthog_organization"."is_member_join_email_enabled",
"posthog_organization"."enforce_2fa",
"posthog_organization"."is_hipaa",
"posthog_organization"."customer_id",
"posthog_organization"."available_product_features",
"posthog_organization"."usage",
"posthog_organization"."never_drop_data",
"posthog_organization"."customer_trust_scores",
"posthog_organization"."setup_section_2_completed",
"posthog_organization"."personalization",
"posthog_organization"."domain_whitelist"
FROM "posthog_organization"
WHERE "posthog_organization"."id" = '00000000-0000-0000-0000-000000000000'::uuid
LIMIT 21
'''
# ---

File diff suppressed because it is too large Load Diff

View File

@ -267,19 +267,21 @@ class TestDashboard(APIBaseTest, QueryMatchingTest):
"insight": "TRENDS",
}
with self.assertNumQueries(11):
baseline = 3
with self.assertNumQueries(baseline + 10):
self.dashboard_api.get_dashboard(dashboard_id, query_params={"no_items_field": "true"})
self.dashboard_api.create_insight({"filters": filter_dict, "dashboards": [dashboard_id]})
with self.assertNumQueries(21):
with self.assertNumQueries(baseline + 10 + 10):
self.dashboard_api.get_dashboard(dashboard_id, query_params={"no_items_field": "true"})
self.dashboard_api.create_insight({"filters": filter_dict, "dashboards": [dashboard_id]})
with self.assertNumQueries(21):
with self.assertNumQueries(baseline + 10 + 10):
self.dashboard_api.get_dashboard(dashboard_id, query_params={"no_items_field": "true"})
self.dashboard_api.create_insight({"filters": filter_dict, "dashboards": [dashboard_id]})
with self.assertNumQueries(21):
with self.assertNumQueries(baseline + 10 + 10):
self.dashboard_api.get_dashboard(dashboard_id, query_params={"no_items_field": "true"})
@snapshot_postgres_queries
@ -296,7 +298,7 @@ class TestDashboard(APIBaseTest, QueryMatchingTest):
)
self.client.force_login(user_with_collaboration)
with self.assertNumQueries(7):
with self.assertNumQueries(9):
self.dashboard_api.list_dashboards()
for i in range(5):
@ -304,7 +306,7 @@ class TestDashboard(APIBaseTest, QueryMatchingTest):
for j in range(3):
self.dashboard_api.create_insight({"dashboards": [dashboard_id], "name": f"insight-{j}"})
with self.assertNumQueries(FuzzyInt(8, 9)):
with self.assertNumQueries(FuzzyInt(10, 11)):
self.dashboard_api.list_dashboards(query_params={"limit": 300})
def test_listing_dashboards_does_not_include_tiles(self) -> None:
@ -1339,6 +1341,7 @@ class TestDashboard(APIBaseTest, QueryMatchingTest):
"tags": [],
"timezone": None,
"updated_at": ANY,
"user_access_level": "editor",
"hogql": ANY,
"types": ANY,
},

File diff suppressed because it is too large Load Diff

View File

@ -95,6 +95,7 @@ class TestNotebooks(APIBaseTest, QueryMatchingTest):
"deleted": False,
"last_modified_at": mock.ANY,
"last_modified_by": response.json()["last_modified_by"],
"user_access_level": "editor",
}
self.assert_notebook_activity(

View File

@ -261,7 +261,7 @@ class TestActionApi(ClickhouseTestMixin, APIBaseTest, QueryMatchingTest):
)
# test queries
with self.assertNumQueries(FuzzyInt(6, 8)):
with self.assertNumQueries(FuzzyInt(9, 11)):
# Django session, user, team, org membership, instance setting, org,
# count, action
self.client.get(f"/api/projects/{self.team.id}/actions/")
@ -361,7 +361,7 @@ class TestActionApi(ClickhouseTestMixin, APIBaseTest, QueryMatchingTest):
# Pre-query to cache things like instance settings
self.client.get(f"/api/projects/{self.team.id}/actions/")
with self.assertNumQueries(6), snapshot_postgres_queries_context(self):
with self.assertNumQueries(9), snapshot_postgres_queries_context(self):
self.client.get(f"/api/projects/{self.team.id}/actions/")
Action.objects.create(
@ -370,7 +370,7 @@ class TestActionApi(ClickhouseTestMixin, APIBaseTest, QueryMatchingTest):
created_by=User.objects.create_and_join(self.organization, "a", ""),
)
with self.assertNumQueries(6), snapshot_postgres_queries_context(self):
with self.assertNumQueries(9), snapshot_postgres_queries_context(self):
self.client.get(f"/api/projects/{self.team.id}/actions/")
Action.objects.create(
@ -379,7 +379,7 @@ class TestActionApi(ClickhouseTestMixin, APIBaseTest, QueryMatchingTest):
created_by=User.objects.create_and_join(self.organization, "b", ""),
)
with self.assertNumQueries(6), snapshot_postgres_queries_context(self):
with self.assertNumQueries(9), snapshot_postgres_queries_context(self):
self.client.get(f"/api/projects/{self.team.id}/actions/")
def test_get_tags_on_non_ee_returns_empty_list(self):

View File

@ -298,7 +298,7 @@ class TestActivityLog(APIBaseTest, QueryMatchingTest):
user=user, defaults={"last_viewed_activity_date": f"2023-0{i}-17T04:36:50Z"}
)
with self.assertNumQueries(FuzzyInt(39, 39)):
with self.assertNumQueries(FuzzyInt(42, 42)):
self.client.get(f"/api/projects/{self.team.id}/activity_log/important_changes")
def test_can_list_all_activity(self) -> None:

View File

@ -36,7 +36,7 @@ class TestAnnotation(APIBaseTest, QueryMatchingTest):
"""
see https://sentry.io/organizations/posthog/issues/3706110236/events/db0167ece56649f59b013cbe9de7ba7a/?project=1899813
"""
with self.assertNumQueries(FuzzyInt(6, 7)), snapshot_postgres_queries_context(self):
with self.assertNumQueries(FuzzyInt(8, 9)), snapshot_postgres_queries_context(self):
response = self.client.get(f"/api/projects/{self.team.id}/annotations/").json()
self.assertEqual(len(response["results"]), 0)
@ -48,7 +48,7 @@ class TestAnnotation(APIBaseTest, QueryMatchingTest):
content=now().isoformat(),
)
with self.assertNumQueries(FuzzyInt(6, 7)), snapshot_postgres_queries_context(self):
with self.assertNumQueries(FuzzyInt(8, 9)), snapshot_postgres_queries_context(self):
response = self.client.get(f"/api/projects/{self.team.id}/annotations/").json()
self.assertEqual(len(response["results"]), 1)
@ -60,7 +60,7 @@ class TestAnnotation(APIBaseTest, QueryMatchingTest):
content=now().isoformat(),
)
with self.assertNumQueries(FuzzyInt(6, 7)), snapshot_postgres_queries_context(self):
with self.assertNumQueries(FuzzyInt(8, 9)), snapshot_postgres_queries_context(self):
response = self.client.get(f"/api/projects/{self.team.id}/annotations/").json()
self.assertEqual(len(response["results"]), 2)

View File

@ -241,7 +241,7 @@ class TestCohort(TestExportMixin, ClickhouseTestMixin, APIBaseTest, QueryMatchin
)
self.assertEqual(response.status_code, 201, response.content)
with self.assertNumQueries(9):
with self.assertNumQueries(12):
response = self.client.get(f"/api/projects/{self.team.id}/cohorts")
assert len(response.json()["results"]) == 1
@ -256,7 +256,7 @@ class TestCohort(TestExportMixin, ClickhouseTestMixin, APIBaseTest, QueryMatchin
)
self.assertEqual(response.status_code, 201, response.content)
with self.assertNumQueries(9):
with self.assertNumQueries(12):
response = self.client.get(f"/api/projects/{self.team.id}/cohorts")
assert len(response.json()["results"]) == 3

View File

@ -4699,7 +4699,7 @@ class TestDecideUsesReadReplica(TransactionTestCase):
response = self.client.get(f"/api/feature_flag/local_evaluation")
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
with self.assertNumQueries(3, using="replica"), self.assertNumQueries(5, using="default"):
with self.assertNumQueries(3, using="replica"), self.assertNumQueries(12, using="default"):
# Captured queries for write DB:
# E 1. UPDATE "posthog_personalapikey" SET "last_used_at" = '2023-08-01T11:26:50.728057+00:00'
# E 2. SELECT "posthog_team"."id", "posthog_team"."uuid", "posthog_team"."organization_id"
@ -4940,7 +4940,7 @@ class TestDecideUsesReadReplica(TransactionTestCase):
PersonalAPIKey.objects.create(label="X", user=self.user, secure_value=hash_key_value(personal_api_key))
cache.clear()
with self.assertNumQueries(4, using="replica"), self.assertNumQueries(5, using="default"):
with self.assertNumQueries(4, using="replica"), self.assertNumQueries(12, using="default"):
# Captured queries for write DB:
# E 1. UPDATE "posthog_personalapikey" SET "last_used_at" = '2023-08-01T11:26:50.728057+00:00'
# E 2. SELECT "posthog_team"."id", "posthog_team"."uuid", "posthog_team"."organization_id"
@ -5210,7 +5210,7 @@ class TestDecideUsesReadReplica(TransactionTestCase):
client.logout()
self.client.logout()
with self.assertNumQueries(4, using="replica"), self.assertNumQueries(5, using="default"):
with self.assertNumQueries(4, using="replica"), self.assertNumQueries(12, using="default"):
# Captured queries for write DB:
# E 1. UPDATE "posthog_personalapikey" SET "last_used_at" = '2023-08-01T11:26:50.728057+00:00'
# E 2. SELECT "posthog_team"."id", "posthog_team"."uuid", "posthog_team"."organization_id"

View File

@ -97,7 +97,7 @@ class TestEvents(ClickhouseTestMixin, APIBaseTest):
# Django session, PostHog user, PostHog team, PostHog org membership,
# instance setting check, person and distinct id
with self.assertNumQueries(7):
with self.assertNumQueries(9):
response = self.client.get(f"/api/projects/{self.team.id}/events/?event=event_name").json()
self.assertEqual(response["results"][0]["event"], "event_name")
@ -125,7 +125,7 @@ class TestEvents(ClickhouseTestMixin, APIBaseTest):
# Django session, PostHog user, PostHog team, PostHog org membership,
# look up if rate limit is enabled (cached after first lookup), instance
# setting (poe, rate limit), person and distinct id
expected_queries = 8
expected_queries = 10
with self.assertNumQueries(expected_queries):
response = self.client.get(

View File

@ -1244,7 +1244,7 @@ class TestFeatureFlag(APIBaseTest, ClickhouseTestMixin):
format="json",
).json()
with self.assertNumQueries(FuzzyInt(5, 6)):
with self.assertNumQueries(FuzzyInt(8, 9)):
response = self.client.get(f"/api/projects/{self.team.id}/feature_flags/my_flags")
self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -1259,7 +1259,7 @@ class TestFeatureFlag(APIBaseTest, ClickhouseTestMixin):
format="json",
).json()
with self.assertNumQueries(FuzzyInt(5, 6)):
with self.assertNumQueries(FuzzyInt(7, 8)):
response = self.client.get(f"/api/projects/{self.team.id}/feature_flags/my_flags")
self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -1274,7 +1274,7 @@ class TestFeatureFlag(APIBaseTest, ClickhouseTestMixin):
format="json",
).json()
with self.assertNumQueries(FuzzyInt(11, 12)):
with self.assertNumQueries(FuzzyInt(14, 15)):
response = self.client.get(f"/api/projects/{self.team.id}/feature_flags")
self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -1289,7 +1289,7 @@ class TestFeatureFlag(APIBaseTest, ClickhouseTestMixin):
format="json",
).json()
with self.assertNumQueries(FuzzyInt(11, 12)):
with self.assertNumQueries(FuzzyInt(14, 15)):
response = self.client.get(f"/api/projects/{self.team.id}/feature_flags")
self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -1313,7 +1313,7 @@ class TestFeatureFlag(APIBaseTest, ClickhouseTestMixin):
name="Flag role access",
)
with self.assertNumQueries(FuzzyInt(11, 12)):
with self.assertNumQueries(FuzzyInt(14, 15)):
response = self.client.get(f"/api/projects/{self.team.id}/feature_flags")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.json()["results"]), 2)
@ -2229,19 +2229,23 @@ class TestFeatureFlag(APIBaseTest, ClickhouseTestMixin):
self.client.logout()
with self.assertNumQueries(12):
# E 1. SAVEPOINT
# E 2. SELECT "posthog_personalapikey"."id"
# E 3. RELEASE SAVEPOINT
# E 4. UPDATE "posthog_personalapikey" SET "last_used_at" = '2024-01-31T13:01:37.394080+00:00'
# E 5. SELECT "posthog_team"."id", "posthog_team"."uuid"
# E 6. SELECT "posthog_organizationmembership"."id", "posthog_organizationmembership"."organization_id"
# E 7. SELECT "posthog_cohort"."id" -- all cohorts
# E 8. SELECT "posthog_featureflag"."id", "posthog_featureflag"."key", -- all flags
# E 9. SELECT "posthog_cohort". id = 99999
# E 10. SELECT "posthog_cohort". id = deleted cohort
# E 11. SELECT "posthog_cohort". id = cohort from other team
# E 12. SELECT "posthog_grouptypemapping"."id", -- group type mapping
with self.assertNumQueries(16):
# 1. SAVEPOINT
# 2. SELECT "posthog_personalapikey"."id",
# 3. RELEASE SAVEPOINT
# 4. UPDATE "posthog_personalapikey" SET "last_used_at"
# 5. SELECT "posthog_team"."id", "posthog_team"."uuid",
# 6. SELECT "posthog_team"."id", "posthog_team"."uuid",
# 7. SELECT "posthog_project"."id", "posthog_project"."organization_id",
# 8. SELECT "posthog_organizationmembership"."id",
# 9. SELECT "ee_accesscontrol"."id",
# 10. SELECT "posthog_organizationmembership"."id",
# 11. SELECT "posthog_cohort"."id" -- all cohorts
# 12. SELECT "posthog_featureflag"."id", "posthog_featureflag"."key", -- all flags
# 13. SELECT "posthog_cohort". id = 99999
# 14. SELECT "posthog_cohort". id = deleted cohort
# 15. SELECT "posthog_cohort". id = cohort from other team
# 16. SELECT "posthog_grouptypemapping"."id", -- group type mapping
response = self.client.get(
f"/api/feature_flag/local_evaluation?token={self.team.api_token}&send_cohorts",

View File

@ -506,11 +506,11 @@ class TestInsight(ClickhouseTestMixin, APIBaseTest, QueryMatchingTest):
# adding more insights doesn't change the query count
self.assertEqual(
[
FuzzyInt(10, 11),
FuzzyInt(10, 11),
FuzzyInt(10, 11),
FuzzyInt(10, 11),
FuzzyInt(10, 11),
FuzzyInt(12, 13),
FuzzyInt(12, 13),
FuzzyInt(12, 13),
FuzzyInt(12, 13),
FuzzyInt(12, 13),
],
query_counts,
f"received query counts\n\n{query_counts}",

View File

@ -159,6 +159,51 @@ class TestOrganizationAPI(APIBaseTest):
"Only the scoped organization should be listed, the other one should be excluded",
)
def test_delete_organizations_and_verify_list(self):
self.organization_membership.level = OrganizationMembership.Level.OWNER
self.organization_membership.save()
# Create two additional organizations
org2 = Organization.objects.bootstrap(self.user)[0]
org3 = Organization.objects.bootstrap(self.user)[0]
self.user.current_organization_id = self.organization.id
self.user.save()
# Verify we start with 3 organizations
response = self.client.get("/api/organizations/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.json()["results"]), 3)
# Delete first organization and verify list
response = self.client.delete(f"/api/organizations/{org2.id}")
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
response = self.client.get("/api/organizations/")
self.assertEqual(len(response.json()["results"]), 2)
org_ids = {org["id"] for org in response.json()["results"]}
self.assertEqual(org_ids, {str(self.organization.id), str(org3.id)})
# Delete second organization and verify list
response = self.client.delete(f"/api/organizations/{org3.id}")
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
response = self.client.get("/api/organizations/")
self.assertEqual(len(response.json()["results"]), 1)
self.assertEqual(response.json()["results"][0]["id"], str(self.organization.id))
# Verify we can't delete the last organization
response = self.client.delete(f"/api/organizations/{self.organization.id}")
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
response = self.client.get("/api/organizations/")
self.assertEqual(
response.json(),
{
"type": "invalid_request",
"code": "not_found",
"detail": "You need to belong to an organization.",
"attr": None,
},
)
def create_organization(name: str) -> Organization:
"""

View File

@ -115,10 +115,10 @@ class TestOrganizationFeatureFlagCopy(APIBaseTest, QueryMatchingTest):
"ensure_experience_continuity": self.feature_flag_to_copy.ensure_experience_continuity,
"rollout_percentage": self.rollout_percentage_to_copy,
"deleted": False,
"created_by": self.user.id,
"id": "__ignore__",
"created_at": "__ignore__",
"usage_dashboard": "__ignore__",
"created_by": ANY,
"id": ANY,
"created_at": ANY,
"usage_dashboard": ANY,
"is_simple_flag": True,
"experiment_set": [],
"surveys": [],
@ -129,22 +129,13 @@ class TestOrganizationFeatureFlagCopy(APIBaseTest, QueryMatchingTest):
"analytics_dashboards": [],
"has_enriched_analytics": False,
"tags": [],
"user_access_level": "editor",
}
flag_response = response.json()["success"][0]
for key, expected_value in expected_flag_response.items():
self.assertIn(key, flag_response)
if expected_value != "__ignore__":
if key == "created_by":
self.assertEqual(flag_response[key]["id"], expected_value)
else:
self.assertEqual(flag_response[key], expected_value)
self.assertSetEqual(
set(expected_flag_response.keys()),
set(flag_response.keys()),
)
assert flag_response == expected_flag_response
assert flag_response["created_by"]["id"] == self.user.id
def test_copy_feature_flag_update_existing(self):
target_project = self.team_2
@ -201,43 +192,34 @@ class TestOrganizationFeatureFlagCopy(APIBaseTest, QueryMatchingTest):
"ensure_experience_continuity": self.feature_flag_to_copy.ensure_experience_continuity,
"rollout_percentage": self.rollout_percentage_to_copy,
"deleted": False,
"created_by": self.user.id,
"created_by": ANY,
"is_simple_flag": True,
"rollback_conditions": None,
"performed_rollback": False,
"can_edit": True,
"has_enriched_analytics": False,
"tags": [],
"id": "__ignore__",
"created_at": "__ignore__",
"usage_dashboard": "__ignore__",
"experiment_set": "__ignore__",
"surveys": "__ignore__",
"features": "__ignore__",
"analytics_dashboards": "__ignore__",
"id": ANY,
"created_at": ANY,
"usage_dashboard": ANY,
"experiment_set": ANY,
"surveys": ANY,
"features": ANY,
"analytics_dashboards": ANY,
"user_access_level": "editor",
}
flag_response = response.json()["success"][0]
for key, expected_value in expected_flag_response.items():
self.assertIn(key, flag_response)
if expected_value != "__ignore__":
if key == "created_by":
self.assertEqual(flag_response[key]["id"], expected_value)
else:
self.assertEqual(flag_response[key], expected_value)
assert flag_response == expected_flag_response
# Linked instances must remain linked
self.assertEqual(experiment.id, flag_response["experiment_set"][0])
self.assertEqual(str(survey.id), flag_response["surveys"][0]["id"])
self.assertEqual(str(feature.id), flag_response["features"][0]["id"])
self.assertEqual(analytics_dashboard.id, flag_response["analytics_dashboards"][0])
self.assertEqual(usage_dashboard.id, flag_response["usage_dashboard"])
self.assertSetEqual(
set(expected_flag_response.keys()),
set(flag_response.keys()),
)
assert flag_response["created_by"]["id"] == self.user.id
assert experiment.id == flag_response["experiment_set"][0]
assert str(survey.id) == flag_response["surveys"][0]["id"]
assert str(feature.id) == flag_response["features"][0]["id"]
assert analytics_dashboard.id == flag_response["analytics_dashboards"][0]
assert usage_dashboard.id == flag_response["usage_dashboard"]
def test_copy_feature_flag_with_old_legacy_flags(self):
url = f"/api/organizations/{self.organization.id}/feature_flags/copy_flags"
@ -331,42 +313,33 @@ class TestOrganizationFeatureFlagCopy(APIBaseTest, QueryMatchingTest):
"ensure_experience_continuity": self.feature_flag_to_copy.ensure_experience_continuity,
"rollout_percentage": self.rollout_percentage_to_copy,
"deleted": False,
"created_by": self.user.id,
"created_by": ANY,
"is_simple_flag": True,
"rollback_conditions": None,
"performed_rollback": False,
"can_edit": True,
"has_enriched_analytics": False,
"tags": [],
"id": "__ignore__",
"created_at": "__ignore__",
"usage_dashboard": "__ignore__",
"experiment_set": "__ignore__",
"surveys": "__ignore__",
"features": "__ignore__",
"analytics_dashboards": "__ignore__",
"id": ANY,
"created_at": ANY,
"usage_dashboard": ANY,
"experiment_set": ANY,
"surveys": ANY,
"features": ANY,
"analytics_dashboards": ANY,
"user_access_level": "editor",
}
flag_response = response.json()["success"][0]
for key, expected_value in expected_flag_response.items():
self.assertIn(key, flag_response)
if expected_value != "__ignore__":
if key == "created_by":
self.assertEqual(flag_response[key]["id"], expected_value)
else:
self.assertEqual(flag_response[key], expected_value)
assert flag_response == expected_flag_response
assert flag_response["created_by"]["id"] == self.user.id
# Linked instances must be overriden for a soft-deleted flag
# Linked instances must be overridden for a soft-deleted flag
self.assertEqual(flag_response["experiment_set"], [])
self.assertEqual(flag_response["surveys"], [])
self.assertNotEqual(flag_response["usage_dashboard"], existing_deleted_flag.usage_dashboard.id)
self.assertEqual(flag_response["analytics_dashboards"], [])
self.assertSetEqual(
set(expected_flag_response.keys()),
set(flag_response.keys()),
)
# target_project_2 should have failed
self.assertEqual(len(response.json()["failed"]), 1)
self.assertEqual(response.json()["failed"][0]["project_id"], target_project_2.id)

View File

@ -873,7 +873,7 @@ class TestPerson(ClickhouseTestMixin, APIBaseTest):
create_person(team_id=self.team.pk, version=0)
returned_ids = []
with self.assertNumQueries(8):
with self.assertNumQueries(10):
response = self.client.get("/api/person/?limit=10").json()
self.assertEqual(len(response["results"]), 9)
returned_ids += [x["distinct_ids"][0] for x in response["results"]]
@ -884,7 +884,7 @@ class TestPerson(ClickhouseTestMixin, APIBaseTest):
created_ids.reverse() # ids are returned in desc order
self.assertEqual(returned_ids, created_ids, returned_ids)
with self.assertNumQueries(6):
with self.assertNumQueries(8):
response_include_total = self.client.get("/api/person/?limit=10&include_total").json()
self.assertEqual(response_include_total["count"], 20) # With `include_total`, the total count is returned too

View File

@ -956,22 +956,22 @@ class TestPluginAPI(APIBaseTest, QueryMatchingTest):
@snapshot_postgres_queries
def test_listing_plugins_is_not_nplus1(self, _mock_get, _mock_reload) -> None:
with self.assertNumQueries(8):
with self.assertNumQueries(10):
self._assert_number_of_when_listed_plugins(0)
Plugin.objects.create(organization=self.organization)
with self.assertNumQueries(8):
with self.assertNumQueries(10):
self._assert_number_of_when_listed_plugins(1)
Plugin.objects.create(organization=self.organization)
with self.assertNumQueries(8):
with self.assertNumQueries(10):
self._assert_number_of_when_listed_plugins(2)
Plugin.objects.create(organization=self.organization)
with self.assertNumQueries(8):
with self.assertNumQueries(10):
self._assert_number_of_when_listed_plugins(3)
def _assert_number_of_when_listed_plugins(self, expected_plugins_count: int) -> None:

View File

@ -391,7 +391,7 @@ class TestSurvey(APIBaseTest):
format="json",
).json()
with self.assertNumQueries(16):
with self.assertNumQueries(20):
response = self.client.get(f"/api/projects/{self.team.id}/feature_flags")
self.assertEqual(response.status_code, status.HTTP_200_OK)
result = response.json()

View File

@ -5,7 +5,7 @@ from ipaddress import ip_address, ip_network
from typing import Any, Optional, cast
from collections.abc import Callable
from loginas.utils import is_impersonated_session, restore_original_login
from posthog.rbac.user_access_control import UserAccessControl
from django.shortcuts import redirect
import structlog
from corsheaders.middleware import CorsMiddleware
@ -274,6 +274,14 @@ class AutoProjectMiddleware:
def can_switch_to_team(self, new_team: Team, request: HttpRequest):
user = cast(User, request.user)
user_permissions = UserPermissions(user)
user_access_control = UserAccessControl(user=user, team=new_team)
# :KLUDGE: This is more inefficient than needed, doing several expensive lookups
# However this should be a rare operation!
if not user_access_control.check_access_level_for_object(new_team, "member"):
# Do something to indicate that they don't have access to the team...
return False
# :KLUDGE: This is more inefficient than needed, doing several expensive lookups
# However this should be a rare operation!
if user_permissions.team(new_team).effective_membership_level is None:

View File

@ -1,15 +1,15 @@
from typing import Optional, cast
import time
from typing import cast
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.db.models import Model
from django.views import View
import posthoganalytics
from rest_framework.exceptions import NotFound, PermissionDenied
from rest_framework.permissions import SAFE_METHODS, BasePermission, IsAdminUser
from rest_framework.request import Request
from rest_framework.views import APIView
from rest_framework.viewsets import ViewSet
from posthog.auth import (
PersonalAPIKeyAuthentication,
@ -19,13 +19,14 @@ from posthog.auth import (
from posthog.cloud_utils import is_cloud
from posthog.exceptions import EnterpriseFeatureException
from posthog.models import Organization, OrganizationMembership, Team, User
from posthog.models.scopes import APIScopeObjectOrNotSupported
from posthog.models.scopes import APIScopeObject, APIScopeObjectOrNotSupported
from posthog.rbac.user_access_control import AccessControlLevel, UserAccessControl, ordered_access_levels
from posthog.utils import get_can_create_org
CREATE_METHODS = ["POST", "PUT"]
CREATE_ACTIONS = ["create", "update"]
def extract_organization(object: Model, view: View) -> Organization:
def extract_organization(object: Model, view: ViewSet) -> Organization:
# This is set as part of the TeamAndOrgViewSetMixin to allow models that are not directly related to an organization
organization_id_rewrite = getattr(view, "filter_rewrite_rules", {}).get("organization_id")
if organization_id_rewrite:
@ -101,10 +102,13 @@ class OrganizationMemberPermissions(BasePermission):
organization = get_organization_from_view(view)
# TODO: Optimize this - we can get it from view.user_access_control
return OrganizationMembership.objects.filter(user=cast(User, request.user), organization=organization).exists()
def has_object_permission(self, request: Request, view: View, object: Model) -> bool:
def has_object_permission(self, request: Request, view, object: Model) -> bool:
organization = extract_organization(object, view)
# TODO: Optimize this - we can get it from view.user_access_control
return OrganizationMembership.objects.filter(user=cast(User, request.user), organization=organization).exists()
@ -135,7 +139,7 @@ class OrganizationAdminWritePermissions(BasePermission):
return membership.level >= OrganizationMembership.Level.ADMIN
def has_object_permission(self, request: Request, view: View, object: Model) -> bool:
def has_object_permission(self, request: Request, view, object: Model) -> bool:
if request.method in SAFE_METHODS:
return True
@ -295,7 +299,53 @@ class TimeSensitiveActionPermission(BasePermission):
return True
class APIScopePermission(BasePermission):
class ScopeBasePermission(BasePermission):
"""
Base class for shared functionality between APIScopePermission and AccessControlPermission
"""
write_actions: list[str] = ["create", "update", "partial_update", "patch", "destroy"]
read_actions: list[str] = ["list", "retrieve"]
scope_object_read_actions: list[str] = []
scope_object_write_actions: list[str] = []
def _get_scope_object(self, request, view) -> APIScopeObjectOrNotSupported:
if not getattr(view, "scope_object", None):
raise ImproperlyConfigured("APIScopePermission requires the view to define the scope_object attribute.")
return view.scope_object
def _get_action(self, request, view) -> str:
# TRICKY: DRF doesn't have an action for non-detail level "patch" calls which we use sometimes
if not view.action:
if request.method == "PATCH" and not view.detail:
return "patch"
return view.action
def _get_required_scopes(self, request, view) -> Optional[list[str]]:
# If required_scopes is set on the view method then use that
# Otherwise use the scope_object and derive the required scope from the action
if getattr(view, "required_scopes", None):
return view.required_scopes
scope_object = self._get_scope_object(request, view)
if scope_object == "INTERNAL":
return None
action = self._get_action(request, view)
read_actions = getattr(view, "scope_object_read_actions", self.read_actions)
write_actions = getattr(view, "scope_object_write_actions", self.write_actions)
if action in write_actions:
return [f"{scope_object}:write"]
elif action in read_actions or request.method == "OPTIONS":
return [f"{scope_object}:read"]
return None
class APIScopePermission(ScopeBasePermission):
"""
The request is via an API key and the user has the appropriate scopes.
@ -306,23 +356,10 @@ class APIScopePermission(BasePermission):
"""
write_actions: list[str] = ["create", "update", "partial_update", "patch", "destroy"]
read_actions: list[str] = ["list", "retrieve"]
scope_object_read_actions: list[str] = []
scope_object_write_actions: list[str] = []
def _get_action(self, request, view) -> str:
# TRICKY: DRF doesn't have an action for non-detail level "patch" calls which we use sometimes
if not view.action:
if request.method == "PATCH" and not view.detail:
return "patch"
return view.action
def has_permission(self, request, view) -> bool:
# NOTE: We do this first to error out quickly if the view is missing the required attribute
# Helps devs remember to add it.
self.get_scope_object(request, view)
self._get_scope_object(request, view)
# API Scopes currently only apply to PersonalAPIKeyAuthentication
if not isinstance(request.successful_authenticator, PersonalAPIKeyAuthentication):
@ -334,7 +371,12 @@ class APIScopePermission(BasePermission):
if not key_scopes:
return True
required_scopes = self.get_required_scopes(request, view)
required_scopes = self._get_required_scopes(request, view)
if not required_scopes:
self.message = f"This action does not support Personal API Key access"
return False
self.check_team_and_org_permissions(request, view)
if "*" in key_scopes:
@ -354,7 +396,7 @@ class APIScopePermission(BasePermission):
return True
def check_team_and_org_permissions(self, request, view) -> None:
scope_object = self.get_scope_object(request, view)
scope_object = self._get_scope_object(request, view)
if scope_object == "user":
return # The /api/users/@me/ endpoint is exempt from team and org scoping
@ -380,35 +422,104 @@ class APIScopePermission(BasePermission):
# Indicates this is not an organization scoped view
pass
def get_required_scopes(self, request, view) -> list[str]:
# If required_scopes is set on the view method then use that
# Otherwise use the scope_object and derive the required scope from the action
if getattr(view, "required_scopes", None):
return view.required_scopes
scope_object = self.get_scope_object(request, view)
class AccessControlPermission(ScopeBasePermission):
"""
Unified permissions access - controls access to any object based on the user's access controls
"""
if scope_object == "INTERNAL":
raise PermissionDenied(f"This action does not support Personal API Key access")
def _get_user_access_control(self, request, view) -> UserAccessControl:
return view.user_access_control
action = self._get_action(request, view)
read_actions = getattr(view, "scope_object_read_actions", self.read_actions)
write_actions = getattr(view, "scope_object_write_actions", self.write_actions)
def _get_required_access_level(self, request, view) -> Optional[AccessControlLevel]:
resource = self._get_scope_object(request, view)
required_scopes = self._get_required_scopes(request, view)
if action in write_actions:
return [f"{scope_object}:write"]
elif action in read_actions or request.method == "OPTIONS":
return [f"{scope_object}:read"]
if resource == "INTERNAL":
return None
# If we get here this typically means an action was called without a required scope
# It is essentially "INTERNAL"
raise PermissionDenied(f"This action does not support Personal API Key access")
READ_LEVEL = ordered_access_levels(resource)[-2]
WRITE_LEVEL = ordered_access_levels(resource)[-1]
def get_scope_object(self, request, view) -> APIScopeObjectOrNotSupported:
if not getattr(view, "scope_object", None):
raise ImproperlyConfigured("APIScopePermission requires the view to define the scope_object attribute.")
if not required_scopes:
return READ_LEVEL if request.method in SAFE_METHODS else WRITE_LEVEL
return view.scope_object
# TODO: This is definitely not right - we need to more safely map the scopes to access levels relevant to the object
for scope in required_scopes:
if scope.endswith(":write"):
return WRITE_LEVEL
return READ_LEVEL
def has_object_permission(self, request, view, object) -> bool:
# At this level we are checking an individual resource - this could be a project or a lower level item like a Dashboard
# NOTE: If the object is a Team then we shortcircuit here and create a UAC
# Reason being that there is a loop from view.user_access_control -> view.team -> view.user_access_control
if isinstance(object, Team):
uac = UserAccessControl(user=request.user, team=object)
else:
uac = self._get_user_access_control(request, view)
if not uac:
# If the view doesn't have a user_access_control then it is not supported by this permission scheme
return True
required_level = self._get_required_access_level(request, view)
if not required_level:
return True
has_access = uac.check_access_level_for_object(object, required_level=required_level)
if not has_access:
self.message = f"You do not have {required_level} access to this resource."
return False
return True
def has_permission(self, request, view) -> bool:
# At this level we are checking that the user can generically access the resource kind.
# Primarily we are checking the user's access to the parent resource type (i.e. project, organization)
# as well as enforcing any global restrictions (e.g. generically only editing of a flag is allowed)
uac = self._get_user_access_control(request, view)
scope_object = self._get_scope_object(request, view)
required_level = self._get_required_access_level(request, view)
team: Team
try:
team = view.team
except (ValueError, KeyError):
# TODO: Change this to a super specific exception...
# TODO: Does this means its okay because there is no team level thing?
return True
# NOTE: This isn't perfect as it will only optimize for endpoints where the pk matches the obj.id
# We can't load the actual object as get_object in turn calls the permissions check
pk = view.kwargs.get("pk")
uac.preload_access_levels(team=team, resource=cast(APIScopeObject, scope_object), resource_id=pk)
is_member = uac.check_access_level_for_object(team, required_level="member")
if not is_member:
self.message = f"You don't have access to the project."
return False
# If the API doesn't have a scope object or a required level for accessing then we can simply allow access
# as it isn't under access control
if scope_object == "INTERNAL" or not required_level:
return True
# TODO: Scope object should probably be applied against the `required_scopes` attribute
has_access = uac.check_access_level_for_resource(scope_object, required_level=required_level)
if not has_access:
self.message = f"You do not have {required_level} access to this resource."
return False
return True
class PostHogFeatureFlagPermission(BasePermission):

View File

@ -0,0 +1,13 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ee.api.rbac.access_control import AccessControlViewSetMixin
else:
try:
from ee.api.rbac.access_control import AccessControlViewSetMixin
except ImportError:
class AccessControlViewSetMixin:
pass

View File

@ -0,0 +1,559 @@
import pytest
from posthog.constants import AvailableFeature
from posthog.models.dashboard import Dashboard
from posthog.models.organization import OrganizationMembership
from posthog.models.team.team import Team
from posthog.models.user import User
from posthog.rbac.user_access_control import UserAccessControl
from posthog.test.base import BaseTest
try:
from ee.models.rbac.access_control import AccessControl
from ee.models.rbac.role import Role, RoleMembership
except ImportError:
pass
class BaseUserAccessControlTest(BaseTest):
user_access_control: UserAccessControl
def _create_access_control(
self, resource="project", resource_id=None, access_level="admin", organization_member=None, team=None, role=None
):
ac, _ = AccessControl.objects.get_or_create(
team=self.team,
resource=resource,
resource_id=resource_id or self.team.id,
organization_member=organization_member,
role=role,
)
ac.access_level = access_level
ac.save()
return ac
def setUp(self):
super().setUp()
self.organization.available_product_features = [
{
"key": AvailableFeature.PROJECT_BASED_PERMISSIONING,
"name": AvailableFeature.PROJECT_BASED_PERMISSIONING,
},
{
"key": AvailableFeature.ROLE_BASED_ACCESS,
"name": AvailableFeature.ROLE_BASED_ACCESS,
},
]
self.organization.save()
self.role_a = Role.objects.create(name="Engineers", organization=self.organization)
self.role_b = Role.objects.create(name="Administrators", organization=self.organization)
RoleMembership.objects.create(user=self.user, role=self.role_a)
self.user_access_control = UserAccessControl(self.user, self.team)
self.other_user = User.objects.create_and_join(self.organization, "other@posthog.com", "testtest")
RoleMembership.objects.create(user=self.other_user, role=self.role_b)
self.other_user_access_control = UserAccessControl(self.other_user, self.team)
self.user_with_no_role = User.objects.create_and_join(self.organization, "norole@posthog.com", "testtest")
self.user_with_no_role_access_control = UserAccessControl(self.user_with_no_role, self.team)
def _clear_uac_caches(self):
self.user_access_control._clear_cache()
self.other_user_access_control._clear_cache()
self.user_with_no_role_access_control._clear_cache()
@pytest.mark.ee
class TestUserAccessControl(BaseUserAccessControlTest):
def test_no_organization_id_passed(self):
# Create a user without an organization
user_without_org = User.objects.create(email="no-org@posthog.com", password="testtest")
user_access_control = UserAccessControl(user_without_org)
assert user_access_control._organization_membership is None
assert user_access_control._organization is None
assert user_access_control._user_role_ids == []
def test_without_available_product_features(self):
self.organization.available_product_features = []
self.organization.save()
self.organization_membership.level = OrganizationMembership.Level.ADMIN
self.organization_membership.save()
assert self.user_access_control.access_level_for_object(self.team) == "admin"
assert self.user_access_control.check_access_level_for_object(self.team, "admin") is True
assert self.other_user_access_control.access_level_for_object(self.team) == "admin"
assert self.other_user_access_control.check_access_level_for_object(self.team, "admin") is True
assert self.user_access_control.access_level_for_resource("project") == "admin"
assert self.other_user_access_control.access_level_for_resource("project") == "admin"
assert self.user_access_control.check_can_modify_access_levels_for_object(self.team) is True
assert self.other_user_access_control.check_can_modify_access_levels_for_object(self.team) is False
def test_ac_object_default_response(self):
self.organization_membership.level = OrganizationMembership.Level.ADMIN
self.organization_membership.save()
assert self.user_access_control.access_level_for_object(self.team) == "admin"
assert self.user_access_control.check_access_level_for_object(self.team, "admin") is True
assert self.other_user_access_control.access_level_for_object(self.team) == "admin"
assert self.other_user_access_control.check_access_level_for_object(self.team, "admin") is True
assert self.user_access_control.access_level_for_resource("project") == "admin"
assert self.other_user_access_control.access_level_for_resource("project") == "admin"
assert self.user_access_control.check_can_modify_access_levels_for_object(self.team) is True
assert self.other_user_access_control.check_can_modify_access_levels_for_object(self.team) is False
def test_ac_object_user_access_control(self):
# Setup member access by default
self._create_access_control(resource_id=self.team.id, access_level="member")
ac = self._create_access_control(
resource="project",
resource_id=str(self.team.id),
access_level="admin",
# context
organization_member=self.organization_membership,
)
assert self.user_access_control.access_level_for_object(self.team) == "admin"
assert self.user_access_control.check_access_level_for_object(self.team, "admin") is True
assert self.other_user_access_control.check_access_level_for_object(self.team, "admin") is False
ac.access_level = "member"
ac.save()
self._clear_uac_caches()
assert self.user_access_control.check_access_level_for_object(self.team, "admin") is False
assert self.user_access_control.check_access_level_for_object(self.team, "member") is True
assert (
self.other_user_access_control.check_access_level_for_object(self.team, "member")
is True # This is the default
) # Fix this - need to load all access controls...
def test_ac_object_project_access_control(self):
# Setup no access by default
ac = self._create_access_control(resource_id=self.team.id, access_level="none")
assert self.user_access_control.access_level_for_object(self.team) == "none"
assert self.user_access_control.check_access_level_for_object(self.team, "admin") is False
assert self.other_user_access_control.check_access_level_for_object(self.team, "admin") is False
ac.access_level = "member"
ac.save()
self._clear_uac_caches()
assert self.user_access_control.check_access_level_for_object(self.team, "admin") is False
assert self.user_access_control.check_access_level_for_object(self.team, "member") is True
assert self.other_user_access_control.check_access_level_for_object(self.team, "admin") is False
assert self.other_user_access_control.check_access_level_for_object(self.team, "member") is True
ac.access_level = "admin"
ac.save()
self._clear_uac_caches()
assert self.user_access_control.check_access_level_for_object(self.team, "admin") is True
assert self.other_user_access_control.check_access_level_for_object(self.team, "admin") is True
def test_ac_object_role_access_control(self):
# Setup member access by default
self._create_access_control(resource_id=self.team.id, access_level="member")
ac = self._create_access_control(resource_id=self.team.id, access_level="admin", role=self.role_a)
assert self.user_access_control.access_level_for_object(self.team) == "admin"
assert self.user_access_control.check_access_level_for_object(self.team, "admin") is True
assert self.other_user_access_control.check_access_level_for_object(self.team, "admin") is False
assert self.user_with_no_role_access_control.check_access_level_for_object(self.team, "admin") is False
ac.access_level = "member"
ac.save()
self._clear_uac_caches()
# Make the default access level none
self._create_access_control(resource_id=self.team.id, access_level="none")
assert self.user_access_control.check_access_level_for_object(self.team, "admin") is False
assert self.user_access_control.check_access_level_for_object(self.team, "member") is True
assert self.other_user_access_control.check_access_level_for_object(self.team, "admin") is False
assert self.other_user_access_control.check_access_level_for_object(self.team, "member") is False
assert self.user_with_no_role_access_control.check_access_level_for_object(self.team, "admin") is False
def test_ac_object_mixed_access_controls(self):
# No access by default
ac_project = self._create_access_control(resource_id=self.team.id, access_level="none")
# Enroll self.user as member
ac_user = self._create_access_control(
resource_id=self.team.id, access_level="member", organization_member=self.organization_membership
)
# Enroll role_a as admin
ac_role = self._create_access_control(
resource_id=self.team.id, access_level="admin", role=self.role_a
) # The highest AC
# Enroll role_b as member
ac_role_2 = self._create_access_control(resource_id=self.team.id, access_level="member", role=self.role_b)
# Enroll self.user in both roles
RoleMembership.objects.create(user=self.user, role=self.role_b)
# Create an unrelated access control for self.user
self._create_access_control(
resource_id="something else", access_level="admin", organization_member=self.organization_membership
)
matching_acs = self.user_access_control._get_access_controls(
self.user_access_control._access_controls_filters_for_object("project", str(self.team.id))
)
assert len(matching_acs) == 4
assert ac_project in matching_acs
assert ac_user in matching_acs
assert ac_role in matching_acs
assert ac_role_2 in matching_acs
# the matching one should be the highest level
assert self.user_access_control.access_level_for_object(self.team) == "admin"
def test_org_admin_always_has_access(self):
self._create_access_control(resource_id=self.team.id, access_level="none")
assert self.other_user_access_control.check_access_level_for_object(self.team, "member") is False
assert self.other_user_access_control.check_access_level_for_object(self.team, "admin") is False
self.organization_membership.level = OrganizationMembership.Level.ADMIN
self.organization_membership.save()
assert self.user_access_control.check_access_level_for_object(self.team, "member") is True
assert self.user_access_control.check_access_level_for_object(self.team, "admin") is True
def test_leaving_the_org_revokes_access(self):
self.user.leave(organization=self.organization)
assert self.user_access_control.check_access_level_for_object(self.team, "member") is False
def test_filters_project_queryset_based_on_acs(self):
team2 = Team.objects.create(organization=self.organization)
team3 = Team.objects.create(organization=self.organization)
# No default access
self._create_access_control(resource="project", resource_id=team2.id, access_level="none")
# No default access
self._create_access_control(resource="project", resource_id=team3.id, access_level="none")
# This user access
self._create_access_control(
resource="project",
resource_id=team3.id,
access_level="member",
organization_member=self.organization_membership,
)
# NOTE: This is different to the API queries as the TeamAndOrgViewsetMixing takes care of filtering out based on the parent org
filtered_teams = list(self.user_access_control.filter_queryset_by_access_level(Team.objects.all()))
assert filtered_teams == [self.team, team3]
other_user_filtered_teams = list(
self.other_user_access_control.filter_queryset_by_access_level(Team.objects.all())
)
assert other_user_filtered_teams == [self.team]
def test_filters_project_queryset_based_on_acs_always_allows_org_admin(self):
team2 = Team.objects.create(organization=self.organization)
team3 = Team.objects.create(organization=self.organization)
# No default access
self._create_access_control(resource="project", resource_id=team2.id, access_level="none")
self._create_access_control(resource="project", resource_id=team3.id, access_level="none")
self.organization_membership.level = OrganizationMembership.Level.ADMIN
self.organization_membership.save()
filtered_teams = list(
self.user_access_control.filter_queryset_by_access_level(Team.objects.all(), include_all_if_admin=True)
)
assert filtered_teams == [self.team, team2, team3]
def test_organization_access_control(self):
# A team isn't always available like for organization level routing
self.organization_membership.level = OrganizationMembership.Level.MEMBER
self.organization_membership.save()
uac = UserAccessControl(user=self.user, organization_id=self.organization.id)
assert uac.check_access_level_for_object(self.organization, "member") is True
assert uac.check_access_level_for_object(self.organization, "admin") is False
self.organization_membership.level = OrganizationMembership.Level.ADMIN
self.organization_membership.save()
uac = UserAccessControl(user=self.user, organization_id=self.organization.id)
assert uac.check_access_level_for_object(self.organization, "admin") is True
class TestUserAccessControlResourceSpecific(BaseUserAccessControlTest):
"""
Most things are identical between "project"s and other resources, but there are some differences particularly in level names
"""
def setUp(self):
super().setUp()
self.dashboard = Dashboard.objects.create(team=self.team)
def test_without_available_product_features(self):
self.organization.available_product_features = []
self.organization.save()
self.organization_membership.level = OrganizationMembership.Level.ADMIN
self.organization_membership.save()
assert self.user_access_control.access_level_for_object(self.dashboard) == "editor"
assert self.other_user_access_control.access_level_for_object(self.dashboard) == "editor"
assert self.user_access_control.access_level_for_resource("dashboard") == "editor"
assert self.other_user_access_control.access_level_for_resource("dashboard") == "editor"
def test_ac_object_default_response(self):
assert self.user_access_control.access_level_for_object(self.dashboard) == "editor"
assert self.other_user_access_control.access_level_for_object(self.dashboard) == "editor"
# class TestUserDashboardPermissions(BaseTest, WithPermissionsBase):
# def setUp(self):
# super().setUp()
# self.organization.available_product_features = [
# {"key": AvailableFeature.ADVANCED_PERMISSIONS, "name": AvailableFeature.ADVANCED_PERMISSIONS},
# ]
# self.organization.save()
# self.dashboard = Dashboard.objects.create(team=self.team)
# def dashboard_permissions(self):
# return self.permissions().dashboard(self.dashboard)
# def test_dashboard_effective_restriction_level(self):
# assert (
# self.dashboard_permissions().effective_restriction_level
# == Dashboard.RestrictionLevel.EVERYONE_IN_PROJECT_CAN_EDIT
# )
# def test_dashboard_effective_restriction_level_explicit(self):
# self.dashboard.restriction_level = Dashboard.RestrictionLevel.ONLY_COLLABORATORS_CAN_EDIT
# self.dashboard.save()
# assert (
# self.dashboard_permissions().effective_restriction_level
# == Dashboard.RestrictionLevel.ONLY_COLLABORATORS_CAN_EDIT
# )
# def test_dashboard_effective_restriction_level_when_feature_not_available(self):
# self.organization.available_product_features = []
# self.organization.save()
# self.dashboard.restriction_level = Dashboard.RestrictionLevel.ONLY_COLLABORATORS_CAN_EDIT
# self.dashboard.save()
# assert (
# self.dashboard_permissions().effective_restriction_level
# == Dashboard.RestrictionLevel.EVERYONE_IN_PROJECT_CAN_EDIT
# )
# def test_dashboard_can_restrict(self):
# assert not self.dashboard_permissions().can_restrict
# def test_dashboard_can_restrict_as_admin(self):
# self.organization_membership.level = OrganizationMembership.Level.ADMIN
# self.organization_membership.save()
# assert self.dashboard_permissions().can_restrict
# def test_dashboard_can_restrict_as_creator(self):
# self.dashboard.created_by = self.user
# self.dashboard.save()
# assert self.dashboard_permissions().can_restrict
# def test_dashboard_effective_privilege_level_when_everyone_can_edit(self):
# self.dashboard.restriction_level = Dashboard.RestrictionLevel.EVERYONE_IN_PROJECT_CAN_EDIT
# self.dashboard.save()
# assert self.dashboard_permissions().effective_privilege_level == Dashboard.PrivilegeLevel.CAN_EDIT
# def test_dashboard_effective_privilege_level_when_collaborators_can_edit(self):
# self.dashboard.restriction_level = Dashboard.RestrictionLevel.ONLY_COLLABORATORS_CAN_EDIT
# self.dashboard.save()
# assert self.dashboard_permissions().effective_privilege_level == Dashboard.PrivilegeLevel.CAN_VIEW
# def test_dashboard_effective_privilege_level_priviledged(self):
# self.dashboard.restriction_level = Dashboard.RestrictionLevel.ONLY_COLLABORATORS_CAN_EDIT
# self.dashboard.save()
# DashboardPrivilege.objects.create(
# user=self.user,
# dashboard=self.dashboard,
# level=Dashboard.PrivilegeLevel.CAN_EDIT,
# )
# assert self.dashboard_permissions().effective_privilege_level == Dashboard.PrivilegeLevel.CAN_EDIT
# def test_dashboard_effective_privilege_level_creator(self):
# self.dashboard.restriction_level = Dashboard.RestrictionLevel.ONLY_COLLABORATORS_CAN_EDIT
# self.dashboard.save()
# self.dashboard.created_by = self.user
# self.dashboard.save()
# assert self.dashboard_permissions().effective_privilege_level == Dashboard.PrivilegeLevel.CAN_EDIT
# def test_dashboard_can_edit_when_everyone_can(self):
# self.dashboard.restriction_level = Dashboard.RestrictionLevel.EVERYONE_IN_PROJECT_CAN_EDIT
# self.dashboard.save()
# assert self.dashboard_permissions().can_edit
# def test_dashboard_can_edit_not_collaborator(self):
# self.dashboard.restriction_level = Dashboard.RestrictionLevel.ONLY_COLLABORATORS_CAN_EDIT
# self.dashboard.save()
# assert not self.dashboard_permissions().can_edit
# def test_dashboard_can_edit_creator(self):
# self.dashboard.restriction_level = Dashboard.RestrictionLevel.ONLY_COLLABORATORS_CAN_EDIT
# self.dashboard.save()
# self.dashboard.created_by = self.user
# self.dashboard.save()
# assert self.dashboard_permissions().can_edit
# def test_dashboard_can_edit_priviledged(self):
# self.dashboard.restriction_level = Dashboard.RestrictionLevel.ONLY_COLLABORATORS_CAN_EDIT
# self.dashboard.save()
# DashboardPrivilege.objects.create(
# user=self.user,
# dashboard=self.dashboard,
# level=Dashboard.PrivilegeLevel.CAN_EDIT,
# )
# assert self.dashboard_permissions().can_edit
# class TestUserInsightPermissions(BaseTest, WithPermissionsBase):
# def setUp(self):
# super().setUp()
# self.organization.available_product_features = [
# {"key": AvailableFeature.ADVANCED_PERMISSIONS, "name": AvailableFeature.ADVANCED_PERMISSIONS},
# ]
# self.organization.save()
# self.dashboard1 = Dashboard.objects.create(
# team=self.team,
# restriction_level=Dashboard.RestrictionLevel.ONLY_COLLABORATORS_CAN_EDIT,
# )
# self.dashboard2 = Dashboard.objects.create(team=self.team)
# self.insight = Insight.objects.create(team=self.team)
# self.tile1 = DashboardTile.objects.create(dashboard=self.dashboard1, insight=self.insight)
# self.tile2 = DashboardTile.objects.create(dashboard=self.dashboard2, insight=self.insight)
# def insight_permissions(self):
# return self.permissions().insight(self.insight)
# def test_effective_restriction_level_limited(self):
# assert (
# self.insight_permissions().effective_restriction_level
# == Dashboard.RestrictionLevel.ONLY_COLLABORATORS_CAN_EDIT
# )
# def test_effective_restriction_level_all_allow(self):
# Dashboard.objects.all().update(restriction_level=Dashboard.RestrictionLevel.EVERYONE_IN_PROJECT_CAN_EDIT)
# assert (
# self.insight_permissions().effective_restriction_level
# == Dashboard.RestrictionLevel.EVERYONE_IN_PROJECT_CAN_EDIT
# )
# def test_effective_restriction_level_with_no_dashboards(self):
# DashboardTile.objects.all().delete()
# assert (
# self.insight_permissions().effective_restriction_level
# == Dashboard.RestrictionLevel.EVERYONE_IN_PROJECT_CAN_EDIT
# )
# def test_effective_restriction_level_with_no_permissioning(self):
# self.organization.available_product_features = []
# self.organization.save()
# assert (
# self.insight_permissions().effective_restriction_level
# == Dashboard.RestrictionLevel.EVERYONE_IN_PROJECT_CAN_EDIT
# )
# def test_effective_privilege_level_all_limited(self):
# Dashboard.objects.all().update(restriction_level=Dashboard.RestrictionLevel.ONLY_COLLABORATORS_CAN_EDIT)
# assert self.insight_permissions().effective_privilege_level == Dashboard.PrivilegeLevel.CAN_VIEW
# def test_effective_privilege_level_some_limited(self):
# assert self.insight_permissions().effective_privilege_level == Dashboard.PrivilegeLevel.CAN_EDIT
# def test_effective_privilege_level_all_limited_as_collaborator(self):
# Dashboard.objects.all().update(restriction_level=Dashboard.RestrictionLevel.ONLY_COLLABORATORS_CAN_EDIT)
# self.dashboard1.created_by = self.user
# self.dashboard1.save()
# assert self.insight_permissions().effective_privilege_level == Dashboard.PrivilegeLevel.CAN_EDIT
# def test_effective_privilege_level_with_no_dashboards(self):
# DashboardTile.objects.all().delete()
# assert self.insight_permissions().effective_privilege_level == Dashboard.PrivilegeLevel.CAN_EDIT
# class TestUserPermissionsEfficiency(BaseTest, WithPermissionsBase):
# def test_dashboard_efficiency(self):
# self.organization.available_product_features = [
# {"key": AvailableFeature.PROJECT_BASED_PERMISSIONING, "name": AvailableFeature.PROJECT_BASED_PERMISSIONING},
# {"key": AvailableFeature.ADVANCED_PERMISSIONS, "name": AvailableFeature.ADVANCED_PERMISSIONS},
# ]
# self.organization.save()
# dashboard = Dashboard.objects.create(
# team=self.team,
# restriction_level=Dashboard.RestrictionLevel.ONLY_COLLABORATORS_CAN_EDIT,
# )
# insights, tiles = [], []
# for _ in range(10):
# insight = Insight.objects.create(team=self.team)
# tile = DashboardTile.objects.create(dashboard=dashboard, insight=insight)
# insights.append(insight)
# tiles.append(tile)
# user_permissions = self.permissions()
# user_permissions.set_preloaded_dashboard_tiles(tiles)
# with self.assertNumQueries(3):
# assert user_permissions.current_team.effective_membership_level is not None
# assert user_permissions.dashboard(dashboard).effective_restriction_level is not None
# assert user_permissions.dashboard(dashboard).can_restrict is not None
# assert user_permissions.dashboard(dashboard).effective_privilege_level is not None
# assert user_permissions.dashboard(dashboard).can_edit is not None
# for insight in insights:
# assert user_permissions.insight(insight).effective_restriction_level is not None
# assert user_permissions.insight(insight).effective_privilege_level is not None
# def test_team_lookup_efficiency(self):
# user = User.objects.create(email="test2@posthog.com", distinct_id="test2")
# models = []
# for _ in range(10):
# organization, membership, team = Organization.objects.bootstrap(
# user=user, team_fields={"access_control": True}
# )
# membership.level = OrganizationMembership.Level.ADMIN # type: ignore
# membership.save() # type: ignore
# organization.available_product_features = [
# {"key": AvailableFeature.PROJECT_BASED_PERMISSIONING, "name": AvailableFeature.PROJECT_BASED_PERMISSIONING},
# ]
# organization.save()
# models.append((organization, membership, team))
# user_permissions = UserPermissions(user)
# with self.assertNumQueries(3):
# assert len(user_permissions.team_ids_visible_for_user) == 10
# for _, _, team in models:
# assert user_permissions.team(team).effective_membership_level == OrganizationMembership.Level.ADMIN

View File

@ -0,0 +1,489 @@
from functools import cached_property
import json
from django.contrib.auth.models import AnonymousUser
from django.db.models import Model, Q, QuerySet
from rest_framework import serializers
from typing import TYPE_CHECKING, Any, Literal, Optional, cast, get_args
from posthog.constants import AvailableFeature
from posthog.models import (
Organization,
OrganizationMembership,
Team,
User,
)
from posthog.models.scopes import APIScopeObject, API_SCOPE_OBJECTS
if TYPE_CHECKING:
from ee.models import AccessControl
_AccessControl = AccessControl
else:
_AccessControl = object
try:
from ee.models.rbac.access_control import AccessControl
except ImportError:
pass
AccessControlLevelNone = Literal["none"]
AccessControlLevelMember = Literal[AccessControlLevelNone, "member", "admin"]
AccessControlLevelResource = Literal[AccessControlLevelNone, "viewer", "editor"]
AccessControlLevel = Literal[AccessControlLevelMember, AccessControlLevelResource]
NO_ACCESS_LEVEL = "none"
ACCESS_CONTROL_LEVELS_MEMBER: tuple[AccessControlLevelMember, ...] = get_args(AccessControlLevelMember)
ACCESS_CONTROL_LEVELS_RESOURCE: tuple[AccessControlLevelResource, ...] = get_args(AccessControlLevelResource)
def ordered_access_levels(resource: APIScopeObject) -> list[AccessControlLevel]:
if resource in ["project", "organization"]:
return list(ACCESS_CONTROL_LEVELS_MEMBER)
return list(ACCESS_CONTROL_LEVELS_RESOURCE)
def default_access_level(resource: APIScopeObject) -> AccessControlLevel:
if resource in ["project"]:
return "admin"
if resource in ["organization"]:
return "member"
return "editor"
def highest_access_level(resource: APIScopeObject) -> AccessControlLevel:
return ordered_access_levels(resource)[-1]
def access_level_satisfied_for_resource(
resource: APIScopeObject, current_level: AccessControlLevel, required_level: AccessControlLevel
) -> bool:
return ordered_access_levels(resource).index(current_level) >= ordered_access_levels(resource).index(required_level)
def model_to_resource(model: Model) -> Optional[APIScopeObject]:
"""
Given a model, return the resource type it represents
"""
if hasattr(model, "_meta"):
name = model._meta.model_name
else:
name = model.__class__.__name__.lower()
# NOTE: These are special mappings where the 1-1 of APIScopeObject doesn't match
if name == "team":
return "project"
if name == "featureflag":
return "feature_flag"
if name == "plugin_config":
return "plugin"
if name not in API_SCOPE_OBJECTS:
return None
return cast(APIScopeObject, name)
class UserAccessControl:
"""
UserAccessControl provides functions for checking unified access to all resources and objects from a Project level downwards.
Typically a Team (Project) is required other than in certain circumstances, particularly when validating which projects a user has access to within an organization.
"""
def __init__(self, user: User, team: Optional[Team] = None, organization_id: Optional[str] = None):
self._user = user
self._team = team
self._cache: dict[str, list[AccessControl]] = {}
if not organization_id and team:
organization_id = str(team.organization_id)
self._organization_id = organization_id
def _clear_cache(self):
# Primarily intended for tests
self._cache = {}
@cached_property
def _organization_membership(self) -> Optional[OrganizationMembership]:
# NOTE: This is optimized to reduce queries - we get the users membership _with_ the organization
try:
if not self._organization_id:
return None
return OrganizationMembership.objects.select_related("organization").get(
organization_id=self._organization_id, user=self._user
)
except OrganizationMembership.DoesNotExist:
return None
@cached_property
def _organization(self) -> Optional[Organization]:
if self._organization_membership:
return self._organization_membership.organization
return None
@cached_property
def _user_role_ids(self):
if not self.rbac_supported:
# Early return to prevent an unnecessary lookup
return []
role_memberships = cast(Any, self._user).role_memberships.select_related("role").all()
return [membership.role.id for membership in role_memberships]
@property
def rbac_supported(self) -> bool:
if not self._organization:
return False
return self._organization.is_feature_available(AvailableFeature.ROLE_BASED_ACCESS)
@property
def access_controls_supported(self) -> bool:
# NOTE: This is a proxy feature. We may want to consider making it explicit later
# ADVANCED_PERMISSIONS was only for dashboard collaborators, PROJECT_BASED_PERMISSIONING for project permissions
# both now apply to this generic access control
if not self._organization:
return False
return self._organization.is_feature_available(
AvailableFeature.PROJECT_BASED_PERMISSIONING
) or self._organization.is_feature_available(AvailableFeature.ADVANCED_PERMISSIONS)
def _filter_options(self, filters: dict[str, Any]) -> Q:
"""
Adds the 3 main filter options to the query
"""
return (
Q( # Access controls applying to this team
**filters, organization_member=None, role=None
)
| Q( # Access controls applying to this user
**filters, organization_member__user=self._user, role=None
)
| Q( # Access controls applying to this user's roles
**filters, organization_member=None, role__in=self._user_role_ids
)
)
def _get_access_controls(self, filters: dict) -> list[_AccessControl]:
key = json.dumps(filters, sort_keys=True)
if key not in self._cache:
self._cache[key] = list(AccessControl.objects.filter(self._filter_options(filters)))
return self._cache[key]
def _access_controls_filters_for_object(self, resource: APIScopeObject, resource_id: str) -> dict:
"""
Used when checking an individual object - gets all access controls for the object and its type
"""
return {"team_id": self._team.id, "resource": resource, "resource_id": resource_id} # type: ignore
def _access_controls_filters_for_resource(self, resource: APIScopeObject) -> dict:
"""
Used when checking overall access to a resource
"""
return {"team_id": self._team.id, "resource": resource, "resource_id": None} # type: ignore
def _access_controls_filters_for_queryset(self, resource: APIScopeObject) -> dict:
"""
Used to filter out IDs from a queryset based on access controls where the specific resource is denied access
"""
common_filters: dict[str, Any] = {"resource": resource, "resource_id__isnull": False}
if self._team and resource != "project":
common_filters["team_id"] = self._team.id
else:
common_filters["team__organization_id"] = str(self._organization_id)
return common_filters
def _fill_filters_cache(self, filter_groups: list[dict], access_controls: list[_AccessControl]) -> None:
for filters in filter_groups:
key = json.dumps(filters, sort_keys=True)
# TRICKY: We have to simulate the entire DB query here:
matching_access_controls = []
for ac in access_controls:
matches = True
for key, value in filters.items():
if key == "resource_id__isnull":
if (ac.resource_id is None) != value:
matches = False
break
elif key == "team__organization_id":
if ac.team.organization_id != value:
matches = False
break
elif getattr(ac, key) != value:
matches = False
break
if matches:
matching_access_controls.append(ac)
self._cache[key] = matching_access_controls
def preload_object_access_controls(self, objects: list[Model]) -> None:
"""
Preload access controls for a list of objects
"""
filter_groups: list[dict] = []
for obj in objects:
resource = model_to_resource(obj)
if not resource:
return
filter_groups.append(self._access_controls_filters_for_object(resource, str(obj.id))) # type: ignore
q = Q()
for filters in filter_groups:
q = q | self._filter_options(filters)
access_controls = list(AccessControl.objects.filter(q))
self._fill_filters_cache(filter_groups, access_controls)
def preload_access_levels(self, team: Team, resource: APIScopeObject, resource_id: Optional[str] = None) -> None:
"""
Checking permissions can involve multiple queries to AccessControl e.g. project level, global resource level, and object level
As we can know this upfront, we can optimize this by loading all the controls we will need upfront.
"""
# Question - are we fundamentally loading every access control for the given resource? If so should we accept that fact and just load them all?
# doing all additional filtering in memory?
filter_groups: list[dict] = []
filter_groups.append(self._access_controls_filters_for_object(resource="project", resource_id=str(team.id)))
filter_groups.append(self._access_controls_filters_for_resource(resource))
if resource_id:
filter_groups.append(self._access_controls_filters_for_object(resource, resource_id=resource_id))
else:
filter_groups.append(self._access_controls_filters_for_queryset(resource))
q = Q()
for filters in filter_groups:
q = q | self._filter_options(filters)
access_controls = list(AccessControl.objects.filter(q))
self._fill_filters_cache(filter_groups, access_controls)
# Object level - checking conditions for specific items
def access_level_for_object(
self, obj: Model, resource: Optional[APIScopeObject] = None, explicit=False
) -> Optional[AccessControlLevel]:
"""
Access levels are strings - the order of which is determined at run time.
We find all relevant access controls and then return the highest value
"""
resource = resource or model_to_resource(obj)
org_membership = self._organization_membership
if not resource or not org_membership:
return None
# Creators always have highest access
if getattr(obj, "created_by", None) == self._user:
return highest_access_level(resource)
# Org admins always have highest access
if org_membership.level >= OrganizationMembership.Level.ADMIN:
return highest_access_level(resource)
if resource == "organization":
# Organization access is controlled via membership level only
if org_membership.level >= OrganizationMembership.Level.ADMIN:
return "admin"
return "member"
# If access controls aren't supported, then we return the default access level
if not self.access_controls_supported:
return default_access_level(resource) if not explicit else None
filters = self._access_controls_filters_for_object(resource, str(obj.id)) # type: ignore
access_controls = self._get_access_controls(filters)
# If there is no specified controls on the resource then we return the default access level
if not access_controls:
return default_access_level(resource) if not explicit else None
# If there are access controls we pick the highest level the user has
return max(
access_controls,
key=lambda access_control: ordered_access_levels(resource).index(access_control.access_level),
).access_level
def check_access_level_for_object(
self, obj: Model, required_level: AccessControlLevel, explicit=False
) -> Optional[bool]:
"""
Entry point for all permissions around a specific object.
If any of the access controls have the same or higher level than the requested level, return True.
Returns true or false if access controls are applied, otherwise None
"""
resource = model_to_resource(obj)
if not resource:
# Permissions do not apply to models without a related scope
return True
access_level = self.access_level_for_object(obj, resource, explicit=explicit)
if not access_level:
return False
# If no access control exists
return access_level_satisfied_for_resource(resource, access_level, required_level)
def check_can_modify_access_levels_for_object(self, obj: Model) -> Optional[bool]:
"""
Special case for checking if the user can modify the access levels for an object.
Unlike check_access_level_for_object, this requires that one of these conditions is true:
1. The user is the creator of the object
2. The user is explicitly a project admin
2. The user is an org admin
"""
if getattr(obj, "created_by", None) == self._user:
# TODO: Should this always be the case, even for projects?
return True
# If they aren't the creator then they need to be a project admin or org admin
# TRICKY: If self._team isn't set, this is likely called for a Team itself so we pass in the object
return self.check_access_level_for_object(self._team or obj, required_level="admin", explicit=True)
# Resource level - checking conditions for the resource type
def access_level_for_resource(self, resource: APIScopeObject) -> Optional[AccessControlLevel]:
"""
Access levels are strings - the order of which is determined at run time.
We find all relevant access controls and then return the highest value
"""
org_membership = self._organization_membership
if not resource or not org_membership:
# In any of these cases, we can't determine the access level
return None
# Org admins always have resource level access
if org_membership.level >= OrganizationMembership.Level.ADMIN:
return highest_access_level(resource)
if not self.access_controls_supported:
# If access controls aren't supported, then return the default access level
return default_access_level(resource)
filters = self._access_controls_filters_for_resource(resource)
access_controls = self._get_access_controls(filters)
if not access_controls:
return default_access_level(resource)
return max(
access_controls,
key=lambda access_control: ordered_access_levels(resource).index(access_control.access_level),
).access_level
def check_access_level_for_resource(self, resource: APIScopeObject, required_level: AccessControlLevel) -> bool:
access_level = self.access_level_for_resource(resource)
if not access_level:
return False
return access_level_satisfied_for_resource(resource, access_level, required_level)
def filter_queryset_by_access_level(self, queryset: QuerySet, include_all_if_admin=False) -> QuerySet:
# Find all items related to the queryset model that have access controls such that the effective level for the user is "none"
# and exclude them from the queryset
model = cast(Model, queryset.model)
resource = model_to_resource(model)
if not resource:
return queryset
if include_all_if_admin:
org_membership = self._organization_membership
if org_membership and org_membership.level >= OrganizationMembership.Level.ADMIN:
return queryset
model_has_creator = hasattr(model, "created_by")
filters = self._access_controls_filters_for_queryset(resource)
access_controls = self._get_access_controls(filters)
blocked_resource_ids: set[str] = set()
resource_id_access_levels: dict[str, list[str]] = {}
for access_control in access_controls:
resource_id_access_levels.setdefault(access_control.resource_id, []).append(access_control.access_level)
for resource_id, access_levels in resource_id_access_levels.items():
# Check if every access level is "none"
if all(access_level == NO_ACCESS_LEVEL for access_level in access_levels):
blocked_resource_ids.add(resource_id)
# Filter the queryset based on the access controls
if blocked_resource_ids:
# Filter out any IDs where the user is not the creator and the id is blocked
if model_has_creator:
queryset = queryset.exclude(Q(id__in=blocked_resource_ids) & ~Q(created_by=self._user))
else:
queryset = queryset.exclude(id__in=blocked_resource_ids)
return queryset
class UserAccessControlSerializerMixin(serializers.Serializer):
"""
Mixin for serializers to add user access control fields
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._preloaded_access_controls = False
user_access_level = serializers.SerializerMethodField(
read_only=True,
help_text="The effective access level the user has for this object",
)
@property
def user_access_control(self) -> Optional[UserAccessControl]:
# NOTE: The user_access_control is typically on the view but in specific cases such as the posthog_app_context it is set at the context level
if "user_access_control" in self.context:
# Get it directly from the context
return self.context["user_access_control"]
elif hasattr(self.context.get("view", None), "user_access_control"):
# Otherwise from the view (the default case)
return self.context["view"].user_access_control
else:
user = cast(User | AnonymousUser, self.context["request"].user)
# The user could be anonymous - if so there is no access control to be used
if user.is_anonymous:
return None
user = cast(User, user)
return UserAccessControl(user, organization_id=str(user.current_organization_id))
def get_user_access_level(self, obj: Model) -> Optional[str]:
if not self.user_access_control:
return None
# Check if self.instance is a list - if so we want to preload the user access controls
if not self._preloaded_access_controls and isinstance(self.instance, list):
self.user_access_control.preload_object_access_controls(self.instance)
self._preloaded_access_controls = True
return self.user_access_control.access_level_for_object(obj)

View File

@ -164,7 +164,7 @@ class TestAutoProjectMiddleware(APIBaseTest):
def test_project_switched_when_accessing_dashboard_of_another_accessible_team(self):
dashboard = Dashboard.objects.create(team=self.second_team)
with self.assertNumQueries(self.base_app_num_queries + 4): # AutoProjectMiddleware adds 4 queries
with self.assertNumQueries(self.base_app_num_queries + 7): # AutoProjectMiddleware adds 4 queries
response_app = self.client.get(f"/dashboard/{dashboard.id}")
response_users_api = self.client.get(f"/api/users/@me/")
response_users_api_data = response_users_api.json()
@ -212,7 +212,7 @@ class TestAutoProjectMiddleware(APIBaseTest):
@override_settings(PERSON_ON_EVENTS_V2_OVERRIDE=False)
def test_project_unchanged_when_accessing_dashboards_list(self):
with self.assertNumQueries(self.base_app_num_queries): # No AutoProjectMiddleware queries
with self.assertNumQueries(self.base_app_num_queries + 2): # No AutoProjectMiddleware queries
response_app = self.client.get(f"/dashboard")
response_users_api = self.client.get(f"/api/users/@me/")
response_users_api_data = response_users_api.json()
@ -282,7 +282,7 @@ class TestAutoProjectMiddleware(APIBaseTest):
def test_project_switched_when_accessing_feature_flag_of_another_accessible_team(self):
feature_flag = FeatureFlag.objects.create(team=self.second_team, created_by=self.user)
with self.assertNumQueries(self.base_app_num_queries + 4):
with self.assertNumQueries(self.base_app_num_queries + 7):
response_app = self.client.get(f"/feature_flags/{feature_flag.id}")
response_users_api = self.client.get(f"/api/users/@me/")
response_users_api_data = response_users_api.json()
@ -296,7 +296,7 @@ class TestAutoProjectMiddleware(APIBaseTest):
@override_settings(PERSON_ON_EVENTS_V2_OVERRIDE=False)
def test_project_unchanged_when_creating_feature_flag(self):
with self.assertNumQueries(self.base_app_num_queries):
with self.assertNumQueries(self.base_app_num_queries + 2):
response_app = self.client.get(f"/feature_flags/new")
response_users_api = self.client.get(f"/api/users/@me/")
response_users_api_data = response_users_api.json()

View File

@ -368,6 +368,7 @@ def render_template(
from posthog.api.project import ProjectSerializer
from posthog.api.user import UserSerializer
from posthog.user_permissions import UserPermissions
from posthog.rbac.user_access_control import UserAccessControl
from posthog.views import preflight_check
posthog_app_context = {
@ -390,9 +391,14 @@ def render_template(
elif request.user.pk:
user = cast("User", request.user)
user_permissions = UserPermissions(user=user, team=user.team)
user_access_control = UserAccessControl(user=user, team=user.team)
user_serialized = UserSerializer(
request.user,
context={"request": request, "user_permissions": user_permissions},
context={
"request": request,
"user_permissions": user_permissions,
"user_access_control": user_access_control,
},
many=False,
)
posthog_app_context["current_user"] = user_serialized.data
@ -400,7 +406,11 @@ def render_template(
if user.team:
team_serialized = TeamSerializer(
user.team,
context={"request": request, "user_permissions": user_permissions},
context={
"request": request,
"user_permissions": user_permissions,
"user_access_control": user_access_control,
},
many=False,
)
posthog_app_context["current_team"] = team_serialized.data

View File

@ -374,7 +374,7 @@ class TestExternalDataSource(APIBaseTest):
self._create_external_data_source()
self._create_external_data_source()
with self.assertNumQueries(17):
with self.assertNumQueries(19):
response = self.client.get(f"/api/projects/{self.team.pk}/external_data_sources/")
payload = response.json()

1090
rust/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -66,7 +66,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_derive = { version = "1.0" }
serde_json = { version = "1.0" }
serde_urlencoded = "0.7.1"
sqlx = { version = "0.7", features = [
sqlx = { version = "0.8.2", features = [
"chrono",
"json",
"migrate",

View File

@ -1,6 +1,5 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::postgres::{PgHasArrayType, PgTypeInfo};
use std::str::FromStr;
use uuid::Uuid;
@ -31,13 +30,6 @@ impl FromStr for JobState {
}
}
impl PgHasArrayType for JobState {
fn array_type_info() -> sqlx::postgres::PgTypeInfo {
// Postgres default naming convention for array types is "_typename"
PgTypeInfo::with_name("_JobState")
}
}
// The chunk of data needed to enqueue a job
#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)]
pub struct JobInit {

View File

@ -32,6 +32,7 @@ sqlx = { workspace = true }
serde_json = { workspace = true }
serde = { workspace = true }
sourcemap = "9.0.0"
symbolic = { version = "12.12.1", features = ["sourcemapcache"] }
reqwest = { workspace = true }
sha2 = "0.10.8"
aws-config = { workspace = true }

View File

@ -7,7 +7,7 @@ use tokio::sync::Mutex;
use tracing::info;
use crate::{
config::Config,
config::{init_global_state, Config},
error::UnhandledError,
frames::resolver::Resolver,
hack::kafka::{create_kafka_producer, KafkaContext, SingleTopicConsumer},
@ -33,6 +33,7 @@ pub struct AppContext {
impl AppContext {
pub async fn new(config: &Config) -> Result<Self, UnhandledError> {
init_global_state(config);
let health_registry = HealthRegistry::new("liveness");
let worker_liveness = health_registry
.register("worker".to_string(), Duration::from_secs(60))

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
use std::sync::Arc;
use std::{cmp::min, collections::HashMap, sync::Arc};
use cymbal::{
config::Config,
@ -15,34 +15,35 @@ use tokio::sync::Mutex;
/**
Input data gathered by running the following, then converting to json:
SELECT
symbol_set.ref as filename,
contents::json->>'mangled_name' as "function",
(contents::json->>'in_app')::boolean as in_app,
CASE
WHEN contents::json->>'line' IS NOT NULL
THEN (contents::json->>'line')::int
END as lineno,
CASE
WHEN contents::json->>'column' IS NOT NULL
THEN (contents::json->>'column')::int
END as colno
contents::json->>'junk_drawer' as junk_drawer
FROM posthog_errortrackingstackframe frame
LEFT JOIN posthog_errortrackingsymbolset symbol_set
ON frame.symbol_set_id = symbol_set.id
WHERE (contents::json->>'resolved_name') is null
WHERE (contents::json->>'resolved_name') is n
AND contents::json->>'lang' = 'javascript'
AND contents::json->>'junk_drawer' IS NOT NULL
AND symbol_set.storage_ptr IS NOT NULL;
This doesn't actually work - we don't have the original line and column number, and
so can't repeat the original resolution. I couldn't find a way to reverse that mapping
with sourcemaps, so instead I'm going to temporarily add the raw frame to the resolve
Frame.
*/
const NAMELESS_FRAMES_IN_RAW_FMT: &str = include_str!("./nameless_frames_in_raw_format.json");
const NAMELESS_FRAMES_IN_RAW_FMT: &str = include_str!("./no_resolved_name_raw_frames.json");
#[tokio::main]
async fn main() {
let start_at: usize = std::env::var("START_AT")
.unwrap_or("0".to_string())
.parse()
.expect("START_AT must be an integer");
let run_until: Option<usize> = std::env::var("RUN_UNTIL")
.ok()
.map(|s| s.parse().expect("RUN_UNTIL must be an integer"));
let early_exit = std::env::var("EARLY_EXIT").is_ok();
// I want a lot of line context while working on this
std::env::set_var("CONTEXT_LINE_COUNT", "1");
let config = Config::init_with_defaults().unwrap();
let provider = SourcemapProvider::new(&config);
let cache = Arc::new(Mutex::new(SymbolSetCache::new(1_000_000_000)));
let provider = Caching::new(provider, cache);
@ -55,32 +56,56 @@ async fn main() {
let frames: Vec<RawJSFrame> = frames
.into_iter()
.map(|f| {
let mut f = f;
let in_app = f["in_app"].as_str().unwrap() == "true";
f["in_app"] = Value::Bool(in_app);
let lineno: u32 = f["lineno"]
.as_str()
.unwrap()
.replace(",", "")
.parse()
.unwrap();
let colno: u32 = f["colno"]
.as_str()
.unwrap()
.replace(",", "")
.parse()
.unwrap();
f["lineno"] = Value::Number(lineno.into());
f["colno"] = Value::Number(colno.into());
serde_json::from_value(f).unwrap()
let junk: HashMap<String, Value> =
serde_json::from_str(f["junk_drawer"].as_str().unwrap()).unwrap();
serde_json::from_value(junk["raw_frame"].clone()).unwrap()
})
.collect();
for frame in frames {
let run_until = min(frames.len(), run_until.unwrap_or(frames.len()));
let mut failures = Vec::new();
let mut resolved = 0;
for (i, frame) in frames
.into_iter()
.enumerate()
.skip(start_at)
.take(run_until - start_at)
{
let res = frame.resolve(0, &catalog).await.unwrap();
if res.resolved_name.is_none() {
panic!("Frame name not resolved: {:?}", frame);
println!("-------------------");
println!("Resolving frame {}", i);
println!("Input frame: {:?}", frame);
println!("Resolved: {}", res);
println!("-------------------");
if res.resolved_name.is_some() {
resolved += 1;
} else if early_exit {
break;
} else {
failures.push((frame.clone(), res, i));
}
}
println!("Failures:");
for failure in failures {
println!("-------------------");
println!(
"Failed to resolve name for frame {}, {:?}",
failure.2, failure.0
);
println!(
"Failure: {}",
failure.1.resolve_failure.as_deref().unwrap_or("unknown")
)
}
println!(
"Resolved {} out of {} frames",
resolved,
run_until - start_at
);
}

View File

@ -1,7 +1,12 @@
use std::sync::atomic::{AtomicUsize, Ordering};
use envconfig::Envconfig;
use crate::hack::kafka::{ConsumerConfig, KafkaConfig};
// TODO - I'm just too lazy to pipe this all the way through the resolve call stack
pub static FRAME_CONTEXT_LINES: AtomicUsize = AtomicUsize::new(15);
#[derive(Envconfig, Clone)]
pub struct Config {
#[envconfig(from = "BIND_HOST", default = "::")]
@ -69,11 +74,21 @@ pub struct Config {
#[envconfig(default = "600")]
pub frame_cache_ttl_seconds: u64,
// Maximum number of lines of pre and post context to get per frame
#[envconfig(default = "15")]
pub context_line_count: usize,
}
impl Config {
pub fn init_with_defaults() -> Result<Self, envconfig::Error> {
ConsumerConfig::set_defaults("error-tracking-rs", "exception_symbolification_events");
Self::init_from_env()
let res = Self::init_from_env()?;
init_global_state(&res);
Ok(res)
}
}
pub fn init_global_state(config: &Config) {
FRAME_CONTEXT_LINES.store(config.context_line_count, Ordering::Relaxed);
}

View File

@ -48,6 +48,9 @@ pub enum JsResolveErr {
// We failed to parse a found source map
#[error("Invalid source map: {0}")]
InvalidSourceMap(String),
// We failed to parse a found source map cache
#[error("Invalid source map cache: {0}")]
InvalidSourceMapCache(String),
// We found and parsed the source map, but couldn't find our frames token in it
#[error("Token not found for frame: {0}:{1}:{2}")]
TokenNotFound(String, u32, u32),

View File

@ -73,6 +73,7 @@ mod test {
use httpmock::MockServer;
use mockall::predicate;
use sqlx::PgPool;
use symbolic::sourcemapcache::SourceMapCacheWriter;
use crate::{
config::Config,
@ -157,6 +158,18 @@ mod test {
test_stack.pop().unwrap()
}
fn get_sourcemapcache_bytes() -> Vec<u8> {
let mut result = Vec::new();
let writer = SourceMapCacheWriter::new(
core::str::from_utf8(MINIFIED).unwrap(),
core::str::from_utf8(MAP).unwrap(),
)
.unwrap();
writer.serialize(&mut result).unwrap();
result
}
fn expect_puts_and_gets(
config: &Config,
mut client: S3Client,
@ -168,7 +181,7 @@ mod test {
.with(
predicate::eq(config.object_storage_bucket.clone()),
predicate::str::starts_with(config.ss_prefix.clone()),
predicate::eq(Vec::from(MAP)),
predicate::always(), // We don't assert on what we store, because who cares
)
.returning(|_, _, _| Ok(()))
.times(puts);
@ -179,7 +192,7 @@ mod test {
predicate::eq(config.object_storage_bucket.clone()),
predicate::str::starts_with(config.ss_prefix.clone()),
)
.returning(|_, _| Ok(Vec::from(MAP)))
.returning(|_, _| Ok(get_sourcemapcache_bytes()))
.times(gets);
client

View File

@ -1,13 +1,16 @@
use std::sync::atomic::Ordering;
use reqwest::Url;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha512};
use sourcemap::{SourceMap, Token};
use symbolic::sourcemapcache::{ScopeLookupResult, SourceLocation, SourcePosition};
use crate::{
config::FRAME_CONTEXT_LINES,
error::{Error, FrameError, JsResolveErr, UnhandledError},
frames::{Context, ContextLine, Frame},
metric_consts::{FRAME_NOT_RESOLVED, FRAME_RESOLVED},
symbol_store::SymbolCatalog,
symbol_store::{sourcemap::OwnedSourceMapCache, SymbolCatalog},
};
// A minifed JS stack frame. Just the minimal information needed to lookup some
@ -35,7 +38,7 @@ pub struct FrameLocation {
impl RawJSFrame {
pub async fn resolve<C>(&self, team_id: i32, catalog: &C) -> Result<Frame, UnhandledError>
where
C: SymbolCatalog<Url, SourceMap>,
C: SymbolCatalog<Url, OwnedSourceMapCache>,
{
match self.resolve_impl(team_id, catalog).await {
Ok(frame) => Ok(frame),
@ -48,7 +51,7 @@ impl RawJSFrame {
async fn resolve_impl<C>(&self, team_id: i32, catalog: &C) -> Result<Frame, Error>
where
C: SymbolCatalog<Url, SourceMap>,
C: SymbolCatalog<Url, OwnedSourceMapCache>,
{
let url = self.source_url()?;
@ -57,7 +60,12 @@ impl RawJSFrame {
};
let sourcemap = catalog.lookup(team_id, url).await?;
let Some(token) = sourcemap.lookup_token(location.line, location.column) else {
let smc = sourcemap.get_smc();
// Note: javascript stack frame lines are 1-indexed, so we have to subtract 1
let Some(location) = smc.lookup(SourcePosition::new(location.line - 1, location.column))
else {
return Err(JsResolveErr::TokenNotFound(
self.fn_name.clone(),
location.line,
@ -66,7 +74,7 @@ impl RawJSFrame {
.into());
};
Ok(Frame::from((self, token)))
Ok(Frame::from((self, location)))
}
// JS frames can only handle JS resolution errors - errors at the network level
@ -136,19 +144,25 @@ impl RawJSFrame {
}
}
impl From<(&RawJSFrame, Token<'_>)> for Frame {
fn from(src: (&RawJSFrame, Token)) -> Self {
impl From<(&RawJSFrame, SourceLocation<'_>)> for Frame {
fn from(src: (&RawJSFrame, SourceLocation)) -> Self {
let (raw_frame, token) = src;
metrics::counter!(FRAME_RESOLVED, "lang" => "javascript").increment(1);
let resolved_name = match token.scope() {
ScopeLookupResult::NamedScope(name) => Some(name.to_string()),
ScopeLookupResult::AnonymousScope => Some("<anonymous>".to_string()),
ScopeLookupResult::Unknown => None,
};
let mut res = Self {
raw_id: String::new(), // We use placeholders here, as they're overriden at the RawFrame level
mangled_name: raw_frame.fn_name.clone(),
line: Some(token.get_src_line()),
column: Some(token.get_src_col()),
source: token.get_source().map(String::from),
line: Some(token.line()),
column: Some(token.column()),
source: token.file().and_then(|f| f.name()).map(|s| s.to_string()),
in_app: raw_frame.in_app,
resolved_name: token.get_name().map(String::from),
resolved_name,
lang: "javascript".to_string(),
resolved: true,
resolve_failure: None,
@ -236,35 +250,36 @@ fn add_raw_to_junk(frame: &mut Frame, raw: &RawJSFrame) {
frame.add_junk("raw_frame", raw.clone()).unwrap();
}
fn get_context(token: &Token) -> Option<Context> {
let sv = token.get_source_view()?;
fn get_context(token: &SourceLocation) -> Option<Context> {
let file = token.file()?;
let token_line_num = token.line();
let src = file.source()?;
let token_line_num = token.get_src_line();
let line_limit = FRAME_CONTEXT_LINES.load(Ordering::Relaxed);
get_context_lines(src, token_line_num as usize, line_limit)
}
let token_line = sv.get_line(token_line_num)?;
fn get_context_lines(src: &str, line: usize, context_len: usize) -> Option<Context> {
let start = line.saturating_sub(context_len).saturating_sub(1);
let mut before = Vec::new();
let mut i = token_line_num;
while before.len() < 5 && i > 0 {
i -= 1;
if let Some(line) = sv.get_line(i) {
before.push(ContextLine::new(i, line));
}
}
before.reverse();
let mut lines = src.lines().enumerate().skip(start);
let before = (&mut lines)
.take(line - start)
.map(|(number, line)| ContextLine::new(number as u32, line))
.collect();
let mut after = Vec::new();
let mut i = token_line_num;
while after.len() < 5 && i < sv.line_count() as u32 {
i += 1;
if let Some(line) = sv.get_line(i) {
after.push(ContextLine::new(i, line));
}
}
let line = lines
.next()
.map(|(number, line)| ContextLine::new(number as u32, line))?;
let after = lines
.take(context_len)
.map(|(number, line)| ContextLine::new(number as u32, line))
.collect();
Some(Context {
before,
line: ContextLine::new(token_line_num, token_line),
line,
after,
})
}

View File

@ -4,7 +4,6 @@ pub const STACK_PROCESSED: &str = "cymbal_stack_track_processed";
pub const BASIC_FETCHES: &str = "cymbal_basic_fetches";
pub const SOURCEMAP_HEADER_FOUND: &str = "cymbal_sourcemap_header_found";
pub const SOURCEMAP_BODY_REF_FOUND: &str = "cymbal_sourcemap_body_ref_found";
pub const SOURCE_REF_BODY_FETCHES: &str = "cymbal_source_ref_body_fetches";
pub const SOURCEMAP_NOT_FOUND: &str = "cymbal_sourcemap_not_found";
pub const SOURCEMAP_BODY_FETCHES: &str = "cymbal_sourcemap_body_fetches";
pub const STORE_CACHE_HITS: &str = "cymbal_store_cache_hits";

View File

@ -2,8 +2,8 @@ use std::sync::Arc;
use axum::async_trait;
use ::sourcemap::SourceMap;
use reqwest::Url;
use sourcemap::OwnedSourceMapCache;
use crate::error::Error;
@ -50,18 +50,18 @@ pub trait Provider: Send + Sync + 'static {
pub struct Catalog {
// "source map provider"
pub smp: Box<dyn Provider<Ref = Url, Set = SourceMap>>,
pub smp: Box<dyn Provider<Ref = Url, Set = OwnedSourceMapCache>>,
}
impl Catalog {
pub fn new(smp: impl Provider<Ref = Url, Set = SourceMap>) -> Self {
pub fn new(smp: impl Provider<Ref = Url, Set = OwnedSourceMapCache>) -> Self {
Self { smp: Box::new(smp) }
}
}
#[async_trait]
impl SymbolCatalog<Url, SourceMap> for Catalog {
async fn lookup(&self, team_id: i32, r: Url) -> Result<Arc<SourceMap>, Error> {
impl SymbolCatalog<Url, OwnedSourceMapCache> for Catalog {
async fn lookup(&self, team_id: i32, r: Url) -> Result<Arc<OwnedSourceMapCache>, Error> {
self.smp.lookup(team_id, r).await
}
}

View File

@ -269,6 +269,7 @@ mod test {
use mockall::predicate;
use reqwest::Url;
use sqlx::PgPool;
use symbolic::sourcemapcache::SourceMapCacheWriter;
use crate::{
config::Config,
@ -283,6 +284,18 @@ mod test {
const MINIFIED: &[u8] = include_bytes!("../../tests/static/chunk-PGUQKT6S.js");
const MAP: &[u8] = include_bytes!("../../tests/static/chunk-PGUQKT6S.js.map");
fn get_sourcemapcache_bytes() -> Vec<u8> {
let mut result = Vec::new();
let writer = SourceMapCacheWriter::new(
core::str::from_utf8(MINIFIED).unwrap(),
core::str::from_utf8(MAP).unwrap(),
)
.unwrap();
writer.serialize(&mut result).unwrap();
result
}
#[sqlx::test(migrations = "./tests/test_migrations")]
async fn test_successful_lookup(db: PgPool) {
let server = MockServer::start();
@ -310,7 +323,7 @@ mod test {
.with(
predicate::eq(config.object_storage_bucket.clone()),
predicate::str::starts_with(config.ss_prefix.clone()),
predicate::eq(Vec::from(MAP)),
predicate::always(), // We won't assert on the contents written
)
.returning(|_, _, _| Ok(()))
.once();
@ -321,7 +334,7 @@ mod test {
predicate::eq(config.object_storage_bucket.clone()),
predicate::str::starts_with(config.ss_prefix.clone()),
)
.returning(|_, _| Ok(Vec::from(MAP)));
.returning(|_, _| Ok(get_sourcemapcache_bytes()));
let smp = SourcemapProvider::new(&config);
let saving_smp = Saving::new(

View File

@ -2,7 +2,7 @@ use std::{sync::Arc, time::Duration};
use axum::async_trait;
use reqwest::Url;
use sourcemap::SourceMap;
use symbolic::sourcemapcache::{SourceMapCache, SourceMapCacheWriter};
use tracing::{info, warn};
use crate::{
@ -10,7 +10,7 @@ use crate::{
error::{Error, JsResolveErr},
metric_consts::{
SOURCEMAP_BODY_FETCHES, SOURCEMAP_BODY_REF_FOUND, SOURCEMAP_FETCH, SOURCEMAP_HEADER_FOUND,
SOURCEMAP_NOT_FOUND, SOURCEMAP_PARSE, SOURCE_REF_BODY_FETCHES,
SOURCEMAP_NOT_FOUND, SOURCEMAP_PARSE,
},
};
@ -20,6 +20,34 @@ pub struct SourcemapProvider {
pub client: reqwest::Client,
}
// Sigh. Later we can be smarter here to only do the parse once, but it involves
// `unsafe` for lifetime reasons. On the other hand, the parse is cheap, so maybe
// it doesn't matter?
#[derive(Debug)]
pub struct OwnedSourceMapCache {
data: Vec<u8>,
}
impl OwnedSourceMapCache {
pub fn new(data: Vec<u8>, r: impl ToString) -> Result<Self, JsResolveErr> {
// Pass-through parse once to assert we're given valid data, so the unwrap below
// is safe.
SourceMapCache::parse(&data).map_err(|e| {
JsResolveErr::InvalidSourceMapCache(format!(
"Got error {} for url {}",
e,
r.to_string()
))
})?;
Ok(Self { data })
}
pub fn get_smc(&self) -> SourceMapCache {
// UNWRAP - we've already parsed this data once, so we know it's valid
SourceMapCache::parse(&self.data).unwrap()
}
}
impl SourcemapProvider {
pub fn new(config: &Config) -> Self {
let timeout = Duration::from_secs(config.sourcemap_timeout_seconds);
@ -43,31 +71,44 @@ impl Fetcher for SourcemapProvider {
type Fetched = Vec<u8>;
async fn fetch(&self, _: i32, r: Url) -> Result<Vec<u8>, Error> {
let start = common_metrics::timing_guard(SOURCEMAP_FETCH, &[]);
let sourcemap_url = find_sourcemap_url(&self.client, r).await?;
let (sourcemap_url, minified_source) = find_sourcemap_url(&self.client, r).await?;
let start = start.label("found_url", "true");
let res = fetch_source_map(&self.client, sourcemap_url).await?;
let sourcemap = fetch_source_map(&self.client, sourcemap_url.clone()).await?;
// TOTAL GUESS at a reasonable capacity here, btw
let mut cache_bytes = Vec::with_capacity(minified_source.len() + sourcemap.len());
let writer = SourceMapCacheWriter::new(&minified_source, &sourcemap).map_err(|e| {
JsResolveErr::InvalidSourceMapCache(format!(
"Failed to construct sourcemap cache: {}, for sourcemap url {}",
e, sourcemap_url
))
})?;
// UNWRAP: writing into a vector always succeeds
writer.serialize(&mut cache_bytes).unwrap();
start.label("found_data", "true").fin();
Ok(res)
Ok(cache_bytes)
}
}
#[async_trait]
impl Parser for SourcemapProvider {
type Source = Vec<u8>;
type Set = SourceMap;
type Set = OwnedSourceMapCache;
async fn parse(&self, data: Vec<u8>) -> Result<Self::Set, Error> {
let start = common_metrics::timing_guard(SOURCEMAP_PARSE, &[]);
let sm = SourceMap::from_reader(data.as_slice()).map_err(JsResolveErr::from)?;
let sm = OwnedSourceMapCache::new(data, "parse")?;
start.label("success", "true").fin();
Ok(sm)
}
}
async fn find_sourcemap_url(client: &reqwest::Client, start: Url) -> Result<Url, Error> {
async fn find_sourcemap_url(client: &reqwest::Client, start: Url) -> Result<(Url, String), Error> {
info!("Fetching sourcemap from {}", start);
// If this request fails, we cannot resolve the frame, and do not hand this error to the frames
@ -83,7 +124,11 @@ async fn find_sourcemap_url(client: &reqwest::Client, start: Url) -> Result<Url,
let headers = res.headers();
let header_url = headers
.get("SourceMap")
.or_else(|| headers.get("X-SourceMap"));
.or_else(|| headers.get("X-SourceMap"))
.cloned();
// We always need the body
let body = res.text().await.map_err(JsResolveErr::from)?;
if let Some(header_url) = header_url {
info!("Found sourcemap header: {:?}", header_url);
@ -104,14 +149,11 @@ async fn find_sourcemap_url(client: &reqwest::Client, start: Url) -> Result<Url,
final_url.set_path(url);
final_url
};
return Ok(url);
return Ok((url, body));
}
// If we didn't find a header, we have to check the body
// Grab the body as text, and split it into lines
metrics::counter!(SOURCE_REF_BODY_FETCHES).increment(1);
let body = res.text().await.map_err(JsResolveErr::from)?;
let lines = body.lines().rev(); // Our needle tends to be at the bottom of the haystack
for line in lines {
if line.starts_with("//# sourceMappingURL=") {
@ -126,7 +168,7 @@ async fn find_sourcemap_url(client: &reqwest::Client, start: Url) -> Result<Url,
final_url.set_path(found);
final_url
};
return Ok(url);
return Ok((url, body));
}
}
@ -137,12 +179,12 @@ async fn find_sourcemap_url(client: &reqwest::Client, start: Url) -> Result<Url,
Err(JsResolveErr::NoSourcemap(final_url.to_string()).into())
}
async fn fetch_source_map(client: &reqwest::Client, url: Url) -> Result<Vec<u8>, Error> {
async fn fetch_source_map(client: &reqwest::Client, url: Url) -> Result<String, Error> {
metrics::counter!(SOURCEMAP_BODY_FETCHES).increment(1);
let res = client.get(url).send().await.map_err(JsResolveErr::from)?;
res.error_for_status_ref().map_err(JsResolveErr::from)?;
let bytes = res.bytes().await.map_err(JsResolveErr::from)?;
Ok(bytes.to_vec())
let sourcemap = res.text().await.map_err(JsResolveErr::from)?;
Ok(sourcemap)
}
#[cfg(test)]
@ -165,7 +207,7 @@ mod test {
let client = reqwest::Client::new();
let url = server.url("/static/chunk-PGUQKT6S.js").parse().unwrap();
let res = find_sourcemap_url(&client, url).await.unwrap();
let (res, _) = find_sourcemap_url(&client, url).await.unwrap();
// We're doing relative-URL resolution here, so we have to account for that
let expected = server.url("/static/chunk-PGUQKT6S.js.map").parse().unwrap();
@ -173,24 +215,6 @@ mod test {
mock.assert_hits(1);
}
#[tokio::test]
async fn fetch_source_map_test() {
// This ones maybe a little silly - we're almost just testing reqwest
let server = MockServer::start();
let mock = server.mock(|when, then| {
when.method("GET").path("/static/chunk-PGUQKT6S.js.map");
then.status(200).body(MAP);
});
let client = reqwest::Client::new();
let url = server.url("/static/chunk-PGUQKT6S.js.map").parse().unwrap();
let res = fetch_source_map(&client, url).await.unwrap();
assert_eq!(res, MAP);
mock.assert_hits(1);
}
#[tokio::test]
async fn full_follows_links_test() {
let server = MockServer::start();