Abstract

Force.com allows developers and administrators to control access to data at many different levels. You can control access at the object-level, the record-level, and at the field-level. This article will focus on methods for controlling access to data at the object and field levels.

After an introduction to object and field level security, this article looks at the different techniques that force.com applications can use to enforce a customer's security settings.

Understanding CRUD and FLS

Object-level security within the salesforce.com environment is referred to as Create-Read-Update-Delete (CRUD) access. CRUD settings are applied at the profile level and can be used to restrict the actions that users can take on each type of standard and custom object. An example use of CRUD would be to remove the ability for a custom "auditor" profile to update, create, or delete any Account record.


Crud settings.png


Field-level security (FLS) is configured similarly to CRUD but allows administrators to define the profiles that can see and write to most fields of standard and custom objects. Example uses of FLS would be to make the Expected Revenue field of the Opportunity object invisible to all profiles outside of sales and management.


Fls by profile.png


Automatic CRUD and FLS Enforcement in VisualForce

When rendering VisualForce pages, the platform will automatically enforce CRUD and FLS when the developer references SObjects and SObject fields directly in the VisualForce page. For example, if a user without FLS visibility to the Phone field of the Contact object was to view the below page, phone numbers would be automatically removed from the table.

<apex:page standardController="Account"> 
  <apex:pageBlock title="Contacts"> 
    <apex:dataTable value="{!account.Contacts}" var="contact" cellPadding="4" border="1"> 
      <apex:column> 
        <apex:facet name="header">Name</apex:facet> 
        {!contact.Name} 
      </apex:column> 
      <apex:column> 
        <apex:facet name="header">Phone</apex:facet> 
        {!contact.Phone} 
      </apex:column> 
    </apex:dataTable> 
  </apex:pageBlock> 
</apex:page> 


VisualForce will also remove fields for which users do not have FLS visibility when rendering edit pages. Additionally, all apex:inputField tags will be rendered as read-only elements for fields that are set to read-only through FLS. Please note that using other input tags such as apex:inputText or apex:inputTextArea with SObject fields indicate to VisualForce that the fields should not be treated as SObject fields and prevent the platform to automatically enforcing FLS.

<apex:page standardController="Contact" recordSetVar="contacts"> 
  <apex:pageBlock title="Contacts"> 
    <apex:form>
      <apex:dataTable value="{!contacts}" var="contact" cellPadding="4" border="1"> 
        <apex:column> 
          <apex:facet name="header">Name</apex:facet> 
            <apex:inputField value="{!contact.LastName}" /> 
        </apex:column> 
        <apex:column> 
          <apex:facet name="header">Phone</apex:facet> 
            <apex:inputField value="{!contact.Phone}" />
        </apex:column> 
      </apex:dataTable>
      <apex:commandButton action="{!save}" value="Save" />
    </apex:form> 
  </apex:pageBlock> 
</apex:page>


Manual CRUD and FLS Enforcement in Apex Controllers

Read Access


There are often cases where developers use VisualForce to display data derived from an SObject field in an indirect or processed form. For instance, a page controller may use internal logic to determine the appropriate value to display. A simple example of this would be a page that displays a random SObject Name field from a list of SObjects:

<apex:page controller="RandomContactController"> 
  <apex:outputText value="{!getRandomName}" />
</apex:page>

and its corresponding Apex controller:

public with sharing class RandomContactController {
    public String getGetRandomName() {
        // Check if the user has read access on the Contact.Name field
        if (!Schema.sObjectType.Contact.fields.Name.isAccessible()){
          return '';
        }
        
        Contact [] myList = [SELECT Name FROM Contact LIMIT 1000];
        // Pick a list entry at random
        Integer index = Math.mod(Math.abs(Crypto.getRandomInteger()),myList.size());
        Contact selected = myList.get(index);
        return selected.Name;
    }
}

The above example indirectly displays the Name field of the Contact object by using a customer getter method that returns a string value. Because VisualForce only sees the return value as a string and not as an SObject field, CRUD and FLS is not automatically enforced and it is necessary to call isAccessible() on the appropriate Describe Field Result in order to manually check the user's CRUD and FLS access. Please note that calling isAccessible() or any field-level access checks on a field automatically checks that the user has the corresponding CRUD access to the object type.


Update Access


Apex code that updates fields or creates objects using customer setters or other internal logic also require manual CRUD/FLS checks. An example would be a page that performs a bulk update of a number of objects:

<apex:page standardcontroller="Contact" extensions="ContactUpdateExtension">
  <apex:pageBlock title="Contacts"> 
    <apex:outputField value="{!Contact.Name}" />
    <apex:outputField value="{!Contact.Phone}" />
  
    <apex:form>
      <apex:selectList value="{!statusToSet}">
        <apex:selectOption itemValue="Verified" itemLabel="Verified" />
        <apex:selectOption itemValue="Not Verified" itemLabel="Not Verified" />
        <apex:selectOption itemValue="Unknown" itemLabel="Unknown" />
      </apex:selectList>
      <apex:commandButton action="{!updateStatus}" value="Update" />
    </apex:form>
  </apex:pageBlock>
</apex:page>

and its supporting controller extension:

public with sharing class ContactUpdateExtension {
  public String statusToSet {get;set;}
  private Contact c;
  
  public ContactUpdateExtension(ApexPages.StandardController ctr) {
    c = [SELECT Status__c FROM Contact WHERE Id=: ctr.getRecord().Id]; 
  }
  public PageReference updateStatus() {
    // Check if the user has update access on the Contact.Name field
    if (!Schema.sObjectType.Contact.fields.Status__c.isUpdateable()){
      return null;
    }
    c.Status__c = statusToSet;
    update c;
    return null;
  }
}


Create Access


A slightly more complicated example creates SObjects and also uses page messages to display access control errors to the user. In the below, a simple VisualForce page displays a lead and allows a user to create basic Account and Contact objects from its information:

<apex:page standardcontroller="Lead" extensions="LeadConverterExtension">
  <apex:pageMessages />
  <apex:pageBlock title="Lead"> 
    <apex:outputField value="{!Lead.Name}" /><br />
    <apex:outputField value="{!Lead.Company}" /><br />
    <apex:outputField value="{!Lead.Phone}" /><br />
    <apex:form>
      <apex:commandButton action="{!convertLead}" value="Convert To Contact" />
    </apex:form>
  </apex:pageBlock>
</apex:page>
public with sharing class LeadConverterExtension {
    private Lead l;
    public LeadConverterExtension(ApexPages.StandardController ctr) {
      l = [SELECT FirstName,LastName,Phone,Company FROM Lead WHERE Id=:ctr.getRecord().Id];
    }

    public PageReference convertLead() {
      // Contact fields that will be copied from the Lead
      String [] contactUpdateFields = new String [] {'FirstName',
                                                     'LastName',
                                                     'Phone',
                                                     'AccountId'};

      // Obtaining the field name/token map for the Contact object
      Map<String,Schema.SObjectField> m = Schema.SObjectType.Contact.fields.getMap();
      for (String fieldToCheck : contactUpdateFields) {
        // Check if the user has create access on the each field
        if (!m.get(fieldToCheck).getDescribe().isCreateable()) {
          ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.FATAL,
                                                    'Insufficient access')); 
          return null;
        }
      }
      
      List<Account> accts = [SELECT Id FROM Account WHERE Name=:l.Company LIMIT 1];
      Account a;
      if (accts.size() == 0) {
        // Check if the user has create access on the Account.Name field
        if (!Schema.sObjectType.Account.fields.Name.isCreateable()){
         ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.FATAL,
                                                    'Insufficient access')); 
         return null;
        }
        a = new Account(Name=l.Company);
        insert a;
        
      } else {
        a = accts.get(0);
      }
      
      Contact c = new Contact(FirstName=l.FirstName,
                              LastName =l.LastName,
                              Phone    =l.Phone,
                              AccountId=a.Id);
      insert c;
      return null;
    }

}

Since there are multiple Contact fields on which to check access, convertLead() first obtains a map of field names and their field tokens. It then iterates over a list of fields that it has been designed to populate and checks if the user has the appropriate access. In this example, the controller aborts the operation completely but developers should consider having graceful degradation within their applications if appropriate for their design.


Delete Access


Delete operations occurring in custom controllers or controller extensions always need to check that the calling user has CRUD/delete access. Delete is only checked at the object level (CRUD) and not at the field level (FLS).

<apex:page standardcontroller="Lead" extensions="LeadDeleteExtension">
  <apex:pageMessages />
  <apex:pageBlock title="Lead"> 
    <apex:outputField value="{!Lead.Name}" /><br />
    <apex:outputField value="{!Lead.Company}" /><br />
    <apex:outputField value="{!Lead.Phone}" /><br />
    <apex:form>
      <apex:commandButton action="{!deleteLead}" value="Delete" />
    </apex:form>
  </apex:pageBlock>
</apex:page>
public with sharing class LeadDeleteExtension {
    private Lead l;
    public LeadDeleteExtension(ApexPages.StandardController ctr) {
      l = [SELECT Id FROM Lead WHERE Id=:ctr.getRecord().Id];
    }
    
    public PageReference deleteLead() {
      // Check if the user has delete access on the Lead object
      if (!Lead.sObjectType.getDescribe().isDeletable()){
        ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.FATAL,
                                                    'Insufficient access')); 
        return null;
      }
      
      delete l;
      return null;
    }

}

If you are using controller extensions and intend to delete the active record, another option is to call the standard controller's delete() function instead of deleting the object within the controller extension. The standard controller will automatically check CRUD access before performing the operation.

Manual CRUD and FLS Enforcement in VisualForce

The field describe calls available in Apex are also available in VisualForce through the $ObjectType element. While enforcement in Apex controllers is preferred for scalability and general robustness reasons, there are situations where manual enforcement in VisualForce is useful.

<!-- This would normally bypass automatic FLS enforcement for accessibility-->
<apex:outputText value="{!contactName}" 
             rendered="{!$ObjectType.Contact.fields.Name.Accessible}" />


<!-- This would normally bypass automatic FLS enforcement for updates-->
<apex:inputText value="{!contactEmail}" 
            rendered="{!$ObjectType.Contact.fields.Email.Updateable}" />


<!-- This isn't a particularly strong way to prevent access to a controller method. 
          The below should only be viewed as an example
 -->
<apex:commandButton action="{!CustomDelete}" 
             rendered="{!$ObjectType.Contact.Deletable}" />


<!-- stringToBecomeNewContactEmail is a generic string type so automatic FLS 
          enforcement is not available
  -->
<apex:inputText value="{!stringToBecomeNewContactEmail}" 
            rendered="{!$ObjectType.Contact.fields.Email.Createable}" />


CRUD and FLS Enforcement in Apex Web Services

Since Apex web services do not have a VisualForce binding layer, all CRUD and FLS enforcement must be done within the Apex code.

global with sharing class ContactWebService {
  // Note that this class does not check for empty SOQL queries

  webservice static Integer createContact(String First,
                                          String Last,
                                          String Phone,
                                          Id accountId){
    String [] contactUpdateFields = new String [] {'FirstName',
                                                   'LastName',
                                                   'Phone',
                                                   'AccountId'};
      
    // Obtaining the field name/token map for the Contact object
    Map<String,Schema.SObjectField> m = Schema.SObjectType.Contact.fields.getMap();
    for (String fieldToCheck : contactUpdateFields) {
      // Check if the user has create access on the each field
      if (!m.get(fieldToCheck).getDescribe().isCreateable()) {
        return 1;
      }
    }
    insert new Contact(FirstName = First,
                       LastName  = Last,
                       Phone     = Phone,
                       AccountId = accountId);
    return 0;
        
  }
   
  webservice static String getContactPhone (Id contactId) {
    // Check if the user has read access on the Phone field
    if (!Schema.sObjectType.Contact.fields.Phone.isAccessible()){
      return null;
    }
    return [SELECT Phone FROM Contact WHERE             
            Id=:contactId].Phone;                                                       
  }
    
  webservice static Integer updatePhone (Id contactId,String Phone) {
    // Check if the user has update access on the Phone field
    if (!Schema.sObjectType.Contact.fields.Phone.isUpdateable()){
      return 1;
    }
    Contact c = [SELECT Phone FROM Contact WHERE Id=:contactId];
    c.Phone = Phone;
    update c;
    return 0;
  }
    
  webservice static Integer deleteContact (Id contactId) {
    // Check if the user has delete access on the Contact object
    if (!Contact.sObjectType.getDescribe().isDeletable()) {
      return 1;
    }
    delete [SELECT Id FROM Contact WHERE Id=: contactId];
    return 0; 
  }
}


References