access_policy-1.0.x-dev/src/AccessPolicySelection.php
src/AccessPolicySelection.php
<?php namespace Drupal\access_policy; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; /** * Handles the selection of access policies. */ class AccessPolicySelection implements AccessPolicySelectionInterface { use StringTranslationTrait; /** * Invalid set key. * * This can happen if an entity is assigned two or more policies that are * not part of a specific set. */ const NULL_SET = 'null_set'; /** * The entity type manager. * * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ protected $entityTypeManager; /** * The access policy settings. * * @var \Drupal\access_policy\EntityTypeSettingsInterface */ protected $entityTypeSettings; /** * The access policy discovery service. * * @var \Drupal\access_policy\AccessPolicyDiscoveryInterface */ protected $accessPolicyDiscovery; /** * The selection strategy plugin manager. * * @var \Drupal\access_policy\SelectionStrategyPluginManager */ protected $selectionStrategyManager; /** * The selection strategy keyed by entity type id. * * @var \Drupal\access_policy\Plugin\access_policy\SelectionStrategy\SelectionStrategyInterface[] */ protected $selectionStrategy; /** * Constructs a new AccessPolicySelection service. * * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. * @param \Drupal\access_policy\EntityTypeSettingsInterface $settings * The access policy settings. * @param \Drupal\access_policy\AccessPolicyDiscoveryInterface $access_policy_discovery * The access policy discovery service. * @param \Drupal\access_policy\SelectionStrategyPluginManager $selection_strategy_manager * The selection strategy plugin manager. */ public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityTypeSettingsInterface $settings, AccessPolicyDiscoveryInterface $access_policy_discovery, SelectionStrategyPluginManager $selection_strategy_manager) { $this->entityTypeManager = $entity_type_manager; $this->entityTypeSettings = $settings; $this->accessPolicyDiscovery = $access_policy_discovery; $this->selectionStrategyManager = $selection_strategy_manager; } /** * {@inheritdoc} */ public function validateAssignAccessPolicy(EntityInterface $entity, $account) { if (!$this->hasAllowedAccessPolicies($entity, $account)) { return FALSE; } if (!$this->hasPermissionToCurrentPolicy($entity, $account)) { return FALSE; } return TRUE; } /** * Determine whether there are any allowed access policies. * * @param \Drupal\Core\Entity\EntityInterface $entity * The current entity. * @param \Drupal\Core\Session\AccountInterface $account * The current user. * * @return bool * TRUE if the user can assign any policies; FALSE otherwise. * * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException */ protected function hasAllowedAccessPolicies(EntityInterface $entity, AccountInterface $account) { $allowed_policies = $this->accessPolicyDiscovery->getAllowedAccessPolicies($entity, $account); if (empty($allowed_policies)) { return FALSE; } return TRUE; } /** * Determine whether the user has access to the current assigned policy. * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity. * @param \Drupal\Core\Session\AccountInterface $account * The current user. * * @return bool * TRUE if the user has access to the current policy; FALSE otherwise. */ protected function hasPermissionToCurrentPolicy(EntityInterface $entity, AccountInterface $account) { // Compare allowed policies with the current policy. If not found, then do // not grant access. $policies = $this->getCurrentPolicy($entity); foreach ($policies as $policy) { if (!$this->hasPermission($policy, $account)) { return FALSE; } } return TRUE; } /** * Get the current policy on the entity. * * @param \Drupal\Core\Entity\EntityInterface $entity * The current entity. * * @return \Drupal\Core\Entity\EntityInterface[] * Array of access policy entities. * * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException */ protected function getCurrentPolicy(EntityInterface $entity) { // Compare allowed policies with the current policy. If not found, then do // not grant access. $current_policies = $entity->get('access_policy')->getValue(); $current_policies = array_filter($current_policies, function ($item) { return (!empty($item['target_id'])); }); $policy_ids = array_map(function ($item) { return $item['target_id']; }, $current_policies); if (empty($policy_ids)) { return []; } return $this->entityTypeManager->getStorage('access_policy')->loadMultiple($policy_ids); } /** * {@inheritdoc} */ public function getAllowedAccessPolicies(EntityInterface $entity, AccountInterface $account) { return $this->accessPolicyDiscovery->getAllowedAccessPolicies($entity, $account); } /** * {@inheritdoc} */ public function getApplicablePolicies(EntityInterface $entity, AccountInterface $account) { return $this->accessPolicyDiscovery->getApplicablePolicies($entity, $account); } /** * Get the access policy discovery service. * * @return \Drupal\access_policy\AccessPolicyDiscoveryInterface * The access policy discovery service. */ public function getDiscovery() { return $this->accessPolicyDiscovery; } /** * Resets the discovery cache. */ public function resetCache() { $this->accessPolicyDiscovery->resetCache(); } /** * {@inheritdoc} */ public function getDefaultPolicy(EntityInterface $entity, AccountInterface $account) { // First check to see if the strategy has overridden this value. $strategy = $this->getSelectionStrategy($entity->getEntityTypeId()); $assignment = $strategy->getOption('dynamic_assignment'); if ($assignment == 'on_create' || $assignment == 'on_save' || $assignment == 'on_change') { return $this->getApplicablePolicies($entity, $account); } return FALSE; } /** * Determine whether the access policy should be updated. * * The access policy should only be updated if it's new or if the fields * observed by selection rules have changed. * * @param \Drupal\Core\Entity\EntityInterface $entity * The current entity. * @param \Drupal\Core\Session\AccountInterface $account * The current user. * * @return bool * TRUE if the access policy should be updated; FALSE otherwise. */ public function shouldUpdateAccessPolicy(EntityInterface $entity, AccountInterface $account) { if ($entity->isNew()) { return TRUE; } // Don't allow any changes to happen if the user does not have permission // to the current policy. if (!$this->hasPermissionToCurrentPolicy($entity, $account)) { return FALSE; } $strategy = $this->getSelectionStrategy($entity->getEntityTypeId()); if ($strategy->getOption('dynamic_assignment') == 'on_change') { $fields = $this->getSelectionRuleFields($entity, $account); if (!empty($fields)) { // If the allowed policies have selection rules then check to see if the // fields changed. But only if dynamic assignment is set to on_change. if ($this->hasEntityChangedBetweenRevisions($entity, $fields)) { return TRUE; } // Finally, check to see if there are any applicable access policies // but for some reason they are missing. return $this->hasApplicableButMissingAccessPolicy($entity, $account); } } return TRUE; } /** * Get the applicable fields for selection rules associated with policies. * * @param \Drupal\Core\Entity\EntityInterface $entity * The current entity. * @param \Drupal\Core\Session\AccountInterface $account * The current user. * * @return array * Array of selection rule fields. */ protected function getSelectionRuleFields(EntityInterface $entity, AccountInterface $account) { // Check only the access policies that the user has access too. $access_policies = $this->getAllowedAccessPolicies($entity, $account); $fields = []; // Get all the fields that are observed by selection rules. foreach ($access_policies as $policy) { $selection_rules = $policy->getHandlers('selection_rule'); foreach ($selection_rules as $selection_rule) { if (isset($selection_rule['field'])) { $fields[] = $selection_rule['field']; } } } return $fields; } /** * Determine whether it's missing an access policy. * * This checks to see if it's missing an access policy even though there are * applicable policies available. * * @param \Drupal\Core\Entity\EntityInterface $entity * The current entity. * @param \Drupal\Core\Session\AccountInterface $account * The current user. * * @return bool * TRUE if it's missing an access policy; FALSE otherwise. */ protected function hasApplicableButMissingAccessPolicy(EntityInterface $entity, AccountInterface $account) { $policies = $entity->get('access_policy')->referencedEntities(); if (!empty($policies)) { return FALSE; } $applicable = $this->getApplicablePolicies($entity, $account); if (!empty($applicable)) { return TRUE; } return FALSE; } /** * Determine whether the entity has changed between revisions. * * @param \Drupal\Core\Entity\EntityInterface $entity * The current entity. * @param array $fields * The array of fields to check. * * @return bool * TRUE if the entity has changed; FALSE otherwise. */ protected function hasEntityChangedBetweenRevisions(EntityInterface $entity, array $fields) { // Determine whether the entity has changed. If it has then we need to // execute the selection rules again. // Get the previous revision. $original = $this->getLatestRevision($entity); foreach ($fields as $field) { if ($entity->hasField($field)) { $original_field = $original->get($field); if (!$entity->get($field)->equals($original_field)) { return TRUE; } } } return FALSE; } /** * Get the latest revision. * * When doing field comparisons, we have to check the latest revision. This is * because $entity->original always contains the current revision and can * lead to unpredictable results when trying to determine if a new policy * should be assigned. * * @param \Drupal\Core\Entity\EntityInterface $entity * The current entity. */ protected function getLatestRevision(EntityInterface $entity) { /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */ $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId()); $vid = $storage->getLatestRevisionId($entity->id()); return $storage->loadRevision($vid); } /** * Determine whether a user has the permission to assign an access policy. * * @param \Drupal\Core\Entity\EntityInterface $policy * The access policy entity. * @param \Drupal\Core\Session\AccountInterface $account * The current user. * * @return bool * TRUE if the user can use the policy; FALSE otherwise. */ protected function hasPermission(EntityInterface $policy, AccountInterface $account) { if (!$account->hasPermission('assign ' . $policy->id() . ' access policy')) { return FALSE; } return TRUE; } /** * Get the selection strategy. * * @param string $entity_type * The entity type. * * @return \Drupal\access_policy\Plugin\access_policy\SelectionStrategy\SelectionStrategyInterface * The selection strategy plugin. */ public function getSelectionStrategy($entity_type) { if (!isset($this->selectionStrategy[$entity_type])) { $strategy = $this->getSetting($entity_type, 'selection_strategy'); $settings = $this->getSetting($entity_type, 'selection_strategy_settings'); $plugin = $this->selectionStrategyManager->createInstance($strategy, $settings); $entity_type_obj = $this->entityTypeManager->getDefinition($entity_type); $plugin->setEntityType($entity_type_obj); $this->selectionStrategy[$entity_type] = $plugin; } return $this->selectionStrategy[$entity_type]; } /** * Determine whether empty access policies are allowed. * * @param string $entity_type * The entity type. * * @return bool * TRUE if empty policies are allowed; FALSE otherwise. */ public function emptyPoliciesAllowed($entity_type) { $strategy = $this->getSelectionStrategy($entity_type); if ($strategy->getOption('allow_empty')) { return TRUE; } return FALSE; } /** * Determine whether the policy should be updated on entity update. * * @param string $entity_type * The entity type. * * @return bool * TRUE if the policy should be updated; FALSE otherwise. */ public function assignPolicyOnUpdate($entity_type) { $strategy = $this->getSelectionStrategy($entity_type); $dynamic_assignment = $strategy->getOption('dynamic_assignment'); if ($dynamic_assignment == 'on_save' || $dynamic_assignment == 'on_change') { return TRUE; } return FALSE; } /** * Determine whether the selection page is enabled. * * @param string $entity_type * The entity type. * * @return bool * TRUE if the selection page is enabled. */ public function isSelectionPageEnabled($entity_type) { $strategy = $this->getSelectionStrategy($entity_type); if ($strategy->getOption('enable_selection_page')) { return TRUE; } return FALSE; } /** * Determine whether the operations link is enabled. * * Note that this can only return true if the selection page is also enabled. * * @param string $entity_type * The entity type. * * @return bool * TRUE if the operations link is enabled. */ public function isOperationsLinkEnabled($entity_type) { $strategy = $this->getSelectionStrategy($entity_type); $selection_page_enabled = $strategy->getOption('enable_selection_page'); $operation_link_enabled = $strategy->getOption('show_operations_link'); if ($selection_page_enabled && $operation_link_enabled) { return TRUE; } return FALSE; } /** * Determine whether the assignment field is enabled. * * @param string $entity_type * The entity type. * * @return bool * TRUE if the policy should be updated; FALSE otherwise. */ public function isPolicyFieldEnabled($entity_type) { $strategy = $this->getSelectionStrategy($entity_type); if ($strategy->getOption('enable_policy_field') == TRUE) { return TRUE; } return FALSE; } /** * Get the selection setting value. * * @param string $entity_type * The entity type. * @param string $key * The strategy setting name. * * @return mixed * The strategy setting value. */ protected function getSetting($entity_type, $key) { return $this->getConfig($entity_type)->get($key); } /** * {@inheritdoc} */ public function getSelectionSetFromEntity(EntityInterface $entity) { $value = $entity->get('access_policy')->getValue(); $policy_ids = array_map(function ($item) { return $item['target_id']; }, $value); $access_policies = $this->entityTypeManager->getStorage('access_policy')->loadMultiple($policy_ids); $assigned_sets = []; foreach ($access_policies as $policy) { $set = $policy->getSelectionSet(); $assigned_sets = array_merge($assigned_sets, $set); } // No selection sets are assigned. if (empty($assigned_sets)) { return FALSE; } // If the count matches the same number of policies that that is the set // that policies are from. $total_policies = count($access_policies); $count_values = array_count_values($assigned_sets); $policy = array_search($total_policies, $count_values); // If no policy was found then return null set. if ($policy === FALSE) { return self::NULL_SET; } return $policy; } /** * {@inheritdoc} */ public function getPoliciesInSelectionSet($entity_type, $name) { $policies = $this->entityTypeManager->getStorage('access_policy')->loadByProperties([ 'target_entity_type_id' => $entity_type, ]); return array_filter($policies, function ($policy) use ($name) { $selection_sets = $policy->getSelectionSet(); if (in_array($name, $selection_sets)) { return TRUE; } }); } /** * {@inheritdoc} */ public function getSelectionSets($entity_type) { return $this->getConfig($entity_type)->get('selection_sets'); } /** * {@inheritdoc} */ public function getSelectionSet($entity_type, $name) { $sets = $this->getConfig($entity_type)->get('selection_sets'); if (isset($sets[$name])) { return $sets[$name]; } return FALSE; } /** * Get the access policy entity type settings. * * @return \Drupal\access_policy\EntityTypeSettingsInterface * A entity type settings. */ private function getConfig($entity_type) { return $this->entityTypeSettings->load($entity_type); } }