8

The DescribeFieldResult.isUpdateable method incorporates the "Edit Access" setting of the Field level Security for a field i.e. it considers the field settings in the current profile.

Is there a way to find out if a field is writeable in System Mode i.e. ignoring the field settings in the profile? One example of a field that is not writeable is a formula field.

Is isCustom() && !isAutoNumber() && !isCalculated() enough? Answer no: an Id value in the detail record referencing a master record generates a "is not editable" if it is set in an update.

(I'm looking for this to write a generic @AuraEnabled method that accepts data from the client side and filters out fields that can't be updated in the "System Mode" that the controller.)

PS

I presently have this pretty ugly logic. The isCascadeDelete is for the case of a master-detail lookup that can't be updated (if the relationship is not reparentable - also unfortunately excludes the case where reparenting is possible).

DescribeFieldResult dfr = f.getDescribe();
if (!dfr.isAutoNumber() && !dfr.isCalculated() && !dfr.isCascadeDelete()
        && (dfr.isCustom() || dfr.getName() == 'Id' || dfr.isNameField())
        ) {
    ...
}
  • What about a query to the FieldPermissions object? – Adrian Larson Feb 5 '18 at 17:41
  • @AdrianLarson That is part of the profile/permission set data isn't it? I'm looking to ignore that and get the "System Mode" behaviour. – Keith C Feb 5 '18 at 17:43
  • But the Boolean should match up with the truthiness of that permissions model across all permission sets, no? – Adrian Larson Feb 5 '18 at 17:44
  • @AdrianLarson I'm assuming that in a scratch org that has just had the SObject pushed into it there would be no FieldPermission records and so always have a count of zero; I'll check. – Keith C Feb 5 '18 at 17:53
4

The only reasonable method I know of is to use a try-catch block:

try {
  record.put(someField, someValue);
} catch(SObjectException e) {
  // Field could not be written
}

Unfortunately, this is an expensive construct (~30ms per failed access), so you might want to at least pre-validate that it is !(isCalculated() || isAutoNumber()).

  • That's bad news... – Keith C Feb 5 '18 at 17:41
2

You should be able to approximate with a query on the FieldPermissions object:

Boolean isFieldEditable = 0 < [
    SELECT count() FROM FieldPermissions
    WHERE Field = 'MyObject__c.MyField__c'
    AND PermissionsEdit = true
];
  • 1
    When I create a scratch org and push in a custom SObject, there are no FieldPermissions rows at all yet in "System Mode: the SObject is accessible. So this approach doesn't work. – Keith C Feb 5 '18 at 18:03
1

I have gone through your question very minutely. After giving profound thoughts based on my capacity, I failed to understand how would you bypass Profile to find the Fields Permissions on any object (Custom or Standard)? As I know with my limited knowledge of Salesforce functionality when we create a custom object with fields without assigning it to any particular profile, Salesforce by default assigned it to the System Administration profile(s). So based on your requirement we can use the Salesforce MetadatAPI Apex wrapper class to query the "Admin" profile and get the field level security of the object in question Because you specified that the object got pushed to the scratch org. We can very much pass the fields information from our client-side JavaScript handler to the server-side @AuraEnabled Apex controller. As per your requirement, "With Sharing" keyword can be omitted to make sure the controller runs on the System Mode.

I have taken the help of Apex Wrapper Salesforce Metadata API created by Andrew Fawcett to demonstrate my PoC. For the detail of copying the Metadata Classes, please follow the instructions of this article Using Named Credentials with the Apex Wrapper Salesforce Metadata API (apex-mdapi). After the successful setup of the Apex Metadata Wrapper class, we can create the following class to test the concept.

public class ApexMetadataFLSCheckingOnProfile {

@AuraEnabled
public static void readProfileFieldSecurity(List<String> Fieldstobeupdated, String objectname)
{
    refreshSessionTrick();        
    MetadataService.MetadataPort service = createService();
    String SOQLQuerySelect = 'SELECT ';
    String SOQLQueryForm = ' FROM '+ objectname;
    String SOQLQueryFields = '';
    String SOQLQueryFinalString = '';
    List<String> updatefieldLst= new List<String>();
    MetadataService.Profile profile = (MetadataService.Profile)service.readMetadata('Profile', new String[]{'Admin'}).getRecords()[0];
    //System.debug('profile--->'+profile.fullName);

    for (MetadataService.ProfileFieldLevelSecurity fls: profile.fieldPermissions){

        String fieldname = splitfieldname(fls.field);
            if (Fieldstobeupdated.contains(fieldname)){
                if (fls.editable){
                    updatefieldLst.add(fieldname);
            }

        }
        //System.debug('Fully Qualified Field Name---->'+fls.field);
        //System.debug('Field Name without the object Prefix-->'+fieldname);
        //System.debug('Field Level security editable---->'+fls.editable);
        //System.debug('Field Level security readable---->'+fls.readable);

    }

    for (Integer i=0; i<updatefieldLst.size(); i++){
        if (i == 0){
            SOQLQueryFields +=  updatefieldLst.get(i)+ ',';
            System.debug('i==0'+i);
        }
        else if (i== updatefieldLst.size()-1){
            SOQLQueryFields +=  updatefieldLst.get(i);
            System.debug('i==else'+i);
        }
        else{
            SOQLQueryFields +=  updatefieldLst.get(i)+ ',';
        }
    }
    SOQLQueryFinalString = SOQLQuerySelect + SOQLQueryFields + SOQLQueryForm;
    System.debug('SOQLQueryFinalString--->'+SOQLQueryFinalString);
    System.debug(updatefieldLst);

    //Please write the code for database query and assign it to the list
    //And then update the list

}

public static MetadataService.MetadataPort createService()
{
    MetadataService.MetadataPort service = new MetadataService.MetadataPort();
    service.endpoint_x = 'callout:ApexMetadaAPI/services/Soap/m/38.0';
    service.SessionHeader = new MetadataService.SessionHeader_element();
    service.SessionHeader.sessionId = '{!$Credential.OAuthToken}';

    return service;
}

public static void refreshSessionTrick(){
    String restUrl = 'callout:ApexMetadaAPI/services/data/v38.0/limits';
    Http h = new Http();
    HttpRequest req = new HttpRequest();
    req.setEndpoint(restUrl);
    req.setMethod('GET');
    HttpResponse res = h.send(req);
}  

public static String splitfieldname(String fieldname){
    String fieldnametoreturn = fieldname.substringAfter('.');
    //System.debug('Splited field-->'+fieldnametoreturn);
    return fieldnametoreturn;
}

}

I know I have been trying to express my views points towards the Salesforce Guru's. Please excuse me for my limited knowledge. I tried to find the solution of your requirement without hurting our expertise.

  • Prolay, Thanks for taking the time to answer. Code can be pushed into an org without the field permissions being pushed - see my comment on Adrian's answer. I think your solution is another way of accessing the same data as Adrian's answer. So I;m afraid it doesn't get me much any further... – Keith C Feb 11 '18 at 12:11
  • @KeithC, I would like to request you update this question if you find the solution of your issue. Thank you. – Prolay Chaudhury Feb 11 '18 at 18:00

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.