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:
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.
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;
}
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) withforce:mdapi:retrieve
or withforce:source:retrieve
.