Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
8b7aeaa
Adds protection module code for testiong.
agentrickard Apr 10, 2019
605e51e
Added more scaffold.
jasonpartyka Apr 10, 2019
7d8b7e7
Generalized check to see if there is an entity being used, and added …
jasonpartyka Apr 12, 2019
86f1869
Added functional tests for workbench_access_protect.
jasonpartyka Apr 12, 2019
99f3018
Changed test run command for workbench access protect.
jasonpartyka Apr 12, 2019
57b75e9
Changed test run command for workbench access protect.
jasonpartyka Apr 12, 2019
e0d8a7a
Changed test run command for workbench access protect.
jasonpartyka Apr 12, 2019
e1c695f
Changed test run command for workbench access protect.
jasonpartyka Apr 12, 2019
096b9bc
Changed test run command for workbench access protect.
jasonpartyka Apr 12, 2019
ceaa67d
Changed test group & travis conifg.
jasonpartyka Apr 14, 2019
8f473f2
Added check for bundle-wide usage. TODO: Limit the bundles we are che…
jasonpartyka May 7, 2019
c30d711
Fixed accidental copy.
jasonpartyka May 8, 2019
4534529
Added an 'ids' index to the configuration so that we can check the bu…
jasonpartyka May 17, 2019
4259001
Added IDs to schema.
jasonpartyka Jul 15, 2019
c4ca783
Corrected copy and paste mistake.
jasonpartyka Jul 15, 2019
1132174
Changed type of test to Functional from Unit for workbench access pro…
jasonpartyka Jul 15, 2019
53fca84
Updating test.
jasonpartyka Jul 17, 2019
bcc1f10
Moved workbench_access_protect test.
jasonpartyka Jul 17, 2019
90ee9c5
Renamed the ids field to parent_ids
jasonpartyka Jul 18, 2019
2c61b04
More renaming.
jasonpartyka Jul 18, 2019
56ee594
Testing a change.
jasonpartyka Jul 18, 2019
be01f86
Forcing a test run.
jasonpartyka Jul 18, 2019
3286023
Update DeleteAccessCheck.php
jasonpartyka Dec 10, 2019
6e40db1
Merge pull request #130 from jasonpartyka/protect
agentrickard Dec 10, 2019
1670de6
Merge branch '8.x-1.x' into protect
agentrickard Dec 10, 2019
7fbcfa4
Removed bad unit test.
agentrickard Dec 11, 2019
d0fa08c
Removes references to parent_id variable.
agentrickard Dec 11, 2019
f328b01
Merge branch '8.x-1.x' into protect
agentrickard Dec 12, 2019
03fcfa3
Cleans up DeleteAccessCheck.
agentrickard Dec 12, 2019
35b5614
Cleans up some code style and naming issues.
agentrickard Dec 13, 2019
2de4c8f
Renames taxonomy protect test properly.
agentrickard Dec 17, 2019
b1a1e96
Passing tests.
agentrickard Dec 17, 2019
43a2cd9
Extends tests to cover user protection.
agentrickard Dec 17, 2019
87436a8
Adds stubs for menu testing. Requires special handling in access checks.
agentrickard Dec 19, 2019
4419c77
Handles cases where deleting a parent deletes its children, which we …
agentrickard Dec 19, 2019
27fdb22
Adds todo for new test.
agentrickard Dec 19, 2019
8c9bf60
Adds menu field note.
agentrickard Dec 19, 2019
297281f
Ensure that we do not run checks for access when they are not required.
agentrickard Mar 10, 2020
7487f5c
Adds api example file.
agentrickard Mar 10, 2020
48a40b5
When checking content fields, ensure that we are pulling from workben…
agentrickard Mar 10, 2020
df4ba0e
Optimize lookups for access checks.
agentrickard Mar 11, 2020
5378a8b
Fixes code style issues.
agentrickard Mar 16, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ before_script:
- /usr/bin/env PHP_OPTIONS="-d sendmail_path=$(which true)" drush --yes --verbose site-install minimal --db-url=mysql://root:@127.0.0.1/wa
# Install modules
- travis_retry drush dl inline_entity_form
- drush --yes en simpletest workbench_access taxonomy
- drush --yes en simpletest workbench_access taxonomy workbench_access_protect
- drush cr

# Start a web server on port 8080 in the background.
Expand All @@ -83,6 +83,7 @@ before_script:
- until curl -s localhost:8080; do true; done > /dev/null

script:
- php core/scripts/run-tests.sh --suppress-deprecations --verbose --color --concurrency 4 --types "PHPUnit-Functional" --php `which php` --url http://localhost:8080 "workbench_access"
- php core/scripts/run-tests.sh --suppress-deprecations --verbose --color --concurrency 4 --types "PHPUnit-Kernel" --php `which php` --url http://localhost:8080 "workbench_access"
- php core/scripts/run-tests.sh --suppress-deprecations --verbose --color --concurrency 4 --types "PHPUnit-Unit" --php `which php` --url http://localhost:8080 "workbench_access"
# - php core/scripts/run-tests.sh --suppress-deprecations --verbose --color --concurrency 4 --types "PHPUnit-Functional" --php `which php` --url http://localhost:8080 "workbench_access"
# - php core/scripts/run-tests.sh --suppress-deprecations --verbose --color --concurrency 4 --types "PHPUnit-Kernel" --php `which php` --url http://localhost:8080 "workbench_access"
# - php core/scripts/run-tests.sh --suppress-deprecations --verbose --color --concurrency 4 --types "PHPUnit-Unit" --php `which php` --url http://localhost:8080 "workbench_access"
- php core/scripts/run-tests.sh --suppress-deprecations --verbose --color --concurrency 4 --types "PHPUnit-Functional" --php `which php` --url http://localhost:8080 "workbench_access_protect"
15 changes: 15 additions & 0 deletions tests/src/Traits/WorkbenchAccessTestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,21 @@ protected function createUserWithRole($rid) {
return $user;
}

/**
* Sets up a user the specific permissions and a unique role.
*
* @param array $additional_permissions
* Array of additional permissions beyond 'access administration pages' and
* 'assign workbench access'.
*
* @return \Drupal\user\Entity\User
* The user entity.
*/
protected function setUpUserUniqueRole($additional_permissions) {
$role = $this->createRole($additional_permissions);
return $this->createUserWithRole($role);
}

/**
* Sets up a taxonomy scheme for a given node type.
*
Expand Down
303 changes: 303 additions & 0 deletions workbench_access_protect/src/Access/DeleteAccessCheck.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
<?php

namespace Drupal\workbench_access_protect\Access;

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Core\Entity\EntityInterface;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\Core\Routing\CurrentRouteMatch;
use Drupal\workbench_access\UserSectionStorage;
use Drupal\Core\Entity\EntityFieldManager;
use Drupal\workbench_access\Entity\AccessSchemeInterface;

/**
* Class DeleteAccessCheck.
*/
class DeleteAccessCheck implements DeleteAccessCheckInterface {

/**
* User account.
*
* @var \Drupal\Core\Session\AccountInterface
*/
private $account;

/**
* Default object for current_route_match service.
*
* @var \Drupal\Core\Routing\CurrentRouteMatch
*/
private $route;

/**
* A class for storing and retrieving sections assigned to a user.
*
* @var \Drupal\workbench_access\UserSectionStorage
*/
private $userSectionStorage;

/**
* Manages the discovery of entity fields.
*
* @var \Drupal\Core\Entity\EntityFieldManager
*/
private $fieldManager;

/**
* Manages entity type plugin definitions.
*
* @var \Drupal\Core\Entity\EntityTypeManager
*/
private $entityTypeManager;

/**
* An interface for an entity type bundle info.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
private $entityTypeBundleInfo;

/**
* Constructs a new DeleteAccessCheck.
*/
public function __construct(CurrentRouteMatch $route,
UserSectionStorage $userSectionStorage,
EntityFieldManager $fieldManager,
EntityTypeManager $entityTypeManager,
EntityTypeBundleInfoInterface $entityTypeBundleInfo) {

$this->route = $route;
$this->userSectionStorage = $userSectionStorage;
$this->fieldManager = $fieldManager;
$this->entityTypeManager = $entityTypeManager;
$this->entityTypeBundleInfo = $entityTypeBundleInfo;

}

/**
* This method is used to determine if it is OK to delete.
*
* The check is based on whether or not it is being actively used for access
* control, and if content is assigned to it. If either of these statements
* is true, then 'forbidden' will be returned to prevent the term
* from being deleted.
*
* @return \Drupal\Core\Access\AccessResultAllowed|\Drupal\Core\Access\AccessResultForbidden
* Returns 'forbidden' if the term is being used for access control.
* Returns 'allowed' if the term is not being used for access control.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function access() {

if ($this->isDeleteAllowed()) {
return AccessResult::allowed();
}

return AccessResult::forbidden();
}

/**
* {@inheritdoc}
*/
public function isDeleteAllowed(EntityInterface $entity) {
$return = TRUE;

// If this entity does not have users assigned to it for access control and
// is not assigned to any pieces of content, it is OK to delete it.
$assigned_members = $this->hasMembers($entity);
if ($assigned_members) {
return FALSE;
}

// Breaking up the IF statement is a performance gain, since most sites
// have more nodes than users.
$assigned_content = $this->hasContent($entity);
if ($assigned_content) {
$return = FALSE;
}

return $return;
}

/**
* {@inheritdoc}
*/
public function getBundles(EntityInterface $entity) {
$bundle = $this->entityTypeManager->getDefinitions()[$entity->getEntityTypeId()]->get('bundle_of');
$bundles = $this->entityTypeBundleInfo->getBundleInfo($bundle);
return array_combine(array_keys($bundles), array_keys($bundles));
}

/**
* {@inheritdoc}
*/
public function hasBundles(EntityInterface $entity) {
return !is_null($this->entityTypeManager->getDefinitions()[$entity->getEntityTypeId()]->get('bundle_of'));
}

/**
* {@inheritdoc}
*/
public function isDeleteAllowedBundle($bundle, $entity) {
$bundle_of = $this->entityTypeManager->getDefinitions()[$entity->getEntityTypeId()]->get('bundle_of');
$entity_id_key = $this->entityTypeManager->getDefinitions()[$entity->getEntityTypeId()]->get('entity_keys')['id'];
$entities = $this->entityTypeManager->getStorage($bundle_of)->loadByProperties(
[$entity_id_key => $bundle]
);

/*
* Cycle through the entities of this bundle. As soon as one is discovered
* as being actively used for access control, we can deny delete.
*/
foreach ($entities as $bundle) {
if ($this->isDeleteAllowed($bundle) === FALSE) {
return FALSE;
}
}

return TRUE;
}

/**
* Determines if this term has active members in it.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to inspect.
*
* @return bool
* TRUE if the term has members, FALSE otherwise.
*/
private function hasMembers(EntityInterface $entity) {
return (bool) (count($this->getActiveSections($entity)) > 0);
}

/**
* Inspects the given entity for active users.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to inspect.
*
* @return array
* An array of the users assigned to this section.
*/
private function getActiveSections(EntityInterface $entity) {
/** @var \Drupal\workbench_access\UserSectionStorageInterface $sectionStorage */
$sectionStorage = $this->userSectionStorage;

$editors = array_reduce($this->entityTypeManager->getStorage('access_scheme')->loadMultiple(),
function (array $editors, AccessSchemeInterface $scheme) use ($sectionStorage, $entity) {
$editors += $sectionStorage->getEditors($scheme, $entity->id());
return $editors;
}, []);

return $editors;
}

/**
* Inspects the given entity for active content.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to inspect.
*
* @return bool
* TRUE if content is assigned to this entity.
* FALSE if content is not assigned to this entity.
*/
private function hasContent(EntityInterface $entity) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a call to a method here that will check to see if the term has children or not.
TODO: figure out how to abstract this so that this class doesn't have any explicit taxonomy or term references. Adding a method to the Drupal\workbench_access\Plugin\AccessControlHierarchy class to see if this scheme has children might be the way to do it.

foreach ($this->getAllReferenceFields($entity) as $name => $fieldConfig) {
// Get the entity reference and determine if it is access controlled.
if ($fieldConfig instanceof FieldStorageConfig) {
// Base query.
$entities = \Drupal::entityQuery($fieldConfig->get('entity_type'));
// Check all children, if required.
// @TODO: allow this to be a configurable setting.
// @TODO: test coverage.
$storage = \Drupal::entityTypeManager()->getStorage($entity->getEntityTypeId());
if (method_exists($storage, 'loadChildren')) {
$children = $storage->loadChildren($entity->id());
if ($children) {
$ids = array_keys($children);
$ids[] = $entity->id();
$entities->condition($fieldConfig->get('field_name'), $ids, 'IN');
$children_checked = TRUE;
}
}
// Else just query this entity.
if (empty($children_checked)) {
$entities->condition($fieldConfig->get('field_name'), $entity->id());
}
$result = $entities->range(0, 1)->execute();
if (count($result) > 0) {
return TRUE;
}
}
// @TODO: check for menu field handling.
}

return FALSE;
}

/**
* Get all entities with referencing fields targeting the ID.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to inspect.
*
* @return array
* An array of entities with referencing fields targeting the given ID.
*/
private function getAllReferenceFields(EntityInterface $entity) {
// First, we are going to try to retrieve a cached instance.
$found_fields = \Drupal::cache()->get('workbench_access_protect');
if ($found_fields === FALSE) {
$found_fields = [];
$scheme_storage = \Drupal::entityTypeManager()
->getStorage('access_scheme');
// Grab the fields configured for each access scheme and then check
// that they are within our list of protected entity types.
foreach ($scheme_storage->loadMultiple() as $scheme) {
$access_scheme = $scheme->getAccessScheme();
$scheme_type = $scheme->get('scheme');
$protect_list = workbench_access_protect_list($scheme_type);
if (in_array($entity->getEntityTypeId(), $protect_list, TRUE)) {
$configuration = $access_scheme->getConfiguration();
foreach ($configuration['fields'] as $field_info) {
$fieldConfig = FieldStorageConfig::loadByName($field_info['entity_type'], $field_info['field']);
if ($fieldConfig) {
$found_fields[$field_info['field']] = $fieldConfig;
}
}
}
}
}
else {
$found_fields = $found_fields->data;
}

\Drupal::cache()
->set('workbench_access_protect', $found_fields, \Drupal::time()->getRequestTime() + 60);

return $found_fields;
}

/**
* {@inheritdoc}
*/
public function isAccessControlled(EntityInterface $entity) {
$schemes = $this->entityTypeManager->getStorage('access_scheme')->loadMultiple();
/** @var \Drupal\workbench_access\Entity\AccessScheme $scheme */
foreach ($schemes as $scheme) {
foreach ($scheme->getAccessScheme()->getConfiguration() as $config) {
if (in_array($entity->bundle(), $config)) {
return TRUE;
}
}
}
return FALSE;
}

}
Loading