Use case term based

Created on 12 November 2023, 8 months ago
Updated 20 November 2023, 7 months ago

Hi, thank you for this very promising module.

This is my use-case:
- I want the node permission to be controlled by a role field in taxonomy

For example:
- term A has a role reference field, and is associated to a role R
- node N has a term reference field, and is associated to term A
- user U is associated with role R and he can access the node N

If this is too much, then an alternative would be:
- term A has a list field, with a value P
- node N has a term reference field, and is associated to term A
- user U belongs to a specific role and he can access nodes with term that has a value P
- other users that don't belong to specific role, can't access

This would mimic the Permission by Term module, where roles are assigned to terms, and users get their access from there.

I have tried to make an Acces Policy but I can't find an Access Rule that matches this.

For a rule to match we need to have a direct field-to-field link:
- User (term) <-> (term) Node

but not

- User (role) <-> (role) Term (term) <-> (term) Node
or
- Node (term) <-> (term) Term (list) <-> (list) A

Is there a way to achieve this?

πŸ’¬ Support request
Status

Closed: works as designed

Version

1.0

Component

Documentation

Created by

πŸ‡΅πŸ‡ΉPortugal jrochate

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @jrochate
  • πŸ‡ΊπŸ‡ΈUnited States partdigital

    Hi jrochate, thanks for reaching out!

    We don't support the Permissions by Term approach because that's not technically a ABAC pattern. You would be managing your roles in multiple places which can quickly grow unwieldy.

    If you want to see a similar use case but implemented the "Access Policy way" take a look at this tutorial.
    https://www.drupal.org/docs/extending-drupal/contributed-modules/contrib... β†’

    With all that said, I can see a use case for the approach you described, especially if you're trying to migrate from Permissions by Term to Access Policy. You can still achieve it with some custom code. I have tested this code and it works.

    First create a new Access policy type. This tells Access Policy to only use Access rules.

    /**
     * Permissions by term access policy type.
     *
     * @AccessPolicyType(
     *   id = "perms_by_term",
     *   label = @Translation("Permissions by Term"),
     *   description = @Translation("Emulate permissions by term."),
     *   operations = {
     *     "view" = {
     *        "rules" = true,
     *     },
     *     "view all revisions" = {
     *         "rules" = true,
     *      },
     *     "update" = {
     *         "rules" = true,
     *      },
     *     "delete" = {
     *         "rules" = true,
     *      },
     *     "view unpublished" = {
     *         "rules" = true,
     *      },
     *     "manage access" = {
     *       "rules" = true,
     *      },
     *   }
     * )
     */
    class PermissionsByTerm extends AccessPolicyTypeBase {
    
    }
    

    Create a new access rule that will compare any roles found on the term with the current user.

    /**
     * @AccessRule(
     *   id = "term_role_reference",
     *   handlers = {
     *     "query_alter" = "\Drupal\example_module\AccessRuleQueryHandler\TermRoleReference"
     *   }
     * )
     */
    class TermRoleReference extends AccessRuleBase implements ContainerFactoryPluginInterface {
    
      /**
       * {@inheritdoc}
       */
      public function isApplicable(EntityInterface $entity) {
        return TRUE;
      }
    
      /**
       * {@inheritdoc}
       */
      public function validate(EntityInterface $entity, AccountInterface $account) {
        $field_name = $this->getDefinition()->getFieldName();
        $roles = $this->getRolesReferencedByTerms($entity, $field_name);
    
        if (empty($roles)) {
          return FALSE;
        }
    
        $account_roles = $account->getRoles(TRUE);
    
        $intersect = array_intersect($roles, $account_roles);
        if (!empty($intersect)) {
          return TRUE;
        }
    
        return FALSE;
      }
    
      /**
       * Get all the roles referenced by the taxonomy terms.
       *
       * @param \Drupal\Core\Entity\EntityInterface $entity
       *   The entity.
       * @param string $field_name
       *   The field name.
       *
       * @return array
       *   Array of role ids.
       */
      protected function getRolesReferencedByTerms(EntityInterface $entity, $field_name) {
        $terms = $entity->get($field_name)->referencedEntities();
        $roles = [];
        foreach ($terms as $term) {
          if ($term->hasField('field_role')) {
            $referenced_roles = $term->get('field_role')->referencedEntities();
            $roles = array_merge($roles, $referenced_roles);
          }
        }
        // Get the roles from the terms.
        return array_map(function ($role) {
          return $role->id();
        }, $roles);
      }
    }
    
    

    Create a query handler so that content is hidden from listing pages.

    /**
     * Term role reference query handler.
     */
    class TermRoleReference extends AccessRuleQueryHandlerBase {
    
      /**
       * {@inheritdoc}
       */
      public function query() {
        $this->ensureMyTable();
    
        $terms = $this->entityTypeManager->getStorage('taxonomy_term')->loadByProperties([
          'field_role' => $this->currentUser->getRoles(TRUE),
        ]);
    
        $term_ids = array_map(function($term) {
          return $term->id();
        }, $terms);
    
        if (!empty($term_ids)) {
          $this->query->condition($this->realField, $term_ids, 'IN');
        }
        // If no term ids found then never show on listing pages.
        else {
          $this->query->condition($this->realField, [0], 'IN');
        }
      }
    
      /**
       * {@inheritdoc}
       */
      public function ensureMyTable() {
        $base_field_placeholder = $this->query->getBaseTable() . '.' . $this->query->getBaseField();
        $this->query->leftJoin($this->tableAlias, $this->tableAlias, $this->tableAlias . ".entity_id = " . $base_field_placeholder);
      }
    
    }
    
    

    Tell access policy about this access rule:

    function example_module_access_policy_data() {
       $data = [];
        $data['node']['field_department_term_role_reference'] = [
          'label' => t('Department: Current user has role referenced by a term in this field.'),
          'plugin_id' => 'term_role_reference',
          'operator' => 'in',
          'entity_type' => 'node',
          'field' => 'field_department',
      ];
    }
    

    Now you should have everything you need to start to emulate permissions by term.

    1. Add a role entity reference field to the taxonomy term.
    2. Add the taxonomy term field to the content type
    3. Create a new Access policy of type Permissions by Term
    4. Add the Access rule: Current user has role referenced by a term in this field.
    5. Assign this access policy to the nodes.

    I probably won't include this in the module itself but perhaps I'll add a tutorial if there is enough demand.

    Thanks for the suggestion, it was fun to investigate this one!

  • πŸ‡΅πŸ‡ΉPortugal jrochate

    Wow! Thanks Joshua, for your kind help and clear explanation.

    I totally understand the code. is very clean and concise. Will give it a try.

    Meanwhile I'm still reading more about your ABAC approach and see if I could keep it strict, without replicating PbT using the above code.

    Once again, thank you very much about the work you've been doing here. Awesome!

  • πŸ‡ΊπŸ‡ΈUnited States partdigital

    You're welcome!

    I'm going to mark this issue as fixed. Feel free to open it again if you have any followup questions.

  • Status changed to Closed: works as designed 7 months ago
  • πŸ‡ΊπŸ‡ΈUnited States partdigital
Production build 0.69.0 2024