CRUD/FLS enforcement, sharing keywords, security-enforced SOQL, permission checks, and credential management
From claude-sfdx-iqnpx claudepluginhub bhanu91221/claude-sfdx-iq --plugin claude-sfdx-iqThis skill uses the workspace's default tool permissions.
Designs and optimizes AI agent action spaces, tool definitions, observation formats, error recovery, and context for higher task completion rates.
Salesforce security operates on multiple layers: object-level (CRUD), field-level (FLS), record-level (sharing), and feature-level (permissions). Every Apex class must explicitly enforce the appropriate security checks.
Throws an exception if the user lacks read access to any field or object in the query.
List<Account> accounts = [
SELECT Id, Name, AnnualRevenue, Custom_Field__c
FROM Account
WHERE Status__c = 'Active'
WITH SECURITY_ENFORCED
];
Behavior:
System.QueryException if any check failsWhen to use: Standard queries where you want strict enforcement and can handle the exception.
Introduced in Spring 2023. Enforces CRUD, FLS, and sharing rules. Unlike SECURITY_ENFORCED, it strips inaccessible fields instead of throwing.
List<Account> accounts = [
SELECT Id, Name, AnnualRevenue, Custom_Field__c
FROM Account
WHERE Status__c = 'Active'
WITH USER_MODE
];
Behavior:
// DML with USER_MODE
Database.insert(accounts, AccessLevel.USER_MODE);
Database.update(accounts, AccessLevel.USER_MODE);
When to use: Preferred for most user-facing queries. Graceful handling of mixed field access.
Bypasses all security checks. Use only for system operations where the code must access all data regardless of user permissions.
// Only in without sharing classes for system operations
List<Account> accounts = [
SELECT Id, Name FROM Account
WITH SYSTEM_MODE
];
When to use: Background jobs, system integrations, and administrative operations where user context is irrelevant.
Programmatically removes inaccessible fields from SObject records. Useful when you need to check before DML or serialize for API responses.
// Strip fields the user cannot read
List<Account> accounts = [SELECT Id, Name, AnnualRevenue, Secret_Field__c FROM Account];
SObjectAccessDecision decision = Security.stripInaccessible(AccessType.READABLE, accounts);
List<Account> sanitized = (List<Account>) decision.getRecords();
// Secret_Field__c is null if user lacks FLS read access
// Strip before update (remove fields user cannot edit)
SObjectAccessDecision updateDecision = Security.stripInaccessible(AccessType.UPDATABLE, accountsToUpdate);
update updateDecision.getRecords();
// Check which fields were removed
Set<String> removedFields = decision.getRemovedFields().get('Account');
AccessType values:
READABLE -- check FLS readCREATABLE -- check FLS createUPDATABLE -- check FLS updateUPSERTABLE -- check FLS create and updateWhen to use:
Enforces the current user's record-level sharing rules (OWD, sharing rules, role hierarchy, manual shares).
public with sharing class AccountService {
// Queries and DML respect the running user's sharing rules
public List<Account> getMyAccounts() {
return [SELECT Id, Name FROM Account]; // Only returns records user can see
}
}
Rule: Use with sharing by default for all classes unless there is a specific, documented reason not to.
Bypasses record-level sharing. The class can access all records regardless of OWD, sharing rules, or role hierarchy.
public without sharing class SystemAccountService {
// Can see ALL accounts regardless of sharing rules
public List<Account> getAllAccounts() {
return [SELECT Id, Name FROM Account];
}
}
When to use:
Inherits the sharing context from the calling class. If called from with sharing, it runs with sharing. If called from without sharing, it runs without.
public inherited sharing class AccountsSelector {
// Sharing depends on who calls this selector
public List<Account> selectById(Set<Id> ids) {
return [SELECT Id, Name FROM Account WHERE Id IN :ids];
}
}
When to use:
| Scenario | Keyword | Reason |
|---|---|---|
| User-facing Service | with sharing | Respect user's data access |
| LWC/Aura Controller | with sharing | UI respects sharing |
| REST API endpoint | with sharing | API caller sees only their data |
| Selector / Utility | inherited sharing | Inherit caller's context |
| Batch Apex | without sharing (usually) | System operation on all records |
| Platform Event handler | without sharing | No user context |
| System integration | without sharing | Must access all data |
| Trigger handler | with sharing | Default; override only if justified |
public class CrudChecker {
public static void checkCreateable(SObjectType sObjectType) {
if (!sObjectType.getDescribe().isCreateable()) {
throw new SecurityException('No create access on ' + sObjectType);
}
}
public static void checkReadable(SObjectType sObjectType) {
if (!sObjectType.getDescribe().isAccessible()) {
throw new SecurityException('No read access on ' + sObjectType);
}
}
public static void checkUpdateable(SObjectType sObjectType) {
if (!sObjectType.getDescribe().isUpdateable()) {
throw new SecurityException('No update access on ' + sObjectType);
}
}
public static void checkDeletable(SObjectType sObjectType) {
if (!sObjectType.getDescribe().isDeletable()) {
throw new SecurityException('No delete access on ' + sObjectType);
}
}
}
public static void checkFieldReadable(SObjectField field) {
if (!field.getDescribe().isAccessible()) {
throw new SecurityException('No read access on field: ' + field);
}
}
public static void checkFieldCreateable(SObjectField field) {
if (!field.getDescribe().isCreateable()) {
throw new SecurityException('No create access on field: ' + field);
}
}
Use Custom Permissions for feature-gating. Check with FeatureManagement.checkPermission().
public class FeatureGate {
public static Boolean canAccessAdvancedReporting() {
return FeatureManagement.checkPermission('Advanced_Reporting');
}
public static void requirePermission(String permissionName) {
if (!FeatureManagement.checkPermission(permissionName)) {
throw new InsufficientAccessException(
'Missing custom permission: ' + permissionName
);
}
}
}
// Usage
public class ReportService {
public static List<Report__c> getAdvancedReports() {
FeatureGate.requirePermission('Advanced_Reporting');
return [SELECT Id, Name FROM Report__c WITH SECURITY_ENFORCED];
}
}
Custom Permissions are assigned via Permission Sets. This is the recommended approach over profile-based checks.
When building integrations, use Named Credentials instead of storing credentials in Custom Settings or code.
// BAD -- credentials in code or custom settings
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.example.com/data');
req.setHeader('Authorization', 'Bearer ' + Custom_Setting__c.getInstance().API_Key__c);
// GOOD -- Named Credential manages auth
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:My_External_API/data');
req.setMethod('GET');
// Authentication header is added automatically
Named Credential benefits:
Never concatenate user input into SOQL strings. Use bind variables or String.escapeSingleQuotes().
// BAD -- SOQL injection vulnerable
String query = 'SELECT Id FROM Account WHERE Name = \'' + userInput + '\'';
List<Account> accounts = Database.query(query);
// GOOD -- bind variable (preferred)
String accountName = userInput;
List<Account> accounts = [SELECT Id FROM Account WHERE Name = :accountName];
// GOOD -- escaped dynamic SOQL (when dynamic query is unavoidable)
String safeName = String.escapeSingleQuotes(userInput);
String query = 'SELECT Id FROM Account WHERE Name = \'' + safeName + '\'';
List<Account> accounts = Database.query(query);
Before deploying code to production, verify:
with sharing, without sharing, or inherited sharing)without sharing classes have a comment justifying the elevated accessWITH SECURITY_ENFORCED or WITH USER_MODEescapeSingleQuotes)stripInaccessible() used before returning data to LWC/APISeeAllData=true in testswithout sharing in some contexts (ambiguous behavior).