The buildQuery() method is used by taxonomy_term_load_multiple() and overrides the base method DrupalDefaultEntityQuery::buildQuery().

Simply put: loader functions cannot have access control restriction tags, since loaders provide the information necessary to make access control determinations.

If we look at https://api.drupal.org/api/drupal/includes!entity.inc/function/DrupalDef... we see that 4 methods override this function. Only TaxonomyTermController adds an access control flag.

This is a bug in the implementation. It appears to be fixed in Drupal 8.

Comments

agentrickard’s picture

Status: Active » Needs review
StatusFileSize
new648 bytes

Let's see what tests this breaks...

xjm’s picture

This will break TAC. Period. :)

xjm’s picture

Also this is a BC break generally; I don't understand what the point of this patch against 7.x is?

xjm’s picture

I'd like to mark this wontfix unless there's an actual manifest bug here.

agentrickard’s picture

The bug is that the behavior is categorically wrong. No other _load() function or method enforces access controls. Access controls are determined based on the results of a load(). The reason that tests don't fail is that core doesn't use this feature.

The bug did manifest in contrib, when someone tried to use taxonomy_term_load_multiple() as part of an autocomplete. #2072661: PDOException: SQLSTATE[42S22]: Column not found: 1054 Unknown column t.tid in where clause

I don't see where this breaks TAC, which only calls taxonomy_term_load_multiple() within _taxonomy_access_autocomplete_validate().

Note also that the core taxonomy_autocomplete doesn't use this method. It uses a proper db_select(), which reinforces that the logic here is wrong.

Should we won't fix this for 7? Maybe. This has been fixed in D8, and we may simply be saying that it's too late to fix this bug. The purist in me does not like that answer.

agentrickard’s picture

Perhaps a better reason to not fix this is that I would call a similar logic error in taxonomy_get_parents(), taxonomy_get_children() and taxonomy_get_tree(), which all tag for access control filtering.

Those are lookup functions that would be better served by attaching #access data the way that the menu system does.

agentrickard’s picture

Title: TaxonomyTermController::buildQuery() tags for access control » Taxonomy load system filters for access control
Version: 7.x-dev » 8.x-dev
Priority: Normal » Major
Status: Needs review » Needs work

Those three functions are still present in D8, so that discussion should be had.

Here's why this logic is wrong.

taxonomy_get_tree "[Returns] an array of all term objects in the tree."

taxonomy_term_load_parents "[Returns] an array of term objects which are the parents of the term $tid."

taxonomy_term_load_children "[Returns] an array of term objects that are the children of the term $tid."

But that is not what these functions do. They return all elements (tree, parents, children) that the current request has permission to access. Now, if these were functions for display, that would be fine. But these are internal API calls. Simple loaders. Loaders should do one thing: load.

These documentations of the API are all incorrect. The expectation should be to return _all_ items, with flags for access for the current request.

Either that or the documentation of the API should change.

The reason this has persisted for so long is that nothing in core actually uses hook_query_term_access_alter().

agentrickard’s picture

Status: Needs work » Needs review
StatusFileSize
new1.19 KB

Curious to see if any core tests break when we remove that logic.

agentrickard’s picture

Aaaand I think that proves my point.

berdir’s picture

No, it doesn't ;) That there is no test coverage of term_access doesn't mean that it can be removed, see #1947270: Add test coverage for the term_access query tag.

Yes, the term_access in buildQuery() was removed because that was incorrect. The issue above was opened as a follow-up when we removed it from there.

Those other methods aren't simple loader functions that load based on id's. They're query + load, so they have to respect term_access. There is no other way to apply access control for those. If other places don't, then that needs to be fixed.

agentrickard’s picture

So there is no API for getting term parents consistently? Or the tree? That result will always be variable based on access control?

That strikes me as a very large bug and it suggests, at the least, that we need two separate functions: one to apply access filtering and one that does not.

In the rest of core entities, the burden of access is on the caller to these type of loading functions. Compare (yes, it's deprecated) node_load_multiple().

My point is that this is horrible DX and should be fixed. Taxonomy can't be the one odd case here. We should have a consistent API.

agentrickard’s picture

It's a shame I didn't find this issue until now, and I'm about to go on vacation (offline) for two weeks.

One further bit of piling on here before then: The term_access query flag doesn't have an operation assigned to it, which means that the query alter system will assume "view" (or, in reality, that all alters are the same for all conditions).

I don't think that works in the use-case I have for taxonomy access, in which users are restricted in what terms they can assign to nodes, but not restricted in any other sense.

xjm’s picture

Status: Needs review » Needs work

The last submitted patch, 8: drupal-2073663-taxonomy-term-load-8.patch, failed testing.

a_thakur’s picture

Assigned: Unassigned » a_thakur
Issue summary: View changes
mgifford’s picture

Assigned: a_thakur » Unassigned

Just unassigning issues that haven't been developed for a bit in the D8 queue.

aditya.ghan’s picture

Category: Bug report » Task

I had a discussion with @xjm regarding this task.

I believe this is not a bug, but rather a new task. Indeed, the taxonomy load functions in Drupal 8 (Version 8.0.x - development) use the term access tag when loading terms. Given below is one example of such a load function. Hence, changing the type of this issue to task.

/**
   * {@inheritdoc}
   */
  public function loadTree($vid, $parent = 0, $max_depth = NULL, $load_entities = FALSE) {
    $cache_key = implode(':', func_get_args());
    if (!isset($this->trees[$cache_key])) {
      // We cache trees, so it's not CPU-intensive to call on a term and its
      // children, too.
      if (!isset($this->treeChildren[$vid])) {
        $this->treeChildren[$vid] = array();
        $this->treeParents[$vid] = array();
        $this->treeTerms[$vid] = array();
        $query = $this->database->select('taxonomy_term_field_data', 't');
        $query->join('taxonomy_term_hierarchy', 'h', 'h.tid = t.tid');
        $result = $query
          ->addTag('term_access')
          ->fields('t')
          ->fields('h', array('parent'))
          ->condition('t.vid', $vid)
          ->condition('t.default_langcode', 1)
          ->orderBy('t.weight')
          ->orderBy('t.name')
          ->execute();
        foreach ($result as $term) {
          $this->treeChildren[$vid][$term->parent][] = $term->tid;
          $this->treeParents[$vid][$term->tid][] = $term->parent;
          $this->treeTerms[$vid][$term->tid] = $term;
        }
      }

      // Load full entities, if necessary. The entity controller statically
      // caches the results.
      $term_entities = array();
      if ($load_entities) {
        $term_entities = $this->loadMultiple(array_keys($this->treeTerms[$vid]));
      }

      $max_depth = (!isset($max_depth)) ? count($this->treeChildren[$vid]) : $max_depth;
      $tree = array();

      // Keeps track of the parents we have to process, the last entry is used
      // for the next processing step.
      $process_parents = array();
      $process_parents[] = $parent;

      // Loops over the parent terms and adds its children to the tree array.
      // Uses a loop instead of a recursion, because it's more efficient.
      while (count($process_parents)) {
        $parent = array_pop($process_parents);
        // The number of parents determines the current depth.
        $depth = count($process_parents);
        if ($max_depth > $depth && !empty($this->treeChildren[$vid][$parent])) {
          $has_children = FALSE;
          $child = current($this->treeChildren[$vid][$parent]);
          do {
            if (empty($child)) {
              break;
            }
            $term = $load_entities ? $term_entities[$child] : $this->treeTerms[$vid][$child];
            if (isset($this->treeParents[$vid][$load_entities ? $term->id() : $term->tid])) {
              // Clone the term so that the depth attribute remains correct
              // in the event of multiple parents.
              $term = clone $term;
            }
            $term->depth = $depth;
            unset($term->parent);
            $tid = $load_entities ? $term->id() : $term->tid;
            $term->parents = $this->treeParents[$vid][$tid];
            $tree[] = $term;
            if (!empty($this->treeChildren[$vid][$tid])) {
              $has_children = TRUE;

              // We have to continue with this parent later.
              $process_parents[] = $parent;
              // Use the current term as parent for the next iteration.
              $process_parents[] = $tid;

              // Reset pointers for child lists because we step in there more
              // often with multi parents.
              reset($this->treeChildren[$vid][$tid]);
              // Move pointer so that we get the correct term the next time.
              next($this->treeChildren[$vid][$parent]);
              break;
            }
          } while ($child = next($this->treeChildren[$vid][$parent]));

          if (!$has_children) {
            // We processed all terms in this hierarchy-level, reset pointer
            // so that this function works the next time it gets called.
            reset($this->treeChildren[$vid][$parent]);
          }
        }
      }
      $this->trees[$cache_key] = $tree;
    }
    return $this->trees[$cache_key];
  }
xjm’s picture

Issue tags: +drupalconasia2016, +Triaged for D8 major current state

Thanks @aditya.ghan!

(Updating issue credit for the major triage and adding the relevant tags, since we've verified the issue does still exist.)

catch’s picture

Title: Taxonomy load system filters for access control » Provide a way to call taxonomy_get_tree() and similar without access control
Issue tags: +Needs issue summary update

This needs a major issue summary update, since the issue has been completely inverted from taxonomy_term_load() checking access in 6.x or 7.x (which should never have been the case), to taxonomy_get_tree() checking it (which has to be the case, but I can see a use case for getting the full tree in API calls without access checking).

Just updating the title for now.

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

xjm’s picture

Title: Provide a way to call taxonomy_get_tree() and similar without access control » Taxonomy load functions filter for access control
Version: 8.1.x-dev » 7.x-dev
Status: Needs work » Closed (won't fix)
Issue tags: -Needs issue summary update +Needs followup

I've confirmed that the tag is added for loadParents(), loadChildren(), loadTree(), and getNodeTerms() in D8, as well as on the relevant Views plugins, but not for loading taxonomy terms themselves.

Based on the fact that this cannot be backported in a BC way, and that it has been fixed in D8, I'm actually going to mark this wontfix against D7. If we want a followup for what @catch describes we can do that in another issue I think, since the current summary is confusingly out of date. Tagging for that, just in case.