Compare commits

...

4 Commits

Author SHA1 Message Date
Charles Bochet aa8113a1f0 Merge branch 'main' into c--improve-permissions-and-RLS-coverage 2026-02-18 22:21:32 +01:00
Charles Bochet 80f87300f0 Merge branch 'main' into c--improve-permissions-and-RLS-coverage 2026-02-09 22:30:00 +01:00
Félix Malfait e26f398f34 Merge branch 'main' into c--improve-permissions-and-RLS-coverage 2026-02-05 14:12:38 +01:00
Weiko e77de316e5 Improve permissions and RLS coverage 2026-02-05 13:34:39 +01:00
4 changed files with 793 additions and 0 deletions
@@ -0,0 +1,60 @@
import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.constants';
import { findManyOperationFactory } from 'test/integration/graphql/utils/find-many-operation-factory.util';
import { makeGraphqlAPIRequestWithApiKey } from 'test/integration/graphql/utils/make-graphql-api-request-with-api-key.util';
import { makeGraphqlAPIRequestWithGuestRole } from 'test/integration/graphql/utils/make-graphql-api-request-with-guest-role.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
describe('findManyObjectRecordsPermissions', () => {
it('should throw a permission error when user does not have permission (guest role)', async () => {
const graphqlOperation = findManyOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
gqlFields: PERSON_GQL_FIELDS,
first: 10,
});
const response = await makeGraphqlAPIRequestWithGuestRole(graphqlOperation);
expect(response.body.data).toStrictEqual({ people: null });
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toBe(
PermissionsExceptionMessage.PERMISSION_DENIED,
);
expect(response.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
});
it('should read object records when user has permission (admin role)', async () => {
const graphqlOperation = findManyOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
gqlFields: PERSON_GQL_FIELDS,
first: 10,
});
const response = await makeGraphqlAPIRequest(graphqlOperation);
expect(response.body.data).toBeDefined();
expect(response.body.data.people).toBeDefined();
expect(response.body.data.people.edges).toBeDefined();
expect(Array.isArray(response.body.data.people.edges)).toBe(true);
});
it('should read object records when executed by api key', async () => {
const graphqlOperation = findManyOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
gqlFields: PERSON_GQL_FIELDS,
first: 10,
});
const response = await makeGraphqlAPIRequestWithApiKey(graphqlOperation);
expect(response.body.data).toBeDefined();
expect(response.body.data.people).toBeDefined();
expect(response.body.data.people.edges).toBeDefined();
expect(Array.isArray(response.body.data.people.edges)).toBe(true);
});
});
@@ -0,0 +1,55 @@
import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.constants';
import { findOneOperationFactory } from 'test/integration/graphql/utils/find-one-operation-factory.util';
import { makeGraphqlAPIRequestWithApiKey } from 'test/integration/graphql/utils/make-graphql-api-request-with-api-key.util';
import { makeGraphqlAPIRequestWithGuestRole } from 'test/integration/graphql/utils/make-graphql-api-request-with-guest-role.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
describe('findOneObjectRecordsPermissions', () => {
it('should throw a permission error when user does not have permission (guest role)', async () => {
const graphqlOperation = findOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
filter: { id: { eq: '777a8457-eb2d-40ac-a707-551b615b6980' } },
});
const response = await makeGraphqlAPIRequestWithGuestRole(graphqlOperation);
expect(response.body.data).toStrictEqual({ person: null });
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toBe(
PermissionsExceptionMessage.PERMISSION_DENIED,
);
expect(response.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
});
it('should read an object record when user has permission (admin role)', async () => {
const graphqlOperation = findOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
filter: { city: { eq: 'Seattle' } },
});
const response = await makeGraphqlAPIRequest(graphqlOperation);
expect(response.body.data).toBeDefined();
expect(response.body.data.person).toBeDefined();
expect(response.body.data.person.city).toBe('Seattle');
});
it('should read an object record when executed by api key', async () => {
const graphqlOperation = findOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
filter: { city: { eq: 'Seattle' } },
});
const response = await makeGraphqlAPIRequestWithApiKey(graphqlOperation);
expect(response.body.data).toBeDefined();
expect(response.body.data.person).toBeDefined();
expect(response.body.data.person.city).toBe('Seattle');
});
});
@@ -0,0 +1,460 @@
import { randomUUID } from 'node:crypto';
import { default as request } from 'supertest';
import { COMPANY_GQL_FIELDS } from 'test/integration/constants/company-gql-fields.constants';
import { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-operation-factory.util';
import { deleteOneOperationFactory } from 'test/integration/graphql/utils/delete-one-operation-factory.util';
import { findManyOperationFactory } from 'test/integration/graphql/utils/find-many-operation-factory.util';
import { findOneOperationFactory } from 'test/integration/graphql/utils/find-one-operation-factory.util';
import { makeGraphqlAPIRequestWithMemberRole } from 'test/integration/graphql/utils/make-graphql-api-request-with-member-role.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { searchFactory } from 'test/integration/graphql/utils/search-factory.util';
import { updateOneOperationFactory } from 'test/integration/graphql/utils/update-one-operation-factory.util';
import { updateWorkspaceMemberRole } from 'test/integration/graphql/utils/update-workspace-member-role.util';
import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util';
import { createOneRole } from 'test/integration/metadata/suites/role/utils/create-one-role.util';
import { deleteOneRole } from 'test/integration/metadata/suites/role/utils/delete-one-role.util';
import { upsertRowLevelPermissionPredicates } from 'test/integration/metadata/suites/row-level-permission-predicate/utils/upsert-row-level-permission-predicates.util';
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
import { deleteRecordsByIds } from 'test/integration/utils/delete-records-by-ids';
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
import { RowLevelPermissionPredicateOperand } from 'twenty-shared/types';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { WORKSPACE_MEMBER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
const client = request(`http://localhost:${APP_PORT}`);
const TEST_COMPANY_ACME_CORP_ID = randomUUID();
const TEST_COMPANY_BETA_INC_ID = randomUUID();
const TEST_COMPANY_ACME_LABS_ID = randomUUID();
describe('RLS predicate enforcement', () => {
let companyObjectMetadataId: string;
let companyNameFieldMetadataId: string;
let createdRoleId: string;
let originalMemberRoleId: string;
beforeAll(async () => {
// Enable RLS feature flag
await updateFeatureFlag({
featureFlag: FeatureFlagKey.IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED,
value: true,
expectToFail: false,
});
// Get object metadata for company
const { objects } = await findManyObjectMetadata({
expectToFail: false,
input: {
filter: {},
paging: { first: 1000 },
},
gqlFields: `
id
nameSingular
fieldsList {
id
name
}
`,
});
jestExpectToBeDefined(objects);
const companyObjectMetadata = objects.find(
(object: { nameSingular: string }) => object.nameSingular === 'company',
);
jestExpectToBeDefined(companyObjectMetadata);
companyObjectMetadataId = companyObjectMetadata.id;
jestExpectToBeDefined(companyObjectMetadata.fieldsList);
const nameField = companyObjectMetadata.fieldsList.find(
(field: { name: string }) => field.name === 'name',
);
jestExpectToBeDefined(nameField);
companyNameFieldMetadataId = nameField.id;
// Get original member role ID
const getRolesQuery = {
query: `
query GetRoles {
getRoles {
id
label
}
}
`,
};
const rolesResponse = await client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send(getRolesQuery);
originalMemberRoleId = rolesResponse.body.data.getRoles.find(
(role: { label: string }) => role.label === 'Member',
).id;
// Create a test role with canReadAllObjectRecords: true
const { data: roleData } = await createOneRole({
expectToFail: false,
input: {
label: 'RLS Test Role',
description: 'A role for RLS predicate enforcement testing',
icon: 'IconSettings',
canUpdateAllSettings: false,
canAccessAllTools: true,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: false,
canBeAssignedToUsers: true,
canBeAssignedToAgents: false,
canBeAssignedToApiKeys: false,
},
});
createdRoleId = roleData?.createOneRole?.id;
jestExpectToBeDefined(createdRoleId);
// Create RLS predicate: company.name CONTAINS "Acme"
await upsertRowLevelPermissionPredicates({
expectToFail: false,
input: {
roleId: createdRoleId,
objectMetadataId: companyObjectMetadataId,
predicates: [
{
fieldMetadataId: companyNameFieldMetadataId,
operand: RowLevelPermissionPredicateOperand.CONTAINS,
value: 'Acme',
},
],
predicateGroups: [],
},
});
// Create test companies
const createCompaniesOperation = createManyOperationFactory({
objectMetadataSingularName: 'company',
objectMetadataPluralName: 'companies',
gqlFields: COMPANY_GQL_FIELDS,
data: [
{ id: TEST_COMPANY_ACME_CORP_ID, name: 'Acme Corp' },
{ id: TEST_COMPANY_BETA_INC_ID, name: 'Beta Inc' },
{ id: TEST_COMPANY_ACME_LABS_ID, name: 'Acme Labs' },
],
});
await makeGraphqlAPIRequest(createCompaniesOperation);
// Assign the RLS test role to Jony (member)
await updateWorkspaceMemberRole({
client,
roleId: createdRoleId,
workspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.JONY,
});
});
afterAll(async () => {
// Restore original member role
const restoreMemberRoleQuery = {
query: `
mutation UpdateWorkspaceMemberRole {
updateWorkspaceMemberRole(
workspaceMemberId: "${WORKSPACE_MEMBER_DATA_SEED_IDS.JONY}"
roleId: "${originalMemberRoleId}"
) {
id
}
}
`,
};
await client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send(restoreMemberRoleQuery);
// Delete test companies
await deleteRecordsByIds('company', [
TEST_COMPANY_ACME_CORP_ID,
TEST_COMPANY_BETA_INC_ID,
TEST_COMPANY_ACME_LABS_ID,
]);
// Delete the test role
if (createdRoleId) {
await deleteOneRole({
expectToFail: false,
input: { idToDelete: createdRoleId },
});
}
// Disable RLS feature flag
await updateFeatureFlag({
featureFlag: FeatureFlagKey.IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED,
value: false,
expectToFail: false,
});
});
describe('findMany with RLS', () => {
it('should only return companies matching RLS predicate for restricted user', async () => {
const graphqlOperation = findManyOperationFactory({
objectMetadataSingularName: 'company',
objectMetadataPluralName: 'companies',
gqlFields: COMPANY_GQL_FIELDS,
filter: {
id: {
in: [
TEST_COMPANY_ACME_CORP_ID,
TEST_COMPANY_BETA_INC_ID,
TEST_COMPANY_ACME_LABS_ID,
],
},
},
});
const response =
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
expect(response.body.data).toBeDefined();
expect(response.body.data.companies).toBeDefined();
expect(response.body.data.companies.edges).toBeDefined();
const companyNames = response.body.data.companies.edges.map(
(edge: { node: { name: string } }) => edge.node.name,
);
// Should only see companies with "Acme" in the name
expect(companyNames).toContain('Acme Corp');
expect(companyNames).toContain('Acme Labs');
expect(companyNames).not.toContain('Beta Inc');
});
it('should return all companies for admin user', async () => {
const graphqlOperation = findManyOperationFactory({
objectMetadataSingularName: 'company',
objectMetadataPluralName: 'companies',
gqlFields: COMPANY_GQL_FIELDS,
filter: {
id: {
in: [
TEST_COMPANY_ACME_CORP_ID,
TEST_COMPANY_BETA_INC_ID,
TEST_COMPANY_ACME_LABS_ID,
],
},
},
});
const response = await makeGraphqlAPIRequest(graphqlOperation);
expect(response.body.data).toBeDefined();
expect(response.body.data.companies).toBeDefined();
const companyNames = response.body.data.companies.edges.map(
(edge: { node: { name: string } }) => edge.node.name,
);
// Admin should see all companies
expect(companyNames).toContain('Acme Corp');
expect(companyNames).toContain('Acme Labs');
expect(companyNames).toContain('Beta Inc');
});
});
describe('findOne with RLS', () => {
it('should return null for company not matching RLS predicate', async () => {
const graphqlOperation = findOneOperationFactory({
objectMetadataSingularName: 'company',
gqlFields: COMPANY_GQL_FIELDS,
filter: { id: { eq: TEST_COMPANY_BETA_INC_ID } },
});
const response =
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
expect(response.body.data).toBeDefined();
expect(response.body.data.company).toBeNull();
});
it('should return company matching RLS predicate', async () => {
const graphqlOperation = findOneOperationFactory({
objectMetadataSingularName: 'company',
gqlFields: COMPANY_GQL_FIELDS,
filter: { id: { eq: TEST_COMPANY_ACME_CORP_ID } },
});
const response =
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
expect(response.body.data).toBeDefined();
expect(response.body.data.company).toBeDefined();
expect(response.body.data.company.name).toBe('Acme Corp');
});
it('should return any company for admin user', async () => {
const graphqlOperation = findOneOperationFactory({
objectMetadataSingularName: 'company',
gqlFields: COMPANY_GQL_FIELDS,
filter: { id: { eq: TEST_COMPANY_BETA_INC_ID } },
});
const response = await makeGraphqlAPIRequest(graphqlOperation);
expect(response.body.data).toBeDefined();
expect(response.body.data.company).toBeDefined();
expect(response.body.data.company.name).toBe('Beta Inc');
});
});
describe('search with RLS', () => {
it('should only return companies matching RLS predicate in search results', async () => {
const graphqlOperation = searchFactory({
searchInput: 'Corp Labs Inc',
includedObjectNameSingulars: ['company'],
limit: 50,
});
const response =
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
expect(response.body.data).toBeDefined();
expect(response.body.data.search).toBeDefined();
const companyLabels = response.body.data.search.edges
.filter(
(edge: { node: { objectNameSingular: string } }) =>
edge.node.objectNameSingular === 'company',
)
.map((edge: { node: { label: string } }) => edge.node.label);
// Check that "Beta Inc" is not in the results
const hasBetaInc = companyLabels.some((label: string) =>
label.includes('Beta'),
);
expect(hasBetaInc).toBe(false);
});
it('should return all matching companies for admin user', async () => {
const graphqlOperation = searchFactory({
searchInput: 'Corp Labs Inc',
includedObjectNameSingulars: ['company'],
limit: 50,
});
const response = await makeGraphqlAPIRequest(graphqlOperation);
expect(response.body.data).toBeDefined();
expect(response.body.data.search).toBeDefined();
expect(response.body.data.search.edges).toBeDefined();
});
});
describe('updateOne with RLS', () => {
it('should fail to update company not matching RLS predicate', async () => {
const graphqlOperation = updateOneOperationFactory({
objectMetadataSingularName: 'company',
gqlFields: COMPANY_GQL_FIELDS,
recordId: TEST_COMPANY_BETA_INC_ID,
data: { name: 'Beta Inc Updated' },
});
const response =
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
// Should either fail with error or return null
const hasError = response.body.errors !== undefined;
const isNull = response.body.data?.updateCompany === null;
expect(hasError || isNull).toBe(true);
});
it('should successfully update company matching RLS predicate', async () => {
const graphqlOperation = updateOneOperationFactory({
objectMetadataSingularName: 'company',
gqlFields: COMPANY_GQL_FIELDS,
recordId: TEST_COMPANY_ACME_CORP_ID,
data: { name: 'Acme Corp Updated' },
});
const response =
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
expect(response.body.data).toBeDefined();
expect(response.body.data.updateCompany).toBeDefined();
expect(response.body.data.updateCompany.name).toBe('Acme Corp Updated');
// Restore original name
const restoreOperation = updateOneOperationFactory({
objectMetadataSingularName: 'company',
gqlFields: COMPANY_GQL_FIELDS,
recordId: TEST_COMPANY_ACME_CORP_ID,
data: { name: 'Acme Corp' },
});
await makeGraphqlAPIRequest(restoreOperation);
});
});
describe('deleteOne with RLS', () => {
it('should fail to delete company not matching RLS predicate', async () => {
const graphqlOperation = deleteOneOperationFactory({
objectMetadataSingularName: 'company',
gqlFields: 'id deletedAt',
recordId: TEST_COMPANY_BETA_INC_ID,
});
const response =
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
// Should either fail with error or return null
const hasError = response.body.errors !== undefined;
const isNull = response.body.data?.deleteCompany === null;
expect(hasError || isNull).toBe(true);
});
it('should successfully delete company matching RLS predicate and then restore it', async () => {
// First, delete the company
const deleteOperation = deleteOneOperationFactory({
objectMetadataSingularName: 'company',
gqlFields: 'id deletedAt',
recordId: TEST_COMPANY_ACME_LABS_ID,
});
const deleteResponse =
await makeGraphqlAPIRequestWithMemberRole(deleteOperation);
expect(deleteResponse.body.data).toBeDefined();
expect(deleteResponse.body.data.deleteCompany).toBeDefined();
expect(deleteResponse.body.data.deleteCompany.deletedAt).not.toBeNull();
// Restore the company using admin for cleanup
const restoreOperation = {
query: `
mutation RestoreCompany($id: UUID!) {
restoreCompany(id: $id) {
id
deletedAt
}
}
`,
variables: {
id: TEST_COMPANY_ACME_LABS_ID,
},
};
await client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send(restoreOperation);
});
});
});
@@ -0,0 +1,218 @@
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 { makeGraphqlAPIRequestWithApiKey } from 'test/integration/graphql/utils/make-graphql-api-request-with-api-key.util';
import { makeGraphqlAPIRequestWithGuestRole } from 'test/integration/graphql/utils/make-graphql-api-request-with-guest-role.util';
import { makeGraphqlAPIRequestWithMemberRole } from 'test/integration/graphql/utils/make-graphql-api-request-with-member-role.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { searchFactory } from 'test/integration/graphql/utils/search-factory.util';
import { updateWorkspaceMemberRole } from 'test/integration/graphql/utils/update-workspace-member-role.util';
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
import { WORKSPACE_MEMBER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
const client = request(`http://localhost:${APP_PORT}`);
describe('searchObjectRecordsPermissions', () => {
describe('basic permission tests', () => {
it('should throw a permission error when user does not have permission (guest role)', async () => {
const graphqlOperation = searchFactory({
searchInput: 'test',
limit: 10,
});
const response =
await makeGraphqlAPIRequestWithGuestRole(graphqlOperation);
expect(response.body.data).toStrictEqual({ search: null });
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toBe(
PermissionsExceptionMessage.PERMISSION_DENIED,
);
expect(response.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
});
it('should return search results when user has permission (admin role)', async () => {
const graphqlOperation = searchFactory({
searchInput: 'Seattle',
limit: 10,
});
const response = await makeGraphqlAPIRequest(graphqlOperation);
expect(response.body.data).toBeDefined();
expect(response.body.data.search).toBeDefined();
expect(response.body.data.search.edges).toBeDefined();
expect(Array.isArray(response.body.data.search.edges)).toBe(true);
});
it('should return search results when executed by api key', async () => {
const graphqlOperation = searchFactory({
searchInput: 'Seattle',
limit: 10,
});
const response = await makeGraphqlAPIRequestWithApiKey(graphqlOperation);
expect(response.body.data).toBeDefined();
expect(response.body.data.search).toBeDefined();
expect(response.body.data.search.edges).toBeDefined();
expect(Array.isArray(response.body.data.search.edges)).toBe(true);
});
});
describe('granular object permissions for search', () => {
let originalMemberRoleId: string;
let customRoleId: string;
beforeAll(async () => {
const getRolesQuery = {
query: `
query GetRoles {
getRoles {
id
label
}
}
`,
};
const rolesResponse = await client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send(getRolesQuery);
originalMemberRoleId = rolesResponse.body.data.getRoles.find(
(role: { label: string }) => role.label === 'Member',
).id;
});
afterAll(async () => {
const restoreMemberRoleQuery = {
query: `
mutation UpdateWorkspaceMemberRole {
updateWorkspaceMemberRole(
workspaceMemberId: "${WORKSPACE_MEMBER_DATA_SEED_IDS.JONY}"
roleId: "${originalMemberRoleId}"
) {
id
}
}
`,
};
await client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send(restoreMemberRoleQuery);
});
afterEach(async () => {
if (customRoleId) {
await deleteRole(client, customRoleId);
}
});
it('should only return person results when user can read person but not company', async () => {
const { roleId } = await createCustomRoleWithObjectPermissions({
label: 'PersonOnlySearchRole',
canReadPerson: true,
canReadCompany: false,
hasAllObjectRecordsReadPermission: false,
});
customRoleId = roleId;
await updateWorkspaceMemberRole({
client,
roleId: customRoleId,
workspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.JONY,
});
const graphqlOperation = searchFactory({
searchInput: 'Seattle',
limit: 50,
});
const response =
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
expect(response.body.data).toBeDefined();
expect(response.body.data.search).toBeDefined();
expect(response.body.data.search.edges).toBeDefined();
const edges = response.body.data.search.edges;
edges.forEach((edge: { node: { objectNameSingular: string } }) => {
expect(edge.node.objectNameSingular).not.toBe('company');
});
});
it('should only return company results when user can read company but not person', async () => {
const { roleId } = await createCustomRoleWithObjectPermissions({
label: 'CompanyOnlySearchRole',
canReadPerson: false,
canReadCompany: true,
hasAllObjectRecordsReadPermission: false,
});
customRoleId = roleId;
await updateWorkspaceMemberRole({
client,
roleId: customRoleId,
workspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.JONY,
});
const graphqlOperation = searchFactory({
searchInput: 'Airbnb',
includedObjectNameSingulars: ['company', 'person'],
limit: 50,
});
const response =
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
expect(response.body.data).toBeDefined();
expect(response.body.data.search).toBeDefined();
expect(response.body.data.search.edges).toBeDefined();
const edges = response.body.data.search.edges;
edges.forEach((edge: { node: { objectNameSingular: string } }) => {
expect(edge.node.objectNameSingular).not.toBe('person');
});
});
it('should return empty results when user cannot read any searchable objects', async () => {
const { roleId } = await createCustomRoleWithObjectPermissions({
label: 'NoReadSearchRole',
canReadPerson: false,
canReadCompany: false,
hasAllObjectRecordsReadPermission: false,
});
customRoleId = roleId;
await updateWorkspaceMemberRole({
client,
roleId: customRoleId,
workspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.JONY,
});
const graphqlOperation = searchFactory({
searchInput: 'Seattle',
includedObjectNameSingulars: ['company', 'person'],
limit: 50,
});
const response =
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
expect(response.body.data).toBeDefined();
expect(response.body.data.search).toBeDefined();
expect(response.body.data.search.edges).toHaveLength(0);
});
});
});