3

I have following requirements:

  • Users have (among others) role "tier1", "tier2" or "tier3". Higher value means more permissions and anonymous users are considered "tier0".
  • Users from tier2 and tier3 can create certain node type - MyNode. And only these users can update and delete them.
  • The node type MyNode contains (my custom) field for restricting access - it's a simple integer. 0 means all users can access it, 1 means users of tier 1-3 can access it, etc.

What's the proper way to implement the node restriction?

What I tried & longer description of the problem

After Googling and reading documentation, I found hook_entity_access. I simply iterated all fields and if I found my access field type, read the value and handle it. It was working pretty nicely for viewing, editing and deleting. But not for views - they completely ignored that.

After some more googling, I found that hook_entity_access/hook_node_access are ignored in views as explained on Node access rights. This page also explains that this problem should be solved by using hook_query_TAG_alter, however I can't understand how it should be used. It seems to me I'm missing some bigger picture and failed to google it. But I found another solution - to use hook_node_access_records with hook_node_grants. I implemented this solution (with a tutorial help).

So each instance of MyNode provide grant based on field value, e.g. gid 2 means tier 2-3 can access the node. Each user is given grants based on their tier role, e.g. user with tier1 receive grants with gid 1, 2 and 3. This fixed view problem, but caused another one - if I set access of a node to 3, then users of tier 0-2 cannot view it (that's correct), but user of tier 2 can edit it (that's problem).

After some testing, it seems like permissions are handled differently for view and update/delete. View grant permissions are AND-ed with basic permission system (/admin/people/permissions), but update/delete permissions are OR-ed. I used the basic permission system to determine which roles can generally create, update and delete MyNode nodes.

So my current solution is merge of both approaches - grant system for views and hook_node_access for working with nodes directly. However, it seems to me quite hacky, dirty solution - I practically implemented same thing twice in different systems, because each system can handle only subset of all operations. And so, my question is - what's the proper implementation? What is correct Drupal-way?

Note: of course, I looked at modules to solve this. I rejected most of them, because they're still in alpha/beta/rc phase, while lifetime of D8 is ending. How much time I would have to wait for D9 implementation? Others were imho too bloated for "simple" "compare numeric value with role and accept/reject".

My system: Drupal version: 8.8.3

4
  • hook_node_access_records() and hook_node_grants() are how I would go with this.
    – Jaypan
    Commented Mar 8, 2020 at 15:32
  • @Jaypan but if I use only these two, when users with tier2 role can edit nodes which are restricted to tier 3. In other words, without double-implementation using hook_node_access(), grants won't merge correctly with basic permission settings which are used to determine who can edit MyNode type...
    – Tom
    Commented Mar 8, 2020 at 16:18
  • I think maybe you haven't understood how to use those hooks properly. With hook_node_access_records(), you set up a series of conditions (basically the rules you laid out above) under which the access is granted when the node is accessed. In hook_node_grants(), you outline the current variables under which the node is being accessed (current user, env vars etc). The access records and grants are overlaid, and if they match, access is granted. These hooks allow node-specific access grants, based on a set of rules - which is what I've understood you to be asking, though maybe mistakenly.
    – Jaypan
    Commented Mar 8, 2020 at 17:42
  • I apologize, I somehow missed your second comment. I implemented it mostly as you described. Only exception is that I use these hooks primarily for view rights and use standard/basic permission system (on path /admin/people/permissions) to configure which user can also edit. But it seems to me that if access to view is denied by grant system, but provided by standard/basic permission system, it's denied as result (permissions are ANDed). However, if access to update/delete is denied by grant system, but provided by standard/basic permission system, it's grated as result (permissions are ORed).
    – Tom
    Commented Mar 8, 2020 at 20:03

2 Answers 2

2

You can implement hook_node_access_records() and hook_node_grants() for this.

The idea is that in hook_node_access_records(), you take the node as it is saved, and create a set of rules by which it can be accessed. These are saved as a row in the {node_access} records table in the database. Then, when the user tries to access the node, their account is provided, and you then build a set of records explaining the current user's information. These records are then compared with the rules by which the node can be accessed, as set in hook_node_access_records(), and if there is a match, access is granted.

I believe this should get you on your way:

/**
 * Implements hook_node_access_records().
 */
function example_node_access_records(\Drupal\node\NodeInterface $node) {
  // Check the content type.
  if ($node->bundle() == 'content_type_with_tier_level') {

    $grants = [];

    // Get the tier level as it has been set on
    // the node.
    $tier_level = (int) $node->get('tier_level')->value;

    // Create access rules based on the tier level.
    switch ($tier_level) {
      // Tier 0.
      case 0:
        // Create a viewing grant for all users
        $grants[] = [
          'realm' => 'tier_view_level',
          // This will be the tier level of the
          // accessing user.
          'gid' => 0,
          'grant_view' => 1,
          'grant_update' => 0,
          'grant_delete' => 0,
        ];
        // Create an admin grant for tier 2 users
        $grants[] = [
          'realm' => 'tier_level_admin',
          // This will be the tier level of the
          // user accessing the node.
          'gid' => 2,
          'grant_view' => 1,
          'grant_update' => 1,
          'grant_delete' => 1,
        ];
        // Create an admin grant for tier 3 users
        $grants[] = [
          'realm' => 'tier_level_admin',
          'gid' => 3,
          'grant_view' => 1,
          'grant_update' => 1,
          'grant_delete' => 1,
        ];

        break;

      // Tier 1.
      case 1:
        // Create a viewing grant for tier 1
        $grants[] = [
          'realm' => 'tier_view_level',
          // This will be the tier level of the
          // accessing user.
          'gid' => 1,
          'grant_view' => 1,
          'grant_update' => 0,
          'grant_delete' => 0,
        ];
        // Create an admin grant for tier 2 users
        $grants[] = [
          'realm' => 'tier_level_admin',
          // This will be the tier level of the
          // user accessing the node.
          'gid' => 2,
          'grant_view' => 1,
          'grant_update' => 1,
          'grant_delete' => 1,
        ];
        // Create an admin grant for tier 3 users
        $grants[] = [
          'realm' => 'tier_level_admin',
          'gid' => 3,
          'grant_view' => 1,
          'grant_update' => 1,
          'grant_delete' => 1,
        ];

        break;

      // Tier 2.
      case 2:
        // Create a viewing grant for tier 2
        $grants[] = [
          'realm' => 'tier_view_level',
          // This will be the tier level of the
          // accessing user.
          'gid' => 2,
          'grant_view' => 1,
          'grant_update' => 0,
          'grant_delete' => 0,
        ];
        // Create an admin grant for tier 2
        $grants[] = [
          'realm' => 'tier_level_admin',
          // This will be the tier level of the
          // user accessing the node.
          'gid' => 2,
          'grant_view' => 1,
          'grant_update' => 1,
          'grant_delete' => 1,
        ];
        // Create an admin grant for tier 3
        $grants[] = [
          'realm' => 'tier_level_admin',
          'gid' => 3,
          'grant_view' => 1,
          'grant_update' => 1,
          'grant_delete' => 1,
        ];

        break;

      // Tier 3.
      case 3:
        // Create a viewing grant for tier 3
        $grants[] = [
          'realm' => 'tier_view_level',
          // This will be the tier level of the
          // accessing user.
          'gid' => 3,
          'grant_view' => 1,
          'grant_update' => 0,
          'grant_delete' => 0,
        ];

        // Create an admin grant for tier 3
        $grants[] = [
          'realm' => 'tier_level_admin',
          'gid' => 3,
          'grant_view' => 1,
          'grant_update' => 1,
          'grant_delete' => 1,
        ];

        break;
    }

    return $grants;
  }
}

/**
 * Implements hook_node_grants().
 */
function example_node_grants(\Drupal\Core\Session\AccountInterface $account, $op) {
  // Build an array of the user's tier levels.
  // All users are allowed to access tier0, so
  // this level is added by default.
  $tier_levels = [0];

  // Add tier 1 to the user's tier levels if they
  // have the tier1 role.
  if ($account->hasRole('tier1')) {
    $tier_levels[] = 1;
  }

  // And so on...
  if ($account->hasRole('tier2')) {
    $tier_levels[] = 2;
  }

  if ($account->hasRole('tier3')) {
    $tier_levels[] = 3;
  }

  // The key is the realm, the value is an array
  // containing one or more GIDs to be compared
  // to the access records. If a matching realm
  // and GID are found, access is granted.
  $grants['tier_view_level'] = $tier_levels;
  $grants['tier_level_admin'] = $tier_levels;

  return $grants;
}
5
  • This solution is very similar to what I already implemented and tried to explain that in both my original question and answer to your comment. That means I still failed to explain what problem I have with this solution and I apologise for that. BUT, I'm trying to read between lines to get the answer, so I would like to confirm it; the main difference between your solution and mine is, that I wanted to configure which tier can edit/delete nodes using standard/basic permission system (/admin/people/permissions).
    – Tom
    Commented Mar 8, 2020 at 19:40
  • Your solution, however, gives these permissions using grant system. So does that mean that my problem is in using grant system AND standard/basic permission system together? Does that mean, that the clean, drupal-way is to use either grant system, or standard/basic permission system, but not together? In other words, do I HAVE to uncheck all permission checkboxes related to my node type and handle them in grant system? (For example through settings of my custom field.)
    – Tom
    Commented Mar 8, 2020 at 19:40
  • These questions are best answered by testing. I wrote the code above off the top of my head, mileage may vary.
    – Jaypan
    Commented Mar 8, 2020 at 21:21
  • First, I apologize for late reply; I can work on this project only during (some) weekend days. First, I disagree with "these questions are best answered by testing", I think they should be answered by knowledge. Anyway, your answer lead me indirectly to the questions stated above which lead me to search how the conflicting permissions are implemented. So, there is node_node_access function in file node.module (in core Drupal code) which grants permissions to update and delete nodes based on configuration of standard permission system (both "any" and "own" variants).
    – Tom
    Commented Mar 14, 2020 at 10:00
  • But this function does not handle view operation, which makes behaviour different for view and update ops. That means the answer to my first question is yes - the problem is in using both grant and standard permission system. I still don't know what the clean, Drupal-way is, but if you include this information (plus that one of the solutions is to deny update/delete operations in custom hook_node_access if view operation is rejected by grant system), I'll accept it and settle with my current knowledge.
    – Tom
    Commented Mar 14, 2020 at 10:00
2

What's the proper way to implement the node restriction?

Don't re-invent the wheel ... The Group module is what you should be looking at. It creates groups as entities, making them fully fieldable, extensible and exportable. For this specific question, you'd enable the gnode submodule, and for each group type you would define the appropriate permissions (view, edit, delete, etc) for the various Content Types.

More info

Resources

3
  • I'm sorry, but I'm really afraid of using module, which is still in rc phase for D8 and beta phase for D7. With D8 being soon replaced by D9, it seems to me quite unreliable. What I'm actually working on is porting very old web to D8 and after I used lots of modules which were discontinued, I'm currently taking a very careful approach - I have quite strict criteria for adding a module and rc-phase with 8k users doesn't pass.
    – Tom
    Commented Mar 8, 2020 at 19:52
  • 1
    Group is very stable and well supported. I'm using it on a project with no bugs.
    – Jaypan
    Commented Mar 8, 2020 at 21:20
  • 1
    fully agree with @Jaypan ...! Commented Mar 9, 2020 at 18:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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