Skip to content

Default application roles cannot express a field allowlist, undermining least-privilege app design #23461

Description

@remihuigen

Summary

defineApplicationRole() does not provide a way to grant an app access to only a selected set of fields on an object.

Instead, field permissions appear to behave as deny-list overrides of an object-level baseline:

  • An object permission is required before the app can access that object’s records.
  • Once object read/update access is granted, every unspecified field is readable/updatable.
  • Setting canReadFieldValue: true / canUpdateFieldValue: true for selected fields does not create an allowlist.
  • To restrict the app to only three fields, I would need to add explicit false permissions for every other existing field.

This is both surprising and poor DX for application roles, where least privilege should be the default and easy to maintain.

Version

  • twenty-sdk: 2.23.0
  • twenty-client-sdk: 2.23.0
  • twenty-server: 2.24.1

Reproduction

I defined a default application role intended to allow read/write access to Company records, but only to address, companyLatitude, and companyLongitude:

import {
  defineApplicationRole,
  STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define'

export default defineApplicationRole({
  universalIdentifier: '...',
  label: 'Restricted app role',
  description: 'Should access only three Company fields',
  canReadAllObjectRecords: false,
  canUpdateAllObjectRecords: false,
  canSoftDeleteAllObjectRecords: false,
  canDestroyAllObjectRecords: false,
  canUpdateAllSettings: false,
  canBeAssignedToAgents: false,
  canBeAssignedToUsers: false,
  canBeAssignedToApiKeys: false,

  objectPermissions: [
    {
      objectUniversalIdentifier:
        STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
      canReadObjectRecords: true,
      canUpdateObjectRecords: true,
      canSoftDeleteObjectRecords: false,
      canDestroyObjectRecords: false,
    },
  ],

  fieldPermissions: [
    {
      objectUniversalIdentifier:
        STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
      fieldUniversalIdentifier:
        STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.address
          .universalIdentifier,
      canReadFieldValue: true,
      canUpdateFieldValue: true,
    },
    {
      objectUniversalIdentifier:
        STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
      fieldUniversalIdentifier: 'companyLatitude universal identifier',
      canReadFieldValue: true,
      canUpdateFieldValue: true,
    },
    {
      objectUniversalIdentifier:
        STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
      fieldUniversalIdentifier: 'companyLongitude universal identifier',
      canReadFieldValue: true,
      canUpdateFieldValue: true,
    },
  ],

  permissionFlagUniversalIdentifiers: [],
})

I tested this using both:

  1. The core API client in an app logic function.
  2. An app front component.

Actual behavior

The app can read and update all Company fields.

If I remove the objectPermissions entry and retain the fieldPermissions, the app cannot access Company records at all.

The apparent workaround is to enumerate every Company field that must not be accessible and set both values to false, which is cumbersome and, in some cases, not even possible—for example, when an app is not aware of the full server schema.

Expected behavior

There should be a supported way to express:

This app may read/update Company records, but it may only read/update these named fields.

For example, one of:

  • An allowlist mode for field permissions.
  • Default-deny field access when an application role declares field permissions for an object.
  • An explicit object-permission option such as defaultFieldAccess: 'none'.
  • Separate application-role semantics that are secure-by-default, while retaining current behavior for user-assigned roles.

Why this matters: least privilege and DX

The current model creates a bad developer experience and makes least-privilege configurations fragile.

An app author must:

  1. Grant object-level record access, because field permissions alone do not permit record access.
  2. Discover and enumerate every other field on the object.
  3. Add an explicit false permission for each one.
  4. Repeat that work whenever Twenty adds a standard field, the workspace adds a custom field, or another app extends the object.

That is error-prone and does not remain least-privilege over time: newly added fields are accessible by default unless the app is updated to deny them.

The app-role documentation says the app token and typed API client are restricted to the role’s permissions, and recommends declaring only the permissions functions need. That wording reasonably suggests that declaring three field permissions should grant access to those three fields—not grant broad access to every unlisted field.

For application code handling potentially sensitive CRM data, a field allowlist is the safer and more natural declaration.

Evidence

The documentation describes defineApplicationRole() as controlling access for app logic functions and front components, and recommends least privilege:

https://docs.twenty.com/developers/extend/apps/config/application#default-function-role

The permissions FAQ describes field permissions as overrides of object-level permissions:

https://docs.twenty.com/user-guide/permissions-access/how-tos/permissions-faq#how-do-permission-overrides-work

The current implementation is consistent with deny-list semantics, but that leaves no future-safe allowlist mechanism for app roles.

Request

Please consider adding a field allowlist/default-deny option for defineApplicationRole() (or changing its field-permission semantics), and clarify the documentation in the meantime.

Metadata

Metadata

Assignees

Labels

Type

Fields

Priority

None yet

Dev status

None yet

Start date

None yet

Target date

None yet

Quarter

None yet

Projects

Status
In project

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions