Now that we've covered how access control works on routes, let's dive into the entity access system and see how we can ensure that only the right users interact with our entities. To demonstrate these, we will work with the Product entity type we created in Chapter 7, Your Own Custom Entity and Plugin Types.
When we created the Product entity type, the annotation we wrote had an admin_permission property, where we referenced the general permission to be used for any interaction with the entities of this type. Since we didn't reference and implement an access control handler, this is the only access checking done on products. In many cases, this is enough. After all, entity types can be created for the sole purpose of structuring some data that nobody even needs to interact with in the UI. However, many other cases require more granular access control on operating with the entities, especially the content-oriented ones, such as Node.
There are four operations for which we can control access when it comes to entities--view, create, update, and delete. The first one is clearly the most common one, but we always need to account for the rest as well. Let's first define permissions for all these operations:
view product entities: title: 'View Product entities' edit product entities: title: 'Edit Product entities' delete product entities: title: 'Delete Product entities' add product entities: title: 'Create new Product entities'
These are four simple permissions that map to the operations that can be performed on Product entities.
Now, let's go ahead and create an access control handler for our Product entity type. You remember what these handlers are from Chapter 6, Data Modeling and Storage, don't you?
First, we will reference the class we will build on the product annotation:
"access" = "Drupal\products\Access\ProductAccessControlHandler",
I choose to put this handler in the Access namespace of the module, but feel free to put it where you want.
Second, we will need the actual class:
namespace Drupal\products\Access;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\products\Entity\ProductInterface;
/**
* Access controller for the Product entity type.
*/
class ProductAccessControlHandler extends EntityAccessControlHandler {
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
/** @var ProductInterface $entity */
switch ($operation) {
case 'view':
return AccessResult::allowedIfHasPermission($account, 'view product entities');
case 'update':
return AccessResult::allowedIfHasPermission($account, 'edit product entities');
case 'delete':
return AccessResult::allowedIfHasPermission($account, 'delete product entities');
}
return AccessResult::neutral();
}
/**
* {@inheritdoc}
*/
protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
return AccessResult::allowedIfHasPermission($account, 'add product entities');
}
}
As I mentioned in Chapter 6, Data Modeling and Storage, entity access control handlers need to extend the EntityAccessControlHandler base class. If one is not specifically provided, that is actually the handler the entity type defaults to. Also, there are two methods we will need to implement here (override):
- checkAccess(), which is used to control access on the view and update and delete operations
- checkCreateAccess(), which is used to control access on the create operation
The reason why these are separate is because for the create operation we don't have an entity we can inspect in the process.
Our access rules for the Product entity type are very simple. For each operation, we allow access if the user has the relevant permission; otherwise, access is neutral. However, what happens in this case?
It's worth looking into the EntityAccessControlHandler base class and understanding what is going on. The main access entry points are the access() and createAccess() methods. We should never override these because the logic happening in there is quite standardized and is expected behavior by everyone. Instead, our rules go inside the two methods we saw in our own handler subclass.
The access() and createAccess() methods invoke entity access hooks (we'll talk about those in a minute). If those do not come back with an access denied message, they call their respective access methods we are overriding in our own subclass, and the results of these are combined with the ones from the access hooks inside an orIf() access result. Remember earlier when we talked about the AccessResult base class and its handy orIf() and andIf() methods?
It's important to note how access is determined with all these factors. If at least one of the hook implementations grants access and none deny it, the user will have access, unless we deny access in our access handler. Neutral access plays no role in this equation, except if all hook implementations and the access handler return neutral access (so no specific access being granted), then the access will be denied.
In our example, we defined permissions, and the handler simply checks for these. Already, this is pretty flexible because administrators can now assign these permissions to roles and control using which users can perform any of these operations. However, there is nothing stopping us from adding more logic to these methods. For example, we can even inspect the entities (and/or the user account) and determine access based on some values. Moreover, we can inject services into the access handler and make use of them in these calculations.