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 FLS/CRUD (use Security.stripInaccessible or `with user_mode`), sharing rules, or external user license logic.
Best use case
apex-user-and-permission-checks is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
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 FLS/CRUD (use Security.stripInaccessible or `with user_mode`), sharing rules, or external user license logic.
Teams using apex-user-and-permission-checks should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/apex-user-and-permission-checks/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How apex-user-and-permission-checks Compares
| Feature / Agent | apex-user-and-permission-checks | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
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 FLS/CRUD (use Security.stripInaccessible or `with user_mode`), sharing rules, or external user license logic.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# 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
- What are you actually gating — UI behavior only, or server-side authorization? Server-side checks must be enforced with `stripInaccessible` / `user_mode` regardless of permission flags.
- Does a **Custom Permission** exist for this concept? If not, create one — it's the supported extensibility point.
- Is the check supposed to ignore admins (e.g., "even admins can't do this") or honor them (most permissions do)? Custom Permissions respect "Modify All Data."
- Does this run in a Queueable, Batch, or `@future`? The running user is the async context user, not the originating user.
---
## 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:**
```apex
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:**
```apex
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:**
```apex
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
| Situation | Recommended Approach | Reason |
|---|---|---|
| Gate a feature | Custom Permission + `FeatureManagement.checkPermission` | Admin-manageable, rename-safe |
| Check user identity | `UserInfo` accessors | They are free and cached |
| Check permission for async originator | Query `PermissionSetAssignment` / `SetupEntityAccess` | `checkPermission` uses running user |
| Distinguish internal vs community | `UserInfo.getUserType()` | License-aware and stable |
| Check Modify All Data | `FeatureManagement.checkPermission('ModifyAllData')` (built-in) or `UserInfo.isMultiCurrencyOrganization` equivalent | Avoid Profile-name checks |
| Test the check in unit tests | `System.runAs(testUser)` with proper permissions assigned | Running as System.runAs with Admin masks bugs |
---
## Recommended Workflow
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
- [ ] No `Profile.Name == 'Something'` checks in security-sensitive code.
- [ ] Custom Permissions exist and have descriptive DeveloperNames.
- [ ] `FeatureManagement.checkPermission` is called only for the running user; async jobs pass the originator Id explicitly.
- [ ] Server-side authorization is paired with FLS/CRUD enforcement.
- [ ] Tests cover both the allowed and denied paths with `System.runAs`.
- [ ] Permission assignments are documented in the feature's admin guide.
---
## 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
| Artifact | Description |
|---|---|
| `references/examples.md` | Custom permission gating, cross-user lookup, UserType branching |
| `references/gotchas.md` | Async context, profile rename, typo silent-false |
| `references/llm-anti-patterns.md` | Common LLM mistakes: profile-name checks, running-user assumption |
| `references/well-architected.md` | Security framing |
| `scripts/check_apex_user_and_permission_checks.py` | Stdlib lint for profile-name gating and cached permission results |
---
## Related Skills
- **apex-security-patterns** — FLS/CRUD enforcement alongside permission gating
- **apex-async-architecture** — user-context switches in async work
- **apex-callable-interface** — permission-sensitive dynamic invocationRelated Skills
permission-set-groups-and-muting
Use when designing or reviewing permission-set-group architecture, especially profile minimization, group composition, muting strategy, and migration away from profile-heavy security models. Triggers: 'permission set group', 'muting permission set', 'profiles to permission sets', 'PSG architecture', 'muted permissions'. NOT for record-sharing design or CRUD/FLS review in Apex code.
guest-user-security
Use when hardening the Experience Cloud guest user profile, controlling unauthenticated access to records and Apex, or investigating data exposure through guest SOQL. Covers object permissions, sharing model enforcement for unauthenticated users, and Apex execution context. NOT for Experience Cloud site creation (use Experience Cloud skills) or for authenticated external user security (use security/experience-cloud-security).
guest-user-security-audit
Auditing the security posture of an Experience Cloud (Community) site's Guest User. Covers the post-Spring '21 secure-by-default lockdown (object permissions removed, sharing rule grants required for any access), the Guest User profile permissions to remove (View All Data, Modify All Data, Manage Users, etc.), guest sharing rules, the Run-As-Guest test, OWASP A01 (Broken Access Control) mapping, and the standard set of leakage vectors (Apex with `without sharing`, Aura / LWC `@AuraEnabled` methods, public-site Visualforce, REST endpoints under `/services/apexrest`). NOT for Experience Cloud authenticated user setup (see experience/experience-cloud-user-management), NOT for general Salesforce profile design (see admin/profile-permset-design).
api-only-user-hardening
Provision and harden integration (API-only) users: no UI login, IP restrictions, minimum permission set, session lifetime, and monitoring. NOT for human admin account hardening.
apex-managed-sharing-patterns
Grant row-level access programmatically via __Share records when declarative sharing rules cannot express the policy. NOT for OWD, role hierarchy, or criteria-based sharing rule design.
lwc-imperative-apex
Call Apex methods imperatively from LWC — on button click, lifecycle hooks, or conditional logic. Covers import syntax, cacheable vs non-cacheable, async/await patterns, error handling, loading states, and Promise.all. NOT for wire service (use wire-service-patterns) and NOT for testing Apex mocks (use lwc-testing).
dataweave-for-apex
Use when transforming structured data inside Apex — CSV → JSON, XML → SObject list, JSON → flattened CSV, or schema-mapping a third-party payload to a Salesforce model — and the existing options (`JSON.deserialize`, `Dom.Document`, hand-written loops) are getting unwieldy. Triggers: 'apex transform csv json xml without external library', 'system.dataweave script', 'salesforce native dataweave apex execute', 'transform xml to sobject apex no mulesoft', 'json reshape salesforce apex script'. NOT for MuleSoft Anypoint DataWeave running off-platform (use mulesoft-anypoint-architecture), NOT for Apex JSON serialization basics (use apex-json-serialization), NOT for Bulk API CSV ingest (use bulk-api-2-patterns).
flow-invocable-from-apex
Author @InvocableMethod Apex classes that Flow can call as Actions. Design the input / output variable contract, bulk semantics (one list in, one list out), null handling, and error surfacing. Also covers the inverse direction: calling a flow from Apex via Flow.Interview. NOT for general Apex authoring (use apex-service-selector-domain). NOT for REST-exposed Apex (use apex-rest-resource-patterns).
flow-apex-defined-types
Design and use Apex-Defined Types as Flow variables for structured non-sObject data (HTTP callout payloads, External Service responses, complex configuration). Trigger keywords: apex-defined type, flow variable, @AuraEnabled class, flow http callout response. Does NOT cover building HTTP Callout Actions themselves, External Services schema, or raw Apex invocable methods.
permission-set-deployment-ordering
Use when deploying permission sets, permission set groups, or profiles and encountering cross-reference errors, silent permission loss, or ordering failures. Triggers: 'permission set deployment fails', 'cross-reference id error during deploy', 'permissions disappear after deployment', 'permission set group deployment error'. NOT for permission set design or architecture decisions (use permission-set-architecture), NOT for creating permission sets from scratch (use admin/permission-set-architecture).
external-user-data-sharing
Configure record visibility for external users (Customer Community, Customer Community Plus, Partner Community) using External OWDs, Sharing Sets, and external sharing rules. Trigger keywords: sharing data with external users, portal user record visibility, Experience Cloud sharing model, sharing set configuration, external OWD setup, Customer Community data access, High-Volume Portal sharing. NOT for internal sharing model configuration. NOT for internal user roles and hierarchies. NOT for guest user profile hardening.
community-user-data-migration
Use this skill to migrate external community/portal user accounts at scale: bulk creating Experience Cloud users via Data Loader, migrating users between license types (Customer Community to Customer Community Plus, or to Partner Community), importing Customer Portal users into Experience Cloud, and resolving Contact/Account hierarchy prerequisites. Trigger keywords: migrate community users, import external users Experience Cloud, bulk create portal users, move users between license types, migrate Customer Community to Partner Community. NOT for internal user data migration. NOT for general data migration (see data/bulk-data-migration). NOT for configuring Experience Cloud sites or profiles from scratch.