Permission checks on twentyORM global manager (#11477)
In this PR we are handling permissions when using twentyORMGlobalManager, and handling permissions for rest api and api key
This commit is contained in:
2
packages/twenty-server/@types/jest.d.ts
vendored
2
packages/twenty-server/@types/jest.d.ts
vendored
@ -9,6 +9,7 @@ declare module '@jest/types' {
|
||||
INVALID_ACCESS_TOKEN: string;
|
||||
MEMBER_ACCESS_TOKEN: string;
|
||||
GUEST_ACCESS_TOKEN: string;
|
||||
API_KEY_ACCESS_TOKEN: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -20,6 +21,7 @@ declare global {
|
||||
const INVALID_ACCESS_TOKEN: string;
|
||||
const MEMBER_ACCESS_TOKEN: string;
|
||||
const GUEST_ACCESS_TOKEN: string;
|
||||
const API_KEY_ACCESS_TOKEN: string;
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
@ -76,6 +76,8 @@ const jestConfig: JestConfigWithTsJest = {
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0zOTU3LTQ5MDgtOWMzNi0yOTI5YTIzZjgzNTciLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNzdkNS00Y2I2LWI2MGEtZjRhODM1YTg1ZDYxIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMzk1Ny00OTA4LTljMzYtMjkyOWEyM2Y4MzUzIiwiaWF0IjoxNzM5NDU5NTcwLCJleHAiOjMzMjk3MDU5NTcwfQ.Er7EEU4IP4YlGN79jCLR_6sUBqBfKx2M3G_qGiDpPRo',
|
||||
GUEST_ACCESS_TOKEN:
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC03MTY5LTQyY2YtYmM0Ny0xY2ZlZjE1MjY0YjgiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMTU1My00NWM2LWEwMjgtNWE5MDY0Y2NlMDdmIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtNzE2OS00MmNmLWJjNDctMWNmZWYxNTI2NGIxIiwiaWF0IjoxNzM5ODg4NDcwLCJleHAiOjMzMjk3NDg4NDcwfQ.0NEu-AWGv3l77rs-56Z5Gt0UTU7HDl6qUTHUcMWNrCc',
|
||||
API_KEY_ACCESS_TOKEN:
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzQ0OTgzNzUwLCJleHAiOjQ4OTg1ODM2OTMsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.4xkkwz_uu2xzs_V8hJSaM15fGziT5zS3vq2lM48OHr0',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
getDevSeedCompanyCustomFields,
|
||||
getDevSeedPeopleCustomFields,
|
||||
} from 'src/database/typeorm-seeds/metadata/fieldsMetadata';
|
||||
import { seedApiKey } from 'src/database/typeorm-seeds/workspace/api-key';
|
||||
import { seedCalendarChannels } from 'src/database/typeorm-seeds/workspace/calendar-channel';
|
||||
import { seedCalendarChannelEventAssociations } from 'src/database/typeorm-seeds/workspace/calendar-channel-event-association';
|
||||
import { seedCalendarEventParticipants } from 'src/database/typeorm-seeds/workspace/calendar-event-participants';
|
||||
@ -184,6 +185,7 @@ export class DataSeedWorkspaceCommand extends CommandRunner {
|
||||
);
|
||||
|
||||
if (dataSourceMetadata.workspaceId === SEED_APPLE_WORKSPACE_ID) {
|
||||
await seedApiKey(entityManager, dataSourceMetadata.schema);
|
||||
await seedMessageThread(entityManager, dataSourceMetadata.schema);
|
||||
await seedConnectedAccount(entityManager, dataSourceMetadata.schema);
|
||||
|
||||
|
||||
@ -88,7 +88,6 @@ export class AddTasksAssignedToMeViewCommand extends ActiveOrSuspendedWorkspaces
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'view',
|
||||
false,
|
||||
);
|
||||
|
||||
const existingView = await viewRepository.findOne({
|
||||
@ -126,7 +125,6 @@ export class AddTasksAssignedToMeViewCommand extends ActiveOrSuspendedWorkspaces
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewFieldWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'viewField',
|
||||
false,
|
||||
);
|
||||
|
||||
const viewFields = viewDefinition.fields.map((field) => ({
|
||||
@ -145,7 +143,6 @@ export class AddTasksAssignedToMeViewCommand extends ActiveOrSuspendedWorkspaces
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewFilterWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'viewFilter',
|
||||
false,
|
||||
);
|
||||
|
||||
const viewFilters = viewDefinition.filters.map((filter) => ({
|
||||
@ -202,7 +199,6 @@ export class AddTasksAssignedToMeViewCommand extends ActiveOrSuspendedWorkspaces
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewGroupWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'viewGroup',
|
||||
false,
|
||||
);
|
||||
|
||||
await viewGroupRepository.insert(viewGroups);
|
||||
|
||||
@ -81,7 +81,9 @@ export class UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand extends Acti
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'view',
|
||||
failOnMetadataCacheMiss,
|
||||
{
|
||||
shouldFailIfMetadataNotFound: failOnMetadataCacheMiss,
|
||||
},
|
||||
);
|
||||
|
||||
await viewRepository.update(
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
const tableName = 'apiKey';
|
||||
|
||||
const API_KEY_ID = '20202020-f401-4d8a-a731-64d007c27bad';
|
||||
|
||||
export const seedApiKey = async (
|
||||
entityManager: EntityManager,
|
||||
schemaName: string,
|
||||
) => {
|
||||
await entityManager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.${tableName}`, ['id', 'name', 'expiresAt'])
|
||||
.orIgnore()
|
||||
.values([
|
||||
{
|
||||
id: API_KEY_ID,
|
||||
name: 'My api key',
|
||||
expiresAt: new Date(
|
||||
new Date().getTime() + 1000 * 60 * 60 * 24 * 365 * 100, // In 100 years
|
||||
),
|
||||
},
|
||||
])
|
||||
.execute();
|
||||
};
|
||||
@ -42,6 +42,7 @@ export class ProcessNestedRelationsV2Helper {
|
||||
authContext,
|
||||
dataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks,
|
||||
}: {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps;
|
||||
@ -52,6 +53,7 @@ export class ProcessNestedRelationsV2Helper {
|
||||
limit: number;
|
||||
authContext: AuthContext;
|
||||
dataSource: WorkspaceDataSource;
|
||||
shouldBypassPermissionChecks: boolean;
|
||||
roleId?: string;
|
||||
}): Promise<void> {
|
||||
const processRelationTasks = Object.entries(relations).map(
|
||||
@ -67,6 +69,7 @@ export class ProcessNestedRelationsV2Helper {
|
||||
limit,
|
||||
authContext,
|
||||
dataSource,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
}),
|
||||
);
|
||||
@ -85,6 +88,7 @@ export class ProcessNestedRelationsV2Helper {
|
||||
limit,
|
||||
authContext,
|
||||
dataSource,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
}: {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
@ -97,6 +101,7 @@ export class ProcessNestedRelationsV2Helper {
|
||||
limit: number;
|
||||
authContext: AuthContext;
|
||||
dataSource: WorkspaceDataSource;
|
||||
shouldBypassPermissionChecks: boolean;
|
||||
roleId?: string;
|
||||
}): Promise<void> {
|
||||
const sourceFieldMetadata =
|
||||
@ -129,6 +134,7 @@ export class ProcessNestedRelationsV2Helper {
|
||||
|
||||
const targetObjectRepository = dataSource.getRepository(
|
||||
targetObjectMetadata.nameSingular,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
);
|
||||
|
||||
@ -199,6 +205,8 @@ export class ProcessNestedRelationsV2Helper {
|
||||
limit,
|
||||
authContext,
|
||||
dataSource,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,6 +46,7 @@ export class ProcessNestedRelationsHelper {
|
||||
authContext,
|
||||
dataSource,
|
||||
isNewRelationEnabled,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
}: {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
@ -58,6 +59,7 @@ export class ProcessNestedRelationsHelper {
|
||||
authContext: AuthContext;
|
||||
dataSource: WorkspaceDataSource;
|
||||
isNewRelationEnabled: boolean;
|
||||
shouldBypassPermissionChecks: boolean;
|
||||
roleId?: string;
|
||||
}): Promise<void> {
|
||||
if (isNewRelationEnabled) {
|
||||
@ -71,6 +73,7 @@ export class ProcessNestedRelationsHelper {
|
||||
limit,
|
||||
authContext,
|
||||
dataSource,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
});
|
||||
}
|
||||
@ -89,6 +92,7 @@ export class ProcessNestedRelationsHelper {
|
||||
authContext,
|
||||
dataSource,
|
||||
isNewRelationEnabled,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
}),
|
||||
);
|
||||
@ -108,6 +112,7 @@ export class ProcessNestedRelationsHelper {
|
||||
authContext,
|
||||
dataSource,
|
||||
isNewRelationEnabled,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
}: {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
@ -118,9 +123,10 @@ export class ProcessNestedRelationsHelper {
|
||||
nestedRelations: any;
|
||||
aggregate: Record<string, AggregationField>;
|
||||
limit: number;
|
||||
authContext: any;
|
||||
authContext: AuthContext;
|
||||
dataSource: DataSource;
|
||||
isNewRelationEnabled: boolean;
|
||||
shouldBypassPermissionChecks: boolean;
|
||||
roleId?: string;
|
||||
}): Promise<void> {
|
||||
const relationFieldMetadata =
|
||||
@ -148,6 +154,7 @@ export class ProcessNestedRelationsHelper {
|
||||
authContext,
|
||||
dataSource,
|
||||
isNewRelationEnabled,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
});
|
||||
}
|
||||
@ -164,6 +171,7 @@ export class ProcessNestedRelationsHelper {
|
||||
authContext,
|
||||
dataSource,
|
||||
isNewRelationEnabled,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
}: {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
@ -177,6 +185,7 @@ export class ProcessNestedRelationsHelper {
|
||||
authContext: AuthContext;
|
||||
dataSource: WorkspaceDataSource;
|
||||
isNewRelationEnabled: boolean;
|
||||
shouldBypassPermissionChecks: boolean;
|
||||
roleId?: string;
|
||||
}): Promise<void> {
|
||||
const { inverseRelationName, referenceObjectMetadata } =
|
||||
@ -188,6 +197,7 @@ export class ProcessNestedRelationsHelper {
|
||||
|
||||
const relationRepository = dataSource.getRepository(
|
||||
referenceObjectMetadata.nameSingular,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
);
|
||||
|
||||
@ -248,6 +258,8 @@ export class ProcessNestedRelationsHelper {
|
||||
authContext,
|
||||
dataSource,
|
||||
isNewRelationEnabled,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -264,6 +276,7 @@ export class ProcessNestedRelationsHelper {
|
||||
authContext,
|
||||
dataSource,
|
||||
isNewRelationEnabled,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
}: {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
@ -277,6 +290,7 @@ export class ProcessNestedRelationsHelper {
|
||||
authContext: any;
|
||||
dataSource: WorkspaceDataSource;
|
||||
isNewRelationEnabled: boolean;
|
||||
shouldBypassPermissionChecks: boolean;
|
||||
roleId?: string;
|
||||
}): Promise<void> {
|
||||
const { referenceObjectMetadata } = this.getRelationMetadata({
|
||||
@ -287,6 +301,7 @@ export class ProcessNestedRelationsHelper {
|
||||
|
||||
const relationRepository = dataSource.getRepository(
|
||||
referenceObjectMetadata.nameSingular,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
);
|
||||
|
||||
@ -346,6 +361,8 @@ export class ProcessNestedRelationsHelper {
|
||||
authContext,
|
||||
dataSource,
|
||||
isNewRelationEnabled,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,6 +45,7 @@ export type GraphqlQueryResolverExecutionArgs<Input extends ResolverArgs> = {
|
||||
repository: WorkspaceRepository<ObjectLiteral>;
|
||||
graphqlQueryParser: GraphqlQueryParser;
|
||||
graphqlQuerySelectedFieldsResult: GraphqlQuerySelectedFieldsResult;
|
||||
isExecutedByApiKey: boolean;
|
||||
roleId?: string;
|
||||
};
|
||||
|
||||
@ -123,8 +124,12 @@ export abstract class GraphqlQueryBaseResolverService<
|
||||
workspaceId: authContext.workspace.id,
|
||||
});
|
||||
|
||||
const executedByApiKey = isDefined(authContext.apiKey);
|
||||
const shouldBypassPermissionChecks = executedByApiKey;
|
||||
|
||||
const repository = dataSource.getRepository(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
);
|
||||
|
||||
@ -150,6 +155,7 @@ export abstract class GraphqlQueryBaseResolverService<
|
||||
repository,
|
||||
graphqlQueryParser,
|
||||
graphqlQuerySelectedFieldsResult,
|
||||
isExecutedByApiKey: executedByApiKey,
|
||||
roleId,
|
||||
};
|
||||
|
||||
|
||||
@ -53,12 +53,15 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
||||
objectMetadataItemWithFieldMaps,
|
||||
);
|
||||
|
||||
const shouldBypassPermissionChecks = executionArgs.isExecutedByApiKey;
|
||||
|
||||
await this.processNestedRelationsIfNeeded(
|
||||
executionArgs,
|
||||
upsertedRecords,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
featureFlagsMap,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
);
|
||||
|
||||
@ -329,6 +332,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps: ObjectMetadataMaps,
|
||||
featureFlagsMap: Record<FeatureFlagKey, boolean>,
|
||||
shouldBypassPermissionChecks: boolean,
|
||||
roleId?: string,
|
||||
): Promise<void> {
|
||||
if (!executionArgs.graphqlQuerySelectedFieldsResult.relations) {
|
||||
@ -346,6 +350,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
||||
isNewRelationEnabled:
|
||||
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
|
||||
roleId,
|
||||
shouldBypassPermissionChecks,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -74,6 +74,7 @@ export class GraphqlQueryCreateOneResolverService extends GraphqlQueryBaseResolv
|
||||
isNewRelationEnabled:
|
||||
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
|
||||
roleId,
|
||||
shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -75,6 +75,7 @@ export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResol
|
||||
isNewRelationEnabled:
|
||||
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
|
||||
roleId,
|
||||
shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -77,6 +77,7 @@ export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolv
|
||||
isNewRelationEnabled:
|
||||
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
|
||||
roleId,
|
||||
shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -73,6 +73,7 @@ export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseReso
|
||||
isNewRelationEnabled:
|
||||
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
|
||||
roleId,
|
||||
shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -73,6 +73,7 @@ export class GraphqlQueryDestroyOneResolverService extends GraphqlQueryBaseResol
|
||||
isNewRelationEnabled:
|
||||
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
|
||||
roleId,
|
||||
shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -160,6 +160,7 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve
|
||||
isNewRelationEnabled:
|
||||
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
|
||||
roleId,
|
||||
shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -83,6 +83,7 @@ export class GraphqlQueryFindOneResolverService extends GraphqlQueryBaseResolver
|
||||
isNewRelationEnabled:
|
||||
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
|
||||
roleId,
|
||||
shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -75,6 +75,7 @@ export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseReso
|
||||
isNewRelationEnabled:
|
||||
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
|
||||
roleId,
|
||||
shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -77,6 +77,7 @@ export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResol
|
||||
isNewRelationEnabled:
|
||||
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
|
||||
roleId,
|
||||
shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -113,6 +113,7 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol
|
||||
isNewRelationEnabled:
|
||||
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
|
||||
roleId,
|
||||
shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -107,6 +107,7 @@ export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolv
|
||||
isNewRelationEnabled:
|
||||
featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled],
|
||||
roleId,
|
||||
shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,16 +1,17 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { Request } from 'express';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service';
|
||||
import { CoreQueryBuilderFactory } from 'src/engine/api/rest/core/query-builder/core-query-builder.factory';
|
||||
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
|
||||
import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
@Injectable()
|
||||
export class RestApiCoreServiceV2 {
|
||||
@ -19,6 +20,7 @@ export class RestApiCoreServiceV2 {
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly recordInputTransformerService: RecordInputTransformerService,
|
||||
protected readonly apiEventEmitterService: ApiEventEmitterService,
|
||||
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
|
||||
) {}
|
||||
|
||||
async delete(request: Request) {
|
||||
@ -137,7 +139,7 @@ export class RestApiCoreServiceV2 {
|
||||
}
|
||||
|
||||
private async getRepositoryAndMetadataOrFail(request: Request) {
|
||||
const { workspace } = request;
|
||||
const { workspace, apiKey, userWorkspaceId } = request;
|
||||
const { object: parsedObject } = parseCorePath(request);
|
||||
|
||||
const objectMetadata = await this.coreQueryBuilderFactory.getObjectMetadata(
|
||||
@ -153,13 +155,25 @@ export class RestApiCoreServiceV2 {
|
||||
throw new BadRequestException('Workspace not found');
|
||||
}
|
||||
|
||||
const dataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace(workspace.id);
|
||||
|
||||
const objectMetadataNameSingular =
|
||||
objectMetadata.objectMetadataMapItem.nameSingular;
|
||||
const repository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ObjectRecord>(
|
||||
workspace.id,
|
||||
objectMetadataNameSingular,
|
||||
);
|
||||
|
||||
const shouldBypassPermissionChecks = !!apiKey;
|
||||
|
||||
const roleId =
|
||||
await this.workspacePermissionsCacheService.getRoleIdFromUserWorkspaceId({
|
||||
workspaceId: workspace.id,
|
||||
userWorkspaceId,
|
||||
});
|
||||
|
||||
const repository = dataSource.getRepository<ObjectRecord>(
|
||||
objectMetadataNameSingular,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
);
|
||||
|
||||
return {
|
||||
objectMetadataNameSingular,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service';
|
||||
import { RestApiCoreBatchController } from 'src/engine/api/rest/core/controllers/rest-api-core-batch.controller';
|
||||
import { RestApiCoreController } from 'src/engine/api/rest/core/controllers/rest-api-core.controller';
|
||||
import { CoreQueryBuilderModule } from 'src/engine/api/rest/core/query-builder/core-query-builder.module';
|
||||
@ -15,9 +16,9 @@ import { RestApiMetadataService } from 'src/engine/api/rest/metadata/rest-api-me
|
||||
import { RestApiService } from 'src/engine/api/rest/rest-api.service';
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module';
|
||||
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -28,6 +29,7 @@ import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-run
|
||||
HttpModule,
|
||||
TwentyORMModule,
|
||||
RecordTransformerModule,
|
||||
WorkspacePermissionsCacheModule,
|
||||
],
|
||||
controllers: [
|
||||
RestApiMetadataController,
|
||||
|
||||
@ -72,7 +72,6 @@ export class AccessTokenService {
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workspaceMember',
|
||||
false,
|
||||
);
|
||||
|
||||
const workspaceMember = await workspaceMemberRepository.findOne({
|
||||
|
||||
@ -5,6 +5,7 @@ import { Repository } from 'typeorm';
|
||||
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
import { USER_SIGNUP_EVENT_NAME } from 'src/engine/api/graphql/workspace-query-runner/constants/user-signup-event-name.constants';
|
||||
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
@ -12,6 +13,7 @@ import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/use
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { PermissionsException } from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
@ -19,8 +21,6 @@ import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||
import { USER_SIGNUP_EVENT_NAME } from 'src/engine/api/graphql/workspace-query-runner/constants/user-signup-event-name.constants';
|
||||
|
||||
describe('UserWorkspaceService', () => {
|
||||
let service: UserWorkspaceService;
|
||||
|
||||
@ -20,6 +20,7 @@ import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { userValidator } from 'src/engine/core-modules/user/user.validate';
|
||||
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import {
|
||||
@ -32,7 +33,6 @@ import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.
|
||||
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { assert } from 'src/utils/assert';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
|
||||
export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
|
||||
constructor(
|
||||
|
||||
@ -31,6 +31,7 @@ import { RemoteTableRelationsModule } from 'src/engine/metadata-modules/remote-s
|
||||
import { SearchVectorModule } from 'src/engine/metadata-modules/search-vector/search-vector.module';
|
||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
|
||||
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
||||
|
||||
import { ObjectMetadataEntity } from './object-metadata.entity';
|
||||
@ -59,6 +60,7 @@ import { UpdateObjectPayload } from './dtos/update-object.input';
|
||||
IndexMetadataModule,
|
||||
FeatureFlagModule,
|
||||
PermissionsModule,
|
||||
WorkspacePermissionsCacheModule,
|
||||
],
|
||||
services: [
|
||||
ObjectMetadataService,
|
||||
|
||||
@ -40,6 +40,7 @@ import { RemoteTableRelationsService } from 'src/engine/metadata-modules/remote-
|
||||
import { SearchVectorService } from 'src/engine/metadata-modules/search-vector/search-vector.service';
|
||||
import { validateNameAndLabelAreSyncOrThrow } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
|
||||
import { CUSTOM_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
@ -68,6 +69,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
private readonly objectMetadataMigrationService: ObjectMetadataMigrationService,
|
||||
private readonly objectMetadataRelatedRecordsService: ObjectMetadataRelatedRecordsService,
|
||||
private readonly indexMetadataService: IndexMetadataService,
|
||||
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {
|
||||
super(objectMetadataRepository);
|
||||
@ -236,6 +238,10 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
objectMetadataInput.workspaceId,
|
||||
);
|
||||
|
||||
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
|
||||
workspaceId: objectMetadataInput.workspaceId,
|
||||
});
|
||||
|
||||
return createdObjectMetadata;
|
||||
}
|
||||
|
||||
@ -441,6 +447,10 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return objectMetadata;
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
ObjectRecordsPermissions,
|
||||
ObjectRecordsPermissionsByRoleId,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
@ -51,19 +52,15 @@ export class WorkspacePermissionsCacheService {
|
||||
ignoreLock?: boolean;
|
||||
roleIds?: string[];
|
||||
}): Promise<void> {
|
||||
const isPermissionsV2Enabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IsPermissionsV2Enabled,
|
||||
workspaceId,
|
||||
);
|
||||
if (!ignoreLock) {
|
||||
const isAlreadyCaching =
|
||||
await this.workspacePermissionsCacheStorageService.getRolesPermissionsOngoingCachingLock(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const isAlreadyCaching =
|
||||
await this.workspacePermissionsCacheStorageService.getRolesPermissionsOngoingCachingLock(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!ignoreLock && isAlreadyCaching) {
|
||||
return;
|
||||
if (isAlreadyCaching) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await this.workspacePermissionsCacheStorageService.addRolesPermissionsOngoingCachingLock(
|
||||
@ -80,6 +77,12 @@ export class WorkspacePermissionsCacheService {
|
||||
);
|
||||
}
|
||||
|
||||
const isPermissionsV2Enabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IsPermissionsV2Enabled,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const recomputedRolesPermissions =
|
||||
await this.getObjectRecordPermissionsForRoles({
|
||||
workspaceId,
|
||||
@ -109,13 +112,15 @@ export class WorkspacePermissionsCacheService {
|
||||
workspaceId: string;
|
||||
ignoreLock?: boolean;
|
||||
}): Promise<void> {
|
||||
const isAlreadyCaching =
|
||||
await this.workspacePermissionsCacheStorageService.getUserWorkspaceRoleMapOngoingCachingLock(
|
||||
workspaceId,
|
||||
);
|
||||
if (!ignoreLock) {
|
||||
const isAlreadyCaching =
|
||||
await this.workspacePermissionsCacheStorageService.getUserWorkspaceRoleMapOngoingCachingLock(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!ignoreLock && isAlreadyCaching) {
|
||||
return;
|
||||
if (isAlreadyCaching) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await this.workspacePermissionsCacheStorageService.addUserWorkspaceRoleMapOngoingCachingLock(
|
||||
@ -183,6 +188,24 @@ export class WorkspacePermissionsCacheService {
|
||||
});
|
||||
}
|
||||
|
||||
async getRoleIdFromUserWorkspaceId({
|
||||
workspaceId,
|
||||
userWorkspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
userWorkspaceId?: string;
|
||||
}): Promise<string | undefined> {
|
||||
if (!isDefined(userWorkspaceId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userWorkspaceRoleMap = await this.getUserWorkspaceRoleMapFromCache({
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return userWorkspaceRoleMap[userWorkspaceId];
|
||||
}
|
||||
|
||||
private async getObjectRecordPermissionsForRoles({
|
||||
workspaceId,
|
||||
isPermissionsV2Enabled,
|
||||
|
||||
@ -41,10 +41,19 @@ export class WorkspaceDataSource extends DataSource {
|
||||
|
||||
override getRepository<Entity extends ObjectLiteral>(
|
||||
target: EntityTarget<Entity>,
|
||||
shouldBypassPermissionChecks = false,
|
||||
roleId?: string,
|
||||
): WorkspaceRepository<Entity> {
|
||||
if (shouldBypassPermissionChecks === true) {
|
||||
return this.manager.getRepository(target, shouldBypassPermissionChecks);
|
||||
}
|
||||
|
||||
if (roleId) {
|
||||
return this.manager.getRepository(target, roleId);
|
||||
return this.manager.getRepository(
|
||||
target,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
);
|
||||
}
|
||||
|
||||
return this.manager.getRepository(target);
|
||||
@ -64,11 +73,11 @@ export class WorkspaceDataSource extends DataSource {
|
||||
this.permissionsPerRoleId = permissionsPerRoleId;
|
||||
}
|
||||
|
||||
setFeatureFlagsMap(featureFlagMap: FeatureFlagMap) {
|
||||
setFeatureFlagMap(featureFlagMap: FeatureFlagMap) {
|
||||
this.featureFlagMap = featureFlagMap;
|
||||
}
|
||||
|
||||
setFeatureFlagsMapVersion(featureFlagMapVersion: string) {
|
||||
setFeatureFlagMapVersion(featureFlagMapVersion: string) {
|
||||
this.featureFlagMapVersion = featureFlagMapVersion;
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,10 +28,17 @@ export class WorkspaceEntityManager extends EntityManager {
|
||||
|
||||
override getRepository<Entity extends ObjectLiteral>(
|
||||
target: EntityTarget<Entity>,
|
||||
shouldBypassPermissionChecks = false,
|
||||
roleId?: string,
|
||||
): WorkspaceRepository<Entity> {
|
||||
const dataSource = this.connection as WorkspaceDataSource;
|
||||
const repositoryKey = `${dataSource.getMetadata(target).name}_${roleId ?? 'default'}${dataSource.rolesPermissionsVersion ? `_${dataSource.rolesPermissionsVersion}` : ''}${dataSource.featureFlagMapVersion ? `_${dataSource.featureFlagMapVersion}` : ''}`;
|
||||
|
||||
const repositoryKey = this.getRepositoryKey({
|
||||
target,
|
||||
dataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks,
|
||||
});
|
||||
const repoFromMap = this.repositories.get(repositoryKey);
|
||||
|
||||
if (repoFromMap) {
|
||||
@ -53,10 +60,36 @@ export class WorkspaceEntityManager extends EntityManager {
|
||||
dataSource.featureFlagMap,
|
||||
this.queryRunner,
|
||||
objectPermissions,
|
||||
shouldBypassPermissionChecks,
|
||||
);
|
||||
|
||||
this.repositories.set(repositoryKey, newRepository);
|
||||
|
||||
return newRepository;
|
||||
}
|
||||
|
||||
private getRepositoryKey({
|
||||
target,
|
||||
dataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks,
|
||||
}: {
|
||||
target: EntityTarget<any>;
|
||||
dataSource: WorkspaceDataSource;
|
||||
shouldBypassPermissionChecks: boolean;
|
||||
roleId?: string;
|
||||
}) {
|
||||
const repositoryPrefix = dataSource.getMetadata(target).name;
|
||||
const roleIdSuffix = roleId ? `_${roleId}` : '';
|
||||
const rolesPermissionsVersionSuffix = dataSource.rolesPermissionsVersion
|
||||
? `_${dataSource.rolesPermissionsVersion}`
|
||||
: '';
|
||||
const featureFlagMapVersionSuffix = dataSource.featureFlagMapVersion
|
||||
? `_${dataSource.featureFlagMapVersion}`
|
||||
: '';
|
||||
|
||||
return shouldBypassPermissionChecks
|
||||
? `${repositoryPrefix}_bypass${featureFlagMapVersionSuffix}`
|
||||
: `${repositoryPrefix}${roleIdSuffix}${rolesPermissionsVersionSuffix}${featureFlagMapVersionSuffix}`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ export class ScopedWorkspaceContextFactory {
|
||||
workspaceId: string | null;
|
||||
workspaceMetadataVersion: number | null;
|
||||
userWorkspaceId: string | null;
|
||||
isExecutedByApiKey: boolean;
|
||||
} {
|
||||
const workspaceId: string | undefined =
|
||||
this.request?.['req']?.['workspaceId'] ||
|
||||
@ -24,6 +25,7 @@ export class ScopedWorkspaceContextFactory {
|
||||
workspaceId: workspaceId ?? null,
|
||||
workspaceMetadataVersion: workspaceMetadataVersion ?? null,
|
||||
userWorkspaceId: this.request?.['req']?.['userWorkspaceId'] ?? null,
|
||||
isExecutedByApiKey: !!this.request?.['req']?.['apiKey'],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -294,9 +294,9 @@ export class WorkspaceDatasourceFactory {
|
||||
currentVersion: workspaceDataSource.featureFlagMapVersion,
|
||||
newVersion: cachedFeatureFlagMapVersion,
|
||||
newData: cachedFeatureFlagMap,
|
||||
setData: (data) => workspaceDataSource.setFeatureFlagsMap(data),
|
||||
setData: (data) => workspaceDataSource.setFeatureFlagMap(data),
|
||||
setVersion: (version) =>
|
||||
workspaceDataSource.setFeatureFlagsMapVersion(version),
|
||||
workspaceDataSource.setFeatureFlagMapVersion(version),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
PermissionsExceptionCode,
|
||||
PermissionsExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
|
||||
const getTargetEntityAndOperationType = (expressionMap: QueryExpressionMap) => {
|
||||
const mainEntity = expressionMap.aliases[0].metadata.name;
|
||||
@ -20,10 +21,26 @@ const getTargetEntityAndOperationType = (expressionMap: QueryExpressionMap) => {
|
||||
export const validateQueryIsPermittedOrThrow = (
|
||||
expressionMap: QueryExpressionMap,
|
||||
objectRecordsPermissions: ObjectRecordsPermissions,
|
||||
objectMetadataMaps: ObjectMetadataMaps,
|
||||
shouldBypassPermissionChecks: boolean,
|
||||
) => {
|
||||
if (shouldBypassPermissionChecks) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { mainEntity, operationType } =
|
||||
getTargetEntityAndOperationType(expressionMap);
|
||||
|
||||
const objectMetadataIdForEntity =
|
||||
objectMetadataMaps.idByNameSingular[mainEntity];
|
||||
|
||||
const objectMetadataIsSystem =
|
||||
objectMetadataMaps.byId[objectMetadataIdForEntity]?.isSystem === true;
|
||||
|
||||
if (objectMetadataIsSystem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const permissionsForEntity = objectRecordsPermissions[mainEntity];
|
||||
|
||||
switch (operationType) {
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { ObjectRecordsPermissions } from 'twenty-shared/types';
|
||||
import { ObjectLiteral, SelectQueryBuilder } from 'typeorm';
|
||||
|
||||
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
|
||||
|
||||
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
|
||||
|
||||
export class WorkspaceQueryBuilder<
|
||||
@ -9,9 +11,15 @@ export class WorkspaceQueryBuilder<
|
||||
constructor(
|
||||
queryBuilder: SelectQueryBuilder<T>,
|
||||
objectRecordsPermissions: ObjectRecordsPermissions,
|
||||
internalContext: WorkspaceInternalContext,
|
||||
shouldBypassPermissionChecks: boolean,
|
||||
) {
|
||||
super(queryBuilder, objectRecordsPermissions);
|
||||
this.objectRecordsPermissions = objectRecordsPermissions;
|
||||
super(
|
||||
queryBuilder,
|
||||
objectRecordsPermissions,
|
||||
internalContext,
|
||||
shouldBypassPermissionChecks,
|
||||
);
|
||||
}
|
||||
|
||||
override clone(): this {
|
||||
@ -20,6 +28,8 @@ export class WorkspaceQueryBuilder<
|
||||
return new WorkspaceQueryBuilder(
|
||||
clonedQueryBuilder,
|
||||
this.objectRecordsPermissions,
|
||||
this.internalContext,
|
||||
this.shouldBypassPermissionChecks,
|
||||
) as this;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@ import { ObjectRecordsPermissions } from 'twenty-shared/types';
|
||||
import { ObjectLiteral, SelectQueryBuilder, UpdateQueryBuilder } from 'typeorm';
|
||||
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||
|
||||
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
|
||||
|
||||
import { validateQueryIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.util';
|
||||
import { WorkspaceUpdateQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-update-query-builder';
|
||||
|
||||
@ -9,12 +11,18 @@ export class WorkspaceSelectQueryBuilder<
|
||||
T extends ObjectLiteral,
|
||||
> extends SelectQueryBuilder<T> {
|
||||
objectRecordsPermissions: ObjectRecordsPermissions;
|
||||
shouldBypassPermissionChecks: boolean;
|
||||
internalContext: WorkspaceInternalContext;
|
||||
constructor(
|
||||
queryBuilder: SelectQueryBuilder<T>,
|
||||
objectRecordsPermissions: ObjectRecordsPermissions,
|
||||
internalContext: WorkspaceInternalContext,
|
||||
shouldBypassPermissionChecks: boolean,
|
||||
) {
|
||||
super(queryBuilder);
|
||||
this.objectRecordsPermissions = objectRecordsPermissions;
|
||||
this.internalContext = internalContext;
|
||||
this.shouldBypassPermissionChecks = shouldBypassPermissionChecks;
|
||||
}
|
||||
|
||||
override update(): WorkspaceUpdateQueryBuilder<T>;
|
||||
@ -33,6 +41,8 @@ export class WorkspaceSelectQueryBuilder<
|
||||
return new WorkspaceUpdateQueryBuilder<T>(
|
||||
updateQueryBuilder,
|
||||
this.objectRecordsPermissions,
|
||||
this.internalContext,
|
||||
this.shouldBypassPermissionChecks,
|
||||
);
|
||||
}
|
||||
|
||||
@ -40,6 +50,8 @@ export class WorkspaceSelectQueryBuilder<
|
||||
validateQueryIsPermittedOrThrow(
|
||||
this.expressionMap,
|
||||
this.objectRecordsPermissions,
|
||||
this.internalContext.objectMetadataMaps,
|
||||
this.shouldBypassPermissionChecks,
|
||||
);
|
||||
|
||||
return super.execute();
|
||||
@ -49,6 +61,8 @@ export class WorkspaceSelectQueryBuilder<
|
||||
validateQueryIsPermittedOrThrow(
|
||||
this.expressionMap,
|
||||
this.objectRecordsPermissions,
|
||||
this.internalContext.objectMetadataMaps,
|
||||
this.shouldBypassPermissionChecks,
|
||||
);
|
||||
|
||||
return super.getMany();
|
||||
|
||||
@ -1,24 +1,34 @@
|
||||
import { ObjectRecordsPermissions } from 'twenty-shared/types';
|
||||
import { ObjectLiteral, UpdateQueryBuilder, UpdateResult } from 'typeorm';
|
||||
|
||||
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
|
||||
|
||||
import { validateQueryIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.util';
|
||||
|
||||
export class WorkspaceUpdateQueryBuilder<
|
||||
Entity extends ObjectLiteral,
|
||||
> extends UpdateQueryBuilder<Entity> {
|
||||
private objectRecordsPermissions: ObjectRecordsPermissions;
|
||||
private shouldBypassPermissionChecks: boolean;
|
||||
private internalContext: WorkspaceInternalContext;
|
||||
constructor(
|
||||
queryBuilder: UpdateQueryBuilder<Entity>,
|
||||
objectRecordsPermissions: ObjectRecordsPermissions,
|
||||
internalContext: WorkspaceInternalContext,
|
||||
shouldBypassPermissionChecks: boolean,
|
||||
) {
|
||||
super(queryBuilder);
|
||||
this.objectRecordsPermissions = objectRecordsPermissions;
|
||||
this.internalContext = internalContext;
|
||||
this.shouldBypassPermissionChecks = shouldBypassPermissionChecks;
|
||||
}
|
||||
|
||||
override execute(): Promise<UpdateResult> {
|
||||
validateQueryIsPermittedOrThrow(
|
||||
this.expressionMap,
|
||||
this.objectRecordsPermissions,
|
||||
this.internalContext.objectMetadataMaps,
|
||||
this.shouldBypassPermissionChecks,
|
||||
);
|
||||
|
||||
return super.execute();
|
||||
|
||||
@ -36,9 +36,9 @@ export class WorkspaceRepository<
|
||||
T extends ObjectLiteral,
|
||||
> extends Repository<T> {
|
||||
private readonly internalContext: WorkspaceInternalContext;
|
||||
private shouldBypassPermissionChecks: boolean;
|
||||
private featureFlagMap: FeatureFlagMap;
|
||||
private objectRecordsPermissions?: ObjectRecordsPermissions;
|
||||
|
||||
constructor(
|
||||
internalContext: WorkspaceInternalContext,
|
||||
target: EntityTarget<T>,
|
||||
@ -46,11 +46,13 @@ export class WorkspaceRepository<
|
||||
featureFlagMap: FeatureFlagMap,
|
||||
queryRunner?: QueryRunner,
|
||||
objectRecordsPermissions?: ObjectRecordsPermissions,
|
||||
shouldBypassPermissionChecks = false,
|
||||
) {
|
||||
super(target, manager, queryRunner);
|
||||
this.internalContext = internalContext;
|
||||
this.featureFlagMap = featureFlagMap;
|
||||
this.objectRecordsPermissions = objectRecordsPermissions;
|
||||
this.shouldBypassPermissionChecks = shouldBypassPermissionChecks;
|
||||
}
|
||||
|
||||
override createQueryBuilder<U extends T>(
|
||||
@ -74,6 +76,8 @@ export class WorkspaceRepository<
|
||||
return new WorkspaceQueryBuilder(
|
||||
queryBuilder,
|
||||
this.objectRecordsPermissions,
|
||||
this.internalContext,
|
||||
this.shouldBypassPermissionChecks,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,19 +15,31 @@ export class TwentyORMGlobalManager {
|
||||
async getRepositoryForWorkspace<T extends ObjectLiteral>(
|
||||
workspaceId: string,
|
||||
workspaceEntity: Type<T>,
|
||||
shouldFailIfMetadataNotFound?: boolean,
|
||||
options?: {
|
||||
shouldBypassPermissionChecks?: boolean;
|
||||
shouldFailIfMetadataNotFound?: boolean;
|
||||
},
|
||||
): Promise<WorkspaceRepository<T>>;
|
||||
|
||||
async getRepositoryForWorkspace<T extends ObjectLiteral>(
|
||||
workspaceId: string,
|
||||
objectMetadataName: string,
|
||||
shouldFailIfMetadataNotFound?: boolean,
|
||||
options?: {
|
||||
shouldBypassPermissionChecks?: boolean;
|
||||
shouldFailIfMetadataNotFound?: boolean;
|
||||
},
|
||||
): Promise<WorkspaceRepository<T>>;
|
||||
|
||||
async getRepositoryForWorkspace<T extends ObjectLiteral>(
|
||||
workspaceId: string,
|
||||
workspaceEntityOrobjectMetadataName: Type<T> | string,
|
||||
shouldFailIfMetadataNotFound = true,
|
||||
options: {
|
||||
shouldBypassPermissionChecks?: boolean;
|
||||
shouldFailIfMetadataNotFound?: boolean;
|
||||
} = {
|
||||
shouldBypassPermissionChecks: false,
|
||||
shouldFailIfMetadataNotFound: true,
|
||||
},
|
||||
): Promise<WorkspaceRepository<T>> {
|
||||
let objectMetadataName: string;
|
||||
|
||||
@ -42,10 +54,13 @@ export class TwentyORMGlobalManager {
|
||||
const workspaceDataSource = await this.workspaceDataSourceFactory.create(
|
||||
workspaceId,
|
||||
null,
|
||||
shouldFailIfMetadataNotFound,
|
||||
options.shouldFailIfMetadataNotFound,
|
||||
);
|
||||
|
||||
const repository = workspaceDataSource.getRepository<T>(objectMetadataName);
|
||||
const repository = workspaceDataSource.getRepository<T>(
|
||||
objectMetadataName,
|
||||
options.shouldBypassPermissionChecks,
|
||||
);
|
||||
|
||||
return repository;
|
||||
}
|
||||
|
||||
@ -30,8 +30,12 @@ export class TwentyORMManager {
|
||||
async getRepository<T extends ObjectLiteral>(
|
||||
workspaceEntityOrobjectMetadataName: Type<T> | string,
|
||||
): Promise<WorkspaceRepository<T>> {
|
||||
const { workspaceId, workspaceMetadataVersion, userWorkspaceId } =
|
||||
this.scopedWorkspaceContextFactory.create();
|
||||
const {
|
||||
workspaceId,
|
||||
workspaceMetadataVersion,
|
||||
userWorkspaceId,
|
||||
isExecutedByApiKey,
|
||||
} = this.scopedWorkspaceContextFactory.create();
|
||||
|
||||
let objectMetadataName: string;
|
||||
|
||||
@ -65,7 +69,13 @@ export class TwentyORMManager {
|
||||
roleId = userWorkspaceRole?.roleId;
|
||||
}
|
||||
|
||||
return workspaceDataSource.getRepository<T>(objectMetadataName, roleId);
|
||||
const shouldBypassPermissionChecks = !!isExecutedByApiKey;
|
||||
|
||||
return workspaceDataSource.getRepository<T>(
|
||||
objectMetadataName,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
);
|
||||
}
|
||||
|
||||
async getDatasource() {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Any, Repository } from 'typeorm';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { Any, Repository } from 'typeorm';
|
||||
|
||||
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Equal, Repository } from 'typeorm';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { Equal, Repository } from 'typeorm';
|
||||
|
||||
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
|
||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
||||
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
|
||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
|
||||
@Injectable()
|
||||
export class ConnectedAccountListener {
|
||||
|
||||
@ -55,6 +55,9 @@ export class CreateCompanyAndContactService {
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
PersonWorkspaceEntity,
|
||||
{
|
||||
shouldBypassPermissionChecks: true,
|
||||
},
|
||||
);
|
||||
|
||||
const workspaceMembers =
|
||||
|
||||
@ -2,9 +2,9 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import uniqBy from 'lodash.uniqby';
|
||||
import { DeepPartial, EntityManager, ILike } from 'typeorm';
|
||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
import { TWENTY_COMPANIES_BASE_URL } from 'twenty-shared/constants';
|
||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
import { DeepPartial, EntityManager, ILike } from 'typeorm';
|
||||
|
||||
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
@ -49,6 +49,9 @@ export class CreateCompanyService {
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
CompanyWorkspaceEntity,
|
||||
{
|
||||
shouldBypassPermissionChecks: true,
|
||||
},
|
||||
);
|
||||
|
||||
// Avoid creating duplicate companies
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
import { DeepPartial, EntityManager } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
|
||||
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
@ -82,6 +82,9 @@ export class CreateContactService {
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
PersonWorkspaceEntity,
|
||||
{
|
||||
shouldBypassPermissionChecks: true,
|
||||
},
|
||||
);
|
||||
|
||||
const lastPersonPosition = await this.getLastPersonPosition(
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
|
||||
@ -2,8 +2,8 @@ import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import snakeCase from 'lodash.snakecase';
|
||||
import { Repository } from 'typeorm';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
|
||||
@ -8,7 +8,6 @@ import {
|
||||
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
@WorkspaceQueryHook(`view.deleteOne`)
|
||||
export class ViewDeleteOnePreQueryHook implements WorkspaceQueryHookInstance {
|
||||
constructor(
|
||||
@ -21,7 +20,6 @@ export class ViewDeleteOnePreQueryHook implements WorkspaceQueryHookInstance {
|
||||
payload: DeleteOneResolverArgs,
|
||||
): Promise<DeleteOneResolverArgs> {
|
||||
const targettedViewId = payload.id;
|
||||
|
||||
const viewRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
authContext.workspace.id,
|
||||
|
||||
@ -2,8 +2,8 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import DOMPurify from 'dompurify';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import { z } from 'zod';
|
||||
import { isDefined, isValidUuid } from 'twenty-shared/utils';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { WorkflowExecutor } from 'src/modules/workflow/workflow-executor/interfaces/workflow-executor.interface';
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
|
||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
||||
import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
|
||||
import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event';
|
||||
@ -16,8 +18,6 @@ import {
|
||||
WorkflowTriggerJob,
|
||||
WorkflowTriggerJobData,
|
||||
} from 'src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job';
|
||||
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
|
||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
|
||||
@Injectable()
|
||||
export class DatabaseEventTriggerListener {
|
||||
|
||||
@ -2,67 +2,219 @@ import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.constants';
|
||||
import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util';
|
||||
import { findOneOperationFactory } from 'test/integration/graphql/utils/find-one-operation-factory.util';
|
||||
import { makeGraphqlAPIRequestWithApiKey } from 'test/integration/graphql/utils/make-graphql-api-request-with-api-key.util';
|
||||
import { makeGraphqlAPIRequestWithGuestRole } from 'test/integration/graphql/utils/make-graphql-api-request-with-guest-role.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { updateFeatureFlagFactory } from 'test/integration/graphql/utils/update-feature-flag-factory.util';
|
||||
import { updateOneOperationFactory } from 'test/integration/graphql/utils/update-one-operation-factory.util';
|
||||
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/database/typeorm-seeds/core/workspaces';
|
||||
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
|
||||
describe('updateOneObjectRecordsPermissions', () => {
|
||||
const personId = randomUUID();
|
||||
|
||||
beforeAll(async () => {
|
||||
const createPersonOperation = createOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
data: {
|
||||
id: personId,
|
||||
jobTitle: 'Software Engineer',
|
||||
},
|
||||
});
|
||||
|
||||
await makeGraphqlAPIRequest(createPersonOperation);
|
||||
});
|
||||
|
||||
it('should throw a permission error when user does not have permission (guest role)', async () => {
|
||||
describe('permissions V2 disabled', () => {
|
||||
const personId = randomUUID();
|
||||
const graphqlOperation = updateOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
recordId: personId,
|
||||
data: {
|
||||
jobTitle: 'Senior Software Engineer',
|
||||
},
|
||||
|
||||
beforeAll(async () => {
|
||||
const createPersonOperation = createOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
data: {
|
||||
id: personId,
|
||||
jobTitle: 'Software Engineer',
|
||||
},
|
||||
});
|
||||
|
||||
await makeGraphqlAPIRequest(createPersonOperation);
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequestWithGuestRole(graphqlOperation);
|
||||
it('should throw a permission error when user does not have permission (guest role)', async () => {
|
||||
const graphqlOperation = updateOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
recordId: personId,
|
||||
data: {
|
||||
jobTitle: 'Senior Software Engineer',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.body.data).toStrictEqual({ updatePerson: null });
|
||||
expect(response.body.errors).toBeDefined();
|
||||
expect(response.body.errors[0].message).toBe(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
);
|
||||
expect(response.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
|
||||
const response =
|
||||
await makeGraphqlAPIRequestWithGuestRole(graphqlOperation);
|
||||
|
||||
expect(response.body.data).toStrictEqual({ updatePerson: null });
|
||||
expect(response.body.errors).toBeDefined();
|
||||
expect(response.body.errors[0].message).toBe(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
);
|
||||
expect(response.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
|
||||
});
|
||||
|
||||
it('should update an object record when user has permission (admin role)', async () => {
|
||||
const graphqlOperation = updateOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
recordId: personId,
|
||||
data: {
|
||||
jobTitle: 'Senior Software Engineer',
|
||||
},
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||
|
||||
expect(response.body.data).toBeDefined();
|
||||
expect(response.body.data.updatePerson).toBeDefined();
|
||||
expect(response.body.data.updatePerson.id).toBe(personId);
|
||||
expect(response.body.data.updatePerson.jobTitle).toBe(
|
||||
'Senior Software Engineer',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should update an object record when user has permission (admin role)', async () => {
|
||||
const graphqlOperation = updateOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
recordId: personId,
|
||||
data: {
|
||||
jobTitle: 'Senior Software Engineer',
|
||||
},
|
||||
describe('permissions V2 enabled', () => {
|
||||
const personId = randomUUID();
|
||||
let allPetsViewId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const createPersonOperation = createOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
data: {
|
||||
id: personId,
|
||||
jobTitle: 'Software Engineer',
|
||||
},
|
||||
});
|
||||
|
||||
await makeGraphqlAPIRequest(createPersonOperation);
|
||||
|
||||
const enablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IsPermissionsV2Enabled',
|
||||
true,
|
||||
);
|
||||
|
||||
await makeGraphqlAPIRequest(enablePermissionsQuery);
|
||||
|
||||
const findAllPetsViewOperation = findOneOperationFactory({
|
||||
objectMetadataSingularName: 'view',
|
||||
gqlFields: 'id',
|
||||
filter: {
|
||||
icon: {
|
||||
eq: 'IconCat',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const findAllPetsViewResponse = await makeGraphqlAPIRequest(
|
||||
findAllPetsViewOperation,
|
||||
);
|
||||
|
||||
allPetsViewId = findAllPetsViewResponse.body.data.view.id;
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||
afterAll(async () => {
|
||||
const disablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IsPermissionsV2Enabled',
|
||||
false,
|
||||
);
|
||||
|
||||
expect(response.body.data).toBeDefined();
|
||||
expect(response.body.data.updatePerson).toBeDefined();
|
||||
expect(response.body.data.updatePerson.id).toBe(personId);
|
||||
expect(response.body.data.updatePerson.jobTitle).toBe(
|
||||
'Senior Software Engineer',
|
||||
);
|
||||
await makeGraphqlAPIRequest(disablePermissionsQuery);
|
||||
|
||||
const updateViewOperation = updateOneOperationFactory({
|
||||
objectMetadataSingularName: 'view',
|
||||
gqlFields: 'id',
|
||||
recordId: allPetsViewId,
|
||||
data: {
|
||||
icon: 'IconCat',
|
||||
},
|
||||
});
|
||||
|
||||
await makeGraphqlAPIRequest(updateViewOperation);
|
||||
});
|
||||
|
||||
it('should throw a permission error when user does not have permission (guest role)', async () => {
|
||||
const graphqlOperation = updateOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
recordId: personId,
|
||||
data: {
|
||||
jobTitle: 'Senior Software Engineer',
|
||||
},
|
||||
});
|
||||
|
||||
const response =
|
||||
await makeGraphqlAPIRequestWithGuestRole(graphqlOperation);
|
||||
|
||||
expect(response.body.data).toStrictEqual({ updatePerson: null });
|
||||
expect(response.body.errors).toBeDefined();
|
||||
expect(response.body.errors[0].message).toBe(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
);
|
||||
expect(response.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
|
||||
});
|
||||
|
||||
it('should allow to update a system object record even without update permission (guest role)', async () => {
|
||||
const graphqlOperation = updateOneOperationFactory({
|
||||
objectMetadataSingularName: 'view',
|
||||
gqlFields: `
|
||||
id
|
||||
icon
|
||||
`,
|
||||
recordId: allPetsViewId,
|
||||
data: {
|
||||
icon: 'IconDog',
|
||||
},
|
||||
});
|
||||
|
||||
const response =
|
||||
await makeGraphqlAPIRequestWithGuestRole(graphqlOperation);
|
||||
|
||||
expect(response.body.data).toBeDefined();
|
||||
expect(response.body.data.updateView).toBeDefined();
|
||||
expect(response.body.data.updateView.id).toBe(allPetsViewId);
|
||||
expect(response.body.data.updateView.icon).toBe('IconDog');
|
||||
});
|
||||
|
||||
it('should update an object record when user has permission (admin role)', async () => {
|
||||
const graphqlOperation = updateOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
recordId: personId,
|
||||
data: {
|
||||
jobTitle: 'Senior Software Engineer',
|
||||
},
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||
|
||||
expect(response.body.data).toBeDefined();
|
||||
expect(response.body.data.updatePerson).toBeDefined();
|
||||
expect(response.body.data.updatePerson.id).toBe(personId);
|
||||
expect(response.body.data.updatePerson.jobTitle).toBe(
|
||||
'Senior Software Engineer',
|
||||
);
|
||||
});
|
||||
|
||||
it('should update an object record when executed by api key', async () => {
|
||||
const graphqlOperation = updateOneOperationFactory({
|
||||
objectMetadataSingularName: 'person',
|
||||
gqlFields: PERSON_GQL_FIELDS,
|
||||
recordId: personId,
|
||||
data: {
|
||||
jobTitle: 'Senior Software Engineer',
|
||||
},
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequestWithApiKey(graphqlOperation);
|
||||
|
||||
expect(response.body.data).toBeDefined();
|
||||
expect(response.body.data.updatePerson).toBeDefined();
|
||||
expect(response.body.data.updatePerson.id).toBe(personId);
|
||||
expect(response.body.data.updatePerson.jobTitle).toBe(
|
||||
'Senior Software Engineer',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
import { ASTNode, print } from 'graphql';
|
||||
import request from 'supertest';
|
||||
|
||||
type GraphqlOperation = {
|
||||
query: ASTNode;
|
||||
variables?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export const makeGraphqlAPIRequestWithApiKey = (
|
||||
graphqlOperation: GraphqlOperation,
|
||||
) => {
|
||||
const client = request(`http://localhost:${APP_PORT}`);
|
||||
|
||||
return client
|
||||
.post('/graphql')
|
||||
.set('Authorization', `Bearer ${API_KEY_ACCESS_TOKEN}`)
|
||||
.send({
|
||||
query: print(graphqlOperation.query),
|
||||
variables: graphqlOperation.variables || {},
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user