Skip to content

Instantly share code, notes, and snippets.

@afawcett
Created June 20, 2014 14:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save afawcett/2545650dc851eb0552e6 to your computer and use it in GitHub Desktop.
Save afawcett/2545650dc851eb0552e6 to your computer and use it in GitHub Desktop.
Preview of FLS Support in Domain Layer (see comments below)
public with sharing class Opportunities extends fflib_SObjectDomain
{
public Opportunities(List<Opportunity> sObjectList)
{
// Domain classes are initialised with lists to enforce bulkification throughout
super(sObjectList);
}
public override void onApplyDefaults()
{
// Apply defaults to Opportunities
for(Opportunity opportunity : (List<Opportunity>) Records)
{
opportunity.DiscountType__c = OpportunitySettings__c.getInstance().DiscountType__c;
}
}
public override void onValidate()
{
// Validate Opportunities
for(Opportunity opp : (List<Opportunity>) Records)
{
if(opp.Type!=null && opp.Type.startsWith('Existing') && opp.AccountId == null)
{
opp.AccountId.addError( error('You must provide an Account for Opportunities for existing Customers.', opp, Opportunity.AccountId) );
}
}
}
public override void onValidate(Map<Id,SObject> existingRecords)
{
// Validate changes to Opportunities
for(Opportunity opp : (List<Opportunity>) Records)
{
Opportunity existingOpp = (Opportunity) existingRecords.get(opp.Id);
if(opp.Type != existingOpp.Type)
{
opp.Type.addError( error('You cannot change the Opportunity type once it has been created.', opp, Opportunity.Type) );
}
}
}
public override void onAfterInsert()
{
// Unit of Work scope for this event
fflib_SObjectUnitOfWork uow =
new fflib_SObjectUnitOfWork(new Schema.SObjectType[] { Account.SObjectType });
// Update last Opportunity activity on the related Accounts (via the Accounts Domain class)
Accounts accounts = new Accounts(
new AccountsSelector().selectByOpportunity(Records));
accounts.updateOpportunityActivity(uow);
// Commit the work
uow.commitWork();
}
public override void onAfterDelete()
{
for(Opportunity opp : (List<Opportunity>) Records)
{
if(opp.StageName!=null && opp.StageName.startsWith('Won'))
{
opp.StageName.addError( error('You cannot delete Won Opportunities.', opp, Opportunity.StageName) );
}
}
}
public void applyDiscount(Decimal discountPercentage, fflib_SObjectUnitOfWork uow)
{
// Custom FLS checking
checkFieldIsUpdateable(Opportunity.StageName);
// Calculate discount factor
Decimal factor = calculateDiscountFactor(discountPercentage);
// Opportunity lines to apply discount to
List<OpportunityLineItem> linesToApplyDiscount = new List<OpportunityLineItem>();
// Apply discount
for(Opportunity opportunity : (List<Opportunity>) Records)
{
// Appply to the Opporunity Amount?
if(opportunity.OpportunityLineItems.size()==0)
{
// Adjust the Amount on the Opportunity if no lines
opportunity.Amount = opportunity.Amount * factor;
uow.registerDirty(opportunity);
}
else
{
// Collect lines to apply discount to
linesToApplyDiscount.addAll(opportunity.OpportunityLineItems);
}
}
// Apply discount to lines
OpportunityLineItems lineItems = new OpportunityLineItems(linesToApplyDiscount);
lineItems.applyDiscount(this, discountPercentage, uow);
}
public static Decimal calculateDiscountFactor(Decimal discountPercentage)
{
// Calculate discount factor
Decimal discountProportion = discountPercentage==null ? 0 : discountPercentage / 100;
Decimal factor = 1 - discountProportion;
return factor;
}
public class Constructor implements fflib_SObjectDomain.IConstructable, fflib_SObjectDomain.IConfigure
{
public fflib_SObjectDomain construct(List<SObject> sObjectList)
{
return new Opportunities(sObjectList);
}
public fflib_SObjectDomain.Configuration configure()
{
return new fflib_SObjectDomain.Configuration().
setEnforceCRUD(true).
setEnforceTriggerFLS(true).
onTriggerEnforceFLSRead(Opportunity.Type).
onTriggerEnforceFLSRead(Opportunity.StageName).
onTriggerEnforceFLSReadWrite(Opportunity.DiscountType__c);
}
}
}
@afawcett
Copy link
Author

Background: Saleforce Security Review now requires FLS checking in your code. I've been thinking about how this can be done in the Apex Enterprise Patterns. The Selector base class was reasonably easy to teach how to check the fields automatically (no coding) since the base class is given the list. The Domain base class is a little harder, since the fields accesed and the usage are driven by the logic wihtin the methods.

Prototype for Feedback. The following is prototype of how the API to support allowing the developer to implement FLS for code in the Domain layer could look! Please review lines 72, 110 and 117 to 125 and share your thoughts in this Gist.

Some questions to prompt feedback...

  1. What do you think will happen when a user without FLS to the fields referenced tries to create or update a record related to this domain class?
  2. There are two ways of enforcing FLS used in the above, do we need two ways? Why do you think there is two ways of enforcing FLS used?
  3. CRUD security checking is currently implemented by the existing patterns, the above also provides the ability to disable this checking. Is that obvious how to do that?

@m0rjc
Copy link

m0rjc commented Jun 20, 2014

There's an interesting fundamental question. Are triggers to enforce constraints or to automatically perform business actions? In a normal three tier application we'd do business actions in the middle tier, but there seems a call in Salesforce to allow direct database access as the norm and people expect to do business actions in triggers. If we always require VisualForce or similar technologies for business actions, could triggers be purely about enforcing constraints? If that is the case, then why should triggers ever care about security settings for the user concerned? A constraint is a constraint whoever you are.

  1. Either the trigger would not run, so allowing the user to continue but risking that we don't enforce constraints. Or the trigger will fail so stopping the user doing anything with the object. Eeek!

Are there situations where the set of fields we'd need to check depends on the action the user is taking? Maybe it depends on the Record Type the user is working with, and a given user could never try to access or change certain fields because they can never access that Record Type. We'd not want the trigger to blow up in that situation. Also perhaps some field validation may depend on others, and a given user could never enter a situation where they cause the trigger to read a field they don't have access to.

Other Questions

What happens if 'triggerEnforceFLS' is true, but no fields set or vice versa? The two are linked, so maybe we could deduce that Enforce FLS is true if fields are set. If no fields are set then do we check all fields or none?

@afawcett
Copy link
Author

That's a good question, i've seen lots of examples of triggers being forced to do to much business logic (utilising a pattern i call 'control fields') and getting into a big mess with recursive calls, data loaders etc.

So without wishing to broaden this into a wider debate of trigger vs service logic. The answer in my view is, we do need logic in the triggers to protect the data integrity and provide some feature function around default fields and manipulating other records (subject to how users interact with the app). Where one draws the line is hard to prescribe though (certainly callouts and batch apex are troublesome).

It also somewhat depends on how your users want to interact with your app via CRUD Native UI and/or via VF/Custom UI, really you have to consider both tbh. One thing is for certain, we have to think carefully before blocking direct object access or not writing approprite trigger code as it may erode platform features like Workflow, Flow and not to mention Salesforce1 Mobile.

In respect to your other quesiton, also a good one. If the developer configures triggerEnforceFLS without giving any fields, i'm tempted to throw an assertion at runtime, to say, something along the lines of 'why ask me to enforce something you've not told me about'. We could of course implicitly enable it the first time onTriggerEnforceFLSRead or onTriggerEnforceFLSReadWrite is called?. It's also present to allow disabling.

New question....

  1. What do you expect the scope of the configuration of the domain class to be, per instance or per execution context? How would you want to configure it in either case?

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