From 8d01d5ef546b136be94e9567b06c241af8c59da9 Mon Sep 17 00:00:00 2001 From: Zach Waterfield Date: Tue, 29 Oct 2024 17:17:27 -0400 Subject: [PATCH] feat: rbac initial set up (#25745) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- ee/api/feature_flag_role_access.py | 6 +- .../organization_resource_access.py | 4 +- ee/api/{ => rbac}/role.py | 4 +- ee/api/test/test_feature_flag.py | 4 +- ee/api/test/test_feature_flag_role_access.py | 4 +- .../test/test_organization_resource_access.py | 2 +- ee/api/test/test_role.py | 4 +- ee/api/test/test_role_membership.py | 2 +- ee/models/__init__.py | 2 +- .../organization_resource_access.py | 2 + ee/models/{ => rbac}/role.py | 2 +- ee/urls.py | 5 +- frontend/src/lib/constants.tsx | 1 + .../settings/user/personalAPIKeysLogic.tsx | 4 +- frontend/src/types.ts | 31 ++++++++++ posthog/api/personal_api_key.py | 3 +- posthog/api/project.py | 2 +- posthog/api/routing.py | 2 +- posthog/api/team.py | 2 +- .../api/test/__snapshots__/test_api_docs.ambr | 6 +- .../test/test_organization_feature_flag.py | 2 +- posthog/models/feature_flag/permissions.py | 2 +- posthog/models/personal_api_key.py | 55 +---------------- posthog/models/scopes.py | 60 +++++++++++++++++++ posthog/permissions.py | 2 +- 25 files changed, 128 insertions(+), 85 deletions(-) rename ee/api/{ => rbac}/organization_resource_access.py (92%) rename ee/api/{ => rbac}/role.py (97%) rename ee/models/{ => rbac}/organization_resource_access.py (95%) rename ee/models/{ => rbac}/role.py (95%) create mode 100644 posthog/models/scopes.py diff --git a/ee/api/feature_flag_role_access.py b/ee/api/feature_flag_role_access.py index 6d03c7a4f36..01aa98a05b9 100644 --- a/ee/api/feature_flag_role_access.py +++ b/ee/api/feature_flag_role_access.py @@ -1,10 +1,10 @@ from rest_framework import exceptions, mixins, serializers, viewsets from rest_framework.permissions import SAFE_METHODS, BasePermission -from ee.api.role import RoleSerializer +from ee.api.rbac.role import RoleSerializer from ee.models.feature_flag_role_access import FeatureFlagRoleAccess -from ee.models.organization_resource_access import OrganizationResourceAccess -from ee.models.role import Role +from ee.models.rbac.organization_resource_access import OrganizationResourceAccess +from ee.models.rbac.role import Role from posthog.api.feature_flag import FeatureFlagSerializer from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.models import FeatureFlag diff --git a/ee/api/organization_resource_access.py b/ee/api/rbac/organization_resource_access.py similarity index 92% rename from ee/api/organization_resource_access.py rename to ee/api/rbac/organization_resource_access.py index bf886566605..9722fc7b02e 100644 --- a/ee/api/organization_resource_access.py +++ b/ee/api/rbac/organization_resource_access.py @@ -1,7 +1,7 @@ from rest_framework import mixins, serializers, viewsets -from ee.api.role import RolePermissions -from ee.models.organization_resource_access import OrganizationResourceAccess +from ee.api.rbac.role import RolePermissions +from ee.models.rbac.organization_resource_access import OrganizationResourceAccess from posthog.api.routing import TeamAndOrgViewSetMixin diff --git a/ee/api/role.py b/ee/api/rbac/role.py similarity index 97% rename from ee/api/role.py rename to ee/api/rbac/role.py index 96041cd0109..ccf8acef1f1 100644 --- a/ee/api/role.py +++ b/ee/api/rbac/role.py @@ -5,8 +5,8 @@ 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.organization_resource_access import OrganizationResourceAccess -from ee.models.role import Role, RoleMembership +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 diff --git a/ee/api/test/test_feature_flag.py b/ee/api/test/test_feature_flag.py index e3dd5849d60..0bc7292f7a8 100644 --- a/ee/api/test/test_feature_flag.py +++ b/ee/api/test/test_feature_flag.py @@ -1,6 +1,6 @@ from ee.api.test.base import APILicensedTest -from ee.models.organization_resource_access import OrganizationResourceAccess -from ee.models.role import Role, RoleMembership +from ee.models.rbac.organization_resource_access import OrganizationResourceAccess +from ee.models.rbac.role import Role, RoleMembership from posthog.models.feature_flag import FeatureFlag from posthog.models.organization import OrganizationMembership diff --git a/ee/api/test/test_feature_flag_role_access.py b/ee/api/test/test_feature_flag_role_access.py index 3cd4e947d90..d73c1c73844 100644 --- a/ee/api/test/test_feature_flag_role_access.py +++ b/ee/api/test/test_feature_flag_role_access.py @@ -2,8 +2,8 @@ from rest_framework import status from ee.api.test.base import APILicensedTest from ee.models.feature_flag_role_access import FeatureFlagRoleAccess -from ee.models.organization_resource_access import OrganizationResourceAccess -from ee.models.role import Role +from ee.models.rbac.organization_resource_access import OrganizationResourceAccess +from ee.models.rbac.role import Role from posthog.models.feature_flag import FeatureFlag from posthog.models.organization import OrganizationMembership from posthog.models.user import User diff --git a/ee/api/test/test_organization_resource_access.py b/ee/api/test/test_organization_resource_access.py index 9123214a092..98206fe519f 100644 --- a/ee/api/test/test_organization_resource_access.py +++ b/ee/api/test/test_organization_resource_access.py @@ -2,7 +2,7 @@ from django.db import IntegrityError from rest_framework import status from ee.api.test.base import APILicensedTest -from ee.models.organization_resource_access import OrganizationResourceAccess +from ee.models.rbac.organization_resource_access import OrganizationResourceAccess from posthog.models.organization import Organization, OrganizationMembership from posthog.test.base import QueryMatchingTest, snapshot_postgres_queries, FuzzyInt diff --git a/ee/api/test/test_role.py b/ee/api/test/test_role.py index 1a3068ff4cf..96503162d5f 100644 --- a/ee/api/test/test_role.py +++ b/ee/api/test/test_role.py @@ -2,8 +2,8 @@ from django.db import IntegrityError from rest_framework import status from ee.api.test.base import APILicensedTest -from ee.models.organization_resource_access import OrganizationResourceAccess -from ee.models.role import Role +from ee.models.rbac.organization_resource_access import OrganizationResourceAccess +from ee.models.rbac.role import Role from posthog.models.organization import Organization, OrganizationMembership diff --git a/ee/api/test/test_role_membership.py b/ee/api/test/test_role_membership.py index f89796d9b7c..c3e67cf0514 100644 --- a/ee/api/test/test_role_membership.py +++ b/ee/api/test/test_role_membership.py @@ -1,7 +1,7 @@ from rest_framework import status from ee.api.test.base import APILicensedTest -from ee.models.role import Role, RoleMembership +from ee.models.rbac.role import Role, RoleMembership from posthog.models.organization import Organization, OrganizationMembership from posthog.models.user import User diff --git a/ee/models/__init__.py b/ee/models/__init__.py index fd87f76bd54..ff5a8fe2dff 100644 --- a/ee/models/__init__.py +++ b/ee/models/__init__.py @@ -5,7 +5,7 @@ from .feature_flag_role_access import FeatureFlagRoleAccess from .hook import Hook from .license import License from .property_definition import EnterprisePropertyDefinition -from .role import Role, RoleMembership +from .rbac.role import Role, RoleMembership __all__ = [ "EnterpriseEventDefinition", diff --git a/ee/models/organization_resource_access.py b/ee/models/rbac/organization_resource_access.py similarity index 95% rename from ee/models/organization_resource_access.py rename to ee/models/rbac/organization_resource_access.py index 924b3e9db28..de4c86d95a8 100644 --- a/ee/models/organization_resource_access.py +++ b/ee/models/rbac/organization_resource_access.py @@ -2,6 +2,8 @@ from django.db import models from posthog.models.organization import Organization +# NOTE: This will be deprecated in favour of the AccessControl model + class OrganizationResourceAccess(models.Model): class AccessLevel(models.IntegerChoices): diff --git a/ee/models/role.py b/ee/models/rbac/role.py similarity index 95% rename from ee/models/role.py rename to ee/models/rbac/role.py index f37170818db..97201835adb 100644 --- a/ee/models/role.py +++ b/ee/models/rbac/role.py @@ -1,6 +1,6 @@ from django.db import models -from ee.models.organization_resource_access import OrganizationResourceAccess +from ee.models.rbac.organization_resource_access import OrganizationResourceAccess from posthog.models.utils import UUIDModel diff --git a/ee/urls.py b/ee/urls.py index f0cf168acff..7c722bc3185 100644 --- a/ee/urls.py +++ b/ee/urls.py @@ -6,6 +6,7 @@ from django.urls import include from django.urls.conf import path from ee.api import integration +from .api.rbac import organization_resource_access, role from .api import ( authentication, @@ -15,8 +16,6 @@ from .api import ( feature_flag_role_access, hooks, license, - organization_resource_access, - role, sentry_stats, subscription, ) @@ -49,6 +48,7 @@ def extend_api_router() -> None: "organization_role_memberships", ["organization_id", "role_id"], ) + # Start: routes to be deprecated project_feature_flags_router.register( r"role_access", feature_flag_role_access.FeatureFlagRoleAccessViewSet, @@ -61,6 +61,7 @@ def extend_api_router() -> None: "organization_resource_access", ["organization_id"], ) + # End: routes to be deprecated register_grandfathered_environment_nested_viewset(r"hooks", hooks.HookViewSet, "environment_hooks", ["team_id"]) register_grandfathered_environment_nested_viewset( r"explicit_members", diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index 3d563ed32d7..1ba86ab79c3 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -220,6 +220,7 @@ export const FEATURE_FLAGS = { LEGACY_ACTION_WEBHOOKS: 'legacy-action-webhooks', // owner: @mariusandra #team-cdp SESSION_REPLAY_URL_TRIGGER: 'session-replay-url-trigger', // owner: @richard-better #team-replay REPLAY_TEMPLATES: 'replay-templates', // owner: @raquelmsmith #team-replay + ROLE_BASED_ACCESS_CONTROL: 'role-based-access-control', // owner: @zach EXPERIMENTS_HOLDOUTS: 'experiments-holdouts', // owner: @jurajmajerik #team-experiments MESSAGING: 'messaging', // owner @mariusandra #team-cdp SESSION_REPLAY_URL_BLOCKLIST: 'session-replay-url-blocklist', // owner: @richard-better #team-replay diff --git a/frontend/src/scenes/settings/user/personalAPIKeysLogic.tsx b/frontend/src/scenes/settings/user/personalAPIKeysLogic.tsx index e4f78f929de..be472786358 100644 --- a/frontend/src/scenes/settings/user/personalAPIKeysLogic.tsx +++ b/frontend/src/scenes/settings/user/personalAPIKeysLogic.tsx @@ -9,7 +9,7 @@ import { lemonToast } from 'lib/lemon-ui/LemonToast/LemonToast' import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' -import { OrganizationBasicType, PersonalAPIKeyType, TeamBasicType } from '~/types' +import { APIScopeObject, OrganizationBasicType, PersonalAPIKeyType, TeamBasicType } from '~/types' import type { personalAPIKeysLogicType } from './personalAPIKeysLogicType' @@ -32,7 +32,7 @@ export const API_KEY_SCOPE_PRESETS = [ ] export type APIScope = { - key: string + key: APIScopeObject info?: string | JSX.Element disabledActions?: ('read' | 'write')[] disabledWhenProjectScoped?: boolean diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 6b78307b817..eb839f5252b 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -3821,6 +3821,37 @@ export interface RoleMemberType { user_uuid: string } +export type APIScopeObject = + | 'action' + | 'activity_log' + | 'annotation' + | 'batch_export' + | 'cohort' + | 'dashboard' + | 'dashboard_template' + | 'early_access_feature' + | 'event_definition' + | 'experiment' + | 'export' + | 'feature_flag' + | 'group' + | 'insight' + | 'query' + | 'notebook' + | 'organization' + | 'organization_member' + | 'person' + | 'plugin' + | 'project' + | 'property_definition' + | 'session_recording' + | 'session_recording_playlist' + | 'sharing_configuration' + | 'subscription' + | 'survey' + | 'user' + | 'webhook' + export interface OrganizationResourcePermissionType { id: string resource: Resource diff --git a/posthog/api/personal_api_key.py b/posthog/api/personal_api_key.py index 23f6d531693..355d6cc1c18 100644 --- a/posthog/api/personal_api_key.py +++ b/posthog/api/personal_api_key.py @@ -5,7 +5,8 @@ from rest_framework import response, serializers, viewsets from rest_framework.permissions import IsAuthenticated from posthog.models import PersonalAPIKey, User -from posthog.models.personal_api_key import API_SCOPE_ACTIONS, API_SCOPE_OBJECTS, hash_key_value, mask_key_value +from posthog.models.personal_api_key import hash_key_value, mask_key_value +from posthog.models.scopes import API_SCOPE_ACTIONS, API_SCOPE_OBJECTS from posthog.models.team.team import Team from posthog.models.utils import generate_random_token_personal from posthog.permissions import TimeSensitiveActionPermission diff --git a/posthog/api/project.py b/posthog/api/project.py index 8efca11b219..c740dc33080 100644 --- a/posthog/api/project.py +++ b/posthog/api/project.py @@ -30,7 +30,7 @@ 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 from posthog.models.organization import OrganizationMembership -from posthog.models.personal_api_key import APIScopeObjectOrNotSupported +from posthog.models.scopes import APIScopeObjectOrNotSupported from posthog.models.product_intent.product_intent import ProductIntent from posthog.models.project import Project from posthog.models.signals import mute_selected_signals diff --git a/posthog/api/routing.py b/posthog/api/routing.py index 9ff2fede764..084ddcc94c3 100644 --- a/posthog/api/routing.py +++ b/posthog/api/routing.py @@ -18,7 +18,7 @@ from posthog.auth import ( SharingAccessTokenAuthentication, ) from posthog.models.organization import Organization -from posthog.models.personal_api_key import APIScopeObjectOrNotSupported +from posthog.models.scopes import APIScopeObjectOrNotSupported from posthog.models.project import Project from posthog.models.team import Team from posthog.models.user import User diff --git a/posthog/api/team.py b/posthog/api/team.py index 148471919cb..88b7f5e28b5 100644 --- a/posthog/api/team.py +++ b/posthog/api/team.py @@ -28,7 +28,7 @@ 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 from posthog.models.organization import OrganizationMembership -from posthog.models.personal_api_key import APIScopeObjectOrNotSupported +from posthog.models.scopes import APIScopeObjectOrNotSupported from posthog.models.project import Project from posthog.models.signals import mute_selected_signals from posthog.models.team.util import delete_batch_exports, delete_bulky_postgres_data diff --git a/posthog/api/test/__snapshots__/test_api_docs.ambr b/posthog/api/test/__snapshots__/test_api_docs.ambr index 6ef31c65301..2abd070a74e 100644 --- a/posthog/api/test/__snapshots__/test_api_docs.ambr +++ b/posthog/api/test/__snapshots__/test_api_docs.ambr @@ -70,9 +70,9 @@ '/home/runner/work/posthog/posthog/posthog/api/project.py: Warning [ProjectViewSet > ProjectBackwardCompatSerializer]: unable to resolve type hint for function "get_product_intents". Consider using a type hint or @extend_schema_field. Defaulting to string.', '/home/runner/work/posthog/posthog/posthog/api/proxy_record.py: Warning [ProxyRecordViewset]: could not derive type of path parameter "organization_id" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. ) or annotating the parameter type with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/proxy_record.py: Warning [ProxyRecordViewset]: could not derive type of path parameter "id" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. ) or annotating the parameter type with @extend_schema. Defaulting to "string".', - '/home/runner/work/posthog/posthog/ee/api/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/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/role.py: Warning [RoleMembershipViewSet]: could not derive type of path parameter "organization_id" because model "ee.models.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_members". 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_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 [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/posthog/api/action.py: Warning [ActionViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.action.action.Action" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/action.py: Warning [ActionViewSet > ActionSerializer]: unable to resolve type hint for function "get_creation_context". Consider using a type hint or @extend_schema_field. Defaulting to string.', '/home/runner/work/posthog/posthog/posthog/api/activity_log.py: Warning [ActivityLogViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.activity_logging.activity_log.ActivityLog" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', diff --git a/posthog/api/test/test_organization_feature_flag.py b/posthog/api/test/test_organization_feature_flag.py index 83550c689d5..65c43575f9b 100644 --- a/posthog/api/test/test_organization_feature_flag.py +++ b/posthog/api/test/test_organization_feature_flag.py @@ -3,7 +3,7 @@ from unittest.mock import ANY from rest_framework import status -from ee.models.organization_resource_access import OrganizationResourceAccess +from ee.models.rbac.organization_resource_access import OrganizationResourceAccess from posthog.api.dashboards.dashboard import Dashboard from posthog.constants import AvailableFeature from posthog.models import FeatureFlag diff --git a/posthog/models/feature_flag/permissions.py b/posthog/models/feature_flag/permissions.py index 8f766b4fccc..d2b0bb858a4 100644 --- a/posthog/models/feature_flag/permissions.py +++ b/posthog/models/feature_flag/permissions.py @@ -6,7 +6,7 @@ def can_user_edit_feature_flag(request, feature_flag): # self hosted check for enterprise models that may not exist try: from ee.models.feature_flag_role_access import FeatureFlagRoleAccess - from ee.models.organization_resource_access import OrganizationResourceAccess + from ee.models.rbac.organization_resource_access import OrganizationResourceAccess except: return True else: diff --git a/posthog/models/personal_api_key.py b/posthog/models/personal_api_key.py index ea886b55757..cac5adefbc6 100644 --- a/posthog/models/personal_api_key.py +++ b/posthog/models/personal_api_key.py @@ -1,4 +1,4 @@ -from typing import Optional, Literal, get_args +from typing import Optional, Literal import hashlib from django.contrib.auth.hashers import PBKDF2PasswordHasher @@ -66,56 +66,3 @@ class PersonalAPIKey(models.Model): null=True, blank=True, ) - - -## API Scopes -# These are the scopes that are used to define the permissions of the API tokens. -# Not every model needs a scope - it should more be for top-level things -# Typically each object should have `read` and `write` scopes, but some objects may have more specific scopes - -# WARNING: Make sure to keep in sync with the frontend! -APIScopeObject = Literal[ - "action", - "activity_log", - "annotation", - "batch_export", - "cohort", - "dashboard", - "dashboard_template", - "early_access_feature", - "event_definition", - "experiment", - "export", - "feature_flag", - "group", - "insight", - "query", # Covers query and events endpoints - "notebook", - "organization", - "organization_member", - "person", - "plugin", - "project", - "property_definition", - "session_recording", - "session_recording_playlist", - "sharing_configuration", - "subscription", - "survey", - "user", - "webhook", -] - -APIScopeActions = Literal[ - "read", - "write", -] - -APIScopeObjectOrNotSupported = Literal[ - APIScopeObject, - "INTERNAL", -] - - -API_SCOPE_OBJECTS: tuple[APIScopeObject, ...] = get_args(APIScopeObject) -API_SCOPE_ACTIONS: tuple[APIScopeActions, ...] = get_args(APIScopeActions) diff --git a/posthog/models/scopes.py b/posthog/models/scopes.py new file mode 100644 index 00000000000..2bd0d48b405 --- /dev/null +++ b/posthog/models/scopes.py @@ -0,0 +1,60 @@ +## API Scopes +# These are the scopes that are used to define the permissions of the API tokens. +# Not every model needs a scope - it should more be for top-level things +# Typically each object should have `read` and `write` scopes, but some objects may have more specific scopes + +# WARNING: Make sure to keep in sync with the frontend! +from typing import Literal, get_args + + +## API Scopes +# These are the scopes that are used to define the permissions of the API tokens. +# Not every model needs a scope - it should more be for top-level things +# Typically each object should have `read` and `write` scopes, but some objects may have more specific scopes + +# WARNING: Make sure to keep in sync with the frontend! +APIScopeObject = Literal[ + "action", + "activity_log", + "annotation", + "batch_export", + "cohort", + "dashboard", + "dashboard_template", + "early_access_feature", + "event_definition", + "experiment", + "export", + "feature_flag", + "group", + "insight", + "query", # Covers query and events endpoints + "notebook", + "organization", + "organization_member", + "person", + "plugin", + "project", + "property_definition", + "session_recording", + "session_recording_playlist", + "sharing_configuration", + "subscription", + "survey", + "user", + "webhook", +] + +APIScopeActions = Literal[ + "read", + "write", +] + +APIScopeObjectOrNotSupported = Literal[ + APIScopeObject, + "INTERNAL", +] + + +API_SCOPE_OBJECTS: tuple[APIScopeObject, ...] = get_args(APIScopeObject) +API_SCOPE_ACTIONS: tuple[APIScopeActions, ...] = get_args(APIScopeActions) diff --git a/posthog/permissions.py b/posthog/permissions.py index 889832c13ba..6de160e0995 100644 --- a/posthog/permissions.py +++ b/posthog/permissions.py @@ -19,7 +19,7 @@ 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.personal_api_key import APIScopeObjectOrNotSupported +from posthog.models.scopes import APIScopeObjectOrNotSupported from posthog.utils import get_can_create_org CREATE_METHODS = ["POST", "PUT"]