Whether this caller is denied ANY field on ANY field-security-enabled entity.
The whole projector is a no-op for everyone else, and this gate is what makes that true — including for the fail-closed branch in deniedFieldsFor, which would otherwise blank audit rows on deployments that never enabled field security at all. Cost is one pass over the entity list plus one denied-set walk per ENABLED entity, and the flag is off on every entity an administrator has not explicitly opted in.
Public because the WRITE side needs the same question: a caller who could have been served a
narrowed payload must not be allowed to write one back. See
ResolverBase.StripRecordChangePayloadFromClientInput.
Projects a whole result set. Returns the ORIGINAL array when nothing needed changing, so an unrestricted caller's rows are never rebuilt — and, on the cache-hit paths, so the cache's own frozen objects are handed back by reference exactly as they are today.
StaticIsWhether an entity is the audit trail this projector guards. Callers gate on this before constructing a projector, so nothing is paid on the other 99% of reads.
Field-level security for the payload columns of
MJ: Record Changes.Why this is not
ProviderBase.ApplyFieldSecurityProjectionEvery other FLS enforcement point computes the denied set against the entity being read. A Record Change row is about a DIFFERENT entity — the one named by its own
EntityIDcolumn — andMJ: Record Changesitself has field security switched off in every default deployment. The main projection therefore short-circuits onEnableFieldLevelSecurityand returns the row untouched, with a denied field's old and new values sitting inChangesJSONin plain text. That is the leak this closes: a user with entity-level read on the audit trail could read a salary they were denied onMJ: Employeesstraight out of it, in the default configuration.What happens to each column
ChangesJSONFullRecordJSONChangesDescriptionChangesDescriptionis human prose ("Salary changed from 100000 to 120000"). Redacting prose on the fly leaks on the first value that appears in an unexpected form, and regenerating it from the filtered JSON is a larger surface than this is worth. Withholding the whole column is coarse and it is safe, which is the right trade for a field whose only job is to be readable text — and callers already degrade, the record-changes UI falling back to 'Changes made'.Rows in one result span different entities
EntityIDvaries row to row, andEntityInfo.GetDeniedReadFieldswalks every field on the entity — its documented performance contract is that it must not be called in a row loop. Every lookup here is memoized on the rawEntityIDstring for the projector's lifetime, which is one request. Construct one projector per request, never one per row.