diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts index 5236f4f2a..e1dfd4e90 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts @@ -101,6 +101,7 @@ export abstract class GraphqlQueryBaseResolverService< } else { if (!isPermissionsV2Enabled) await this.validateObjectRecordPermissionsOrThrow({ + objectMetadataId: objectMetadataItemWithFieldMaps.id, operationName, options, }); @@ -219,9 +220,11 @@ export abstract class GraphqlQueryBaseResolverService< } private async validateObjectRecordPermissionsOrThrow({ + objectMetadataId, operationName, options, }: { + objectMetadataId: string; operationName: WorkspaceResolverBuilderMethodNames; options: WorkspaceQueryRunnerOptions; }) { @@ -234,6 +237,7 @@ export abstract class GraphqlQueryBaseResolverService< requiredPermission, workspaceId: options.authContext.workspace.id, isExecutedByApiKey: isDefined(options.authContext.apiKey), + objectMetadataId, }); if (!userHasPermission) { diff --git a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts index ee828db79..c17583473 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts @@ -226,11 +226,13 @@ export class PermissionsService { workspaceId, requiredPermission, isExecutedByApiKey, + objectMetadataId, }: { userWorkspaceId?: string; workspaceId: string; requiredPermission: PermissionsOnAllObjectRecords; isExecutedByApiKey: boolean; + objectMetadataId: string; }): Promise { const isPermissionsV2Enabled = await this.featureFlagService.isFeatureEnabled( @@ -279,11 +281,10 @@ export class PermissionsService { const objectPermissionKey = this.getObjectPermissionKeyForRequiredPermission(requiredPermission); - // until permissions V2 is enabled all objects have the same permission values deriving from role, ex role.canReadAllObjectRecords const objectPermissionValue = - rolePermissionsForUserWorkspaceRole[ - Object.keys(rolePermissionsForUserWorkspaceRole)[0] - ]?.[objectPermissionKey]; + rolePermissionsForUserWorkspaceRole[objectMetadataId]?.[ + objectPermissionKey + ]; return objectPermissionValue === true; } diff --git a/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/granular-object-records-permissions.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/granular-object-records-permissions.integration-spec.ts new file mode 100644 index 000000000..0551ac679 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/granular-object-records-permissions.integration-spec.ts @@ -0,0 +1,196 @@ +import { default as request } from 'supertest'; +import { createCustomRoleWithObjectPermissions } from 'test/integration/graphql/utils/create-custom-role-with-object-permissions.util'; +import { deleteRole } from 'test/integration/graphql/utils/delete-one-role.util'; +import { findOneOperationFactory } from 'test/integration/graphql/utils/find-one-operation-factory.util'; +import { makeGraphqlAPIRequestWithMemberRole as makeGraphqlAPIRequestWithJony } from 'test/integration/graphql/utils/make-graphql-api-request-with-member-role.util'; +import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; +import { updateFeatureFlagFactory } from 'test/integration/graphql/utils/update-feature-flag-factory.util'; +import { updateWorkspaceMemberRole } from 'test/integration/graphql/utils/update-workspace-member-role.util'; + +import { SEED_APPLE_WORKSPACE_ID } from 'src/database/typeorm-seeds/core/workspaces'; +import { DEV_SEED_WORKSPACE_MEMBER_IDS } from 'src/database/typeorm-seeds/workspace/workspace-members'; +import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('granularObjectRecordsPermissions', () => { + describe('permissions V2 enabled', () => { + let originalMemberRoleId: string; + let customRoleId: string; + + beforeAll(async () => { + // Enable Permissions V2 + const enablePermissionsQuery = updateFeatureFlagFactory( + SEED_APPLE_WORKSPACE_ID, + 'IS_PERMISSIONS_V2_ENABLED', + true, + ); + + await makeGraphqlAPIRequest(enablePermissionsQuery); + + // Get the original Member role ID for restoration later + const getRolesQuery = { + query: ` + query GetRoles { + getRoles { + id + label + } + } + `, + }; + + const rolesResponse = await client + .post('/graphql') + .set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`) + .send(getRolesQuery); + + originalMemberRoleId = rolesResponse.body.data.getRoles.find( + (role: any) => role.label === 'Member', + ).id; + }); + + afterAll(async () => { + const restoreMemberRoleQuery = { + query: ` + mutation UpdateWorkspaceMemberRole { + updateWorkspaceMemberRole( + workspaceMemberId: "${DEV_SEED_WORKSPACE_MEMBER_IDS.JONY}" + roleId: "${originalMemberRoleId}" + ) { + id + } + } + `, + }; + + await client + .post('/graphql') + .set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`) + .send(restoreMemberRoleQuery); + + // Disable Permissions V2 + const disablePermissionsQuery = updateFeatureFlagFactory( + SEED_APPLE_WORKSPACE_ID, + 'IS_PERMISSIONS_V2_ENABLED', + false, + ); + + await makeGraphqlAPIRequest(disablePermissionsQuery); + }); + + afterEach(async () => { + await deleteRole(client, customRoleId); + }); + + it('should throw permission error when querying person while person reading rights are overriden to false', async () => { + // Arrange + const { roleId } = await createCustomRoleWithObjectPermissions({ + label: 'PersonReadRightsExcludedRole', + canReadPerson: false, + }); + + customRoleId = roleId; + + await updateWorkspaceMemberRole({ + client, + roleId: customRoleId, + workspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.JONY, + }); + + // Act + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'person', + gqlFields: ` + id + city + jobTitle + `, + filter: { city: { eq: 'Seattle' } }, + }); + + const companyGraphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'company', + gqlFields: ` + id + name + `, + filter: { name: { eq: 'Apple' } }, + }); + + const personResponse = + await makeGraphqlAPIRequestWithJony(graphqlOperation); + + const companyResponse = await makeGraphqlAPIRequestWithJony( + companyGraphqlOperation, + ); + + // Assert + expect(personResponse.body.errors).toBeDefined(); + expect(personResponse.body.errors[0].message).toBe( + PermissionsExceptionMessage.PERMISSION_DENIED, + ); + expect(personResponse.body.errors[0].extensions.code).toBe( + ErrorCode.FORBIDDEN, + ); + expect(companyResponse.body.data).toBeDefined(); + expect(companyResponse.body.data.company).toBeDefined(); + }); + + it('should successfully query person when person reading rights are overriden to true', async () => { + // Arrange + const { roleId } = await createCustomRoleWithObjectPermissions({ + label: 'PersonRole', + canReadPerson: true, + hasAllObjectRecordsReadPermission: false, + }); + + customRoleId = roleId; + + await updateWorkspaceMemberRole({ + client, + roleId: customRoleId, + workspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.JONY, + }); + + // Act + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'person', + gqlFields: ` + id + city + jobTitle + `, + filter: { city: { eq: 'Seattle' } }, + }); + + const companyGraphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'company', + gqlFields: ` + id + name + `, + filter: { name: { eq: 'Apple' } }, + }); + + const personResponse = + await makeGraphqlAPIRequestWithJony(graphqlOperation); + + const companyResponse = await makeGraphqlAPIRequestWithJony( + companyGraphqlOperation, + ); + + // Assert + expect(personResponse.body.data).toBeDefined(); + expect(personResponse.body.data.person).toBeDefined(); + expect(companyResponse.body.errors).toBeDefined(); + expect(companyResponse.body.errors[0].message).toBe( + PermissionsExceptionMessage.PERMISSION_DENIED, + ); + expect(companyResponse.body.errors[0].extensions.code).toBe( + ErrorCode.FORBIDDEN, + ); + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/permissions-on-relations.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/permissions-on-relations.integration-spec.ts new file mode 100644 index 000000000..f7b4dc46d --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/permissions-on-relations.integration-spec.ts @@ -0,0 +1,252 @@ +import { randomUUID } from 'crypto'; + +import { default as request } from 'supertest'; +import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.constants'; +import { createCustomRoleWithObjectPermissions } from 'test/integration/graphql/utils/create-custom-role-with-object-permissions.util'; +import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util'; +import { deleteRole } from 'test/integration/graphql/utils/delete-one-role.util'; +import { findManyOperationFactory } from 'test/integration/graphql/utils/find-many-operation-factory.util'; +import { makeGraphqlAPIRequestWithMemberRole as makeGraphqlAPIRequestWithJony } from 'test/integration/graphql/utils/make-graphql-api-request-with-member-role.util'; +import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; +import { updateFeatureFlagFactory } from 'test/integration/graphql/utils/update-feature-flag-factory.util'; +import { updateWorkspaceMemberRole } from 'test/integration/graphql/utils/update-workspace-member-role.util'; + +import { SEED_APPLE_WORKSPACE_ID } from 'src/database/typeorm-seeds/core/workspaces'; +import { DEV_SEED_WORKSPACE_MEMBER_IDS } from 'src/database/typeorm-seeds/workspace/workspace-members'; +import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('permissionsOnRelations', () => { + describe('permissions V2 enabled', () => { + let originalMemberRoleId: string; + let customRoleId: string; + + beforeAll(async () => { + // Enable Permissions V2 + const enablePermissionsQuery = updateFeatureFlagFactory( + SEED_APPLE_WORKSPACE_ID, + 'IS_PERMISSIONS_V2_ENABLED', + true, + ); + + await makeGraphqlAPIRequest(enablePermissionsQuery); + + // Get the original Member role ID for restoration later + const getRolesQuery = { + query: ` + query GetRoles { + getRoles { + id + label + } + } + `, + }; + + const rolesResponse = await client + .post('/graphql') + .set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`) + .send(getRolesQuery); + + originalMemberRoleId = rolesResponse.body.data.getRoles.find( + (role: any) => role.label === 'Member', + ).id; + + // Create a person record + const companyId = randomUUID(); + const graphqlOperationForCompanyCreation = createOneOperationFactory({ + objectMetadataSingularName: 'company', + gqlFields: ` + name + `, + data: { + id: companyId, + name: 'Twenty', + }, + }); + + await makeGraphqlAPIRequest(graphqlOperationForCompanyCreation); + + const graphqlOperationForPersonCreation = createOneOperationFactory({ + objectMetadataSingularName: 'person', + gqlFields: PERSON_GQL_FIELDS, + data: { + id: randomUUID(), + name: { + firstName: 'Marie', + }, + city: 'Paris', + companyId, + }, + }); + + await makeGraphqlAPIRequest(graphqlOperationForPersonCreation); + }); + + afterAll(async () => { + const restoreMemberRoleQuery = { + query: ` + mutation UpdateWorkspaceMemberRole { + updateWorkspaceMemberRole( + workspaceMemberId: "${DEV_SEED_WORKSPACE_MEMBER_IDS.JONY}" + roleId: "${originalMemberRoleId}" + ) { + id + } + } + `, + }; + + await client + .post('/graphql') + .set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`) + .send(restoreMemberRoleQuery); + + // Disable Permissions V2 + const disablePermissionsQuery = updateFeatureFlagFactory( + SEED_APPLE_WORKSPACE_ID, + 'IS_PERMISSIONS_V2_ENABLED', + false, + ); + + await makeGraphqlAPIRequest(disablePermissionsQuery); + }); + + afterEach(async () => { + await deleteRole(client, customRoleId); + }); + + it('should throw permission error when querying person with company relation without company read permission', async () => { + // Create a role with person read permission but no company read permission + const { roleId } = await createCustomRoleWithObjectPermissions({ + label: 'PersonOnlyRole', + canReadPerson: true, + canReadCompany: false, + }); + + customRoleId = roleId; + + await updateWorkspaceMemberRole({ + client, + roleId: customRoleId, + workspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.JONY, + }); + + // Create GraphQL query that includes company relation + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'person', + objectMetadataPluralName: 'people', + gqlFields: ` + id + city + jobTitle + company { + id + name + } + `, + }); + + const response = await makeGraphqlAPIRequestWithJony(graphqlOperation); + + // The query should fail when trying to access company relation without permission + expect(response.body.errors[0].message).toBe( + PermissionsExceptionMessage.PERMISSION_DENIED, + ); + expect(response.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN); + }); + + it('should successfully query person with company relation when having both permissions', async () => { + // Create a role with both person and company read permissions + const { roleId } = await createCustomRoleWithObjectPermissions({ + label: 'PersonAndCompanyRole', + canReadPerson: true, + canReadCompany: true, + }); + + customRoleId = roleId; + + await updateWorkspaceMemberRole({ + client, + roleId: customRoleId, + workspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.JONY, + }); + + // Create GraphQL query that includes company relation + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'person', + objectMetadataPluralName: 'people', + gqlFields: ` + id + city + jobTitle + company { + id + name + } + `, + }); + + const response = await makeGraphqlAPIRequestWithJony(graphqlOperation); + + // The query should succeed + expect(response.body.data).toBeDefined(); + expect(response.body.data.people).toBeDefined(); + const person = response.body.data.people.edges[0].node; + + expect(person.company).toBeDefined(); + expect(response.body.error).toBeUndefined(); + }); + + it('nested relations - should throw permission error when querying nested opportunity relation without opportunity read permission', async () => { + // Where user has person and company read permissions but not opportunity read permission + + const { roleId } = await createCustomRoleWithObjectPermissions({ + label: 'PersonCompanyOnlyRole', + canReadPerson: true, + canReadCompany: true, + canReadOpportunities: false, + }); + + customRoleId = roleId; + + await updateWorkspaceMemberRole({ + client, + roleId: customRoleId, + workspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.JONY, + }); + + // Create a query with nested relations + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'person', + objectMetadataPluralName: 'people', + gqlFields: ` + id + city + jobTitle + company { + id + name + opportunities { + edges { + node { + name + } + } + } + } + `, + }); + + const response = await makeGraphqlAPIRequestWithJony(graphqlOperation); + + expect(response.body.errors).toBeDefined(); + expect(response.body.errors[0].message).toBe( + PermissionsExceptionMessage.PERMISSION_DENIED, + ); + expect(response.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN); + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/settings-permissions/granular-settings-permissions.ts b/packages/twenty-server/test/integration/graphql/suites/settings-permissions/granular-settings-permissions.ts index 33ae79e69..a1e3646e2 100644 --- a/packages/twenty-server/test/integration/graphql/suites/settings-permissions/granular-settings-permissions.ts +++ b/packages/twenty-server/test/integration/graphql/suites/settings-permissions/granular-settings-permissions.ts @@ -3,6 +3,7 @@ import request from 'supertest'; import { deleteOneRoleOperationFactory } from 'test/integration/graphql/utils/delete-one-role-operation-factory.util'; import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; import { updateFeatureFlagFactory } from 'test/integration/graphql/utils/update-feature-flag-factory.util'; +import { updateWorkspaceMemberRole } from 'test/integration/graphql/utils/update-workspace-member-role.util'; import { createOneObjectMetadataQueryFactory } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata-query-factory.util'; import { deleteOneObjectMetadataQueryFactory } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata-query-factory.util'; @@ -99,27 +100,11 @@ describe('Granular settings permissions', () => { .send(upsertSettingPermissionsQuery); // Assign the custom role to JONY (who uses MEMBER_ACCESS_TOKEN) - const updateMemberRoleQuery = { - query: ` - mutation UpdateWorkspaceMemberRole { - updateWorkspaceMemberRole( - workspaceMemberId: "${DEV_SEED_WORKSPACE_MEMBER_IDS.JONY}" - roleId: "${customRoleId}" - ) { - id - roles { - id - label - } - } - } - `, - }; - - await client - .post('/graphql') - .set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`) - .send(updateMemberRoleQuery); + await updateWorkspaceMemberRole({ + client, + roleId: customRoleId, + workspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.JONY, + }); }); afterAll(async () => { diff --git a/packages/twenty-server/test/integration/graphql/utils/create-custom-role-with-object-permissions.util.ts b/packages/twenty-server/test/integration/graphql/utils/create-custom-role-with-object-permissions.util.ts new file mode 100644 index 000000000..55abd71f3 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/utils/create-custom-role-with-object-permissions.util.ts @@ -0,0 +1,131 @@ +import gql from 'graphql-tag'; +import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; + +export const createCustomRoleWithObjectPermissions = async (options: { + label: string; + canReadPerson?: boolean; + canReadCompany?: boolean; + canReadOpportunities?: boolean; + hasAllObjectRecordsReadPermission?: boolean; +}) => { + const createRoleOperation = { + query: gql` + mutation CreateOneRole { + createOneRole(createRoleInput: { + label: "${options.label}" + description: "Test role for permission testing" + canUpdateAllSettings: ${options.hasAllObjectRecordsReadPermission ?? true} + canReadAllObjectRecords: ${options.hasAllObjectRecordsReadPermission ?? true} + canUpdateAllObjectRecords: ${options.hasAllObjectRecordsReadPermission ?? true} + canSoftDeleteAllObjectRecords: ${options.hasAllObjectRecordsReadPermission ?? true} + canDestroyAllObjectRecords: ${options.hasAllObjectRecordsReadPermission ?? true} + }) { + id + label + } + } + `, + }; + + const response = await makeGraphqlAPIRequest(createRoleOperation); + const roleId = response.body.data.createOneRole.id; + + // Get object metadata IDs for Person and Company + const getObjectMetadataOperation = { + query: gql` + query { + objects(paging: { first: 1000 }) { + edges { + node { + id + nameSingular + } + } + } + } + `, + }; + + const objectMetadataResponse = await makeMetadataAPIRequest( + getObjectMetadataOperation, + ); + const objects = objectMetadataResponse.body.data.objects.edges; + + const personObjectId = objects.find( + (obj: any) => obj.node.nameSingular === 'person', + )?.node.id; + const companyObjectId = objects.find( + (obj: any) => obj.node.nameSingular === 'company', + )?.node.id; + const opportunityObjectId = objects.find( + (obj: any) => obj.node.nameSingular === 'opportunity', + )?.node.id; + + // Create object permissions based on the options + const objectPermissions = []; + + if (options.canReadPerson !== undefined) { + objectPermissions.push({ + objectMetadataId: personObjectId, + canReadObjectRecords: options.canReadPerson, + canUpdateObjectRecords: false, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: false, + }); + } + + if (options.canReadCompany !== undefined) { + objectPermissions.push({ + objectMetadataId: companyObjectId, + canReadObjectRecords: options.canReadCompany, + canUpdateObjectRecords: false, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: false, + }); + } + + if (options.canReadOpportunities !== undefined) { + objectPermissions.push({ + objectMetadataId: opportunityObjectId, + canReadObjectRecords: options.canReadOpportunities, + canUpdateObjectRecords: false, + canSoftDeleteObjectRecords: false, + canDestroyObjectRecords: false, + }); + } + + if (objectPermissions.length > 0) { + const upsertObjectPermissionsOperation = { + query: gql` + mutation UpsertObjectPermissions( + $roleId: String! + $objectPermissions: [ObjectPermissionInput!]! + ) { + upsertObjectPermissions( + upsertObjectPermissionsInput: { + roleId: $roleId + objectPermissions: $objectPermissions + } + ) { + objectMetadataId + canReadObjectRecords + } + } + `, + variables: { + roleId, + objectPermissions, + }, + }; + + await makeGraphqlAPIRequest(upsertObjectPermissionsOperation); + } + + return { + roleId, + personObjectId, + companyObjectId, + opportunityObjectId, + }; +}; diff --git a/packages/twenty-server/test/integration/graphql/utils/delete-one-role.util.ts b/packages/twenty-server/test/integration/graphql/utils/delete-one-role.util.ts new file mode 100644 index 000000000..2dbd4c844 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/utils/delete-one-role.util.ts @@ -0,0 +1,10 @@ +import { deleteOneRoleOperationFactory } from 'test/integration/graphql/utils/delete-one-role-operation-factory.util'; + +export const deleteRole = async (client: any, roleId: string) => { + const deleteRoleQuery = deleteOneRoleOperationFactory(roleId); + + await client + .post('/graphql') + .set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`) + .send(deleteRoleQuery); +}; diff --git a/packages/twenty-server/test/integration/graphql/utils/update-workspace-member-role.util.ts b/packages/twenty-server/test/integration/graphql/utils/update-workspace-member-role.util.ts new file mode 100644 index 000000000..aa1652c1d --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/utils/update-workspace-member-role.util.ts @@ -0,0 +1,31 @@ +export const updateWorkspaceMemberRole = async ({ + client, + roleId, + workspaceMemberId, +}: { + client: any; + roleId: string; + workspaceMemberId: string; +}) => { + const updateMemberRoleQuery = { + query: ` + mutation UpdateWorkspaceMemberRole { + updateWorkspaceMemberRole( + workspaceMemberId: "${workspaceMemberId}" + roleId: "${roleId}" + ) { + id + roles { + id + label + } + } + } + `, + }; + + await client + .post('/graphql') + .set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`) + .send(updateMemberRoleQuery); +};