3 namespace Drupal\Core\Entity;
5 use Drupal\Core\Access\AccessResult;
6 use Drupal\Core\Field\FieldItemListInterface;
7 use Drupal\Core\Field\FieldDefinitionInterface;
8 use Drupal\Core\Language\LanguageInterface;
9 use Drupal\Core\Session\AccountInterface;
12 * Defines a default implementation for entity access control handler.
14 class EntityAccessControlHandler extends EntityHandlerBase implements EntityAccessControlHandlerInterface {
17 * Stores calculated access check results.
21 protected $accessCache = [];
24 * The entity type ID of the access control handler instance.
28 protected $entityTypeId;
31 * Information about the entity type.
33 * @var \Drupal\Core\Entity\EntityTypeInterface
35 protected $entityType;
38 * Allows to grant access to just the labels.
40 * By default, the "view label" operation falls back to "view". Set this to
41 * TRUE to allow returning different access when just listing entity labels.
45 protected $viewLabelOperation = FALSE;
48 * Constructs an access control handler instance.
50 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
51 * The entity type definition.
53 public function __construct(EntityTypeInterface $entity_type) {
54 $this->entityTypeId = $entity_type->id();
55 $this->entityType = $entity_type;
61 public function access(EntityInterface $entity, $operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
62 $account = $this->prepareUser($account);
63 $langcode = $entity->language()->getId();
65 if ($operation === 'view label' && $this->viewLabelOperation == FALSE) {
69 // If an entity does not have a UUID, either from not being set or from not
70 // having them, use the 'entity type:ID' pattern as the cache $cid.
71 $cid = $entity->uuid() ?: $entity->getEntityTypeId() . ':' . $entity->id();
73 // If the entity is revisionable, then append the revision ID to allow
74 // individual revisions to have specific access control and be cached
76 if ($entity instanceof RevisionableInterface) {
77 /** @var $entity \Drupal\Core\Entity\RevisionableInterface */
78 $cid .= ':' . $entity->getRevisionId();
81 if (($return = $this->getCache($cid, $operation, $langcode, $account)) !== NULL) {
82 // Cache hit, no work necessary.
83 return $return_as_object ? $return : $return->isAllowed();
86 // Invoke hook_entity_access() and hook_ENTITY_TYPE_access(). Hook results
87 // take precedence over overridden implementations of
88 // EntityAccessControlHandler::checkAccess(). Entities that have checks that
89 // need to be done before the hook is invoked should do so by overriding
92 // We grant access to the entity if both of these conditions are met:
93 // - No modules say to deny access.
94 // - At least one module says to grant access.
95 $access = array_merge(
96 $this->moduleHandler()->invokeAll('entity_access', [$entity, $operation, $account]),
97 $this->moduleHandler()->invokeAll($entity->getEntityTypeId() . '_access', [$entity, $operation, $account])
100 $return = $this->processAccessHookResults($access);
102 // Also execute the default access check except when the access result is
103 // already forbidden, as in that case, it can not be anything else.
104 if (!$return->isForbidden()) {
105 $return = $return->orIf($this->checkAccess($entity, $operation, $account));
107 $result = $this->setCache($return, $cid, $operation, $langcode, $account);
108 return $return_as_object ? $result : $result->isAllowed();
112 * We grant access to the entity if both of these conditions are met:
113 * - No modules say to deny access.
114 * - At least one module says to grant access.
116 * @param \Drupal\Core\Access\AccessResultInterface[] $access
117 * An array of access results of the fired access hook.
119 * @return \Drupal\Core\Access\AccessResultInterface
120 * The combined result of the various access checks' results. All their
121 * cacheability metadata is merged as well.
123 * @see \Drupal\Core\Access\AccessResultInterface::orIf()
125 protected function processAccessHookResults(array $access) {
126 // No results means no opinion.
127 if (empty($access)) {
128 return AccessResult::neutral();
131 /** @var \Drupal\Core\Access\AccessResultInterface $result */
132 $result = array_shift($access);
133 foreach ($access as $other) {
134 $result = $result->orIf($other);
140 * Performs access checks.
142 * This method is supposed to be overwritten by extending classes that
143 * do their own custom access checking.
145 * @param \Drupal\Core\Entity\EntityInterface $entity
146 * The entity for which to check access.
147 * @param string $operation
148 * The entity operation. Usually one of 'view', 'view label', 'update' or
150 * @param \Drupal\Core\Session\AccountInterface $account
151 * The user for which to check access.
153 * @return \Drupal\Core\Access\AccessResultInterface
156 protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
157 if ($operation == 'delete' && $entity->isNew()) {
158 return AccessResult::forbidden()->addCacheableDependency($entity);
160 if ($admin_permission = $this->entityType->getAdminPermission()) {
161 return AccessResult::allowedIfHasPermission($account, $admin_permission);
165 return AccessResult::neutral();
170 * Tries to retrieve a previously cached access value from the static cache.
173 * Unique string identifier for the entity/operation, for example the
174 * entity UUID or a custom string.
175 * @param string $operation
176 * The entity operation. Usually one of 'view', 'update', 'create' or
178 * @param string $langcode
179 * The language code for which to check access.
180 * @param \Drupal\Core\Session\AccountInterface $account
181 * The user for which to check access.
183 * @return \Drupal\Core\Access\AccessResultInterface|null
184 * The cached AccessResult, or NULL if there is no record for the given
185 * user, operation, langcode and entity in the cache.
187 protected function getCache($cid, $operation, $langcode, AccountInterface $account) {
188 // Return from cache if a value has been set for it previously.
189 if (isset($this->accessCache[$account->id()][$cid][$langcode][$operation])) {
190 return $this->accessCache[$account->id()][$cid][$langcode][$operation];
195 * Statically caches whether the given user has access.
197 * @param \Drupal\Core\Access\AccessResultInterface $access
200 * Unique string identifier for the entity/operation, for example the
201 * entity UUID or a custom string.
202 * @param string $operation
203 * The entity operation. Usually one of 'view', 'update', 'create' or
205 * @param string $langcode
206 * The language code for which to check access.
207 * @param \Drupal\Core\Session\AccountInterface $account
208 * The user for which to check access.
210 * @return \Drupal\Core\Access\AccessResultInterface
211 * Whether the user has access, plus cacheability metadata.
213 protected function setCache($access, $cid, $operation, $langcode, AccountInterface $account) {
214 // Save the given value in the static cache and directly return it.
215 return $this->accessCache[$account->id()][$cid][$langcode][$operation] = $access;
221 public function resetCache() {
222 $this->accessCache = [];
228 public function createAccess($entity_bundle = NULL, AccountInterface $account = NULL, array $context = [], $return_as_object = FALSE) {
229 $account = $this->prepareUser($account);
231 'entity_type_id' => $this->entityTypeId,
232 'langcode' => LanguageInterface::LANGCODE_DEFAULT,
235 $cid = $entity_bundle ? 'create:' . $entity_bundle : 'create';
236 if (($access = $this->getCache($cid, 'create', $context['langcode'], $account)) !== NULL) {
237 // Cache hit, no work necessary.
238 return $return_as_object ? $access : $access->isAllowed();
241 // Invoke hook_entity_create_access() and hook_ENTITY_TYPE_create_access().
242 // Hook results take precedence over overridden implementations of
243 // EntityAccessControlHandler::checkCreateAccess(). Entities that have
244 // checks that need to be done before the hook is invoked should do so by
245 // overriding this method.
247 // We grant access to the entity if both of these conditions are met:
248 // - No modules say to deny access.
249 // - At least one module says to grant access.
250 $access = array_merge(
251 $this->moduleHandler()->invokeAll('entity_create_access', [$account, $context, $entity_bundle]),
252 $this->moduleHandler()->invokeAll($this->entityTypeId . '_create_access', [$account, $context, $entity_bundle])
255 $return = $this->processAccessHookResults($access);
257 // Also execute the default access check except when the access result is
258 // already forbidden, as in that case, it can not be anything else.
259 if (!$return->isForbidden()) {
260 $return = $return->orIf($this->checkCreateAccess($account, $context, $entity_bundle));
262 $result = $this->setCache($return, $cid, 'create', $context['langcode'], $account);
263 return $return_as_object ? $result : $result->isAllowed();
267 * Performs create access checks.
269 * This method is supposed to be overwritten by extending classes that
270 * do their own custom access checking.
272 * @param \Drupal\Core\Session\AccountInterface $account
273 * The user for which to check access.
274 * @param array $context
275 * An array of key-value pairs to pass additional context when needed.
276 * @param string|null $entity_bundle
277 * (optional) The bundle of the entity. Required if the entity supports
278 * bundles, defaults to NULL otherwise.
280 * @return \Drupal\Core\Access\AccessResultInterface
283 protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
284 if ($admin_permission = $this->entityType->getAdminPermission()) {
285 return AccessResult::allowedIfHasPermission($account, $admin_permission);
289 return AccessResult::neutral();
294 * Loads the current account object, if it does not exist yet.
296 * @param \Drupal\Core\Session\AccountInterface $account
297 * The account interface instance.
299 * @return \Drupal\Core\Session\AccountInterface
300 * Returns the current account object.
302 protected function prepareUser(AccountInterface $account = NULL) {
304 $account = \Drupal::currentUser();
312 public function fieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account = NULL, FieldItemListInterface $items = NULL, $return_as_object = FALSE) {
313 $account = $this->prepareUser($account);
315 // Get the default access restriction that lives within this field.
316 $default = $items ? $items->defaultAccess($operation, $account) : AccessResult::allowed();
318 // Explicitly disallow changing the entity ID and entity UUID.
319 $entity = $items ? $items->getEntity() : NULL;
320 if ($operation === 'edit' && $entity) {
321 if ($field_definition->getName() === $this->entityType->getKey('id')) {
322 // String IDs can be set when creating the entity.
323 if (!($entity->isNew() && $field_definition->getType() === 'string')) {
324 return $return_as_object ? AccessResult::forbidden('The entity ID cannot be changed.')->addCacheableDependency($entity) : FALSE;
327 elseif ($field_definition->getName() === $this->entityType->getKey('uuid')) {
328 // UUIDs can be set when creating an entity.
329 if (!$entity->isNew()) {
330 return $return_as_object ? AccessResult::forbidden('The entity UUID cannot be changed.')->addCacheableDependency($entity) : FALSE;
335 // Get the default access restriction as specified by the access control
337 $entity_default = $this->checkFieldAccess($operation, $field_definition, $account, $items);
339 // Combine default access, denying access wins.
340 $default = $default->andIf($entity_default);
342 // Invoke hook and collect grants/denies for field access from other
343 // modules. Our default access flag is masked under the ':default' key.
344 $grants = [':default' => $default];
345 $hook_implementations = $this->moduleHandler()->getImplementations('entity_field_access');
346 foreach ($hook_implementations as $module) {
347 $grants = array_merge($grants, [$module => $this->moduleHandler()->invoke($module, 'entity_field_access', [$operation, $field_definition, $account, $items])]);
350 // Also allow modules to alter the returned grants/denies.
352 'operation' => $operation,
353 'field_definition' => $field_definition,
355 'account' => $account,
357 $this->moduleHandler()->alter('entity_field_access', $grants, $context);
359 $result = $this->processAccessHookResults($grants);
360 return $return_as_object ? $result : $result->isAllowed();
364 * Default field access as determined by this access control handler.
366 * @param string $operation
367 * The operation access should be checked for.
368 * Usually one of "view" or "edit".
369 * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
370 * The field definition.
371 * @param \Drupal\Core\Session\AccountInterface $account
372 * The user session for which to check access.
373 * @param \Drupal\Core\Field\FieldItemListInterface $items
374 * (optional) The field values for which to check access, or NULL if access
375 * is checked for the field definition, without any specific value
376 * available. Defaults to NULL.
378 * @return \Drupal\Core\Access\AccessResultInterface
381 protected function checkFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
382 return AccessResult::allowed();