Problem/Motivation

Right now you have three ways of defining an access checker:

  1. implement the StaticAccessCheckInterface. You can add one of the strings returned by the appliesTo to your route definition requirements key.
  2. implement AccessCheckInterface for dynamic checking.
  3. use the _custom_access: \Drupal\mymodule\MyClass::myAccessMethod approach in the routing requirements

While it is not particularly hard to write an appliesTo method, the reverse is nearly impossible: to find where the actual code resides for a particular appliesTo string in a routing definition. For a machine, you have no chance but to actually run the code. For a human you need to know very well Drupal 8 to be able to figure this out, you need to grep for a string and make sure you don't think for example that hook_entity_access has anything to do with appliesTo: _entity_access.

Proposed resolution

dawehner came up with a very easy and non-disruptive solution to this problem: nuke the appliesTo method and move the contents of it to the already existing service definition.

  access_check.entity:
    class: Drupal\Core\Entity\EntityAccessCheck
    tags:
      - { name: access_check, applies: _entity_access }

Since appliesTo() used to return an array, this also needs to support multiple keys.
Thankfully, Symfony allows for a service to be tagged twice with no extra overhead:

  access_check.mymodule_checker:
    class: Drupal\mymodule\Access\MyChecker
    tags:
      - { name: access_check, applies: _access_my_checker }
      - { name: access_check, applies: _access_my_checker_alternate }

Now machines and humans alike can read a rather simple YAML file and quickly sort out what belongs where.

Remaining tasks

Code this.

User interface changes

None.

API changes

StaticAccessCheckInterface either becomes empty or is nuked altogether and the returned value of appliesTo moves to the service YAML. This can be scripted (as a drush php-script, I would not particularly want to make this a standalone one).

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

dawehner’s picture

For everything else we use the plugin system. We could cover both cases in a more performant, more DX friendly way: simply the access checker plugin IDs in an access section of the route and be done. For the dynamic case, we can keep an applies() method which in a base class is implemented simply as return TRUE.

We currently have no plugins inside the routing layer. Which parts are involved in the routing layer which could be plugins as well:

  • Breadcrumbs
  • Access checker
  • Route enhancer
  • Path processing
  • Route subscriber (potentially)

I agree that some of these examples are not implemented that often though also breadcrumbs are potentially valuable as plugins.
If we move to plugins on access checkers we are less consistent inside the routing system but maybe more consistent throughout core.

  • implement the StaticAccessCheckInterface. You can add the string returned by the appliesTo to your route definition requirements key.
  • implement AccessCheckInterface for dynamic checking. These all need to be fired runtime.

So when the routes are compiled (added to the router table) each route store which access checkers apply to them. This adds
potentially N (count of times) times M (count of access checkers) calls to the applies() method. Static access checkers don't have an apply method so they don't have to be run.
AccessCheckInterface access checkers applies() method are also just executed on the COMPILE TIME.

Crell’s picture

Priority: Critical » Minor

Disagree. Plugins are a good fit when you have:

1) Multiple instances of an object
2) Configured by a user
3) Through the UI

Access checkers, route enhancers, path processors, breadcrumbs... *none* of those criteria apply to any of those systems. One could have, say, a breadcrumb builder that is backed by user configuration, and that particular builder could leverage plugins internally if it makes sense to. But none of these systems "fit" the plugin use case.

"For everything else we use plugins" is simply not true.

Frequency of implementation has nothing to do with it whatsoever.

chx’s picture

Priority: Minor » Critical

1) I am not sure what "multiple instances of an object" means in this case; very often we have only one plugin instance alive in a particular request. 2) Configured by the user -- well, what is a route YAML if not that? Is this splitting of hairs what is 'configured' means and more, what 'user' means? 3) There are quite a lot of plugin types already which have no UI and doubtful to have any in contrib.

And that all you list is in the routing system just means that the routing system made bad decisions and all those need to be plugins as well. Care to list something that is not a plugin outside of the routing system? Frequency of implementation has everything to do with it: people need to learn plugins and the more they can apply the better. I would look at this way: is there anything that makes plugins a bad fit for access checkers? Absolutely not! It's a great fit! The most common access checker, the static one, is already doing the exact same it just uses a static method to return the plugin in instead of using @PluginId.

chx’s picture

webchick’s picture

I'm not comfortable this being listed as a critical DX task until / unless the issue summary is updated to explain the before/after and how this would be beneficial to people learning Drupal.

webchick’s picture

Priority: Critical » Normal
Issue tags: +DX (Developer Experience)

At a glance, though, it sounds like it might be more confusing, because it's broadening the definition of what plugins means to encompass things that don't fit its current definition, if #2 is valid.

Splitting the difference at normal, and re-adding the tag.

chx’s picture

Priority: Normal » Critical

Updated the issue summary. Back to critical.

dawehner’s picture

Updated the issue summary.

If you update the issue summary please take the last statement of #1 into account.

dawehner’s picture

Issue summary: View changes

Updated issue summary.

chx’s picture

Done

dawehner’s picture

Thank you very much!

dawehner’s picture

Issue summary: View changes

Updated issue summary.

chx’s picture

Because webchick asked in her AMA (even without mentioning names, I can read it well enough) I am willing to put up with the status quo if the internals gets documented and the DX gets fixed:

  1. #2109035: Make access checkers (much) easier to find
  2. #2092529: [meta] Improve DX for defining custom routes
  3. #2046367: Document the internals of the router
tstoeckler’s picture

Plugins are a good fit when you have:
1) Multiple instances of an object
2) Configured by a user
3) Through the UI

I am assuming that is an OR list, but even then we have a couple of plugin types in core that are a bad fit, in case that statement is true:
* Local tasks
* Local actions
* Contextual links
There are a couple others that are less obvious violations of the above, so I'm not bringing them up here to avoid derailing the discussion.

tim.plunkett’s picture

Title: Access checkers need to be plugins » Access checkers could be plugins
Issue summary: View changes

There is a third option for how access checkers can be defined: by just giving it a class::method pair.

chx’s picture

I am fine with that!

chx’s picture

Title: Access checkers could be plugins » Change StaticAccessCheckInterface/AccessCheckInterface to plugins
Issue summary: View changes
chx’s picture

Issue summary: View changes
Crell’s picture

Issue summary: View changes

Removed incorrect statement. (The _custom_access approach doesn't require a service to be registered.)

With _custom_access, is this even necessary anymore? And what would be the performance impact of going through plugins for every request? Plus on link generation, which is already too slow...

Crell’s picture

tstoeckler: No, it was an AND list, or maybe at best a "2 of 3". Things like tabs as plugins are a bit odd, but there was no better alternative. We have a viable alternative for access control now, which keeps the system loosely coupled.

Given how many hundreds of hours we've poured into decoupling systems in Drupal 8, and hiding details behind sane and purpose-built interfaces, I'm very reluctant to introduce more coupling unless there's a very very strong need. I don't see that strong need here.

chx’s picture

Title: Change StaticAccessCheckInterface/AccessCheckInterface to plugins » Make access checkers (much) easier to find
Issue summary: View changes
chx’s picture

We had a wonderful and fruitful IRC discussion today and I have posted an issue summary from it. First we identified the real pain points then dawehner solved it.

tim.plunkett’s picture

Assigned: Unassigned » tim.plunkett

Working on this.

tim.plunkett’s picture

Issue summary: View changes
Status: Active » Needs review
FileSize
68.38 KB

Symfony has native support for attributes on tags, I chose to use that instead of adding a top-level key to the definition.
It still supports everything we need.

chx’s picture

FileSize
309 bytes

Here's a handy awk script to facilitate the search, usage example:

awk -v search=_entity_access -f access_search.txt **/*.services.yml
Drupal\Core\Entity\EntityAccessCheck
chx’s picture

Instead of posting updated versions of this script, it now lives in a sandbox. https://drupal.org/sandbox/chx/2142873

Crell’s picture

I like this. It does look cleaner, and doesn't increase coupling with any other systems. (Well, maybe with the DIC but that's incidental and you can still use the code without it.) +1.

tim.plunkett’s picture

Assigned: tim.plunkett » damiankloip

Damien did most of the work on StaticAccessCheckInterface so I really want his RTBC on this.

dawehner’s picture

My implementation pretty much looks the same I coded offline.

Here are some small adapations.

* We don't need to support multiple ones ...
@tim Deleting a file is often just wrong, but I understand why you did that ... we have an issue to fix that already: https://drupal.org/node/1912602

Status: Needs review » Needs work

The last submitted patch, 27: access_manager-2109035.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
FileSize
963 bytes
67.44 KB

Maybe this works now ...

Status: Needs review » Needs work

The last submitted patch, 29: access-2109035.patch, failed testing.

tim.plunkett’s picture

  1. +++ b/core/lib/Drupal/Core/Access/AccessManager.php
    @@ -117,12 +117,13 @@ public function setRequest(Request $request) {
    -    foreach ($applies_checks as $applies_check) {
    +
    +    if ($applies_check) {
    
    +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterAccessChecksPass.php
    @@ -26,13 +26,14 @@ public function process(ContainerBuilder $container) {
    +          $applies = $attribute['applies_to'];
    

    This is how they work. They are arrays, even when there is only one.
    EDIT: I mean, this has to be reverted. It's just how Symfony does attributes in tagged services

  2. +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterAccessChecksPass.php
    @@ -26,13 +26,14 @@ public function process(ContainerBuilder $container) {
         foreach ($container->findTaggedServiceIds('access_check') as $id => $attributes) {
    ...
    +        if ($attribute['name'] == 'access_check' &&  isset($attribute['applies_to'])) {
    

    When will the name not be 'access_check'? That's what this does, AFAIK.

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
67.33 KB

This uses the applies_to and the correct changes to views that @dawehner had, but otherwise is my original patch.
It also makes the second param to addCheckService() optional.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Yeah I had a little bit of a rage mode there.

damiankloip’s picture

  1. +++ b/core/lib/Drupal/Core/Access/AccessManager.php
    @@ -328,12 +332,11 @@ protected function loadCheck($service_id) {
       public function loadAccessRequirementMap() {
    

    This method might as well be renamed I guess.

  2. +++ b/core/lib/Drupal/Core/Access/CustomAccessCheck.php
    @@ -8,6 +8,7 @@
    +use Drupal\Core\Routing\Access\AccessInterface as RoutingAccessInterface;
    
    @@ -22,7 +23,7 @@
    +class CustomAccessCheck implements RoutingAccessInterface {
    

    Do we need to alias these? There is currently a mixture of both.

  3. +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterAccessChecksPass.php
    @@ -26,7 +26,13 @@ public function process(ContainerBuilder $container) {
    +      $access_manager->addMethodCall('addCheckService', array($id, array_unique($applies)));
    

    Is this a bit too paranoid? Someone would have to register the same line twice in a YAML file? We don't do this checking in other places.

The only other thing that I would say is a 'concern' is having to do this:

    tags:
      - { name: access_check, applies: _access_my_checker }
      - { name: access_check, applies: _access_my_checker_alternate }

It just looks weird having to specify the name each time to be honest.

tim.plunkett’s picture

1) To what?
2) Yes, we do when the access checker is in Drupal\Core\Access
3) I'm not usually one for paranoia. You mean the array_unique? Doesn't bother me either way.

And yes, that is weird, but it is a) how it works in Symfony b) not used really in core.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 32: access-checks-2109035-32.patch, failed testing.

damiankloip’s picture

1. Something with the word dynamic in? Not too bothered though really.
2. Ha, yes of course! Then another question, is it weird to have checkers in Core\Access using an interface in Core\Routing\Access?
3. Yep, the array_unique. Seems unnecessary.
4. I guess it's not a common case, so meh. Not the end of the world by any means.

I don't really see how this patch makes it that much easier to find access checkers but i can see the logic of looking in yaml. Plus does have some nice cleanups in.

tim.plunkett’s picture

Assigned: damiankloip » Unassigned
Status: Needs work » Needs review
Related issues: +#1912602: Changing view access from "Permission" to "Role" causes AJAX error message re getRoles()
FileSize
2.29 KB
68.39 KB

Borrowing a oneline fix from #1912602: Changing view access from "Permission" to "Role" causes AJAX error message re getRoles()

1) Renamed to match the protected property
2) Yes it is. Blame Xano (#2095125: Use access constants in every access control context)
3) If this causes a bug later on, it's on you :)

chx’s picture

> I don't really see how this patch makes it that much easier to find access checkers but i can see the logic of looking in yaml.

While I did write a complicated script to do the search in the majority of cases grep -B 3 applies_to: _entity_access **/*.services.yml|grep class will just do fine. That's way, way easier than with current HEAD where the only thing you can grep for is the string _entity_access.

damiankloip’s picture

Yes, it is easier. Only if you know where to look though :) That is why I said it is not that much easier. I do agree it *is* easier though.

  1. +++ b/core/lib/Drupal/Core/Access/AccessManager.php
    @@ -118,9 +117,14 @@ public function setRequest(Request $request) {
    +   *   (optional) An array of static checks for a route to apply to.
    

    This doc doesn't sound right. Doesn't it want to be something like "An array of route requirement keys the check service applies to", or something...

  2. +++ b/core/lib/Drupal/Core/Access/AccessManager.php
    @@ -341,14 +344,8 @@ public function loadAccessRequirementMap() {
    -      if (is_subclass_of($this->checks[$service_id], 'Drupal\Core\Access\StaticAccessCheckInterface')) {
    ...
    +      if ($this->checks[$service_id] instanceof AccessCheckInterface) {
    

    We check the AccessCheckInterface for dynamic checkers but nothing for static checkers anymore. Do you think we should do this too?

3) If this causes a bug later on, it's on you :)

Ha, fair enough :) However, if we are wanting to make sure this isn't a problem, the RegisterAccessChecksPass is not the place to do this, seems like it should be in the access manager itself. If anywhere.

damiankloip’s picture

Issue tags: +API change
Crell’s picture

Issue tags: +beta blocker
dawehner’s picture

FileSize
738 bytes
68.42 KB

We check the AccessCheckInterface for dynamic checkers but nothing for static checkers anymore. Do you think we should do this too?

I don't know what to check actually.

damiankloip’s picture

FileSize
67.25 KB
1.04 KB

Rerolled. Plus, can't we just add it here to loadCheck() as all checkers will have to implement this interface in some form? That's what I was getting at in my comment about the checking.

dawehner’s picture

+1 for that change.

dawehner’s picture

44: 2109035-44.patch queued for re-testing.

The last submitted patch, 44: 2109035-44.patch, failed testing.

damiankloip’s picture

FileSize
67.62 KB

Reroll

chx’s picture

Assigned: Unassigned » Crell

Assigning to Crell for final review.

Crell’s picture

+++ b/core/lib/Drupal/Core/Access/AccessManager.php
@@ -331,19 +337,24 @@ protected function loadCheck($service_id) {
+      throw new AccessException('All access checks must implement AccessInterface.');

The docblock for AccessException says:

"An exception thrown for invalid access callback return values."

But that's not how it's being used here. Either we need to use a new exception class or update the docblock of AccessException. I don't have a strong preference at the moment.

Other than that nit, I'm good here. Go ahead and mark RTBC when fixing the above.

damiankloip’s picture

FileSize
66.53 KB
580 bytes

Rerolled, and changed that docbock for AccessException.

Crell’s picture

Status: Needs review » Reviewed & tested by the community

Works for me. Thanks.

webchick’s picture

Title: Make access checkers (much) easier to find » Change notice + docs update: Make access checkers (much) easier to find
Status: Reviewed & tested by the community » Active
Issue tags: +Needs change record, +Needs documentation

Wow, this is a great change!

- 63 files changed, 231 insertions, 435 deletions FTW
- I also have no idea what a StaticAccessCheckerInterface is, so happy to see that removed in favour of something more general
- I also like the idea of encoding route relationships into the route definitions vs. having to open a separate file to get those

Coupled with the improved "greppability" defined in the issue summary, this patch looks like a big DX win to me!

Committed and pushed to 8.x. Thanks!

We need to update the existing change notices and documentation under https://drupal.org/node/2122071 now.

chx’s picture

And you can mention https://drupal.org/sandbox/chx/2142873 this handy script as well.

dawehner’s picture

Crell’s picture

Assigned: Crell » Unassigned
Status: Active » Fixed
Issue tags: -Needs change record

Made a slight tweak to the change notice. Looks good now.

Berdir’s picture

Title: Change notice + docs update: Make access checkers (much) easier to find » Make access checkers (much) easier to find
Issue tags: -Needs documentation

Updating tags and title.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

ParisLiakos’s picture

+++ b/core/modules/aggregator/aggregator.services.yml
@@ -8,3 +8,11 @@ services:
+  access_check.aggregator.categories:
+    class: Drupal\aggregator\Access\CategoriesAccessCheck
+    arguments: ['@database']
+    tags:
+      - { name: access_check, applies_to: _access_aggregator_categories }
+  aggregator.category.storage:
+    class: Drupal\aggregator\CategoryStorageController
+    arguments: ['@database']

+++ b/core/modules/aggregator/lib/Drupal/aggregator/Access/CategoriesAccessCheck.php
@@ -0,0 +1,45 @@
+<?php
...
+}
...
+/**
...
+
...
+ * Contains \Drupal\aggregator\Access\CategoriesAccess.
...
+ * @file
...
+ */
+
+namespace Drupal\aggregator\Access;
...
+use Drupal\Core\Database\Connection;
...
+
...
+use Drupal\Core\Routing\Access\AccessInterface;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Provides an access check for aggregator categories routes.
+ */
+class CategoriesAccessCheck implements AccessInterface {
...
+  /**
...
+
...
+   * The database connection.
...
+   * @var \Drupal\Core\Database\Connection
...
+   *
...
+   */
+  protected $database;
+
+  /**
...
+   *
...
+   *   The database connection.
...
+   * Constructs a CategoriesAccessCheck object.
...
+   * @param \Drupal\Core\Database\Connection
...
+   */
...
+    $this->database = $database;
...
+  public function __construct(Connection $database) {
...
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access(Route $route, Request $request, AccountInterface $account) {
+    return $account->hasPermission('access news feeds') && (bool) $this->database->queryRange('SELECT 1 FROM {aggregator_category}', 0, 1)->fetchField() ? static::ALLOW : static::DENY;
+  }
+

junk
Opened #2187313: Remove reintroduced category code in aggregator