Skip to content

Commit c2c8f6e

Browse files
authored
fix: prevent API keys from creating UNLISTED views (#16770)
## 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 |
1 parent ba76cf4 commit c2c8f6e

File tree

1 file changed

+76
-65
lines changed

1 file changed

+76
-65
lines changed

packages/twenty-server/src/engine/metadata-modules/view-permissions/services/view-access.service.ts

Lines changed: 76 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Injectable } from '@nestjs/common';
22

3-
import { isDefined } from 'twenty-shared/utils';
43
import { PermissionFlagType } from 'twenty-shared/constants';
4+
import { isDefined } from 'twenty-shared/utils';
55

66
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
77
import { type ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
@@ -79,44 +79,26 @@ export class ViewAccessService {
7979
workspaceId: string,
8080
apiKeyId?: string,
8181
): Promise<boolean> {
82-
// For WORKSPACE visibility views, check VIEWS permission
83-
if (visibility === ViewVisibility.WORKSPACE) {
84-
let hasViewsPermission = false;
85-
86-
if (isDefined(userWorkspaceId)) {
87-
const permissions =
88-
await this.permissionsService.getUserWorkspacePermissions({
89-
userWorkspaceId,
90-
workspaceId,
91-
});
92-
93-
hasViewsPermission =
94-
permissions.permissionFlags[PermissionFlagType.VIEWS] ?? false;
95-
} else if (isDefined(apiKeyId)) {
96-
hasViewsPermission =
97-
await this.permissionsService.userHasWorkspaceSettingPermission({
98-
workspaceId,
99-
apiKeyId,
100-
setting: PermissionFlagType.VIEWS,
101-
});
82+
// UNLISTED views can only be created by users (not API keys)
83+
if (visibility === ViewVisibility.UNLISTED) {
84+
if (!isDefined(userWorkspaceId)) {
85+
this.throwCreatePermissionDenied();
10286
}
10387

104-
if (!hasViewsPermission) {
105-
throw new ViewException(
106-
generateViewExceptionMessage(
107-
ViewExceptionMessageKey.VIEW_CREATE_PERMISSION_DENIED,
108-
),
109-
ViewExceptionCode.VIEW_CREATE_PERMISSION_DENIED,
110-
{
111-
userFriendlyMessage: generateViewUserFriendlyExceptionMessage(
112-
ViewExceptionMessageKey.VIEW_CREATE_PERMISSION_DENIED,
113-
),
114-
},
115-
);
116-
}
88+
return true;
89+
}
90+
91+
// WORKSPACE visibility views require VIEWS permission
92+
const hasPermission = await this.hasViewsPermission(
93+
userWorkspaceId,
94+
workspaceId,
95+
apiKeyId,
96+
);
97+
98+
if (!hasPermission) {
99+
this.throwCreatePermissionDenied();
117100
}
118101

119-
// For UNLISTED views, allow creation
120102
return true;
121103
}
122104

@@ -126,50 +108,79 @@ export class ViewAccessService {
126108
workspaceId: string,
127109
apiKeyId?: string,
128110
): Promise<boolean> {
129-
let hasViewsPermission = false;
111+
const hasPermission = await this.hasViewsPermission(
112+
userWorkspaceId,
113+
workspaceId,
114+
apiKeyId,
115+
);
116+
117+
if (hasPermission) {
118+
return true;
119+
}
120+
121+
// Users without VIEWS permission can only manipulate their own unlisted views
122+
const isOwnUnlistedView =
123+
view.visibility === ViewVisibility.UNLISTED &&
124+
view.createdByUserWorkspaceId === userWorkspaceId;
125+
126+
if (isOwnUnlistedView) {
127+
return true;
128+
}
130129

130+
this.throwModifyPermissionDenied();
131+
}
132+
133+
private async hasViewsPermission(
134+
userWorkspaceId: string | undefined,
135+
workspaceId: string,
136+
apiKeyId?: string,
137+
): Promise<boolean> {
131138
if (isDefined(userWorkspaceId)) {
132139
const permissions =
133140
await this.permissionsService.getUserWorkspacePermissions({
134141
userWorkspaceId,
135142
workspaceId,
136143
});
137144

138-
hasViewsPermission =
139-
permissions.permissionFlags[PermissionFlagType.VIEWS] ?? false;
140-
} else if (isDefined(apiKeyId)) {
141-
hasViewsPermission =
142-
await this.permissionsService.userHasWorkspaceSettingPermission({
143-
workspaceId,
144-
apiKeyId,
145-
setting: PermissionFlagType.VIEWS,
146-
});
145+
return permissions.permissionFlags[PermissionFlagType.VIEWS] ?? false;
147146
}
148147

149-
// Users/API keys with VIEWS permission can manipulate all views
150-
if (hasViewsPermission) {
151-
return true;
148+
if (isDefined(apiKeyId)) {
149+
return this.permissionsService.userHasWorkspaceSettingPermission({
150+
workspaceId,
151+
apiKeyId,
152+
setting: PermissionFlagType.VIEWS,
153+
});
152154
}
153155

154-
// Users without VIEWS permission can only manipulate their own unlisted views
155-
const canAccess =
156-
view.visibility === ViewVisibility.UNLISTED &&
157-
view.createdByUserWorkspaceId === userWorkspaceId;
156+
return false;
157+
}
158158

159-
if (!canAccess) {
160-
throw new ViewException(
161-
generateViewExceptionMessage(
162-
ViewExceptionMessageKey.VIEW_MODIFY_PERMISSION_DENIED,
159+
private throwCreatePermissionDenied(): never {
160+
throw new ViewException(
161+
generateViewExceptionMessage(
162+
ViewExceptionMessageKey.VIEW_CREATE_PERMISSION_DENIED,
163+
),
164+
ViewExceptionCode.VIEW_CREATE_PERMISSION_DENIED,
165+
{
166+
userFriendlyMessage: generateViewUserFriendlyExceptionMessage(
167+
ViewExceptionMessageKey.VIEW_CREATE_PERMISSION_DENIED,
163168
),
164-
ViewExceptionCode.VIEW_MODIFY_PERMISSION_DENIED,
165-
{
166-
userFriendlyMessage: generateViewUserFriendlyExceptionMessage(
167-
ViewExceptionMessageKey.VIEW_MODIFY_PERMISSION_DENIED,
168-
),
169-
},
170-
);
171-
}
169+
},
170+
);
171+
}
172172

173-
return true;
173+
private throwModifyPermissionDenied(): never {
174+
throw new ViewException(
175+
generateViewExceptionMessage(
176+
ViewExceptionMessageKey.VIEW_MODIFY_PERMISSION_DENIED,
177+
),
178+
ViewExceptionCode.VIEW_MODIFY_PERMISSION_DENIED,
179+
{
180+
userFriendlyMessage: generateViewUserFriendlyExceptionMessage(
181+
ViewExceptionMessageKey.VIEW_MODIFY_PERMISSION_DENIED,
182+
),
183+
},
184+
);
174185
}
175186
}

0 commit comments

Comments
 (0)