Modifying Commerce Line Item Access control in Drupal Commerce 7

Drupal Commerce provides a number of “entities” that serve as content containers for the various elements of an e-commerce order. These entities include products, customer profiles (addresses), and orders. In addition, Drupal Commerce uses a “line item” entity to specify entries outlining products, tax and shipping fees that make up an order.

In most cases, access to an entity is governed by ownership or “authored by” property. Access control to view, edit or delete a particular entity is typically defined by whether someone “owns” a particular entity or whether a user has a blanket authority to interact with all entities of that particular type. For example, we may give one set of users the permission to edit their own content of a particular type, while provide another set of users the ability to edit and delete any content of a particular type, regardless of whether they authored it.

An example of setting access rules in Admin -> People -> Permissions
An example of setting access rules in Admin -> People -> Permissions

 

Specifically, access control for an entity is defined in Drupal when the entity itself is defined using “hook_entity_info.”  Within the array that defines the entity, there is a key; “access callback,” that defines a function that is called to determine whether a user can view or otherwise modify an entity. With Drupal Commerce, access to an entity winds its way to a common “commerce_entity_access” function.  In order to determine if a user has access to “view” a commerce entity, this function specifies a query which would return true based on the conditions assigned to it, such as whether the entity is “owned” by a particular user.

The conditions assigned to this query are included by way of other functions that are identified by a naming convention specified by the “access tag” key in the array used to define the entity in hook_entity_info.  The access key is used by “hook_query_TAG_alter” which can be used in a custom module to modify the conditions of the query, where TAG is replaced by the value of “access tag.”  The interesting thing is that if there are no other modules utilizing the hook_query_TAG_alter, “commerce_entity_access” will allow all users to view the commerce entity.

Recently, I’ve been working in Drupal Commerce on a method to provide selective access to order information to certain “salesperson” users who are neither “owners” of the order or site administrators. In this situation, a salesperson has been assigned a certain subset of users and has the authority to initiate orders for existing, custom customer profiles that are associated by way of an user reference to each customer profile.

A simple model illustrating how salesperson is referenced by a field added to a custom customer profile.
A simple model illustrating how salesperson is referenced by a field added to a custom customer profile.  The author property is built in by default.

 

In a custom module I am developing, I want to give a salesperson access to view the orders of their assigned users.  As it turned out, it was fairly straightforward to override access control to menu driven content to show the order information.  Specifically, I was able to use hook_menu_alter to change the “access_callback” for the order page detail to my own function.

<?php
function MY_MODULE_menu_alter(&$items) {
$items['user/%user/orders/%commerce_order']['access callback'] = 'MY_MODULE_customer_order_view_access'; // my custom access function
}
?>

In this case, I added a check to see if the salesperson was referenced by the order’s customer profile before showing the order.  This worked great for the order content, however the view that shows the various line items associated with the order showed no results.  It must be that the access control for the line items was incorporated as conditions to the query that generated the view.  The question was, what query conditions were being added and what could I do to modify these conditions to be able to show these line items.

There are a couple Drupal modules that were essential to diagnosing the issue.  First is the Devel module, which provides a number of helper functions for displaying variables, arrays and objects so that you can identify what data you have available.  Another is Views Show Query, which allows you to see the actual views query in preview after all of the query_alter hooks have run as well as specify the user id for the query.  Of course, you will first need to also have the Show SQL Query box checked on the Views settings page (/admin/structure/views/settings).

It actually took some effort to figure out that “hook_query_TAG_alter” was a  solution.  However, in searching for line item access issue, I came across a post on Drupal.org on the question of how to allow “view” override for commerce_entity_access.  The first response to the question, Ryan Szrama suggests using hook_query_TAG_alter since the entity has an access tag that can be referenced.  Using the “access tag” for the line item entity, I was able to search for any existing functions that were using that tag to alter the query.  I found the existing function, “commerce_line_item_query_commerce_line_item_access_alter” which helped me better understand how queries were altered, as well as gave me a starting point for my own hook_query_TAG_alter function:

<?php
function MY_MODULE_query_commerce_line_item_access_alter(QueryAlterableInterface $query) {
  // Read the account from the query if available or default to the current user.
  if (!isset($account) && !$account = $query->getMetaData('account')) {
    global $user;
    $account = $user;
  }
  // If the user has the administration permission, nothing to do.
  if (user_access('administer line items', $account)) {
    return;
  }
 
  if (user_access('view customer profile orders')) { // permission assigned
    $ids = explode(',',$query->getMetaData('view')->args[0]); // get the line item id arguments and put in array
    if ($line_item = commerce_line_item_load($ids[0])) {  // load the first argument
      $order_wrapper = entity_metadata_wrapper('commerce_order', $line_item->order_id);
      $profile_reference = $order_wrapper->commerce_customer_customer->value(); // get the custom customer profile
      $profile_wrapper = entity_metadata_wrapper('commerce_customer_profile', $profile_reference->profile_id);
      $salesperson = $profile_wrapper->field_sales_representative->value(); // get the salesperson user reference
      if (($salesperson) && ($salesperson->uid == $account->uid)) {
        $where =& $query->conditions();
        unset($where[1]); //removes conditional for access to order
      } 
    }
  }
}
?>

I could go through a long explanation of the various ways I banged my head against trying to understand the code (and I’m still trying to understand it), but instead, let me explain some key points that helped me get a handle on how I could modify the access control to line items:

  1. The $order object associated with the line item isn’t available, so if I want to be able to get that information in order to determine if I give access I am going to have to reference it by the query itself.  I can add “dpm($query->getMetaData(‘view’)) to the function in order to get an idea of how I can access the line item numbers as a starting point.  In this case, they are in the arguments passed to the view.
  2. The view arguments are a comma separated list of line item ids, which I explode into an array.  Right now I am only passing the first id through a series of steps to eventually find the user id of salesperson associated with a customer profile that is associated with the order to which the line item is attached.  If this salesperson user id matches that of the user requesting the line item, then we will authenticate them to view it.
  3. There may be security implications to just using the first line item id in the list of arguments if there could be a situation where the first id would pass authentication, but it would be followed by other id’s where the user should not have access.  At this point, I don’t know a situation where this would obviously happen, but it could occur if there was an error in arguments passed to the view.
  4. The query conditions are returned by reference to a $where array.  In this case, we simply remove the key in the array that has the various access control additions, allowing the line items to be accessed.  In that post response, Ryan Szrama indicates that we could just insert another OR condition into the query, but my attempts to do this could not place the OR condition to offset the other conditions.  Removing all the access conditions seems to work at this point, however.  More information of query alternation here.

My next step will be to include User Orders view to include those orders made by users for customer profiles that reference the salesperson.  I believe that can be done by adding a custom validation function to the view’s contextual filter form.  Stay tuned…