4

Background:

I built a new Salesforce application in my Enterprise Org's (Developer Pro) sandbox just to get a minimal viable project up and running quickly and demo this to some users. There's a bunch of custom objects, a couple of Apex triggers, a handful of Apex classes and an inbound REST endpoint and some other odds and sods.

Obviously at some point I needed to extract this app and get it under source control in a Salesforce project and begin using SFDX. So I followed the steps as described in:

Migrating Existing Projects to Salesforce DX

This all went swimmingly well and I am able to push the app to a scratch org just fine.

The Problem:

After opening the app in my scratch org it seems I need to create a permission set so I can access my app, see its tabs and add/update records. It seems that app permission sets aren't included in the metadata retrieved using sfdx force:mdapi:retrieve.

To overcome this I followed the steps in section 11 of Migrating Existing Projects to Salesforce DX which is fine. Then I got to the "Object Settings" permissions bit as seen here (I've greyed out some stuff that's sensitive):

enter image description here

Each one of my custom objects has between 30-40 fields, do I really need to tick every "Read Access" and "Edit Access" checkbox individually?

Is there a better and more efficient way to do this?

3
  • 2
    The SFSE Q&A you link regarding not being able to extract your perm set from the Sandbox isn't actually relevant. That Q&A addresses using force:source:pull against any non-scratch (ie, not "source-tracked") org. You can extract a permset from your Sandbox by either including it in the manifest specified (explicitly or implictly) with force:mdapi:retrieve or with force:source:retrieve. Apr 22 '19 at 15:58
  • @ThomasTaylor - not had time to revisit this in depth. However I did give it a quick shot and see that this doesn't seem to return individual field permissions on my custom objects.
    – Kev
    Apr 24 '19 at 20:45
  • Which command did you use? Are the custom objects in your sfdx project? Apr 24 '19 at 21:36
1

There are a few suggested options in Migrating to Permission Sets for DX by Shane McLaughlin. It generally involves using your Admin profile as a basis for creating a corresponding permission set.

The options that seem more automated include:

  1. Using a custom SFDX CLI plugin that will convert the existing Profile into a Permission set:

    sfdx shane:profile:convert -p Admin -n MyNewPermSet
    I believe this requires having the full metadata source for the Profile locally to work, which is one of the underlying challenges with Profile Metadata. If you just retrieve the Profile it won't come back with all the associated permissions. You need to query those as well. At which point you are back to building everything up again.

  2. Using an AppExchange package that will convert a profile to a permission set. (04t0b000001Hy0gAAC).
    Note: YMMV with this package. It failed to install for me.


As a brute force alternative I've been experimenting with anonymous Apex in the resulting scratch org. It grants access to the current (presumably System Admin) user so they can access all the custom objects and associated fields. This also applies to the custom metadata types. There are likely other parts of the permission sets that will still need to be addressed.

The end goal is that you can run this once in the scratch org and then pull the resulting permission set down into your source control.

// https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_erd_profile_permissions.htm
PermissionSet ps = new PermissionSet(Label = 'baselinePermSet', Name = 'baselinePermSet');
insert ps;

// Add the current executing user to the new Permission Set
PermissionSetAssignment psa = new PermissionSetAssignment(PermissionSetId = ps.Id, AssigneeId = UserInfo.getUserId() );
insert psa; 

// Specific standard SObjects that we want access to in addition to any custom objects
Set<string> requiredStandardObjects = new Set<string>{'Account','Contact','Opportunity'};

// Map to grant access to sobjects. Keyed by sobject metadata that we want access to.
Map<Schema.DescribeSObjectResult, ObjectPermissions> sObjectsToPermissionMap = new Map<Schema.DescribeSObjectResult, ObjectPermissions>();
List<SetupEntityAccess> cmdtAccessToInsert = new List<SetupEntityAccess>();

Map<String, Schema.SObjectType> gd = Schema.getGlobalDescribe(); 

for(Schema.SObjectType objTyp : gd.Values()) {

    Schema.DescribeSObjectResult objDes = objTyp.getDescribe(SObjectDescribeOptions.DEFERRED);

    String sObjectTypeName = objDes.getName();

    // Exclude all the unwanted Sobjects e.g. History, Share etc..
    if(objDes.getKeyPrefix() == null || // Built in System *ChangeEvent descibes don't have a keyprefix
        sObjectTypeName.endsWithIgnoreCase('__ChangeEvent') ||
        objDes.isCustomSetting() ||
        (!objDes.isCustom() && !requiredStandardObjects.contains(sObjectTypeName))) {
        continue;
    }
    
    if(sObjectTypeName.endsWithIgnoreCase('__mdt')) {
        // Custom Metadata Types

        // Need to lookup the EntityDefinition.DurableId via the QualifiedApiName
        // TODO: Bulkify this SOQL query. 
        Id customMetadataDurableId = [Select DurableId from EntityDefinition where QualifiedApiName = :sObjectTypeName ].DurableId;
        cmdtAccessToInsert.add(new SetupEntityAccess(SetupEntityId = customMetadataDurableId, ParentId = ps.Id));
        continue;
    }

    ObjectPermissions objectPermission = new ObjectPermissions(SobjectType = sObjectTypeName,
                                                                    PermissionsCreate = true,
                                                                    PermissionsDelete = true,
                                                                    PermissionsEdit = true,
                                                                    PermissionsRead = true,
                                                                    ParentId = ps.Id
                                                                );

    sObjectsToPermissionMap.put(objDes, objectPermission); 
}

insert cmdtAccessToInsert;

List<ObjectPermissions> objectPermissionsToCreate = sObjectsToPermissionMap.values();
insert objectPermissionsToCreate;


// Add field permissions for all the sObjects of interest
List<FieldPermissions> readWritePerms = new List<FieldPermissions>();
for(Schema.DescribeSObjectResult objDes : sObjectsToPermissionMap.keySet()) {
    ObjectPermissions objectPermission = sObjectsToPermissionMap.get(objDes);
    List<FieldPermissions> fieldPermsForSObject = createFieldPermissions(ps.Id, objDes, true, true);
    readWritePerms.addAll(fieldPermsForSObject);
}
insert readWritePerms;
                                                            
private static List<FieldPermissions> createFieldPermissions(Id permissionSetId, Schema.DescribeSObjectResult objDes, boolean read, boolean edit) { 

    string sObjectTypeName = objDes.getName();

    Map<String, Schema.SObjectField> fields = objDes.fields.getMap();

    List<FieldPermissions> fieldPerms = new List<FieldPermissions>();
    for(string fieldName : fields.keySet()) {

        Schema.DescribeFieldResult dfr = fields.get(fieldName).getDescribe();

        if(!dfr.isPermissionable()) {
            System.debug(LoggingLevel.Debug, 'Skipping not Permissionable field: ' + sObjectTypeName + '.' + fieldName);
            continue;
        }

        if(dfr.getCompoundFieldName() != null) {
            // This is a component field of a parent compound field. E.g. It is Account.BillingStreet from Account.BillingAddress.
            System.debug(LoggingLevel.Debug, 'Skipping component field: ' + sObjectTypeName + '.' + fieldName);
            continue;
        }

        string fieldNameForPermission = sObjectTypeName + '.' + fieldName;
        if(dfr.type == Schema.DisplayType.reference && !dfr.isCustom()) {
            // The Account.ParentId field requires the FieldPermissions.Field to be set to Account.Parent 
            // But... A custom field shouldn't use the relationship name (ending in __r). It should use the field name (ending in __c)
            fieldNameForPermission = sObjectTypeName + '.' + dfr.relationshipName;
        }

        boolean editPerm = edit;
        if(dfr.isCalculated()) {
            editPerm = false;
        }            

        FieldPermissions contactFieldPermission = new FieldPermissions(SobjectType = sObjectTypeName,
                                                            Field = fieldNameForPermission,
                                                            ParentId = permissionSetId,
                                                            PermissionsRead = read,
                                                            PermissionsEdit = editPerm
                                                        );
        fieldPerms.add(contactFieldPermission);
    }
    return fieldPerms;
}
2
  • 1
    Thanks for spending time answering my question Daniel. Saleforce is no longer a thing in my life for now so I'll not be able to provide feed back if any of this works.
    – Kev
    Aug 31 '20 at 0:00
  • @Kev No problem. At the very least this will hopefully be helpful to the next person who arrives here. Aug 31 '20 at 23:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.