Fix field permission validation rejecting undefined optional fields (#19243)

The backend validation for field permissions was using !== null to check
canReadFieldValue and canUpdateFieldValue, but these optional GraphQL
fields can also be undefined when omitted from the input. This caused
the validation to incorrectly reject legitimate requests (e.g.,
restricting only "update" without specifying "read") with the error
"Field permissions can only be used to restrict access, not to grant
additional permissions."

Replaced !== null checks with isDefined() so that both null and
undefined are treated as "no opinion" on that permission.

To reproduce:
- Create a single FieldPermission on an object with canEdit: false
without any other rule on that same object
This commit is contained in:
Weiko
2026-04-02 15:15:14 +02:00
committed by GitHub
parent 6f89098340
commit 6ae5900ac9
3 changed files with 37 additions and 58 deletions
@@ -130,8 +130,8 @@ export class FieldPermissionService {
{
objectMetadataId: fieldPermission.objectMetadataId,
fieldMetadataId: fieldPermission.fieldMetadataId,
canReadFieldValue: fieldPermission.canReadFieldValue ?? undefined,
canUpdateFieldValue: fieldPermission.canUpdateFieldValue ?? undefined,
canReadFieldValue: fieldPermission.canReadFieldValue,
canUpdateFieldValue: fieldPermission.canUpdateFieldValue,
},
);
}
@@ -183,9 +183,13 @@ export class FieldPermissionService {
);
} else {
const effectiveCanRead =
desired.canReadFieldValue ?? current.canReadFieldValue;
desired.canReadFieldValue !== undefined
? desired.canReadFieldValue
: current.canReadFieldValue;
const effectiveCanUpdate =
desired.canUpdateFieldValue ?? current.canUpdateFieldValue;
desired.canUpdateFieldValue !== undefined
? desired.canUpdateFieldValue
: current.canUpdateFieldValue;
const changed =
effectiveCanRead !== current.canReadFieldValue ||
effectiveCanUpdate !== current.canUpdateFieldValue;
@@ -201,8 +205,8 @@ export class FieldPermissionService {
current.objectMetadataUniversalIdentifier,
fieldMetadataUniversalIdentifier:
current.fieldMetadataUniversalIdentifier,
canReadFieldValue: effectiveCanRead ?? undefined,
canUpdateFieldValue: effectiveCanUpdate ?? undefined,
canReadFieldValue: effectiveCanRead,
canUpdateFieldValue: effectiveCanUpdate,
createdAt: current.createdAt,
updatedAt: now,
});
@@ -210,9 +214,16 @@ export class FieldPermissionService {
}
}
const inputFieldKeys = new Set(
input.fieldPermissions.map((fp) =>
keyFrom(fp.objectMetadataId, fp.fieldMetadataId),
),
);
for (const current of currentFieldPermissionsForRole) {
const key = keyFrom(current.objectMetadataId, current.fieldMetadataId);
if (!desiredMap.has(key)) {
if (inputFieldKeys.has(key) && !desiredMap.has(key)) {
flatEntityToDelete.push({
universalIdentifier: current.universalIdentifier,
applicationUniversalIdentifier:
@@ -324,9 +335,9 @@ export class FieldPermissionService {
}
if (
(fieldPermission.canUpdateFieldValue !== null &&
(isDefined(fieldPermission.canUpdateFieldValue) &&
fieldPermission.canUpdateFieldValue !== false) ||
(fieldPermission.canReadFieldValue !== null &&
(isDefined(fieldPermission.canReadFieldValue) &&
fieldPermission.canReadFieldValue !== false)
) {
throw new PermissionsException(