4

I'm writing a managed package, and I'm using a number of fields that are controlled by the system and written to from apex.

Of course, I can restrict access to the fields via a profile, but I don't know how this works with System Administrators in the client's org.

My question is: If I mark a field in my Managed Package as readonly to a system administrator profile, and subsequently deploy that profile when installing the package in a Client's org: Will the system administrator be able to write to that field?

Additionally: Will the client org be able to write their own apex to write to that field?

How do you guys deal with this?

thanks -JN

3 Answers 3

3

I'm seeing two issues (one in your question and one raised by @sfdcfox):

For the custom roll-up field, you can combat this by putting a trigger on the CHILD (rolled-up) object and one on the PARENT (holding the field) object. You would definitely need to worry about recursion, so plan on having a class with a static boolean to avoid that, but it can be done. Then, any time someone edits that field, it just resets to its proper value.

For the issue sfdcfox raised, that's a use-case for custom settings. I like to make a List setting with the following fields: ValueBoolean__c, ValueText__c, etc. Then just store whatever you want in a key-value fashion.

1
  • The double trigger idea would work: but it doesn't really solve the problem, just avoids it. Additionally like you said: you've got to deal with recursion. Custom settings use cases are somewhat limited. They're really only good for things you need to run the app but that no one should ever see. Many times when it's configuration based settings, I just use a custom object and give the client a nice interface to manage them on his own. The issue at hand is controlling what has access to what fields at what times. So with the access controller class I wrote: I think I've nailed it down Commented Apr 24, 2014 at 20:14
2

The person that installs a managed package will determine the field level security during installation (and anybody with "View Setup and Configuration" + "Manage users" can subsequently change these). So you won't be able to protect your fields that way.

Plus generally sysadmins bypass the field level security anyway because of the "modify all data".

Generally I think that when admin wants to do something, he'll find a way. It would help if you'd write why you want to protect some stuff, what's your scenario.

If it's a custom setting - you can mark it as hidden. Could be handy if you intend to charge by amount of records created or something like that...

One thing that comes to mind is to have a helper class with static Boolean flag. Then in every controller you have - set this flag before doing any DML. And make the triggers check that flag first and complain if one of your special fields is created / modified without that flag.

This will effectively mean the standard ways to create records (standard page layout, Data Loader, Chatter actions...) become useless and people are forced to use your VF page + controller (or maybe some global helper method you've exposed so client code can call it - you'd let them set only the fields you want).

But it sounds like a bad end user experience, I'm not sure whether what you're trying to do is "right". Remember that Force.com platform is successful because (among others of course) it's so extensible - and you want to lock your customers down.

3
  • But, I'd say the ability to lock out certain fields is essential. Imagine that you're managing encryption keys on records, and even set the field as encrypted to avoid exposure. An admin could still mess up the key and break your app, or use your key in ways not intended (using View Encrypted Text Fields). Or maybe it's some other piece of data that's critical to your application's function and shouldn't be modified by any user at all, like some special phonetic algorithm (SOUNDEX?) to find similar records...
    – sfdcfox
    Commented Apr 17, 2014 at 19:56
  • my scenario is even more simple: a custom roll up field. I can't have some admin poking around and filling out field values when the system is supposed to be driving them. The only way seems to be a static variable that is basically set by a permitted operation/method and unset directly afterward. Then some apex based validation rules to run checks against the changes... If I figure out something good - I'll post Commented Apr 17, 2014 at 22:07
  • Custom rollup field you say... but admin will have to be able to write to that field when the record is being created and has no children yet. Even if the value will be defaulted to 0. I think the best you can do is to detect the changes to that field's value and figure out whether they're legit (static flag set in the relevant code) or you have to throw an error / overwrite the change with the right value.
    – eyescream
    Commented Apr 21, 2014 at 18:48
0

I've developed a simple system to control the write access against certain fields, on certain records.

The primary class - WriteAccessControl. This class is instantiated with a set of restricted fields that can be checked against in your objects validator class/method.

public class WriteAccessControl{

//The fields that have Write restrictions
public set<string> Fields {
    get {
        if(Fields == null) {
            Fields = new set<string>();
        }
        return Fields;
    } private set;
}

//the storage of permissions against fields against ids
private map<id,map<string,boolean>> WriteMap {
    get {
        if(WriteMap == null) {
            WriteMap = new map<id,map<string,boolean>>();
        }
        return WriteMap;
    }
    set;
}

//Set the access of an id, and field
public void SetAccess(id pId, string pField, boolean pAccess) {
    if(Fields.Contains(pField.ToUpperCase())) {
        if(WriteMap.get(pId) == null) {
            WriteMap.put(pId, new map<string, boolean>());
        }
        WriteMap.get(pId).put(pField.ToUpperCase(),pAccess);
    } else {
        throw new JNHException(JNHSystem.MessageCodes.Get('WRITE_CONTROL100'),pField.ToUpperCase());
    }
}

//Get the write access of the field:
//  pass in the Id of the record in question, and the name of field
//  Get back a boolean of TRUE if the field can be written to, or FALSE if not
public boolean GetAccess(id pId, string pField) {
    system.debug('UDBG:::WriteAccessControl.GetAccess: ENTERED with id: ' + pId + ', and field: ' + pField);
    boolean b = false;
    system.debug('UDBG:::WriteAccessControl.GetAccess: fields are currently: ' + fields + '. Does it contain ' + pField.ToUpperCase() + '? ' + (Fields.Contains(pField.ToUpperCase())));
    if(Fields.Contains(pField.ToUpperCase())) {
        //Check that we are in the write map
        if(WriteMap.get(pId) != null) {
            //And check the field is in the list
            if(WriteMap.get(pId).get(pField.ToUpperCase()) != null) {
                //And finally the boolean
                b = WriteMap.get(pId).get(pField.ToUpperCase());
            }
        }
    } else {
        throw new JNHException(JNHSystem.MessageCodes.Get('WRITE_CONTROL100'),pField.ToUpperCase());
    }
    return b;
}

//Construct with your set of fields
public WriteAccessControl(set<string> pFields) {
    for(string s : pFields) {
        Fields.add(s.ToUpperCase());
    }
}

}

To instantiate this in your validator class:

public static JNHSystem.WriteAccessControl WriteAccessControl{
get {
    if(WriteAccessControl == null) {
        WriteAccessControl = new JNHSystem.WriteAccessControl(new set<string>{
            'MyCustomField__c',
            'MyOtherCustomField__c'
        });
    }
    return WriteAccessControl;
}
private set;
}

And finally, when you need to access this via a validating method at the end of your business automation:

//The following field(s) are not writable at this time
public void PO116(){
    for(integer i : Context.NewRecords.KeySet()) {
        for(string field : PurchaseOrder_val.WriteAccessControl.Fields) {
            if( ((Context.IsNew(i) && Context.GetRecord(i).Get(field) != null) ||
                (Context.Ischanged(i,field))) && !PurchaseOrder_val.WriteAccessControl.GetAccess(Context.GetRecord(i).Id, field)) {
                Context.GetRecord(i).AddError(
                    new JNHException(
                        GetMessageCode('PO116'),
                        field
                ),false);
            }
        }
    }
}

Don't be confused by the above method - I have a class that serializes triggers and gives me helpers like "IsNew" or "IsChanged". the principle is that you want to check all records in context, and what circumstances you deem necessary.

I hope others find this useful.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .