From 954dc07e92af60beb22d5525dca8076453210bb6 Mon Sep 17 00:00:00 2001
From: bojanz <bojanz@86106.no-reply.drupal.org>
Date: Wed, 28 Sep 2016 11:23:04 +0100
Subject: [PATCH] Issue #2801031 by mglaman, bojanz: Provide a generic entity
 access handler and permissions

---
 entity.permissions.yml                        |   2 +
 src/EntityAccessControlHandler.php            | 109 +++++++
 src/EntityPermissionProvider.php              | 266 ++++++++++++++++++
 src/EntityPermissionProviderInterface.php     |  23 ++
 src/EntityPermissions.php                     |  60 ++++
 .../entity_module_test.permissions.yml        |   4 -
 .../src/Entity/EnhancedEntity.php             |   3 +
 .../src/Entity/EnhancedOwnerEntity.php        | 139 +++++++++
 .../src/Entity/EnhancedOwnerEntityBundle.php  |  81 ++++++
 .../src/EntityEnhancedOwnerPermissions.php    |  19 ++
 .../src/EntityEnhancedPermissions.php         |  19 ++
 tests/src/Kernel/PermissionsTest.php          | 122 ++++++++
 12 files changed, 843 insertions(+), 4 deletions(-)
 create mode 100644 entity.permissions.yml
 create mode 100644 src/EntityAccessControlHandler.php
 create mode 100644 src/EntityPermissionProvider.php
 create mode 100644 src/EntityPermissionProviderInterface.php
 create mode 100644 src/EntityPermissions.php
 create mode 100644 tests/modules/entity_module_test/src/Entity/EnhancedOwnerEntity.php
 create mode 100644 tests/modules/entity_module_test/src/Entity/EnhancedOwnerEntityBundle.php
 create mode 100644 tests/modules/entity_module_test/src/EntityEnhancedOwnerPermissions.php
 create mode 100644 tests/modules/entity_module_test/src/EntityEnhancedPermissions.php
 create mode 100644 tests/src/Kernel/PermissionsTest.php

diff --git a/entity.permissions.yml b/entity.permissions.yml
new file mode 100644
index 0000000..1676e88
--- /dev/null
+++ b/entity.permissions.yml
@@ -0,0 +1,2 @@
+permission_callbacks:
+  - \Drupal\entity\EntityPermissions::buildPermissions
diff --git a/src/EntityAccessControlHandler.php b/src/EntityAccessControlHandler.php
new file mode 100644
index 0000000..8f8cb43
--- /dev/null
+++ b/src/EntityAccessControlHandler.php
@@ -0,0 +1,109 @@
+<?php
+
+namespace Drupal\entity;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityAccessControlHandler as CoreEntityAccessControlHandler;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\user\EntityOwnerInterface;
+
+/**
+ * Provides per-bundle entity CRUD permissions.
+ */
+class EntityAccessControlHandler extends CoreEntityAccessControlHandler {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    $account = $this->prepareUser($account);
+    /** @var \Drupal\Core\Access\AccessResult $result */
+    $result = parent::checkAccess($entity, $operation, $account);
+
+    if ($result->isNeutral()) {
+      if ($entity instanceof EntityOwnerInterface) {
+        $result = $this->checkEntityOwnerPermissions($entity, $operation, $account);
+      }
+      else {
+        $result = $this->checkEntityPermissions($entity, $operation, $account);
+      }
+    }
+
+    // Ensure that access is evaluated again when the entity changes.
+    return $result->addCacheableDependency($entity);
+  }
+
+  /**
+   * Checks the entity operation and bundle permissions.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity for which to check access.
+   * @param string $operation
+   *   The entity operation. Usually one of 'view', 'view label', 'update' or
+   *   'delete'.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The user for which to check access.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  protected function checkEntityPermissions(EntityInterface $entity, $operation, AccountInterface $account) {
+    return AccessResult::allowedIfHasPermissions($account, [
+      "$operation {$entity->getEntityTypeId()}",
+      "$operation {$entity->bundle()} {$entity->getEntityTypeId()}",
+    ], 'OR');
+  }
+
+  /**
+   * Checks the entity operation and bundle permissions, with owners.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity for which to check access.
+   * @param string $operation
+   *   The entity operation. Usually one of 'view', 'view label', 'update' or
+   *   'delete'.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The user for which to check access.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  protected function checkEntityOwnerPermissions(EntityInterface $entity, $operation, AccountInterface $account) {
+    /** @var \Drupal\Core\Entity\EntityInterface|\Drupal\user\EntityOwnerInterface $entity */
+    if (($account->id() == $entity->getOwnerId())) {
+      $result = AccessResult::allowedIfHasPermissions($account, [
+        "$operation own {$entity->getEntityTypeId()}",
+        "$operation any {$entity->getEntityTypeId()}",
+        "$operation own {$entity->bundle()} {$entity->getEntityTypeId()}",
+        "$operation any {$entity->bundle()} {$entity->getEntityTypeId()}",
+      ], 'OR');
+    }
+    else {
+      $result = AccessResult::allowedIfHasPermissions($account, [
+        "$operation any {$entity->getEntityTypeId()}",
+        "$operation any {$entity->bundle()} {$entity->getEntityTypeId()}",
+      ], 'OR');
+    }
+
+    return $result->cachePerUser();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
+    $result = parent::checkCreateAccess($account, $context, $entity_bundle);
+    if ($result->isNeutral()) {
+      $result = AccessResult::allowedIfHasPermissions($account, [
+        'administer ' . $this->entityTypeId,
+        'create ' . $entity_bundle . ' ' . $this->entityTypeId,
+        'create any ' . $entity_bundle . ' ' . $this->entityTypeId,
+        'create own ' . $entity_bundle . ' ' . $this->entityTypeId,
+      ], 'OR');
+    }
+
+    return $result;
+  }
+
+}
diff --git a/src/EntityPermissionProvider.php b/src/EntityPermissionProvider.php
new file mode 100644
index 0000000..502fba2
--- /dev/null
+++ b/src/EntityPermissionProvider.php
@@ -0,0 +1,266 @@
+<?php
+
+namespace Drupal\entity;
+
+use Drupal\Core\Entity\EntityHandlerInterface;
+use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\user\EntityOwnerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides permissions for entities.
+ */
+class EntityPermissionProvider implements EntityPermissionProviderInterface, EntityHandlerInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The entity type bundle info.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
+   */
+  protected $entityTypeBundleInfo;
+
+  /**
+   * Constructs a new EntityPermissionProvider object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
+   *   The entity type bundle info.
+   */
+  public function __construct(EntityTypeBundleInfoInterface $entity_type_bundle_info) {
+    $this->entityTypeBundleInfo = $entity_type_bundle_info;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+    return new static(
+      $container->get('entity_type.bundle.info')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildPermissions(EntityTypeInterface $entity_type) {
+    $entity_type_id = $entity_type->id();
+    $plural_label = $entity_type->getPluralLabel();
+
+    $permissions = [];
+    $permissions["administer {$entity_type_id}"] = [
+      'title' => $this->t('Administer @type', ['@type' => $plural_label]),
+      'restrict access' => TRUE,
+    ];
+    $permissions["access {$entity_type_id} overview"] = [
+      'title' => $this->t('Access the @type overview page', ['@type' => $plural_label]),
+    ];
+    if ($entity_type->getPermissionGranularity() == 'entity_type') {
+      $permissions += $this->buildEntityTypePermissions($entity_type);
+    }
+    else {
+      $permissions += $this->buildBundlePermissions($entity_type);
+    }
+
+    foreach ($permissions as $name => $permission) {
+      // Permissions are grouped by provider on admin/people/permissions.
+      $permissions[$name]['provider'] = $entity_type->getProvider();
+      // TranslatableMarkup objects don't sort properly.
+      $permissions[$name]['title'] = (string) $permission['title'];
+    }
+
+    return $permissions;
+  }
+
+  /**
+   * Builds permissions for the entity_type granularity.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return array
+   *   The permissions.
+   */
+  protected function buildEntityTypePermissions(EntityTypeInterface $entity_type) {
+    $entity_type_id = $entity_type->id();
+    $has_owner = $entity_type->isSubclassOf(EntityOwnerInterface::class);
+    $singular_label = $entity_type->getSingularLabel();
+    $plural_label = $entity_type->getPluralLabel();
+
+    $permissions = [];
+    if ($has_owner) {
+      $permissions["create any {$entity_type_id}"] = [
+        'title' => $this->t('Create any @type', [
+          '@type' => $singular_label,
+        ]),
+      ];
+      $permissions["create own {$entity_type_id}"] = [
+        'title' => $this->t('Create own @type', [
+          '@type' => $plural_label,
+        ]),
+      ];
+
+      $permissions["view any {$entity_type_id}"] = [
+        'title' => $this->t('View any @type', [
+          '@type' => $singular_label,
+        ]),
+      ];
+      $permissions["view own {$entity_type_id}"] = [
+        'title' => $this->t('View own @type', [
+          '@type' => $plural_label,
+        ]),
+      ];
+
+      $permissions["update any {$entity_type_id}"] = [
+        'title' => $this->t('Update any @type', [
+          '@type' => $singular_label,
+        ]),
+      ];
+      $permissions["update own {$entity_type_id}"] = [
+        'title' => $this->t('Update own @type', [
+          '@type' => $plural_label,
+        ]),
+      ];
+
+      $permissions["delete any {$entity_type_id}"] = [
+        'title' => $this->t('Delete any @type', [
+          '@type' => $singular_label,
+        ]),
+      ];
+      $permissions["delete own {$entity_type_id}"] = [
+        'title' => $this->t('Delete own @type', [
+          '@type' => $plural_label,
+        ]),
+      ];
+    }
+    else {
+      $permissions["create {$entity_type_id}"] = [
+        'title' => $this->t('Create @type', [
+          '@type' => $plural_label,
+        ]),
+      ];
+      $permissions["view {$entity_type_id}"] = [
+        'title' => $this->t('View @type', [
+          '@type' => $plural_label,
+        ]),
+      ];
+      $permissions["update {$entity_type_id}"] = [
+        'title' => $this->t('Update @type', [
+          '@type' => $plural_label,
+        ]),
+      ];
+      $permissions["delete {$entity_type_id}"] = [
+        'title' => $this->t('Delete @type', [
+          '@type' => $plural_label,
+        ]),
+      ];
+    }
+
+    return $permissions;
+  }
+
+  /**
+   * Builds permissions for the bundle granularity.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return array
+   *   The permissions.
+   */
+  protected function buildBundlePermissions(EntityTypeInterface $entity_type) {
+    $entity_type_id = $entity_type->id();
+    $bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id);
+    $has_owner = $entity_type->isSubclassOf(EntityOwnerInterface::class);
+    $singular_label = $entity_type->getSingularLabel();
+    $plural_label = $entity_type->getPluralLabel();
+
+    $permissions = [];
+    foreach ($bundles as $bundle_name => $bundle_info) {
+      if ($has_owner) {
+        $permissions["create any {$bundle_name} {$entity_type_id}"] = [
+          'title' => $this->t('@bundle: Create any @type', [
+            '@bundle' => $bundle_info['label'],
+            '@type' => $singular_label,
+          ]),
+        ];
+        $permissions["create own {$bundle_name} {$entity_type_id}"] = [
+          'title' => $this->t('@bundle: Create own @type', [
+            '@bundle' => $bundle_info['label'],
+            '@type' => $plural_label,
+          ]),
+        ];
+
+        $permissions["view any {$bundle_name} {$entity_type_id}"] = [
+          'title' => $this->t('@bundle: View own @type', [
+            '@bundle' => $bundle_info['label'],
+            '@type' => $singular_label,
+          ]),
+        ];
+        $permissions["view own {$bundle_name} {$entity_type_id}"] = [
+          'title' => $this->t('@bundle: View own @type', [
+            '@bundle' => $bundle_info['label'],
+            '@type' => $plural_label,
+          ]),
+        ];
+
+        $permissions["update any {$bundle_name} {$entity_type_id}"] = [
+          'title' => $this->t('@bundle: Update any @type', [
+            '@bundle' => $bundle_info['label'],
+            '@type' => $singular_label,
+          ]),
+        ];
+        $permissions["update own {$bundle_name} {$entity_type_id}"] = [
+          'title' => $this->t('@bundle: Update own @type', [
+            '@bundle' => $bundle_info['label'],
+            '@type' => $plural_label,
+          ]),
+        ];
+
+        $permissions["delete any {$bundle_name} {$entity_type_id}"] = [
+          'title' => $this->t('@bundle: Delete any @type', [
+            '@bundle' => $bundle_info['label'],
+            '@type' => $singular_label,
+          ]),
+        ];
+        $permissions["delete own {$bundle_name} {$entity_type_id}"] = [
+          'title' => $this->t('@bundle: Delete own @type', [
+            '@bundle' => $bundle_info['label'],
+            '@type' => $plural_label,
+          ]),
+        ];
+      }
+      else {
+        $permissions["create {$bundle_name} {$entity_type_id}"] = [
+          'title' => $this->t('@bundle: Create @type', [
+            '@bundle' => $bundle_info['label'],
+            '@type' => $plural_label,
+          ]),
+        ];
+        $permissions["view {$bundle_name} {$entity_type_id}"] = [
+          'title' => $this->t('@bundle: View @type', [
+            '@bundle' => $bundle_info['label'],
+            '@type' => $plural_label,
+          ]),
+        ];
+        $permissions["update {$bundle_name} {$entity_type_id}"] = [
+          'title' => $this->t('@bundle: Update @type', [
+            '@bundle' => $bundle_info['label'],
+            '@type' => $plural_label,
+          ]),
+        ];
+        $permissions["delete {$bundle_name} {$entity_type_id}"] = [
+          'title' => $this->t('@bundle: Delete @type', [
+            '@bundle' => $bundle_info['label'],
+            '@type' => $plural_label,
+          ]),
+        ];
+      }
+    }
+
+    return $permissions;
+  }
+
+}
diff --git a/src/EntityPermissionProviderInterface.php b/src/EntityPermissionProviderInterface.php
new file mode 100644
index 0000000..50f3cca
--- /dev/null
+++ b/src/EntityPermissionProviderInterface.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Drupal\entity;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+
+/**
+ * Allows entity types to provide permissions.
+ */
+interface EntityPermissionProviderInterface {
+
+  /**
+   * Builds permissions for the given entity type.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return array
+   *   The permissions.
+   */
+  public function buildPermissions(EntityTypeInterface $entity_type);
+
+}
diff --git a/src/EntityPermissions.php b/src/EntityPermissions.php
new file mode 100644
index 0000000..18002d9
--- /dev/null
+++ b/src/EntityPermissions.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Drupal\entity;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Class for generating per-bundle CRUD permissions.
+ */
+class EntityPermissions implements ContainerInjectionInterface {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a new EntityPermissions object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * Builds a list of permissions for the participating entity types.
+   *
+   * @return array
+   *   The permissions.
+   */
+  public function buildPermissions() {
+    $permissions = [];
+    /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
+    foreach ($this->entityTypeManager->getDefinitions() as $entity_type) {
+      if ($entity_type->hasHandlerClass('permission_provider')) {
+        $permission_provider_class = $entity_type->getHandlerClass('permission_provider');
+        $permission_provider = $this->entityTypeManager->createHandlerInstance($permission_provider_class, $entity_type);
+        $permissions += $permission_provider->buildPermissions($entity_type);
+      }
+    }
+
+    return $permissions;
+  }
+
+}
diff --git a/tests/modules/entity_module_test/entity_module_test.permissions.yml b/tests/modules/entity_module_test/entity_module_test.permissions.yml
index b9ff91d..8aa844e 100644
--- a/tests/modules/entity_module_test/entity_module_test.permissions.yml
+++ b/tests/modules/entity_module_test/entity_module_test.permissions.yml
@@ -1,7 +1,3 @@
-'administer entity_test_enhanced':
-  title: 'Administer entity_test_enhanced'
-  'restrict access': TRUE
-
 'view all entity_test_enhanced revisions':
   title: 'View all entity_test_enhanced revisions'
   'restrict access': TRUE
diff --git a/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php b/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php
index 32b2bbd..5114b49 100644
--- a/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php
+++ b/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php
@@ -19,6 +19,8 @@ use Drupal\entity\Revision\RevisionableContentEntityBase;
  *   label = @Translation("Entity test with enhancements"),
  *   handlers = {
  *     "storage" = "\Drupal\Core\Entity\Sql\SqlContentEntityStorage",
+ *     "access" = "\Drupal\entity\EntityAccessControlHandler",
+ *     "permission_provider" = "\Drupal\entity\EntityPermissionProvider",
  *     "form" = {
  *       "add" = "\Drupal\entity\Form\RevisionableContentEntityForm",
  *       "edit" = "\Drupal\entity\Form\RevisionableContentEntityForm",
@@ -38,6 +40,7 @@ use Drupal\entity\Revision\RevisionableContentEntityBase;
  *   translatable = TRUE,
  *   revisionable = TRUE,
  *   admin_permission = "administer entity_test_enhanced",
+ *   permission_granularity = "bundle",
  *   entity_keys = {
  *     "id" = "id",
  *     "bundle" = "type",
diff --git a/tests/modules/entity_module_test/src/Entity/EnhancedOwnerEntity.php b/tests/modules/entity_module_test/src/Entity/EnhancedOwnerEntity.php
new file mode 100644
index 0000000..938ea89
--- /dev/null
+++ b/tests/modules/entity_module_test/src/Entity/EnhancedOwnerEntity.php
@@ -0,0 +1,139 @@
+<?php
+
+namespace Drupal\entity_module_test\Entity;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\entity\Revision\RevisionableContentEntityBase;
+use Drupal\user\EntityOwnerInterface;
+use Drupal\user\UserInterface;
+
+/**
+ * Provides a test entity which uses all the capabilities of entity module.
+ *
+ * @ContentEntityType(
+ *   id = "entity_test_owner",
+ *   label = @Translation("Entity owner test with enhancements"),
+ *   handlers = {
+ *     "storage" = "\Drupal\Core\Entity\Sql\SqlContentEntityStorage",
+ *     "access" = "\Drupal\entity\EntityAccessControlHandler",
+ *     "permission_provider" = "\Drupal\entity\EntityPermissionProvider",
+ *     "form" = {
+ *       "add" = "\Drupal\entity\Form\RevisionableContentEntityForm",
+ *       "edit" = "\Drupal\entity\Form\RevisionableContentEntityForm",
+ *       "delete" = "\Drupal\Core\Entity\EntityDeleteForm",
+ *     },
+ *     "route_provider" = {
+ *       "html" = "\Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider",
+ *       "revision" = "\Drupal\entity\Routing\RevisionRouteProvider",
+ *       "delete-multiple" = "\Drupal\entity\Routing\DeleteMultipleRouteProvider",
+ *     },
+ *     "list_builder" = "\Drupal\Core\Entity\EntityListBuilder",
+ *   },
+ *   base_table = "entity_test_owner",
+ *   data_table = "entity_test_owner_field_data",
+ *   revision_table = "entity_test_owner_revision",
+ *   revision_data_table = "entity_test_owner_field_revision",
+ *   translatable = TRUE,
+ *   revisionable = TRUE,
+ *   admin_permission = "administer entity_test_owner",
+ *   permission_granularity = "bundle",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "bundle" = "type",
+ *     "revision" = "vid",
+ *     "langcode" = "langcode",
+ *     "uuid" = "uuid",
+ *   },
+ *   links = {
+ *     "add-page" = "/entity_test_owner/add",
+ *     "add-form" = "/entity_test_owner/add/{type}",
+ *     "edit-form" = "/entity_test_owner/{entity_test_owner}/edit",
+ *     "canonical" = "/entity_test_owner/{entity_test_owner}",
+ *     "collection" = "/entity_test_owner",
+ *     "delete-multiple-form" = "/entity_test_owner/delete",
+ *     "revision" = "/entity_test_owner/{entity_test_owner}/revisions/{entity_test_owner_revision}/view",
+ *     "revision-revert-form" = "/entity_test_owner/{entity_test_owner}/revisions/{entity_test_owner_revision}/revert",
+ *     "version-history" = "/entity_test_owner/{entity_test_owner}/revisions",
+ *   },
+ *   bundle_entity_type = "entity_test_owner_bundle",
+ * )
+ */
+class EnhancedOwnerEntity extends RevisionableContentEntityBase implements EntityOwnerInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOwner() {
+    return $this->get('uid')->entity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOwnerId() {
+    return $this->get('uid')->target_id;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setOwnerId($uid) {
+    $this->set('uid', $uid);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setOwner(UserInterface $account) {
+    $this->set('uid', $account->id());
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+    $fields = parent::baseFieldDefinitions($entity_type);
+
+    $fields['name'] = BaseFieldDefinition::create('string')
+      ->setLabel('Name')
+      ->setRevisionable(TRUE)
+      ->setDisplayOptions('view', [
+        'label' => 'hidden',
+        'type' => 'string',
+        'weight' => -5,
+      ]);
+
+    $fields['uid'] = BaseFieldDefinition::create('entity_reference')
+      ->setLabel(t('Owner'))
+      ->setDescription(t('The order owner.'))
+      ->setSetting('target_type', 'user')
+      ->setSetting('handler', 'default')
+      ->setDefaultValueCallback('Drupal\entity_module_test\Entity\EnhancedOwnerEntity::getCurrentUserId')
+      ->setTranslatable(TRUE)
+      ->setDisplayOptions('view', [
+        'label' => 'above',
+        'type' => 'author',
+        'weight' => 0,
+      ])
+      ->setDisplayConfigurable('form', TRUE)
+      ->setDisplayConfigurable('view', TRUE);
+
+    return $fields;
+  }
+
+  /**
+   * Default value callback for 'uid' base field definition.
+   *
+   * @see ::baseFieldDefinitions()
+   *
+   * @return array
+   *   An array of default values.
+   */
+  public static function getCurrentUserId() {
+    return [\Drupal::currentUser()->id()];
+  }
+
+}
diff --git a/tests/modules/entity_module_test/src/Entity/EnhancedOwnerEntityBundle.php b/tests/modules/entity_module_test/src/Entity/EnhancedOwnerEntityBundle.php
new file mode 100644
index 0000000..8686e3e
--- /dev/null
+++ b/tests/modules/entity_module_test/src/Entity/EnhancedOwnerEntityBundle.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace Drupal\entity_module_test\Entity;
+
+use Drupal\Core\Config\Entity\ConfigEntityBundleBase;
+use Drupal\Core\Entity\EntityDescriptionInterface;
+use Drupal\entity\Entity\RevisionableEntityBundleInterface;
+
+/**
+ * Provides bundles for the test entity.
+ *
+ * @ConfigEntityType(
+ *   id = "entity_test_owner_bundle",
+ *   label = @Translation("Entity owner test with enhancements - Bundle"),
+ *   admin_permission = "administer entity_test_enhanced_owner",
+ *   config_prefix = "entity_test_owner_bundle",
+ *   bundle_of = "entity_test_owner",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "label" = "label"
+ *   },
+ *   config_export = {
+ *     "id",
+ *     "label",
+ *     "description"
+ *   },
+ * )
+ */
+class EnhancedOwnerEntityBundle extends ConfigEntityBundleBase implements EntityDescriptionInterface, RevisionableEntityBundleInterface {
+
+  /**
+   * The bundle ID.
+   *
+   * @var string
+   */
+  protected $id;
+
+  /**
+   * The bundle label.
+   *
+   * @var string
+   */
+  protected $label;
+
+  /**
+   * The bundle description.
+   *
+   * @var string
+   */
+  protected $description;
+
+  /**
+   * Should new entities of this bundle have a new revision by default.
+   *
+   * @var bool
+   */
+  protected $new_revision = FALSE;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return $this->description;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setDescription($description) {
+    $this->description = $description;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function shouldCreateNewRevision() {
+    return $this->new_revision;
+  }
+
+}
diff --git a/tests/modules/entity_module_test/src/EntityEnhancedOwnerPermissions.php b/tests/modules/entity_module_test/src/EntityEnhancedOwnerPermissions.php
new file mode 100644
index 0000000..acccd64
--- /dev/null
+++ b/tests/modules/entity_module_test/src/EntityEnhancedOwnerPermissions.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Drupal\entity_module_test;
+
+use Drupal\entity\EntityPermissions;
+
+/**
+ * Permissions implementation for entity_test_enhanced.
+ */
+class EntityEnhancedOwnerPermissions extends EntityPermissions {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEntityTypeId() {
+    return 'entity_test_owner';
+  }
+
+}
diff --git a/tests/modules/entity_module_test/src/EntityEnhancedPermissions.php b/tests/modules/entity_module_test/src/EntityEnhancedPermissions.php
new file mode 100644
index 0000000..79617d9
--- /dev/null
+++ b/tests/modules/entity_module_test/src/EntityEnhancedPermissions.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Drupal\entity_module_test;
+
+use Drupal\entity\EntityPermissions;
+
+/**
+ * Permissions implementation for entity_test_enhanced.
+ */
+class EntityEnhancedPermissions extends EntityPermissions {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEntityTypeId() {
+    return 'entity_test_enhanced';
+  }
+
+}
diff --git a/tests/src/Kernel/PermissionsTest.php b/tests/src/Kernel/PermissionsTest.php
new file mode 100644
index 0000000..1f3d2a4
--- /dev/null
+++ b/tests/src/Kernel/PermissionsTest.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace Drupal\Tests\entity\Kernel;
+
+use Drupal\entity_module_test\Entity\EnhancedEntity;
+use Drupal\entity_module_test\Entity\EnhancedEntityBundle;
+use Drupal\entity_module_test\Entity\EnhancedOwnerEntity;
+use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
+
+/**
+ * Tests the permissions builder and generic entity access control handler.
+ *
+ * @group entity
+ */
+class PermissionsTest extends EntityKernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['entity_module_test', 'system', 'user', 'entity'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installEntitySchema('user');
+    $this->installEntitySchema('entity_test_enhanced');
+    $this->installEntitySchema('entity_test_owner');
+    $this->installSchema('system', 'router');
+    $this->installConfig(['system']);
+
+    $bundle = EnhancedEntityBundle::create([
+      'id' => 'default',
+      'label' => 'Default',
+    ]);
+    $bundle->save();
+    $bundle = EnhancedEntityBundle::create([
+      'id' => 'tester',
+      'label' => 'Tester',
+    ]);
+    $bundle->save();
+
+    $this->container->get('router.builder')->rebuild();
+  }
+
+  /**
+   * Tests the generated permissions.
+   */
+  public function testGeneratedPermissions() {
+    $permissions = $this->container->get('user.permissions')->getPermissions();
+
+    $this->assertTrue(isset($permissions['administer entity_test_enhanced']));
+    $this->assertTrue(isset($permissions['access entity_test_enhanced overview']));
+    $this->assertTrue(isset($permissions['create default entity_test_enhanced']));
+    $this->assertTrue(isset($permissions['create tester entity_test_enhanced']));
+    $this->assertFalse(isset($permissions['create own tester entity_test_enhanced']));
+  }
+
+  /**
+   * Tests the access controller.
+   */
+  public function testAccessControlHandler() {
+    // Offset uid = 1.
+    $this->createUser();
+
+    $entity = EnhancedEntity::create([
+      'name' => 'Llama',
+      'type' => 'default',
+    ]);
+    $entity->save();
+
+    $user1 = $this->createUser([], ['administer entity_test_enhanced']);
+    $user2 = $this->createUser([], ['create default entity_test_enhanced', 'update default entity_test_enhanced']);
+    $user3 = $this->createUser([], ['create tester entity_test_enhanced', 'update tester entity_test_enhanced']);
+
+    $this->assertTrue($entity->access('create', $user1));
+    $this->assertTrue($entity->access('create', $user2));
+    $this->assertFalse($entity->access('create', $user3));
+    $this->assertTrue($entity->access('create', $user1));
+    $this->assertTrue($entity->access('create', $user2));
+    $this->assertFalse($entity->access('create', $user3));
+    $this->assertTrue($entity->access('update', $user1));
+    $this->assertTrue($entity->access('update', $user2));
+    $this->assertFalse($entity->access('update', $user3));
+
+    $user4 = $this->createUser([], ['update own default entity_test_owner']);
+    $user5 = $this->createUser([], ['update any default entity_test_owner']);
+    $user6 = $this->createUser([], ['administer entity_test_owner']);
+
+    $entity = EnhancedOwnerEntity::create([
+      'name' => 'Alpaca',
+      'type' => 'default',
+      'uid' => $user4->id(),
+    ]);
+    $entity->save();
+    $other_entity = EnhancedOwnerEntity::create([
+      'name' => 'Emu',
+      'type' => 'default',
+      'uid' => $user5->id(),
+    ]);
+    $other_entity->save();
+
+    // Owner can update entity.
+    $this->assertTrue($entity->access('update', $user4));
+
+    // User cannot update entities they do not own.
+    $this->assertFalse($other_entity->access('update', $user4));
+
+    // User with "any" can update entities they do not own.
+    $this->assertTrue($entity->access('update', $user5));
+
+    // User with "any" can update their own entries.
+    $this->assertTrue($other_entity->access('update', $user5));
+
+    // User with "administer" can update both entities.
+    $this->assertTrue($entity->access('update', $user6));
+    $this->assertTrue($other_entity->access('update', $user6));
+  }
+
+}
-- 
GitLab