[Apps] App misc - fixes + settings permissions for apps + uploadFile (#17167)

In this PR

- handle settings permission check for applications. Until then this was
unhandled and applications could not perform actions requiring settings
permissions, even if they were granted them

- fix ties to attachment, noteTarget etc: 
When an object had their fields synchronized in the app, the system
fields created as a side-effect of the object creation (relations to
noteTarget, attachment, taskTarget, favorites, timelineActivities -
created with `isCustom: true`, not sure that is correct btw) were then
deleted because they are not declared in the app, and identified as
deletable because of `isCustom: true`.
Updating the logic to exclude system fields from the logic that detects
fields to delete.
I think this outline the confusion we have around isCustom, isSystem
etc.

- introduce uploadFile util in generated twenty client as it cannot be
handled by the client's query / mutation. I had to use this for my
invoicing app
This commit is contained in:
Marie
2026-01-18 19:41:48 +01:00
committed by GitHub
parent 6070dbf16a
commit 2c596e7b1e
9 changed files with 124 additions and 37 deletions
@@ -4,6 +4,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
import { PermissionFlagType } from 'twenty-shared/constants';
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
@@ -32,6 +33,10 @@ describe('PermissionsService', () => {
provide: WorkspaceCacheService,
useValue: {},
},
{
provide: ApplicationService,
useValue: {},
},
],
}).compile();
@@ -47,6 +47,7 @@ export enum PermissionsExceptionCode {
COMPOSITE_TYPE_NOT_FOUND = 'COMPOSITE_TYPE_NOT_FOUND',
ROLE_MUST_HAVE_AT_LEAST_ONE_TARGET = 'ROLE_MUST_HAVE_AT_LEAST_ONE_TARGET',
ROLE_CANNOT_BE_ASSIGNED_TO_USERS = 'ROLE_CANNOT_BE_ASSIGNED_TO_USERS',
APPLICATION_ROLE_NOT_FOUND = 'APPLICATION_ROLE_NOT_FOUND',
}
const getPermissionsExceptionUserFriendlyMessage = (
@@ -137,6 +138,8 @@ const getPermissionsExceptionUserFriendlyMessage = (
return msg`Role must have at least one target.`;
case PermissionsExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_USERS:
return msg`This role cannot be assigned to users.`;
case PermissionsExceptionCode.APPLICATION_ROLE_NOT_FOUND:
return msg`No role assigned to the application.`;
default:
assertUnreachable(code);
}
@@ -184,4 +187,5 @@ export enum PermissionsExceptionMessage {
EMPTY_FIELD_PERMISSION_NOT_ALLOWED = 'Empty field permission not allowed',
ROLE_MUST_HAVE_AT_LEAST_ONE_TARGET = 'Role must be assignable to at least one target type',
ROLE_CANNOT_BE_ASSIGNED_TO_USERS = 'Role cannot be assigned to users',
APPLICATION_ROLE_NOT_FOUND = 'Application role not found',
}
@@ -3,9 +3,10 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { RoleTargetModule } from 'src/engine/metadata-modules/role-target/role-target.module';
@@ -19,13 +20,14 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
RoleEntity,
RoleTargetEntity,
ApiKeyEntity,
WorkspaceEntity,
ApplicationEntity,
]),
FeatureFlagModule,
TypeOrmModule.forFeature([UserWorkspaceEntity]),
UserRoleModule,
WorkspaceCacheModule,
RoleTargetModule,
ApplicationModule,
],
providers: [ApiKeyRoleService, PermissionsService],
exports: [PermissionsService, ApiKeyRoleService],
@@ -7,6 +7,7 @@ import { isDefined } from 'twenty-shared/utils';
import { In, Repository } from 'typeorm';
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { TOOL_PERMISSION_FLAGS } from 'src/engine/metadata-modules/permissions/constants/tool-permission-flags';
import {
PermissionsException,
@@ -27,6 +28,7 @@ export class PermissionsService {
private readonly apiKeyRoleService: ApiKeyRoleService,
@InjectRepository(RoleEntity)
private readonly roleRepository: Repository<RoleEntity>,
private readonly applicationService: ApplicationService,
) {}
private isToolPermission(feature: string) {
@@ -126,11 +128,13 @@ export class PermissionsService {
workspaceId,
setting,
apiKeyId,
applicationId,
}: {
userWorkspaceId?: string;
workspaceId: string;
setting: PermissionFlagType;
apiKeyId?: string;
applicationId?: string;
}): Promise<boolean> {
if (isDefined(apiKeyId)) {
const roleId = await this.apiKeyRoleService.getRoleIdForApiKeyId(
@@ -177,6 +181,31 @@ export class PermissionsService {
return this.checkRolePermissions(roleOfUserWorkspace, setting);
}
if (applicationId) {
const applicationRoleId =
await this.applicationService.findApplicationRoleId(
applicationId,
workspaceId,
);
const role = await this.roleRepository.findOne({
where: { id: applicationRoleId, workspaceId },
relations: ['permissionFlags'],
});
if (!isDefined(role)) {
throw new PermissionsException(
PermissionsExceptionMessage.APPLICATION_ROLE_NOT_FOUND,
PermissionsExceptionCode.APPLICATION_ROLE_NOT_FOUND,
{
userFriendlyMessage: msg`The application does not have a valid role assigned. Please check your application configuration.`,
},
);
}
return this.checkRolePermissions(role, setting);
}
throw new PermissionsException(
PermissionsExceptionMessage.NO_AUTHENTICATION_CONTEXT,
PermissionsExceptionCode.NO_AUTHENTICATION_CONTEXT,
@@ -69,6 +69,7 @@ export const permissionGraphqlApiExceptionHandler = (
case PermissionsExceptionCode.JOIN_COLUMN_NAME_REQUIRED:
case PermissionsExceptionCode.COMPOSITE_TYPE_NOT_FOUND:
case PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND:
case PermissionsExceptionCode.APPLICATION_ROLE_NOT_FOUND:
throw error;
default: {
return assertUnreachable(error.code);