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..d8541c4 --- /dev/null +++ b/src/EntityAccessControlHandler.php @@ -0,0 +1,112 @@ +prepareUser($account); + /** @var \Drupal\Core\Access\AccessResult $result */ + $result = parent::checkAccess($entity, $operation, $account); + + if ($result->isNeutral()) { + $result = AccessResult::allowedIfHasPermission($account, "bypass {$this->entityTypeId} access"); + } + + if ($result->isNeutral()) { + if ($entity instanceof EntityOwnerInterface) { + $result = $this->checkEntityOwnerPermissions($entity, $operation, $account); + } + else { + $result = $this->checkEntityPermissions($entity, $operation, $account); + } + } + $result->cachePerUser()->cachePerPermissions()->addCacheableDependency($entity); + + return $result; + } + + /** + * 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) { + 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; + } + + /** + * {@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, [ + 'bypass ' . $this->entityTypeId . ' access', + 'create ' . $entity_bundle . ' ' . $this->entityTypeId, + 'create any ' . $entity_bundle . ' ' . $this->entityTypeId, + 'create own ' . $entity_bundle . ' ' . $this->entityTypeId, + ], 'OR'); + } + + return $result; + } + +} diff --git a/src/EntityPermissions.php b/src/EntityPermissions.php new file mode 100644 index 0000000..5f0d409 --- /dev/null +++ b/src/EntityPermissions.php @@ -0,0 +1,301 @@ +entityTypeManager = $entity_type_manager; + $this->entityTypeBundleInfo = $entity_type_bundle_info; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + $container->get('entity_type.bundle.info') + ); + } + + /** + * Generates permissions for entity types. + */ + public function buildPermissions() { + $entity_types = $this->entityTypeManager->getDefinitions(); + $entity_types = array_filter($entity_types, function ($entity_type) { + /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */ + $access_control_handler = $this->entityTypeManager->getAccessControlHandler($entity_type->id()); + $permissions_generate = TRUE; + if ($entity_type->hasKey('permissions_generate')) { + $permissions_generate = $entity_type->get('permissions_generate'); + } + + return $access_control_handler instanceof EntityAccessControlHandler && $permissions_generate; + }); + + $permissions = []; + foreach (array_keys($entity_types) as $entity_type_id) { + $permissions += $this->getPermissions($entity_type_id); + } + return $permissions; + } + + /** + * Gets the permissions. + * + * @param string $entity_type_id + * The entity type ID. + * + * @return array + * Returns an array of permissions. + */ + public function getPermissions($entity_type_id) { + $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); + + $permissions["administer $entity_type_id"] = [ + 'title' => $this->t('Administer @type', ['@type' => $entity_type->getPluralLabel()]), + 'restrict access' => TRUE, + ]; + $permissions["bypass $entity_type_id access"] = [ + 'title' => $this->t('View, edit and delete all @type regardless of permission restrictions.', ['@type' => $entity_type->getPluralLabel()]), + 'restrict access' => TRUE, + ]; + $permissions["access $entity_type_id overview"] = [ + 'title' => $this->t('Access @type overview page', ['@type' => $entity_type->getPluralLabel()]), + ]; + if ($entity_type->getPermissionGranularity() == 'entity_type') { + $permissions += $this->getEntityTypePermissions($entity_type_id); + } + else { + $permissions += $this->getBundlePermissions($entity_type_id); + } + + return $permissions; + } + + /** + * Gets the permissions for entity_type granularity. + * + * @param string $entity_type_id + * The entity type ID. + * + * @return array + * The array of permissions. + */ + protected function getEntityTypePermissions($entity_type_id) { + $permissions = []; + $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); + $has_owner = $entity_type->isSubclassOf(EntityOwnerInterface::class); + + if ($has_owner) { + $permissions["create any {$entity_type_id}"] = [ + 'title' => $this->t('Create any @type', [ + '@type' => $entity_type->getPluralLabel(), + ]), + ]; + $permissions["create own {$entity_type_id}"] = [ + 'title' => $this->t('Create own @type', [ + '@type' => $entity_type->getPluralLabel(), + ]), + ]; + + $permissions["view any {$entity_type_id}"] = [ + 'title' => $this->t('View any @type', [ + '@type' => $entity_type->getPluralLabel(), + ]), + ]; + $permissions["view own {$entity_type_id}"] = [ + 'title' => $this->t('View own @type', [ + '@type' => $entity_type->getPluralLabel(), + ]), + ]; + + $permissions["update any {$entity_type_id}"] = [ + 'title' => $this->t('Update any @type', [ + '@type' => $entity_type->getPluralLabel(), + ]), + ]; + $permissions["update own {$entity_type_id}"] = [ + 'title' => $this->t('Update own @type', [ + '@type' => $entity_type->getPluralLabel(), + ]), + ]; + + $permissions["delete any {$entity_type_id}"] = [ + 'title' => $this->t('Delete any @type', [ + '@type' => $entity_type->getPluralLabel(), + ]), + ]; + $permissions["delete own {$entity_type_id}"] = [ + 'title' => $this->t('Delete own @type', [ + '@type' => $entity_type->getPluralLabel(), + ]), + ]; + } + else { + $permissions["create {$entity_type_id}"] = [ + 'title' => $this->t('@bundle: Create @type', [ + '@type' => $entity_type->getPluralLabel(), + ]), + ]; + $permissions["view {$entity_type_id}"] = [ + 'title' => $this->t('@bundle: View @type', [ + '@type' => $entity_type->getPluralLabel(), + ]), + ]; + $permissions["update {$entity_type_id}"] = [ + 'title' => $this->t('@bundle: Update @type', [ + '@type' => $entity_type->getPluralLabel(), + ]), + ]; + $permissions["delete {$entity_type_id}"] = [ + 'title' => $this->t('@bundle: Delete @type', [ + '@type' => $entity_type->getPluralLabel(), + ]), + ]; + } + + return $permissions; + } + + /** + * Gets the permissions for bundle granularity. + * + * @param string $entity_type_id + * The entity type ID. + * + * @return array + * The array of permissions. + */ + protected function getBundlePermissions($entity_type_id) { + $permissions = []; + $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); + $has_owner = $entity_type->isSubclassOf(EntityOwnerInterface::class); + + $bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id); + $bundle_entity_type_id = $entity_type->getBundleEntityType(); + $bundle_type_storage = $this->entityTypeManager->getStorage($bundle_entity_type_id); + foreach (array_keys($bundles) as $bundle_id) { + $bundle = $bundle_type_storage->load($bundle_id); + + if ($has_owner) { + $permissions["create any {$bundle_id} {$entity_type_id}"] = [ + 'title' => $this->t('@bundle: Create @type', [ + '@bundle' => $bundle->label(), + '@type' => $entity_type->getPluralLabel(), + ]), + ]; + $permissions["create own {$bundle_id} {$entity_type_id}"] = [ + 'title' => $this->t('@bundle: Create @type', [ + '@bundle' => $bundle->label(), + '@type' => $entity_type->getPluralLabel(), + ]), + ]; + + $permissions["view any {$bundle_id} {$entity_type_id}"] = [ + 'title' => $this->t('@bundle: View @type', [ + '@bundle' => $bundle->label(), + '@type' => $entity_type->getPluralLabel(), + ]), + ]; + $permissions["view own {$bundle_id} {$entity_type_id}"] = [ + 'title' => $this->t('@bundle: View @type', [ + '@bundle' => $bundle->label(), + '@type' => $entity_type->getPluralLabel(), + ]), + ]; + + $permissions["update any {$bundle_id} {$entity_type_id}"] = [ + 'title' => $this->t('@bundle: Update @type', [ + '@bundle' => $bundle->label(), + '@type' => $entity_type->getPluralLabel(), + ]), + ]; + $permissions["update own {$bundle_id} {$entity_type_id}"] = [ + 'title' => $this->t('@bundle: Update @type', [ + '@bundle' => $bundle->label(), + '@type' => $entity_type->getPluralLabel(), + ]), + ]; + + $permissions["delete any {$bundle_id} {$entity_type_id}"] = [ + 'title' => $this->t('@bundle: Delete @type', [ + '@bundle' => $bundle->label(), + '@type' => $entity_type->getPluralLabel(), + ]), + ]; + $permissions["delete own {$bundle_id} {$entity_type_id}"] = [ + 'title' => $this->t('@bundle: Delete @type', [ + '@bundle' => $bundle->label(), + '@type' => $entity_type->getPluralLabel(), + ]), + ]; + } + else { + $permissions["create {$bundle_id} {$entity_type_id}"] = [ + 'title' => $this->t('@bundle: Create @type', [ + '@bundle' => $bundle->label(), + '@type' => $entity_type->getPluralLabel(), + ]), + ]; + $permissions["view {$bundle_id} {$entity_type_id}"] = [ + 'title' => $this->t('@bundle: View @type', [ + '@bundle' => $bundle->label(), + '@type' => $entity_type->getPluralLabel(), + ]), + ]; + $permissions["update {$bundle_id} {$entity_type_id}"] = [ + 'title' => $this->t('@bundle: Update @type', [ + '@bundle' => $bundle->label(), + '@type' => $entity_type->getPluralLabel(), + ]), + ]; + $permissions["delete {$bundle_id} {$entity_type_id}"] = [ + 'title' => $this->t('@bundle: Delete @type', [ + '@bundle' => $bundle->label(), + '@type' => $entity_type->getPluralLabel(), + ]), + ]; + } + } + + 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 c2cd808..e8f9057 100644 --- a/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php +++ b/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php @@ -14,6 +14,7 @@ use Drupal\entity\Revision\RevisionableContentEntityBase; * label = @Translation("Entity test with enhancements"), * handlers = { * "storage" = "\Drupal\Core\Entity\Sql\SqlContentEntityStorage", + * "access" = "\Drupal\entity\EntityAccessControlHandler", * "form" = { * "add" = "\Drupal\entity\Form\RevisionableContentEntityForm", * "edit" = "\Drupal\entity\Form\RevisionableContentEntityForm", @@ -33,6 +34,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..4cd7cb9 --- /dev/null +++ b/tests/modules/entity_module_test/src/Entity/EnhancedOwnerEntity.php @@ -0,0 +1,138 @@ +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 @@ +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 @@ +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([], ['bypass entity_test_enhanced access']); + $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([], ['bypass entity_test_owner access']); + + $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 bypass can update both entities. + $this->assertTrue($entity->access('update', $user6)); + $this->assertTrue($other_entity->access('update', $user6)); + } + +}