[permissions] Enforce object-records permission checks in resolvers (#10304)
Closes https://github.com/twentyhq/core-team-issues/issues/393 - enforcing object-records permission checks in resolvers for now. we will move the logic to a lower level asap - add integration tests that will still be useful when we have moved the logic - introduce guest seeded role to test limited permissions on object-records
This commit is contained in:
2
packages/twenty-server/@types/jest.d.ts
vendored
2
packages/twenty-server/@types/jest.d.ts
vendored
@ -7,6 +7,7 @@ declare module '@jest/types' {
|
||||
ADMIN_ACCESS_TOKEN: string;
|
||||
EXPIRED_ACCESS_TOKEN: string;
|
||||
MEMBER_ACCESS_TOKEN: string;
|
||||
GUEST_ACCESS_TOKEN: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -16,6 +17,7 @@ declare global {
|
||||
const ADMIN_ACCESS_TOKEN: string;
|
||||
const EXPIRED_ACCESS_TOKEN: string;
|
||||
const MEMBER_ACCESS_TOKEN: string;
|
||||
const GUEST_ACCESS_TOKEN: string;
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
@ -64,6 +64,8 @@ const jestConfig: JestConfigWithTsJest = {
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwiaWF0IjoxNzM4MzIzODc5LCJleHAiOjE3MzgzMjU2Nzl9.m73hHVpnw5uGNGrSuKxn6XtKEUK3Wqkp4HsQdYfZiHo',
|
||||
MEMBER_ACCESS_TOKEN:
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0zOTU3LTQ5MDgtOWMzNi0yOTI5YTIzZjgzNTciLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNzdkNS00Y2I2LWI2MGEtZjRhODM1YTg1ZDYxIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMzk1Ny00OTA4LTljMzYtMjkyOWEyM2Y4MzUzIiwiaWF0IjoxNzM5NDU5NTcwLCJleHAiOjMzMjk3MDU5NTcwfQ.Er7EEU4IP4YlGN79jCLR_6sUBqBfKx2M3G_qGiDpPRo',
|
||||
GUEST_ACCESS_TOKEN:
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC03MTY5LTQyY2YtYmM0Ny0xY2ZlZjE1MjY0YjgiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMTU1My00NWM2LWEwMjgtNWE5MDY0Y2NlMDdmIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtNzE2OS00MmNmLWJjNDctMWNmZWYxNTI2NGIxIiwiaWF0IjoxNzM5ODg4NDcwLCJleHAiOjMzMjk3NDg4NDcwfQ.0NEu-AWGv3l77rs-56Z5Gt0UTU7HDl6qUTHUcMWNrCc',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
|
||||
import graphqlFields from 'graphql-fields';
|
||||
import { capitalize, SettingsFeatures } from 'twenty-shared';
|
||||
import {
|
||||
capitalize,
|
||||
isObjectRecordUnderObjectRecordsPermissions,
|
||||
PermissionsOnAllObjectRecords,
|
||||
SettingsFeatures,
|
||||
} from 'twenty-shared';
|
||||
import { DataSource, ObjectLiteral } from 'typeorm';
|
||||
|
||||
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
@ -23,6 +28,7 @@ import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-quer
|
||||
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
|
||||
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||
import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
@ -92,7 +98,20 @@ export abstract class GraphqlQueryBaseResolverService<
|
||||
featureFlagsMap[FeatureFlagKey.IsPermissionsEnabled] &&
|
||||
objectMetadataItemWithFieldMaps.isSystem === true
|
||||
) {
|
||||
await this.validateSystemObjectPermissions(options);
|
||||
await this.validateSystemObjectPermissionsOrThrow(options);
|
||||
}
|
||||
|
||||
if (
|
||||
featureFlagsMap[FeatureFlagKey.IsPermissionsEnabled] &&
|
||||
isObjectRecordUnderObjectRecordsPermissions({
|
||||
isCustom: objectMetadataItemWithFieldMaps.isCustom,
|
||||
nameSingular: objectMetadataItemWithFieldMaps.nameSingular,
|
||||
})
|
||||
) {
|
||||
await this.validateCustomObjectPermissionsOrThrow({
|
||||
operationName,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
const hookedArgs =
|
||||
@ -170,7 +189,7 @@ export abstract class GraphqlQueryBaseResolverService<
|
||||
}
|
||||
}
|
||||
|
||||
private async validateSystemObjectPermissions(
|
||||
private async validateSystemObjectPermissionsOrThrow(
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
) {
|
||||
const { authContext, objectMetadataItemWithFieldMaps } = options;
|
||||
@ -210,6 +229,70 @@ export abstract class GraphqlQueryBaseResolverService<
|
||||
}
|
||||
}
|
||||
|
||||
private async validateCustomObjectPermissionsOrThrow({
|
||||
operationName,
|
||||
options,
|
||||
}: {
|
||||
operationName: WorkspaceResolverBuilderMethodNames;
|
||||
options: WorkspaceQueryRunnerOptions;
|
||||
}) {
|
||||
if (!options.authContext.apiKey) {
|
||||
if (!options.authContext.userWorkspaceId) {
|
||||
throw new AuthException(
|
||||
'Missing userWorkspaceId in authContext',
|
||||
AuthExceptionCode.USER_WORKSPACE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const requiredPermission =
|
||||
this.getRequiredPermissionForMethod(operationName);
|
||||
|
||||
const userHasPermission =
|
||||
await this.permissionsService.userHasObjectRecordsPermission({
|
||||
userWorkspaceId: options.authContext.userWorkspaceId,
|
||||
requiredPermission,
|
||||
workspaceId: options.authContext.workspace.id,
|
||||
});
|
||||
|
||||
if (!userHasPermission) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getRequiredPermissionForMethod(
|
||||
operationName: WorkspaceResolverBuilderMethodNames,
|
||||
) {
|
||||
switch (operationName) {
|
||||
case RESOLVER_METHOD_NAMES.FIND_MANY:
|
||||
case RESOLVER_METHOD_NAMES.FIND_ONE:
|
||||
case RESOLVER_METHOD_NAMES.FIND_DUPLICATES:
|
||||
case RESOLVER_METHOD_NAMES.SEARCH:
|
||||
return PermissionsOnAllObjectRecords.READ_ALL_OBJECT_RECORDS;
|
||||
case RESOLVER_METHOD_NAMES.CREATE_MANY:
|
||||
case RESOLVER_METHOD_NAMES.CREATE_ONE:
|
||||
case RESOLVER_METHOD_NAMES.UPDATE_MANY:
|
||||
case RESOLVER_METHOD_NAMES.UPDATE_ONE:
|
||||
return PermissionsOnAllObjectRecords.UPDATE_ALL_OBJECT_RECORDS;
|
||||
case RESOLVER_METHOD_NAMES.DELETE_MANY:
|
||||
case RESOLVER_METHOD_NAMES.DELETE_ONE:
|
||||
case RESOLVER_METHOD_NAMES.RESTORE_MANY:
|
||||
case RESOLVER_METHOD_NAMES.RESTORE_ONE:
|
||||
return PermissionsOnAllObjectRecords.SOFT_DELETE_ALL_OBJECT_RECORDS;
|
||||
case RESOLVER_METHOD_NAMES.DESTROY_MANY:
|
||||
case RESOLVER_METHOD_NAMES.DESTROY_ONE:
|
||||
return PermissionsOnAllObjectRecords.DESTROY_ALL_OBJECT_RECORDS;
|
||||
default:
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.UNKNOWN_OPERATION_NAME,
|
||||
PermissionsExceptionCode.UNKNOWN_OPERATION_NAME,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract resolve(
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<Input>,
|
||||
featureFlagsMap: Record<FeatureFlagKey, boolean>,
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import {
|
||||
InternalServerError,
|
||||
NotFoundError,
|
||||
UserInputError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
|
||||
export const graphqlQueryRunnerExceptionHandler = (
|
||||
error: GraphqlQueryRunnerException,
|
||||
) => {
|
||||
switch (error.code) {
|
||||
case GraphqlQueryRunnerExceptionCode.INVALID_ARGS_FIRST:
|
||||
case GraphqlQueryRunnerExceptionCode.INVALID_ARGS_LAST:
|
||||
case GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND:
|
||||
case GraphqlQueryRunnerExceptionCode.MAX_DEPTH_REACHED:
|
||||
case GraphqlQueryRunnerExceptionCode.INVALID_CURSOR:
|
||||
case GraphqlQueryRunnerExceptionCode.INVALID_DIRECTION:
|
||||
case GraphqlQueryRunnerExceptionCode.UNSUPPORTED_OPERATOR:
|
||||
case GraphqlQueryRunnerExceptionCode.ARGS_CONFLICT:
|
||||
case GraphqlQueryRunnerExceptionCode.FIELD_NOT_FOUND:
|
||||
case GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT:
|
||||
throw new UserInputError(error.message);
|
||||
case GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND:
|
||||
throw new NotFoundError(error.message);
|
||||
default:
|
||||
throw new InternalServerError(error.message);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,51 @@
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { QueryFailedError } from 'typeorm';
|
||||
|
||||
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
|
||||
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
|
||||
export const handleDuplicateKeyError = (
|
||||
error: QueryFailedError,
|
||||
context: WorkspaceQueryRunnerOptions,
|
||||
) => {
|
||||
const indexNameMatch = error.message.match(/"([^"]+)"/);
|
||||
|
||||
if (indexNameMatch) {
|
||||
const indexName = indexNameMatch[1];
|
||||
|
||||
const deletedAtFieldMetadata =
|
||||
context.objectMetadataItemWithFieldMaps.fieldsByName['deletedAt'];
|
||||
|
||||
const affectedColumns =
|
||||
context.objectMetadataItemWithFieldMaps.indexMetadatas
|
||||
.find((index) => index.name === indexName)
|
||||
?.indexFieldMetadatas?.filter(
|
||||
(field) => field.fieldMetadataId !== deletedAtFieldMetadata?.id,
|
||||
)
|
||||
.map((indexField) => {
|
||||
const fieldMetadata =
|
||||
context.objectMetadataItemWithFieldMaps.fieldsById[
|
||||
indexField.fieldMetadataId
|
||||
];
|
||||
|
||||
return fieldMetadata?.label;
|
||||
});
|
||||
|
||||
if (!isDefined(affectedColumns)) {
|
||||
throw new UserInputError(`A duplicate entry was detected`);
|
||||
}
|
||||
|
||||
const columnNames = affectedColumns.join(', ');
|
||||
|
||||
if (affectedColumns?.length === 1) {
|
||||
throw new UserInputError(
|
||||
`Duplicate ${columnNames}. Please set a unique one.`,
|
||||
);
|
||||
}
|
||||
|
||||
throw new UserInputError(
|
||||
`A duplicate entry was detected. The combination of ${columnNames} must be unique.`,
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,32 @@
|
||||
import {
|
||||
WorkspaceQueryRunnerException,
|
||||
WorkspaceQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
|
||||
import {
|
||||
ForbiddenError,
|
||||
InternalServerError,
|
||||
NotFoundError,
|
||||
TimeoutError,
|
||||
UserInputError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
|
||||
export const workspaceExceptionHandler = (
|
||||
error: WorkspaceQueryRunnerException,
|
||||
) => {
|
||||
switch (error.code) {
|
||||
case WorkspaceQueryRunnerExceptionCode.DATA_NOT_FOUND:
|
||||
throw new NotFoundError(error.message);
|
||||
case WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT:
|
||||
throw new UserInputError(error.message);
|
||||
case WorkspaceQueryRunnerExceptionCode.QUERY_VIOLATES_UNIQUE_CONSTRAINT:
|
||||
case WorkspaceQueryRunnerExceptionCode.QUERY_VIOLATES_FOREIGN_KEY_CONSTRAINT:
|
||||
case WorkspaceQueryRunnerExceptionCode.TOO_MANY_ROWS_AFFECTED:
|
||||
case WorkspaceQueryRunnerExceptionCode.NO_ROWS_AFFECTED:
|
||||
throw new ForbiddenError(error.message);
|
||||
case WorkspaceQueryRunnerExceptionCode.QUERY_TIMEOUT:
|
||||
throw new TimeoutError(error.message);
|
||||
case WorkspaceQueryRunnerExceptionCode.INTERNAL_SERVER_ERROR:
|
||||
default:
|
||||
throw new InternalServerError(error.message);
|
||||
}
|
||||
};
|
||||
@ -1,114 +1,35 @@
|
||||
import { QueryFailedError } from 'typeorm';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import {
|
||||
WorkspaceQueryRunnerException,
|
||||
WorkspaceQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
|
||||
import {
|
||||
ForbiddenError,
|
||||
InternalServerError,
|
||||
NotFoundError,
|
||||
TimeoutError,
|
||||
UserInputError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { GraphqlQueryRunnerException } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { graphqlQueryRunnerExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/graphql-query-runner-exception-handler.util';
|
||||
import { handleDuplicateKeyError } from 'src/engine/api/graphql/workspace-query-runner/utils/handle-duplicate-key-error.util';
|
||||
import { workspaceExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-exception-handler.util';
|
||||
import { WorkspaceQueryRunnerException } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
|
||||
import { PermissionsException } from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { permissionGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/permissions/utils/permission-graphql-api-exception-handler.util';
|
||||
|
||||
export const workspaceQueryRunnerGraphqlApiExceptionHandler = (
|
||||
error: Error,
|
||||
context: WorkspaceQueryRunnerOptions,
|
||||
) => {
|
||||
if (error instanceof QueryFailedError) {
|
||||
if (
|
||||
error.message.includes('duplicate key value violates unique constraint')
|
||||
) {
|
||||
const indexNameMatch = error.message.match(/"([^"]+)"/);
|
||||
|
||||
if (indexNameMatch) {
|
||||
const indexName = indexNameMatch[1];
|
||||
|
||||
const deletedAtFieldMetadata =
|
||||
context.objectMetadataItemWithFieldMaps.fieldsByName['deletedAt'];
|
||||
|
||||
const affectedColumns =
|
||||
context.objectMetadataItemWithFieldMaps.indexMetadatas
|
||||
.find((index) => index.name === indexName)
|
||||
?.indexFieldMetadatas?.filter(
|
||||
(field) => field.fieldMetadataId !== deletedAtFieldMetadata?.id,
|
||||
)
|
||||
.map((indexField) => {
|
||||
const fieldMetadata =
|
||||
context.objectMetadataItemWithFieldMaps.fieldsById[
|
||||
indexField.fieldMetadataId
|
||||
];
|
||||
|
||||
return fieldMetadata?.label;
|
||||
});
|
||||
|
||||
if (!isDefined(affectedColumns)) {
|
||||
throw new UserInputError(`A duplicate entry was detected`);
|
||||
}
|
||||
|
||||
const columnNames = affectedColumns.join(', ');
|
||||
|
||||
if (affectedColumns?.length === 1) {
|
||||
throw new UserInputError(
|
||||
`Duplicate ${columnNames}. Please set a unique one.`,
|
||||
);
|
||||
}
|
||||
|
||||
throw new UserInputError(
|
||||
`A duplicate entry was detected. The combination of ${columnNames} must be unique.`,
|
||||
);
|
||||
switch (true) {
|
||||
case error instanceof QueryFailedError: {
|
||||
if (
|
||||
error.message.includes('duplicate key value violates unique constraint')
|
||||
) {
|
||||
return handleDuplicateKeyError(error, context);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw error;
|
||||
case error instanceof PermissionsException:
|
||||
return permissionGraphqlApiExceptionHandler(error);
|
||||
case error instanceof WorkspaceQueryRunnerException:
|
||||
return workspaceExceptionHandler(error);
|
||||
case error instanceof GraphqlQueryRunnerException:
|
||||
return graphqlQueryRunnerExceptionHandler(error);
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (error instanceof WorkspaceQueryRunnerException) {
|
||||
switch (error.code) {
|
||||
case WorkspaceQueryRunnerExceptionCode.DATA_NOT_FOUND:
|
||||
throw new NotFoundError(error.message);
|
||||
case WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT:
|
||||
throw new UserInputError(error.message);
|
||||
case WorkspaceQueryRunnerExceptionCode.QUERY_VIOLATES_UNIQUE_CONSTRAINT:
|
||||
case WorkspaceQueryRunnerExceptionCode.QUERY_VIOLATES_FOREIGN_KEY_CONSTRAINT:
|
||||
case WorkspaceQueryRunnerExceptionCode.TOO_MANY_ROWS_AFFECTED:
|
||||
case WorkspaceQueryRunnerExceptionCode.NO_ROWS_AFFECTED:
|
||||
throw new ForbiddenError(error.message);
|
||||
case WorkspaceQueryRunnerExceptionCode.QUERY_TIMEOUT:
|
||||
throw new TimeoutError(error.message);
|
||||
case WorkspaceQueryRunnerExceptionCode.INTERNAL_SERVER_ERROR:
|
||||
default:
|
||||
throw new InternalServerError(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (error instanceof GraphqlQueryRunnerException) {
|
||||
switch (error.code) {
|
||||
case GraphqlQueryRunnerExceptionCode.INVALID_ARGS_FIRST:
|
||||
case GraphqlQueryRunnerExceptionCode.INVALID_ARGS_LAST:
|
||||
case GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND:
|
||||
case GraphqlQueryRunnerExceptionCode.MAX_DEPTH_REACHED:
|
||||
case GraphqlQueryRunnerExceptionCode.INVALID_CURSOR:
|
||||
case GraphqlQueryRunnerExceptionCode.INVALID_DIRECTION:
|
||||
case GraphqlQueryRunnerExceptionCode.UNSUPPORTED_OPERATOR:
|
||||
case GraphqlQueryRunnerExceptionCode.ARGS_CONFLICT:
|
||||
case GraphqlQueryRunnerExceptionCode.FIELD_NOT_FOUND:
|
||||
case GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT:
|
||||
throw new UserInputError(error.message);
|
||||
case GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND:
|
||||
throw new NotFoundError(error.message);
|
||||
default:
|
||||
throw new InternalServerError(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
};
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
export const RESOLVER_METHOD_NAMES = {
|
||||
FIND_MANY: 'findMany',
|
||||
FIND_ONE: 'findOne',
|
||||
FIND_DUPLICATES: 'findDuplicates',
|
||||
SEARCH: 'search',
|
||||
CREATE_MANY: 'createMany',
|
||||
CREATE_ONE: 'createOne',
|
||||
UPDATE_MANY: 'updateMany',
|
||||
UPDATE_ONE: 'updateOne',
|
||||
DELETE_MANY: 'deleteMany',
|
||||
DELETE_ONE: 'deleteOne',
|
||||
RESTORE_MANY: 'restoreMany',
|
||||
RESTORE_ONE: 'restoreOne',
|
||||
DESTROY_MANY: 'destroyMany',
|
||||
DESTROY_ONE: 'destroyOne',
|
||||
} as const;
|
||||
@ -9,12 +9,13 @@ import {
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
|
||||
import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
|
||||
@Injectable()
|
||||
export class CreateManyResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'createMany' as const;
|
||||
public static methodName = RESOLVER_METHOD_NAMES.CREATE_MANY;
|
||||
|
||||
constructor(
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryCreateManyResolverService,
|
||||
|
||||
@ -9,12 +9,13 @@ import {
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
|
||||
import { GraphqlQueryCreateOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
|
||||
@Injectable()
|
||||
export class CreateOneResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'createOne' as const;
|
||||
public static methodName = RESOLVER_METHOD_NAMES.CREATE_ONE;
|
||||
|
||||
constructor(
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryCreateOneResolverService,
|
||||
|
||||
@ -9,12 +9,13 @@ import {
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
|
||||
import { GraphqlQueryDeleteManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteManyResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'deleteMany' as const;
|
||||
public static methodName = RESOLVER_METHOD_NAMES.DELETE_MANY;
|
||||
|
||||
constructor(
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryDeleteManyResolverService,
|
||||
|
||||
@ -9,13 +9,13 @@ import {
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
|
||||
import { GraphqlQueryDeleteOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteOneResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'deleteOne' as const;
|
||||
|
||||
public static methodName = RESOLVER_METHOD_NAMES.DELETE_ONE;
|
||||
constructor(
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryDeleteOneResolverService,
|
||||
) {}
|
||||
|
||||
@ -9,12 +9,13 @@ import {
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
|
||||
import { GraphqlQueryDestroyManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
|
||||
@Injectable()
|
||||
export class DestroyManyResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'destroyMany' as const;
|
||||
public static methodName = RESOLVER_METHOD_NAMES.DESTROY_MANY;
|
||||
|
||||
constructor(
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryDestroyManyResolverService,
|
||||
|
||||
@ -9,12 +9,13 @@ import {
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
|
||||
import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
|
||||
@Injectable()
|
||||
export class DestroyOneResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'destroyOne' as const;
|
||||
public static methodName = RESOLVER_METHOD_NAMES.DESTROY_ONE;
|
||||
|
||||
constructor(
|
||||
private readonly graphQLQueryRunnerService: GraphqlQueryDestroyOneResolverService,
|
||||
|
||||
@ -9,12 +9,13 @@ import {
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
|
||||
import { GraphqlQueryFindDuplicatesResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
|
||||
@Injectable()
|
||||
export class FindDuplicatesResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'findDuplicates' as const;
|
||||
public static methodName = RESOLVER_METHOD_NAMES.FIND_DUPLICATES;
|
||||
|
||||
constructor(
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryFindDuplicatesResolverService,
|
||||
|
||||
@ -9,12 +9,13 @@ import {
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
|
||||
import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
|
||||
@Injectable()
|
||||
export class FindManyResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'findMany' as const;
|
||||
public static methodName = RESOLVER_METHOD_NAMES.FIND_MANY;
|
||||
|
||||
constructor(
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryFindManyResolverService,
|
||||
|
||||
@ -9,12 +9,13 @@ import {
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
|
||||
import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
|
||||
@Injectable()
|
||||
export class FindOneResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'findOne' as const;
|
||||
public static methodName = RESOLVER_METHOD_NAMES.FIND_ONE;
|
||||
|
||||
constructor(
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryFindOneResolverService,
|
||||
|
||||
@ -9,12 +9,13 @@ import {
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
|
||||
import { GraphqlQueryRestoreManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
|
||||
@Injectable()
|
||||
export class RestoreManyResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'restoreMany' as const;
|
||||
public static methodName = RESOLVER_METHOD_NAMES.RESTORE_MANY;
|
||||
|
||||
constructor(
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryRestoreManyResolverService,
|
||||
|
||||
@ -9,12 +9,13 @@ import {
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
|
||||
import { GraphqlQueryRestoreOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
|
||||
@Injectable()
|
||||
export class RestoreOneResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'restoreOne' as const;
|
||||
public static methodName = RESOLVER_METHOD_NAMES.RESTORE_ONE;
|
||||
|
||||
constructor(
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryRestoreOneResolverService,
|
||||
|
||||
@ -9,12 +9,13 @@ import {
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
|
||||
import { GraphqlQuerySearchResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
|
||||
@Injectable()
|
||||
export class SearchResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'search' as const;
|
||||
public static methodName = RESOLVER_METHOD_NAMES.SEARCH;
|
||||
|
||||
constructor(
|
||||
private readonly graphqlQueryRunnerService: GraphqlQuerySearchResolverService,
|
||||
|
||||
@ -9,13 +9,12 @@ import {
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
|
||||
import { GraphqlQueryUpdateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service';
|
||||
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
@Injectable()
|
||||
export class UpdateManyResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'updateMany' as const;
|
||||
|
||||
public static methodName = RESOLVER_METHOD_NAMES.UPDATE_MANY;
|
||||
constructor(
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryUpdateManyResolverService,
|
||||
) {}
|
||||
|
||||
@ -9,12 +9,13 @@ import {
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
|
||||
import { GraphqlQueryUpdateOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service';
|
||||
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
|
||||
|
||||
@Injectable()
|
||||
export class UpdateOneResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'updateOne' as const;
|
||||
public static methodName = RESOLVER_METHOD_NAMES.UPDATE_ONE;
|
||||
|
||||
constructor(
|
||||
private readonly graphqlQueryRunnerService: GraphqlQueryUpdateOneResolverService,
|
||||
|
||||
@ -16,6 +16,8 @@ export enum PermissionsExceptionCode {
|
||||
WORKSPACE_MEMBER_NOT_FOUND = 'WORKSPACE_MEMBER_NOT_FOUND',
|
||||
ROLE_NOT_FOUND = 'ROLE_NOT_FOUND',
|
||||
CANNOT_UNASSIGN_LAST_ADMIN = 'CANNOT_UNASSIGN_LAST_ADMIN',
|
||||
UNKNOWN_OPERATION_NAME = 'UNKNOWN_OPERATION_NAME',
|
||||
UNKNOWN_REQUIRED_PERMISSION = 'UNKNOWN_REQUIRED_PERMISSION',
|
||||
}
|
||||
|
||||
export enum PermissionsExceptionMessage {
|
||||
@ -28,4 +30,6 @@ export enum PermissionsExceptionMessage {
|
||||
WORKSPACE_MEMBER_NOT_FOUND = 'Workspace member not found',
|
||||
ROLE_NOT_FOUND = 'Role not found',
|
||||
CANNOT_UNASSIGN_LAST_ADMIN = 'Cannot unassign last admin',
|
||||
UNKNOWN_OPERATION_NAME = 'Unknown operation name, cannot determine required permission',
|
||||
UNKNOWN_REQUIRED_PERMISSION = 'Unknown required permission',
|
||||
}
|
||||
|
||||
@ -3,6 +3,12 @@ import { Injectable } from '@nestjs/common';
|
||||
import { PermissionsOnAllObjectRecords, SettingsFeatures } from 'twenty-shared';
|
||||
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import {
|
||||
PermissionsException,
|
||||
PermissionsExceptionCode,
|
||||
PermissionsExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
|
||||
@Injectable()
|
||||
@ -86,7 +92,49 @@ export class PermissionsService {
|
||||
return false;
|
||||
}
|
||||
|
||||
public async userHasObjectRecordsPermission({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
requiredPermission,
|
||||
}: {
|
||||
userWorkspaceId: string;
|
||||
workspaceId: string;
|
||||
requiredPermission: PermissionsOnAllObjectRecords;
|
||||
}): Promise<boolean> {
|
||||
const [roleOfUserWorkspace] = await this.userRoleService
|
||||
.getRolesByUserWorkspaces({
|
||||
userWorkspaceIds: [userWorkspaceId],
|
||||
workspaceId,
|
||||
})
|
||||
.then((roles) => roles?.get(userWorkspaceId) ?? []);
|
||||
|
||||
const roleColumn =
|
||||
this.getRoleColumnForRequiredPermission(requiredPermission);
|
||||
|
||||
return roleOfUserWorkspace?.[roleColumn] === true;
|
||||
}
|
||||
|
||||
public async isPermissionsEnabled(): Promise<boolean> {
|
||||
return this.environmentService.get('PERMISSIONS_ENABLED') === true;
|
||||
}
|
||||
|
||||
private getRoleColumnForRequiredPermission(
|
||||
requiredPermission: PermissionsOnAllObjectRecords,
|
||||
): keyof RoleEntity {
|
||||
switch (requiredPermission) {
|
||||
case PermissionsOnAllObjectRecords.READ_ALL_OBJECT_RECORDS:
|
||||
return 'canReadAllObjectRecords';
|
||||
case PermissionsOnAllObjectRecords.UPDATE_ALL_OBJECT_RECORDS:
|
||||
return 'canUpdateAllObjectRecords';
|
||||
case PermissionsOnAllObjectRecords.SOFT_DELETE_ALL_OBJECT_RECORDS:
|
||||
return 'canSoftDeleteAllObjectRecords';
|
||||
case PermissionsOnAllObjectRecords.DESTROY_ALL_OBJECT_RECORDS:
|
||||
return 'canDestroyAllObjectRecords';
|
||||
default:
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.UNKNOWN_REQUIRED_PERMISSION,
|
||||
PermissionsExceptionCode.UNKNOWN_REQUIRED_PERMISSION,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
import {
|
||||
ForbiddenError,
|
||||
InternalServerError,
|
||||
NotFoundError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import {
|
||||
PermissionsException,
|
||||
PermissionsExceptionCode,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
|
||||
export const permissionGraphqlApiExceptionHandler = (
|
||||
error: PermissionsException,
|
||||
) => {
|
||||
switch (error.code) {
|
||||
case PermissionsExceptionCode.PERMISSION_DENIED:
|
||||
case PermissionsExceptionCode.CANNOT_UNASSIGN_LAST_ADMIN:
|
||||
throw new ForbiddenError(error.message);
|
||||
case PermissionsExceptionCode.ROLE_NOT_FOUND:
|
||||
case PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND:
|
||||
throw new NotFoundError(error.message);
|
||||
default:
|
||||
throw new InternalServerError(error.message);
|
||||
}
|
||||
};
|
||||
@ -1,27 +1,11 @@
|
||||
import { Catch, ExceptionFilter } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
ForbiddenError,
|
||||
InternalServerError,
|
||||
NotFoundError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import {
|
||||
PermissionsException,
|
||||
PermissionsExceptionCode,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { PermissionsException } from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { permissionGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/permissions/utils/permission-graphql-api-exception-handler.util';
|
||||
|
||||
@Catch(PermissionsException)
|
||||
export class PermissionsGraphqlApiExceptionFilter implements ExceptionFilter {
|
||||
catch(exception: PermissionsException) {
|
||||
switch (exception.code) {
|
||||
case PermissionsExceptionCode.PERMISSION_DENIED:
|
||||
case PermissionsExceptionCode.CANNOT_UNASSIGN_LAST_ADMIN:
|
||||
throw new ForbiddenError(exception.message);
|
||||
case PermissionsExceptionCode.ROLE_NOT_FOUND:
|
||||
case PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND:
|
||||
throw new NotFoundError(exception.message);
|
||||
default:
|
||||
throw new InternalServerError(exception.message);
|
||||
}
|
||||
return permissionGraphqlApiExceptionHandler(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,4 +56,23 @@ export class RoleService {
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
// Only used for dev seeding and testing
|
||||
public async createGuestRole({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}): Promise<RoleEntity> {
|
||||
return this.roleRepository.save({
|
||||
label: 'Guest',
|
||||
description: 'Guest role',
|
||||
canUpdateAllSettings: false,
|
||||
canReadAllObjectRecords: true,
|
||||
canUpdateAllObjectRecords: false,
|
||||
canSoftDeleteAllObjectRecords: false,
|
||||
canDestroyAllObjectRecords: false,
|
||||
isEditable: false,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,7 +109,7 @@ export class UserRoleService {
|
||||
}: {
|
||||
userWorkspaceIds: string[];
|
||||
workspaceId: string;
|
||||
}): Promise<Map<string, RoleDTO[]>> {
|
||||
}): Promise<Map<string, RoleEntity[]>> {
|
||||
if (!userWorkspaceIds.length) {
|
||||
return new Map();
|
||||
}
|
||||
@ -128,7 +128,7 @@ export class UserRoleService {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const rolesMap = new Map<string, RoleDTO[]>();
|
||||
const rolesMap = new Map<string, RoleEntity[]>();
|
||||
|
||||
for (const userWorkspaceId of userWorkspaceIds) {
|
||||
const userWorkspaceRolesOfUserWorkspace = allUserWorkspaceRoles.filter(
|
||||
|
||||
@ -278,6 +278,17 @@ export class WorkspaceManagerService {
|
||||
if (workspaceId === SEED_APPLE_WORKSPACE_ID) {
|
||||
adminUserWorkspaceId = DEV_SEED_USER_WORKSPACE_IDS.TIM;
|
||||
memberUserWorkspaceId = DEV_SEED_USER_WORKSPACE_IDS.JONY;
|
||||
|
||||
// Create guest role only in this workspace
|
||||
const guestRole = await this.roleService.createGuestRole({
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
await this.userRoleService.assignRoleToUserWorkspace({
|
||||
workspaceId,
|
||||
userWorkspaceId: DEV_SEED_USER_WORKSPACE_IDS.PHIL,
|
||||
roleId: guestRole.id,
|
||||
});
|
||||
} else if (workspaceId === SEED_ACME_WORKSPACE_ID) {
|
||||
adminUserWorkspaceId = DEV_SEED_USER_WORKSPACE_IDS.TIM_ACME;
|
||||
}
|
||||
|
||||
@ -0,0 +1,85 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.constants';
|
||||
import { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-operation-factory.util';
|
||||
import { makeGraphqlAPIRequestWithGuestRole } from 'test/integration/graphql/utils/make-graphql-api-request-with-guest-role.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { updateFeatureFlagFactory } from 'test/integration/graphql/utils/update-feature-flag-factory.util';
|
||||
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/database/typeorm-seeds/core/workspaces';
|
||||
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
|
||||
describe('createManyObjectRecordsPermissions', () => {
|
||||
beforeAll(async () => {
|
||||
const enablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IsPermissionsEnabled',
|
||||
true,
|
||||
);
|
||||
|
||||
await makeGraphqlAPIRequest(enablePermissionsQuery);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const disablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IsPermissionsEnabled',
|
||||
false,
|
||||
);
|
||||
|
||||
await makeGraphqlAPIRequest(disablePermissionsQuery);
|
||||
});
|
||||
|
||||
it('should throw a permission error when user does not have permission (guest role)', async () => {
|
||||
const graphqlOperation = createManyOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
objectMetadataPluralName: 'people',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
data: [
|
||||
{
|
||||
id: randomUUID(),
|
||||
},
|
||||
{
|
||||
id: randomUUID(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequestWithGuestRole(graphqlOperation);
|
||||
|
||||
expect(response.body.data).toStrictEqual({ createPeople: null });
|
||||
expect(response.body.errors).toBeDefined();
|
||||
expect(response.body.errors[0].message).toBe(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
);
|
||||
expect(response.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
|
||||
});
|
||||
|
||||
it('should create multiple object records when user has permission (admin role)', async () => {
|
||||
const personId1 = randomUUID();
|
||||
const personId2 = randomUUID();
|
||||
|
||||
const graphqlOperation = createManyOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
objectMetadataPluralName: 'people',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
data: [
|
||||
{
|
||||
id: personId1,
|
||||
},
|
||||
{
|
||||
id: personId2,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||
|
||||
expect(response.body.data).toBeDefined();
|
||||
expect(response.body.data.createPeople).toBeDefined();
|
||||
expect(response.body.data.createPeople).toHaveLength(2);
|
||||
expect(response.body.data.createPeople[0].id).toBe(personId1);
|
||||
expect(response.body.data.createPeople[1].id).toBe(personId2);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,69 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.constants';
|
||||
import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util';
|
||||
import { makeGraphqlAPIRequestWithGuestRole } from 'test/integration/graphql/utils/make-graphql-api-request-with-guest-role.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { updateFeatureFlagFactory } from 'test/integration/graphql/utils/update-feature-flag-factory.util';
|
||||
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/database/typeorm-seeds/core/workspaces';
|
||||
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
|
||||
describe('createOneObjectRecordsPermissions', () => {
|
||||
beforeAll(async () => {
|
||||
const enablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IsPermissionsEnabled',
|
||||
true,
|
||||
);
|
||||
|
||||
await makeGraphqlAPIRequest(enablePermissionsQuery);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const disablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IsPermissionsEnabled',
|
||||
false,
|
||||
);
|
||||
|
||||
await makeGraphqlAPIRequest(disablePermissionsQuery);
|
||||
});
|
||||
|
||||
it('should throw a permission error when user does not have permission (guest role)', async () => {
|
||||
const graphqlOperation = createOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
data: {
|
||||
id: randomUUID(),
|
||||
},
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequestWithGuestRole(graphqlOperation);
|
||||
|
||||
expect(response.body.data).toStrictEqual({ createPerson: null });
|
||||
expect(response.body.errors).toBeDefined();
|
||||
expect(response.body.errors[0].message).toBe(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
);
|
||||
expect(response.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
|
||||
});
|
||||
|
||||
it('should create an object record when user has permission (admin role)', async () => {
|
||||
const personId = randomUUID();
|
||||
const graphqlOperation = createOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
data: {
|
||||
id: personId,
|
||||
},
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||
|
||||
expect(response.body.data).toBeDefined();
|
||||
expect(response.body.data.createPerson).toBeDefined();
|
||||
expect(response.body.data.createPerson.id).toBe(personId);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,96 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.constants';
|
||||
import { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-operation-factory.util';
|
||||
import { deleteManyOperationFactory } from 'test/integration/graphql/utils/delete-many-operation-factory.util';
|
||||
import { makeGraphqlAPIRequestWithGuestRole } from 'test/integration/graphql/utils/make-graphql-api-request-with-guest-role.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { updateFeatureFlagFactory } from 'test/integration/graphql/utils/update-feature-flag-factory.util';
|
||||
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/database/typeorm-seeds/core/workspaces';
|
||||
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
|
||||
describe('deleteManyObjectRecordsPermissions', () => {
|
||||
beforeAll(async () => {
|
||||
const enablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IsPermissionsEnabled',
|
||||
true,
|
||||
);
|
||||
|
||||
await makeGraphqlAPIRequest(enablePermissionsQuery);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const disablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IsPermissionsEnabled',
|
||||
false,
|
||||
);
|
||||
|
||||
await makeGraphqlAPIRequest(disablePermissionsQuery);
|
||||
});
|
||||
|
||||
it('should throw a permission error when user does not have permission (guest role)', async () => {
|
||||
const graphqlOperation = deleteManyOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
objectMetadataPluralName: 'people',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
filter: {
|
||||
id: {
|
||||
in: [randomUUID(), randomUUID()],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequestWithGuestRole(graphqlOperation);
|
||||
|
||||
expect(response.body.data).toStrictEqual({ deletePeople: null });
|
||||
expect(response.body.errors).toBeDefined();
|
||||
expect(response.body.errors[0].message).toBe(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
);
|
||||
expect(response.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
|
||||
});
|
||||
|
||||
it('should delete multiple object records when user has permission (admin role)', async () => {
|
||||
const personId1 = randomUUID();
|
||||
const personId2 = randomUUID();
|
||||
|
||||
const createGraphqlOperation = createManyOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
objectMetadataPluralName: 'people',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
data: [
|
||||
{
|
||||
id: personId1,
|
||||
},
|
||||
{
|
||||
id: personId2,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await makeGraphqlAPIRequest(createGraphqlOperation);
|
||||
|
||||
const deleteGraphqlOperation = deleteManyOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
objectMetadataPluralName: 'people',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
filter: {
|
||||
id: {
|
||||
in: [personId1, personId2],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequest(deleteGraphqlOperation);
|
||||
|
||||
expect(response.body.data).toBeDefined();
|
||||
expect(response.body.data.deletePeople).toBeDefined();
|
||||
expect(response.body.data.deletePeople).toHaveLength(2);
|
||||
expect(response.body.data.deletePeople[0].id).toBe(personId1);
|
||||
expect(response.body.data.deletePeople[1].id).toBe(personId2);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,78 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.constants';
|
||||
import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util';
|
||||
import { deleteOneOperationFactory } from 'test/integration/graphql/utils/delete-one-operation-factory.util';
|
||||
import { makeGraphqlAPIRequestWithGuestRole } from 'test/integration/graphql/utils/make-graphql-api-request-with-guest-role.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { updateFeatureFlagFactory } from 'test/integration/graphql/utils/update-feature-flag-factory.util';
|
||||
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/database/typeorm-seeds/core/workspaces';
|
||||
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
|
||||
describe('deleteOneObjectRecordsPermissions', () => {
|
||||
const personId = randomUUID();
|
||||
|
||||
beforeAll(async () => {
|
||||
const enablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IsPermissionsEnabled',
|
||||
true,
|
||||
);
|
||||
|
||||
await makeGraphqlAPIRequest(enablePermissionsQuery);
|
||||
|
||||
const createGraphqlOperation = createOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
data: {
|
||||
id: personId,
|
||||
},
|
||||
});
|
||||
|
||||
await makeGraphqlAPIRequest(createGraphqlOperation);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const disablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IsPermissionsEnabled',
|
||||
false,
|
||||
);
|
||||
|
||||
await makeGraphqlAPIRequest(disablePermissionsQuery);
|
||||
});
|
||||
|
||||
it('should throw a permission error when user does not have permission (guest role)', async () => {
|
||||
const personId = randomUUID();
|
||||
const graphqlOperation = deleteOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
recordId: personId,
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequestWithGuestRole(graphqlOperation);
|
||||
|
||||
expect(response.body.data).toStrictEqual({ deletePerson: null });
|
||||
expect(response.body.errors).toBeDefined();
|
||||
expect(response.body.errors[0].message).toBe(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
);
|
||||
expect(response.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
|
||||
});
|
||||
|
||||
it('should delete an object record when user has permission (admin role)', async () => {
|
||||
const deleteGraphqlOperation = deleteOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
recordId: personId,
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequest(deleteGraphqlOperation);
|
||||
|
||||
expect(response.body.data).toBeDefined();
|
||||
expect(response.body.data.deletePerson).toBeDefined();
|
||||
expect(response.body.data.deletePerson.id).toBe(personId);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,96 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.constants';
|
||||
import { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-operation-factory.util';
|
||||
import { destroyManyOperationFactory } from 'test/integration/graphql/utils/destroy-many-operation-factory.util';
|
||||
import { makeGraphqlAPIRequestWithGuestRole } from 'test/integration/graphql/utils/make-graphql-api-request-with-guest-role.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { updateFeatureFlagFactory } from 'test/integration/graphql/utils/update-feature-flag-factory.util';
|
||||
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/database/typeorm-seeds/core/workspaces';
|
||||
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
|
||||
describe('destroyManyObjectRecordsPermissions', () => {
|
||||
beforeAll(async () => {
|
||||
const enablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IsPermissionsEnabled',
|
||||
true,
|
||||
);
|
||||
|
||||
await makeGraphqlAPIRequest(enablePermissionsQuery);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const disablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IsPermissionsEnabled',
|
||||
false,
|
||||
);
|
||||
|
||||
await makeGraphqlAPIRequest(disablePermissionsQuery);
|
||||
});
|
||||
|
||||
it('should throw a permission error when user does not have permission (guest role)', async () => {
|
||||
const graphqlOperation = destroyManyOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
objectMetadataPluralName: 'people',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
filter: {
|
||||
id: {
|
||||
in: [randomUUID(), randomUUID()],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequestWithGuestRole(graphqlOperation);
|
||||
|
||||
expect(response.body.data).toStrictEqual({ destroyPeople: null });
|
||||
expect(response.body.errors).toBeDefined();
|
||||
expect(response.body.errors[0].message).toBe(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
);
|
||||
expect(response.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
|
||||
});
|
||||
|
||||
it('should destroy multiple object records when user has permission (admin role)', async () => {
|
||||
const personId1 = randomUUID();
|
||||
const personId2 = randomUUID();
|
||||
|
||||
const createGraphqlOperation = createManyOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
objectMetadataPluralName: 'people',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
data: [
|
||||
{
|
||||
id: personId1,
|
||||
},
|
||||
{
|
||||
id: personId2,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await makeGraphqlAPIRequest(createGraphqlOperation);
|
||||
|
||||
const graphqlOperation = destroyManyOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
objectMetadataPluralName: 'people',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
filter: {
|
||||
id: {
|
||||
in: [personId1, personId2],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||
|
||||
expect(response.body.data).toBeDefined();
|
||||
expect(response.body.data.destroyPeople).toBeDefined();
|
||||
expect(response.body.data.destroyPeople).toHaveLength(2);
|
||||
expect(response.body.data.destroyPeople[0].id).toBe(personId1);
|
||||
expect(response.body.data.destroyPeople[1].id).toBe(personId2);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,78 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.constants';
|
||||
import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util';
|
||||
import { destroyOneOperationFactory } from 'test/integration/graphql/utils/destroy-one-operation-factory.util';
|
||||
import { makeGraphqlAPIRequestWithGuestRole } from 'test/integration/graphql/utils/make-graphql-api-request-with-guest-role.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { updateFeatureFlagFactory } from 'test/integration/graphql/utils/update-feature-flag-factory.util';
|
||||
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/database/typeorm-seeds/core/workspaces';
|
||||
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
|
||||
describe('destroyOneObjectRecordsPermissions', () => {
|
||||
const personId = randomUUID();
|
||||
|
||||
beforeAll(async () => {
|
||||
const enablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IsPermissionsEnabled',
|
||||
true,
|
||||
);
|
||||
|
||||
await makeGraphqlAPIRequest(enablePermissionsQuery);
|
||||
|
||||
const createGraphqlOperation = createOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
data: {
|
||||
id: personId,
|
||||
},
|
||||
});
|
||||
|
||||
await makeGraphqlAPIRequest(createGraphqlOperation);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const disablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IsPermissionsEnabled',
|
||||
false,
|
||||
);
|
||||
|
||||
await makeGraphqlAPIRequest(disablePermissionsQuery);
|
||||
});
|
||||
|
||||
it('should throw a permission error when user does not have permission (guest role)', async () => {
|
||||
const personId = randomUUID();
|
||||
const graphqlOperation = destroyOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
recordId: personId,
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequestWithGuestRole(graphqlOperation);
|
||||
|
||||
expect(response.body.data).toStrictEqual({ destroyPerson: null });
|
||||
expect(response.body.errors).toBeDefined();
|
||||
expect(response.body.errors[0].message).toBe(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
);
|
||||
expect(response.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
|
||||
});
|
||||
|
||||
it('should destroy an object record when user has permission (admin role)', async () => {
|
||||
const graphqlOperation = destroyOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
recordId: personId,
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||
|
||||
expect(response.body.data).toBeDefined();
|
||||
expect(response.body.data.destroyPerson).toBeDefined();
|
||||
expect(response.body.data.destroyPerson.id).toBe(personId);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,112 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.constants';
|
||||
import { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-operation-factory.util';
|
||||
import { deleteManyOperationFactory } from 'test/integration/graphql/utils/delete-many-operation-factory.util';
|
||||
import { makeGraphqlAPIRequestWithGuestRole } from 'test/integration/graphql/utils/make-graphql-api-request-with-guest-role.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { restoreManyOperationFactory } from 'test/integration/graphql/utils/restore-many-operation-factory.util';
|
||||
import { updateFeatureFlagFactory } from 'test/integration/graphql/utils/update-feature-flag-factory.util';
|
||||
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/database/typeorm-seeds/core/workspaces';
|
||||
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
|
||||
describe('restoreManyObjectRecordsPermissions', () => {
|
||||
const personId1 = randomUUID();
|
||||
const personId2 = randomUUID();
|
||||
|
||||
beforeAll(async () => {
|
||||
const enablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IsPermissionsEnabled',
|
||||
true,
|
||||
);
|
||||
|
||||
await makeGraphqlAPIRequest(enablePermissionsQuery);
|
||||
|
||||
// Create people
|
||||
const createGraphqlOperation = createManyOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
objectMetadataPluralName: 'people',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
data: [
|
||||
{
|
||||
id: personId1,
|
||||
},
|
||||
{
|
||||
id: personId2,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await makeGraphqlAPIRequest(createGraphqlOperation);
|
||||
|
||||
// Delete people
|
||||
const deleteGraphqlOperation = deleteManyOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
objectMetadataPluralName: 'people',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
filter: {
|
||||
id: {
|
||||
in: [personId1, personId2],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await makeGraphqlAPIRequest(deleteGraphqlOperation);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const disablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IsPermissionsEnabled',
|
||||
false,
|
||||
);
|
||||
|
||||
await makeGraphqlAPIRequest(disablePermissionsQuery);
|
||||
});
|
||||
|
||||
it('should throw a permission error when user does not have permission (guest role)', async () => {
|
||||
const graphqlOperation = restoreManyOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
objectMetadataPluralName: 'people',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
filter: {
|
||||
id: {
|
||||
in: [personId1, personId2],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequestWithGuestRole(graphqlOperation);
|
||||
|
||||
expect(response.body.data).toStrictEqual({ restorePeople: null });
|
||||
expect(response.body.errors).toBeDefined();
|
||||
expect(response.body.errors[0].message).toBe(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
);
|
||||
expect(response.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
|
||||
});
|
||||
|
||||
it('should restore multiple object records when user has permission (admin role)', async () => {
|
||||
const graphqlOperation = restoreManyOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
objectMetadataPluralName: 'people',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
filter: {
|
||||
id: {
|
||||
in: [personId1, personId2],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||
|
||||
expect(response.body.data).toBeDefined();
|
||||
expect(response.body.data.restorePeople).toBeDefined();
|
||||
expect(response.body.data.restorePeople).toHaveLength(2);
|
||||
expect(response.body.data.restorePeople[0].id).toBe(personId1);
|
||||
expect(response.body.data.restorePeople[1].id).toBe(personId2);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,88 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.constants';
|
||||
import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util';
|
||||
import { deleteOneOperationFactory } from 'test/integration/graphql/utils/delete-one-operation-factory.util';
|
||||
import { makeGraphqlAPIRequestWithGuestRole } from 'test/integration/graphql/utils/make-graphql-api-request-with-guest-role.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { restoreOneOperationFactory } from 'test/integration/graphql/utils/restore-one-operation-factory.util';
|
||||
import { updateFeatureFlagFactory } from 'test/integration/graphql/utils/update-feature-flag-factory.util';
|
||||
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/database/typeorm-seeds/core/workspaces';
|
||||
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
|
||||
describe('restoreOneObjectRecordsPermissions', () => {
|
||||
const personId = randomUUID();
|
||||
|
||||
beforeAll(async () => {
|
||||
const enablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IsPermissionsEnabled',
|
||||
true,
|
||||
);
|
||||
|
||||
await makeGraphqlAPIRequest(enablePermissionsQuery);
|
||||
|
||||
// Create a person
|
||||
const createGraphqlOperation = createOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
data: {
|
||||
id: personId,
|
||||
},
|
||||
});
|
||||
|
||||
await makeGraphqlAPIRequest(createGraphqlOperation);
|
||||
|
||||
// Delete the person
|
||||
const deleteGraphqlOperation = deleteOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
recordId: personId,
|
||||
});
|
||||
|
||||
await makeGraphqlAPIRequest(deleteGraphqlOperation);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const disablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IsPermissionsEnabled',
|
||||
false,
|
||||
);
|
||||
|
||||
await makeGraphqlAPIRequest(disablePermissionsQuery);
|
||||
});
|
||||
|
||||
it('should throw a permission error when user does not have permission (guest role)', async () => {
|
||||
const graphqlOperation = restoreOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
recordId: personId,
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequestWithGuestRole(graphqlOperation);
|
||||
|
||||
expect(response.body.data).toStrictEqual({ restorePerson: null });
|
||||
expect(response.body.errors).toBeDefined();
|
||||
expect(response.body.errors[0].message).toBe(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
);
|
||||
expect(response.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
|
||||
});
|
||||
|
||||
it('should restore an object record when user has permission (admin role)', async () => {
|
||||
const graphqlOperation = restoreOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
recordId: personId,
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||
|
||||
expect(response.body.data).toBeDefined();
|
||||
expect(response.body.data.restorePerson).toBeDefined();
|
||||
expect(response.body.data.restorePerson.id).toBe(personId);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,102 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.constants';
|
||||
import { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-operation-factory.util';
|
||||
import { makeGraphqlAPIRequestWithGuestRole } from 'test/integration/graphql/utils/make-graphql-api-request-with-guest-role.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { updateFeatureFlagFactory } from 'test/integration/graphql/utils/update-feature-flag-factory.util';
|
||||
import { updateManyOperationFactory } from 'test/integration/graphql/utils/update-many-operation-factory.util';
|
||||
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/database/typeorm-seeds/core/workspaces';
|
||||
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
|
||||
describe('updateManyObjectRecordsPermissions', () => {
|
||||
const personId1 = randomUUID();
|
||||
const personId2 = randomUUID();
|
||||
|
||||
beforeAll(async () => {
|
||||
const enablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IsPermissionsEnabled',
|
||||
true,
|
||||
);
|
||||
|
||||
await makeGraphqlAPIRequest(enablePermissionsQuery);
|
||||
|
||||
const createGraphqlOperation = createManyOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
objectMetadataPluralName: 'people',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
data: [
|
||||
{
|
||||
id: personId1,
|
||||
},
|
||||
{
|
||||
id: personId2,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await makeGraphqlAPIRequest(createGraphqlOperation);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const disablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IsPermissionsEnabled',
|
||||
false,
|
||||
);
|
||||
|
||||
await makeGraphqlAPIRequest(disablePermissionsQuery);
|
||||
});
|
||||
|
||||
it('should throw a permission error when user does not have permission (guest role)', async () => {
|
||||
const graphqlOperation = updateManyOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
objectMetadataPluralName: 'people',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
filter: {
|
||||
id: {
|
||||
in: [randomUUID(), randomUUID()],
|
||||
},
|
||||
},
|
||||
data: {
|
||||
jobTitle: 'Architect',
|
||||
},
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequestWithGuestRole(graphqlOperation);
|
||||
|
||||
expect(response.body.data).toStrictEqual({ updatePeople: null });
|
||||
expect(response.body.errors).toBeDefined();
|
||||
expect(response.body.errors[0].message).toBe(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
);
|
||||
expect(response.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
|
||||
});
|
||||
|
||||
it('should update multiple object records when user has permission (admin role)', async () => {
|
||||
const graphqlOperation = updateManyOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
objectMetadataPluralName: 'people',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
filter: {
|
||||
id: {
|
||||
in: [personId1, personId2],
|
||||
},
|
||||
},
|
||||
data: {
|
||||
jobTitle: 'Architect',
|
||||
},
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||
|
||||
expect(response.body.data).toBeDefined();
|
||||
expect(response.body.data.updatePeople).toBeDefined();
|
||||
expect(response.body.data.updatePeople).toHaveLength(2);
|
||||
expect(response.body.data.updatePeople[0].jobTitle).toBe('Architect');
|
||||
expect(response.body.data.updatePeople[1].jobTitle).toBe('Architect');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,88 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.constants';
|
||||
import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util';
|
||||
import { makeGraphqlAPIRequestWithGuestRole } from 'test/integration/graphql/utils/make-graphql-api-request-with-guest-role.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { updateFeatureFlagFactory } from 'test/integration/graphql/utils/update-feature-flag-factory.util';
|
||||
import { updateOneOperationFactory } from 'test/integration/graphql/utils/update-one-operation-factory.util';
|
||||
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/database/typeorm-seeds/core/workspaces';
|
||||
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
|
||||
describe('updateOneObjectRecordsPermissions', () => {
|
||||
const personId = randomUUID();
|
||||
|
||||
beforeAll(async () => {
|
||||
const enablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IsPermissionsEnabled',
|
||||
true,
|
||||
);
|
||||
|
||||
await makeGraphqlAPIRequest(enablePermissionsQuery);
|
||||
|
||||
const createPersonOperation = createOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
data: {
|
||||
id: personId,
|
||||
jobTitle: 'Software Engineer',
|
||||
},
|
||||
});
|
||||
|
||||
await makeGraphqlAPIRequest(createPersonOperation);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const disablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IsPermissionsEnabled',
|
||||
false,
|
||||
);
|
||||
|
||||
await makeGraphqlAPIRequest(disablePermissionsQuery);
|
||||
});
|
||||
|
||||
it('should throw a permission error when user does not have permission (guest role)', async () => {
|
||||
const personId = randomUUID();
|
||||
const graphqlOperation = updateOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
recordId: personId,
|
||||
data: {
|
||||
jobTitle: 'Senior Software Engineer',
|
||||
},
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequestWithGuestRole(graphqlOperation);
|
||||
|
||||
expect(response.body.data).toStrictEqual({ updatePerson: null });
|
||||
expect(response.body.errors).toBeDefined();
|
||||
expect(response.body.errors[0].message).toBe(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
);
|
||||
expect(response.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
|
||||
});
|
||||
|
||||
it('should update an object record when user has permission (admin role)', async () => {
|
||||
const graphqlOperation = updateOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
recordId: personId,
|
||||
data: {
|
||||
jobTitle: 'Senior Software Engineer',
|
||||
},
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||
|
||||
expect(response.body.data).toBeDefined();
|
||||
expect(response.body.data.updatePerson).toBeDefined();
|
||||
expect(response.body.data.updatePerson.id).toBe(personId);
|
||||
expect(response.body.data.updatePerson.jobTitle).toBe(
|
||||
'Senior Software Engineer',
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,21 @@
|
||||
import { ASTNode, print } from 'graphql';
|
||||
import request from 'supertest';
|
||||
|
||||
type GraphqlOperation = {
|
||||
query: ASTNode;
|
||||
variables?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export const makeGraphqlAPIRequestWithGuestRole = (
|
||||
graphqlOperation: GraphqlOperation,
|
||||
) => {
|
||||
const client = request(`http://localhost:${APP_PORT}`);
|
||||
|
||||
return client
|
||||
.post('/graphql')
|
||||
.set('Authorization', `Bearer ${GUEST_ACCESS_TOKEN}`)
|
||||
.send({
|
||||
query: print(graphqlOperation.query),
|
||||
variables: graphqlOperation.variables || {},
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,27 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { capitalize } from 'twenty-shared';
|
||||
|
||||
type RestoreManyOperationFactoryParams = {
|
||||
objectMetadataSingularName: string;
|
||||
objectMetadataPluralName: string;
|
||||
gqlFields: string;
|
||||
filter: object;
|
||||
};
|
||||
|
||||
export const restoreManyOperationFactory = ({
|
||||
objectMetadataSingularName,
|
||||
objectMetadataPluralName,
|
||||
gqlFields,
|
||||
filter,
|
||||
}: RestoreManyOperationFactoryParams) => ({
|
||||
query: gql`
|
||||
mutation Restore${capitalize(objectMetadataPluralName)}($filter: ${capitalize(objectMetadataSingularName)}FilterInput!) {
|
||||
restore${capitalize(objectMetadataPluralName)}(filter: $filter) {
|
||||
${gqlFields}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
filter,
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,25 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { capitalize } from 'twenty-shared';
|
||||
|
||||
type RestoreOneOperationFactoryParams = {
|
||||
objectMetadataSingularName: string;
|
||||
gqlFields: string;
|
||||
recordId: string;
|
||||
};
|
||||
|
||||
export const restoreOneOperationFactory = ({
|
||||
objectMetadataSingularName,
|
||||
gqlFields,
|
||||
recordId,
|
||||
}: RestoreOneOperationFactoryParams) => ({
|
||||
query: gql`
|
||||
mutation Restore${capitalize(objectMetadataSingularName)}($${objectMetadataSingularName}Id: ID!) {
|
||||
restore${capitalize(objectMetadataSingularName)}(id: $${objectMetadataSingularName}Id) {
|
||||
${gqlFields}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
[`${objectMetadataSingularName}Id`]: recordId,
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,7 @@
|
||||
export const STANDARD_OBJECT_RECORDS_UNDER_OBJECT_RECORDS_PERMISSIONS = [
|
||||
'person',
|
||||
'company',
|
||||
'opportunity',
|
||||
'note',
|
||||
'task',
|
||||
];
|
||||
@ -1,6 +1,6 @@
|
||||
export * from './FieldForTotalCountAggregateOperation';
|
||||
export * from './PermissionsOnAllObjectRecords';
|
||||
export * from './SettingsFeatures';
|
||||
export * from './StandardObjectRecordsUnderObjectRecordsPermissions';
|
||||
export * from './TwentyCompaniesBaseUrl';
|
||||
export * from './TwentyIconsBaseUrl';
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export * from './fieldMetadata';
|
||||
export * from './image';
|
||||
export * from './permissions';
|
||||
export * from './strings';
|
||||
export * from './validation';
|
||||
export * from './validation';
|
||||
|
||||
1
packages/twenty-shared/src/utils/permissions/index.ts
Normal file
1
packages/twenty-shared/src/utils/permissions/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './isObjectRecordUnderObjectRecordsPermissions';
|
||||
@ -0,0 +1,16 @@
|
||||
import { STANDARD_OBJECT_RECORDS_UNDER_OBJECT_RECORDS_PERMISSIONS } from 'src/constants';
|
||||
|
||||
export const isObjectRecordUnderObjectRecordsPermissions = ({
|
||||
isCustom,
|
||||
nameSingular,
|
||||
}: {
|
||||
isCustom: boolean;
|
||||
nameSingular: string;
|
||||
}) => {
|
||||
return (
|
||||
isCustom ||
|
||||
STANDARD_OBJECT_RECORDS_UNDER_OBJECT_RECORDS_PERMISSIONS.includes(
|
||||
nameSingular,
|
||||
)
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user