I want to try to write a paragraph or two to clarify the purpose of the admin_permissions key in the @ContentEntityType annotation. I don't know if this is adequately documented anywhere.

The only language I can find in a documentation page at drupal.org is this...

The admin_permission key automatically allows all access for users with that permission. In case more logic is required, a custom access controller can be specified.

However, this language needs to be fleshed out and made more specific. What is "access" in this context?

Please comment if there is already some good documentation somewhere. I will use this issue to make notes as I determine how this admin_permission value affects other access control like EntityAccessControlHandler.

Here are a couple documentation pages that have code examples with the admin_permission key but need some language explaining its purpose:
https://www.drupal.org/docs/8/api/entity-api/structure-of-an-entity-anno...
https://www.drupal.org/docs/8/api/entity-api/creating-a-content-entity-t...

An old discussion that gave rise to the admin_permissions key: https://www.drupal.org/project/drupal/issues/2105557

Comments

arnoldbird created an issue. See original summary.

arnoldbird’s picture

Looking at the "links" key in the @ContentEntityType annotation, I see...

  • The admin_permission key affects view access at my collection path, which in my case are administrative lists of entities.
  • The admin_permission key does NOT affect access or use of canonical, add-form, edit-form and delete-form paths.
  • If my user doesn't have the permission in the admin_permission key, they can't view the administrative list of entities, but can perform all other operations if permitted in a class that extends EntityAccessControlHandler.
arnoldbird’s picture

If I dump the $operation in my checkAccess method (in my class extending EntityAccessControlHandler), I see that in my collection path, checks are performed for each of the "Operations" buttons -- in my case the "update" and "delete" strings are dumped. Thus, an access check is performed for determining which options to present in the operations widget, but no check is performed on the collection path itself.

So, it seems to be the job of the admin_permissions key to determine if the user can view the contents of a list, but the links in the operations widget are determined by the checkAccess method.

arnoldbird’s picture

I'm having a difficult time determining what controls the inclusion of links in menus.

The admin_permission key in some cases does affect whether links are included in menus, but not in all cases. There are definitely times when changing the admin_permission key affects links in my menu block for one of my users. However, I can't determine any pattern. This behavior seems to be erratic and is affected by a combination of factors, somehow.

arnoldbird’s picture

Issue summary: View changes
arnoldbird’s picture

Issue summary: View changes

The admin_permission key in some cases does affect whether links are included in menus, but not in all cases. There are definitely times when changing the admin_permission key affects links in my menu block for one of my users. However, I can't determine any pattern. This behavior seems to be erratic and is affected by a combination of factors, somehow.

I had a bug in my code that was making it seem like the menu behavior was erratic, but now it looks like it all works as designed.

The admin_permission key controls...

1) Whether the user can access the page/resource at the 'collection' path (as set in the entity type annotation).
2) Whether the link to that page/resource will be visible to the user in menus.

Meanwhile, the appropriate place to control access for all entity operations (view, update, delete, add) is in one's implementation of EntityAccessControlHandler. That implementation controls access to the operations and, appropriately enough, whether the corresponding action links are visible to the user.

To alter the access to routes more generally, extend RouteSubscriberBase. This class can be extended to alter access to any route, including, in theory, the same route affected by the admin_permission key. If a route subscriber sets a different access level for the same route as the one affected by admin_permission, the more restrictive permission is applied.

All of the above seems like good design to me, and it all seems to work consistently at the moment.

arnoldbird’s picture

Arguably, for clarity's sake, a permission string like the following would make sense to use in custom modules...

admin_permission = "access my_entity collection",

...since the admin_permission key controls access to the collection path. Or even something that references the annotation keys explicitly like...

admin_permission = "my_entity links_collection",

Looking at it another way, this is probably a bad use of the admin_permission key...

admin_permission = "administer my_entity entities",

...because that could lead another developer or admin to think that if they tick the box for the admin permission in the UI, that they are granting the user administrative privileges, when most likely they are only granting the user permission to access one page in the admin interface... unless the "administer my_entity entities" permission is *also* used for some other purpose, which would likely be bad design in a lot of cases.

arnoldbird’s picture

I just discovered that the admin_permission key also affects $entity_type->getAdminPermission(). The getAdminPermission method returns the string set in the admin_permission key.

This adds some uncertainty about how best to set the admin_permission key. It's not very surprising that a getAdminPermission method would return a value set in an admin_permission key, but I figure there is a chance other developers will expect they can call that getAdminPermission method on my entity, so I have to provide something in the admin_permission key that makes sense. What I might end up doing is set it to "do_not_use". That is probably more than sufficient within the context of my project to make it clear to all collaborators that we aren't using the key, and I guess we'll probably know soon enough if this somehow will cause a problem in terms of how the core interacts with our modules.

I might actually be making too much of it but I am trying to think through the access very deliberately because there will be a lot of code built on top of my code and I don't want to take a wrong turn.

arnoldbird’s picture

Berdir’s picture

Responding to comments in the other issue

* Yeah, you're right, the default collection route also uses it if defined, forgot about that. But it's only the generation of the route that uses it, at runtime, it's just a permission check. And you can override it and use something else instead.
* Yes, getAdminPermission() is the official way to access it, that is an API and you're welcome to use that whenever you want. Just keep in mind that it's optional like \Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider::getCollectionRoute(). It is defined as optional even though that documentation could be improved a bit.
* Just because the route builder uses it for the collection doesn't mean it's not also used for CRUD, it most certainly is, in \Drupal\Core\Entity\EntityAccessControlHandler::checkAccess() and \Drupal\Core\Entity\EntityAccessControlHandler::checkCreateAccess(). But all CRUD access checks on an entity should always go through the access() methods as entity types can still override it and have conditions when the admin permission doesn't actually allow access (for example to have locked entities that can't be deleted, ever, by anyone). So your next comments are then IMHO all basically wrong. It's a lot easier to a to a single annotation instead of a new class and two methods.

At the moment, the best solution for working around the admin_permission key is to misuse the collection key, if I'm not mistaken. But I have not yet found the place in the core where the admin_permission key impacts access to the collection path. The best solution may end up being to set the collection key to the path that serves as the landing page for the entity's admin functions (e.g. misuse the key) or to set a dummy path for the collection to try to ensure that neither the admin_permission key nor the collection key will have any impact on my application now or in the future.

I don't know what you mean with that and why it should be a problem. DefaultHtmlRouteProvider is optional, don't use it if you don't like it.

arnoldbird’s picture

Thanks again Berdir. I am once again thinking the code is probably all fine and I just need to understand it better.

Just because the route builder uses it for the collection doesn't mean it's not also used for CRUD, it most certainly is

I just generated a module with console called my_debug. If I do not give the user "administer debug entities" permissions, but do give the user "create debug entities" permission, the user can create debug entities. I don't see any sense in which the admin_permission key affects any CRUD operations. Am I missing something, or is there something idiosyncratic about the modules generated by console?

Even if I remove the checkAccess function from MyDebugAccessControlHandler, the user can still add an entity if they have create permission but not the admin_permission.

So it seems to me the admin_permission controls access to these two pages...

  • admin/structure/my_debug
  • admin/structure/my_debug/settings
Berdir’s picture

Your conclusion isn't correct.

Just because the specific create permission allows create access doesn't mean that the administer permission doesn't also allow that. Also, create access is checked in checkCreateAccess(), not checkAccess(), so removing that won't change that.

Again, I'm talking about the default access implementation in the entity access control handler. which you can easily check yourself. If you have/generate additional permissions and access checks then additional rules apply based on your implementtaion of course.

arnoldbird’s picture

Berdir,

Just because the specific create permission allows create access doesn't mean that the administer permission doesn't also allow that.

Though it may not be authoritative, the module generated by Drupal console doesn't support what you are saying. No doubt a developer could modify the code subsequent to generation to make it support what you are saying, but I'm noting that directly after generation, assignment of the admin_permission does not affect whether my user can CRUD an entity.

I understand that the console-generated module is not the ultimate authority, but as I try to learn how access works in Drupal 8, I am referring to the module code generated by console just because that is a fixed point that I can test against. Is there something idiosyncratic about the console-generated module? Is the console-generated module leading me astray? Do we need to suggest changes to the console-generated module to bring it in line with Drupal best practices? (As you can see, I'm in the process of learning what Drupal 8 best practices might be, with regard to entity access.)

I am just going on direct obervation. If I generate a module and content type with console, and make no changes at all to that code, and then assign a user create perms but not the admin permission, the user can indeed create entities.

Whatever is actually the case, that is what I will want to describe in my documentation. Are you finding that the admin_permission affects CRUD operations? If so, how can I reproduce that?

arnoldbird’s picture

Also, create access is checked in checkCreateAccess(), not checkAccess(), so removing that won't change that.

True! Thanks for catching that. However, unfortunately it doesn't change my findings. I still can't configure my way to any situation where the admin_permission affects CRUD access unless I modify the code that console generated. And if I remove both the checkAccess and createCheckAccess functions, the admin_permission still does not affect CRUD access.

(I know there may be some meta considerations here, like whether I should be thinking of the console-generated module as a kind of documentation... but I think in many cases developers will learn how things work from the example modules and from the console-generated modules. And probably moreso the latter.)

Berdir’s picture

We seem to be talking in circles :)

> If I generate a module and content type with console, and make no changes at all to that code, and then assign a user create perms but not the admin permission, the user can indeed create entities.

I did not say that's not true. It is true because drupal console generated the create permission and code that it checks for that permission. But the opposite is also true. If you give someone the admin permission, he can *also* create content. That's the exactly point of the admin permission. The admin permission allows full access, more specific permissions allow more specific access control but that is then up to the specific entity type.

The most common use case, where the assumption in the route builder makes sense, is when you *only* have that one permission. That's often the case for config entities where you don't need more separation. There is one permission to create, edit and delete node types, there is no reason to only edit your own or allowing to create but not delete. Then also the collection should use that route. But for example with nodes, if you can create new nodes, edit and maybe delete some of them then you need to be able to see the list of content without administer permission, which is why node has "access content overview" as a separate permission.

The point is that the admin permission is an optional feature that the entity system provides that you can use if you have a simple use case for your permission handling. If you don't want it, then don't use it.

> Are you finding that the admin_permission affects CRUD operations? If so, how can I reproduce that?

I'm saying that based on the code that is in core that I referenced above.

That said, it's definitely possible that the code generation in console has bugs. In fact, I know it does some strange things still but I wasn't aware that there are strange things in the access handling. So lets see, I've now installed console and generated a content entity type, also called "my_debug" no bundles, default settings.

And yes, I can confirm that the default code that was generated does not respect the admin permission:

  /**
   * {@inheritdoc}
   */
  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
    return AccessResult::allowedIfHasPermission($account, 'add my debug entities');
  }

The admin permission check is in the parent method. So right now, it would allow add permission only with this add my debug entities permission. But if I remove that method, then it will check the parent and that looks like this:

  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
    if ($admin_permission = $this->entityType->getAdminPermission()) {
      return AccessResult::allowedIfHasPermission($account, $admin_permission);
    }
    else {
      // No opinion.
      return AccessResult::neutral();
    }
  }

So optionally, if there is one, it checks the admin permission. The correct thing to do, in my opinion, would be check both permissions or first ask the parent and then orIf() the create permission. But there is no magic. CRUD involves asking your entity access control handler and whatever that returns is the truth. And if you have custom (generated) code that does not not consider the admin permission then it's not. But the *default* implementation does.

That's the thing with code generation. drupal console proudly tells you that it just created 686 lines in 15 files.. but if you don't understand what those lines do then the result can be very confusing.

What I'd recommend is create an issue for console so that their default implementation actually considers the admin permission. Or alternatively don't create a ton of permissions out of the box that I might not even need. Ask if I just want a single admin permission in which case you can just use the default access control handler or if I really want to have more complex access control that you probably need to customize then.

arnoldbird’s picture

Berdir,

#15 is very helpful. I can now confirm that the in the console-generated module, the admin permission does (or can) affect access to CRUD operations, and the only reason I thought it didn't was, as you say, the checkCreateAccess method in MyDebugAccessControlHandler. When I remove the checkCreateAccess function and make no other changes to the console-generated module, this is clear.

The point is that the admin permission is an optional feature that the entity system provides that you can use if you have a simple use case for your permission handling. If you don't want it, then don't use it.

Yes, we should note this in the documentation somewhere, perhaps at https://www.drupal.org/docs/8/api/entity-api/structure-of-an-entity-anno...

Strictly speaking, we probably can't say it's an optional feature of the annotation itself, because its value will be returned when other developers call getAdminPermission(). If the key is removed from the annotation, there is an error when you install the module. It is optional in the sense that a module developer need not call getAdminPermission() in their own module if they don't want, and can also override the methods in EntityAccessControlHandler where getAdminPermission is called.

The existing bit of documentation that I noted in the issue summary:

The admin_permission key automatically allows all access for users with that permission. In case more logic is required, a custom access controller can be specified.

Maybe we can be more explicit. Maybe something like...

In a simple, default implementation, the admin_permission provides full access to the entity, including all CRUD operations. To change this default behavior, you can override the EntityAccessControlHandler::checkAccess or EntityAccessControlHandler::checkCreateAccess in your custom module.

For an example showing how you can override the checkAccess and checkCreateAccess in a custom access controller, refer to the content_entity_example module, or generate a content entity module with Drupal console. In the content_entity_example and console-generated module, note how the getAdminPermission() method serves to retrieve the admin_permission value for an entity type.

What do you think of the above?

What I'd recommend is create an issue for console so that their default implementation actually considers the admin permission.

I suppose the trick is to add something to the example module and console-generated module that helps document the feature, but without adding code to the module that no one would actually use. Maybe the default implementation in example modules could be...

  /**
   * {@inheritdoc}
   */
  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {

    if ($admin_permission = $this->entityType->getAdminPermission()) {
      // Check the admin_permission as defined in your @ContentEntityType annotation.
      return AccessResult::allowedIfHasPermission($account, $admin_permission);
    }
    else {
      return AccessResult::allowedIfHasPermission($account, 'add my_entity entities');
    }
  }
Berdir’s picture

> If the key is removed from the annotation, there is an error when you install the module.

No, there is no error. There would only be an error if another module relies on there being an admin permission but that's extremely unlikely. Node, which is basically the most entity of all entities does not have that key. Nothing is failing.

As I mentioned earlier, getAdminPermission() is documented as having a string or false as return value if there is none.

About the documentation, we don't usually refer to contrib/example modules out of core, there are plenty of examples in core and we could just include an example snippet.

> Maybe the default implementation in example modules could be...

No, that's not correct, that will only check the create permission if there is no admin permission. You want to check both, either by calling parent::checkCreateAccess()->orIf(AccessResult::... 'create permission') or with allowedIfHasPermission($both_permissions)

arnoldbird’s picture

No, there is no error.

Here is what I see when I try to install a custom module without the admin_permission key:
The website encountered an unexpected error. Please try again later.</br></br><em class="placeholder">InvalidArgumentException</em>: Routing requirement for &quot;_permission&quot; must be a string. in <em class="placeholder">Symfony\Component\Routing\Route-&gt;sanitizeRequirement()</em> (line <em class="placeholder">572</em> of <em class="placeholder">/var/www/my/path/vendor/symfony/routing/Route.php</em>).

arnoldbird’s picture

Another possibility for checkCreateAccess() for the examples module:

/**
   * {@inheritdoc}
   */
  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {

    if (\Drupal::currentUser()->hasPermission($this->entityType->getAdminPermission())) {
      return AccessResult::allowed();
    }
    else {
      return AccessResult::allowedIfHasPermission($account, 'add my_entity entities');
    }
  }

Yes, the previous attempt was incorrect, for the reason you mentioned.

Version: 8.4.4 » 8.4.x-dev

Core issues are now filed against the dev versions where changes will be made. Document the specific release you are using in your issue comment. More information about choosing a version.

Version: 8.4.x-dev » 8.9.x-dev

Drupal 8.8.7 was released on June 3, 2020 and is the final full bugfix release for the Drupal 8.8.x series. Branches prior to 8.8.x are not supported, and Drupal 8.8.x will not receive any further development aside from security fixes. Sites should prepare to update to Drupal 8.9.0 or Drupal 9.0.0 for ongoing support.

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

heddn’s picture

Version: 8.9.x-dev » 9.3.x-dev

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.