6f3a86c4a9
Insert operations blocked by field-level update permissions on non-editable fields The insert code path in permissions.utils.ts fell through to the update case (no break), causing validateUpdateFieldPermissionOrThrow to reject inserts when any field had "Edit disabled" which could conflict with RLS predicates (used for insertion of new records) Fixes https://github.com/twentyhq/twenty/issues/19201 We will keep checking update permissions for insertion (until we decide to have a separate permission flag for insertion) but to fix the issue we will skip this part if it conflicts with an RLS predicate
525 lines
15 KiB
TypeScript
525 lines
15 KiB
TypeScript
import gql from 'graphql-tag';
|
|
import { TEST_COMPANY_1_ID } from 'test/integration/constants/test-company-ids.constants';
|
|
import { TEST_PERSON_1_ID } from 'test/integration/constants/test-person-ids.constants';
|
|
import { TEST_PRIMARY_LINK_URL } from 'test/integration/constants/test-primary-link-url.constant';
|
|
import { upsertFieldPermissions } from 'test/integration/graphql/utils/upsert-field-permissions.util';
|
|
import { upsertRowLevelPermissionPredicates } from 'test/integration/metadata/suites/row-level-permission-predicate/utils/upsert-row-level-permission-predicates.util';
|
|
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
|
import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util';
|
|
import { generateRecordName } from 'test/integration/utils/generate-record-name';
|
|
import { RowLevelPermissionPredicateOperand } from 'twenty-shared/types';
|
|
|
|
describe('Restricted fields', () => {
|
|
let personCity: string;
|
|
let memberRoleId: string;
|
|
let personObjectId: string;
|
|
let emailsFieldId: string;
|
|
let phonesFieldId: string;
|
|
|
|
beforeAll(async () => {
|
|
personCity = generateRecordName(TEST_PERSON_1_ID);
|
|
|
|
await makeRestAPIRequest({
|
|
method: 'post',
|
|
path: '/companies',
|
|
body: {
|
|
id: TEST_COMPANY_1_ID,
|
|
domainName: {
|
|
primaryLinkUrl: TEST_PRIMARY_LINK_URL,
|
|
},
|
|
},
|
|
});
|
|
|
|
await makeRestAPIRequest({
|
|
method: 'post',
|
|
path: '/people',
|
|
body: {
|
|
id: TEST_PERSON_1_ID,
|
|
city: personCity,
|
|
emails: {
|
|
primaryEmail: 'test@test.com',
|
|
},
|
|
phones: {
|
|
primaryPhoneNumber: '123456789',
|
|
primaryPhoneCountryCode: 'US',
|
|
primaryPhoneCallingCode: '+1',
|
|
},
|
|
},
|
|
});
|
|
|
|
// 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;
|
|
|
|
personObjectId = objects.find(
|
|
(obj: any) => obj.node.nameSingular === 'person',
|
|
)?.node.id;
|
|
|
|
// Get field metadata ID for email field
|
|
const getFieldMetadataOperation = {
|
|
query: gql`
|
|
query {
|
|
fields(paging: { first: 1000 }) {
|
|
edges {
|
|
node {
|
|
id
|
|
name
|
|
object {
|
|
nameSingular
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
};
|
|
|
|
const fieldMetadataResponse = await makeMetadataAPIRequest(
|
|
getFieldMetadataOperation,
|
|
);
|
|
const fields = fieldMetadataResponse.body.data.fields.edges;
|
|
|
|
emailsFieldId = fields.find(
|
|
(field: any) =>
|
|
field.node.name === 'emails' &&
|
|
field.node.object.nameSingular === 'person',
|
|
).node.id;
|
|
|
|
phonesFieldId = fields.find(
|
|
(field: any) =>
|
|
field.node.name === 'phones' &&
|
|
field.node.object.nameSingular === 'person',
|
|
).node.id;
|
|
|
|
// Get member role ID
|
|
const getRolesOperation = {
|
|
query: gql`
|
|
query {
|
|
getRoles {
|
|
id
|
|
label
|
|
}
|
|
}
|
|
`,
|
|
};
|
|
|
|
const rolesResponse = await makeMetadataAPIRequest(getRolesOperation);
|
|
|
|
memberRoleId = rolesResponse.body.data.getRoles.find(
|
|
(role: any) => role.label === 'Member',
|
|
)?.id;
|
|
|
|
// Create field permission restricting read access to email field
|
|
await upsertFieldPermissions({
|
|
roleId: memberRoleId,
|
|
fieldPermissions: [
|
|
{
|
|
objectMetadataId: personObjectId,
|
|
fieldMetadataId: emailsFieldId,
|
|
canReadFieldValue: false,
|
|
canUpdateFieldValue: null,
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should hide fields when user has restricted read permissions - findOne', async () => {
|
|
await makeRestAPIRequest({
|
|
method: 'get',
|
|
path: `/people/${TEST_PERSON_1_ID}`,
|
|
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
|
|
})
|
|
.expect(200)
|
|
.expect((res) => {
|
|
const person = res.body.data.person;
|
|
|
|
expect(person).toBeDefined();
|
|
expect(person.id).toBeDefined();
|
|
expect(person.emails).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('updateOne', () => {
|
|
it('should hide fields in the response when user has restricted read permissions', async () => {
|
|
// Create field permission restricting update access to phones field
|
|
await upsertFieldPermissions({
|
|
roleId: memberRoleId,
|
|
fieldPermissions: [
|
|
{
|
|
objectMetadataId: personObjectId,
|
|
fieldMetadataId: phonesFieldId,
|
|
canReadFieldValue: false,
|
|
canUpdateFieldValue: null,
|
|
},
|
|
],
|
|
});
|
|
|
|
await makeRestAPIRequest({
|
|
method: 'patch',
|
|
path: `/people/${TEST_PERSON_1_ID}`,
|
|
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
|
|
body: {
|
|
name: {
|
|
firstName: 'John',
|
|
},
|
|
},
|
|
})
|
|
.expect(200)
|
|
.expect((res) => {
|
|
const updatedPerson = res.body.data.updatePerson;
|
|
|
|
expect(updatedPerson.name.firstName).toBe('John');
|
|
expect(updatedPerson.phones).toBeUndefined();
|
|
});
|
|
});
|
|
it('should block update when user tries to update non-updatable field', async () => {
|
|
// Create field permission restricting update access to phones field
|
|
await upsertFieldPermissions({
|
|
roleId: memberRoleId,
|
|
fieldPermissions: [
|
|
{
|
|
objectMetadataId: personObjectId,
|
|
fieldMetadataId: phonesFieldId,
|
|
canReadFieldValue: null,
|
|
canUpdateFieldValue: false,
|
|
},
|
|
],
|
|
});
|
|
|
|
await makeRestAPIRequest({
|
|
method: 'patch',
|
|
path: `/people/${TEST_PERSON_1_ID}`,
|
|
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
|
|
body: {
|
|
phones: {
|
|
primaryPhoneNumber: '987654321',
|
|
primaryPhoneCountryCode: 'FR',
|
|
primaryPhoneCallingCode: '+33',
|
|
},
|
|
},
|
|
})
|
|
.expect(400)
|
|
.expect((res) => {
|
|
expect(res.body.messages[0]).toContain(
|
|
'Entity performing the request does not have permission',
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should allow update when user has no restricted update permissions', async () => {
|
|
// Remove field permission restrictions
|
|
await upsertFieldPermissions({
|
|
roleId: memberRoleId,
|
|
fieldPermissions: [
|
|
{
|
|
objectMetadataId: personObjectId,
|
|
fieldMetadataId: phonesFieldId,
|
|
canReadFieldValue: null,
|
|
canUpdateFieldValue: null,
|
|
},
|
|
],
|
|
});
|
|
|
|
await makeRestAPIRequest({
|
|
method: 'patch',
|
|
path: `/people/${TEST_PERSON_1_ID}`,
|
|
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
|
|
body: {
|
|
city: 'Updated City',
|
|
},
|
|
})
|
|
.expect(200)
|
|
.expect((res) => {
|
|
const updatedPerson = res.body.data.updatePerson;
|
|
|
|
expect(updatedPerson.city).toBe('Updated City');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('createOne', () => {
|
|
it('should block create when restricted field is not in any RLS predicate', async () => {
|
|
await upsertFieldPermissions({
|
|
roleId: memberRoleId,
|
|
fieldPermissions: [
|
|
{
|
|
objectMetadataId: personObjectId,
|
|
fieldMetadataId: phonesFieldId,
|
|
canReadFieldValue: null,
|
|
canUpdateFieldValue: false,
|
|
},
|
|
],
|
|
});
|
|
|
|
await makeRestAPIRequest({
|
|
method: 'post',
|
|
path: `/people`,
|
|
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
|
|
body: {
|
|
phones: {
|
|
primaryPhoneNumber: '555123456',
|
|
primaryPhoneCountryCode: 'US',
|
|
primaryPhoneCallingCode: '+1',
|
|
},
|
|
},
|
|
})
|
|
.expect(400)
|
|
.expect((res) => {
|
|
expect(res.body.messages[0]).toContain(
|
|
'Entity performing the request does not have permission',
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should allow create when restricted field is referenced in an RLS predicate', async () => {
|
|
await upsertFieldPermissions({
|
|
roleId: memberRoleId,
|
|
fieldPermissions: [
|
|
{
|
|
objectMetadataId: personObjectId,
|
|
fieldMetadataId: phonesFieldId,
|
|
canReadFieldValue: null,
|
|
canUpdateFieldValue: false,
|
|
},
|
|
],
|
|
});
|
|
|
|
await upsertRowLevelPermissionPredicates({
|
|
input: {
|
|
roleId: memberRoleId,
|
|
objectMetadataId: personObjectId,
|
|
predicates: [
|
|
{
|
|
fieldMetadataId: phonesFieldId,
|
|
operand: RowLevelPermissionPredicateOperand.IS_NOT_EMPTY,
|
|
},
|
|
],
|
|
predicateGroups: [],
|
|
},
|
|
});
|
|
|
|
await makeRestAPIRequest({
|
|
method: 'post',
|
|
path: `/people`,
|
|
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
|
|
body: {
|
|
phones: {
|
|
primaryPhoneNumber: '555123456',
|
|
primaryPhoneCountryCode: 'US',
|
|
primaryPhoneCallingCode: '+1',
|
|
},
|
|
},
|
|
})
|
|
.expect(201)
|
|
.expect((res) => {
|
|
const createdPerson = res.body.data.createPerson;
|
|
|
|
expect(createdPerson).toBeDefined();
|
|
expect(createdPerson.phones.primaryPhoneNumber).toBe('555123456');
|
|
});
|
|
|
|
await upsertRowLevelPermissionPredicates({
|
|
input: {
|
|
roleId: memberRoleId,
|
|
objectMetadataId: personObjectId,
|
|
predicates: [],
|
|
predicateGroups: [],
|
|
},
|
|
});
|
|
});
|
|
|
|
it('should allow create when user has no restricted update permissions', async () => {
|
|
await upsertFieldPermissions({
|
|
roleId: memberRoleId,
|
|
fieldPermissions: [
|
|
{
|
|
objectMetadataId: personObjectId,
|
|
fieldMetadataId: phonesFieldId,
|
|
canReadFieldValue: null,
|
|
canUpdateFieldValue: null,
|
|
},
|
|
{
|
|
objectMetadataId: personObjectId,
|
|
fieldMetadataId: emailsFieldId,
|
|
canReadFieldValue: false,
|
|
canUpdateFieldValue: null,
|
|
},
|
|
],
|
|
});
|
|
|
|
await makeRestAPIRequest({
|
|
method: 'post',
|
|
path: `/people`,
|
|
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
|
|
body: {
|
|
city: 'New City',
|
|
},
|
|
})
|
|
.expect(201)
|
|
.expect((res) => {
|
|
const createdPerson = res.body.data.createPerson;
|
|
|
|
expect(createdPerson.city).toBe('New City');
|
|
expect(createdPerson.emails).toBeUndefined();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('createMany', () => {
|
|
it('should block createMany when restricted field is not in any RLS predicate', async () => {
|
|
await upsertFieldPermissions({
|
|
roleId: memberRoleId,
|
|
fieldPermissions: [
|
|
{
|
|
objectMetadataId: personObjectId,
|
|
fieldMetadataId: phonesFieldId,
|
|
canReadFieldValue: null,
|
|
canUpdateFieldValue: false,
|
|
},
|
|
],
|
|
});
|
|
|
|
await makeRestAPIRequest({
|
|
method: 'post',
|
|
path: `/batch/people`,
|
|
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
|
|
body: [
|
|
{
|
|
phones: {
|
|
primaryPhoneNumber: '555123456',
|
|
primaryPhoneCountryCode: 'US',
|
|
primaryPhoneCallingCode: '+1',
|
|
},
|
|
},
|
|
],
|
|
})
|
|
.expect(400)
|
|
.expect((res) => {
|
|
expect(res.body.messages[0]).toContain(
|
|
'Entity performing the request does not have permission',
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should allow createMany when restricted field is referenced in an RLS predicate', async () => {
|
|
await upsertFieldPermissions({
|
|
roleId: memberRoleId,
|
|
fieldPermissions: [
|
|
{
|
|
objectMetadataId: personObjectId,
|
|
fieldMetadataId: phonesFieldId,
|
|
canReadFieldValue: null,
|
|
canUpdateFieldValue: false,
|
|
},
|
|
],
|
|
});
|
|
|
|
await upsertRowLevelPermissionPredicates({
|
|
input: {
|
|
roleId: memberRoleId,
|
|
objectMetadataId: personObjectId,
|
|
predicates: [
|
|
{
|
|
fieldMetadataId: phonesFieldId,
|
|
operand: RowLevelPermissionPredicateOperand.IS_NOT_EMPTY,
|
|
},
|
|
],
|
|
predicateGroups: [],
|
|
},
|
|
});
|
|
|
|
await makeRestAPIRequest({
|
|
method: 'post',
|
|
path: `/batch/people`,
|
|
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
|
|
body: [
|
|
{
|
|
phones: {
|
|
primaryPhoneNumber: '555123456',
|
|
primaryPhoneCountryCode: 'US',
|
|
primaryPhoneCallingCode: '+1',
|
|
},
|
|
},
|
|
],
|
|
})
|
|
.expect(201)
|
|
.expect((res) => {
|
|
const createdPeople = res.body.data.createPeople;
|
|
|
|
expect(createdPeople).toHaveLength(1);
|
|
expect(createdPeople[0].phones.primaryPhoneNumber).toBe('555123456');
|
|
});
|
|
|
|
await upsertRowLevelPermissionPredicates({
|
|
input: {
|
|
roleId: memberRoleId,
|
|
objectMetadataId: personObjectId,
|
|
predicates: [],
|
|
predicateGroups: [],
|
|
},
|
|
});
|
|
});
|
|
|
|
it('should allow createMany when user has no restricted update permissions', async () => {
|
|
await upsertFieldPermissions({
|
|
roleId: memberRoleId,
|
|
fieldPermissions: [
|
|
{
|
|
objectMetadataId: personObjectId,
|
|
fieldMetadataId: phonesFieldId,
|
|
canReadFieldValue: null,
|
|
canUpdateFieldValue: null,
|
|
},
|
|
{
|
|
objectMetadataId: personObjectId,
|
|
fieldMetadataId: emailsFieldId,
|
|
canReadFieldValue: false,
|
|
canUpdateFieldValue: null,
|
|
},
|
|
],
|
|
});
|
|
|
|
await makeRestAPIRequest({
|
|
method: 'post',
|
|
path: `/batch/people`,
|
|
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
|
|
body: [
|
|
{
|
|
city: 'Batch City 1',
|
|
},
|
|
{
|
|
city: 'Batch City 2',
|
|
},
|
|
],
|
|
})
|
|
.expect(201)
|
|
.expect((res) => {
|
|
const createdPeople = res.body.data.createPeople;
|
|
|
|
expect(createdPeople).toHaveLength(2);
|
|
expect(createdPeople[0].city).toBe('Batch City 1');
|
|
expect(createdPeople[0].emails).toBeUndefined();
|
|
expect(createdPeople[1].city).toBe('Batch City 2');
|
|
expect(createdPeople[1].emails).toBeUndefined();
|
|
});
|
|
});
|
|
});
|
|
});
|