SfSkills Domains Docs GitHub

Home / apex / apex-user-and-permission-checks

apex-user-and-permission-checks

Use when Apex needs to check what the running user is, can see, or can do — via UserInfo, FeatureManagement, FeatureManagement.checkPermission, or FeatureManagement.checkPermissionType. Covers custom permissions, permission sets, user licenses, and profile checks. NOT for CRUD/FLS enforcement — use apex/apex-stripinaccessible-and-fls-enforcement. NOT for creating or assigning custom permissions in Setup — use admin/custom-permissions.

Open the skill folder on GitHub


Apex User And Permission Checks

Activates when Apex needs to branch on who the running user is or what they are allowed to do. Produces correct FeatureManagement.checkPermission usage, safe identity reads, and guidance to use custom permissions over profile-name checks.


Before Starting


Core Concepts

Prefer Custom Permissions Over Profile / Permission-Set Name Checks

The supported way to gate a feature in Apex is a Custom Permission. In Setup create Perform_Bulk_Refund, assign it to permission sets or profiles, and check with FeatureManagement.checkPermission('Perform_Bulk_Refund'). This returns true if the user has the permission via any assignment path.

Checking Profile.Name == 'Sales Manager' is brittle: the name can be renamed in production, cloned profiles drift, and this check misses permission-set-based grants.

UserInfo Gives Identity, Not Authorization

UserInfo.getUserId(), .getUserName(), .getProfileId(), and .getSessionId() report identity. Use them for logging and relationship lookups. Don't use UserInfo.getProfileId() to drive authorization — couple the gate to a permission the admin can grant.

Async Context Switches The User

Apex in @future, Queueable, Batch, Scheduled, and platform event triggers runs as the async context user (often the user who fired the async, but for scheduled jobs the scheduler, for platform events the "Automated Process" user). FeatureManagement.checkPermission then checks that user's permissions. If you need the originating user, pass their Id explicitly and look up permissions via a query (see patterns).

Custom Permission Lookup Paths

FeatureManagement.checkPermission('Name') checks if the running user has the custom permission via Profile or Permission Set assignment. Multi-permission checks need an AND/OR logic built in Apex.

There is no checkPermissionFor(userId, 'Name') built-in. For a user other than the running user, query the SetupEntityAccess / PermissionSetAssignment / CustomPermission graph yourself.


Common Patterns

Pattern 1: Gate A Feature On A Custom Permission

When to use: Any code path that should be available only to users an admin has blessed.

How it works:

public with sharing class BulkRefundService {
    public static void initiate(Set<Id> paymentIds) {
        if (!FeatureManagement.checkPermission('Perform_Bulk_Refund')) {
            throw new NoAccessException('You do not have permission to perform bulk refunds.');
        }
        // proceed
    }
}

Why not the alternative: Hardcoding Profile.Name == 'Finance Admin' misses permission-set assignments and breaks on rename.

Pattern 2: Check Custom Permission For A Different User

When to use: A Queueable running as the automated context needs to verify the originating user's permission.

How it works:

public class RefundQueueable implements Queueable {
    private final Id initiatingUserId;
    private final Set<Id> paymentIds;

    public RefundQueueable(Id initiatingUserId, Set<Id> paymentIds) {
        this.initiatingUserId = initiatingUserId;
        this.paymentIds = paymentIds;
    }

    public void execute(QueueableContext ctx) {
        if (!hasPermission(initiatingUserId, 'Perform_Bulk_Refund')) {
            throw new NoAccessException('Initiating user lacks bulk refund permission.');
        }
        // proceed
    }

    private static Boolean hasPermission(Id userId, String permApiName) {
        return ![
            SELECT Id FROM PermissionSetAssignment
            WHERE AssigneeId = :userId
            AND PermissionSet.PermissionsCustomizeApplication = false
            AND PermissionSetId IN (
                SELECT ParentId FROM SetupEntityAccess
                WHERE SetupEntityType = 'CustomPermission'
                AND SetupEntityId IN (
                    SELECT Id FROM CustomPermission WHERE DeveloperName = :permApiName
                )
            )
        ].isEmpty();
    }
}

Why not the alternative: FeatureManagement.checkPermission silently checks the running (async context) user, not the originating user.

Pattern 3: Distinguish Internal Vs Community Users

When to use: Code behaves differently for Experience Cloud users vs internal licenses.

How it works:

public with sharing class UserContextUtil {
    public static Boolean isInternal() {
        UserType t = UserInfo.getUserType();
        return t == UserType.Standard;
    }
}

Where UserType is an enum and Standard represents internal users. Partner, Customer Success, CspLitePortal and others represent external. Treating internal-only code paths as the default is safer than enumerating every external type.


Decision Guidance

SituationRecommended ApproachReason
Gate a featureCustom Permission + FeatureManagement.checkPermissionAdmin-manageable, rename-safe
Check user identityUserInfo accessorsThey are free and cached
Check permission for async originatorQuery PermissionSetAssignment / SetupEntityAccesscheckPermission uses running user
Distinguish internal vs communityUserInfo.getUserType()License-aware and stable
Check Modify All DataFeatureManagement.checkPermission('ModifyAllData') (built-in) or UserInfo.isMultiCurrencyOrganization equivalentAvoid Profile-name checks
Test the check in unit testsSystem.runAs(testUser) with proper permissions assignedRunning as System.runAs with Admin masks bugs

  1. Identify the concept being gated (e.g., "can initiate a refund"). Create a Custom Permission with a descriptive DeveloperName.
  2. Assign the Custom Permission to the relevant Permission Sets (prefer over profiles).
  3. In Apex, call FeatureManagement.checkPermission('<DeveloperName>').
  4. For server-side authorization, pair the check with WITH USER_MODE on SOQL and DML, or Security.stripInaccessible.
  5. Write tests under System.runAs(userWithPerm) and System.runAs(userWithoutPerm) to prove both paths.
  6. Avoid caching the result across transactions — permission assignments change.
  7. Document the permission in the feature's README so admins know what to grant.

Review Checklist


Salesforce-Specific Gotchas

See references/gotchas.md for the full list.

  1. Async context users differ from originators — checkPermission in a Queueable checks the async user.
  2. Custom Permissions respect "Modify All Data" — admins pass any permission check by default.
  3. Profile rename breaks hardcoded name checks — prefer Custom Permissions.
  4. System.runAs(admin) in tests masks permission bugs — test as real users.
  5. FeatureManagement.checkPermission returns false for undefined permissions without throwing — typos silently deny.

Output Artifacts

ArtifactDescription
references/examples.mdCustom permission gating, cross-user lookup, UserType branching
references/gotchas.mdAsync context, profile rename, typo silent-false
references/llm-anti-patterns.mdCommon LLM mistakes: profile-name checks, running-user assumption
references/well-architected.mdSecurity framing
scripts/check_apex_user_and_permission_checks.pyStdlib lint for profile-name gating and cached permission results

Package files