Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aa8113a1f0 | |||
| 80f87300f0 | |||
| e26f398f34 | |||
| e77de316e5 |
+60
@@ -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);
|
||||
});
|
||||
});
|
||||
+55
@@ -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');
|
||||
});
|
||||
});
|
||||
+460
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
+218
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user