# Issue 656 FLS Effective Permissions

Status: draft
Scope class: runtime security contract
Date: 2026-05-12

## Purpose

Issue #656 exposed that NexusCRM stores `FieldPermissions` metadata but does
not consistently enforce it at request time. A case-sensitive lookup miss makes
an effective FLS row look absent, and the runtime then falls back to object
CRUD. The result can be a read leak, a write grant, and an incomplete
`/permissions/effective` response.

This document pins the intended contract before implementation: explicit FLS is
the field-level source of truth, while a truly absent FLS row preserves issue
#656's existing object-CRUD fallback until the platform owns a complete
backfill/cutover.

## Contract

- Effective field access is computed from the caller profile's owned
  `PermissionSet`, directly assigned permission sets, and permission set groups.
  The sources are additive, matching the Salesforce permission model: an active
  source row with `PermissionsRead=true` or `PermissionsEdit=true` grants that
  capability, and a `false` value is not a global deny across other sources.
  Subtractive semantics belong to Salesforce-style muting, not ordinary
  `FieldPermissions` rows.
- `FieldPermissions.SobjectType` and `FieldPermissions.Field` are matched
  case-insensitively.
- `FieldPermissions.Field` may be stored as either a simple field API name
  (`Secret__c`) or the Salesforce-qualified form (`Account.Secret__c`). Both
  forms address the same logical field for `Account.Secret__c`.
- Runtime field read/edit checks use the active effective `FieldPermissions`
  aggregate when one exists. An aggregate `false` denies. If no active effective
  row exists for the logical field, runtime keeps the legacy object-level
  read/edit fallback required by issue #656.
- Create-time field capability uses the same rule: an active effective field
  edit aggregate of `false` blocks create, and an absent row falls back to the
  already-verified object Create permission rather than requiring object Edit.
- The only bypasses are existing platform bypasses: superuser/system admin,
  elevated/scoped internal access at the caller, and derived-access-policy
  objects that do not own self `FieldPermissions` rows.
- `GET /permissions/effective` always returns a `fieldPermissions` array. The
  public `Field` value is Salesforce-qualified as `SobjectType.Field`.

## Known Behavior Change

The behavior change is intentionally limited to active rows that already exist
but were previously missed because of case or simple-vs-qualified field
spelling. Those rows now participate in additive effective FLS. A non-admin user
with object-level access and no active effective `FieldPermissions` row for a
field keeps the prior read/write/create fallback.

An assigned permission set row with `PermissionsRead=false` or
`PermissionsEdit=false` does not override a profile-owned or otherwise assigned
grant. If NexusCRM later needs muting-aware FLS subtraction, that should be
implemented as a Salesforce-style muting primitive, not by changing
`FieldPermissions` aggregation from grant-wins to deny-wins.

Fail-closed missing-row semantics are a separate cutover. They require a
metadata backfill and an issue/API contract update before runtime can deny
absent rows safely.

## Implementation Shape

- Add a shared field-permission name helper for splitting, matching, and
  qualifying `FieldPermissions.Field`.
- Update repository queries to use case-insensitive matching and to consider
  both simple and qualified field spellings.
- Aggregate list responses by logical `(SobjectType, field component)` so cache
  warmup and `/permissions/effective` cannot split duplicate rows by spelling.
- Keep read, update, and create missing-row fallbacks in place while ensuring
  active effective rows always override object CRUD.
- Filter soft-deleted permission rows, direct `PermissionSetAssignment` rows,
  permission sets, and permission set groups from effective permission queries
  so runtime checks, cache warmup, and `/permissions/effective` cannot
  resurrect revoked access.
- Reactivate soft-deleted `ObjectPermissions` and `FieldPermissions` rows on
  explicit upsert so unique-key collisions do not leave newly authored
  permissions invisible.
- Include the field-permission dimension in the effective-permissions REST
  response.

## Verification

- Helper unit tests cover simple names, qualified names, case variants, and
  mismatched object prefixes.
- Permission service tests cover effective deny, missing-row fallback, superuser
  bypass, and derived-access-policy behavior.
- Persistence tests cover the case-insensitive and simple/qualified query
  shapes, additive grant-wins aggregation, active-only permission rows and
  assignments, and soft-deleted row reactivation on upsert.
- Handler tests cover `fieldPermissions` response shape, qualified field output,
  and sanitized 5xx responses.

References:

- Issue #656: https://github.com/pingcap-inc/NexusCRM/issues/656
- Salesforce field access overview:
  https://help.salesforce.com/s/articleView?id=platform.customize_fieldaccess.htm
- Salesforce permission sets:
  https://help.salesforce.com/s/articleView?id=sf.perm_sets_overview.htm
- Salesforce FLS read/edit meanings:
  https://help.salesforce.com/s/articleView?id=000382713
