Salesforce license compliance

Verifying User access to Tabs and Objects

Part 1: the Objects

Some licenses in a Salesforce org are restricted in term of objects and tabs that a user can access. Salesforce does not provide any tool to check the configuration against that limit, and the limit is not enforced.
If your org is in a situation where you are above this contractual engagement, you could get a fine.
To verify the situation, it is much more complicate than verifying profile definitions. You now have permissions (permission groups, permission sets...) that can create different situations for users having the same profile.
The most exhaustive situation is to check by user. If you have lots of users in your org, it will be quite complicate to verify everything. That's why we need to automate the analysis
First, you have to understand Salesforce datamodel that is granting permissions to a user: Understanding Access and Permissions PermissionSetAssignments Assignments Assignments PermissionSetGroupComponents User UserName ProfileId ... PermissionSetAssignment AssigneeId PermissionSetGroupId PermissionSetId PermissionSetGroup MasterLabel DeveloperName Description Language (fr, en_US, ja...) Status (Updated, Outdated, Updating, Failed) PermissionSetGroupComponent PermissionSetGroupId PermissionSetId PermissionSet PermissionSetGroupId Name Label LicenseId ProfileId Description PermissionXxx... SessionPermSetActivation AuthSessionId PermissionSetId UserId Description SessionActivations FieldPermissions ParentId SobjectType Field PermissionsEdit PermissionsRead ObjectPermissions ParentId SobjectType PermissionsCreate PermissionsDelete PermissionsEdit PermissionsModifyAllRecords PermissionsRead PermissionsViewAllRecords PermissionSetTabSetting ParentId Name Visibility SetupEntityAccess ParentId SetupEntityId SetupEntityType (ApexClass,APexPage...) SetupEntityAccessItems ObjectPerms FieldPerms PermissionSetGroupComponents SessionPermSetActivations CustomPermission DeveloperName Language MasterLabel NamespacePrefix Description GrantedByLicense CustomPermissionId PermissionSetLicenseId PermissionSetLicense DeveloperName ExpirationDate Language MasterLabel MaximumXxxx... Status TotalLicenses USedLicenses PermissionSetLicenseAssign AssigneeId PermissionSetLicenseId Assigneee Profile Name PermissionsXxx... UserLicenseId UserType (Standard, Guest...) UserLicense LicenseDefinitionKey MasterLabel MonthlyLoginsEntitlement MonthlyLoginsUsed Name Status (Active, Disabled) TotalLicenses UsedLicenses CustomObjectUserLicenseMetrics CustomObjectId CustomObjectName CustomObjectType MetricsDate ObjectCount USerLicenseId Users CustomPermissionDependency CustomPermissionId RequiredCustomPermissionId MutingPermissionSet DeveloperName MasterLabel Description PermissionXxx... PortalDelegablePermissionSet PermissionSetId ProfileId UserPermissionAccessPermissionXxx... UserSetupEntityAccess DeveloperName DurableId KeyPrefix LastCacheUpdate NamespacePrefix SetupEntityId
As you can see, it is quite complex. To identify objects that can be accessed by a user, you have to go through PermissionSets and use the relationships Assignments and ObjectPerms.
On the ObjectPermissions, we will need the SobjectType. We don't need to check if it is Read, Delete, or any other access level, as long as a user can access an object there will be a record in this relationship.
On the Assignments, we will need to retrieve the UserName through a cross object relationship (Assignee.UserName).
We will filter some licenses where it is not meaningful: Guest User, Identity
Any code or query below will need to be run with Administrator rights, or at least with permissions to access all objects
The first query to get Object permissions for users:
SELECT	Name
	,Label
	,Profile.Name
	,License.Name
	,IsCustom
	,Description
	,NamespacePrefix
	,Type
	,(	SELECT	AssigneeId
			,Assignee.Name
		FROM	Assignments ) 
	,(	SELECT	SobjectType
		FROM	ObjectPerms ) 
FROM PermissionSet
WHERE (NOT License.Name IN( 'Guest', 'Guest User License', 'Identity' ))

We will use this query in Apex Code, and we will filter to discard results when there is no Assignments, and results without ObjectPerms too. As Apex code cannot manage a huge cardinality (lots of users with lots of objects) we will later adapt the logic.
We have to create a Map to associate UserIds (ASsigneeId) to a list of Custom Objects (identified by the API Name that is ending with __c)
The list of Custom Objects has to be filtered to exclude Custom Objects comming from certified managed package. As there is no way in Apex or SOQL to identify if a package is certified ot not, we will consider that all installed packages are certified.
In Apex, we will consider a custom list of packages (that should be configured by you according to your org) that are not certified, counting against the limits. You have to enter the NamespacePrefix in the Apex definition of the list. Example:
String[] countingPackages=new String[]{'SmartObs'};

To help you identify the NamespacePrefixes, you can consider this query
SELECT NamespacePrefix FROM PackageLicense GROUP BY NamespacePrefix

We will list the objects that are either without Namespace or listed in the countingPackages list. For this purpose we construct a list of safe packages
String[] countingPackages=new String[]{'SmartObs'};
String[] safePackages=new String[]{};
for(AggregateResult p:[SELECT NamespacePrefix
			FROM PackageLicense
			WHERE NamespacePrefix NOT IN :countingPackages GROUP BY NamespacePrefix]){
	safePackages.add((String)p.get('NamespacePrefix'));
}
system.debug(safePackages);
Now we can retrieve a filtered list of Sobjects
String[] objList=new String[]{};
for(EntityDefinition entity:[SELECT QualifiedApiName
	FROM EntityDefinition
	WHERE IsCustomizable=true
		AND QualifiedApiName LIKE '%__c'
		AND (NamespacePrefix NOT IN :safePackages)]){
	objList.add(entity.QualifiedApiName);
}
We have to prepare a Map to store objects assigned to users
Map<Id,String[]> psetObjs=new Map<Id,String[]>();
for(PermissionSet ps:[SELECT	Id
		,(	SELECT	AssigneeId, Assignee.Name
			FROM	Assignments limit 1)
		,(	SELECT	SobjectType
			FROM	ObjectPerms where SobjectType in :objList)
	FROM PermissionSet
	WHERE (not License.Name IN( 'Guest','Guest User License','Identity' ))])
		if(!ps.ObjectPerms.isEmpty())if(!ps.Assignments.isEmpty()){
			String[] objs=new String[]{};
			for(ObjectPermissions op:ps.ObjectPerms)objs.add(op.SobjectType);
			psetObjs.put(ps.Id,objs);
		}
and we need to add the list of objects to the users
Map<String,Set<String>> usrObjs=new Map<String,Set<String>>();
for(PermissionSetAssignment psa:[SELECT Assignee.UserName,PermissionSetId
				FROM PermissionSetAssignment
				WHERE PermissionSetId IN :psetObjs.keySet()
					AND Assignee.IsActive=true]){
	if(!usrObjs.containsKey(psa.Assignee.UserName)){
		usrObjs.put(psa.Assignee.UserName,new Set<String>(psetObjs.get(psa.PermissionSetId)));
	}else usrObjs.get(psa.Assignee.UserName).addAll(new Set<String>(psetObjs.get(psa.PermissionSetId)));
}
Now we can count objects per user and display a resulting summary grouping the quantity of objects
Map<Integer,Integer> objCountUserCount=new Map<Integer,Integer>();
for(String s:usrObjs.keySet()){//for each user
	Integer count=usrObjs.get(s).size();
	if(!objCountUserCount.containsKey(count)){
		objCountUserCount.put(count,1);
	}else objCountUserCount.put(count,objCountUserCount.get(count)+1);
}

for(Integer i:objCountUserCount.keySet()){
	system.debug(objCountUserCount.get(i) + ' users have access to '+i+' objects');
}
Here is the final complete code
/*apex*/
//(c) JLA, released under GNU GPLv3 License

//Note : active users that have no Custom Object assigned will not be listed.
String[] countingPackages=new String[]{'SmartObs'};
String[] safePackages=new String[]{};
for(AggregateResult p:[SELECT NamespacePrefix
			FROM PackageLicense
			WHERE NamespacePrefix NOT IN :countingPackages GROUP BY NamespacePrefix]){
	safePackages.add((String)p.get('NamespacePrefix'));
}
String[] objList=new String[]{};
for(EntityDefinition entity:[SELECT QualifiedApiName
	FROM EntityDefinition
	WHERE IsCustomizable=true
		AND QualifiedApiName LIKE '%__c'
		AND (NamespacePrefix NOT IN :safePackages)]){
	objList.add(entity.QualifiedApiName);
}

//Map a list of objects per pset
Map<Id,String[]> psetObjs=new Map<Id,String[]>();
for(PermissionSet ps:[SELECT	Id
		,(	SELECT	AssigneeId, Assignee.Name
			FROM	Assignments limit 1)
		,(	SELECT	SobjectType
			FROM	ObjectPerms where SobjectType in :objList)
	FROM PermissionSet
	WHERE (not License.Name IN( 'Guest','Guest User License','Identity' ))])
		if(!ps.ObjectPerms.isEmpty())if(!ps.Assignments.isEmpty()){
			String[] objs=new String[]{};
			for(ObjectPermissions op:ps.ObjectPerms)objs.add(op.SobjectType);
			psetObjs.put(ps.Id,objs);
		}

//get a list of objects accessible per user
Map<String,Set<String>> usrObjs=new Map<String,Set<String>>();
for(PermissionSetAssignment psa:[SELECT Assignee.UserName,PermissionSetId
				FROM PermissionSetAssignment
				WHERE PermissionSetId IN :psetObjs.keySet()
					AND Assignee.IsActive=true]){
	if(!usrObjs.containsKey(psa.Assignee.UserName)){
		usrObjs.put(psa.Assignee.UserName,new Set<String>(psetObjs.get(psa.PermissionSetId)));
	}else usrObjs.get(psa.Assignee.UserName).addAll(new Set<String>(psetObjs.get(psa.PermissionSetId)));
}

system.debug(usrObjs.size()+' users found');

//Group users per quantity of accessible objects
Map<Integer,Integer> objCountUserCount=new Map<Integer,Integer>();
for(String s:usrObjs.keySet()){//for each user
	Integer count=usrObjs.get(s).size();
	if(!objCountUserCount.containsKey(count)){
		objCountUserCount.put(count,1);
	}else objCountUserCount.put(count,objCountUserCount.get(count)+1);
}

for(Integer i:objCountUserCount.keySet()){
	system.debug(objCountUserCount.get(i) + ' users have access to '+i+' objects');
}

//Optionnally list accessible objects per user
for(String s:usrObjs.keySet()){//for each user
	system.debug(s+'|'+usrObjs.get(s));
}

Part 2: the Tabs

To identify the list of the tabs that a user can access, we will have the same principle, but there is a difficulty because the object PermissionSetTabSetting is a child object of PermissionSet, without a relationship name.
We have to exclude standard tabs, and custom tabs contained in managed packages
To get a list of filtered Tabs, we will run this query and remove all entries where Name is starting with a NamespacePrefix
SELECT Name,Label,Url FROM TabDefinition WHERE IsCustom=true
We will use this result to query the PermissionSetTabSetting object, getting links between permissions and Tabs
String[] prefixes=new String[]{};
for(AggregateResult p:[SELECT NamespacePrefix
			FROM PackageLicense
			GROUP BY NamespacePrefix]){
	prefixes.add((String)p.get('NamespacePrefix')+'__');
}
String[] lstTabs=new String[]{};
for(TabDefinition t:[SELECT Name FROM TabDefinition WHERE IsCustom=true]){
	Boolean found=false;
	for(String s:prefixes)if(t.Name.startsWith(s)){
		found=true;
		break;
	}
	if(!found)lstTabs.add(t.Name);
}
system.debug(lstTabs);
Map<Id,String[]>permTab=new Map<Id,String[]>();
for(PermissionSetTabSetting p:[SELECT ParentId,Name FROM PermissionSetTabSetting WHERE Name in :lstTabs]){
	if(permTab.containsKey(p.ParentId)){
		permTab.get(p.ParentId).add(p.Name);
	}else permTab.put(p.ParentId,new String[]{p.Name});
}
system.debug(permTab);
Now that we have the list of Tabs associated to Permissions, we just need to figure out users that have been granted these permissions
//get a list of tabs accessible per user
Map<String,Set<String>> usrTabs=new Map<String,Set<String>>();
for(PermissionSetAssignment psa:[SELECT Assignee.UserName,PermissionSetId
				FROM PermissionSetAssignment
				WHERE PermissionSetId IN :permTab.keySet()
					AND Assignee.IsActive=true]){
	if(!usrTabs.containsKey(psa.Assignee.UserName)){
		usrTabs.put(psa.Assignee.UserName,new Set<String>(permTab.get(psa.PermissionSetId)));
	}else usrTabs.get(psa.Assignee.UserName).addAll(new Set<String>(permTab.get(psa.PermissionSetId)));
}
system.debug(usrTabs.size()+' users found');
Now we can group Users per quantity of Tabs that have been granted to them
//Group users per quantity of accessible tabs
Map<Integer,Integer> tabCountUserCount=new Map<Integer,Integer>();
for(String s:usrTabs.keySet()){//for each user
	Integer count=usrTabs.get(s).size();
	if(!tabCountUserCount.containsKey(count)){
		tabCountUserCount.put(count,1);
	}else tabCountUserCount.put(count,tabCountUserCount.get(count)+1);
}

for(Integer i:tabCountUserCount.keySet()){
	system.debug(tabCountUserCount.get(i) + ' users have access to '+i+' tabs');
}
And now you have the full code that will list the quantity of users per quantity of assigned tabs, and optionally per user the list of the custom tabs that have been granted
//(c) JLA, released under GNU GPLv3 License

String[] prefixes=new String[]{};
for(AggregateResult p:[SELECT NamespacePrefix
			FROM PackageLicense
			GROUP BY NamespacePrefix]){
	prefixes.add((String)p.get('NamespacePrefix')+'__');
}
String[] lstTabs=new String[]{};
for(TabDefinition t:[SELECT Name FROM TabDefinition WHERE IsCustom=true]){
	Boolean found=false;
	for(String s:prefixes)if(t.Name.startsWith(s)){
		found=true;
		break;
	}
	if(!found)lstTabs.add(t.Name);
}
system.debug(lstTabs);
Map<Id,String[]>permTab=new Map<Id,String[]>();
for(PermissionSetTabSetting p:[SELECT ParentId,Name FROM PermissionSetTabSetting WHERE Name in :lstTabs]){
	if(permTab.containsKey(p.ParentId)){
		permTab.get(p.ParentId).add(p.Name);
	}else permTab.put(p.ParentId,new String[]{p.Name});
}
system.debug(permTab);

//get a list of tabs accessible per user
Map<String,Set<String>> usrTabs=new Map<String,Set<String>>();
for(PermissionSetAssignment psa:[SELECT Assignee.UserName,PermissionSetId
				FROM PermissionSetAssignment
				WHERE PermissionSetId IN :permTab.keySet()
					AND Assignee.IsActive=true]){
	if(!usrTabs.containsKey(psa.Assignee.UserName)){
		usrTabs.put(psa.Assignee.UserName,new Set<String>(permTab.get(psa.PermissionSetId)));
	}else usrTabs.get(psa.Assignee.UserName).addAll(new Set<String>(permTab.get(psa.PermissionSetId)));
}
system.debug(usrTabs.size()+' users found');

//Group users per quantity of accessible tabs
Map<Integer,Integer> tabCountUserCount=new Map<Integer,Integer>();
for(String s:usrTabs.keySet()){//for each user
	Integer count=usrTabs.get(s).size();
	if(!tabCountUserCount.containsKey(count)){
		tabCountUserCount.put(count,1);
	}else tabCountUserCount.put(count,tabCountUserCount.get(count)+1);
}

for(Integer i:tabCountUserCount.keySet()){
	system.debug(tabCountUserCount.get(i) + ' users have access to '+i+' tabs');
}

//Optionnally list accessible objects per user
for(String s:usrTabs.keySet()){//for each user
	system.debug(s+'|'+usrTabs.get(s));
}
This site uses cookies. Find out more.