[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:
+19
-30
@@ -369,12 +369,16 @@ export class ApplicationSyncService {
|
||||
private isRelationFieldManifest(
|
||||
field: FieldManifest | RelationFieldManifest,
|
||||
): field is RelationFieldManifest {
|
||||
return field.type === FieldMetadataType.RELATION;
|
||||
return this.isFieldTypeRelation(field.type);
|
||||
}
|
||||
|
||||
private async syncFields({
|
||||
private isFieldTypeRelation(type: FieldMetadataType): boolean {
|
||||
return type === FieldMetadataType.RELATION;
|
||||
}
|
||||
|
||||
private async syncFieldsWithoutRelations({
|
||||
objectId,
|
||||
fieldsToSync,
|
||||
fieldsToSync: allFieldsToSync,
|
||||
workspaceId,
|
||||
applicationId,
|
||||
}: {
|
||||
@@ -383,33 +387,14 @@ export class ApplicationSyncService {
|
||||
applicationId: string;
|
||||
fieldsToSync?: (FieldManifest | RelationFieldManifest)[];
|
||||
}) {
|
||||
if (!isDefined(fieldsToSync)) {
|
||||
if (!isDefined(allFieldsToSync)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const regularFields = fieldsToSync.filter(
|
||||
const fieldsToSync = allFieldsToSync.filter(
|
||||
(field): field is FieldManifest => !this.isRelationFieldManifest(field),
|
||||
);
|
||||
|
||||
await this.syncRegularFields({
|
||||
objectId,
|
||||
fieldsToSync: regularFields,
|
||||
workspaceId,
|
||||
applicationId,
|
||||
});
|
||||
}
|
||||
|
||||
private async syncRegularFields({
|
||||
objectId,
|
||||
fieldsToSync,
|
||||
workspaceId,
|
||||
applicationId,
|
||||
}: {
|
||||
objectId: string;
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
fieldsToSync: FieldManifest[];
|
||||
}) {
|
||||
if (fieldsToSync.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -425,7 +410,10 @@ export class ApplicationSyncService {
|
||||
const existingFields = Object.values(
|
||||
existingFlatFieldMetadataMaps.byId,
|
||||
).filter(
|
||||
(field) => isDefined(field) && field.objectMetadataId === objectId,
|
||||
(field) =>
|
||||
isDefined(field) &&
|
||||
field.objectMetadataId === objectId &&
|
||||
!this.isFieldTypeRelation(field.type),
|
||||
) as FlatFieldMetadata[];
|
||||
|
||||
const fieldsToSyncUniversalIds = fieldsToSync.map(
|
||||
@@ -440,7 +428,8 @@ export class ApplicationSyncService {
|
||||
(field) =>
|
||||
isDefined(field.universalIdentifier) &&
|
||||
!fieldsToSyncUniversalIds.includes(field.universalIdentifier) &&
|
||||
field.isCustom === true,
|
||||
field.isCustom === true &&
|
||||
field.isSystem === false,
|
||||
);
|
||||
|
||||
const fieldsToUpdate = existingFields.filter(
|
||||
@@ -690,7 +679,7 @@ export class ApplicationSyncService {
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
await this.syncFields({
|
||||
await this.syncFieldsWithoutRelations({
|
||||
fieldsToSync: objectToSync.fields,
|
||||
objectId: objectToUpdate.id,
|
||||
workspaceId,
|
||||
@@ -712,8 +701,8 @@ export class ApplicationSyncService {
|
||||
icon: objectToCreate.icon || undefined,
|
||||
description: objectToCreate.description || undefined,
|
||||
standardId: objectToCreate.universalIdentifier,
|
||||
universalIdentifier: objectToCreate.universalIdentifier,
|
||||
dataSourceId: dataSourceMetadata.id,
|
||||
universalIdentifier: objectToCreate.universalIdentifier,
|
||||
applicationId,
|
||||
};
|
||||
|
||||
@@ -723,7 +712,7 @@ export class ApplicationSyncService {
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
await this.syncFields({
|
||||
await this.syncFieldsWithoutRelations({
|
||||
fieldsToSync: objectToCreate.fields,
|
||||
objectId: createdObject.id,
|
||||
workspaceId,
|
||||
@@ -837,7 +826,7 @@ export class ApplicationSyncService {
|
||||
}
|
||||
|
||||
// Sync regular fields for this extension
|
||||
await this.syncFields({
|
||||
await this.syncFieldsWithoutRelations({
|
||||
objectId: targetObjectId,
|
||||
fieldsToSync: fields,
|
||||
workspaceId,
|
||||
|
||||
@@ -25,6 +25,27 @@ export class ApplicationService {
|
||||
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
) {}
|
||||
|
||||
async findApplicationRoleId(
|
||||
applicationId: string,
|
||||
workspaceId: string,
|
||||
): Promise<string> {
|
||||
const application = await this.applicationRepository.findOne({
|
||||
where: { id: applicationId, workspaceId },
|
||||
});
|
||||
|
||||
if (
|
||||
!isDefined(application) ||
|
||||
!isDefined(application.defaultServerlessFunctionRoleId)
|
||||
) {
|
||||
throw new ApplicationException(
|
||||
`Could not find application ${applicationId}`,
|
||||
ApplicationExceptionCode.APPLICATION_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return application.defaultServerlessFunctionRoleId;
|
||||
}
|
||||
|
||||
async findWorkspaceTwentyStandardAndCustomApplicationOrThrow({
|
||||
workspace: workspaceInput,
|
||||
workspaceId,
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { type PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
|
||||
import {
|
||||
PermissionsException,
|
||||
@@ -47,6 +47,7 @@ export const SettingsPermissionGuard = (
|
||||
setting: requiredPermission,
|
||||
workspaceId,
|
||||
apiKeyId: ctx.getContext().req.apiKey?.id,
|
||||
applicationId: ctx.getContext().req.application?.id,
|
||||
});
|
||||
|
||||
if (hasPermission === true) {
|
||||
|
||||
+5
@@ -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();
|
||||
|
||||
|
||||
+4
@@ -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',
|
||||
}
|
||||
|
||||
+4
-2
@@ -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,
|
||||
|
||||
+1
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user