From c2c8f6e41cc61bf9c47d97629fc681693ff17481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Tue, 23 Dec 2025 10:13:00 +0100 Subject: [PATCH] fix: prevent API keys from creating UNLISTED views (#16770) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary UNLISTED views are personal views tied to a specific user, so API keys should not be able to create them. ## Changes - Added check in `canUserCreateView` to block API keys from creating UNLISTED views - Refactored the service to use smaller functions with early returns (no nested if/else) ## Behavior Matrix ### Creating Views | Caller | Visibility | Has VIEWS Permission | Result | |--------|------------|---------------------|--------| | User | UNLISTED | (not checked) | ✅ Allow | | User | WORKSPACE | Yes | ✅ Allow | | User | WORKSPACE | No | ❌ Denied | | **API Key** | **UNLISTED** | (not checked) | **❌ Denied** | | API Key | WORKSPACE | Yes | ✅ Allow | | API Key | WORKSPACE | No | ❌ Denied | --- .../services/view-access.service.ts | 141 ++++++++++-------- 1 file changed, 76 insertions(+), 65 deletions(-) diff --git a/packages/twenty-server/src/engine/metadata-modules/view-permissions/services/view-access.service.ts b/packages/twenty-server/src/engine/metadata-modules/view-permissions/services/view-access.service.ts index 9cbf49cbd7..42d0ce23e7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-permissions/services/view-access.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-permissions/services/view-access.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; -import { isDefined } from 'twenty-shared/utils'; import { PermissionFlagType } from 'twenty-shared/constants'; +import { isDefined } from 'twenty-shared/utils'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; import { type ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; @@ -79,44 +79,26 @@ export class ViewAccessService { workspaceId: string, apiKeyId?: string, ): Promise { - // For WORKSPACE visibility views, check VIEWS permission - if (visibility === ViewVisibility.WORKSPACE) { - let hasViewsPermission = false; - - if (isDefined(userWorkspaceId)) { - const permissions = - await this.permissionsService.getUserWorkspacePermissions({ - userWorkspaceId, - workspaceId, - }); - - hasViewsPermission = - permissions.permissionFlags[PermissionFlagType.VIEWS] ?? false; - } else if (isDefined(apiKeyId)) { - hasViewsPermission = - await this.permissionsService.userHasWorkspaceSettingPermission({ - workspaceId, - apiKeyId, - setting: PermissionFlagType.VIEWS, - }); + // UNLISTED views can only be created by users (not API keys) + if (visibility === ViewVisibility.UNLISTED) { + if (!isDefined(userWorkspaceId)) { + this.throwCreatePermissionDenied(); } - if (!hasViewsPermission) { - throw new ViewException( - generateViewExceptionMessage( - ViewExceptionMessageKey.VIEW_CREATE_PERMISSION_DENIED, - ), - ViewExceptionCode.VIEW_CREATE_PERMISSION_DENIED, - { - userFriendlyMessage: generateViewUserFriendlyExceptionMessage( - ViewExceptionMessageKey.VIEW_CREATE_PERMISSION_DENIED, - ), - }, - ); - } + return true; + } + + // WORKSPACE visibility views require VIEWS permission + const hasPermission = await this.hasViewsPermission( + userWorkspaceId, + workspaceId, + apiKeyId, + ); + + if (!hasPermission) { + this.throwCreatePermissionDenied(); } - // For UNLISTED views, allow creation return true; } @@ -126,8 +108,33 @@ export class ViewAccessService { workspaceId: string, apiKeyId?: string, ): Promise { - let hasViewsPermission = false; + const hasPermission = await this.hasViewsPermission( + userWorkspaceId, + workspaceId, + apiKeyId, + ); + if (hasPermission) { + return true; + } + + // Users without VIEWS permission can only manipulate their own unlisted views + const isOwnUnlistedView = + view.visibility === ViewVisibility.UNLISTED && + view.createdByUserWorkspaceId === userWorkspaceId; + + if (isOwnUnlistedView) { + return true; + } + + this.throwModifyPermissionDenied(); + } + + private async hasViewsPermission( + userWorkspaceId: string | undefined, + workspaceId: string, + apiKeyId?: string, + ): Promise { if (isDefined(userWorkspaceId)) { const permissions = await this.permissionsService.getUserWorkspacePermissions({ @@ -135,41 +142,45 @@ export class ViewAccessService { workspaceId, }); - hasViewsPermission = - permissions.permissionFlags[PermissionFlagType.VIEWS] ?? false; - } else if (isDefined(apiKeyId)) { - hasViewsPermission = - await this.permissionsService.userHasWorkspaceSettingPermission({ - workspaceId, - apiKeyId, - setting: PermissionFlagType.VIEWS, - }); + return permissions.permissionFlags[PermissionFlagType.VIEWS] ?? false; } - // Users/API keys with VIEWS permission can manipulate all views - if (hasViewsPermission) { - return true; + if (isDefined(apiKeyId)) { + return this.permissionsService.userHasWorkspaceSettingPermission({ + workspaceId, + apiKeyId, + setting: PermissionFlagType.VIEWS, + }); } - // Users without VIEWS permission can only manipulate their own unlisted views - const canAccess = - view.visibility === ViewVisibility.UNLISTED && - view.createdByUserWorkspaceId === userWorkspaceId; + return false; + } - if (!canAccess) { - throw new ViewException( - generateViewExceptionMessage( + private throwCreatePermissionDenied(): never { + throw new ViewException( + generateViewExceptionMessage( + ViewExceptionMessageKey.VIEW_CREATE_PERMISSION_DENIED, + ), + ViewExceptionCode.VIEW_CREATE_PERMISSION_DENIED, + { + userFriendlyMessage: generateViewUserFriendlyExceptionMessage( + ViewExceptionMessageKey.VIEW_CREATE_PERMISSION_DENIED, + ), + }, + ); + } + + private throwModifyPermissionDenied(): never { + throw new ViewException( + generateViewExceptionMessage( + ViewExceptionMessageKey.VIEW_MODIFY_PERMISSION_DENIED, + ), + ViewExceptionCode.VIEW_MODIFY_PERMISSION_DENIED, + { + userFriendlyMessage: generateViewUserFriendlyExceptionMessage( ViewExceptionMessageKey.VIEW_MODIFY_PERMISSION_DENIED, ), - ViewExceptionCode.VIEW_MODIFY_PERMISSION_DENIED, - { - userFriendlyMessage: generateViewUserFriendlyExceptionMessage( - ViewExceptionMessageKey.VIEW_MODIFY_PERMISSION_DENIED, - ), - }, - ); - } - - return true; + }, + ); } }