feat: multi role permission intersection (#15150)
Implements permission intersection (AND logic) to prevent permission escalation when agents act on behalf of users. ### Changes: - **Permission Intersection**: Operations requiring both user AND agent permissions - **RoleContext Type**: Unified type supporting single `roleId` or multiple `roleIds` for intersection - **CRUD Services**: Updated to accept `roleContext` for granular permission control - **Agent Integration**: Chat agents now use user + agent role intersection for all operations - **ORM Layer**: Enhanced `getRepository` to support multi-role permission checks ### Related: - Part 2 of ["Acting on behalf of user" concept PR](https://github.com/twentyhq/twenty/pull/15103) [Closes #1661](https://github.com/twentyhq/core-team-issues/issues/1661) --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
+7
-3
@@ -187,7 +187,7 @@ export class DeduplicateUniqueFieldsCommand extends ActiveOrSuspendedWorkspacesM
|
||||
}) {
|
||||
const workspaceMemberRepository = dataSource.getRepository(
|
||||
'workspaceMember',
|
||||
true,
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const duplicates = await workspaceMemberRepository
|
||||
@@ -245,7 +245,9 @@ export class DeduplicateUniqueFieldsCommand extends ActiveOrSuspendedWorkspacesM
|
||||
dataSource: WorkspaceDataSource;
|
||||
dryRun: boolean;
|
||||
}) {
|
||||
const companyRepository = dataSource.getRepository('company', true);
|
||||
const companyRepository = dataSource.getRepository('company', {
|
||||
shouldBypassPermissionChecks: true,
|
||||
});
|
||||
|
||||
const duplicates = await companyRepository
|
||||
.createQueryBuilder('company')
|
||||
@@ -303,7 +305,9 @@ export class DeduplicateUniqueFieldsCommand extends ActiveOrSuspendedWorkspacesM
|
||||
dataSource: WorkspaceDataSource;
|
||||
dryRun: boolean;
|
||||
}) {
|
||||
const personRepository = dataSource.getRepository('person', true);
|
||||
const personRepository = dataSource.getRepository('person', {
|
||||
shouldBypassPermissionChecks: true,
|
||||
});
|
||||
|
||||
const duplicates = await personRepository
|
||||
.createQueryBuilder('person')
|
||||
|
||||
+4
-5
@@ -79,19 +79,18 @@ export abstract class CommonBaseQueryRunnerService {
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
const rolePermissionConfig = { unionOf: [roleId] };
|
||||
|
||||
const repository = workspaceDataSource.getRepository(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
false,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
authContext,
|
||||
);
|
||||
|
||||
return {
|
||||
workspaceDataSource,
|
||||
repository,
|
||||
isExecutedByApiKey: isDefined(authContext.apiKey),
|
||||
roleId,
|
||||
shouldBypassPermissionChecks: false,
|
||||
rolePermissionConfig,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+10
-17
@@ -31,6 +31,7 @@ import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/typ
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
|
||||
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
|
||||
@Injectable()
|
||||
export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerService {
|
||||
@@ -62,15 +63,11 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
workspaceDataSource,
|
||||
repository,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks,
|
||||
} = await this.prepareQueryRunnerContext({
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
});
|
||||
const { workspaceDataSource, repository, rolePermissionConfig } =
|
||||
await this.prepareQueryRunnerContext({
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
});
|
||||
|
||||
const processedArgs = await this.processQueryArgs({
|
||||
authContext,
|
||||
@@ -98,10 +95,9 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
||||
records: upsertedRecords,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
authContext,
|
||||
workspaceDataSource,
|
||||
shouldBypassPermissionChecks,
|
||||
});
|
||||
|
||||
return upsertedRecords;
|
||||
@@ -371,19 +367,17 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
||||
records,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
authContext,
|
||||
workspaceDataSource,
|
||||
shouldBypassPermissionChecks,
|
||||
}: {
|
||||
args: CreateManyQueryArgs;
|
||||
records: ObjectRecord[];
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
roleId?: string;
|
||||
rolePermissionConfig?: RolePermissionConfig;
|
||||
authContext: AuthContext;
|
||||
workspaceDataSource: WorkspaceDataSource;
|
||||
shouldBypassPermissionChecks: boolean;
|
||||
}): Promise<void> {
|
||||
if (!args.selectedFieldsResult.relations) {
|
||||
return;
|
||||
@@ -401,8 +395,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks,
|
||||
rolePermissionConfig,
|
||||
selectedFields: args.selectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
|
||||
+6
-11
@@ -60,15 +60,11 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
workspaceDataSource,
|
||||
repository,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks,
|
||||
} = await this.prepareQueryRunnerContext({
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
});
|
||||
const { workspaceDataSource, repository, rolePermissionConfig } =
|
||||
await this.prepareQueryRunnerContext({
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
});
|
||||
|
||||
const processedArgs = await this.processQueryArgs({
|
||||
authContext,
|
||||
@@ -195,8 +191,7 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks,
|
||||
rolePermissionConfig,
|
||||
selectedFields: processedArgs.selectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
|
||||
+6
-11
@@ -46,15 +46,11 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
workspaceDataSource,
|
||||
repository,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks,
|
||||
} = await this.prepareQueryRunnerContext({
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
});
|
||||
const { workspaceDataSource, repository, rolePermissionConfig } =
|
||||
await this.prepareQueryRunnerContext({
|
||||
authContext,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
});
|
||||
|
||||
const processedArgs = await this.processQueryArgs({
|
||||
authContext,
|
||||
@@ -128,8 +124,7 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks,
|
||||
rolePermissionConfig,
|
||||
selectedFields: args.selectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
|
||||
+8
-14
@@ -20,6 +20,7 @@ import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/objec
|
||||
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
|
||||
import { type WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
|
||||
import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||
|
||||
@Injectable()
|
||||
@@ -36,8 +37,7 @@ export class ProcessNestedRelationsV2Helper {
|
||||
limit,
|
||||
authContext,
|
||||
workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks,
|
||||
rolePermissionConfig,
|
||||
selectedFields,
|
||||
}: {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
@@ -50,10 +50,9 @@ export class ProcessNestedRelationsV2Helper {
|
||||
limit: number;
|
||||
authContext: AuthContext;
|
||||
workspaceDataSource: WorkspaceDataSource;
|
||||
shouldBypassPermissionChecks: boolean;
|
||||
rolePermissionConfig?: RolePermissionConfig;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
selectedFields: Record<string, any>;
|
||||
roleId?: string;
|
||||
}): Promise<void> {
|
||||
const processRelationTasks = Object.entries(relations).map(
|
||||
([sourceFieldName, nestedRelations]) =>
|
||||
@@ -68,8 +67,7 @@ export class ProcessNestedRelationsV2Helper {
|
||||
limit,
|
||||
authContext,
|
||||
workspaceDataSource,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
selectedFields:
|
||||
selectedFields[sourceFieldName] instanceof Object
|
||||
? selectedFields[sourceFieldName]
|
||||
@@ -91,8 +89,7 @@ export class ProcessNestedRelationsV2Helper {
|
||||
limit,
|
||||
authContext,
|
||||
workspaceDataSource,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
selectedFields,
|
||||
}: {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
@@ -106,8 +103,7 @@ export class ProcessNestedRelationsV2Helper {
|
||||
limit: number;
|
||||
authContext: AuthContext;
|
||||
workspaceDataSource: WorkspaceDataSource;
|
||||
shouldBypassPermissionChecks: boolean;
|
||||
roleId?: string;
|
||||
rolePermissionConfig?: RolePermissionConfig;
|
||||
selectedFields: Record<string, unknown>;
|
||||
}): Promise<void> {
|
||||
const sourceFieldMetadata = getFieldMetadataFromGraphQLField({
|
||||
@@ -147,8 +143,7 @@ export class ProcessNestedRelationsV2Helper {
|
||||
|
||||
const targetObjectRepository = workspaceDataSource.getRepository(
|
||||
targetObjectMetadata.nameSingular,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
);
|
||||
|
||||
const targetObjectNameSingular = targetObjectMetadata.nameSingular;
|
||||
@@ -240,8 +235,7 @@ export class ProcessNestedRelationsV2Helper {
|
||||
limit,
|
||||
authContext,
|
||||
workspaceDataSource,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
selectedFields,
|
||||
});
|
||||
}
|
||||
|
||||
+4
-6
@@ -9,6 +9,7 @@ import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-contex
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { type WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
|
||||
@Injectable()
|
||||
export class ProcessNestedRelationsHelper {
|
||||
@@ -26,8 +27,7 @@ export class ProcessNestedRelationsHelper {
|
||||
limit,
|
||||
authContext,
|
||||
workspaceDataSource,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
selectedFields,
|
||||
}: {
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
@@ -40,10 +40,9 @@ export class ProcessNestedRelationsHelper {
|
||||
limit: number;
|
||||
authContext: AuthContext;
|
||||
workspaceDataSource: WorkspaceDataSource;
|
||||
shouldBypassPermissionChecks: boolean;
|
||||
rolePermissionConfig?: RolePermissionConfig;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
selectedFields: Record<string, any>;
|
||||
roleId?: string;
|
||||
}): Promise<void> {
|
||||
return this.processNestedRelationsV2Helper.processNestedRelations({
|
||||
objectMetadataMaps,
|
||||
@@ -55,8 +54,7 @@ export class ProcessNestedRelationsHelper {
|
||||
limit,
|
||||
authContext,
|
||||
workspaceDataSource,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
selectedFields,
|
||||
});
|
||||
}
|
||||
|
||||
+7
-8
@@ -40,6 +40,7 @@ import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role
|
||||
import { type WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
|
||||
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
|
||||
export type GraphqlQueryResolverExecutionArgs<Input extends ResolverArgs> = {
|
||||
args: Input;
|
||||
@@ -49,8 +50,7 @@ export type GraphqlQueryResolverExecutionArgs<Input extends ResolverArgs> = {
|
||||
graphqlQueryParser: GraphqlQueryParser;
|
||||
graphqlQuerySelectedFieldsResult: GraphqlQuerySelectedFieldsResult;
|
||||
isExecutedByApiKey: boolean;
|
||||
roleId?: string;
|
||||
shouldBypassPermissionChecks: boolean;
|
||||
rolePermissionConfig?: RolePermissionConfig;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -120,7 +120,6 @@ export abstract class GraphqlQueryBaseResolverService<
|
||||
)) as Input;
|
||||
|
||||
let roleId: string | undefined;
|
||||
let shouldBypassPermissionChecks = false;
|
||||
|
||||
if (isDefined(authContext.apiKey)) {
|
||||
roleId = await this.apiKeyRoleService.getRoleIdForApiKey(
|
||||
@@ -152,11 +151,12 @@ export abstract class GraphqlQueryBaseResolverService<
|
||||
PermissionsExceptionCode.NO_AUTHENTICATION_CONTEXT,
|
||||
);
|
||||
}
|
||||
|
||||
const rolePermissionConfig: RolePermissionConfig | undefined = roleId
|
||||
? { unionOf: [roleId] }
|
||||
: undefined;
|
||||
const repository = workspaceDataSource.getRepository(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
authContext,
|
||||
);
|
||||
|
||||
@@ -182,8 +182,7 @@ export abstract class GraphqlQueryBaseResolverService<
|
||||
graphqlQueryParser,
|
||||
graphqlQuerySelectedFieldsResult,
|
||||
isExecutedByApiKey: isDefined(authContext.apiKey),
|
||||
roleId,
|
||||
shouldBypassPermissionChecks,
|
||||
rolePermissionConfig,
|
||||
};
|
||||
const results = await this.resolve(
|
||||
graphqlQueryResolverExecutionArgs,
|
||||
|
||||
+1
-7
@@ -55,8 +55,6 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
||||
const { objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
const { roleId } = executionArgs;
|
||||
|
||||
const objectRecords = await this.insertOrUpsertRecords(executionArgs);
|
||||
|
||||
const upsertedRecords = await this.fetchUpsertedRecords(
|
||||
@@ -71,7 +69,6 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
||||
records: upsertedRecords,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
roleId,
|
||||
});
|
||||
|
||||
return this.formatRecordsForResponse(
|
||||
@@ -462,13 +459,11 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
||||
records,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps,
|
||||
roleId,
|
||||
}: {
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<CreateManyResolverArgs>;
|
||||
records: ObjectRecord[];
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
roleId?: string;
|
||||
}): Promise<void> {
|
||||
if (!executionArgs.graphqlQuerySelectedFieldsResult.relations) {
|
||||
return;
|
||||
@@ -482,8 +477,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext: executionArgs.options.authContext,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks: executionArgs.shouldBypassPermissionChecks,
|
||||
rolePermissionConfig: executionArgs.rolePermissionConfig,
|
||||
selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
|
||||
+1
-5
@@ -27,8 +27,6 @@ export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResol
|
||||
const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
const { roleId } = executionArgs;
|
||||
|
||||
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
@@ -66,9 +64,7 @@ export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResol
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks:
|
||||
executionArgs.shouldBypassPermissionChecks,
|
||||
rolePermissionConfig: executionArgs.rolePermissionConfig,
|
||||
selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
|
||||
+1
-5
@@ -30,8 +30,6 @@ export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolv
|
||||
const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
const { roleId } = executionArgs;
|
||||
|
||||
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
@@ -67,9 +65,7 @@ export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolv
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks:
|
||||
executionArgs.shouldBypassPermissionChecks,
|
||||
rolePermissionConfig: executionArgs.rolePermissionConfig,
|
||||
selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
|
||||
+1
-5
@@ -24,8 +24,6 @@ export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseReso
|
||||
const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
const { roleId } = executionArgs;
|
||||
|
||||
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
@@ -58,9 +56,7 @@ export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseReso
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks:
|
||||
executionArgs.shouldBypassPermissionChecks,
|
||||
rolePermissionConfig: executionArgs.rolePermissionConfig,
|
||||
selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
|
||||
+1
-5
@@ -28,8 +28,6 @@ export class GraphqlQueryDestroyOneResolverService extends GraphqlQueryBaseResol
|
||||
const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
const { roleId } = executionArgs;
|
||||
|
||||
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
@@ -64,9 +62,7 @@ export class GraphqlQueryDestroyOneResolverService extends GraphqlQueryBaseResol
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks:
|
||||
executionArgs.shouldBypassPermissionChecks,
|
||||
rolePermissionConfig: executionArgs.rolePermissionConfig,
|
||||
selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
|
||||
+1
-5
@@ -47,8 +47,6 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve
|
||||
const objectMetadataNameSingular =
|
||||
objectMetadataItemWithFieldMaps.nameSingular;
|
||||
|
||||
const { roleId } = executionArgs;
|
||||
|
||||
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
||||
objectMetadataNameSingular,
|
||||
);
|
||||
@@ -163,9 +161,7 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks:
|
||||
executionArgs.shouldBypassPermissionChecks,
|
||||
rolePermissionConfig: executionArgs.rolePermissionConfig,
|
||||
selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
|
||||
+1
-5
@@ -34,8 +34,6 @@ export class GraphqlQueryFindOneResolverService extends GraphqlQueryBaseResolver
|
||||
const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
const { roleId } = executionArgs;
|
||||
|
||||
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
@@ -82,9 +80,7 @@ export class GraphqlQueryFindOneResolverService extends GraphqlQueryBaseResolver
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks:
|
||||
executionArgs.shouldBypassPermissionChecks,
|
||||
rolePermissionConfig: executionArgs.rolePermissionConfig,
|
||||
selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
|
||||
+3
-10
@@ -47,7 +47,6 @@ export class GraphqlQueryMergeManyResolverService extends GraphqlQueryBaseResolv
|
||||
const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
const { ids, conflictPriorityIndex, dryRun } = executionArgs.args;
|
||||
const { roleId } = executionArgs;
|
||||
|
||||
const recordsToMerge = await this.fetchRecordsToMerge(executionArgs, ids);
|
||||
|
||||
@@ -96,12 +95,11 @@ export class GraphqlQueryMergeManyResolverService extends GraphqlQueryBaseResolv
|
||||
mergedData,
|
||||
);
|
||||
|
||||
if (roleId) {
|
||||
if (executionArgs.rolePermissionConfig) {
|
||||
await this.processNestedRelations({
|
||||
executionArgs,
|
||||
updatedRecords: [updatedRecord],
|
||||
authContext,
|
||||
roleId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -335,8 +333,7 @@ export class GraphqlQueryMergeManyResolverService extends GraphqlQueryBaseResolv
|
||||
try {
|
||||
const repository = executionArgs.workspaceDataSource.getRepository(
|
||||
relationField.objectMetadata.nameSingular,
|
||||
executionArgs.isExecutedByApiKey,
|
||||
executionArgs.roleId,
|
||||
executionArgs.rolePermissionConfig,
|
||||
);
|
||||
|
||||
const whereCondition = { [relationField.joinColumnName]: In(fromIds) };
|
||||
@@ -363,12 +360,10 @@ export class GraphqlQueryMergeManyResolverService extends GraphqlQueryBaseResolv
|
||||
executionArgs,
|
||||
updatedRecords,
|
||||
authContext,
|
||||
roleId,
|
||||
}: {
|
||||
executionArgs: GraphqlQueryResolverExecutionArgs<MergeManyResolverArgs>;
|
||||
updatedRecords: ObjectRecord[];
|
||||
authContext: AuthContext;
|
||||
roleId: string;
|
||||
}): Promise<void> {
|
||||
const { objectMetadataMaps, objectMetadataItemWithFieldMaps } =
|
||||
executionArgs.options;
|
||||
@@ -382,9 +377,7 @@ export class GraphqlQueryMergeManyResolverService extends GraphqlQueryBaseResolv
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks:
|
||||
executionArgs.shouldBypassPermissionChecks,
|
||||
rolePermissionConfig: executionArgs.rolePermissionConfig,
|
||||
selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
|
||||
+1
-5
@@ -27,8 +27,6 @@ export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseReso
|
||||
const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
const { roleId } = executionArgs;
|
||||
|
||||
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
@@ -66,9 +64,7 @@ export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseReso
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks:
|
||||
executionArgs.shouldBypassPermissionChecks,
|
||||
rolePermissionConfig: executionArgs.rolePermissionConfig,
|
||||
selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
|
||||
+1
-5
@@ -30,8 +30,6 @@ export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResol
|
||||
const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
const { roleId } = executionArgs;
|
||||
|
||||
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
@@ -68,9 +66,7 @@ export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResol
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks:
|
||||
executionArgs.shouldBypassPermissionChecks,
|
||||
rolePermissionConfig: executionArgs.rolePermissionConfig,
|
||||
selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
|
||||
+1
-5
@@ -27,8 +27,6 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol
|
||||
const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
const { roleId } = executionArgs;
|
||||
|
||||
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
@@ -67,9 +65,7 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks:
|
||||
executionArgs.shouldBypassPermissionChecks,
|
||||
rolePermissionConfig: executionArgs.rolePermissionConfig,
|
||||
selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
|
||||
+1
-5
@@ -30,8 +30,6 @@ export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolv
|
||||
const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } =
|
||||
executionArgs.options;
|
||||
|
||||
const { roleId } = executionArgs;
|
||||
|
||||
const queryBuilder = executionArgs.repository.createQueryBuilder(
|
||||
objectMetadataItemWithFieldMaps.nameSingular,
|
||||
);
|
||||
@@ -68,9 +66,7 @@ export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolv
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks:
|
||||
executionArgs.shouldBypassPermissionChecks,
|
||||
rolePermissionConfig: executionArgs.rolePermissionConfig,
|
||||
selectedFields: executionArgs.graphqlQuerySelectedFieldsResult.select,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -175,8 +175,9 @@ export abstract class RestApiBaseHandler {
|
||||
|
||||
const repository = workspaceDataSource.getRepository<ObjectRecord>(
|
||||
objectMetadataNameSingular,
|
||||
false,
|
||||
roleId,
|
||||
{
|
||||
unionOf: [roleId],
|
||||
},
|
||||
);
|
||||
|
||||
const objectMetadataPermissions =
|
||||
|
||||
+1
-1
@@ -285,7 +285,7 @@ describe('McpService', () => {
|
||||
});
|
||||
|
||||
expect(toolService.listTools).toHaveBeenCalledWith(
|
||||
mockAdminRoleId,
|
||||
{ unionOf: [mockAdminRoleId] },
|
||||
mockWorkspace.id,
|
||||
);
|
||||
expect(mockTool.execute).toHaveBeenCalledWith(
|
||||
|
||||
+11
-6
@@ -9,6 +9,7 @@ import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.ty
|
||||
import { type Tool } from 'src/engine/core-modules/tool/types/tool.type';
|
||||
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
|
||||
const createMockToolRegistry = () => ({
|
||||
getAllToolTypes: jest.fn(),
|
||||
@@ -18,7 +19,7 @@ const createMockToolRegistry = () => ({
|
||||
const createMockPermissions = () => ({
|
||||
hasToolPermission: jest.fn<
|
||||
Promise<boolean>,
|
||||
[string, string, PermissionFlagType]
|
||||
[RolePermissionConfig, string, PermissionFlagType]
|
||||
>(),
|
||||
});
|
||||
|
||||
@@ -89,7 +90,9 @@ describe('ToolAdapterService', () => {
|
||||
|
||||
expect(Object.keys(toolsNoContext)).toContain('http_request');
|
||||
|
||||
const toolsWithPartialContext = await service.getTools('role-1');
|
||||
const toolsWithPartialContext = await service.getTools({
|
||||
unionOf: ['role-1'],
|
||||
});
|
||||
|
||||
expect(Object.keys(toolsWithPartialContext)).toContain('http_request');
|
||||
});
|
||||
@@ -99,7 +102,9 @@ describe('ToolAdapterService', () => {
|
||||
|
||||
expect(Object.keys(toolsNoContext)).not.toContain('send_email');
|
||||
|
||||
const toolsRoleOnly = await service.getTools('role-1');
|
||||
const toolsRoleOnly = await service.getTools({
|
||||
unionOf: ['role-1'],
|
||||
});
|
||||
|
||||
expect(Object.keys(toolsRoleOnly)).not.toContain('send_email');
|
||||
|
||||
@@ -111,10 +116,10 @@ describe('ToolAdapterService', () => {
|
||||
it('should include flagged tools when permission is granted', async () => {
|
||||
mockPermissions.hasToolPermission.mockResolvedValueOnce(true);
|
||||
|
||||
const tools = await service.getTools('role-1', 'ws-1');
|
||||
const tools = await service.getTools({ unionOf: ['role-1'] }, 'ws-1');
|
||||
|
||||
expect(mockPermissions.hasToolPermission).toHaveBeenCalledWith(
|
||||
'role-1',
|
||||
{ unionOf: ['role-1'] },
|
||||
'ws-1',
|
||||
PermissionFlagType.SEND_EMAIL_TOOL,
|
||||
);
|
||||
@@ -125,7 +130,7 @@ describe('ToolAdapterService', () => {
|
||||
it('should exclude flagged tools when permission is denied', async () => {
|
||||
mockPermissions.hasToolPermission.mockResolvedValueOnce(false);
|
||||
|
||||
const tools = await service.getTools('role-1', 'ws-1');
|
||||
const tools = await service.getTools({ unionOf: ['role-1'] }, 'ws-1');
|
||||
|
||||
expect(Object.keys(tools)).not.toContain('send_email');
|
||||
});
|
||||
|
||||
+5
-5
@@ -69,10 +69,10 @@ describe('ToolService', () => {
|
||||
data: {
|
||||
[roleId]: {
|
||||
[testObject.id]: {
|
||||
canRead: true,
|
||||
canUpdate: true,
|
||||
canSoftDelete: true,
|
||||
canDestroy: false,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: false,
|
||||
restrictedFields: {},
|
||||
},
|
||||
},
|
||||
@@ -128,7 +128,7 @@ describe('ToolService', () => {
|
||||
|
||||
describe('listTools', () => {
|
||||
it('should return tools based on role permissions', async () => {
|
||||
const tools = await service.listTools(roleId, workspaceId);
|
||||
const tools = await service.listTools({ unionOf: [roleId] }, workspaceId);
|
||||
|
||||
expect(
|
||||
permissionsCacheService.getRolesPermissionsFromCache,
|
||||
|
||||
@@ -124,7 +124,10 @@ export class McpService {
|
||||
apiKey,
|
||||
);
|
||||
|
||||
const toolSet = await this.toolService.listTools(roleId, workspace.id);
|
||||
const toolSet = await this.toolService.listTools(
|
||||
{ unionOf: [roleId] },
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
if (method === 'tools/call' && params) {
|
||||
return await this.handleToolCall(id, toolSet, params);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.ty
|
||||
import { type Tool } from 'src/engine/core-modules/tool/types/tool.type';
|
||||
import { type PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
|
||||
@Injectable()
|
||||
export class ToolAdapterService {
|
||||
@@ -15,7 +16,10 @@ export class ToolAdapterService {
|
||||
private readonly permissionsService: PermissionsService,
|
||||
) {}
|
||||
|
||||
async getTools(roleId?: string, workspaceId?: string): Promise<ToolSet> {
|
||||
async getTools(
|
||||
rolePermissionConfig?: RolePermissionConfig,
|
||||
workspaceId?: string,
|
||||
): Promise<ToolSet> {
|
||||
const tools: ToolSet = {};
|
||||
|
||||
for (const toolType of this.toolRegistry.getAllToolTypes()) {
|
||||
@@ -23,9 +27,9 @@ export class ToolAdapterService {
|
||||
|
||||
if (!tool.flag) {
|
||||
tools[toolType.toLowerCase()] = this.createToolSet(tool);
|
||||
} else if (roleId && workspaceId) {
|
||||
} else if (rolePermissionConfig && workspaceId) {
|
||||
const hasPermission = await this.permissionsService.hasToolPermission(
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
workspaceId,
|
||||
tool.flag as PermissionFlagType,
|
||||
);
|
||||
|
||||
@@ -18,6 +18,8 @@ import { type ActorMetadata } from 'src/engine/metadata-modules/field-metadata/c
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.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';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
import { computePermissionIntersection } from 'src/engine/twenty-orm/utils/compute-permission-intersection.util';
|
||||
|
||||
@Injectable()
|
||||
export class ToolService {
|
||||
@@ -32,7 +34,7 @@ export class ToolService {
|
||||
) {}
|
||||
|
||||
async listTools(
|
||||
roleId: string,
|
||||
rolePermissionConfig: RolePermissionConfig,
|
||||
workspaceId: string,
|
||||
actorContext?: ActorMetadata,
|
||||
): Promise<ToolSet> {
|
||||
@@ -43,7 +45,29 @@ export class ToolService {
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const objectPermissions = rolesPermissions[roleId];
|
||||
let objectPermissions;
|
||||
|
||||
if ('unionOf' in rolePermissionConfig) {
|
||||
if (rolePermissionConfig.unionOf.length === 1) {
|
||||
objectPermissions = rolesPermissions[rolePermissionConfig.unionOf[0]];
|
||||
} else {
|
||||
// TODO: Implement union logic for multiple roles
|
||||
throw new Error(
|
||||
'Union permission logic for multiple roles not yet implemented',
|
||||
);
|
||||
}
|
||||
} else if ('intersectionOf' in rolePermissionConfig) {
|
||||
const allRolePermissions = rolePermissionConfig.intersectionOf.map(
|
||||
(roleId: string) => rolesPermissions[roleId],
|
||||
);
|
||||
|
||||
objectPermissions =
|
||||
allRolePermissions.length === 1
|
||||
? allRolePermissions[0]
|
||||
: computePermissionIntersection(allRolePermissions);
|
||||
} else {
|
||||
return tools;
|
||||
}
|
||||
|
||||
const allObjectMetadata =
|
||||
await this.objectMetadataService.findManyWithinWorkspace(workspaceId, {
|
||||
@@ -65,7 +89,7 @@ export class ToolService {
|
||||
return;
|
||||
}
|
||||
|
||||
if (objectPermission.canUpdate) {
|
||||
if (objectPermission.canUpdateObjectRecords) {
|
||||
tools[`create_${objectMetadata.nameSingular}`] = {
|
||||
description: `Create a new ${objectMetadata.labelSingular} record. Provide all required fields and any optional fields you want to set. The system will automatically handle timestamps and IDs. Returns the created record with all its data.`,
|
||||
inputSchema: getRecordInputSchema(objectMetadata),
|
||||
@@ -74,7 +98,7 @@ export class ToolService {
|
||||
objectName: objectMetadata.nameSingular,
|
||||
objectRecord: parameters.input,
|
||||
workspaceId,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
createdBy: actorContext,
|
||||
});
|
||||
},
|
||||
@@ -91,13 +115,13 @@ export class ToolService {
|
||||
objectRecordId: id,
|
||||
objectRecord,
|
||||
workspaceId,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (objectPermission.canRead) {
|
||||
if (objectPermission.canReadObjectRecords) {
|
||||
tools[`find_${objectMetadata.nameSingular}`] = {
|
||||
description: `Search for ${objectMetadata.labelSingular} records using flexible filtering criteria. Supports exact matches, pattern matching, ranges, and null checks. Use limit/offset for pagination. Returns an array of matching records with their full data.`,
|
||||
inputSchema: generateFindToolSchema(objectMetadata),
|
||||
@@ -110,7 +134,7 @@ export class ToolService {
|
||||
limit,
|
||||
offset,
|
||||
workspaceId,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -124,13 +148,13 @@ export class ToolService {
|
||||
filter: { id: { eq: parameters.input.id } },
|
||||
limit: 1,
|
||||
workspaceId,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (objectPermission.canSoftDelete) {
|
||||
if (objectPermission.canSoftDeleteObjectRecords) {
|
||||
tools[`soft_delete_${objectMetadata.nameSingular}`] = {
|
||||
description: `Soft delete a ${objectMetadata.labelSingular} record by marking it as deleted. The record remains in the database but is hidden from normal queries. This is reversible and preserves all data. Use this for temporary removal.`,
|
||||
inputSchema: generateSoftDeleteToolSchema(),
|
||||
@@ -139,7 +163,7 @@ export class ToolService {
|
||||
objectName: objectMetadata.nameSingular,
|
||||
objectRecordId: parameters.input.id,
|
||||
workspaceId,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
soft: true,
|
||||
});
|
||||
},
|
||||
@@ -153,7 +177,7 @@ export class ToolService {
|
||||
objectMetadata.nameSingular,
|
||||
parameters.input,
|
||||
workspaceId,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -167,14 +191,14 @@ export class ToolService {
|
||||
objectName: string,
|
||||
parameters: Record<string, unknown>,
|
||||
workspaceId: string,
|
||||
roleId: string,
|
||||
rolePermissionConfig: RolePermissionConfig,
|
||||
) {
|
||||
try {
|
||||
const repository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
objectName,
|
||||
{ roleId },
|
||||
rolePermissionConfig,
|
||||
);
|
||||
|
||||
const { filter } = parameters;
|
||||
|
||||
+3
-2
@@ -28,7 +28,8 @@ export class CreateRecordService {
|
||||
) {}
|
||||
|
||||
async execute(params: CreateRecordParams): Promise<ToolOutput> {
|
||||
const { objectName, objectRecord, workspaceId, roleId } = params;
|
||||
const { objectName, objectRecord, workspaceId, rolePermissionConfig } =
|
||||
params;
|
||||
|
||||
if (!workspaceId) {
|
||||
return {
|
||||
@@ -43,7 +44,7 @@ export class CreateRecordService {
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
objectName,
|
||||
roleId ? { roleId } : { shouldBypassPermissionChecks: true },
|
||||
rolePermissionConfig,
|
||||
);
|
||||
|
||||
const { objectMetadataItemWithFieldsMaps } =
|
||||
|
||||
+2
-2
@@ -27,7 +27,7 @@ export class DeleteRecordService {
|
||||
objectName,
|
||||
objectRecordId,
|
||||
workspaceId,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
soft = true,
|
||||
} = params;
|
||||
|
||||
@@ -52,7 +52,7 @@ export class DeleteRecordService {
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
objectName,
|
||||
roleId ? { roleId } : { shouldBypassPermissionChecks: true },
|
||||
rolePermissionConfig,
|
||||
);
|
||||
|
||||
const { objectMetadataItemWithFieldsMaps } =
|
||||
|
||||
+2
-2
@@ -37,7 +37,7 @@ export class FindRecordsService {
|
||||
limit,
|
||||
offset = 0,
|
||||
workspaceId,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
} = params;
|
||||
|
||||
if (!workspaceId) {
|
||||
@@ -53,7 +53,7 @@ export class FindRecordsService {
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
objectName,
|
||||
roleId ? { roleId } : { shouldBypassPermissionChecks: true },
|
||||
rolePermissionConfig,
|
||||
);
|
||||
|
||||
const { objectMetadataItemWithFieldsMaps, objectMetadataMaps } =
|
||||
|
||||
+2
-2
@@ -32,7 +32,7 @@ export class UpdateRecordService {
|
||||
objectRecord,
|
||||
fieldsToUpdate,
|
||||
workspaceId,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
} = params;
|
||||
|
||||
if (!workspaceId) {
|
||||
@@ -56,7 +56,7 @@ export class UpdateRecordService {
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
objectName,
|
||||
roleId ? { roleId } : { shouldBypassPermissionChecks: true },
|
||||
rolePermissionConfig,
|
||||
);
|
||||
|
||||
const previousObjectRecord = await repository.findOne({
|
||||
|
||||
+2
-1
@@ -1,10 +1,11 @@
|
||||
import { type ObjectRecordProperties } from 'src/engine/core-modules/record-crud/types/object-record-properties.type';
|
||||
import { type ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
|
||||
export type CreateRecordParams = {
|
||||
objectName: string;
|
||||
objectRecord: ObjectRecordProperties;
|
||||
workspaceId: string;
|
||||
roleId?: string;
|
||||
rolePermissionConfig?: RolePermissionConfig;
|
||||
createdBy?: ActorMetadata;
|
||||
};
|
||||
|
||||
+3
-1
@@ -1,7 +1,9 @@
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
|
||||
export type DeleteRecordParams = {
|
||||
objectName: string;
|
||||
objectRecordId: string;
|
||||
workspaceId: string;
|
||||
roleId?: string;
|
||||
rolePermissionConfig?: RolePermissionConfig;
|
||||
soft?: boolean;
|
||||
};
|
||||
|
||||
+3
-1
@@ -3,6 +3,8 @@ import {
|
||||
type ObjectRecordOrderBy,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
|
||||
export type FindRecordsParams = {
|
||||
objectName: string;
|
||||
filter?:
|
||||
@@ -14,5 +16,5 @@ export type FindRecordsParams = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
workspaceId: string;
|
||||
roleId?: string;
|
||||
rolePermissionConfig?: RolePermissionConfig;
|
||||
};
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
import { type ObjectRecordProperties } from 'src/engine/core-modules/record-crud/types/object-record-properties.type';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
|
||||
export type UpdateRecordParams = {
|
||||
objectName: string;
|
||||
@@ -6,5 +7,5 @@ export type UpdateRecordParams = {
|
||||
objectRecord: ObjectRecordProperties;
|
||||
fieldsToUpdate?: string[];
|
||||
workspaceId: string;
|
||||
roleId?: string;
|
||||
rolePermissionConfig?: RolePermissionConfig;
|
||||
};
|
||||
|
||||
+10
-16
@@ -1,5 +1,4 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import {
|
||||
convertToModelMessages,
|
||||
@@ -13,15 +12,15 @@ import {
|
||||
} from 'ai';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { getAppPath } from 'twenty-shared/utils';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { In } from 'typeorm';
|
||||
|
||||
import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils';
|
||||
import { AIBillingService } from 'src/engine/core-modules/ai/services/ai-billing.service';
|
||||
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||
import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AgentHandoffToolService } from 'src/engine/metadata-modules/agent/agent-handoff-tool.service';
|
||||
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||
import { AGENT_CONFIG } from 'src/engine/metadata-modules/agent/constants/agent-config.const';
|
||||
import { AGENT_SYSTEM_PROMPTS } from 'src/engine/metadata-modules/agent/constants/agent-system-prompts.const';
|
||||
import { AgentActorContextService } from 'src/engine/metadata-modules/agent/services/agent-actor-context.service';
|
||||
@@ -48,7 +47,6 @@ export class AgentExecutionService implements AgentExecutionContext {
|
||||
|
||||
constructor(
|
||||
private readonly agentHandoffToolService: AgentHandoffToolService,
|
||||
private readonly fileService: FileService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
|
||||
@@ -57,8 +55,7 @@ export class AgentExecutionService implements AgentExecutionContext {
|
||||
private readonly agentModelConfigService: AgentModelConfigService,
|
||||
private readonly aiBillingService: AIBillingService,
|
||||
private readonly agentActorContextService: AgentActorContextService,
|
||||
@InjectRepository(AgentEntity)
|
||||
private readonly agentRepository: Repository<AgentEntity>,
|
||||
private readonly agentService: AgentService,
|
||||
) {}
|
||||
|
||||
async prepareAIRequestConfig({
|
||||
@@ -66,14 +63,14 @@ export class AgentExecutionService implements AgentExecutionContext {
|
||||
system,
|
||||
agent,
|
||||
actorContext,
|
||||
roleIdOverride,
|
||||
roleIds,
|
||||
excludeHandoffTools = false,
|
||||
}: {
|
||||
system: string;
|
||||
agent: AgentEntity | null;
|
||||
messages: UIMessage<unknown, UIDataTypes, UITools>[];
|
||||
actorContext?: ActorMetadata;
|
||||
roleIdOverride?: string;
|
||||
roleIds?: string[];
|
||||
excludeHandoffTools?: boolean;
|
||||
}) {
|
||||
try {
|
||||
@@ -95,7 +92,7 @@ export class AgentExecutionService implements AgentExecutionContext {
|
||||
agent.id,
|
||||
agent.workspaceId,
|
||||
actorContext,
|
||||
roleIdOverride,
|
||||
roleIds,
|
||||
);
|
||||
|
||||
let handoffTools = {};
|
||||
@@ -193,8 +190,7 @@ export class AgentExecutionService implements AgentExecutionContext {
|
||||
|
||||
const repository = workspaceDataSource.getRepository(
|
||||
recordsWithObjectMetadataNameSingular.objectMetadataNameSingular,
|
||||
false,
|
||||
roleId,
|
||||
{ unionOf: [roleId] },
|
||||
);
|
||||
|
||||
const restrictedFields =
|
||||
@@ -254,9 +250,7 @@ export class AgentExecutionService implements AgentExecutionContext {
|
||||
recordIdsByObjectMetadataNameSingular: RecordIdsByObjectMetadataNameSingularType;
|
||||
}) {
|
||||
try {
|
||||
const agent = await this.agentRepository.findOneOrFail({
|
||||
where: { id: agentId },
|
||||
});
|
||||
const agent = await this.agentService.findOneAgent(agentId, workspace.id);
|
||||
|
||||
let contextString = '';
|
||||
|
||||
@@ -271,7 +265,7 @@ export class AgentExecutionService implements AgentExecutionContext {
|
||||
}
|
||||
|
||||
const { actorContext, roleId } =
|
||||
await this.agentActorContextService.buildUserActorContext(
|
||||
await this.agentActorContextService.buildUserAndAgentActorContext(
|
||||
userWorkspaceId,
|
||||
workspace.id,
|
||||
);
|
||||
@@ -281,7 +275,7 @@ export class AgentExecutionService implements AgentExecutionContext {
|
||||
agent,
|
||||
messages,
|
||||
actorContext,
|
||||
roleIdOverride: roleId,
|
||||
roleIds: [roleId, ...(agent?.roleId ? [agent?.roleId] : [])],
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
|
||||
+8
-24
@@ -6,7 +6,6 @@ import { Repository } from 'typeorm';
|
||||
|
||||
import { ToolAdapterService } from 'src/engine/core-modules/ai/services/tool-adapter.service';
|
||||
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
|
||||
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||
import { type ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
@@ -24,58 +23,43 @@ export class AgentToolGeneratorService {
|
||||
private readonly toolService: ToolService,
|
||||
private readonly workflowToolService: WorkflowToolService,
|
||||
private readonly permissionsService: PermissionsService,
|
||||
private readonly agentService: AgentService,
|
||||
) {}
|
||||
|
||||
async generateToolsForAgent(
|
||||
agentId: string,
|
||||
workspaceId: string,
|
||||
actorContext?: ActorMetadata,
|
||||
roleIdOverride?: string,
|
||||
roleIds?: string[],
|
||||
): Promise<ToolSet> {
|
||||
let tools: ToolSet = {};
|
||||
|
||||
try {
|
||||
const agent = await this.agentService.findOneAgent(agentId, workspaceId);
|
||||
const actionTools = await this.toolAdapterService.getTools();
|
||||
|
||||
tools = { ...actionTools };
|
||||
|
||||
const effectiveRoleId = roleIdOverride || agent.roleId;
|
||||
|
||||
if (!effectiveRoleId) {
|
||||
return tools;
|
||||
}
|
||||
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: {
|
||||
id: effectiveRoleId,
|
||||
workspaceId,
|
||||
},
|
||||
relations: ['permissionFlags'],
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
if (!roleIds) {
|
||||
return tools;
|
||||
}
|
||||
|
||||
const hasWorkflowPermission =
|
||||
this.permissionsService.checkRolePermissions(
|
||||
role,
|
||||
await this.permissionsService.checkRolesPermissions(
|
||||
{ intersectionOf: roleIds },
|
||||
workspaceId,
|
||||
PermissionFlagType.WORKFLOWS,
|
||||
);
|
||||
|
||||
if (hasWorkflowPermission) {
|
||||
const workflowTools = this.workflowToolService.generateWorkflowTools(
|
||||
workspaceId,
|
||||
effectiveRoleId,
|
||||
{ intersectionOf: roleIds },
|
||||
);
|
||||
|
||||
tools = { ...tools, ...workflowTools };
|
||||
}
|
||||
|
||||
const databaseTools = await this.toolService.listTools(
|
||||
effectiveRoleId,
|
||||
{ intersectionOf: roleIds },
|
||||
workspaceId,
|
||||
actorContext,
|
||||
);
|
||||
@@ -83,7 +67,7 @@ export class AgentToolGeneratorService {
|
||||
tools = { ...tools, ...databaseTools };
|
||||
|
||||
const roleActionTools = await this.toolAdapterService.getTools(
|
||||
effectiveRoleId,
|
||||
{ intersectionOf: roleIds },
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
|
||||
+13
-3
@@ -12,7 +12,7 @@ import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.
|
||||
|
||||
export type AgentActorContext = {
|
||||
actorContext: ActorMetadata;
|
||||
roleId: string | undefined;
|
||||
roleId: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -23,7 +23,7 @@ export class AgentActorContextService {
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
async buildUserActorContext(
|
||||
async buildUserAndAgentActorContext(
|
||||
userWorkspaceId: string,
|
||||
workspaceId: string,
|
||||
): Promise<AgentActorContext> {
|
||||
@@ -61,11 +61,21 @@ export class AgentActorContextService {
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (!roleId) {
|
||||
throw new AgentException(
|
||||
'User role not found',
|
||||
AgentExceptionCode.AGENT_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
const actorContext = buildCreatedByFromFullNameMetadata({
|
||||
fullNameMetadata: workspaceMember.name,
|
||||
workspaceMemberId: workspaceMember.id,
|
||||
});
|
||||
|
||||
return { actorContext, roleId };
|
||||
return {
|
||||
actorContext,
|
||||
roleId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+2
-10
@@ -1,7 +1,7 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { type ObjectsPermissionsByRoleIdDeprecated } from 'twenty-shared/types';
|
||||
import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types';
|
||||
import { In, type Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
@@ -53,13 +53,9 @@ describe('FieldPermissionService', () => {
|
||||
isEditable: true,
|
||||
} as RoleEntity;
|
||||
|
||||
const mockRolesPermissions: ObjectsPermissionsByRoleIdDeprecated = {
|
||||
const mockRolesPermissions: ObjectsPermissionsByRoleId = {
|
||||
[testRoleId]: {
|
||||
[testObjectMetadataId]: {
|
||||
canRead: true,
|
||||
canUpdate: true,
|
||||
canSoftDelete: false,
|
||||
canDestroy: false,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
@@ -67,10 +63,6 @@ describe('FieldPermissionService', () => {
|
||||
restrictedFields: {},
|
||||
},
|
||||
[fieldRelationMock.objectMetadataId]: {
|
||||
canRead: true,
|
||||
canUpdate: true,
|
||||
canSoftDelete: false,
|
||||
canDestroy: false,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { type ObjectsPermissionsByRoleIdDeprecated } from 'twenty-shared/types';
|
||||
import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
@@ -183,7 +183,7 @@ export class FieldPermissionService {
|
||||
allFieldPermissions: UpsertFieldPermissionsInput['fieldPermissions'];
|
||||
fieldPermission: UpsertFieldPermissionsInput['fieldPermissions'][0];
|
||||
objectMetadataMapsById: ObjectMetadataMaps['byId'];
|
||||
rolesPermissions: ObjectsPermissionsByRoleIdDeprecated;
|
||||
rolesPermissions: ObjectsPermissionsByRoleId;
|
||||
role: RoleEntity;
|
||||
}) {
|
||||
const duplicateFieldPermissions = allFieldPermissions.filter(
|
||||
|
||||
+87
-15
@@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service';
|
||||
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
|
||||
@@ -17,6 +17,7 @@ import { type UserWorkspacePermissions } from 'src/engine/metadata-modules/permi
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
|
||||
@Injectable()
|
||||
export class PermissionsService {
|
||||
@@ -188,30 +189,101 @@ export class PermissionsService {
|
||||
);
|
||||
}
|
||||
|
||||
private async getRolesFromPermissionConfig(
|
||||
rolePermissionConfig: RolePermissionConfig,
|
||||
workspaceId: string,
|
||||
relations: string[] = [],
|
||||
): Promise<{ roles: RoleEntity[]; useIntersection: boolean } | null> {
|
||||
if ('shouldBypassPermissionChecks' in rolePermissionConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let roleIds: string[] = [];
|
||||
let useIntersection = false;
|
||||
|
||||
if ('intersectionOf' in rolePermissionConfig) {
|
||||
roleIds = rolePermissionConfig.intersectionOf;
|
||||
useIntersection = true;
|
||||
} else if ('unionOf' in rolePermissionConfig) {
|
||||
roleIds = rolePermissionConfig.unionOf;
|
||||
useIntersection = false;
|
||||
}
|
||||
|
||||
if (roleIds.length === 0) {
|
||||
throw new Error('No role IDs provided');
|
||||
}
|
||||
|
||||
const roles = await this.roleRepository.find({
|
||||
where: { id: In(roleIds), workspaceId },
|
||||
relations,
|
||||
});
|
||||
|
||||
if (roles.length !== roleIds.length) {
|
||||
throw new Error('Some roles not found');
|
||||
}
|
||||
|
||||
return { roles, useIntersection };
|
||||
}
|
||||
|
||||
public async checkRolesPermissions(
|
||||
rolePermissionConfig: RolePermissionConfig,
|
||||
workspaceId: string,
|
||||
setting: PermissionFlagType,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const result = await this.getRolesFromPermissionConfig(
|
||||
rolePermissionConfig,
|
||||
workspaceId,
|
||||
['permissionFlags'],
|
||||
);
|
||||
|
||||
if (result === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { roles, useIntersection } = result;
|
||||
|
||||
return useIntersection
|
||||
? roles.every((role) => this.checkRolePermissions(role, setting))
|
||||
: roles.some((role) => this.checkRolePermissions(role, setting));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async hasToolPermission(
|
||||
roleId: string,
|
||||
rolePermissionConfig: RolePermissionConfig,
|
||||
workspaceId: string,
|
||||
flag: PermissionFlagType,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: { id: roleId, workspaceId },
|
||||
relations: ['permissionFlags'],
|
||||
});
|
||||
const result = await this.getRolesFromPermissionConfig(
|
||||
rolePermissionConfig,
|
||||
workspaceId,
|
||||
['permissionFlags'],
|
||||
);
|
||||
|
||||
if (!role) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (role.canAccessAllTools === true) {
|
||||
if (result === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const permissionFlags = role.permissionFlags ?? [];
|
||||
const { roles, useIntersection } = result;
|
||||
|
||||
return permissionFlags.some(
|
||||
(permissionFlag) => permissionFlag.flag === flag,
|
||||
);
|
||||
const checkRoleHasPermission = (role: RoleEntity) => {
|
||||
if (role.canAccessAllTools === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const permissionFlags = role.permissionFlags ?? [];
|
||||
|
||||
return permissionFlags.some(
|
||||
(permissionFlag) => permissionFlag.flag === flag,
|
||||
);
|
||||
};
|
||||
|
||||
return useIntersection
|
||||
? roles.every(checkRoleHasPermission)
|
||||
: roles.some(checkRoleHasPermission);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { type ObjectsPermissionsByRoleIdDeprecated } from 'twenty-shared/types';
|
||||
import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
|
||||
@@ -34,12 +34,12 @@ export class WorkspacePermissionsCacheStorageService {
|
||||
|
||||
async setRolesPermissions(
|
||||
workspaceId: string,
|
||||
permissions: ObjectsPermissionsByRoleIdDeprecated,
|
||||
permissions: ObjectsPermissionsByRoleId,
|
||||
): Promise<{
|
||||
newRolesPermissionsVersion: string;
|
||||
}> {
|
||||
const [, newRolesPermissionsVersion] = await Promise.all([
|
||||
this.cacheStorageService.set<ObjectsPermissionsByRoleIdDeprecated>(
|
||||
this.cacheStorageService.set<ObjectsPermissionsByRoleId>(
|
||||
`${WORKSPACE_CACHE_KEYS.MetadataPermissionsRolesPermissions}:${workspaceId}`,
|
||||
permissions,
|
||||
TTL_INFINITE,
|
||||
@@ -52,8 +52,8 @@ export class WorkspacePermissionsCacheStorageService {
|
||||
|
||||
getRolesPermissions(
|
||||
workspaceId: string,
|
||||
): Promise<ObjectsPermissionsByRoleIdDeprecated | undefined> {
|
||||
return this.cacheStorageService.get<ObjectsPermissionsByRoleIdDeprecated>(
|
||||
): Promise<ObjectsPermissionsByRoleId | undefined> {
|
||||
return this.cacheStorageService.get<ObjectsPermissionsByRoleId>(
|
||||
`${WORKSPACE_CACHE_KEYS.MetadataPermissionsRolesPermissions}:${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
+8
-16
@@ -3,9 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Record } from 'cloudflare/core';
|
||||
import {
|
||||
ObjectsPermissions,
|
||||
type ObjectsPermissionsByRoleIdDeprecated,
|
||||
type ObjectsPermissionsDeprecated,
|
||||
type ObjectsPermissions,
|
||||
type ObjectsPermissionsByRoleId,
|
||||
type RestrictedFieldsPermissions,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -48,7 +47,7 @@ export class WorkspacePermissionsCacheService {
|
||||
private readonly workspacePermissionsCacheStorageService: WorkspacePermissionsCacheStorageService,
|
||||
private readonly getRolesPermissionsFromCacheWithRecomputeService: GetDataFromCacheWithRecomputeService<
|
||||
string,
|
||||
ObjectsPermissionsByRoleIdDeprecated
|
||||
ObjectsPermissionsByRoleId
|
||||
>,
|
||||
private readonly getUserWorkspaceRoleMapFromCacheWithRecomputeService: GetDataFromCacheWithRecomputeService<
|
||||
string,
|
||||
@@ -67,9 +66,7 @@ export class WorkspacePermissionsCacheService {
|
||||
workspaceId: string;
|
||||
roleIds?: string[];
|
||||
}): Promise<void> {
|
||||
let currentRolesPermissions:
|
||||
| ObjectsPermissionsByRoleIdDeprecated
|
||||
| undefined;
|
||||
let currentRolesPermissions: ObjectsPermissionsByRoleId | undefined;
|
||||
|
||||
if (roleIds) {
|
||||
currentRolesPermissions =
|
||||
@@ -124,7 +121,7 @@ export class WorkspacePermissionsCacheService {
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}): Promise<CacheResult<string, ObjectsPermissionsByRoleIdDeprecated>> {
|
||||
}): Promise<CacheResult<string, ObjectsPermissionsByRoleId>> {
|
||||
return this.getRolesPermissionsFromCacheWithRecomputeService.getFromCacheWithRecompute(
|
||||
{
|
||||
workspaceId,
|
||||
@@ -194,7 +191,7 @@ export class WorkspacePermissionsCacheService {
|
||||
}: {
|
||||
workspaceId: string;
|
||||
roleIds?: string[];
|
||||
}): Promise<ObjectsPermissionsByRoleIdDeprecated> {
|
||||
}): Promise<ObjectsPermissionsByRoleId> {
|
||||
let roles: RoleEntity[] = [];
|
||||
|
||||
roles = await this.roleRepository.find({
|
||||
@@ -208,11 +205,10 @@ export class WorkspacePermissionsCacheService {
|
||||
const workspaceObjectMetadataCollection =
|
||||
await this.getWorkspaceObjectMetadataCollection(workspaceId);
|
||||
|
||||
const permissionsByRoleId: ObjectsPermissionsByRoleIdDeprecated = {};
|
||||
const permissionsByRoleId: ObjectsPermissionsByRoleId = {};
|
||||
|
||||
for (const role of roles) {
|
||||
const objectRecordsPermissions: ObjectsPermissionsDeprecated &
|
||||
ObjectsPermissions = {};
|
||||
const objectRecordsPermissions: ObjectsPermissions = {};
|
||||
|
||||
for (const objectMetadata of workspaceObjectMetadataCollection) {
|
||||
const { id: objectMetadataId, isSystem, standardId } = objectMetadata;
|
||||
@@ -288,10 +284,6 @@ export class WorkspacePermissionsCacheService {
|
||||
}
|
||||
|
||||
objectRecordsPermissions[objectMetadataId] = {
|
||||
canRead,
|
||||
canUpdate,
|
||||
canSoftDelete,
|
||||
canDestroy,
|
||||
canReadObjectRecords: canRead,
|
||||
canUpdateObjectRecords: canUpdate,
|
||||
canSoftDeleteObjectRecords: canSoftDelete,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type Entity } from '@microsoft/microsoft-graph-types';
|
||||
import { isDefined } from 'class-validator';
|
||||
import { type ObjectsPermissionsByRoleIdDeprecated } from 'twenty-shared/types';
|
||||
import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types';
|
||||
import {
|
||||
DataSource,
|
||||
type DataSourceOptions,
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
|
||||
import { type WorkspaceQueryRunner } from 'src/engine/twenty-orm/query-runner/workspace-query-runner';
|
||||
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
|
||||
type CreateQueryBuilderOptions = {
|
||||
calledByWorkspaceEntityManager?: boolean;
|
||||
@@ -34,7 +35,7 @@ export class WorkspaceDataSource extends DataSource {
|
||||
featureFlagMapVersion: string;
|
||||
featureFlagMap: FeatureFlagMap;
|
||||
rolesPermissionsVersion: string;
|
||||
permissionsPerRoleId: ObjectsPermissionsByRoleIdDeprecated;
|
||||
permissionsPerRoleId: ObjectsPermissionsByRoleId;
|
||||
dataSourceWithOverridenCreateQueryBuilder: WorkspaceDataSource;
|
||||
isPoolSharingEnabled: boolean;
|
||||
|
||||
@@ -44,7 +45,7 @@ export class WorkspaceDataSource extends DataSource {
|
||||
featureFlagMapVersion: string,
|
||||
featureFlagMap: FeatureFlagMap,
|
||||
rolesPermissionsVersion: string,
|
||||
permissionsPerRoleId: ObjectsPermissionsByRoleIdDeprecated,
|
||||
permissionsPerRoleId: ObjectsPermissionsByRoleId,
|
||||
isPoolSharingEnabled: boolean,
|
||||
) {
|
||||
super(options);
|
||||
@@ -60,31 +61,10 @@ export class WorkspaceDataSource extends DataSource {
|
||||
|
||||
override getRepository<Entity extends ObjectLiteral>(
|
||||
target: EntityTarget<Entity>,
|
||||
shouldBypassPermissionChecks = false,
|
||||
roleId?: string,
|
||||
permissionOptions?: RolePermissionConfig,
|
||||
authContext?: AuthContext,
|
||||
): WorkspaceRepository<Entity> {
|
||||
if (shouldBypassPermissionChecks === true) {
|
||||
return this.manager.getRepository(
|
||||
target,
|
||||
{
|
||||
shouldBypassPermissionChecks: true,
|
||||
},
|
||||
authContext,
|
||||
);
|
||||
}
|
||||
|
||||
if (roleId) {
|
||||
return this.manager.getRepository(
|
||||
target,
|
||||
{
|
||||
roleId,
|
||||
},
|
||||
authContext,
|
||||
);
|
||||
}
|
||||
|
||||
return this.manager.getRepository(target, undefined, authContext);
|
||||
return this.manager.getRepository(target, permissionOptions, authContext);
|
||||
}
|
||||
|
||||
override createEntityManager(
|
||||
@@ -238,9 +218,7 @@ export class WorkspaceDataSource extends DataSource {
|
||||
this.rolesPermissionsVersion = rolesPermissionsVersion;
|
||||
}
|
||||
|
||||
setRolesPermissions(
|
||||
permissionsPerRoleId: ObjectsPermissionsByRoleIdDeprecated,
|
||||
) {
|
||||
setRolesPermissions(permissionsPerRoleId: ObjectsPermissionsByRoleId) {
|
||||
this.permissionsPerRoleId = permissionsPerRoleId;
|
||||
}
|
||||
|
||||
|
||||
+6
-6
@@ -1,4 +1,4 @@
|
||||
import { type ObjectsPermissionsDeprecated } from 'twenty-shared/types';
|
||||
import { type ObjectsPermissions } from 'twenty-shared/types';
|
||||
import { EntityManager } from 'typeorm';
|
||||
import { EntityPersistExecutor } from 'typeorm/persistence/EntityPersistExecutor';
|
||||
import { PlainObjectToDatabaseEntityTransformer } from 'typeorm/query-builder/transformer/PlainObjectToDatabaseEntityTransformer';
|
||||
@@ -75,7 +75,7 @@ describe('WorkspaceEntityManager', () => {
|
||||
let mockDataSource: WorkspaceDataSource;
|
||||
let mockPermissionOptions: {
|
||||
shouldBypassPermissionChecks: boolean;
|
||||
objectRecordsPermissions?: ObjectsPermissionsDeprecated;
|
||||
objectRecordsPermissions?: ObjectsPermissions;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -184,10 +184,10 @@ describe('WorkspaceEntityManager', () => {
|
||||
shouldBypassPermissionChecks: false,
|
||||
objectRecordsPermissions: {
|
||||
'test-entity': {
|
||||
canRead: true,
|
||||
canUpdate: false,
|
||||
canSoftDelete: false,
|
||||
canDestroy: false,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
restrictedFields: {},
|
||||
},
|
||||
},
|
||||
|
||||
+54
-22
@@ -1,5 +1,8 @@
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { type ObjectsPermissionsDeprecated } from 'twenty-shared/types';
|
||||
import {
|
||||
type ObjectsPermissions,
|
||||
type ObjectsPermissionsByRoleId,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type DeleteResult,
|
||||
@@ -53,14 +56,16 @@ import {
|
||||
} from 'src/engine/twenty-orm/repository/permissions.utils';
|
||||
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
|
||||
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
import { computePermissionIntersection } from 'src/engine/twenty-orm/utils/compute-permission-intersection.util';
|
||||
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
|
||||
import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util';
|
||||
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
|
||||
|
||||
type PermissionOptions = {
|
||||
shouldBypassPermissionChecks?: boolean;
|
||||
objectRecordsPermissions?: ObjectsPermissionsDeprecated;
|
||||
objectRecordsPermissions?: ObjectsPermissions;
|
||||
};
|
||||
|
||||
export class WorkspaceEntityManager extends EntityManager {
|
||||
@@ -83,35 +88,62 @@ export class WorkspaceEntityManager extends EntityManager {
|
||||
return this.connection.featureFlagMap;
|
||||
}
|
||||
|
||||
private getPermissionsForRole(
|
||||
roleId: string,
|
||||
permissionsPerRoleId: ObjectsPermissionsByRoleId,
|
||||
): ObjectsPermissions {
|
||||
if (!isDefined(permissionsPerRoleId?.[roleId])) {
|
||||
throw new PermissionsException(
|
||||
`No permissions found for role in datasource (roleId: ${roleId})`,
|
||||
PermissionsExceptionCode.NO_PERMISSIONS_FOUND_IN_DATASOURCE,
|
||||
);
|
||||
}
|
||||
|
||||
return permissionsPerRoleId[roleId];
|
||||
}
|
||||
|
||||
override getRepository<Entity extends ObjectLiteral>(
|
||||
target: EntityTarget<Entity>,
|
||||
permissionOptions?: {
|
||||
shouldBypassPermissionChecks?: boolean;
|
||||
roleId?: string;
|
||||
},
|
||||
rolePermissionConfig?: RolePermissionConfig,
|
||||
authContext?: AuthContext,
|
||||
): WorkspaceRepository<Entity> {
|
||||
const dataSource = this.connection;
|
||||
|
||||
let objectPermissions = {};
|
||||
let shouldBypassPermissionChecks = false;
|
||||
const objectPermissionsByRoleId = dataSource.permissionsPerRoleId;
|
||||
|
||||
if (permissionOptions?.roleId) {
|
||||
const objectPermissionsByRoleId = dataSource.permissionsPerRoleId;
|
||||
if (
|
||||
rolePermissionConfig &&
|
||||
'shouldBypassPermissionChecks' in rolePermissionConfig
|
||||
) {
|
||||
shouldBypassPermissionChecks =
|
||||
rolePermissionConfig.shouldBypassPermissionChecks;
|
||||
}
|
||||
|
||||
if (!isDefined(objectPermissionsByRoleId?.[permissionOptions.roleId])) {
|
||||
throw new PermissionsException(
|
||||
`No permissions found for role in datasource (missing ${
|
||||
!isDefined(objectPermissionsByRoleId)
|
||||
? 'objectPermissionsByRoleId object'
|
||||
: `roleId in objectPermissionsByRoleId object (${permissionOptions.roleId})`
|
||||
})`,
|
||||
PermissionsExceptionCode.NO_PERMISSIONS_FOUND_IN_DATASOURCE,
|
||||
if (rolePermissionConfig && 'unionOf' in rolePermissionConfig) {
|
||||
if (rolePermissionConfig.unionOf.length === 1) {
|
||||
objectPermissions = this.getPermissionsForRole(
|
||||
rolePermissionConfig.unionOf[0],
|
||||
objectPermissionsByRoleId,
|
||||
);
|
||||
} else {
|
||||
objectPermissions = objectPermissionsByRoleId[permissionOptions.roleId];
|
||||
// TODO: Implement union logic for combining permissions across multiple roles
|
||||
throw new Error(
|
||||
'Union permission logic for multiple roles not yet implemented',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (rolePermissionConfig && 'intersectionOf' in rolePermissionConfig) {
|
||||
const allRolePermissions = rolePermissionConfig.intersectionOf.map(
|
||||
(roleId: string) =>
|
||||
this.getPermissionsForRole(roleId, objectPermissionsByRoleId),
|
||||
);
|
||||
|
||||
objectPermissions = computePermissionIntersection(allRolePermissions);
|
||||
}
|
||||
|
||||
const newRepository = new WorkspaceRepository<Entity>(
|
||||
this.internalContext,
|
||||
target,
|
||||
@@ -119,7 +151,7 @@ export class WorkspaceEntityManager extends EntityManager {
|
||||
dataSource.featureFlagMap,
|
||||
this.queryRunner,
|
||||
objectPermissions,
|
||||
permissionOptions?.shouldBypassPermissionChecks,
|
||||
shouldBypassPermissionChecks,
|
||||
authContext,
|
||||
);
|
||||
|
||||
@@ -132,7 +164,7 @@ export class WorkspaceEntityManager extends EntityManager {
|
||||
queryRunner?: QueryRunner,
|
||||
options: {
|
||||
shouldBypassPermissionChecks?: boolean;
|
||||
objectRecordsPermissions?: ObjectsPermissionsDeprecated;
|
||||
objectRecordsPermissions?: ObjectsPermissions;
|
||||
} = {
|
||||
shouldBypassPermissionChecks: false,
|
||||
objectRecordsPermissions: {},
|
||||
@@ -200,7 +232,7 @@ export class WorkspaceEntityManager extends EntityManager {
|
||||
conflictPathsOrOptions: string[] | UpsertOptions<Entity>,
|
||||
permissionOptions?: {
|
||||
shouldBypassPermissionChecks?: boolean;
|
||||
objectRecordsPermissions?: ObjectsPermissionsDeprecated;
|
||||
objectRecordsPermissions?: ObjectsPermissions;
|
||||
},
|
||||
selectedColumns: string[] | '*' = '*',
|
||||
): Promise<InsertResult> {
|
||||
@@ -389,7 +421,7 @@ export class WorkspaceEntityManager extends EntityManager {
|
||||
operationType: OperationType;
|
||||
permissionOptions?: {
|
||||
shouldBypassPermissionChecks?: boolean;
|
||||
objectRecordsPermissions?: ObjectsPermissionsDeprecated;
|
||||
objectRecordsPermissions?: ObjectsPermissions;
|
||||
};
|
||||
selectedColumns: string[];
|
||||
updatedColumns?: string[];
|
||||
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { type ObjectsPermissionsByRoleIdDeprecated } from 'twenty-shared/types';
|
||||
import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { EntitySchema, Repository } from 'typeorm';
|
||||
|
||||
@@ -55,7 +55,7 @@ export class WorkspaceDatasourceFactory {
|
||||
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
|
||||
private readonly getFromCacheWithRecomputeService: GetDataFromCacheWithRecomputeService<
|
||||
string,
|
||||
ObjectsPermissionsByRoleIdDeprecated
|
||||
ObjectsPermissionsByRoleId
|
||||
>,
|
||||
) {}
|
||||
|
||||
@@ -239,7 +239,7 @@ export class WorkspaceDatasourceFactory {
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}): Promise<CacheResult<string, ObjectsPermissionsByRoleIdDeprecated>> {
|
||||
}): Promise<CacheResult<string, ObjectsPermissionsByRoleId>> {
|
||||
return this.getFromCacheWithRecomputeService.getFromCacheWithRecompute({
|
||||
workspaceId,
|
||||
getCacheData: () =>
|
||||
@@ -292,7 +292,7 @@ export class WorkspaceDatasourceFactory {
|
||||
}: {
|
||||
workspaceDataSource: WorkspaceDataSource;
|
||||
cachedRolesPermissionsVersion: string;
|
||||
cachedRolesPermissions: ObjectsPermissionsByRoleIdDeprecated;
|
||||
cachedRolesPermissions: ObjectsPermissionsByRoleId;
|
||||
}): Promise<void> {
|
||||
this.updateWorkspaceDataSourceIfNeeded({
|
||||
workspaceDataSource,
|
||||
|
||||
+6
-6
@@ -1,4 +1,4 @@
|
||||
import { type ObjectsPermissionsDeprecated } from 'twenty-shared/types';
|
||||
import { type ObjectsPermissions } from 'twenty-shared/types';
|
||||
import {
|
||||
type DeepPartial,
|
||||
type FindManyOptions,
|
||||
@@ -20,7 +20,7 @@ describe('WorkspaceRepository', () => {
|
||||
let mockEntityManager: jest.Mocked<WorkspaceEntityManager>;
|
||||
let mockInternalContext: WorkspaceInternalContext;
|
||||
let mockFeatureFlagMap: FeatureFlagMap;
|
||||
let mockObjectRecordsPermissions: ObjectsPermissionsDeprecated;
|
||||
let mockObjectRecordsPermissions: ObjectsPermissions;
|
||||
let mockQueryRunner: QueryRunner;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -71,10 +71,10 @@ describe('WorkspaceRepository', () => {
|
||||
);
|
||||
mockObjectRecordsPermissions = {
|
||||
'test-entity': {
|
||||
canRead: true,
|
||||
canUpdate: false,
|
||||
canSoftDelete: false,
|
||||
canDestroy: false,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
restrictedFields: {},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import {
|
||||
type ObjectsPermissionsDeprecated,
|
||||
type ObjectsPermissions,
|
||||
type RestrictedFieldsPermissions,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -38,7 +38,7 @@ export type OperationType =
|
||||
type ValidateOperationIsPermittedOrThrowArgs = {
|
||||
entityName: string;
|
||||
operationType: OperationType;
|
||||
objectsPermissions: ObjectsPermissionsDeprecated;
|
||||
objectsPermissions: ObjectsPermissions;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
selectedColumns: string[] | '*';
|
||||
allFieldsSelected: boolean;
|
||||
@@ -86,7 +86,7 @@ export const validateOperationIsPermittedOrThrow = ({
|
||||
|
||||
switch (operationType) {
|
||||
case 'select':
|
||||
if (!permissionsForEntity?.canRead) {
|
||||
if (!permissionsForEntity?.canReadObjectRecords) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
@@ -102,7 +102,7 @@ export const validateOperationIsPermittedOrThrow = ({
|
||||
break;
|
||||
case 'insert':
|
||||
case 'update':
|
||||
if (!permissionsForEntity?.canUpdate) {
|
||||
if (!permissionsForEntity?.canUpdateObjectRecords) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
@@ -124,7 +124,7 @@ export const validateOperationIsPermittedOrThrow = ({
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
if (!permissionsForEntity?.canDestroy) {
|
||||
if (!permissionsForEntity?.canDestroyObjectRecords) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
@@ -139,7 +139,7 @@ export const validateOperationIsPermittedOrThrow = ({
|
||||
break;
|
||||
case 'restore':
|
||||
case 'soft-delete':
|
||||
if (!permissionsForEntity?.canSoftDelete) {
|
||||
if (!permissionsForEntity?.canSoftDeleteObjectRecords) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
@@ -166,7 +166,7 @@ export const validateOperationIsPermittedOrThrow = ({
|
||||
|
||||
type ValidateQueryIsPermittedOrThrowArgs = {
|
||||
expressionMap: QueryExpressionMap;
|
||||
objectsPermissions: ObjectsPermissionsDeprecated;
|
||||
objectsPermissions: ObjectsPermissions;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
shouldBypassPermissionChecks: boolean;
|
||||
};
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
import { type ObjectsPermissionsDeprecated } from 'twenty-shared/types';
|
||||
import { type ObjectsPermissions } from 'twenty-shared/types';
|
||||
import {
|
||||
DeleteQueryBuilder,
|
||||
type DeleteResult,
|
||||
@@ -25,21 +25,21 @@ import { type WorkspaceSoftDeleteQueryBuilder } from 'src/engine/twenty-orm/repo
|
||||
import { type WorkspaceUpdateQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-update-query-builder';
|
||||
import { applyTableAliasOnWhereCondition } from 'src/engine/twenty-orm/utils/apply-table-alias-on-where-condition';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util';
|
||||
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
|
||||
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
|
||||
import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util';
|
||||
|
||||
export class WorkspaceDeleteQueryBuilder<
|
||||
T extends ObjectLiteral,
|
||||
> extends DeleteQueryBuilder<T> {
|
||||
private objectRecordsPermissions: ObjectsPermissionsDeprecated;
|
||||
private objectRecordsPermissions: ObjectsPermissions;
|
||||
private shouldBypassPermissionChecks: boolean;
|
||||
private internalContext: WorkspaceInternalContext;
|
||||
private authContext?: AuthContext;
|
||||
private featureFlagMap?: FeatureFlagMap;
|
||||
constructor(
|
||||
queryBuilder: DeleteQueryBuilder<T>,
|
||||
objectRecordsPermissions: ObjectsPermissionsDeprecated,
|
||||
objectRecordsPermissions: ObjectsPermissions,
|
||||
internalContext: WorkspaceInternalContext,
|
||||
shouldBypassPermissionChecks: boolean,
|
||||
authContext?: AuthContext,
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
import { type ObjectsPermissionsDeprecated } from 'twenty-shared/types';
|
||||
import { type ObjectsPermissions } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type EntityTarget,
|
||||
@@ -29,13 +29,13 @@ import { type WorkspaceSoftDeleteQueryBuilder } from 'src/engine/twenty-orm/repo
|
||||
import { type WorkspaceUpdateQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-update-query-builder';
|
||||
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
|
||||
import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util';
|
||||
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
|
||||
|
||||
export class WorkspaceInsertQueryBuilder<
|
||||
T extends ObjectLiteral,
|
||||
> extends InsertQueryBuilder<T> {
|
||||
private objectRecordsPermissions: ObjectsPermissionsDeprecated;
|
||||
private objectRecordsPermissions: ObjectsPermissions;
|
||||
private shouldBypassPermissionChecks: boolean;
|
||||
private internalContext: WorkspaceInternalContext;
|
||||
private authContext?: AuthContext;
|
||||
@@ -47,7 +47,7 @@ export class WorkspaceInsertQueryBuilder<
|
||||
|
||||
constructor(
|
||||
queryBuilder: InsertQueryBuilder<T>,
|
||||
objectRecordsPermissions: ObjectsPermissionsDeprecated,
|
||||
objectRecordsPermissions: ObjectsPermissions,
|
||||
internalContext: WorkspaceInternalContext,
|
||||
shouldBypassPermissionChecks: boolean,
|
||||
authContext?: AuthContext,
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import { type ObjectsPermissionsDeprecated } from 'twenty-shared/types';
|
||||
import { type ObjectsPermissions } from 'twenty-shared/types';
|
||||
import {
|
||||
type EntityTarget,
|
||||
type ObjectLiteral,
|
||||
@@ -30,14 +30,14 @@ import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/g
|
||||
export class WorkspaceSelectQueryBuilder<
|
||||
T extends ObjectLiteral,
|
||||
> extends SelectQueryBuilder<T> {
|
||||
objectRecordsPermissions: ObjectsPermissionsDeprecated;
|
||||
objectRecordsPermissions: ObjectsPermissions;
|
||||
shouldBypassPermissionChecks: boolean;
|
||||
internalContext: WorkspaceInternalContext;
|
||||
authContext?: AuthContext;
|
||||
featureFlagMap?: FeatureFlagMap;
|
||||
constructor(
|
||||
queryBuilder: SelectQueryBuilder<T>,
|
||||
objectRecordsPermissions: ObjectsPermissionsDeprecated,
|
||||
objectRecordsPermissions: ObjectsPermissions,
|
||||
internalContext: WorkspaceInternalContext,
|
||||
shouldBypassPermissionChecks: boolean,
|
||||
authContext?: AuthContext,
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
import { type ObjectsPermissionsDeprecated } from 'twenty-shared/types';
|
||||
import { type ObjectsPermissions } from 'twenty-shared/types';
|
||||
import {
|
||||
type EntityTarget,
|
||||
type InsertQueryBuilder,
|
||||
@@ -22,13 +22,13 @@ import { type WorkspaceDeleteQueryBuilder } from 'src/engine/twenty-orm/reposito
|
||||
import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
|
||||
import { type WorkspaceUpdateQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-update-query-builder';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
|
||||
import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util';
|
||||
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
|
||||
|
||||
export class WorkspaceSoftDeleteQueryBuilder<
|
||||
T extends ObjectLiteral,
|
||||
> extends SoftDeleteQueryBuilder<T> {
|
||||
private objectRecordsPermissions: ObjectsPermissionsDeprecated;
|
||||
private objectRecordsPermissions: ObjectsPermissions;
|
||||
private shouldBypassPermissionChecks: boolean;
|
||||
private internalContext: WorkspaceInternalContext;
|
||||
private authContext?: AuthContext;
|
||||
@@ -36,7 +36,7 @@ export class WorkspaceSoftDeleteQueryBuilder<
|
||||
|
||||
constructor(
|
||||
queryBuilder: SoftDeleteQueryBuilder<T>,
|
||||
objectRecordsPermissions: ObjectsPermissionsDeprecated,
|
||||
objectRecordsPermissions: ObjectsPermissions,
|
||||
internalContext: WorkspaceInternalContext,
|
||||
shouldBypassPermissionChecks: boolean,
|
||||
authContext?: AuthContext,
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
import { type ObjectsPermissionsDeprecated } from 'twenty-shared/types';
|
||||
import { type ObjectsPermissions } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type EntityTarget,
|
||||
@@ -28,13 +28,13 @@ import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/wo
|
||||
import { type WorkspaceSoftDeleteQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-soft-delete-query-builder';
|
||||
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
|
||||
import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util';
|
||||
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
|
||||
|
||||
export class WorkspaceUpdateQueryBuilder<
|
||||
T extends ObjectLiteral,
|
||||
> extends UpdateQueryBuilder<T> {
|
||||
private objectRecordsPermissions: ObjectsPermissionsDeprecated;
|
||||
private objectRecordsPermissions: ObjectsPermissions;
|
||||
private shouldBypassPermissionChecks: boolean;
|
||||
private internalContext: WorkspaceInternalContext;
|
||||
private authContext?: AuthContext;
|
||||
@@ -50,7 +50,7 @@ export class WorkspaceUpdateQueryBuilder<
|
||||
|
||||
constructor(
|
||||
queryBuilder: UpdateQueryBuilder<T>,
|
||||
objectRecordsPermissions: ObjectsPermissionsDeprecated,
|
||||
objectRecordsPermissions: ObjectsPermissions,
|
||||
internalContext: WorkspaceInternalContext,
|
||||
shouldBypassPermissionChecks: boolean,
|
||||
authContext?: AuthContext,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type ObjectsPermissionsDeprecated } from 'twenty-shared/types';
|
||||
import { type ObjectsPermissions } from 'twenty-shared/types';
|
||||
import {
|
||||
type DeepPartial,
|
||||
type DeleteResult,
|
||||
@@ -40,7 +40,7 @@ export class WorkspaceRepository<
|
||||
private readonly internalContext: WorkspaceInternalContext;
|
||||
private shouldBypassPermissionChecks: boolean;
|
||||
private featureFlagMap: FeatureFlagMap;
|
||||
public readonly objectRecordsPermissions?: ObjectsPermissionsDeprecated;
|
||||
public readonly objectRecordsPermissions?: ObjectsPermissions;
|
||||
private authContext?: AuthContext;
|
||||
declare manager: WorkspaceEntityManager;
|
||||
|
||||
@@ -50,7 +50,7 @@ export class WorkspaceRepository<
|
||||
manager: WorkspaceEntityManager,
|
||||
featureFlagMap: FeatureFlagMap,
|
||||
queryRunner?: QueryRunner,
|
||||
objectRecordsPermissions?: ObjectsPermissionsDeprecated,
|
||||
objectRecordsPermissions?: ObjectsPermissions,
|
||||
shouldBypassPermissionChecks = false,
|
||||
authContext?: AuthContext,
|
||||
) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { type ObjectLiteral } from 'typeorm';
|
||||
|
||||
import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory';
|
||||
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
|
||||
|
||||
@Injectable()
|
||||
@@ -15,30 +16,19 @@ export class TwentyORMGlobalManager {
|
||||
async getRepositoryForWorkspace<T extends ObjectLiteral>(
|
||||
workspaceId: string,
|
||||
workspaceEntity: Type<T>,
|
||||
options?: {
|
||||
shouldBypassPermissionChecks?: boolean;
|
||||
roleId?: string;
|
||||
},
|
||||
options?: RolePermissionConfig,
|
||||
): Promise<WorkspaceRepository<T>>;
|
||||
|
||||
async getRepositoryForWorkspace<T extends ObjectLiteral>(
|
||||
workspaceId: string,
|
||||
objectMetadataName: string,
|
||||
options?: {
|
||||
shouldBypassPermissionChecks?: boolean;
|
||||
roleId?: string;
|
||||
},
|
||||
options?: RolePermissionConfig,
|
||||
): Promise<WorkspaceRepository<T>>;
|
||||
|
||||
async getRepositoryForWorkspace<T extends ObjectLiteral>(
|
||||
workspaceId: string,
|
||||
workspaceEntityOrObjectMetadataName: Type<T> | string,
|
||||
options: {
|
||||
shouldBypassPermissionChecks?: boolean;
|
||||
roleId?: string;
|
||||
} = {
|
||||
shouldBypassPermissionChecks: false,
|
||||
},
|
||||
options?: RolePermissionConfig,
|
||||
): Promise<WorkspaceRepository<T>> {
|
||||
let objectMetadataName: string;
|
||||
|
||||
@@ -55,8 +45,7 @@ export class TwentyORMGlobalManager {
|
||||
|
||||
const repository = workspaceDataSource.getRepository<T>(
|
||||
objectMetadataName,
|
||||
options.shouldBypassPermissionChecks,
|
||||
options.roleId,
|
||||
options,
|
||||
);
|
||||
|
||||
return repository;
|
||||
|
||||
@@ -74,8 +74,7 @@ export class TwentyORMManager {
|
||||
|
||||
return workspaceDataSource.getRepository<T>(
|
||||
objectMetadataName,
|
||||
false,
|
||||
roleId,
|
||||
roleId ? { unionOf: [roleId] } : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
type RoleId = string;
|
||||
|
||||
export type RolePermissionConfig =
|
||||
| { shouldBypassPermissionChecks: true }
|
||||
| { unionOf: RoleId[] }
|
||||
| { intersectionOf: RoleId[] };
|
||||
+340
@@ -0,0 +1,340 @@
|
||||
import { type ObjectsPermissions } from 'twenty-shared/types';
|
||||
|
||||
import { computePermissionIntersection } from 'src/engine/twenty-orm/utils/compute-permission-intersection.util';
|
||||
|
||||
describe('computePermissionIntersection', () => {
|
||||
const objectMetadataId1 = 'object-1';
|
||||
const objectMetadataId2 = 'object-2';
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should return empty object for empty array', () => {
|
||||
const result = computePermissionIntersection([]);
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should return same permissions for single role', () => {
|
||||
const permissions: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
restrictedFields: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = computePermissionIntersection([permissions]);
|
||||
|
||||
expect(result).toEqual(permissions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('intersection logic (AND)', () => {
|
||||
it('should require ALL roles to have permission (true AND true = true)', () => {
|
||||
const role1Permissions: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {},
|
||||
},
|
||||
};
|
||||
|
||||
const role2Permissions: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = computePermissionIntersection([
|
||||
role1Permissions,
|
||||
role2Permissions,
|
||||
]);
|
||||
|
||||
expect(result[objectMetadataId1]).toEqual({
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should deny if ANY role lacks permission (true AND false = false)', () => {
|
||||
const role1Permissions: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {},
|
||||
},
|
||||
};
|
||||
|
||||
const role2Permissions: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = computePermissionIntersection([
|
||||
role1Permissions,
|
||||
role2Permissions,
|
||||
]);
|
||||
|
||||
expect(result[objectMetadataId1]).toEqual({
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should deny all permissions if role lacks access to object entirely', () => {
|
||||
const role1Permissions: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {},
|
||||
},
|
||||
};
|
||||
|
||||
const role2Permissions: ObjectsPermissions = {};
|
||||
|
||||
const result = computePermissionIntersection([
|
||||
role1Permissions,
|
||||
role2Permissions,
|
||||
]);
|
||||
|
||||
expect(result[objectMetadataId1]).toEqual({
|
||||
canReadObjectRecords: false,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
restrictedFields: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple objects', () => {
|
||||
it('should compute intersection independently for each object', () => {
|
||||
const role1Permissions: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
restrictedFields: {},
|
||||
},
|
||||
[objectMetadataId2]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
restrictedFields: {},
|
||||
},
|
||||
};
|
||||
|
||||
const role2Permissions: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: false,
|
||||
restrictedFields: {},
|
||||
},
|
||||
[objectMetadataId2]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
restrictedFields: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = computePermissionIntersection([
|
||||
role1Permissions,
|
||||
role2Permissions,
|
||||
]);
|
||||
|
||||
expect(result[objectMetadataId1]).toEqual({
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
restrictedFields: {},
|
||||
});
|
||||
|
||||
expect(result[objectMetadataId2]).toEqual({
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: false,
|
||||
restrictedFields: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('restricted fields', () => {
|
||||
it('should compute intersection for restricted fields', () => {
|
||||
const role1Permissions: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {
|
||||
email: {
|
||||
canRead: null,
|
||||
canUpdate: false,
|
||||
},
|
||||
salary: {
|
||||
canRead: false,
|
||||
canUpdate: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const role2Permissions: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {
|
||||
email: {
|
||||
canRead: null,
|
||||
canUpdate: null,
|
||||
},
|
||||
salary: {
|
||||
canRead: null,
|
||||
canUpdate: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = computePermissionIntersection([
|
||||
role1Permissions,
|
||||
role2Permissions,
|
||||
]);
|
||||
|
||||
expect(result[objectMetadataId1].restrictedFields).toEqual({
|
||||
email: {
|
||||
canRead: null,
|
||||
canUpdate: false,
|
||||
},
|
||||
salary: {
|
||||
canRead: false,
|
||||
canUpdate: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle fields that only exist in some roles', () => {
|
||||
const role1Permissions: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {
|
||||
email: {
|
||||
canRead: false,
|
||||
canUpdate: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const role2Permissions: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {
|
||||
salary: {
|
||||
canRead: false,
|
||||
canUpdate: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = computePermissionIntersection([
|
||||
role1Permissions,
|
||||
role2Permissions,
|
||||
]);
|
||||
|
||||
expect(result[objectMetadataId1].restrictedFields).toEqual({
|
||||
email: {
|
||||
canRead: false,
|
||||
canUpdate: false,
|
||||
},
|
||||
salary: {
|
||||
canRead: false,
|
||||
canUpdate: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('three or more roles', () => {
|
||||
it('should compute intersection across all roles', () => {
|
||||
const role1: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {},
|
||||
},
|
||||
};
|
||||
|
||||
const role2: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {},
|
||||
},
|
||||
};
|
||||
|
||||
const role3: ObjectsPermissions = {
|
||||
[objectMetadataId1]: {
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = computePermissionIntersection([role1, role2, role3]);
|
||||
|
||||
expect(result[objectMetadataId1]).toEqual({
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
type ObjectsPermissions,
|
||||
type RestrictedFieldPermissions,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
export const computePermissionIntersection = (
|
||||
permissionsArray: ObjectsPermissions[],
|
||||
): ObjectsPermissions => {
|
||||
if (permissionsArray.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (permissionsArray.length === 1) {
|
||||
return permissionsArray[0];
|
||||
}
|
||||
|
||||
const result: ObjectsPermissions = {};
|
||||
|
||||
const allObjectMetadataIds = new Set<string>();
|
||||
|
||||
for (const permissions of permissionsArray) {
|
||||
for (const id of Object.keys(permissions)) {
|
||||
allObjectMetadataIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const objectMetadataId of allObjectMetadataIds) {
|
||||
let canReadObjectRecords = true;
|
||||
let canUpdateObjectRecords = true;
|
||||
let canSoftDeleteObjectRecords = true;
|
||||
let canDestroyObjectRecords = true;
|
||||
const restrictedFields: Record<string, RestrictedFieldPermissions> = {};
|
||||
|
||||
for (const permissions of permissionsArray) {
|
||||
const objPerm = permissions[objectMetadataId];
|
||||
|
||||
if (!objPerm) {
|
||||
canReadObjectRecords = false;
|
||||
canUpdateObjectRecords = false;
|
||||
canSoftDeleteObjectRecords = false;
|
||||
canDestroyObjectRecords = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
canReadObjectRecords =
|
||||
canReadObjectRecords && objPerm.canReadObjectRecords === true;
|
||||
canUpdateObjectRecords =
|
||||
canUpdateObjectRecords && objPerm.canUpdateObjectRecords === true;
|
||||
canSoftDeleteObjectRecords =
|
||||
canSoftDeleteObjectRecords &&
|
||||
objPerm.canSoftDeleteObjectRecords === true;
|
||||
canDestroyObjectRecords =
|
||||
canDestroyObjectRecords && objPerm.canDestroyObjectRecords === true;
|
||||
|
||||
if (objPerm.restrictedFields) {
|
||||
for (const [fieldName, fieldPerm] of Object.entries(
|
||||
objPerm.restrictedFields,
|
||||
)) {
|
||||
if (!restrictedFields[fieldName]) {
|
||||
restrictedFields[fieldName] = {
|
||||
canRead: null,
|
||||
canUpdate: null,
|
||||
};
|
||||
}
|
||||
|
||||
const current = restrictedFields[fieldName];
|
||||
|
||||
restrictedFields[fieldName] = {
|
||||
canRead:
|
||||
current.canRead === false || fieldPerm.canRead === false
|
||||
? false
|
||||
: null,
|
||||
canUpdate:
|
||||
current.canUpdate === false || fieldPerm.canUpdate === false
|
||||
? false
|
||||
: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result[objectMetadataId] = {
|
||||
canReadObjectRecords,
|
||||
canUpdateObjectRecords,
|
||||
canSoftDeleteObjectRecords,
|
||||
canDestroyObjectRecords,
|
||||
restrictedFields,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
+5
-1
@@ -49,10 +49,14 @@ export class WorkflowExecutionContextService {
|
||||
});
|
||||
}
|
||||
|
||||
const rolePermissionConfig = roleId
|
||||
? { unionOf: [roleId] }
|
||||
: { shouldBypassPermissionChecks: true as const };
|
||||
|
||||
return {
|
||||
isActingOnBehalfOfUser,
|
||||
initiator: workflowRun.createdBy,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
import { type ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
|
||||
export type WorkflowExecutionContext = {
|
||||
isActingOnBehalfOfUser: boolean;
|
||||
initiator: ActorMetadata;
|
||||
roleId?: string;
|
||||
rolePermissionConfig: RolePermissionConfig;
|
||||
};
|
||||
|
||||
+1
-1
@@ -85,7 +85,7 @@ export class AiAgentWorkflowAction implements WorkflowAction {
|
||||
actorContext: executionContext.isActingOnBehalfOfUser
|
||||
? executionContext.initiator
|
||||
: undefined,
|
||||
roleId: executionContext.roleId,
|
||||
rolePermissionConfig: executionContext.rolePermissionConfig,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
+28
-10
@@ -18,6 +18,7 @@ import { AGENT_SYSTEM_PROMPTS } from 'src/engine/metadata-modules/agent/constant
|
||||
import { convertOutputSchemaToZod } from 'src/engine/metadata-modules/agent/utils/convert-output-schema-to-zod';
|
||||
import { type ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
import { OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
|
||||
|
||||
@Injectable()
|
||||
@@ -35,7 +36,7 @@ export class AiAgentExecutorService {
|
||||
agentId: string,
|
||||
workspaceId: string,
|
||||
actorContext?: ActorMetadata,
|
||||
roleIdOverride?: string,
|
||||
rolePermissionConfig?: RolePermissionConfig,
|
||||
): Promise<ToolSet> {
|
||||
const roleTarget = await this.roleTargetsRepository.findOne({
|
||||
where: {
|
||||
@@ -45,21 +46,33 @@ export class AiAgentExecutorService {
|
||||
select: ['roleId'],
|
||||
});
|
||||
|
||||
const effectiveRoleId = roleIdOverride || roleTarget?.roleId;
|
||||
const agentRoleId = roleTarget?.roleId;
|
||||
|
||||
if (!effectiveRoleId) {
|
||||
const actionTools = await this.toolAdapterService.getTools();
|
||||
if (!rolePermissionConfig && !agentRoleId) {
|
||||
return await this.toolAdapterService.getTools();
|
||||
}
|
||||
|
||||
return { ...actionTools };
|
||||
let effectiveRoleContext: RolePermissionConfig;
|
||||
|
||||
if (
|
||||
rolePermissionConfig &&
|
||||
('intersectionOf' in rolePermissionConfig ||
|
||||
'unionOf' in rolePermissionConfig)
|
||||
) {
|
||||
effectiveRoleContext = rolePermissionConfig;
|
||||
} else if (agentRoleId) {
|
||||
effectiveRoleContext = { unionOf: [agentRoleId] };
|
||||
} else {
|
||||
return await this.toolAdapterService.getTools();
|
||||
}
|
||||
|
||||
const actionTools = await this.toolAdapterService.getTools(
|
||||
effectiveRoleId,
|
||||
effectiveRoleContext,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const databaseTools = await this.toolService.listTools(
|
||||
effectiveRoleId,
|
||||
effectiveRoleContext,
|
||||
workspaceId,
|
||||
actorContext,
|
||||
);
|
||||
@@ -75,20 +88,25 @@ export class AiAgentExecutorService {
|
||||
schema,
|
||||
userPrompt,
|
||||
actorContext,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
}: {
|
||||
agent: AgentEntity | null;
|
||||
schema: OutputSchema;
|
||||
userPrompt: string;
|
||||
actorContext?: ActorMetadata;
|
||||
roleId?: string;
|
||||
rolePermissionConfig?: RolePermissionConfig;
|
||||
}): Promise<AgentExecutionResult> {
|
||||
try {
|
||||
const registeredModel =
|
||||
await this.aiModelRegistryService.resolveModelForAgent(agent);
|
||||
|
||||
const tools = agent
|
||||
? await this.getTools(agent.id, agent.workspaceId, actorContext, roleId)
|
||||
? await this.getTools(
|
||||
agent.id,
|
||||
agent.workspaceId,
|
||||
actorContext,
|
||||
rolePermissionConfig,
|
||||
)
|
||||
: {};
|
||||
|
||||
this.logger.log(`Generated ${Object.keys(tools).length} tools for agent`);
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ export class CreateRecordWorkflowAction implements WorkflowAction {
|
||||
objectRecord: workflowActionInput.objectRecord,
|
||||
workspaceId,
|
||||
createdBy,
|
||||
roleId: executionContext.roleId,
|
||||
rolePermissionConfig: executionContext.rolePermissionConfig,
|
||||
});
|
||||
|
||||
if (!toolOutput.success) {
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ export class DeleteRecordWorkflowAction implements WorkflowAction {
|
||||
objectName: workflowActionInput.objectName,
|
||||
objectRecordId: workflowActionInput.objectRecordId,
|
||||
workspaceId,
|
||||
roleId: executionContext.roleId,
|
||||
rolePermissionConfig: executionContext.rolePermissionConfig,
|
||||
soft: true,
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ export class FindRecordsWorkflowAction implements WorkflowAction {
|
||||
orderBy: workflowActionInput.orderBy,
|
||||
limit: workflowActionInput.limit,
|
||||
workspaceId,
|
||||
roleId: executionContext.roleId,
|
||||
rolePermissionConfig: executionContext.rolePermissionConfig,
|
||||
});
|
||||
|
||||
if (!toolOutput.success) {
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ export class UpdateRecordWorkflowAction implements WorkflowAction {
|
||||
objectRecord: workflowActionInput.objectRecord,
|
||||
fieldsToUpdate: workflowActionInput.fieldsToUpdate,
|
||||
workspaceId,
|
||||
roleId: executionContext.roleId,
|
||||
rolePermissionConfig: executionContext.rolePermissionConfig,
|
||||
});
|
||||
|
||||
if (!toolOutput.success) {
|
||||
|
||||
+17
-13
@@ -8,6 +8,7 @@ import type { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/wor
|
||||
import type { UpdateWorkflowVersionPositionsInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-version-positions-input.dto';
|
||||
import type { UpdateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-version-step-input.dto';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
import { WorkflowVersionStatus } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
||||
import { WorkflowStatus } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
|
||||
import { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service';
|
||||
@@ -43,7 +44,10 @@ export class WorkflowToolWorkspaceService {
|
||||
private readonly recordPositionService: RecordPositionService,
|
||||
) {}
|
||||
|
||||
generateWorkflowTools(workspaceId: string, roleId: string): ToolSet {
|
||||
generateWorkflowTools(
|
||||
workspaceId: string,
|
||||
rolePermissionConfig: RolePermissionConfig,
|
||||
): ToolSet {
|
||||
const tools: ToolSet = {};
|
||||
|
||||
tools.create_complete_workflow = {
|
||||
@@ -88,7 +92,7 @@ This is the most efficient way for AI to create workflows as it handles all the
|
||||
const workflowId = await this.createWorkflow({
|
||||
workspaceId,
|
||||
name: parameters.name,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
});
|
||||
|
||||
const workflowVersionId = await this.createWorkflowVersion({
|
||||
@@ -96,7 +100,7 @@ This is the most efficient way for AI to create workflows as it handles all the
|
||||
workflowId,
|
||||
trigger: parameters.trigger,
|
||||
steps: parameters.steps,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
});
|
||||
|
||||
if (parameters.stepPositions && parameters.stepPositions.length > 0) {
|
||||
@@ -132,7 +136,7 @@ This is the most efficient way for AI to create workflows as it handles all the
|
||||
workspaceId,
|
||||
workflowId,
|
||||
workflowVersionId,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -400,17 +404,17 @@ This is the most efficient way for AI to create workflows as it handles all the
|
||||
private async createWorkflow({
|
||||
workspaceId,
|
||||
name,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
name: string;
|
||||
roleId: string;
|
||||
rolePermissionConfig: RolePermissionConfig;
|
||||
}): Promise<string> {
|
||||
const workflowRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'workflow',
|
||||
{ roleId },
|
||||
rolePermissionConfig,
|
||||
);
|
||||
|
||||
const workflowPosition =
|
||||
@@ -440,19 +444,19 @@ This is the most efficient way for AI to create workflows as it handles all the
|
||||
workflowId,
|
||||
trigger,
|
||||
steps,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
workflowId: string;
|
||||
trigger: WorkflowTrigger;
|
||||
steps: WorkflowAction[];
|
||||
roleId: string;
|
||||
rolePermissionConfig: RolePermissionConfig;
|
||||
}): Promise<string> {
|
||||
const workflowVersionRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'workflowVersion',
|
||||
{ roleId },
|
||||
rolePermissionConfig,
|
||||
);
|
||||
|
||||
const versionPosition =
|
||||
@@ -484,18 +488,18 @@ This is the most efficient way for AI to create workflows as it handles all the
|
||||
workspaceId,
|
||||
workflowId,
|
||||
workflowVersionId,
|
||||
roleId,
|
||||
rolePermissionConfig,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
workflowId: string;
|
||||
workflowVersionId: string;
|
||||
roleId: string;
|
||||
rolePermissionConfig: RolePermissionConfig;
|
||||
}) {
|
||||
const workflowRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'workflow',
|
||||
{ roleId },
|
||||
rolePermissionConfig,
|
||||
);
|
||||
|
||||
await workflowRepository.update(workflowId, {
|
||||
|
||||
+12
-33
@@ -22,11 +22,8 @@ describe('AgentToolGeneratorService Integration', () => {
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(context.agentService, 'findOneAgent')
|
||||
.mockResolvedValue(context.testAgent as any);
|
||||
jest
|
||||
.spyOn(context.roleRepository, 'findOne')
|
||||
.mockResolvedValue(roleWithFullPermissions);
|
||||
.spyOn(context.roleRepository, 'find')
|
||||
.mockResolvedValue([roleWithFullPermissions]);
|
||||
jest
|
||||
.spyOn(
|
||||
context.workspacePermissionsCacheService,
|
||||
@@ -36,10 +33,6 @@ describe('AgentToolGeneratorService Integration', () => {
|
||||
data: {
|
||||
[context.testRoleId]: {
|
||||
[context.testObjectMetadata.id]: {
|
||||
canRead: true,
|
||||
canUpdate: true,
|
||||
canSoftDelete: true,
|
||||
canDestroy: true,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
@@ -57,6 +50,8 @@ describe('AgentToolGeneratorService Integration', () => {
|
||||
const tools = await context.agentToolService.generateToolsForAgent(
|
||||
context.testAgentId,
|
||||
context.testWorkspaceId,
|
||||
undefined,
|
||||
[context.testRoleId],
|
||||
);
|
||||
|
||||
expect(tools).toBeDefined();
|
||||
@@ -72,11 +67,8 @@ describe('AgentToolGeneratorService Integration', () => {
|
||||
|
||||
it('should generate read-only tools for agent with read permissions only', async () => {
|
||||
jest
|
||||
.spyOn(context.agentService, 'findOneAgent')
|
||||
.mockResolvedValue(context.testAgent as any);
|
||||
jest
|
||||
.spyOn(context.roleRepository, 'findOne')
|
||||
.mockResolvedValue(context.testRole);
|
||||
.spyOn(context.roleRepository, 'find')
|
||||
.mockResolvedValue([context.testRole]);
|
||||
jest
|
||||
.spyOn(
|
||||
context.workspacePermissionsCacheService,
|
||||
@@ -86,10 +78,6 @@ describe('AgentToolGeneratorService Integration', () => {
|
||||
data: {
|
||||
[context.testRoleId]: {
|
||||
[context.testObjectMetadata.id]: {
|
||||
canRead: true,
|
||||
canUpdate: false,
|
||||
canSoftDelete: false,
|
||||
canDestroy: false,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: false,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
@@ -107,6 +95,8 @@ describe('AgentToolGeneratorService Integration', () => {
|
||||
const tools = await context.agentToolService.generateToolsForAgent(
|
||||
context.testAgentId,
|
||||
context.testWorkspaceId,
|
||||
undefined,
|
||||
[context.testRoleId],
|
||||
);
|
||||
|
||||
expect(tools).toBeDefined();
|
||||
@@ -118,12 +108,6 @@ describe('AgentToolGeneratorService Integration', () => {
|
||||
});
|
||||
|
||||
it('should return only http request tool for agent without role', async () => {
|
||||
const agentWithoutRole = { ...context.testAgent, roleId: null };
|
||||
|
||||
jest
|
||||
.spyOn(context.agentService, 'findOneAgent')
|
||||
.mockResolvedValue(agentWithoutRole as any);
|
||||
|
||||
const tools = await context.agentToolService.generateToolsForAgent(
|
||||
context.testAgentId,
|
||||
context.testWorkspaceId,
|
||||
@@ -141,11 +125,8 @@ describe('AgentToolGeneratorService Integration', () => {
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(context.agentService, 'findOneAgent')
|
||||
.mockResolvedValue(context.testAgent as any);
|
||||
jest
|
||||
.spyOn(context.roleRepository, 'findOne')
|
||||
.mockResolvedValue(context.testRole);
|
||||
.spyOn(context.roleRepository, 'find')
|
||||
.mockResolvedValue([context.testRole]);
|
||||
jest
|
||||
.spyOn(
|
||||
context.workspacePermissionsCacheService,
|
||||
@@ -155,10 +136,6 @@ describe('AgentToolGeneratorService Integration', () => {
|
||||
data: {
|
||||
[context.testRoleId]: {
|
||||
[workflowObject.id]: {
|
||||
canRead: true,
|
||||
canUpdate: true,
|
||||
canSoftDelete: true,
|
||||
canDestroy: false,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
@@ -176,6 +153,8 @@ describe('AgentToolGeneratorService Integration', () => {
|
||||
const tools = await context.agentToolService.generateToolsForAgent(
|
||||
context.testAgentId,
|
||||
context.testWorkspaceId,
|
||||
undefined,
|
||||
[context.testRoleId],
|
||||
);
|
||||
|
||||
expect(Object.keys(tools)).toHaveLength(7);
|
||||
|
||||
+2
-4
@@ -65,6 +65,7 @@ export const createAgentToolTestModule =
|
||||
provide: getRepositoryToken(RoleEntity),
|
||||
useValue: {
|
||||
findOne: jest.fn(),
|
||||
find: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -161,6 +162,7 @@ export const createAgentToolTestModule =
|
||||
useValue: {
|
||||
hasToolPermission: jest.fn(),
|
||||
checkRolePermissions: jest.fn().mockReturnValue(true),
|
||||
checkRolesPermissions: jest.fn().mockResolvedValue(true),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -341,10 +343,6 @@ export const setupBasicPermissions = (context: AgentToolTestContext) => {
|
||||
data: {
|
||||
[context.testRoleId]: {
|
||||
[context.testObjectMetadata.id]: {
|
||||
canRead: true,
|
||||
canUpdate: true,
|
||||
canSoftDelete: true,
|
||||
canDestroy: false,
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { type RestrictedFieldsPermissions } from './RestrictedFieldsPermissions';
|
||||
|
||||
// TODO: DEPRECATE THIS
|
||||
export type ObjectPermissionsDeprecated = {
|
||||
canRead: boolean;
|
||||
canUpdate: boolean;
|
||||
canSoftDelete: boolean;
|
||||
canDestroy: boolean;
|
||||
restrictedFields: RestrictedFieldsPermissions;
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
import { type ObjectsPermissions } from '@/types/ObjectsPermissions';
|
||||
import { type ObjectsPermissionsDeprecated } from './ObjectsPermissionsDeprecated';
|
||||
|
||||
type RoleId = string;
|
||||
|
||||
// TODO: DEPRECATE THIS
|
||||
export type ObjectsPermissionsByRoleIdDeprecated = Record<
|
||||
RoleId,
|
||||
ObjectsPermissionsDeprecated & ObjectsPermissions
|
||||
>;
|
||||
@@ -1,9 +0,0 @@
|
||||
import { type ObjectPermissionsDeprecated } from './ObjectPermissionsDeprecated';
|
||||
|
||||
type ObjectMetadataId = string;
|
||||
|
||||
// TODO: DEPRECATE THIS
|
||||
export type ObjectsPermissionsDeprecated = Record<
|
||||
ObjectMetadataId,
|
||||
ObjectPermissionsDeprecated
|
||||
>;
|
||||
@@ -34,7 +34,6 @@ export type { NonNullableRequired } from './NonNullableRequired';
|
||||
export type { Nullable } from './Nullable';
|
||||
export type { NullablePartial } from './NullablePartial';
|
||||
export type { ObjectPermissions } from './ObjectPermissions';
|
||||
export type { ObjectPermissionsDeprecated } from './ObjectPermissionsDeprecated';
|
||||
export type { ObjectRecord } from './ObjectRecord';
|
||||
export type {
|
||||
AggregateOrderByWithGroupByField,
|
||||
@@ -47,8 +46,6 @@ export { OrderByDirection } from './ObjectRecordGroupBy';
|
||||
export { ObjectRecordGroupByDateGranularity } from './ObjectRecordGroupByDateGranularity';
|
||||
export type { ObjectsPermissions } from './ObjectsPermissions';
|
||||
export type { ObjectsPermissionsByRoleId } from './ObjectsPermissionsByRoleId';
|
||||
export type { ObjectsPermissionsByRoleIdDeprecated } from './ObjectsPermissionsByRoleIdDeprecated';
|
||||
export type { ObjectsPermissionsDeprecated } from './ObjectsPermissionsDeprecated';
|
||||
export type { PartialFieldMetadataItem } from './PartialFieldMetadataItem';
|
||||
export type { PartialFieldMetadataItemOption } from './PartialFieldMetadataOption';
|
||||
export { RecordFilterGroupLogicalOperator } from './RecordFilterGroupLogicalOperator';
|
||||
|
||||
Reference in New Issue
Block a user