Object-Level & Field-Level Security (CRUD/FLS)
💬 In plain
words: Two locks on data:
object-level (can you touch Accounts at all — Create/Read/Update/Delete) and
field-level (okay, but can you see the Salary field?). Sharing decides WHICH
records; CRUD/FLS decides WHAT you can do with the object and its fields.
📌 Example: Asha can see the Candidate object (object-level
✓) but the Salary field is hidden from her profile (field-level ✗). She opens
the record fine — the Salary column is simply not there, even in reports and
the API.
Concept
CRUD (object permissions) and FLS (field permissions) are granted via profiles/permission sets and are enforced automatically in the standard UI, standard controllers, and Lightning Data Service — but NOT automatically in Apex, which runs in system mode by default on API v66 and earlier (from v67, user mode is the default — see 3.7). In older code Apex must opt in: SOQL with WITH USER_MODE, DML with 'as user', or Security.stripInaccessible() to sanitize records. This is distinct from record-level sharing (Module 3) — CRUD/FLS answers 'which objects/fields', sharing answers 'which rows'. Directly reused in Module 11.3 (secure SOQL) and Module 9 (LDS enforces FLS for LWC).
🧠 CRUD/FLS vs
Sharing: CRUD/FLS = WHICH
objects & fields. Sharing = WHICH rows. Apex runs in SYSTEM mode → you must
opt in (USER_MODE / stripInaccessible).
Core Q&A
Q: How do you enforce
CRUD/FLS in Apex, and which mechanism do you choose when?
🎯
Say this first: Use WITH USER_MODE in SOQL/DML as the
default; Security.stripInaccessible for cleaning data; manual describe checks
only for special cases.
A: Prefer
user-mode database operations: 'SELECT ... WITH USER_MODE' and 'insert
as user records' — they enforce CRUD, FLS, and sharing together
and report all violations. WITH SECURITY_ENFORCED is the older
query-only option; it throws on the first violation, ignores polymorphic
fields, and does nothing for DML. Security.stripInaccessible(AccessType.READABLE/CREATABLE,
records) is the graceful-degradation tool — instead of throwing, it strips
fields the user cannot access, which is right for integration-facing or bulk
paths where partial success is acceptable. Rule of thumb: user mode as the
default in new code, stripInaccessible when you must not throw,
SECURITY_ENFORCED only in legacy code you are not refactoring — and note it is
REMOVED at API v67+: it no longer compiles (3.7).
// Modern
default
List<Case>
cases = [SELECT Id, Subject FROM Case
WHERE Status = 'Open' WITH
USER_MODE];
// Graceful
degradation
SObjectAccessDecision
d = Security.stripInaccessible(
AccessType.READABLE, cases);
return d.getRecords();
Follow-ups (scenario-based)
Q1: The standard UI hides a
field from a user, but your LWC calling an Apex @AuraEnabled method still shows
it. Why, and what are the two distinct fixes?
A1: Because
the Apex method runs in system mode: FLS was checked by the page layout,
not by your query. Fix one: make the Apex enforce security (WITH USER_MODE
or stripInaccessible before returning). Fix two: eliminate the custom
Apex read path and use Lightning Data Service / getRecord wire, which enforces
FLS and CRUD for free and gives you caching (ties into Module 9.6). An
architect answer notes the second option is preferred when no complex server
logic is needed.
Q2: A configurable approval
engine lets business users change rules without engineering. How do you stop
that from becoming a privilege-escalation path?