Fix insert new record with RLS (#17164)

## Context
Now that RLS predicates are applied, creating a record through the FE
(which is empty by default) is failing if your role has predicates and
your input does not respect them (which will always be true since, as
said above, input will be pretty much empty)

## Implementation
- Moved isMatching* filters to twenty-shared
- Implemented isMatchingRlsPredicates utils in the backend (ORM) to
check before insertion/update if the record is matching the current user
role Rls predicates, reusing the isMatching* filters utils moved to
twenty-shared
- Frontend now applies RLS predicates before creating a new record
(similarly to what we do with view filters)

Note:
It seems composite were not properly handled with view-filter insertion
logic, since I'm reusing the util for now, the issue remains for RLS and
will need to be addressed
This commit is contained in:
Weiko
2026-01-15 17:40:47 +01:00
committed by GitHub
parent 2cff75d772
commit 8413c6f3dd
84 changed files with 1499 additions and 463 deletions
@@ -145,6 +145,8 @@ describe('WorkspaceRepository', () => {
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
restrictedFields: {},
rowLevelPermissionPredicates: [],
rowLevelPermissionPredicateGroups: [],
},
};
mockQueryRunner = {} as QueryRunner;
@@ -31,6 +31,7 @@ import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util';
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
import { validateRLSPredicatesForRecords } from 'src/engine/twenty-orm/utils/validate-rls-predicates-for-records.util';
export class WorkspaceInsertQueryBuilder<
T extends ObjectLiteral,
@@ -178,6 +179,8 @@ export class WorkspaceInsertQueryBuilder<
this.expressionMap.valuesSet = updatedValues;
}
this.validateRLSPredicatesForInsert();
const result = await super.execute();
const eventSelectQueryBuilder = (
this.connection.manager as WorkspaceEntityManager
@@ -269,6 +272,26 @@ export class WorkspaceInsertQueryBuilder<
}
}
private validateRLSPredicatesForInsert(): void {
const mainAliasTarget = this.getMainAliasTarget();
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
const valuesToInsert = Array.isArray(this.expressionMap.valuesSet)
? this.expressionMap.valuesSet
: [this.expressionMap.valuesSet];
validateRLSPredicatesForRecords({
records: valuesToInsert,
objectMetadata,
internalContext: this.internalContext,
authContext: this.authContext,
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
});
}
private getMainAliasTarget(): EntityTarget<T> {
const mainAliasTarget = this.expressionMap.mainAlias?.target;
@@ -37,6 +37,7 @@ import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util';
import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util';
import { validateRLSPredicatesForRecords } from 'src/engine/twenty-orm/utils/validate-rls-predicates-for-records.util';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
export class WorkspaceUpdateQueryBuilder<
@@ -175,6 +176,21 @@ export class WorkspaceUpdateQueryBuilder<
this.applyRowLevelPermissionPredicates();
const valuesSet = this.expressionMap.valuesSet ?? {};
const updatedRecords: T[] = formattedBefore.map(
(record, index) =>
({
...record,
...(Array.isArray(valuesSet)
? (valuesSet[index] ?? valuesSet[0] ?? {})
: valuesSet),
}) as T,
);
this.validateRLSPredicatesForUpdate({
updatedRecords,
});
const result = await super.execute();
const after = await eventSelectQueryBuilder.getMany();
@@ -323,12 +339,34 @@ export class WorkspaceUpdateQueryBuilder<
}));
}
const beforeRecordById = new Map<string, T>();
for (const beforeRecord of formattedBefore) {
if (isDefined(beforeRecord.id)) {
beforeRecordById.set(beforeRecord.id, beforeRecord);
}
}
for (const input of this.manyInputs) {
this.expressionMap.valuesSet = input.partialEntity;
this.where({ id: input.criteria });
this.applyRowLevelPermissionPredicates();
const beforeRecord = beforeRecordById.get(input.criteria);
const updatedRecords = beforeRecord
? [
{
...beforeRecord,
...input.partialEntity,
} as T,
]
: [];
this.validateRLSPredicatesForUpdate({
updatedRecords,
});
const result = await super.execute();
results.push(result);
@@ -521,4 +559,26 @@ export class WorkspaceUpdateQueryBuilder<
featureFlagMap: this.featureFlagMap,
});
}
private validateRLSPredicatesForUpdate({
updatedRecords,
}: {
updatedRecords: T[];
}): void {
const mainAliasTarget = this.getMainAliasTarget();
const objectMetadata = getObjectMetadataFromEntityTarget(
mainAliasTarget,
this.internalContext,
);
validateRLSPredicatesForRecords({
records: updatedRecords,
objectMetadata,
internalContext: this.internalContext,
authContext: this.authContext,
shouldBypassPermissionChecks: this.shouldBypassPermissionChecks,
errorMessage:
'Updated record does not satisfy row-level security constraints of your current role',
});
}
}