Adding Access Control to Drupal 7 List Field Allowed Values

I recently had a need to restrict the allowed values on a Drupal list field to certain roles. So users of one role would be able to choose from any of them, but users of another role could only choose a few of them. This was a text list field with checkboxes as the widget.

You might say that these options should be in a separate field, and you'd probably be right! But in my case this field was already in place, with many other items on the website already configured to use the values of this field, when the request came in to restrict access to the options. So it was either add the access control to this field's options, or add a new field and redo all of the other parts of the site. This ended up a lot quicker.

I'm no stranger to custom access control on nodes, fields, users, and lots of other areas of Drupal. And when it comes to form items it's usually as simple as setting #access to FALSE on the field in a hook_form_alter(), but this was the first time I needed to add access control to one of the options in the field, not the field itself, and a few issues came up.

For the purposes of explanation here, let's assume I have a "Staff Access" field with five options and I'm adding this code to a custom List Value Access module.

Staff Access Field Options

We want our Content Admin role to be able to select all of the options. But we also have another Article Editor role that we only want to be able to select from the Interns, Full Time, and Assistant Managers options.

Where to set the access?

The first issue was where I could actually set the access. I inspected the form structure with dpm() in a hook_form_alter() only to find that although the allowed values were listed in the field's #options array as expected, I couldn't see anywhere to set the access on those values. After some colourful language and a healthy amount of googling, I came across a post on Stack Exchange that explained an undocumented way of setting access on the options.

As it turns out you can just add your own array to the field that's keyed with the machine name of the option, then set access accordingly, or set the field to be disabled. Either of those was fine in this circumstance. Normally I don't believe in showing users items that they can't select, but setting the option to be disabled instead of disallowing access to it made it easier for me to see what was going on during development. So I ended up with something like this in my hook_form_alter():

<?php
$form
['field_staff_access'][LANGUAGE_NONE]['managers']['#disabled'] = TRUE;
?>

That disables the Managers option. In that example "field_staff_access" is the machine name of the field, and "managers" is the machine name, or key, that was added in the field UI for that option.

To completely remove that option from the field it would look like this:

<?php
$form
['field_staff_access'][LANGUAGE_NONE]['managers']['#access'] = FALSE;
?>

Cool! That's just like we would normally set an entire field to be disabled or deny access to it. Only this time it's just on the option we want in the field.

But obviously that doesn't allow for different roles having different access to the options, so we need to add a way to determine who can select what option.

Add Custom Permissions

Since in my case different roles had access to different options, using Drupal's permission system was a no brainer. First, we need to get the allowed values for that field and create a permission for each one using hook_permission().

<?php
/**
 * Implements hook_permission().
 */
function list_value_access_permission() {
 
$perms = array();
 
 
// Get the allowed values from our field.
 
$field = field_info_field('field_staff_access');
 
$allowed_values = list_allowed_values($field);
 
 
// Create a permission for each allowed value.
 
foreach($allowed_values as $key => $label) {
   
$perms['assign ' . $key . ' staff access'] = array(
     
'title' => t('Assign %label staff access', array('%label' => $label)),
     
'description' => t('Allows the user to select the %label value in the Staff Access field.', array('%label' => $label)),
    );
  }
 
  return
$perms;
}
?>

Now we can assign our roles the permissions we want so they can only select the options we want them to.

List Value Access module permissions

With our permissions now properly set up we now just need to loop through all of the options in the form, check if the user has access to assign that value, and if not, make sure they can't select, or deselect, that option.

<?php
/**
 * Implements hook_form_alter().
 */
function list_value_access_form_alter(&$form, &$form_state, $form_id) {
 
 
// Restrict options available on Staff Access field.
 
if (isset($form['field_staff_access'])) {
   
    foreach(
$form['field_staff_access'][LANGUAGE_NONE]['#options'] as $key => $label) {
      if (!
user_access('assign ' . $key . ' staff access')) {
       
$form['field_staff_access'][LANGUAGE_NONE][$key]['#disabled'] = TRUE;
      }
    }
  }
}
?>

Now if a user with the Article Editor role adds or edits an article, the options they don't have permission to assign will be disabled. In this case they are editing a node that was already created by a user who had permission to add all options:

Disabled options

The Field Loses Data

So now our Article Manager has made the edits she needs to do and the node is saved. Oh no! The values that were disabled, and in our case added by another user, are gone. This was a big bummer for me at this point and even more colourful language was shouted.

Missing data

In my experience with setting access control on fields, if a user doesn't have permission to edit that field, the field doesn't lose its values when the node is edited. But in this case that does happen on the individual values. And it happens if the list option is set to be disabled or access is denied.

Ah, Crap! Now What?

I was quite pleased with getting this working up until this point, but now a major problem has cropped up since we can't have that field losing data. So we need a way to determine if the field already had any values set that the current user doesn't have permission to add. Good thing we can access the default value of the field in the form! That's the data that's already been saved in the field and we can use that to repopulate the missing data when the form is submitted.

To do so we need to add a custom validation callback to the form. So in our hook_form_alter() we just need to add:

<?php
array_unshift
($form['#validate'], 'list_value_access_add_default_values');
?>

We're using array_unshift() to make sure our function named "list_value_access_add_default_values" is called first.

In that function we need to check if the form had any default values that the current user doesn't have access to add, and then set that field value to contain the values our user did add, as well as the values she can't modify.

First we need to get the values set when the form was submitted. Then loop through the default values and check if a value was already set that the current user can't add. If so, we add that back in.

<?php
/**
 * Validation function to add in default values that the current user may not have access to assign.
 */
function list_value_access_add_default_values($form, &$form_state) {
 
 
// Get the values entered in the form.
 
$all_values = $form_state['values']['field_staff_access'];
 
 
// Check if the current user cannot assign any default values and make sure they get added to form values.
 
foreach ($form_state['complete form']['field_staff_access'][LANGUAGE_NONE]['#default_value'] as $key) {
    if (!
user_access('assign ' . $key . ' staff access')) {
     
$all_values[LANGUAGE_NONE][] = array('value' => $key);
    }
  }
 
 
// Set the field values.
 
form_set_value($form['field_staff_access'], $all_values, $form_state);
}
?>

Hooray!

Now it all works. Our user can't set certain values, and when the form is submitted no data is lost.

I'm not really sure if this is the best approach or not since I don't like the idea of having to add values back into the field, but it works so I'm happy. I was a bit surprised this happened at all, but as with most Drupal issues, there's going to be a way to make it happen. So if you've dealt with this sort of thing before, please let me know how you solved it.

Example Module

I've added an example module on GitHub that contains all of the code used here. It's not really meant for public use but there's no harm in doing so, just change "field_staff_access" to whatever the name of your field is, and update the permission strings accordingly.

It's important to note that I've only tried this with checkboxes, so I'm not sure how well this will work with any other selection widgets.

I can see this being useful for other people, so I might go ahead and create a full project out of this at some point. So if it's something you'd want to use just leave some feedback in the comments or the module's issue queue.

Comments

Hi !
I have change the name of my field but it doesn't work !
great code but I have this :
Warning : Invalid argument supplied for foreach() dans list_value_access_permission() (ligne 18...)

Do you have an idea ?

I use drupal 7.41 and all the module are update

thx
Paps

Is your field a multiple value field? That foreach on line 18 is looping over the $allowed_values variable which should be an array of the allowed values in your field. I'd inspect that in a dpm() to make sure it actually contains your allowed values.

Add new comment