Skip to content

Commit

Permalink
feat: Personal API key scopes (#20151)
Browse files Browse the repository at this point in the history
  • Loading branch information
benjackwhite authored Feb 20, 2024
1 parent d13ffcf commit 4dd3c35
Show file tree
Hide file tree
Showing 96 changed files with 1,665 additions and 501 deletions.
1 change: 0 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@
"SKIP_SERVICE_VERSION_REQUIREMENTS": "1",
"PRINT_SQL": "1",
"REPLAY_EVENTS_NEW_CONSUMER_RATIO": "1.0",
"AUTO_LOGIN": "True",
"BILLING_SERVICE_URL": "https://billing.dev.posthog.dev"
},
"console": "integratedTerminal",
Expand Down
2 changes: 2 additions & 0 deletions ee/api/billing.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ class BillingViewset(TeamAndOrgViewSetMixin, viewsets.GenericViewSet):
serializer_class = BillingSerializer
derive_current_team_from_user_only = True

scope_object = "INTERNAL"

def list(self, request: Request, *args: Any, **kwargs: Any) -> Response:
license = get_cached_instance_license()
if license and not license.is_v2_license:
Expand Down
2 changes: 1 addition & 1 deletion ee/api/dashboard_collaborator.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,13 @@ class DashboardCollaboratorViewSet(
mixins.DestroyModelMixin,
viewsets.GenericViewSet,
):
scope_object = "INTERNAL"
permission_classes = [CanEditDashboardCollaborator]
pagination_class = None
queryset = DashboardPrivilege.objects.select_related("dashboard").filter(user__is_active=True)
lookup_field = "user__uuid"
serializer_class = DashboardCollaboratorSerializer
filter_rewrite_rules = {"team_id": "dashboard__team_id"}
include_in_docs = False

def get_serializer_context(self) -> Dict[str, Any]:
context = super().get_serializer_context()
Expand Down
2 changes: 1 addition & 1 deletion ee/api/explicit_team_member.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,14 @@ def validate(self, attrs):


class ExplicitTeamMemberViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet):
scope_object = "project"
pagination_class = None
queryset = ExplicitTeamMembership.objects.filter(parent_membership__user__is_active=True).select_related(
"team", "parent_membership", "parent_membership__user"
)
lookup_field = "parent_membership__user__uuid"
ordering = ["level", "-joined_at"]
serializer_class = ExplicitTeamMemberSerializer
include_in_docs = True

permission_classes = [IsAuthenticated, TeamMemberStrictManagementPermission]

Expand Down
1 change: 1 addition & 0 deletions ee/api/feature_flag_role_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class FeatureFlagRoleAccessViewSet(
mixins.RetrieveModelMixin,
viewsets.GenericViewSet,
):
scope_object = "feature_flag"
permission_classes = [FeatureFlagRoleAccessPermissions]
serializer_class = FeatureFlagRoleAccessSerializer
queryset = FeatureFlagRoleAccess.objects.select_related("feature_flag")
Expand Down
4 changes: 4 additions & 0 deletions ee/api/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ class HookViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet):
Retrieve, create, update or destroy REST hooks.
"""

scope_object = "webhook"
# NOTE: This permissions is needed for Zapier calls but we don't want to expose it in the API docs until
# it is able to support more than Zapier
hide_api_docs = True
queryset = Hook.objects.all()
ordering = "-created_at"
serializer_class = HookSerializer
Expand Down
1 change: 1 addition & 0 deletions ee/api/organization_resource_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class OrganizationResourceAccessViewSet(
mixins.DestroyModelMixin,
viewsets.GenericViewSet,
):
scope_object = "INTERNAL"
permission_classes = [RolePermissions]
serializer_class = OrganizationResourceAccessSerializer
queryset = OrganizationResourceAccess.objects.all()
2 changes: 2 additions & 0 deletions ee/api/role.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ class RoleViewSet(
mixins.DestroyModelMixin,
viewsets.GenericViewSet,
):
scope_object = "organization"
permission_classes = [RolePermissions]
serializer_class = RoleSerializer
queryset = Role.objects.all()
Expand Down Expand Up @@ -133,6 +134,7 @@ class RoleMembershipViewSet(
mixins.DestroyModelMixin,
viewsets.GenericViewSet,
):
scope_object = "organization"
permission_classes = [RolePermissions]
serializer_class = RoleMembershipSerializer
queryset = RoleMembership.objects.select_related("role")
Expand Down
1 change: 1 addition & 0 deletions ee/api/subscription.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def update(self, instance: Subscription, validated_data: dict, *args: Any, **kwa


class SubscriptionViewSet(TeamAndOrgViewSetMixin, ForbidDestroyModel, viewsets.ModelViewSet):
scope_object = "subscription"
queryset = Subscription.objects.all()
serializer_class = SubscriptionSerializer
permission_classes = [PremiumFeaturePermission]
Expand Down
34 changes: 30 additions & 4 deletions ee/api/test/test_team.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from rest_framework.status import (
HTTP_200_OK,
HTTP_204_NO_CONTENT,
HTTP_400_BAD_REQUEST,
HTTP_403_FORBIDDEN,
HTTP_404_NOT_FOUND,
)
Expand Down Expand Up @@ -98,17 +97,44 @@ def test_user_that_does_not_belong_to_an_org_cannot_create_a_project(self):
self.client.force_login(user)

response = self.client.post("/api/projects/", {"name": "Test"})
self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST, response.content)
self.assertEqual(response.status_code, HTTP_404_NOT_FOUND, response.content)
self.assertEqual(
response.json(),
{
"type": "validation_error",
"code": "invalid_input",
"type": "invalid_request",
"code": "not_found",
"detail": "You need to belong to an organization.",
"attr": None,
},
)

def test_user_create_project_for_org_via_url(self):
# Set both current and new org to high enough membership level
self.organization_membership.level = OrganizationMembership.Level.ADMIN
self.organization_membership.save()

current_org, _, _ = Organization.objects.bootstrap(self.user, name="other_org")
other_org = self.organization # Bootstrapping above sets it to the current org

assert current_org.id == self.user.current_organization_id
response = self.client.post(f"/api/organizations/{current_org.id}/projects/", {"name": "Via current org"})
self.assertEqual(response.status_code, 201)
assert response.json()["organization"] == str(current_org.id)

assert other_org.id != self.user.current_organization_id
response = self.client.post(f"/api/organizations/{other_org.id}/projects/", {"name": "Via path org"})
self.assertEqual(response.status_code, 201, msg=response.json())
assert response.json()["organization"] == str(other_org.id)

def test_user_cannot_create_project_in_org_without_access(self):
_, _, _ = Organization.objects.bootstrap(self.user, name="other_org")
other_org = self.organization # Bootstrapping above sets it to the current org

assert other_org.id != self.user.current_organization_id
response = self.client.post(f"/api/organizations/{other_org.id}/projects/", {"name": "Via path org"})
self.assertEqual(response.status_code, 403, msg=response.json())
assert response.json() == self.permission_denied_response("Your organization access level is insufficient.")

# Deleting projects

def test_delete_team_as_org_admin_allowed(self):
Expand Down
1 change: 1 addition & 0 deletions ee/clickhouse/views/experiments.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ def update(self, instance: Experiment, validated_data: dict, *args: Any, **kwarg


class ClickhouseExperimentsViewSet(TeamAndOrgViewSetMixin, viewsets.ModelViewSet):
scope_object = "experiment"
serializer_class = ExperimentSerializer
queryset = Experiment.objects.all()
permission_classes = [PremiumFeaturePermission]
Expand Down
2 changes: 2 additions & 0 deletions ee/clickhouse/views/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class Meta:


class ClickhouseGroupsTypesView(TeamAndOrgViewSetMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
scope_object = "group"
serializer_class = GroupTypeSerializer
queryset = GroupTypeMapping.objects.all().order_by("group_type_index")
pagination_class = None
Expand Down Expand Up @@ -54,6 +55,7 @@ class Meta:


class ClickhouseGroupsView(TeamAndOrgViewSetMixin, mixins.ListModelMixin, viewsets.GenericViewSet):
scope_object = "group"
serializer_class = GroupSerializer
queryset = Group.objects.all()
pagination_class = GroupCursorPagination
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# serializer version: 1
# name: ClickhouseTestExperimentSecondaryResults.test_basic_secondary_metric_results
'''
/* user_id:124 celery:posthog.tasks.tasks.sync_insight_caching_state */
/* user_id:123 celery:posthog.tasks.tasks.sync_insight_caching_state */
SELECT team_id,
date_diff('second', max(timestamp), now()) AS age
FROM events
Expand Down
2 changes: 1 addition & 1 deletion ee/session_recordings/session_recording_playlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,12 +163,12 @@ def _check_can_create_playlist(self, team: Team) -> bool:


class SessionRecordingPlaylistViewSet(TeamAndOrgViewSetMixin, ForbidDestroyModel, viewsets.ModelViewSet):
scope_object = "session_recording_playlist"
queryset = SessionRecordingPlaylist.objects.all()
serializer_class = SessionRecordingPlaylistSerializer
throttle_classes = [ClickHouseBurstRateThrottle, ClickHouseSustainedRateThrottle]
filter_backends = [DjangoFilterBackend]
filterset_fields = ["short_id", "created_by"]
include_in_docs = True
lookup_field = "short_id"

def get_queryset(self) -> QuerySet:
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions frontend/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
OrganizationFeatureFlagsCopyBody,
OrganizationResourcePermissionType,
OrganizationType,
PersonalAPIKeyType,
PersonListParams,
PersonType,
PluginConfigTypeNew,
Expand Down Expand Up @@ -717,6 +718,15 @@ class ApiRequest {
return this.projectsDetail(teamId).addPathComponent('activity_log')
}

// Personal API keys
public personalApiKeys(): ApiRequest {
return this.addPathComponent('personal_api_keys')
}

public personalApiKey(id: PersonalAPIKeyType['id']): ApiRequest {
return this.personalApiKeys().addPathComponent(id)
}

// Request finalization
public async get(options?: ApiMethodOptions): Promise<any> {
return await api.get(this.assembleFullUrl(), options)
Expand Down Expand Up @@ -1966,6 +1976,21 @@ const api = {
},
},

personalApiKeys: {
async list(): Promise<PersonalAPIKeyType[]> {
return await new ApiRequest().personalApiKeys().get()
},
async create(data: Partial<PersonalAPIKeyType>): Promise<PersonalAPIKeyType> {
return await new ApiRequest().personalApiKeys().create({ data })
},
async update(id: PersonalAPIKeyType['id'], data: Partial<PersonalAPIKeyType>): Promise<PersonalAPIKeyType> {
return await new ApiRequest().personalApiKey(id).update({ data })
},
async delete(id: PersonalAPIKeyType['id']): Promise<void> {
await new ApiRequest().personalApiKey(id).delete()
},
},

queryURL: (): string => {
return new ApiRequest().query().assembleFullUrl(true)
},
Expand Down
38 changes: 7 additions & 31 deletions frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ import { sidePanelLogic } from '~/layout/navigation-3000/sidepanel/sidePanelLogi
import { sidePanelStateLogic } from '~/layout/navigation-3000/sidepanel/sidePanelStateLogic'
import { InsightType } from '~/types'

import { personalAPIKeysLogic } from '../../../scenes/settings/user/personalAPIKeysLogic'
import { commandBarLogic } from '../CommandBar/commandBarLogic'
import { BarStatus } from '../CommandBar/types'
import { hedgehogBuddyLogic } from '../HedgehogBuddy/hedgehogBuddyLogic'
Expand Down Expand Up @@ -140,8 +139,6 @@ export const commandPaletteLogic = kea<commandPaletteLogicType>([
path(['lib', 'components', 'CommandPalette', 'commandPaletteLogic']),
connect({
actions: [
personalAPIKeysLogic,
['createKey'],
router,
['push'],
userLogic,
Expand Down Expand Up @@ -663,6 +660,13 @@ export const commandPaletteLogic = kea<commandPaletteLogicType>([
userLogic.actions.logout()
},
},
{
icon: IconUnlock,
display: 'Go to Personal API Keys',
executor: () => {
push(urls.settings('user-api-keys'))
},
},
],
}

Expand Down Expand Up @@ -758,33 +762,6 @@ export const commandPaletteLogic = kea<commandPaletteLogicType>([
},
}

const createPersonalApiKey: Command = {
key: 'create-personal-api-key',
scope: GLOBAL_COMMAND_SCOPE,
resolver: {
icon: IconUnlock,
display: 'Create Personal API Key',
executor: () => ({
instruction: 'Give your key a label',
icon: IconKeyboard,
scope: 'Creating Personal API Key',
resolver: (argument) => {
if (argument?.length) {
return {
icon: IconUnlock,
display: `Create Key "${argument}"`,
executor: () => {
personalAPIKeysLogic.actions.createKey(argument)
push(urls.settings('user'), {}, 'personal-api-keys')
},
}
}
return null
},
}),
},
}

const createDashboard: Command = {
key: 'create-dashboard',
scope: GLOBAL_COMMAND_SCOPE,
Expand Down Expand Up @@ -962,7 +939,6 @@ export const commandPaletteLogic = kea<commandPaletteLogicType>([
actions.registerCommand(openUrls)
actions.registerCommand(debugClickhouseQueries)
actions.registerCommand(calculator)
actions.registerCommand(createPersonalApiKey)
actions.registerCommand(createDashboard)
actions.registerCommand(shareFeedback)
actions.registerCommand(debugCopySessionRecordingURL)
Expand Down
Loading

0 comments on commit 4dd3c35

Please sign in to comment.