Last active
July 10, 2020 02:04
-
-
Save afawcett/bc482bfdc840d5ac2858 to your computer and use it in GitHub Desktop.
CustomPermissionsReader
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Copyright (c), Andrew Fawcett | |
* All rights reserved. | |
* | |
* Redistribution and use in source and binary forms, with or without modification, | |
* are permitted provided that the following conditions are met: | |
* | |
* - Redistributions of source code must retain the above copyright notice, | |
* this list of conditions and the following disclaimer. | |
* - Redistributions in binary form must reproduce the above copyright notice, | |
* this list of conditions and the following disclaimer in the documentation | |
* and/or other materials provided with the distribution. | |
* - Neither the name of the Andrew Fawcett, nor the names of its contributors | |
* may be used to endorse or promote products derived from this software without | |
* specific prior written permission. | |
* | |
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | |
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES | |
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL | |
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, | |
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS | |
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY | |
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
**/ | |
/** | |
* IMPORTANT UPDATE: | |
* Since API 41 (Winter'18) there is now a native way to read Custom Permissions. | |
* The following may still be useful if you have requirements not met by the native method. | |
* See Apex Developer Guide for FeatureManagement.checkPermission. | |
* https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_class_System_FeatureManagement.htm | |
**/ | |
/** | |
* This class is designed to help with caching the results of querying (via SOQL) Custom Permissions for the | |
* current user, it will load all defined Custom Permissions in one go for a given default or specified | |
* namespace (so not all defined in the org). This is done on the basis the caller will make 2 or more calls to | |
* the hasPermission method, thus benifiting from the bulkificaiton approach used. | |
* Note that the query to the database is demand loaded only on the first call to the hasPermission method | |
* thus constructing the object carries no SOQL / database overhead. | |
**/ | |
public virtual class CustomPermissionsReader { | |
private SObjectType managedObject; | |
private Set<String> customPermissionNames; | |
private Set<String> customPermissionsForCurrentUser; | |
/** | |
* This default constructor will seek out all unmanaged/default namespace Custom Permissions | |
**/ | |
public CustomPermissionsReader() { | |
this(null); | |
} | |
/** | |
* This constructor will load Custom Permissions associated with the namespace of the object passed in, | |
* this is the best constructor to use if you are developing a managed AppExchange package! The object | |
* passed in does not matter so long as its one from the package itself. | |
* | |
* If the object is running in a managed context (e.g. packaging org or installed package) namespace is used to constrain the query | |
* If the object is not running in a managed context (e.g. developer org not namespaced) the default namespace is used to query | |
**/ | |
public CustomPermissionsReader(SObjectType managedObject) { | |
this.managedObject = managedObject; | |
} | |
public Boolean hasPermission(String customPermissionName) { | |
// Demand load the custom permissions from the database? | |
if(customPermissionNames==null) | |
init(); | |
// Is this a valid custom permission name? | |
if(!customPermissionNames.contains(customPermissionName)) | |
throw new CustomPermissionsException('Custom Permission ' + customPermissionName + ' is not valid.'); | |
// Has this user been assigned this custom permission? | |
return customPermissionsForCurrentUser.contains(customPermissionName); | |
} | |
/** | |
* Loads Custom Permissions sets for either the default namespace or | |
* the current namespace context (derived from the managed object reference) | |
**/ | |
private void init() { | |
customPermissionNames = new Set<String>(); | |
customPermissionsForCurrentUser = new Set<String>(); | |
// Determine the namespace context for the custom permissions via the SObject passed in? | |
String namespacePrefix = null; | |
if(managedObject!=null) { | |
DescribeSObjectResult describe = managedObject.getDescribe(); | |
String name = describe.getName(); | |
String localName = describe.getLocalName(); | |
namespacePrefix = name.removeEnd(localName).removeEnd('__'); | |
} | |
// Query the full set of Custom Permissions for the given namespace | |
Map<Id, String> customPermissionNamesById = new Map<Id, String>(); | |
List<CustomPermission> customPermissions = | |
[select Id, DeveloperName from CustomPermission where NamespacePrefix = :namespacePrefix]; | |
for(CustomPermission customPermission : customPermissions) { | |
customPermissionNames.add(customPermission.DeveloperName); | |
customPermissionNamesById.put(customPermission.Id, customPermission.DeveloperName); | |
} | |
// Query to determine which of these custome settings are assigned to this user | |
List<SetupEntityAccess> setupEntities = | |
[SELECT SetupEntityId | |
FROM SetupEntityAccess | |
WHERE SetupEntityId in :customPermissionNamesById.keySet() AND | |
ParentId | |
IN (SELECT PermissionSetId | |
FROM PermissionSetAssignment | |
WHERE AssigneeId = :UserInfo.getUserId())]; | |
for(SetupEntityAccess setupEntity : setupEntities) | |
customPermissionsForCurrentUser.add(customPermissionNamesById.get(setupEntity.SetupEntityId)); | |
} | |
public class CustomPermissionsException extends Exception {} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Copyright (c), Andrew Fawcett | |
* All rights reserved. | |
* | |
* Redistribution and use in source and binary forms, with or without modification, | |
* are permitted provided that the following conditions are met: | |
* | |
* - Redistributions of source code must retain the above copyright notice, | |
* this list of conditions and the following disclaimer. | |
* - Redistributions in binary form must reproduce the above copyright notice, | |
* this list of conditions and the following disclaimer in the documentation | |
* and/or other materials provided with the distribution. | |
* - Neither the name of the Andrew Fawcett, nor the names of its contributors | |
* may be used to endorse or promote products derived from this software without | |
* specific prior written permission. | |
* | |
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | |
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES | |
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL | |
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, | |
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS | |
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY | |
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
**/ | |
@IsTest | |
private class CustomPermissionsReaderTest { | |
/** | |
* This will need to be modified to reflect a Custom Permission in the org, | |
* since DML in test code cannot create them :-( | |
**/ | |
private static final String TEST_CUSTOM_PERMISSION = 'Reset'; | |
@IsTest | |
private static void testCustomPermissionAssigned() { | |
// Create PermissionSet with Custom Permission and asisgn to test user | |
PermissionSet ps = new PermissionSet(); | |
ps.Name = 'CustomPermissionsReaderTest'; | |
ps.Label = 'CustomPermissionsReaderTest'; | |
insert ps; | |
SetupEntityAccess sea = new SetupEntityAccess(); | |
sea.ParentId = ps.Id; | |
sea.SetupEntityId = [select Id from CustomPermission where DeveloperName = :TEST_CUSTOM_PERMISSION][0].Id; | |
insert sea; | |
PermissionSetAssignment psa = new PermissionSetAssignment(); | |
psa.AssigneeId = UserInfo.getUserId(); | |
psa.PermissionSetId = ps.Id; | |
insert psa; | |
// Create reader | |
// Note: SObjectType for managed package developers should be a Custom Object from that package | |
CustomPermissionsReader cpr = new CustomPermissionsReader(Account.SObjectType); | |
// Assert the CustomPermissionsReader confirms custom permission assigned | |
System.assertEquals(true, cpr.hasPermission(TEST_CUSTOM_PERMISSION)); | |
} | |
@IsTest | |
private static void testCustomPermissionNotAssigned() { | |
// Assert the CustomPermissionsReader confirms custom permission not assigned | |
System.assertEquals(false, new CustomPermissionsReader(Account.SObjectType).hasPermission(TEST_CUSTOM_PERMISSION)); | |
} | |
@IsTest | |
private static void testCustomPermissionNotValid() { | |
try { | |
// Assert the CustomPermissionsReader throws an exception for an invalid custom permission | |
System.assertEquals(false, new CustomPermissionsReader(Account.SObjectType).hasPermission('NotValid')); | |
System.assert(false, 'Expected an exception'); | |
} catch (Exception e) { | |
System.assertEquals('Custom Permission NotValid is not valid.', e.getMessage()); | |
} | |
} | |
@IsTest | |
private static void testCustomPermisionDefaultConstructor() { | |
// Assert the CustomPermissionsReader confirms custom permission not assigned | |
System.assertEquals(false, new CustomPermissionsReader().hasPermission(TEST_CUSTOM_PERMISSION)); | |
} | |
} |
@afawcett - If I am creating a managed package, will the TEST_CUSTOM_PERMISSION be present when the unit tests for the package install are run. Given the complexity to test this, I thought it would be best asked.
As always, appreciate your time and contributions to the community. Thanks.
awesome stuff..
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi Andrew. Are you accepting pull-request/issues for this repo? I think we found one issue with managed package scenarios and would like to submit a pull-request. Say I include CustomPermissionsReader in PackageA, and I need to use it to check a custom permission that is part of PackageB. I instantiate the CPR class, passing in the SObjectType of an SObject defined in PackageB (let's call it PackageB__MyObject__c). The problem is that when CPR tries to resolve the namespace of PackageB, it fails, because on lines 86 and 87, both getName and getLocalName return the value PackageB__MyObject__c, and then on line 88, name.removeEnd(localName) basically results in a blank string, and therefore it ends up looking in the default namespace for the custom permission, rather than in PackageB.
We modified the code to determine the namespace by splitting the value returned from describe.getName() on the '__' pattern (double underscore). Since the platform won't allow you to explicitly create an object name containing a double underscore pattern, you will end up with either a 2, or 3 element array after splitting. If the split array contains two elements, then the object is in the default namespace. If the split array contains three elements ( [PackageB, MyObject, c] ), then element 0 of the returned array is your package namespace.