7

I have a custom entity. There is an access callback defined in hook_entity_info() and it is called when I access the entity. Also in views when I select to display rendered entities, the entity is only shown when the user is allowed to see the entity.

However when I change the view to show 'fields' instead of 'rendered entities', the access callback is no longer called and users can see all fields (and properties) of the entity, regardless of any permissions. Looking at the executed query this makes sense, the field values are joined in and the entity is never really loaded.

So, how should one implement entity access for views when displaying fields (in Drupal 7)?

I've found How to check for entity type in Views when using hook_field_access?, but I assume that only works for joined fields and not for base entity properties, so that would be only a part of a solution.

  • Have you considered using a custom view_mode? – Darvanen Jul 25 '16 at 6:41
  • @Darvanen yes, but I need the system to be fool proof. So I cannot assume everybody will use rendered entities. – Neograph734 Jul 25 '16 at 13:45
  • @Neograph734 That entity of yours is a node or something else ? – mchar Jul 28 '16 at 8:23
  • @mchar It is a custom entity constructed with the Entity API module. – Neograph734 Jul 28 '16 at 8:28
  • I think (correct if I am wrong) that the solution to your issue is the implementation of hook_node_grants and hook_node_access_records but I am not sure if these hooks apply to custom entities (hopefully yes), at least this is the most efficient way to control access as far as it concerns nodes in Drupal. – mchar Jul 28 '16 at 8:41
6
+200

Presuming your entity providing module has Views integration already setup, and you're not worried about pagination, you can invoke hook_views_pre_render() to iterate over the results and invoke your access callback for each entity in your base table and filter out entries that the user doesn't have access to:

/**
 * Implements hook_views_pre_render().
 */
function MYMODULE_views_pre_render(&$view) {
  global $user;

  // Iterate over View results for our custom entity
  if ($view->base_table == 'my_entity_base_table') {
    foreach ($view->result as $index => $row) {

      // Presuming eid is the entity PK
      $results = entity_load('my_entity_machine_name', array($row->eid));
      if (!empty($results)) {
        $entity = $results[$row->eid];

        // If the custom access callback returns FALSE, remove from results.
        if (!MYMODULE_my_entity_access_callback('view', $entity, $user)) {
          unset($view->result[$index]);
        }
      }
    }
  }
}

If pagination is a concern, it's a tougher problem; adjusting the view results break consistent offsetting (e.g. page 1 might return 4 results, page 2 might return 10 results). Plus, the results of SQL query can't adjust for information that is only known by executing PHP.

In those instances, you'll have to adjust your method (e.g. hook_views_query_alter() if the access callback is DB query based, altering your View pager options, etc.) to accommodate for the access callback manipulating the view.

  • Though this is definitely a start, it breaks the views pager (not 25 items per page but less, sometimes even 0). Is there a way to process this before the pagination is calculated? – Neograph734 Jul 24 '16 at 10:59
  • It appears the pagination is already in the query: ... LIMIT 25 OFFSET 0, so the only way to keep pagination working would be to act before the query is completed. But I cannot embed my access callback in the query. So it cannot be done? – Neograph734 Jul 24 '16 at 14:00
  • I didn't even think about it since it wasn't mentioned. Anyway, the views object contains the $query property which pager property can be manipulated to reflect the altered count. – Shawn Conn Jul 24 '16 at 17:39
  • Neither did I Shawn, but I ran into it when implementing your solution. This doesn't quite work yet (you set the total amount of items to the amount the query returned, so it will never be bigger then the amount of items per page (25)). I'll try this with a separate count query to obtain the total amount of items. – Neograph734 Jul 25 '16 at 13:48
  • Apologies, that was a hasty edit when I was crunched for time. I thought I'd previously solved this exact problem, but upon revisiting that code, it was instance where we could get by with having fuzzy pagination. I updated the answer accordingly. – Shawn Conn Jul 28 '16 at 3:13
0

In the end I've managed to find a working method which works in a similar way as the views does with nodes.

In hook_views_data() (or hook_views_data_alter()), make sure to add the access query tag table key. You can see Views does this for nodes as well in node_views_data().

$data['example_table']['table']['base'] = array(
  'field' => 'nid', // This is the identifier field for the view.
  'title' => t('Example table'),
  'help' => t('Example table contains example content and can be related to nodes.'),
  'weight' => -10,

  'access query tag' => 'my_entity_access' // <- Add this.
);

Then add your own implementation of hook_query_TAG_alter. This will alter every query where this tag is added. Because of our above alteration this will be automatically applied to all Views data listings, but the tag can also be added manually.

There are some great tricks in _node_query_node_access_alter() called from node_query_node_access_alter() (node modules implementation of hook_query_TAG_alter).

function mymodule_query_my_entity_access_alter(QueryAlterableInterface $query) {
  global $user;

  // Read meta-data from query, if provided.
  if (!$account = $query->getMetaData('account')) {
    $account = $user;
  }
  if (!$op = $query->getMetaData('op')) {
    $op = 'view';
  }

  // From here every query will be different depending on your own needs.
  // Since my entity has a privacy parameter that is either public or private,
  // I chose to implement this as follows:

  // Prepare a database OR.
  $or = db_or();

  // If the user has public view permissions, add it to the OR.
  if (user_access('view public my_entities', $account)) {
    $or->condition('example_table.privacy', 'public');
  }

  // If the user has non-public view permissions, add it to the OR.
  if (user_access('view private my_entities', $account)) {
    $or->condition('example_table.privacy', 'public', '<>');
  }

  // Add the compiled set of rules to the query. 
  $query->condition($or);
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.