Skip to content

Instantly share code, notes, and snippets.

@afawcett
Last active July 10, 2020 02:04
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save afawcett/bc482bfdc840d5ac2858 to your computer and use it in GitHub Desktop.
Save afawcett/bc482bfdc840d5ac2858 to your computer and use it in GitHub Desktop.
CustomPermissionsReader
/**
* 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 {}
}
/**
* 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));
}
}
@harmon
Copy link

harmon commented May 24, 2016

Hey, this is pretty cool! I was curious why you accept an object in the constructor and not just a string, like "mymanagedpackage"? I don't believe custom permissions are tied to any specific object.

@tfuda
Copy link

tfuda commented Jun 7, 2016

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.

@jkentjnr
Copy link

@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.

@ankit1421
Copy link

awesome stuff..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment