Exporting Salesforce users and their license types is not a Salesforce user access review. A Platform license and an unused Salesforce license tell the auditor almost nothing about who can view every Opportunity, export reports, or manage Connected Apps. Effective access is the union of the profile, permission sets, permission set groups, public groups, queues, and the occasional Mutual TLS or delegated-admin grant. That union is what we certify.
GRC platforms will cheerfully show you "Salesforce is connected" and a list of active users. The review still fails when the sample asks "show me what this admin could do" and the evidence is a license SKU. We build the effective-access extract and the reviewer workflow around that extract.
Salesforce User Access Review Is Not a License Report
Salesforce licenses gate which user record can exist. They do not describe object, field, or system permissions. Two users on the same Salesforce license can be a read-only seller and a Modify All Data administrator. Seat count is useful for employee offboarding reclaim. It is the wrong grain for SOC 2 access review sampling.
NIST AC-2 / AC-6 language is about accounts and least privilege, not vendor SKUs. The TSC CC6 family is the same idea: access authorized, access reviewed, access removed. Reviewers need a row they can approve or revoke.
How We Compute Effective Access
Salesforce stores the pieces in different objects. We pull them and fold:
- Profile — the base; still where a lot of orgs hide Modify All Data.
- PermissionSetAssignment — direct permission sets.
- Permission set groups — the bundle reviewers forget is a bundle.
- GroupMember — public groups and queues that drive sharing, not just UX.
- PermissionSet / SetupEntityAccess / ObjectPermissions / FieldPermissions — the actual rights.
- User.lastLoginDate and LoginHistory — usage, so unused admin is visible.
def effective_salesforce_access(user_id):
profile = soql.profile_for(user_id)
sets = soql.permission_sets_for(user_id) # includes group expansion
groups = soql.public_groups_for(user_id)
rights = merge_rights(profile, sets)
return {
"user_id": user_id,
"username": profile.username,
"license": profile.license, # context only
"profile": profile.name,
"permission_sets": [s.name for s in sets],
"groups": [g.name for g in groups],
"system": rights.system_flags, # ModifyAllData, ViewAllData, ManageUsers...
"objects": rights.object_matrix, # CRUD per object, not 400 rows dumped
"last_login": profile.last_login,
"unused_privileged": unused(rights, profile.last_login),
}
The object matrix is summarized. Reviewers cannot certify 400 object rows. We highlight privileged flags and objects tagged in-scope for the audit (Account, Opportunity, Case, custom objects that hold customer data). Everything else is available as a drill-down, not the default packet.
Permission set groups expand before merge. If we only list the group name, a reviewer will approve a bundle they have never opened. We show the group and the sets inside it.
What Reviewers Actually Certify
Each campaign produces a queue:
- Privileged users — Manage Users, Modify All Data, View All Data, author Apex, manage Connected Apps. These get a named reviewer (usually the Salesforce owner, not "any manager").
- In-scope object access that is write or view-all, with last login.
- Orphaned assignments — permission sets granted to inactive users, or users whose HRIS department no longer matches the set's owner.
- Revoke candidates from usage-vs-grant, the same idea as privilege creep detection.
The decision is accept or revoke per assignment, not "looks fine" on the whole org. Revoke writes back through the API we already use for JML: remove the permission set, or open a break-glass ticket if the profile itself is the problem (profiles are slower to change; we do not silently swap a profile on a seller mid-quarter without a change window).
Integrations and Connected Apps Belong in the Same Review
Human users are half the Salesforce identity story. Connected Apps, integration users, and OAuth grants are the other half. An integration user on a full Salesforce license with Modify All Data is a standing credential with a UI. We list those users separately, require an owner, and refuse to treat "it's a service account" as a pass.
OAuth app grants that sit outside Salesforce proper are covered in OAuth app access review. Inside Salesforce, we still dump Connected App authorized users and the scopes they hold. A user access review that ignores those grants is a UI review.
Evidence That Survives a Sample
The auditor will pick users. For each we need:
- Snapshot timestamp (see evidence freshness tracking — a screenshot from the start of the window is not a Type 2 sample)
- Effective-access record as computed above
- Reviewer identity, decision, and time
- Follow-up revoke timestamp if the decision was revoke
A CSV of Username, Profile, License is the artifact we replace. Running this review does not issue a SOC 2 report. We produce the CC6.2 evidence the sample actually uses, the same class of work as user access review automation pointed at this vendor's entitlement model.
This extract is custom engineering on top of whatever GRC connector you already turned on. The connector proves the app exists. The fold proves who could do what.
Salesforce user access review is part of security automation. When the same org also needs lifecycle events, it hangs off the joiner mover leaver map so new permission sets are birthright or exception, not tribal knowledge.
See what an Access & Credential Governance Diagnostic finds in your environment: rutagon.com/contact · 907-841-8407 · contact@rutagon.com.
Frequently Asked Questions
Why isn't a Salesforce license report enough for an access review?
Licenses only say a user is allowed to have an account of a certain type. They do not list object CRUD, system permissions, or group-driven sharing. Two users on one license SKU can have opposite privilege.
Do we have to review every object permission for every user?
No. We review privileged system flags and in-scope objects by default, with drill-down available. A 400-column object dump is how reviews get rubber-stamped.
How do permission set groups change the review?
They hide sets inside a bundle. We expand the group before the reviewer sees the row, so approving the group is approving the listed sets, not a friendly name.
What about Salesforce users that are really integration accounts?
They stay in the population, with an owner and a tighter question: is this credential still used, and does it still need Modify All Data? "Service account" is not an exemption from CC6.
Can our GRC platform's Salesforce integration replace this?
It can list users and sometimes profiles. It rarely computes effective access across permission set groups and sharing groups, and it does not write revokes back. We sit behind the platform you already run and close that fold.