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:
commit
80d42d0527
194
ee/api/rbac/access_control.py
Normal file
194
ee/api/rbac/access_control.py
Normal 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)
|
@ -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())
|
||||
|
598
ee/api/rbac/test/test_access_control.py
Normal file
598
ee/api/rbac/test/test_access_control.py
Normal 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
|
@ -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)
|
||||
'''
|
||||
# ---
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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 }
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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]
|
||||
|
@ -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]]:
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
"""
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
]
|
||||
|
@ -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
|
||||
'''
|
||||
# ---
|
||||
|
@ -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
|
||||
'''
|
||||
# ---
|
||||
|
@ -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".',
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
'''
|
||||
# ---
|
||||
|
@ -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
@ -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
|
||||
|
@ -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
@ -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
@ -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(
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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(
|
||||
|
@ -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",
|
||||
|
@ -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}",
|
||||
|
@ -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:
|
||||
"""
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
@ -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:
|
||||
|
@ -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):
|
||||
|
13
posthog/rbac/access_control_api_mixin.py
Normal file
13
posthog/rbac/access_control_api_mixin.py
Normal 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
|
559
posthog/rbac/test/test_user_access_control.py
Normal file
559
posthog/rbac/test/test_user_access_control.py
Normal 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
|
489
posthog/rbac/user_access_control.py
Normal file
489
posthog/rbac/user_access_control.py
Normal 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)
|
File diff suppressed because it is too large
Load Diff
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
1090
rust/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
@ -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 {
|
||||
|
@ -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 }
|
||||
|
@ -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
4163
rust/cymbal/src/bin/no_resolved_name_raw_frames.json
Normal file
4163
rust/cymbal/src/bin/no_resolved_name_raw_frames.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user