Enhance role-check system with stricter checks (#15392)
## Overview This PR strengthens our permission system by introducing more granular role-based access control across the platform. ## Changes ### New Permissions Added - **Applications** - Control who can install and manage applications - **Layouts** - Control who can customize page layouts and UI structure - **AI** - Control access to AI features and agents - **Upload File** - Separate permission for file uploads - **Download File** - Separate permission for file downloads (frontend visibility) ### Security Enhancements - Implemented whitelist-based validation for workspace field updates - Added explicit permission guards to core entity resolvers - Enhanced ESLint rule to enforce permission checks on all mutations - Created `CustomPermissionGuard` and `NoPermissionGuard` for better code documentation ### Affected Components - Core entity resolvers: webhooks, files, domains, applications, layouts, postgres credentials - Workspace update mutations now use whitelist validation - Settings UI updated with new permission controls ### Developer Experience - ESLint now catches missing permission guards during development - Explicit guard markers make permission requirements clear in code review - Comprehensive test coverage for new permission logic ## Testing - ✅ All TypeScript type checks pass - ✅ ESLint validation passes - ✅ New permission guards properly enforced - ✅ Frontend UI displays new permissions correctly ## Migration Notes Existing workspaces will need to assign the new permissions to roles as needed. By default, all new permissions are set to `false` for non-admin roles.
This commit is contained in:
+6
@@ -16,6 +16,9 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { CreateViewSortPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/create-view-sort-permission.guard';
|
||||
import { DeleteViewSortPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/delete-view-sort-permission.guard';
|
||||
import { UpdateViewSortPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/update-view-sort-permission.guard';
|
||||
import { CreateViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/create-view-sort.input';
|
||||
import { UpdateViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/update-view-sort.input';
|
||||
import { type ViewSortDTO } from 'src/engine/metadata-modules/view-sort/dtos/view-sort.dto';
|
||||
@@ -73,6 +76,7 @@ export class ViewSortController {
|
||||
}
|
||||
|
||||
@Post()
|
||||
@UseGuards(CreateViewSortPermissionGuard)
|
||||
async create(
|
||||
@Body() input: CreateViewSortInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@@ -84,6 +88,7 @@ export class ViewSortController {
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@UseGuards(UpdateViewSortPermissionGuard)
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body() input: UpdateViewSortInput,
|
||||
@@ -99,6 +104,7 @@ export class ViewSortController {
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(DeleteViewSortPermissionGuard)
|
||||
async delete(
|
||||
@Param('id') id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
|
||||
+10
-19
@@ -1,5 +1,5 @@
|
||||
import { msg, t } from '@lingui/core/macro';
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
@@ -18,6 +18,7 @@ export class ViewSortException extends CustomException {
|
||||
export enum ViewSortExceptionCode {
|
||||
VIEW_SORT_NOT_FOUND = 'VIEW_SORT_NOT_FOUND',
|
||||
INVALID_VIEW_SORT_DATA = 'INVALID_VIEW_SORT_DATA',
|
||||
VIEW_NOT_FOUND = 'VIEW_NOT_FOUND',
|
||||
}
|
||||
|
||||
export enum ViewSortExceptionMessageKey {
|
||||
@@ -26,39 +27,29 @@ export enum ViewSortExceptionMessageKey {
|
||||
VIEW_SORT_NOT_FOUND = 'VIEW_SORT_NOT_FOUND',
|
||||
INVALID_VIEW_SORT_DATA = 'INVALID_VIEW_SORT_DATA',
|
||||
FIELD_METADATA_ID_REQUIRED = 'FIELD_METADATA_ID_REQUIRED',
|
||||
VIEW_NOT_FOUND = 'VIEW_NOT_FOUND',
|
||||
}
|
||||
|
||||
export const generateViewSortExceptionMessage = (
|
||||
key: ViewSortExceptionMessageKey,
|
||||
id?: string,
|
||||
) => {
|
||||
let message = '';
|
||||
|
||||
switch (key) {
|
||||
case ViewSortExceptionMessageKey.WORKSPACE_ID_REQUIRED:
|
||||
message = t`WorkspaceId is required`;
|
||||
break;
|
||||
return 'WorkspaceId is required';
|
||||
case ViewSortExceptionMessageKey.VIEW_ID_REQUIRED:
|
||||
message = t`ViewId is required`;
|
||||
break;
|
||||
return 'ViewId is required';
|
||||
case ViewSortExceptionMessageKey.VIEW_SORT_NOT_FOUND:
|
||||
message = id
|
||||
? t`View sort (id: ${id}) not found`
|
||||
: t`View sort not found`;
|
||||
break;
|
||||
return `View sort${id ? ` (id: ${id})` : ''} not found`;
|
||||
case ViewSortExceptionMessageKey.INVALID_VIEW_SORT_DATA:
|
||||
message = id
|
||||
? t`Invalid view sort data for view sort id: ${id}`
|
||||
: t`Invalid view sort data`;
|
||||
break;
|
||||
return `Invalid view sort data${id ? ` for view sort id: ${id}` : ''}`;
|
||||
case ViewSortExceptionMessageKey.FIELD_METADATA_ID_REQUIRED:
|
||||
message = t`FieldMetadataId is required`;
|
||||
break;
|
||||
return 'FieldMetadataId is required';
|
||||
case ViewSortExceptionMessageKey.VIEW_NOT_FOUND:
|
||||
return `View${id ? ` (id: ${id})` : ''} not found`;
|
||||
default:
|
||||
assertUnreachable(key);
|
||||
}
|
||||
|
||||
return message;
|
||||
};
|
||||
|
||||
export const generateViewSortUserFriendlyExceptionMessage = (
|
||||
|
||||
+9
-6
@@ -6,6 +6,10 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { CreateViewSortPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/create-view-sort-permission.guard';
|
||||
import { DeleteViewSortPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/delete-view-sort-permission.guard';
|
||||
import { DestroyViewSortPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/destroy-view-sort-permission.guard';
|
||||
import { UpdateViewSortPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/update-view-sort-permission.guard';
|
||||
import { CreateViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/create-view-sort.input';
|
||||
import { UpdateViewSortInput } from 'src/engine/metadata-modules/view-sort/dtos/inputs/update-view-sort.input';
|
||||
import { ViewSortDTO } from 'src/engine/metadata-modules/view-sort/dtos/view-sort.dto';
|
||||
@@ -40,6 +44,7 @@ export class ViewSortResolver {
|
||||
}
|
||||
|
||||
@Mutation(() => ViewSortDTO)
|
||||
@UseGuards(CreateViewSortPermissionGuard)
|
||||
async createCoreViewSort(
|
||||
@Args('input') input: CreateViewSortInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@@ -51,6 +56,7 @@ export class ViewSortResolver {
|
||||
}
|
||||
|
||||
@Mutation(() => ViewSortDTO)
|
||||
@UseGuards(UpdateViewSortPermissionGuard)
|
||||
async updateCoreViewSort(
|
||||
@Args('id', { type: () => String }) id: string,
|
||||
@Args('input') input: UpdateViewSortInput,
|
||||
@@ -60,6 +66,7 @@ export class ViewSortResolver {
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
@UseGuards(DeleteViewSortPermissionGuard)
|
||||
async deleteCoreViewSort(
|
||||
@Args('id', { type: () => String }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@@ -70,15 +77,11 @@ export class ViewSortResolver {
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
@UseGuards(DestroyViewSortPermissionGuard)
|
||||
async destroyCoreViewSort(
|
||||
@Args('id', { type: () => String }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<boolean> {
|
||||
const deletedViewSort = await this.viewSortService.destroy(
|
||||
id,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
return isDefined(deletedViewSort);
|
||||
return this.viewSortService.destroy(id, workspace.id);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -295,12 +295,16 @@ describe('ViewSortService', () => {
|
||||
const id = 'view-sort-id';
|
||||
const workspaceId = 'workspace-id';
|
||||
|
||||
jest.spyOn(viewSortService, 'findById').mockResolvedValue(mockViewSort);
|
||||
jest.spyOn(viewSortRepository, 'findOne').mockResolvedValue(mockViewSort);
|
||||
jest.spyOn(viewSortRepository, 'delete').mockResolvedValue({} as any);
|
||||
|
||||
const result = await viewSortService.destroy(id, workspaceId);
|
||||
|
||||
expect(viewSortService.findById).toHaveBeenCalledWith(id, workspaceId);
|
||||
expect(viewSortRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { id, workspaceId },
|
||||
relations: ['workspace', 'view'],
|
||||
withDeleted: true,
|
||||
});
|
||||
expect(viewSortRepository.delete).toHaveBeenCalledWith(id);
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
|
||||
+8
-1
@@ -163,7 +163,14 @@ export class ViewSortService {
|
||||
}
|
||||
|
||||
async destroy(id: string, workspaceId: string): Promise<boolean> {
|
||||
const viewSort = await this.findById(id, workspaceId);
|
||||
const viewSort = await this.viewSortRepository.findOne({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
},
|
||||
relations: ['workspace', 'view'],
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
if (!isDefined(viewSort)) {
|
||||
throw new ViewSortException(
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { ViewPermissionsModule } from 'src/engine/metadata-modules/view-permissions/view-permissions.module';
|
||||
import { ViewSortController } from 'src/engine/metadata-modules/view-sort/controllers/view-sort.controller';
|
||||
import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity';
|
||||
import { ViewSortResolver } from 'src/engine/metadata-modules/view-sort/resolvers/view-sort.resolver';
|
||||
import { ViewSortService } from 'src/engine/metadata-modules/view-sort/services/view-sort.service';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([ViewSortEntity]),
|
||||
TypeOrmModule.forFeature([ViewSortEntity, ViewEntity]),
|
||||
PermissionsModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
ViewPermissionsModule,
|
||||
],
|
||||
controllers: [ViewSortController],
|
||||
providers: [ViewSortService, ViewSortResolver],
|
||||
|
||||
Reference in New Issue
Block a user