# The Salesforce access enforcement matrix

Verified 2026-09 against
<https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_keywords_sharing.htm>
and
<https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_perms_enforcing.htm>.

## Three separate things

Apex runs in system mode by default. "Secure" means enforcing all three of these, and each needs a
different construct:

| What | Construct | Covers | Does **not** cover |
|---|---|---|---|
| Record access (sharing rules, role hierarchy, OWD) | `with sharing` on the class | Which *rows* the user can see | Object or field permissions |
| Object and field permissions (CRUD/FLS) | `WITH USER_MODE` on SOQL, `AccessLevel.USER_MODE` on DML | Which *objects and fields* the user may read or write | Nothing beyond the operation it is on |
| Data already fetched in system mode | `Security.stripInaccessible` | Removes fields and records the user cannot access | Does not re-run the query |

The single most common mistake is assuming `with sharing` is enough. It is not: a class declared
`with sharing` will still happily read a field the running user has no access to.

## Class-level sharing

```apex
public with sharing class OrderService { }      // enforces sharing rules
public with sharing class OrderSelector { }     // selectors declare it too
public without sharing class BatchReconciler { } // system mode — justify it
```

- `with sharing` — the default choice for anything reached from a user action, and for shared
  services and selectors.
- `without sharing` — deliberate elevation only. Every use needs a comment saying why, and the
  method should be narrow enough that the elevation is contained.
- The org standard recognises these **two keywords only**. `inherited sharing` exists on the
  platform but is not used here: the reviewer's question is always "which mode does this class
  run in", and an explicit answer is cheaper than tracing the caller. A class with no declaration
  at all defaults to system mode when entered directly, so the keyword is never optional.

Note: sharing declarations are not inherited by inner classes from the outer class in the way people
expect — declare inner classes explicitly.

## Query and DML access level

```apex
// Static SOQL
List<Account> accounts = [SELECT Id, Name FROM Account WITH USER_MODE];

// Dynamic SOQL — bind variables, never concatenation
String q = 'SELECT Id, Name FROM Account WHERE Industry = :industry';
List<Account> results = Database.queryWithBinds(
    q,
    new Map<String, Object>{ 'industry' => industry },
    AccessLevel.USER_MODE
);

// DML
Database.insert(newAccounts, AccessLevel.USER_MODE);
Database.update(changed, AccessLevel.USER_MODE);
```

`AccessLevel.SYSTEM_MODE` is available and explicit — use it where elevation is intended, so that the
intent is visible at the call site rather than implied by its absence.

## Migrating `WITH SECURITY_ENFORCED`

`WITH USER_MODE` supersedes `WITH SECURITY_ENFORCED`. It:

- covers polymorphic lookup fields, which `SECURITY_ENFORCED` does not,
- applies to DML through `AccessLevel`, giving one consistent model,
- produces clearer exceptions.

Migration rule: **replace, never combine.**

```apex
// Before
List<Contact> cs = [SELECT Id, Email FROM Contact WITH SECURITY_ENFORCED];

// After
List<Contact> cs = [SELECT Id, Email FROM Contact WITH USER_MODE];
```

Writing both clauses on one query is redundant and signals that nobody was sure which applied.

## `Security.stripInaccessible`

For the case where a query genuinely ran in system mode but the result goes back to a user:

```apex
SObjectAccessDecision decision = Security.stripInaccessible(
    AccessType.READABLE,
    systemModeRecords
);
return decision.getRecords();
```

- Use `AccessType.READABLE` before returning, `CREATABLE`/`UPDATABLE` before DML on user-supplied
  data.
- `getRemovedFields()` tells you what was stripped — useful in tests to assert enforcement actually
  happened.
- It strips fields; it does not add back rows the user should have seen, and it does not make a
  system-mode query safe if the *rows* were the problem. If you can run the query in user mode,
  do that instead.

## Where enforcement is easy to miss

- **`@AuraEnabled` methods** — the entry point from any component, including from a guest user on a
  public site. Each one needs the full set.
- **Apex REST / SOAP endpoints** — same, plus input validation.
- **Batch, Queueable and Schedulable** — run as the user who enqueued them but commonly in system
  mode; be explicit about which access level each query should use.
- **Triggers** — run in system mode by design. Enforcement belongs at the service entry point, not
  in the trigger.
- **Flows calling invocable Apex** — the invocable method's class declaration decides sharing.

## Testing enforcement

Assert it, do not assume it:

```apex
@IsTest
static void restrictedUserSeesNothing() {
    User u = TestDataFactory.restrictedUser();
    System.runAs(u) {
        Test.startTest();
        List<Order> result = OrderService.recentOrders(accountId);
        Test.stopTest();
        Assert.areEqual(0, result.size(), 'Restricted user should see no orders');
    }
}
```

A test that only runs as the admin proves nothing about enforcement — the admin passes every check.
