65dced14ff
Fixes #16739 - Remove empty string coercion in createCoreView that caused PostgreSQL UUID errors for API keys - Add permission check allowing API keys with VIEWS permission to manage workspace views they created API keys with 'Manage Views' permission can now create, update, and delete workspace views via both GraphQL and REST APIs.
176 lines
5.1 KiB
TypeScript
176 lines
5.1 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
import { PermissionFlagType } from 'twenty-shared/constants';
|
|
|
|
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
|
import { type ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
|
import { ViewVisibility } from 'src/engine/metadata-modules/view/enums/view-visibility.enum';
|
|
import {
|
|
ViewException,
|
|
ViewExceptionCode,
|
|
ViewExceptionMessageKey,
|
|
generateViewExceptionMessage,
|
|
generateViewUserFriendlyExceptionMessage,
|
|
} from 'src/engine/metadata-modules/view/exceptions/view.exception';
|
|
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
|
|
|
|
@Injectable()
|
|
export class ViewAccessService {
|
|
constructor(
|
|
private readonly viewService: ViewService,
|
|
private readonly permissionsService: PermissionsService,
|
|
) {}
|
|
|
|
async canUserModifyView(
|
|
viewId: string | null,
|
|
userWorkspaceId: string | undefined,
|
|
workspaceId: string,
|
|
apiKeyId?: string,
|
|
): Promise<boolean> {
|
|
// If viewId is null, the entity doesn't exist - allow the operation
|
|
// so the service can handle the NOT_FOUND error properly
|
|
if (!viewId) {
|
|
return true;
|
|
}
|
|
|
|
const view = await this.viewService.findByIdIncludingDeleted(
|
|
viewId,
|
|
workspaceId,
|
|
);
|
|
|
|
// If view doesn't exist, allow through to service for proper error message
|
|
if (!view) {
|
|
return true;
|
|
}
|
|
|
|
return this.checkViewAccess(view, userWorkspaceId, workspaceId, apiKeyId);
|
|
}
|
|
|
|
async canUserModifyViewByChildEntity(
|
|
viewId: string | null,
|
|
userWorkspaceId: string | undefined,
|
|
workspaceId: string,
|
|
apiKeyId?: string,
|
|
): Promise<boolean> {
|
|
// If viewId is null, the child entity doesn't exist
|
|
// Allow through so the service can throw the proper entity-specific error
|
|
// (e.g., "View field not found" instead of generic "View not found")
|
|
if (!viewId) {
|
|
return true;
|
|
}
|
|
|
|
const view = await this.viewService.findByIdIncludingDeleted(
|
|
viewId,
|
|
workspaceId,
|
|
);
|
|
|
|
// If view doesn't exist, allow through to service for proper error message
|
|
if (!view) {
|
|
return true;
|
|
}
|
|
|
|
return this.checkViewAccess(view, userWorkspaceId, workspaceId, apiKeyId);
|
|
}
|
|
|
|
async canUserCreateView(
|
|
visibility: ViewVisibility,
|
|
userWorkspaceId: string | undefined,
|
|
workspaceId: string,
|
|
apiKeyId?: string,
|
|
): Promise<boolean> {
|
|
// 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,
|
|
});
|
|
}
|
|
|
|
if (!hasViewsPermission) {
|
|
throw new ViewException(
|
|
generateViewExceptionMessage(
|
|
ViewExceptionMessageKey.VIEW_CREATE_PERMISSION_DENIED,
|
|
),
|
|
ViewExceptionCode.VIEW_CREATE_PERMISSION_DENIED,
|
|
{
|
|
userFriendlyMessage: generateViewUserFriendlyExceptionMessage(
|
|
ViewExceptionMessageKey.VIEW_CREATE_PERMISSION_DENIED,
|
|
),
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
// For UNLISTED views, allow creation
|
|
return true;
|
|
}
|
|
|
|
private async checkViewAccess(
|
|
view: ViewEntity,
|
|
userWorkspaceId: string | undefined,
|
|
workspaceId: string,
|
|
apiKeyId?: string,
|
|
): Promise<boolean> {
|
|
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,
|
|
});
|
|
}
|
|
|
|
// Users/API keys with VIEWS permission can manipulate all views
|
|
if (hasViewsPermission) {
|
|
return true;
|
|
}
|
|
|
|
// Users without VIEWS permission can only manipulate their own unlisted views
|
|
const canAccess =
|
|
view.visibility === ViewVisibility.UNLISTED &&
|
|
view.createdByUserWorkspaceId === userWorkspaceId;
|
|
|
|
if (!canAccess) {
|
|
throw new ViewException(
|
|
generateViewExceptionMessage(
|
|
ViewExceptionMessageKey.VIEW_MODIFY_PERMISSION_DENIED,
|
|
),
|
|
ViewExceptionCode.VIEW_MODIFY_PERMISSION_DENIED,
|
|
{
|
|
userFriendlyMessage: generateViewUserFriendlyExceptionMessage(
|
|
ViewExceptionMessageKey.VIEW_MODIFY_PERMISSION_DENIED,
|
|
),
|
|
},
|
|
);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|