[permissions] Add read field permission check layer (part 1) (#13376)
In this PR, behind a feature flag, we add a permission layer check based on the read permission. It is done by computing a map of an object's fields, where keys are the column names and values the fieldMetadata id, making them comparable to the restricted fields ids list stored in the permission cache. For mutations (create, update, delete, destroy), we need to check the read permission on the returned field, as they may differ from the updated field. The write field permission will be tackled in a different PR.
This commit is contained in:
@ -0,0 +1,80 @@
|
|||||||
|
import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
|
||||||
|
|
||||||
|
describe('ProcessAggregateHelper', () => {
|
||||||
|
describe('extractColumnNamesFromAggregateExpression', () => {
|
||||||
|
it('should extract column names from CONCAT expression', () => {
|
||||||
|
const selection =
|
||||||
|
'CASE WHEN COUNT(*) = 0 THEN NULL ELSE COUNT(*) - COUNT(NULLIF(CONCAT("firstName","lastName")) END, \'\')';
|
||||||
|
const result =
|
||||||
|
ProcessAggregateHelper.extractColumnNamesFromAggregateExpression(
|
||||||
|
selection,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual(['firstName', 'lastName']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract column names from CONCAT expression - 2', () => {
|
||||||
|
const selection =
|
||||||
|
'CASE WHEN COUNT(*) = 0 THEN NULL ELSE COUNT(*) - COUNT(NULLIF(CONCAT("firstName")) END, \'\')';
|
||||||
|
const result =
|
||||||
|
ProcessAggregateHelper.extractColumnNamesFromAggregateExpression(
|
||||||
|
selection,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual(['firstName']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract column names from CONCAT expression - 3', () => {
|
||||||
|
const selection =
|
||||||
|
'CASE WHEN COUNT(*) = 0 THEN NULL ELSE COUNT(*) - COUNT(NULLIF(CONCAT("firstName","lastName","nickName")) END, \'\')';
|
||||||
|
const result =
|
||||||
|
ProcessAggregateHelper.extractColumnNamesFromAggregateExpression(
|
||||||
|
selection,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual(['firstName', 'lastName', 'nickName']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract column name from non-CONCAT expression', () => {
|
||||||
|
const selection =
|
||||||
|
'CASE WHEN COUNT(*) = 0 THEN NULL ELSE COUNT("firstName") END';
|
||||||
|
const result =
|
||||||
|
ProcessAggregateHelper.extractColumnNamesFromAggregateExpression(
|
||||||
|
selection,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual(['firstName']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract column name from aggregate expression', () => {
|
||||||
|
const selection = 'AVG("amount")';
|
||||||
|
const result =
|
||||||
|
ProcessAggregateHelper.extractColumnNamesFromAggregateExpression(
|
||||||
|
selection,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual(['amount']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when no column names found', () => {
|
||||||
|
const selection = 'COUNT(*)';
|
||||||
|
const result =
|
||||||
|
ProcessAggregateHelper.extractColumnNamesFromAggregateExpression(
|
||||||
|
selection,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract column name from boolean expression', () => {
|
||||||
|
const selection =
|
||||||
|
'CASE WHEN "isActive"::boolean = TRUE THEN 1 ELSE NULL END';
|
||||||
|
const result =
|
||||||
|
ProcessAggregateHelper.extractColumnNamesFromAggregateExpression(
|
||||||
|
selection,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual(['isActive']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,5 +1,3 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
||||||
@ -7,9 +5,8 @@ import { AggregationField } from 'src/engine/api/graphql/workspace-schema-builde
|
|||||||
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
|
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
|
||||||
import { formatColumnNamesFromCompositeFieldAndSubfields } from 'src/engine/twenty-orm/utils/format-column-names-from-composite-field-and-subfield.util';
|
import { formatColumnNamesFromCompositeFieldAndSubfields } from 'src/engine/twenty-orm/utils/format-column-names-from-composite-field-and-subfield.util';
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ProcessAggregateHelper {
|
export class ProcessAggregateHelper {
|
||||||
public addSelectedAggregatedFieldsQueriesToQueryBuilder = ({
|
public static addSelectedAggregatedFieldsQueriesToQueryBuilder = ({
|
||||||
selectedAggregatedFields,
|
selectedAggregatedFields,
|
||||||
queryBuilder,
|
queryBuilder,
|
||||||
}: {
|
}: {
|
||||||
@ -110,4 +107,32 @@ export class ProcessAggregateHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static extractColumnNamesFromAggregateExpression = (
|
||||||
|
selection: string,
|
||||||
|
): string[] | null => {
|
||||||
|
// Match content between CONCAT(" and ") - handle multiple columns
|
||||||
|
const concatMatches = selection.match(
|
||||||
|
/CONCAT\("([^"]+)"(?:,"([^"]+)")*\)/g,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (concatMatches) {
|
||||||
|
// Extract all column names between quotes after CONCAT
|
||||||
|
const columnNames = selection
|
||||||
|
.match(/"([^"]+)"/g)
|
||||||
|
?.map((match) => match.slice(1, -1));
|
||||||
|
|
||||||
|
return columnNames || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-CONCAT expressions, match content between double quotes
|
||||||
|
// Using positive lookbehind and lookahead to match content between quotes without including quotes
|
||||||
|
const columnMatch = selection.match(/(?<=")([^"]+)(?=")/);
|
||||||
|
|
||||||
|
if (columnMatch) {
|
||||||
|
return [columnMatch[0]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,9 +25,7 @@ import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ProcessNestedRelationsV2Helper {
|
export class ProcessNestedRelationsV2Helper {
|
||||||
constructor(
|
constructor() {}
|
||||||
private readonly processAggregateHelper: ProcessAggregateHelper,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public async processNestedRelations<T extends ObjectRecord = ObjectRecord>({
|
public async processNestedRelations<T extends ObjectRecord = ObjectRecord>({
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
@ -324,12 +322,10 @@ export class ProcessNestedRelationsV2Helper {
|
|||||||
if (aggregateForRelation) {
|
if (aggregateForRelation) {
|
||||||
const aggregateQueryBuilder = referenceQueryBuilder.clone();
|
const aggregateQueryBuilder = referenceQueryBuilder.clone();
|
||||||
|
|
||||||
this.processAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder(
|
ProcessAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({
|
||||||
{
|
|
||||||
selectedAggregatedFields: aggregateForRelation,
|
selectedAggregatedFields: aggregateForRelation,
|
||||||
queryBuilder: aggregateQueryBuilder,
|
queryBuilder: aggregateQueryBuilder,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const aggregatedFieldsValues = await aggregateQueryBuilder
|
const aggregatedFieldsValues = await aggregateQueryBuilder
|
||||||
.addSelect(column)
|
.addSelect(column)
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import {
|
|||||||
GraphqlQueryRunnerExceptionCode,
|
GraphqlQueryRunnerExceptionCode,
|
||||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||||
|
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
|
||||||
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
|
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
|
||||||
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
||||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||||
@ -68,7 +69,19 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
|||||||
executionArgs: GraphqlQueryResolverExecutionArgs<CreateManyResolverArgs>,
|
executionArgs: GraphqlQueryResolverExecutionArgs<CreateManyResolverArgs>,
|
||||||
): Promise<InsertResult> {
|
): Promise<InsertResult> {
|
||||||
if (!executionArgs.args.upsert) {
|
if (!executionArgs.args.upsert) {
|
||||||
return await executionArgs.repository.insert(executionArgs.args.data);
|
const { objectMetadataItemWithFieldMaps } = executionArgs.options;
|
||||||
|
|
||||||
|
const selectedColumns = buildColumnsToReturn({
|
||||||
|
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||||
|
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||||
|
objectMetadataItemWithFieldMaps,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await executionArgs.repository.insert(
|
||||||
|
executionArgs.args.data,
|
||||||
|
undefined,
|
||||||
|
selectedColumns,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.performUpsertOperation(executionArgs);
|
return this.performUpsertOperation(executionArgs);
|
||||||
@ -78,12 +91,20 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
|||||||
executionArgs: GraphqlQueryResolverExecutionArgs<CreateManyResolverArgs>,
|
executionArgs: GraphqlQueryResolverExecutionArgs<CreateManyResolverArgs>,
|
||||||
): Promise<InsertResult> {
|
): Promise<InsertResult> {
|
||||||
const { objectMetadataItemWithFieldMaps } = executionArgs.options;
|
const { objectMetadataItemWithFieldMaps } = executionArgs.options;
|
||||||
|
|
||||||
|
const selectedColumns = buildColumnsToSelect({
|
||||||
|
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||||
|
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||||
|
objectMetadataItemWithFieldMaps,
|
||||||
|
});
|
||||||
|
|
||||||
const conflictingFields = this.getConflictingFields(
|
const conflictingFields = this.getConflictingFields(
|
||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
);
|
);
|
||||||
const existingRecords = await this.findExistingRecords(
|
const existingRecords = await this.findExistingRecords(
|
||||||
executionArgs,
|
executionArgs,
|
||||||
conflictingFields,
|
conflictingFields,
|
||||||
|
selectedColumns,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { recordsToUpdate, recordsToInsert } = this.categorizeRecords(
|
const { recordsToUpdate, recordsToInsert } = this.categorizeRecords(
|
||||||
@ -98,17 +119,25 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
|||||||
raw: [],
|
raw: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const columnsToReturn = buildColumnsToReturn({
|
||||||
|
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||||
|
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||||
|
objectMetadataItemWithFieldMaps,
|
||||||
|
});
|
||||||
|
|
||||||
await this.processRecordsToUpdate({
|
await this.processRecordsToUpdate({
|
||||||
partialRecordsToUpdate: recordsToUpdate,
|
partialRecordsToUpdate: recordsToUpdate,
|
||||||
repository: executionArgs.repository,
|
repository: executionArgs.repository,
|
||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
result,
|
result,
|
||||||
|
columnsToReturn,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.processRecordsToInsert({
|
await this.processRecordsToInsert({
|
||||||
recordsToInsert,
|
recordsToInsert,
|
||||||
repository: executionArgs.repository,
|
repository: executionArgs.repository,
|
||||||
result,
|
result,
|
||||||
|
columnsToReturn,
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@ -161,6 +190,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
|||||||
fullPath: string;
|
fullPath: string;
|
||||||
column: string;
|
column: string;
|
||||||
}[],
|
}[],
|
||||||
|
selectedColumns: Record<string, boolean>,
|
||||||
): Promise<Partial<ObjectRecord>[]> {
|
): Promise<Partial<ObjectRecord>[]> {
|
||||||
const { objectMetadataItemWithFieldMaps } = executionArgs.options;
|
const { objectMetadataItemWithFieldMaps } = executionArgs.options;
|
||||||
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
||||||
@ -176,7 +206,11 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
|||||||
queryBuilder.orWhere(condition);
|
queryBuilder.orWhere(condition);
|
||||||
});
|
});
|
||||||
|
|
||||||
return await queryBuilder.getMany();
|
return await queryBuilder
|
||||||
|
.setFindOptions({
|
||||||
|
select: selectedColumns,
|
||||||
|
})
|
||||||
|
.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getValueFromPath(
|
private getValueFromPath(
|
||||||
@ -265,11 +299,13 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
|||||||
repository,
|
repository,
|
||||||
objectMetadataItemWithFieldMaps,
|
objectMetadataItemWithFieldMaps,
|
||||||
result,
|
result,
|
||||||
|
columnsToReturn,
|
||||||
}: {
|
}: {
|
||||||
partialRecordsToUpdate: Partial<ObjectRecord>[];
|
partialRecordsToUpdate: Partial<ObjectRecord>[];
|
||||||
repository: WorkspaceRepository<ObjectLiteral>;
|
repository: WorkspaceRepository<ObjectLiteral>;
|
||||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||||
result: InsertResult;
|
result: InsertResult;
|
||||||
|
columnsToReturn: string[];
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
for (const partialRecordToUpdate of partialRecordsToUpdate) {
|
for (const partialRecordToUpdate of partialRecordsToUpdate) {
|
||||||
const recordId = partialRecordToUpdate.id as string;
|
const recordId = partialRecordToUpdate.id as string;
|
||||||
@ -284,6 +320,8 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
|||||||
await repository.update(
|
await repository.update(
|
||||||
recordId,
|
recordId,
|
||||||
partialRecordToUpdateWithoutCreatedByUpdate,
|
partialRecordToUpdateWithoutCreatedByUpdate,
|
||||||
|
undefined,
|
||||||
|
columnsToReturn,
|
||||||
);
|
);
|
||||||
|
|
||||||
result.identifiers.push({ id: recordId });
|
result.identifiers.push({ id: recordId });
|
||||||
@ -295,13 +333,19 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
|||||||
recordsToInsert,
|
recordsToInsert,
|
||||||
repository,
|
repository,
|
||||||
result,
|
result,
|
||||||
|
columnsToReturn,
|
||||||
}: {
|
}: {
|
||||||
recordsToInsert: Partial<ObjectRecord>[];
|
recordsToInsert: Partial<ObjectRecord>[];
|
||||||
repository: WorkspaceRepository<ObjectLiteral>;
|
repository: WorkspaceRepository<ObjectLiteral>;
|
||||||
result: InsertResult;
|
result: InsertResult;
|
||||||
|
columnsToReturn: string[];
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
if (recordsToInsert.length > 0) {
|
if (recordsToInsert.length > 0) {
|
||||||
const insertResult = await repository.insert(recordsToInsert);
|
const insertResult = await repository.insert(
|
||||||
|
recordsToInsert,
|
||||||
|
undefined,
|
||||||
|
columnsToReturn,
|
||||||
|
);
|
||||||
|
|
||||||
result.identifiers.push(...insertResult.identifiers);
|
result.identifiers.push(...insertResult.identifiers);
|
||||||
result.generatedMaps.push(...insertResult.generatedMaps);
|
result.generatedMaps.push(...insertResult.generatedMaps);
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-qu
|
|||||||
import { CreateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
import { CreateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||||
|
|
||||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||||
|
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
|
||||||
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
|
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
|
||||||
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
||||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||||
@ -29,12 +30,27 @@ export class GraphqlQueryCreateOneResolverService extends GraphqlQueryBaseResolv
|
|||||||
|
|
||||||
const { roleId } = executionArgs;
|
const { roleId } = executionArgs;
|
||||||
|
|
||||||
|
const selectedColumns = buildColumnsToReturn({
|
||||||
|
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||||
|
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||||
|
objectMetadataItemWithFieldMaps,
|
||||||
|
});
|
||||||
|
|
||||||
const objectRecords: InsertResult = !executionArgs.args.upsert
|
const objectRecords: InsertResult = !executionArgs.args.upsert
|
||||||
? await executionArgs.repository.insert(executionArgs.args.data)
|
? await executionArgs.repository.insert(
|
||||||
: await executionArgs.repository.upsert(executionArgs.args.data, {
|
executionArgs.args.data,
|
||||||
|
undefined,
|
||||||
|
selectedColumns,
|
||||||
|
)
|
||||||
|
: await executionArgs.repository.upsert(
|
||||||
|
executionArgs.args.data,
|
||||||
|
{
|
||||||
conflictPaths: ['id'],
|
conflictPaths: ['id'],
|
||||||
skipUpdateIfNoValuesChanged: true,
|
skipUpdateIfNoValuesChanged: true,
|
||||||
});
|
},
|
||||||
|
undefined,
|
||||||
|
selectedColumns,
|
||||||
|
);
|
||||||
|
|
||||||
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
||||||
objectMetadataItemWithFieldMaps.nameSingular,
|
objectMetadataItemWithFieldMaps.nameSingular,
|
||||||
|
|||||||
@ -35,7 +35,7 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve
|
|||||||
FindManyResolverArgs,
|
FindManyResolverArgs,
|
||||||
IConnection<ObjectRecord>
|
IConnection<ObjectRecord>
|
||||||
> {
|
> {
|
||||||
constructor(private readonly processAggregateHelper: ProcessAggregateHelper) {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,13 +109,11 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve
|
|||||||
appliedFilters,
|
appliedFilters,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.processAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder(
|
ProcessAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({
|
||||||
{
|
|
||||||
selectedAggregatedFields:
|
selectedAggregatedFields:
|
||||||
executionArgs.graphqlQuerySelectedFieldsResult.aggregate,
|
executionArgs.graphqlQuerySelectedFieldsResult.aggregate,
|
||||||
queryBuilder: aggregateQueryBuilder,
|
queryBuilder: aggregateQueryBuilder,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const limit =
|
const limit =
|
||||||
executionArgs.args.first ?? executionArgs.args.last ?? QUERY_MAX_RECORDS;
|
executionArgs.args.first ?? executionArgs.args.last ?? QUERY_MAX_RECORDS;
|
||||||
|
|||||||
@ -255,6 +255,10 @@ export class WorkspacePermissionsCacheService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (const fieldPermission of fieldPermissions) {
|
for (const fieldPermission of fieldPermissions) {
|
||||||
|
if (
|
||||||
|
isDefined(fieldPermission.canReadFieldValue) ||
|
||||||
|
isDefined(fieldPermission.canUpdateFieldValue)
|
||||||
|
) {
|
||||||
restrictedFields[fieldPermission.fieldMetadataId] = {
|
restrictedFields[fieldPermission.fieldMetadataId] = {
|
||||||
canRead: fieldPermission.canReadFieldValue,
|
canRead: fieldPermission.canReadFieldValue,
|
||||||
canUpdate: fieldPermission.canUpdateFieldValue,
|
canUpdate: fieldPermission.canUpdateFieldValue,
|
||||||
@ -262,6 +266,7 @@ export class WorkspacePermissionsCacheService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
objectRecordsPermissions[objectMetadataId] = {
|
objectRecordsPermissions[objectMetadataId] = {
|
||||||
canRead,
|
canRead,
|
||||||
|
|||||||
@ -22,6 +22,7 @@ const mockedWorkspaceUpdateQueryBuilder = {
|
|||||||
execute: jest
|
execute: jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockResolvedValue({ affected: 1, raw: [], generatedMaps: [] }),
|
.mockResolvedValue({ affected: 1, raw: [], generatedMaps: [] }),
|
||||||
|
returning: jest.fn().mockReturnThis(),
|
||||||
})),
|
})),
|
||||||
execute: jest
|
execute: jest
|
||||||
.fn()
|
.fn()
|
||||||
@ -38,6 +39,7 @@ jest.mock('../repository/workspace-select-query-builder', () => ({
|
|||||||
.fn()
|
.fn()
|
||||||
.mockResolvedValue({ affected: 1, raw: [], generatedMaps: [] }),
|
.mockResolvedValue({ affected: 1, raw: [], generatedMaps: [] }),
|
||||||
setFindOptions: jest.fn().mockReturnThis(),
|
setFindOptions: jest.fn().mockReturnThis(),
|
||||||
|
returning: jest.fn().mockReturnThis(),
|
||||||
update: jest.fn().mockReturnValue(mockedWorkspaceUpdateQueryBuilder),
|
update: jest.fn().mockReturnValue(mockedWorkspaceUpdateQueryBuilder),
|
||||||
insert: jest.fn().mockReturnThis(),
|
insert: jest.fn().mockReturnThis(),
|
||||||
})),
|
})),
|
||||||
@ -269,17 +271,20 @@ describe('WorkspaceEntityManager', () => {
|
|||||||
{ reload: false },
|
{ reload: false },
|
||||||
mockPermissionOptions,
|
mockPermissionOptions,
|
||||||
);
|
);
|
||||||
expect(entityManager['validatePermissions']).toHaveBeenCalledWith(
|
expect(entityManager['validatePermissions']).toHaveBeenCalledWith({
|
||||||
'test-entity',
|
target: 'test-entity',
|
||||||
'update',
|
operationType: 'update',
|
||||||
mockPermissionOptions,
|
permissionOptions: mockPermissionOptions,
|
||||||
);
|
selectedColumns: [],
|
||||||
|
});
|
||||||
expect(validateOperationIsPermittedOrThrow).toHaveBeenCalledWith({
|
expect(validateOperationIsPermittedOrThrow).toHaveBeenCalledWith({
|
||||||
entityName: 'test-entity',
|
entityName: 'test-entity',
|
||||||
operationType: 'update',
|
operationType: 'update',
|
||||||
objectMetadataMaps: mockInternalContext.objectMetadataMaps,
|
objectMetadataMaps: mockInternalContext.objectMetadataMaps,
|
||||||
objectRecordsPermissions:
|
objectRecordsPermissions:
|
||||||
mockPermissionOptions.objectRecordsPermissions,
|
mockPermissionOptions.objectRecordsPermissions,
|
||||||
|
selectedColumns: [],
|
||||||
|
allFieldsSelected: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -299,17 +304,20 @@ describe('WorkspaceEntityManager', () => {
|
|||||||
describe('Other Methods', () => {
|
describe('Other Methods', () => {
|
||||||
it('should call validatePermissions and validateOperationIsPermittedOrThrow for clear', async () => {
|
it('should call validatePermissions and validateOperationIsPermittedOrThrow for clear', async () => {
|
||||||
await entityManager.clear('test-entity', mockPermissionOptions);
|
await entityManager.clear('test-entity', mockPermissionOptions);
|
||||||
expect(entityManager['validatePermissions']).toHaveBeenCalledWith(
|
expect(entityManager['validatePermissions']).toHaveBeenCalledWith({
|
||||||
'test-entity',
|
target: 'test-entity',
|
||||||
'delete',
|
operationType: 'delete',
|
||||||
mockPermissionOptions,
|
permissionOptions: mockPermissionOptions,
|
||||||
);
|
selectedColumns: [],
|
||||||
|
});
|
||||||
expect(validateOperationIsPermittedOrThrow).toHaveBeenCalledWith({
|
expect(validateOperationIsPermittedOrThrow).toHaveBeenCalledWith({
|
||||||
entityName: 'test-entity',
|
entityName: 'test-entity',
|
||||||
operationType: 'delete',
|
operationType: 'delete',
|
||||||
objectMetadataMaps: mockInternalContext.objectMetadataMaps,
|
objectMetadataMaps: mockInternalContext.objectMetadataMaps,
|
||||||
objectRecordsPermissions:
|
objectRecordsPermissions:
|
||||||
mockPermissionOptions.objectRecordsPermissions,
|
mockPermissionOptions.objectRecordsPermissions,
|
||||||
|
selectedColumns: [],
|
||||||
|
allFieldsSelected: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -164,6 +164,8 @@ export class WorkspaceEntityManager extends EntityManager {
|
|||||||
options?.objectRecordsPermissions ?? {},
|
options?.objectRecordsPermissions ?? {},
|
||||||
this.internalContext,
|
this.internalContext,
|
||||||
options?.shouldBypassPermissionChecks ?? false,
|
options?.shouldBypassPermissionChecks ?? false,
|
||||||
|
undefined,
|
||||||
|
this.getFeatureFlagMap(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,6 +174,7 @@ export class WorkspaceEntityManager extends EntityManager {
|
|||||||
entity:
|
entity:
|
||||||
| QueryDeepPartialEntityWithRelationConnect<Entity>
|
| QueryDeepPartialEntityWithRelationConnect<Entity>
|
||||||
| QueryDeepPartialEntityWithRelationConnect<Entity>[],
|
| QueryDeepPartialEntityWithRelationConnect<Entity>[],
|
||||||
|
selectedColumns: string[] = [],
|
||||||
permissionOptions?: PermissionOptions,
|
permissionOptions?: PermissionOptions,
|
||||||
): Promise<InsertResult> {
|
): Promise<InsertResult> {
|
||||||
const entityArray = Array.isArray(entity) ? entity : [entity];
|
const entityArray = Array.isArray(entity) ? entity : [entity];
|
||||||
@ -191,6 +194,7 @@ export class WorkspaceEntityManager extends EntityManager {
|
|||||||
.insert()
|
.insert()
|
||||||
.into(target)
|
.into(target)
|
||||||
.values(connectedEntities)
|
.values(connectedEntities)
|
||||||
|
.returning(selectedColumns)
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,6 +208,7 @@ export class WorkspaceEntityManager extends EntityManager {
|
|||||||
shouldBypassPermissionChecks?: boolean;
|
shouldBypassPermissionChecks?: boolean;
|
||||||
objectRecordsPermissions?: ObjectRecordsPermissions;
|
objectRecordsPermissions?: ObjectRecordsPermissions;
|
||||||
},
|
},
|
||||||
|
selectedColumns: string[] = [],
|
||||||
): Promise<InsertResult> {
|
): Promise<InsertResult> {
|
||||||
const metadata = this.connection.getMetadata(target);
|
const metadata = this.connection.getMetadata(target);
|
||||||
let options;
|
let options;
|
||||||
@ -257,6 +262,7 @@ export class WorkspaceEntityManager extends EntityManager {
|
|||||||
this.connection.driver.supportedUpsertTypes[0],
|
this.connection.driver.supportedUpsertTypes[0],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
.returning(selectedColumns)
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,6 +280,7 @@ export class WorkspaceEntityManager extends EntityManager {
|
|||||||
| unknown,
|
| unknown,
|
||||||
partialEntity: QueryDeepPartialEntity<Entity>,
|
partialEntity: QueryDeepPartialEntity<Entity>,
|
||||||
permissionOptions?: PermissionOptions,
|
permissionOptions?: PermissionOptions,
|
||||||
|
selectedColumns: string[] = [],
|
||||||
): Promise<UpdateResult> {
|
): Promise<UpdateResult> {
|
||||||
const metadata = this.connection.getMetadata(target);
|
const metadata = this.connection.getMetadata(target);
|
||||||
|
|
||||||
@ -304,6 +311,7 @@ export class WorkspaceEntityManager extends EntityManager {
|
|||||||
.update()
|
.update()
|
||||||
.set(partialEntity)
|
.set(partialEntity)
|
||||||
.whereInIds(criteria)
|
.whereInIds(criteria)
|
||||||
|
.returning(selectedColumns)
|
||||||
.execute();
|
.execute();
|
||||||
} else {
|
} else {
|
||||||
return this.createQueryBuilder(
|
return this.createQueryBuilder(
|
||||||
@ -315,6 +323,7 @@ export class WorkspaceEntityManager extends EntityManager {
|
|||||||
.update()
|
.update()
|
||||||
.set(partialEntity)
|
.set(partialEntity)
|
||||||
.where(criteria)
|
.where(criteria)
|
||||||
|
.returning(selectedColumns)
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -325,6 +334,7 @@ export class WorkspaceEntityManager extends EntityManager {
|
|||||||
propertyPath: string,
|
propertyPath: string,
|
||||||
value: number | string,
|
value: number | string,
|
||||||
permissionOptions?: PermissionOptions,
|
permissionOptions?: PermissionOptions,
|
||||||
|
selectedColumns: string[] = [],
|
||||||
): Promise<UpdateResult> {
|
): Promise<UpdateResult> {
|
||||||
const metadata = this.connection.getMetadata(target);
|
const metadata = this.connection.getMetadata(target);
|
||||||
const column = metadata.findColumnWithPropertyPath(propertyPath);
|
const column = metadata.findColumnWithPropertyPath(propertyPath);
|
||||||
@ -351,17 +361,24 @@ export class WorkspaceEntityManager extends EntityManager {
|
|||||||
.update(target as QueryDeepPartialEntity<Entity>)
|
.update(target as QueryDeepPartialEntity<Entity>)
|
||||||
.set(values)
|
.set(values)
|
||||||
.where(criteria)
|
.where(criteria)
|
||||||
|
.returning(selectedColumns)
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
validatePermissions<Entity extends ObjectLiteral>(
|
validatePermissions<Entity extends ObjectLiteral>({
|
||||||
target: EntityTarget<Entity> | Entity,
|
target,
|
||||||
operationType: OperationType,
|
operationType,
|
||||||
|
permissionOptions,
|
||||||
|
selectedColumns,
|
||||||
|
}: {
|
||||||
|
target: EntityTarget<Entity> | Entity;
|
||||||
|
operationType: OperationType;
|
||||||
permissionOptions?: {
|
permissionOptions?: {
|
||||||
shouldBypassPermissionChecks?: boolean;
|
shouldBypassPermissionChecks?: boolean;
|
||||||
objectRecordsPermissions?: ObjectRecordsPermissions;
|
objectRecordsPermissions?: ObjectRecordsPermissions;
|
||||||
},
|
};
|
||||||
): void {
|
selectedColumns: string[];
|
||||||
|
}): void {
|
||||||
if (permissionOptions?.shouldBypassPermissionChecks === true) {
|
if (permissionOptions?.shouldBypassPermissionChecks === true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -377,6 +394,8 @@ export class WorkspaceEntityManager extends EntityManager {
|
|||||||
objectRecordsPermissions:
|
objectRecordsPermissions:
|
||||||
permissionOptions?.objectRecordsPermissions ?? {},
|
permissionOptions?.objectRecordsPermissions ?? {},
|
||||||
objectMetadataMaps: this.internalContext.objectMetadataMaps,
|
objectMetadataMaps: this.internalContext.objectMetadataMaps,
|
||||||
|
selectedColumns,
|
||||||
|
allFieldsSelected: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -858,7 +877,12 @@ export class WorkspaceEntityManager extends EntityManager {
|
|||||||
entityClass: EntityTarget<Entity>,
|
entityClass: EntityTarget<Entity>,
|
||||||
permissionOptions?: PermissionOptions,
|
permissionOptions?: PermissionOptions,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.validatePermissions(entityClass, 'delete', permissionOptions);
|
this.validatePermissions({
|
||||||
|
target: entityClass,
|
||||||
|
operationType: 'delete',
|
||||||
|
permissionOptions,
|
||||||
|
selectedColumns: [], // TODO
|
||||||
|
});
|
||||||
|
|
||||||
return super.clear(entityClass);
|
return super.clear(entityClass);
|
||||||
}
|
}
|
||||||
@ -910,6 +934,7 @@ export class WorkspaceEntityManager extends EntityManager {
|
|||||||
propertyPath: string,
|
propertyPath: string,
|
||||||
value: number | string,
|
value: number | string,
|
||||||
permissionOptions?: PermissionOptions,
|
permissionOptions?: PermissionOptions,
|
||||||
|
selectedColumns: string[] = [],
|
||||||
): Promise<UpdateResult> {
|
): Promise<UpdateResult> {
|
||||||
const metadata = this.connection.getMetadata(target);
|
const metadata = this.connection.getMetadata(target);
|
||||||
const column = metadata.findColumnWithPropertyPath(propertyPath);
|
const column = metadata.findColumnWithPropertyPath(propertyPath);
|
||||||
@ -935,6 +960,7 @@ export class WorkspaceEntityManager extends EntityManager {
|
|||||||
.update(target as QueryDeepPartialEntity<Entity>)
|
.update(target as QueryDeepPartialEntity<Entity>)
|
||||||
.set(values)
|
.set(values)
|
||||||
.where(criteria)
|
.where(criteria)
|
||||||
|
.returning(selectedColumns)
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1029,11 +1055,12 @@ export class WorkspaceEntityManager extends EntityManager {
|
|||||||
? maybeOptionsOrMaybePermissionOptions
|
? maybeOptionsOrMaybePermissionOptions
|
||||||
: permissionOptions;
|
: permissionOptions;
|
||||||
|
|
||||||
this.validatePermissions(
|
this.validatePermissions({
|
||||||
targetOrEntity,
|
target: targetOrEntity,
|
||||||
'update',
|
operationType: 'update',
|
||||||
permissionOptionsFromArgs,
|
permissionOptions: permissionOptionsFromArgs,
|
||||||
);
|
selectedColumns: [], // TODO
|
||||||
|
});
|
||||||
|
|
||||||
let target =
|
let target =
|
||||||
arguments.length > 1 &&
|
arguments.length > 1 &&
|
||||||
@ -1170,11 +1197,12 @@ export class WorkspaceEntityManager extends EntityManager {
|
|||||||
? (maybeOptionsOrMaybePermissionOptions as PermissionOptions)
|
? (maybeOptionsOrMaybePermissionOptions as PermissionOptions)
|
||||||
: permissionOptions;
|
: permissionOptions;
|
||||||
|
|
||||||
this.validatePermissions(
|
this.validatePermissions({
|
||||||
targetOrEntity,
|
target: targetOrEntity,
|
||||||
'delete',
|
operationType: 'delete',
|
||||||
permissionOptionsFromArgs,
|
permissionOptions: permissionOptionsFromArgs,
|
||||||
);
|
selectedColumns: [], // TODO
|
||||||
|
});
|
||||||
|
|
||||||
const target =
|
const target =
|
||||||
arguments.length > 1 &&
|
arguments.length > 1 &&
|
||||||
@ -1281,11 +1309,12 @@ export class WorkspaceEntityManager extends EntityManager {
|
|||||||
? (maybeOptionsOrMaybePermissionOptions as PermissionOptions)
|
? (maybeOptionsOrMaybePermissionOptions as PermissionOptions)
|
||||||
: permissionOptions;
|
: permissionOptions;
|
||||||
|
|
||||||
this.validatePermissions(
|
this.validatePermissions({
|
||||||
targetOrEntityOrEntities,
|
target: targetOrEntityOrEntities,
|
||||||
'soft-delete',
|
operationType: 'soft-delete',
|
||||||
permissionOptionsFromArgs,
|
permissionOptions: permissionOptionsFromArgs,
|
||||||
);
|
selectedColumns: [], // TODO
|
||||||
|
});
|
||||||
|
|
||||||
let target =
|
let target =
|
||||||
arguments.length > 1 &&
|
arguments.length > 1 &&
|
||||||
@ -1387,11 +1416,12 @@ export class WorkspaceEntityManager extends EntityManager {
|
|||||||
? (maybeOptionsOrMaybePermissionOptions as PermissionOptions)
|
? (maybeOptionsOrMaybePermissionOptions as PermissionOptions)
|
||||||
: permissionOptions;
|
: permissionOptions;
|
||||||
|
|
||||||
this.validatePermissions(
|
this.validatePermissions({
|
||||||
targetOrEntityOrEntities,
|
target: targetOrEntityOrEntities,
|
||||||
'restore',
|
operationType: 'restore',
|
||||||
permissionOptionsFromArgs,
|
permissionOptions: permissionOptionsFromArgs,
|
||||||
);
|
selectedColumns: [], // TODO
|
||||||
|
});
|
||||||
|
|
||||||
let target =
|
let target =
|
||||||
arguments.length > 1 &&
|
arguments.length > 1 &&
|
||||||
|
|||||||
@ -268,6 +268,7 @@ describe('WorkspaceRepository', () => {
|
|||||||
expect(mockEntityManager.insert).toHaveBeenCalledWith(
|
expect(mockEntityManager.insert).toHaveBeenCalledWith(
|
||||||
'test-entity',
|
'test-entity',
|
||||||
{ id: 'test-id' },
|
{ id: 'test-id' },
|
||||||
|
undefined,
|
||||||
{
|
{
|
||||||
shouldBypassPermissionChecks: false,
|
shouldBypassPermissionChecks: false,
|
||||||
objectRecordsPermissions: mockObjectRecordsPermissions,
|
objectRecordsPermissions: mockObjectRecordsPermissions,
|
||||||
@ -294,6 +295,7 @@ describe('WorkspaceRepository', () => {
|
|||||||
shouldBypassPermissionChecks: false,
|
shouldBypassPermissionChecks: false,
|
||||||
objectRecordsPermissions: mockObjectRecordsPermissions,
|
objectRecordsPermissions: mockObjectRecordsPermissions,
|
||||||
},
|
},
|
||||||
|
[],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -319,6 +321,7 @@ describe('WorkspaceRepository', () => {
|
|||||||
shouldBypassPermissionChecks: false,
|
shouldBypassPermissionChecks: false,
|
||||||
objectRecordsPermissions: mockObjectRecordsPermissions,
|
objectRecordsPermissions: mockObjectRecordsPermissions,
|
||||||
},
|
},
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -362,6 +365,7 @@ describe('WorkspaceRepository', () => {
|
|||||||
shouldBypassPermissionChecks: false,
|
shouldBypassPermissionChecks: false,
|
||||||
objectRecordsPermissions: mockObjectRecordsPermissions,
|
objectRecordsPermissions: mockObjectRecordsPermissions,
|
||||||
},
|
},
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,14 +1,21 @@
|
|||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
import { ObjectRecordsPermissions } from 'twenty-shared/types';
|
import isEmpty from 'lodash.isempty';
|
||||||
|
import {
|
||||||
|
ObjectRecordsPermissions,
|
||||||
|
RestrictedFields,
|
||||||
|
} from 'twenty-shared/types';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { QueryExpressionMap } from 'typeorm/query-builder/QueryExpressionMap';
|
import { QueryExpressionMap } from 'typeorm/query-builder/QueryExpressionMap';
|
||||||
|
|
||||||
|
import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
|
||||||
|
import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||||
import {
|
import {
|
||||||
PermissionsException,
|
PermissionsException,
|
||||||
PermissionsExceptionCode,
|
PermissionsExceptionCode,
|
||||||
PermissionsExceptionMessage,
|
PermissionsExceptionMessage,
|
||||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||||
|
import { getFieldMetadataIdForColumnNameMap } from 'src/engine/twenty-orm/utils/get-field-metadata-id-for-column-name.util';
|
||||||
|
|
||||||
const getTargetEntityAndOperationType = (expressionMap: QueryExpressionMap) => {
|
const getTargetEntityAndOperationType = (expressionMap: QueryExpressionMap) => {
|
||||||
const mainEntity = expressionMap.aliases[0].metadata.name;
|
const mainEntity = expressionMap.aliases[0].metadata.name;
|
||||||
@ -33,11 +40,17 @@ export const validateOperationIsPermittedOrThrow = ({
|
|||||||
operationType,
|
operationType,
|
||||||
objectRecordsPermissions,
|
objectRecordsPermissions,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
|
selectedColumns,
|
||||||
|
isFieldPermissionsEnabled,
|
||||||
|
allFieldsSelected,
|
||||||
}: {
|
}: {
|
||||||
entityName: string;
|
entityName: string;
|
||||||
operationType: OperationType;
|
operationType: OperationType;
|
||||||
objectRecordsPermissions: ObjectRecordsPermissions;
|
objectRecordsPermissions: ObjectRecordsPermissions;
|
||||||
objectMetadataMaps: ObjectMetadataMaps;
|
objectMetadataMaps: ObjectMetadataMaps;
|
||||||
|
selectedColumns: string[];
|
||||||
|
isFieldPermissionsEnabled?: boolean;
|
||||||
|
allFieldsSelected: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const objectMetadataIdForEntity =
|
const objectMetadataIdForEntity =
|
||||||
objectMetadataMaps.idByNameSingular[entityName];
|
objectMetadataMaps.idByNameSingular[entityName];
|
||||||
@ -64,6 +77,10 @@ export const validateOperationIsPermittedOrThrow = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fieldMetadataIdForColumnNameMap = isFieldPermissionsEnabled
|
||||||
|
? getFieldMetadataIdForColumnNameMap(objectMetadata)
|
||||||
|
: {};
|
||||||
|
|
||||||
const permissionsForEntity =
|
const permissionsForEntity =
|
||||||
objectRecordsPermissions[objectMetadataIdForEntity];
|
objectRecordsPermissions[objectMetadataIdForEntity];
|
||||||
|
|
||||||
@ -75,6 +92,15 @@ export const validateOperationIsPermittedOrThrow = ({
|
|||||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isFieldPermissionsEnabled) {
|
||||||
|
validateReadFieldPermissionOrThrow({
|
||||||
|
restrictedFields: permissionsForEntity.restrictedFields,
|
||||||
|
selectedColumns,
|
||||||
|
fieldMetadataIdForColumnNameMap,
|
||||||
|
allFieldsSelected,
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'insert':
|
case 'insert':
|
||||||
case 'update':
|
case 'update':
|
||||||
@ -84,6 +110,14 @@ export const validateOperationIsPermittedOrThrow = ({
|
|||||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isFieldPermissionsEnabled) {
|
||||||
|
validateReadFieldPermissionOrThrow({
|
||||||
|
restrictedFields: permissionsForEntity.restrictedFields,
|
||||||
|
selectedColumns,
|
||||||
|
fieldMetadataIdForColumnNameMap,
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
if (!permissionsForEntity?.canDestroy) {
|
if (!permissionsForEntity?.canDestroy) {
|
||||||
@ -92,6 +126,14 @@ export const validateOperationIsPermittedOrThrow = ({
|
|||||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isFieldPermissionsEnabled) {
|
||||||
|
validateReadFieldPermissionOrThrow({
|
||||||
|
restrictedFields: permissionsForEntity.restrictedFields,
|
||||||
|
selectedColumns,
|
||||||
|
fieldMetadataIdForColumnNameMap,
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'restore':
|
case 'restore':
|
||||||
case 'soft-delete':
|
case 'soft-delete':
|
||||||
@ -101,6 +143,14 @@ export const validateOperationIsPermittedOrThrow = ({
|
|||||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isFieldPermissionsEnabled) {
|
||||||
|
validateReadFieldPermissionOrThrow({
|
||||||
|
restrictedFields: permissionsForEntity.restrictedFields,
|
||||||
|
selectedColumns,
|
||||||
|
fieldMetadataIdForColumnNameMap,
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new PermissionsException(
|
throw new PermissionsException(
|
||||||
@ -108,14 +158,25 @@ export const validateOperationIsPermittedOrThrow = ({
|
|||||||
PermissionsExceptionCode.UNKNOWN_OPERATION_NAME,
|
PermissionsExceptionCode.UNKNOWN_OPERATION_NAME,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isEmpty(permissionsForEntity.restrictedFields)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const validateQueryIsPermittedOrThrow = (
|
export const validateQueryIsPermittedOrThrow = ({
|
||||||
expressionMap: QueryExpressionMap,
|
expressionMap,
|
||||||
objectRecordsPermissions: ObjectRecordsPermissions,
|
objectRecordsPermissions,
|
||||||
objectMetadataMaps: ObjectMetadataMaps,
|
objectMetadataMaps,
|
||||||
shouldBypassPermissionChecks: boolean,
|
shouldBypassPermissionChecks,
|
||||||
) => {
|
isFieldPermissionsEnabled,
|
||||||
|
}: {
|
||||||
|
expressionMap: QueryExpressionMap;
|
||||||
|
objectRecordsPermissions: ObjectRecordsPermissions;
|
||||||
|
objectMetadataMaps: ObjectMetadataMaps;
|
||||||
|
shouldBypassPermissionChecks: boolean;
|
||||||
|
isFieldPermissionsEnabled?: boolean;
|
||||||
|
}) => {
|
||||||
if (shouldBypassPermissionChecks) {
|
if (shouldBypassPermissionChecks) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -123,10 +184,119 @@ export const validateQueryIsPermittedOrThrow = (
|
|||||||
const { mainEntity, operationType } =
|
const { mainEntity, operationType } =
|
||||||
getTargetEntityAndOperationType(expressionMap);
|
getTargetEntityAndOperationType(expressionMap);
|
||||||
|
|
||||||
|
const allFieldsSelected = expressionMap.selects.some(
|
||||||
|
(select) => select.selection === mainEntity,
|
||||||
|
);
|
||||||
|
|
||||||
|
let selectedColumns: string[] = [];
|
||||||
|
|
||||||
|
if (isFieldPermissionsEnabled) {
|
||||||
|
selectedColumns = getSelectedColumnsFromExpressionMap({
|
||||||
|
operationType,
|
||||||
|
expressionMap,
|
||||||
|
allFieldsSelected,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
validateOperationIsPermittedOrThrow({
|
validateOperationIsPermittedOrThrow({
|
||||||
entityName: mainEntity,
|
entityName: mainEntity,
|
||||||
operationType: operationType as OperationType,
|
operationType: operationType as OperationType,
|
||||||
objectRecordsPermissions,
|
objectRecordsPermissions,
|
||||||
objectMetadataMaps,
|
objectMetadataMaps,
|
||||||
|
selectedColumns: selectedColumns,
|
||||||
|
isFieldPermissionsEnabled,
|
||||||
|
allFieldsSelected,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validateReadFieldPermissionOrThrow = ({
|
||||||
|
restrictedFields,
|
||||||
|
selectedColumns,
|
||||||
|
fieldMetadataIdForColumnNameMap,
|
||||||
|
allFieldsSelected,
|
||||||
|
}: {
|
||||||
|
restrictedFields: RestrictedFields;
|
||||||
|
selectedColumns: string[];
|
||||||
|
fieldMetadataIdForColumnNameMap: Record<string, string>;
|
||||||
|
allFieldsSelected?: boolean;
|
||||||
|
}) => {
|
||||||
|
if (isEmpty(restrictedFields)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allFieldsSelected) {
|
||||||
|
throw new PermissionsException(
|
||||||
|
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||||
|
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const column of selectedColumns) {
|
||||||
|
const fieldMetadataId = fieldMetadataIdForColumnNameMap[column];
|
||||||
|
|
||||||
|
if (!fieldMetadataId) {
|
||||||
|
throw new InternalServerError(
|
||||||
|
`Field metadata id not found for column name ${column}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (restrictedFields[fieldMetadataId]?.canRead === false) {
|
||||||
|
throw new PermissionsException(
|
||||||
|
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||||
|
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSelectedColumnsFromExpressionMap = ({
|
||||||
|
operationType,
|
||||||
|
expressionMap,
|
||||||
|
allFieldsSelected,
|
||||||
|
}: {
|
||||||
|
operationType: string;
|
||||||
|
expressionMap: QueryExpressionMap;
|
||||||
|
allFieldsSelected: boolean;
|
||||||
|
}) => {
|
||||||
|
let selectedColumns: string[] = [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
['update', 'insert', 'delete', 'soft-delete', 'restore'].includes(
|
||||||
|
operationType,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (isEmpty(expressionMap.returning)) {
|
||||||
|
throw new InternalServerError(
|
||||||
|
'Returning columns are not set for update query',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
selectedColumns = [expressionMap.returning].flat();
|
||||||
|
} else if (!allFieldsSelected) {
|
||||||
|
selectedColumns = getSelectedColumnsFromExpressionMapSelects(
|
||||||
|
expressionMap.selects,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedColumns;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSelectedColumnsFromExpressionMapSelects = (
|
||||||
|
selects: { selection: string }[],
|
||||||
|
) => {
|
||||||
|
return selects
|
||||||
|
?.map((select) => {
|
||||||
|
const columnsFromAggregateExpression =
|
||||||
|
ProcessAggregateHelper.extractColumnNamesFromAggregateExpression(
|
||||||
|
select.selection,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (columnsFromAggregateExpression) {
|
||||||
|
return columnsFromAggregateExpression;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = select.selection.split('.');
|
||||||
|
|
||||||
|
return parts[parts.length - 1];
|
||||||
|
})
|
||||||
|
.flat();
|
||||||
|
};
|
||||||
|
|||||||
@ -8,10 +8,12 @@ import {
|
|||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||||
|
|
||||||
|
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
|
||||||
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
|
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
|
||||||
|
|
||||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||||
|
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||||
import {
|
import {
|
||||||
TwentyORMException,
|
TwentyORMException,
|
||||||
TwentyORMExceptionCode,
|
TwentyORMExceptionCode,
|
||||||
@ -30,18 +32,21 @@ export class WorkspaceDeleteQueryBuilder<
|
|||||||
private shouldBypassPermissionChecks: boolean;
|
private shouldBypassPermissionChecks: boolean;
|
||||||
private internalContext: WorkspaceInternalContext;
|
private internalContext: WorkspaceInternalContext;
|
||||||
private authContext?: AuthContext;
|
private authContext?: AuthContext;
|
||||||
|
private featureFlagMap?: FeatureFlagMap;
|
||||||
constructor(
|
constructor(
|
||||||
queryBuilder: DeleteQueryBuilder<T>,
|
queryBuilder: DeleteQueryBuilder<T>,
|
||||||
objectRecordsPermissions: ObjectRecordsPermissions,
|
objectRecordsPermissions: ObjectRecordsPermissions,
|
||||||
internalContext: WorkspaceInternalContext,
|
internalContext: WorkspaceInternalContext,
|
||||||
shouldBypassPermissionChecks: boolean,
|
shouldBypassPermissionChecks: boolean,
|
||||||
authContext?: AuthContext,
|
authContext?: AuthContext,
|
||||||
|
featureFlagMap?: FeatureFlagMap,
|
||||||
) {
|
) {
|
||||||
super(queryBuilder);
|
super(queryBuilder);
|
||||||
this.objectRecordsPermissions = objectRecordsPermissions;
|
this.objectRecordsPermissions = objectRecordsPermissions;
|
||||||
this.internalContext = internalContext;
|
this.internalContext = internalContext;
|
||||||
this.shouldBypassPermissionChecks = shouldBypassPermissionChecks;
|
this.shouldBypassPermissionChecks = shouldBypassPermissionChecks;
|
||||||
this.authContext = authContext;
|
this.authContext = authContext;
|
||||||
|
this.featureFlagMap = featureFlagMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
override clone(): this {
|
override clone(): this {
|
||||||
@ -57,12 +62,14 @@ export class WorkspaceDeleteQueryBuilder<
|
|||||||
}
|
}
|
||||||
|
|
||||||
override async execute(): Promise<DeleteResult & { generatedMaps: T[] }> {
|
override async execute(): Promise<DeleteResult & { generatedMaps: T[] }> {
|
||||||
validateQueryIsPermittedOrThrow(
|
validateQueryIsPermittedOrThrow({
|
||||||
this.expressionMap,
|
expressionMap: this.expressionMap,
|
||||||
this.objectRecordsPermissions,
|
objectRecordsPermissions: this.objectRecordsPermissions,
|
||||||
this.internalContext.objectMetadataMaps,
|
objectMetadataMaps: this.internalContext.objectMetadataMaps,
|
||||||
this.shouldBypassPermissionChecks,
|
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
|
||||||
);
|
isFieldPermissionsEnabled:
|
||||||
|
this.featureFlagMap?.[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED],
|
||||||
|
});
|
||||||
|
|
||||||
const mainAliasTarget = this.getMainAliasTarget();
|
const mainAliasTarget = this.getMainAliasTarget();
|
||||||
|
|
||||||
|
|||||||
@ -7,10 +7,12 @@ import {
|
|||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||||
|
|
||||||
|
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
|
||||||
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
|
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
|
||||||
|
|
||||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||||
|
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||||
import {
|
import {
|
||||||
TwentyORMException,
|
TwentyORMException,
|
||||||
TwentyORMExceptionCode,
|
TwentyORMExceptionCode,
|
||||||
@ -31,6 +33,7 @@ export class WorkspaceInsertQueryBuilder<
|
|||||||
private shouldBypassPermissionChecks: boolean;
|
private shouldBypassPermissionChecks: boolean;
|
||||||
private internalContext: WorkspaceInternalContext;
|
private internalContext: WorkspaceInternalContext;
|
||||||
private authContext?: AuthContext;
|
private authContext?: AuthContext;
|
||||||
|
private featureFlagMap?: FeatureFlagMap;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
queryBuilder: InsertQueryBuilder<T>,
|
queryBuilder: InsertQueryBuilder<T>,
|
||||||
@ -38,12 +41,14 @@ export class WorkspaceInsertQueryBuilder<
|
|||||||
internalContext: WorkspaceInternalContext,
|
internalContext: WorkspaceInternalContext,
|
||||||
shouldBypassPermissionChecks: boolean,
|
shouldBypassPermissionChecks: boolean,
|
||||||
authContext?: AuthContext,
|
authContext?: AuthContext,
|
||||||
|
featureFlagMap?: FeatureFlagMap,
|
||||||
) {
|
) {
|
||||||
super(queryBuilder);
|
super(queryBuilder);
|
||||||
this.objectRecordsPermissions = objectRecordsPermissions;
|
this.objectRecordsPermissions = objectRecordsPermissions;
|
||||||
this.internalContext = internalContext;
|
this.internalContext = internalContext;
|
||||||
this.shouldBypassPermissionChecks = shouldBypassPermissionChecks;
|
this.shouldBypassPermissionChecks = shouldBypassPermissionChecks;
|
||||||
this.authContext = authContext;
|
this.authContext = authContext;
|
||||||
|
this.featureFlagMap = featureFlagMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
override clone(): this {
|
override clone(): this {
|
||||||
@ -55,6 +60,7 @@ export class WorkspaceInsertQueryBuilder<
|
|||||||
this.internalContext,
|
this.internalContext,
|
||||||
this.shouldBypassPermissionChecks,
|
this.shouldBypassPermissionChecks,
|
||||||
this.authContext,
|
this.authContext,
|
||||||
|
this.featureFlagMap,
|
||||||
) as this;
|
) as this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,12 +80,14 @@ export class WorkspaceInsertQueryBuilder<
|
|||||||
}
|
}
|
||||||
|
|
||||||
override async execute(): Promise<InsertResult> {
|
override async execute(): Promise<InsertResult> {
|
||||||
validateQueryIsPermittedOrThrow(
|
validateQueryIsPermittedOrThrow({
|
||||||
this.expressionMap,
|
expressionMap: this.expressionMap,
|
||||||
this.objectRecordsPermissions,
|
objectRecordsPermissions: this.objectRecordsPermissions,
|
||||||
this.internalContext.objectMetadataMaps,
|
objectMetadataMaps: this.internalContext.objectMetadataMaps,
|
||||||
this.shouldBypassPermissionChecks,
|
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
|
||||||
);
|
isFieldPermissionsEnabled:
|
||||||
|
this.featureFlagMap?.[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED],
|
||||||
|
});
|
||||||
|
|
||||||
const mainAliasTarget = this.getMainAliasTarget();
|
const mainAliasTarget = this.getMainAliasTarget();
|
||||||
|
|
||||||
|
|||||||
@ -2,9 +2,11 @@ import { ObjectRecordsPermissions } from 'twenty-shared/types';
|
|||||||
import { EntityTarget, ObjectLiteral, SelectQueryBuilder } from 'typeorm';
|
import { EntityTarget, ObjectLiteral, SelectQueryBuilder } from 'typeorm';
|
||||||
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||||
|
|
||||||
|
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
|
||||||
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
|
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
|
||||||
|
|
||||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||||
|
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||||
import {
|
import {
|
||||||
PermissionsException,
|
PermissionsException,
|
||||||
PermissionsExceptionCode,
|
PermissionsExceptionCode,
|
||||||
@ -28,18 +30,21 @@ export class WorkspaceSelectQueryBuilder<
|
|||||||
shouldBypassPermissionChecks: boolean;
|
shouldBypassPermissionChecks: boolean;
|
||||||
internalContext: WorkspaceInternalContext;
|
internalContext: WorkspaceInternalContext;
|
||||||
authContext?: AuthContext;
|
authContext?: AuthContext;
|
||||||
|
featureFlagMap?: FeatureFlagMap;
|
||||||
constructor(
|
constructor(
|
||||||
queryBuilder: SelectQueryBuilder<T>,
|
queryBuilder: SelectQueryBuilder<T>,
|
||||||
objectRecordsPermissions: ObjectRecordsPermissions,
|
objectRecordsPermissions: ObjectRecordsPermissions,
|
||||||
internalContext: WorkspaceInternalContext,
|
internalContext: WorkspaceInternalContext,
|
||||||
shouldBypassPermissionChecks: boolean,
|
shouldBypassPermissionChecks: boolean,
|
||||||
authContext?: AuthContext,
|
authContext?: AuthContext,
|
||||||
|
featureFlagMap?: FeatureFlagMap,
|
||||||
) {
|
) {
|
||||||
super(queryBuilder);
|
super(queryBuilder);
|
||||||
this.objectRecordsPermissions = objectRecordsPermissions;
|
this.objectRecordsPermissions = objectRecordsPermissions;
|
||||||
this.internalContext = internalContext;
|
this.internalContext = internalContext;
|
||||||
this.shouldBypassPermissionChecks = shouldBypassPermissionChecks;
|
this.shouldBypassPermissionChecks = shouldBypassPermissionChecks;
|
||||||
this.authContext = authContext;
|
this.authContext = authContext;
|
||||||
|
this.featureFlagMap = featureFlagMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
getFindOptions() {
|
getFindOptions() {
|
||||||
@ -55,6 +60,7 @@ export class WorkspaceSelectQueryBuilder<
|
|||||||
this.internalContext,
|
this.internalContext,
|
||||||
this.shouldBypassPermissionChecks,
|
this.shouldBypassPermissionChecks,
|
||||||
this.authContext,
|
this.authContext,
|
||||||
|
this.featureFlagMap,
|
||||||
) as this;
|
) as this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,6 +210,7 @@ export class WorkspaceSelectQueryBuilder<
|
|||||||
this.internalContext,
|
this.internalContext,
|
||||||
this.shouldBypassPermissionChecks,
|
this.shouldBypassPermissionChecks,
|
||||||
this.authContext,
|
this.authContext,
|
||||||
|
this.featureFlagMap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,6 +233,7 @@ export class WorkspaceSelectQueryBuilder<
|
|||||||
this.internalContext,
|
this.internalContext,
|
||||||
this.shouldBypassPermissionChecks,
|
this.shouldBypassPermissionChecks,
|
||||||
this.authContext,
|
this.authContext,
|
||||||
|
this.featureFlagMap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,6 +246,7 @@ export class WorkspaceSelectQueryBuilder<
|
|||||||
this.internalContext,
|
this.internalContext,
|
||||||
this.shouldBypassPermissionChecks,
|
this.shouldBypassPermissionChecks,
|
||||||
this.authContext,
|
this.authContext,
|
||||||
|
this.featureFlagMap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,6 +259,7 @@ export class WorkspaceSelectQueryBuilder<
|
|||||||
this.internalContext,
|
this.internalContext,
|
||||||
this.shouldBypassPermissionChecks,
|
this.shouldBypassPermissionChecks,
|
||||||
this.authContext,
|
this.authContext,
|
||||||
|
this.featureFlagMap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,6 +272,7 @@ export class WorkspaceSelectQueryBuilder<
|
|||||||
this.internalContext,
|
this.internalContext,
|
||||||
this.shouldBypassPermissionChecks,
|
this.shouldBypassPermissionChecks,
|
||||||
this.authContext,
|
this.authContext,
|
||||||
|
this.featureFlagMap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,12 +284,16 @@ export class WorkspaceSelectQueryBuilder<
|
|||||||
}
|
}
|
||||||
|
|
||||||
private validatePermissions(): void {
|
private validatePermissions(): void {
|
||||||
validateQueryIsPermittedOrThrow(
|
const isFieldPermissionsEnabled =
|
||||||
this.expressionMap,
|
this.featureFlagMap?.[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED];
|
||||||
this.objectRecordsPermissions,
|
|
||||||
this.internalContext.objectMetadataMaps,
|
validateQueryIsPermittedOrThrow({
|
||||||
this.shouldBypassPermissionChecks,
|
expressionMap: this.expressionMap,
|
||||||
);
|
objectRecordsPermissions: this.objectRecordsPermissions,
|
||||||
|
objectMetadataMaps: this.internalContext.objectMetadataMaps,
|
||||||
|
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
|
||||||
|
isFieldPermissionsEnabled,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private getMainAliasTarget(): EntityTarget<T> {
|
private getMainAliasTarget(): EntityTarget<T> {
|
||||||
|
|||||||
@ -7,10 +7,12 @@ import {
|
|||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { SoftDeleteQueryBuilder } from 'typeorm/query-builder/SoftDeleteQueryBuilder';
|
import { SoftDeleteQueryBuilder } from 'typeorm/query-builder/SoftDeleteQueryBuilder';
|
||||||
|
|
||||||
|
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
|
||||||
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
|
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
|
||||||
|
|
||||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||||
|
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||||
import {
|
import {
|
||||||
TwentyORMException,
|
TwentyORMException,
|
||||||
TwentyORMExceptionCode,
|
TwentyORMExceptionCode,
|
||||||
@ -29,6 +31,7 @@ export class WorkspaceSoftDeleteQueryBuilder<
|
|||||||
private shouldBypassPermissionChecks: boolean;
|
private shouldBypassPermissionChecks: boolean;
|
||||||
private internalContext: WorkspaceInternalContext;
|
private internalContext: WorkspaceInternalContext;
|
||||||
private authContext?: AuthContext;
|
private authContext?: AuthContext;
|
||||||
|
private featureFlagMap?: FeatureFlagMap;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
queryBuilder: SoftDeleteQueryBuilder<T>,
|
queryBuilder: SoftDeleteQueryBuilder<T>,
|
||||||
@ -36,12 +39,14 @@ export class WorkspaceSoftDeleteQueryBuilder<
|
|||||||
internalContext: WorkspaceInternalContext,
|
internalContext: WorkspaceInternalContext,
|
||||||
shouldBypassPermissionChecks: boolean,
|
shouldBypassPermissionChecks: boolean,
|
||||||
authContext?: AuthContext,
|
authContext?: AuthContext,
|
||||||
|
featureFlagMap?: FeatureFlagMap,
|
||||||
) {
|
) {
|
||||||
super(queryBuilder);
|
super(queryBuilder);
|
||||||
this.objectRecordsPermissions = objectRecordsPermissions;
|
this.objectRecordsPermissions = objectRecordsPermissions;
|
||||||
this.internalContext = internalContext;
|
this.internalContext = internalContext;
|
||||||
this.shouldBypassPermissionChecks = shouldBypassPermissionChecks;
|
this.shouldBypassPermissionChecks = shouldBypassPermissionChecks;
|
||||||
this.authContext = authContext;
|
this.authContext = authContext;
|
||||||
|
this.featureFlagMap = featureFlagMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
override clone(): this {
|
override clone(): this {
|
||||||
@ -57,12 +62,14 @@ export class WorkspaceSoftDeleteQueryBuilder<
|
|||||||
}
|
}
|
||||||
|
|
||||||
override async execute(): Promise<UpdateResult> {
|
override async execute(): Promise<UpdateResult> {
|
||||||
validateQueryIsPermittedOrThrow(
|
validateQueryIsPermittedOrThrow({
|
||||||
this.expressionMap,
|
expressionMap: this.expressionMap,
|
||||||
this.objectRecordsPermissions,
|
objectRecordsPermissions: this.objectRecordsPermissions,
|
||||||
this.internalContext.objectMetadataMaps,
|
objectMetadataMaps: this.internalContext.objectMetadataMaps,
|
||||||
this.shouldBypassPermissionChecks,
|
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
|
||||||
);
|
isFieldPermissionsEnabled:
|
||||||
|
this.featureFlagMap?.[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED],
|
||||||
|
});
|
||||||
|
|
||||||
const mainAliasTarget = this.getMainAliasTarget();
|
const mainAliasTarget = this.getMainAliasTarget();
|
||||||
|
|
||||||
|
|||||||
@ -7,10 +7,12 @@ import {
|
|||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||||
|
|
||||||
|
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
|
||||||
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
|
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
|
||||||
|
|
||||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||||
|
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||||
import {
|
import {
|
||||||
TwentyORMException,
|
TwentyORMException,
|
||||||
TwentyORMExceptionCode,
|
TwentyORMExceptionCode,
|
||||||
@ -30,18 +32,21 @@ export class WorkspaceUpdateQueryBuilder<
|
|||||||
private shouldBypassPermissionChecks: boolean;
|
private shouldBypassPermissionChecks: boolean;
|
||||||
private internalContext: WorkspaceInternalContext;
|
private internalContext: WorkspaceInternalContext;
|
||||||
private authContext?: AuthContext;
|
private authContext?: AuthContext;
|
||||||
|
private featureFlagMap?: FeatureFlagMap;
|
||||||
constructor(
|
constructor(
|
||||||
queryBuilder: UpdateQueryBuilder<T>,
|
queryBuilder: UpdateQueryBuilder<T>,
|
||||||
objectRecordsPermissions: ObjectRecordsPermissions,
|
objectRecordsPermissions: ObjectRecordsPermissions,
|
||||||
internalContext: WorkspaceInternalContext,
|
internalContext: WorkspaceInternalContext,
|
||||||
shouldBypassPermissionChecks: boolean,
|
shouldBypassPermissionChecks: boolean,
|
||||||
authContext?: AuthContext,
|
authContext?: AuthContext,
|
||||||
|
featureFlagMap?: FeatureFlagMap,
|
||||||
) {
|
) {
|
||||||
super(queryBuilder);
|
super(queryBuilder);
|
||||||
this.objectRecordsPermissions = objectRecordsPermissions;
|
this.objectRecordsPermissions = objectRecordsPermissions;
|
||||||
this.internalContext = internalContext;
|
this.internalContext = internalContext;
|
||||||
this.shouldBypassPermissionChecks = shouldBypassPermissionChecks;
|
this.shouldBypassPermissionChecks = shouldBypassPermissionChecks;
|
||||||
this.authContext = authContext;
|
this.authContext = authContext;
|
||||||
|
this.featureFlagMap = featureFlagMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
override clone(): this {
|
override clone(): this {
|
||||||
@ -53,16 +58,19 @@ export class WorkspaceUpdateQueryBuilder<
|
|||||||
this.internalContext,
|
this.internalContext,
|
||||||
this.shouldBypassPermissionChecks,
|
this.shouldBypassPermissionChecks,
|
||||||
this.authContext,
|
this.authContext,
|
||||||
|
this.featureFlagMap,
|
||||||
) as this;
|
) as this;
|
||||||
}
|
}
|
||||||
|
|
||||||
override async execute(): Promise<UpdateResult> {
|
override async execute(): Promise<UpdateResult> {
|
||||||
validateQueryIsPermittedOrThrow(
|
validateQueryIsPermittedOrThrow({
|
||||||
this.expressionMap,
|
expressionMap: this.expressionMap,
|
||||||
this.objectRecordsPermissions,
|
objectRecordsPermissions: this.objectRecordsPermissions,
|
||||||
this.internalContext.objectMetadataMaps,
|
objectMetadataMaps: this.internalContext.objectMetadataMaps,
|
||||||
this.shouldBypassPermissionChecks,
|
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
|
||||||
);
|
isFieldPermissionsEnabled:
|
||||||
|
this.featureFlagMap?.[FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED],
|
||||||
|
});
|
||||||
|
|
||||||
const mainAliasTarget = this.getMainAliasTarget();
|
const mainAliasTarget = this.getMainAliasTarget();
|
||||||
|
|
||||||
@ -77,6 +85,7 @@ export class WorkspaceUpdateQueryBuilder<
|
|||||||
this.internalContext,
|
this.internalContext,
|
||||||
true,
|
true,
|
||||||
this.authContext,
|
this.authContext,
|
||||||
|
this.featureFlagMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
eventSelectQueryBuilder.expressionMap.wheres = this.expressionMap.wheres;
|
eventSelectQueryBuilder.expressionMap.wheres = this.expressionMap.wheres;
|
||||||
@ -109,9 +118,15 @@ export class WorkspaceUpdateQueryBuilder<
|
|||||||
authContext: this.authContext,
|
authContext: this.authContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const formattedResult = formatResult<T[]>(
|
||||||
|
result.raw,
|
||||||
|
objectMetadata,
|
||||||
|
this.internalContext.objectMetadataMaps,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
raw: result.raw,
|
raw: result.raw,
|
||||||
generatedMaps: formattedAfter,
|
generatedMaps: formattedResult,
|
||||||
affected: result.affected,
|
affected: result.affected,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -81,6 +81,7 @@ export class WorkspaceRepository<
|
|||||||
this.internalContext,
|
this.internalContext,
|
||||||
this.shouldBypassPermissionChecks,
|
this.shouldBypassPermissionChecks,
|
||||||
this.authContext,
|
this.authContext,
|
||||||
|
this.featureFlagMap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -538,6 +539,7 @@ export class WorkspaceRepository<
|
|||||||
| QueryDeepPartialEntityWithRelationConnect<T>
|
| QueryDeepPartialEntityWithRelationConnect<T>
|
||||||
| QueryDeepPartialEntityWithRelationConnect<T>[],
|
| QueryDeepPartialEntityWithRelationConnect<T>[],
|
||||||
entityManager?: WorkspaceEntityManager,
|
entityManager?: WorkspaceEntityManager,
|
||||||
|
selectedColumns?: string[],
|
||||||
): Promise<InsertResult> {
|
): Promise<InsertResult> {
|
||||||
const manager = entityManager || this.manager;
|
const manager = entityManager || this.manager;
|
||||||
|
|
||||||
@ -546,7 +548,12 @@ export class WorkspaceRepository<
|
|||||||
objectRecordsPermissions: this.objectRecordsPermissions,
|
objectRecordsPermissions: this.objectRecordsPermissions,
|
||||||
};
|
};
|
||||||
|
|
||||||
return manager.insert(this.target, entity, permissionOptions);
|
return manager.insert(
|
||||||
|
this.target,
|
||||||
|
entity,
|
||||||
|
selectedColumns,
|
||||||
|
permissionOptions,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -565,6 +572,7 @@ export class WorkspaceRepository<
|
|||||||
| FindOptionsWhere<T>,
|
| FindOptionsWhere<T>,
|
||||||
partialEntity: QueryDeepPartialEntity<T>,
|
partialEntity: QueryDeepPartialEntity<T>,
|
||||||
entityManager?: WorkspaceEntityManager,
|
entityManager?: WorkspaceEntityManager,
|
||||||
|
selectedColumns?: string[],
|
||||||
): Promise<UpdateResult> {
|
): Promise<UpdateResult> {
|
||||||
const manager = entityManager || this.manager;
|
const manager = entityManager || this.manager;
|
||||||
|
|
||||||
@ -582,6 +590,7 @@ export class WorkspaceRepository<
|
|||||||
criteria,
|
criteria,
|
||||||
partialEntity,
|
partialEntity,
|
||||||
permissionOptions,
|
permissionOptions,
|
||||||
|
selectedColumns,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -589,6 +598,7 @@ export class WorkspaceRepository<
|
|||||||
entityOrEntities: QueryDeepPartialEntity<T> | QueryDeepPartialEntity<T>[],
|
entityOrEntities: QueryDeepPartialEntity<T> | QueryDeepPartialEntity<T>[],
|
||||||
conflictPathsOrOptions: string[] | UpsertOptions<T>,
|
conflictPathsOrOptions: string[] | UpsertOptions<T>,
|
||||||
entityManager?: WorkspaceEntityManager,
|
entityManager?: WorkspaceEntityManager,
|
||||||
|
selectedColumns: string[] = [],
|
||||||
): Promise<InsertResult> {
|
): Promise<InsertResult> {
|
||||||
const manager = entityManager || this.manager;
|
const manager = entityManager || this.manager;
|
||||||
|
|
||||||
@ -602,6 +612,7 @@ export class WorkspaceRepository<
|
|||||||
entityOrEntities,
|
entityOrEntities,
|
||||||
conflictPathsOrOptions,
|
conflictPathsOrOptions,
|
||||||
permissionOptions,
|
permissionOptions,
|
||||||
|
selectedColumns,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -777,6 +788,7 @@ export class WorkspaceRepository<
|
|||||||
propertyPath: string,
|
propertyPath: string,
|
||||||
value: number | string,
|
value: number | string,
|
||||||
entityManager?: WorkspaceEntityManager,
|
entityManager?: WorkspaceEntityManager,
|
||||||
|
selectedColumns?: string[],
|
||||||
): Promise<UpdateResult> {
|
): Promise<UpdateResult> {
|
||||||
const manager = entityManager || this.manager;
|
const manager = entityManager || this.manager;
|
||||||
const computedConditions = await this.transformOptions({
|
const computedConditions = await this.transformOptions({
|
||||||
@ -794,6 +806,7 @@ export class WorkspaceRepository<
|
|||||||
propertyPath,
|
propertyPath,
|
||||||
value,
|
value,
|
||||||
permissionOptions,
|
permissionOptions,
|
||||||
|
selectedColumns,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -802,6 +815,7 @@ export class WorkspaceRepository<
|
|||||||
propertyPath: string,
|
propertyPath: string,
|
||||||
value: number | string,
|
value: number | string,
|
||||||
entityManager?: WorkspaceEntityManager,
|
entityManager?: WorkspaceEntityManager,
|
||||||
|
selectedColumns?: string[],
|
||||||
): Promise<UpdateResult> {
|
): Promise<UpdateResult> {
|
||||||
const manager = entityManager || this.manager;
|
const manager = entityManager || this.manager;
|
||||||
const computedConditions = await this.transformOptions({
|
const computedConditions = await this.transformOptions({
|
||||||
@ -819,6 +833,7 @@ export class WorkspaceRepository<
|
|||||||
propertyPath,
|
propertyPath,
|
||||||
value,
|
value,
|
||||||
permissionOptions,
|
permissionOptions,
|
||||||
|
selectedColumns,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,47 @@
|
|||||||
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
|
||||||
|
import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||||
|
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||||
|
import {
|
||||||
|
computeColumnName,
|
||||||
|
computeCompositeColumnName,
|
||||||
|
} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||||
|
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||||
|
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||||
|
|
||||||
|
export function getFieldMetadataIdForColumnNameMap(
|
||||||
|
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
|
||||||
|
) {
|
||||||
|
const columnNameToFieldMetadataIdMap: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (const [fieldMetadataId, fieldMetadata] of Object.entries(
|
||||||
|
objectMetadataItemWithFieldMaps.fieldsById,
|
||||||
|
)) {
|
||||||
|
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||||
|
const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
|
||||||
|
|
||||||
|
if (!compositeType) {
|
||||||
|
throw new InternalServerError(
|
||||||
|
`Composite type not found for field metadata type ${fieldMetadata.type}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
compositeType.properties.forEach((compositeProperty) => {
|
||||||
|
const columnName = computeCompositeColumnName(
|
||||||
|
fieldMetadata.name,
|
||||||
|
compositeProperty,
|
||||||
|
);
|
||||||
|
|
||||||
|
columnNameToFieldMetadataIdMap[columnName] = fieldMetadataId;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const columnName = computeColumnName(fieldMetadata, {
|
||||||
|
isForeignKey: fieldMetadata.type === FieldMetadataType.RELATION,
|
||||||
|
});
|
||||||
|
|
||||||
|
columnNameToFieldMetadataIdMap[columnName] = fieldMetadataId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return columnNameToFieldMetadataIdMap;
|
||||||
|
}
|
||||||
@ -119,6 +119,8 @@ export class CalendarEventImportErrorHandlerService {
|
|||||||
},
|
},
|
||||||
'throttleFailureCount',
|
'throttleFailureCount',
|
||||||
1,
|
1,
|
||||||
|
undefined,
|
||||||
|
['throttleFailureCount', 'id'],
|
||||||
);
|
);
|
||||||
|
|
||||||
switch (syncStep) {
|
switch (syncStep) {
|
||||||
|
|||||||
@ -141,6 +141,8 @@ export class MessageImportExceptionHandlerService {
|
|||||||
{ id: messageChannel.id },
|
{ id: messageChannel.id },
|
||||||
'throttleFailureCount',
|
'throttleFailureCount',
|
||||||
1,
|
1,
|
||||||
|
undefined,
|
||||||
|
['throttleFailureCount', 'id'],
|
||||||
);
|
);
|
||||||
|
|
||||||
switch (syncStep) {
|
switch (syncStep) {
|
||||||
|
|||||||
@ -0,0 +1,497 @@
|
|||||||
|
import { randomUUID } from 'crypto';
|
||||||
|
|
||||||
|
import gql from 'graphql-tag';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { createCustomRoleWithObjectPermissions } from 'test/integration/graphql/utils/create-custom-role-with-object-permissions.util';
|
||||||
|
import { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-operation-factory.util';
|
||||||
|
import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util';
|
||||||
|
import { deleteManyOperationFactory } from 'test/integration/graphql/utils/delete-many-operation-factory.util';
|
||||||
|
import { deleteOneOperationFactory } from 'test/integration/graphql/utils/delete-one-operation-factory.util';
|
||||||
|
import { deleteRole } from 'test/integration/graphql/utils/delete-one-role.util';
|
||||||
|
import { findManyOperationFactory } from 'test/integration/graphql/utils/find-many-operation-factory.util';
|
||||||
|
import { findOneOperationFactory } from 'test/integration/graphql/utils/find-one-operation-factory.util';
|
||||||
|
import { makeGraphqlAPIRequestWithMemberRole } from 'test/integration/graphql/utils/make-graphql-api-request-with-member-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 { updateOneOperationFactory } from 'test/integration/graphql/utils/update-one-operation-factory.util';
|
||||||
|
import { updateWorkspaceMemberRole } from 'test/integration/graphql/utils/update-workspace-member-role.util';
|
||||||
|
import { upsertFieldPermissions } from 'test/integration/graphql/utils/upsert-field-permissions.util';
|
||||||
|
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||||
|
|
||||||
|
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||||
|
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||||
|
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
||||||
|
import { WORKSPACE_MEMBER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
|
||||||
|
|
||||||
|
const client = request(`http://localhost:${APP_PORT}`);
|
||||||
|
|
||||||
|
const COMPANY_GQL_FIELDS_WITH_PEOPLE_CITY = `
|
||||||
|
id
|
||||||
|
name
|
||||||
|
people {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name {
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
}
|
||||||
|
city
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const COMPANY_GQL_FIELDS_WITH_EMPLOYEES = `
|
||||||
|
id
|
||||||
|
name
|
||||||
|
employees
|
||||||
|
people {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name {
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const COMPANY_GQL_FIELDS_WITHOUT_EMPLOYEES_AND_WITHOUT_PEOPLE_CITY = `
|
||||||
|
id
|
||||||
|
name
|
||||||
|
people {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name {
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const COMPANY_GQL_FIELDS_WITH_PEOPLE_CITY_AGGREGATE = `
|
||||||
|
id
|
||||||
|
name
|
||||||
|
people {
|
||||||
|
percentageEmptyCity
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const expectPermissionDeniedError = (response: any) => {
|
||||||
|
expect(response.body.errors[0].message).toBe(
|
||||||
|
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||||
|
);
|
||||||
|
expect(response.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Field permissions restrictions', () => {
|
||||||
|
let companyId: string;
|
||||||
|
let personId: string;
|
||||||
|
let customRoleId: string;
|
||||||
|
let companyObjectId: string;
|
||||||
|
let personObjectId: string;
|
||||||
|
let restrictedCompanyFieldId: string;
|
||||||
|
let restrictedPersonFieldId: string;
|
||||||
|
let originalMemberRoleId: string;
|
||||||
|
|
||||||
|
const restrictAccessToCompanyEmployee = async (
|
||||||
|
roleId: string,
|
||||||
|
companyObjectId: string,
|
||||||
|
restrictedCompanyFieldId: string,
|
||||||
|
) => {
|
||||||
|
await upsertFieldPermissions({
|
||||||
|
roleId,
|
||||||
|
fieldPermissions: [
|
||||||
|
{
|
||||||
|
objectMetadataId: companyObjectId,
|
||||||
|
fieldMetadataId: restrictedCompanyFieldId,
|
||||||
|
canReadFieldValue: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const restrictAccessToPersonCity = async (
|
||||||
|
roleId: string,
|
||||||
|
personObjectId: string,
|
||||||
|
restrictedPersonFieldId: string,
|
||||||
|
) => {
|
||||||
|
await upsertFieldPermissions({
|
||||||
|
roleId,
|
||||||
|
fieldPermissions: [
|
||||||
|
{
|
||||||
|
objectMetadataId: personObjectId,
|
||||||
|
fieldMetadataId: restrictedPersonFieldId,
|
||||||
|
canReadFieldValue: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
// Enable the feature flag
|
||||||
|
const enablePermissionsQuery = updateFeatureFlagFactory(
|
||||||
|
SEED_APPLE_WORKSPACE_ID,
|
||||||
|
'IS_FIELDS_PERMISSIONS_ENABLED',
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
await makeGraphqlAPIRequest(enablePermissionsQuery);
|
||||||
|
|
||||||
|
// Get the original Member role ID for restoration later
|
||||||
|
const getRolesQuery = {
|
||||||
|
query: `
|
||||||
|
query GetRoles {
|
||||||
|
getRoles {
|
||||||
|
id
|
||||||
|
label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
const rolesResponse = await client
|
||||||
|
.post('/graphql')
|
||||||
|
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
|
||||||
|
.send(getRolesQuery);
|
||||||
|
|
||||||
|
originalMemberRoleId = rolesResponse.body.data.getRoles.find(
|
||||||
|
(role: any) => role.label === 'Member',
|
||||||
|
).id;
|
||||||
|
|
||||||
|
// Create a company and a person
|
||||||
|
companyId = randomUUID();
|
||||||
|
personId = randomUUID();
|
||||||
|
const createCompanyOp = createOneOperationFactory({
|
||||||
|
objectMetadataSingularName: 'company',
|
||||||
|
gqlFields: 'id name',
|
||||||
|
data: { id: companyId, name: 'TestCompany' },
|
||||||
|
});
|
||||||
|
|
||||||
|
await makeGraphqlAPIRequest(createCompanyOp);
|
||||||
|
const createPersonOperation = createOneOperationFactory({
|
||||||
|
objectMetadataSingularName: 'person',
|
||||||
|
gqlFields: 'id city',
|
||||||
|
data: { id: personId, city: 'Paris', companyId },
|
||||||
|
});
|
||||||
|
|
||||||
|
await makeGraphqlAPIRequest(createPersonOperation);
|
||||||
|
|
||||||
|
// Get object and field metadata IDs
|
||||||
|
const getObjectMetadataOp = {
|
||||||
|
query: gql`
|
||||||
|
query {
|
||||||
|
objects(paging: { first: 1000 }) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
nameSingular
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
const objectMetadataResponse =
|
||||||
|
await makeMetadataAPIRequest(getObjectMetadataOp);
|
||||||
|
const objects = objectMetadataResponse.body.data.objects.edges;
|
||||||
|
|
||||||
|
companyObjectId = objects.find(
|
||||||
|
(obj: any) => obj.node.nameSingular === 'company',
|
||||||
|
).node.id;
|
||||||
|
personObjectId = objects.find(
|
||||||
|
(obj: any) => obj.node.nameSingular === 'person',
|
||||||
|
).node.id;
|
||||||
|
|
||||||
|
const getFieldMetadataOp = {
|
||||||
|
query: gql`
|
||||||
|
query {
|
||||||
|
fields(paging: { first: 1000 }) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
object {
|
||||||
|
nameSingular
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
const fieldMetadataResponse =
|
||||||
|
await makeMetadataAPIRequest(getFieldMetadataOp);
|
||||||
|
const fields = fieldMetadataResponse.body.data.fields.edges;
|
||||||
|
|
||||||
|
restrictedCompanyFieldId = fields.find(
|
||||||
|
(field: any) =>
|
||||||
|
field.node.name === 'employees' &&
|
||||||
|
field.node.object.nameSingular === 'company',
|
||||||
|
).node.id;
|
||||||
|
restrictedPersonFieldId = fields.find(
|
||||||
|
(field: any) =>
|
||||||
|
field.node.name === 'city' &&
|
||||||
|
field.node.object.nameSingular === 'person',
|
||||||
|
).node.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
// Restore the feature flag
|
||||||
|
const disablePermissionsQuery = updateFeatureFlagFactory(
|
||||||
|
SEED_APPLE_WORKSPACE_ID,
|
||||||
|
'IS_FIELDS_PERMISSIONS_ENABLED',
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
await makeGraphqlAPIRequest(disablePermissionsQuery);
|
||||||
|
|
||||||
|
// Restore original role
|
||||||
|
const restoreMemberRoleQuery = {
|
||||||
|
query: `
|
||||||
|
mutation UpdateWorkspaceMemberRole {
|
||||||
|
updateWorkspaceMemberRole(
|
||||||
|
workspaceMemberId: "${WORKSPACE_MEMBER_DATA_SEED_IDS.JONY}"
|
||||||
|
roleId: "${originalMemberRoleId}"
|
||||||
|
) { id }
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
await client
|
||||||
|
.post('/graphql')
|
||||||
|
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
|
||||||
|
.send(restoreMemberRoleQuery);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const { roleId } = await createCustomRoleWithObjectPermissions({
|
||||||
|
label: 'CompanyPeopleRole',
|
||||||
|
canReadCompany: true,
|
||||||
|
canReadPerson: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
customRoleId = roleId;
|
||||||
|
await updateWorkspaceMemberRole({
|
||||||
|
client,
|
||||||
|
roleId: customRoleId,
|
||||||
|
workspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.JONY,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (customRoleId) {
|
||||||
|
await deleteRole(client, customRoleId);
|
||||||
|
customRoleId = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should throw an error if requesting a restricted field', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await restrictAccessToCompanyEmployee(
|
||||||
|
customRoleId,
|
||||||
|
companyObjectId,
|
||||||
|
restrictedCompanyFieldId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('1. findMany', async () => {
|
||||||
|
const graphqlOperation = findManyOperationFactory({
|
||||||
|
objectMetadataSingularName: 'company',
|
||||||
|
objectMetadataPluralName: 'companies',
|
||||||
|
gqlFields: COMPANY_GQL_FIELDS_WITH_EMPLOYEES,
|
||||||
|
});
|
||||||
|
const response =
|
||||||
|
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
|
||||||
|
|
||||||
|
expectPermissionDeniedError(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('2. findOne', async () => {
|
||||||
|
const graphqlOperation = findOneOperationFactory({
|
||||||
|
objectMetadataSingularName: 'company',
|
||||||
|
gqlFields: COMPANY_GQL_FIELDS_WITH_EMPLOYEES,
|
||||||
|
filter: { id: { eq: companyId } },
|
||||||
|
});
|
||||||
|
const response =
|
||||||
|
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
|
||||||
|
|
||||||
|
expectPermissionDeniedError(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('3. updateMany', async () => {
|
||||||
|
const graphqlOperation = updateManyOperationFactory({
|
||||||
|
objectMetadataSingularName: 'company',
|
||||||
|
objectMetadataPluralName: 'companies',
|
||||||
|
gqlFields: COMPANY_GQL_FIELDS_WITH_EMPLOYEES,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response =
|
||||||
|
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
|
||||||
|
|
||||||
|
expectPermissionDeniedError(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('4. updateOne', async () => {
|
||||||
|
const graphqlOperation = updateOneOperationFactory({
|
||||||
|
objectMetadataSingularName: 'company',
|
||||||
|
gqlFields: COMPANY_GQL_FIELDS_WITH_EMPLOYEES,
|
||||||
|
recordId: companyId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response =
|
||||||
|
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
|
||||||
|
|
||||||
|
expectPermissionDeniedError(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('5. createMany', async () => {
|
||||||
|
const graphqlOperation = createManyOperationFactory({
|
||||||
|
objectMetadataSingularName: 'company',
|
||||||
|
objectMetadataPluralName: 'companies',
|
||||||
|
gqlFields: COMPANY_GQL_FIELDS_WITH_EMPLOYEES,
|
||||||
|
data: [
|
||||||
|
{ id: randomUUID(), name: 'TestCompany' },
|
||||||
|
{ id: randomUUID(), name: 'TestCompany2' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const response =
|
||||||
|
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
|
||||||
|
|
||||||
|
expectPermissionDeniedError(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('5. createOne', async () => {
|
||||||
|
const graphqlOperation = createOneOperationFactory({
|
||||||
|
objectMetadataSingularName: 'company',
|
||||||
|
gqlFields: COMPANY_GQL_FIELDS_WITH_EMPLOYEES,
|
||||||
|
data: { id: randomUUID(), name: 'TestCompany3' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const response =
|
||||||
|
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
|
||||||
|
|
||||||
|
expectPermissionDeniedError(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('6. deleteMany', async () => {
|
||||||
|
const graphqlOperation = deleteManyOperationFactory({
|
||||||
|
objectMetadataSingularName: 'company',
|
||||||
|
objectMetadataPluralName: 'companies',
|
||||||
|
gqlFields: COMPANY_GQL_FIELDS_WITH_EMPLOYEES,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response =
|
||||||
|
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
|
||||||
|
|
||||||
|
expectPermissionDeniedError(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('7. deleteOne', async () => {
|
||||||
|
const graphqlOperation = deleteOneOperationFactory({
|
||||||
|
objectMetadataSingularName: 'company',
|
||||||
|
gqlFields: COMPANY_GQL_FIELDS_WITH_EMPLOYEES,
|
||||||
|
recordId: companyId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response =
|
||||||
|
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
|
||||||
|
|
||||||
|
expectPermissionDeniedError(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('2. should throw an error if requesting a restricted field of a related object', async () => {
|
||||||
|
await restrictAccessToPersonCity(
|
||||||
|
customRoleId,
|
||||||
|
personObjectId,
|
||||||
|
restrictedPersonFieldId,
|
||||||
|
);
|
||||||
|
const graphqlOperation = findManyOperationFactory({
|
||||||
|
objectMetadataSingularName: 'company',
|
||||||
|
objectMetadataPluralName: 'companies',
|
||||||
|
gqlFields: COMPANY_GQL_FIELDS_WITH_PEOPLE_CITY,
|
||||||
|
});
|
||||||
|
const response =
|
||||||
|
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
|
||||||
|
|
||||||
|
expectPermissionDeniedError(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('3. should succeed if restricted fields exist but are not requested', async () => {
|
||||||
|
await restrictAccessToCompanyEmployee(
|
||||||
|
customRoleId,
|
||||||
|
companyObjectId,
|
||||||
|
restrictedCompanyFieldId,
|
||||||
|
);
|
||||||
|
await restrictAccessToPersonCity(
|
||||||
|
customRoleId,
|
||||||
|
personObjectId,
|
||||||
|
restrictedPersonFieldId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Query NOT requesting the restricted field
|
||||||
|
const graphqlOperation = findManyOperationFactory({
|
||||||
|
objectMetadataSingularName: 'company',
|
||||||
|
objectMetadataPluralName: 'companies',
|
||||||
|
gqlFields: COMPANY_GQL_FIELDS_WITHOUT_EMPLOYEES_AND_WITHOUT_PEOPLE_CITY,
|
||||||
|
});
|
||||||
|
const response =
|
||||||
|
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
|
||||||
|
|
||||||
|
expect(response.body.errors).toBeUndefined();
|
||||||
|
expect(response.body.data).toBeDefined();
|
||||||
|
expect(response.body.data.companies.edges[0].node.id).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Aggregate operations', () => {
|
||||||
|
it('1. should throw an error if requesting a restricted field through aggregates', async () => {
|
||||||
|
await restrictAccessToCompanyEmployee(
|
||||||
|
customRoleId,
|
||||||
|
companyObjectId,
|
||||||
|
restrictedCompanyFieldId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Query requesting the aggregate restricted field
|
||||||
|
const graphqlOperation = {
|
||||||
|
query: gql`
|
||||||
|
query Companies {
|
||||||
|
companies {
|
||||||
|
countEmptyEmployees
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
const response =
|
||||||
|
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
|
||||||
|
|
||||||
|
expectPermissionDeniedError(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('2. should throw an error if requesting a restricted field on related object through aggregates', async () => {
|
||||||
|
await restrictAccessToPersonCity(
|
||||||
|
customRoleId,
|
||||||
|
personObjectId,
|
||||||
|
restrictedPersonFieldId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Query requesting the aggregate restricted field
|
||||||
|
const graphqlOperation = findManyOperationFactory({
|
||||||
|
objectMetadataSingularName: 'company',
|
||||||
|
objectMetadataPluralName: 'companies',
|
||||||
|
gqlFields: COMPANY_GQL_FIELDS_WITH_PEOPLE_CITY_AGGREGATE,
|
||||||
|
});
|
||||||
|
const response =
|
||||||
|
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
|
||||||
|
|
||||||
|
expectPermissionDeniedError(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user