From c30d636417840e484df758aa598db764e48f355b Mon Sep 17 00:00:00 2001 From: Seamus Lee Date: Tue, 11 Apr 2023 11:18:41 +1000 Subject: [PATCH] [WIP] Permit ACL rules that negate (deny) Further fixes Expose the deny field on the ACL form and fix current test failures Update lanague as per Coleman --- CRM/ACL/BAO/ACL.php | 57 ++++++++++++++++++++++++++++++++++ CRM/ACL/Form/ACL.php | 5 ++- CRM/ACL/Page/ACL.php | 1 + templates/CRM/ACL/Form/ACL.tpl | 6 +++- templates/CRM/ACL/Page/ACL.tpl | 2 ++ 5 files changed, 69 insertions(+), 2 deletions(-) diff --git a/CRM/ACL/BAO/ACL.php b/CRM/ACL/BAO/ACL.php index 6668ff4e33..154b178357 100644 --- a/CRM/ACL/BAO/ACL.php +++ b/CRM/ACL/BAO/ACL.php @@ -236,6 +236,7 @@ SELECT a.operation, a.object_id AND a.is_active = 1 AND a.object_table = 'civicrm_saved_search' AND a.id IN ( $aclKeys ) + AND a.deny = 0 ORDER BY a.object_id "; @@ -254,6 +255,21 @@ ORDER BY a.object_id $ids[] = $dao->object_id; } } + $denyQuery = "SELECT a.operation, a.object_id + FROM civicrm_acl_cache c, civicrm_acl a + WHERE c.acl_id = a.id + AND a.is_active = 1 + AND a.object_table = 'civicrm_saved_search' + AND a.id IN ( $aclKeys ) + AND a.deny = 1 + AND a.object_id IN (%1) +ORDER BY a.object_id +"; + $denyDao = CRM_Core_DAO::executeQuery($denyQuery, [1 => [implode(',', $ids), 'CommaSeparatedIntegers']]); + while ($denyDao->fetch()) { + $key = array_search($denyDao->object_id, $ids); + unset($ids[$key]); + } if (!empty($ids)) { $ids = implode(',', $ids); @@ -347,6 +363,8 @@ SELECT g.* $ids = $cache->get($cacheKey); if (!is_array($ids)) { $ids = self::loadPermittedIDs((int) $contactID, $tableName, $type, $allGroups); + $denyIds = self::loadDenyIDs((int) $contactID, $tableName, $type, $allGroups); + $ids = array_diff($ids, $denyIds); $cache->set($cacheKey, $ids); } } @@ -468,6 +486,7 @@ SELECT a.operation, a.object_id AND a.is_active = 1 AND a.object_table = %1 AND a.id IN ( $aclKeys ) + AND a.deny = 0 GROUP BY a.operation,a.object_id ORDER BY a.object_id "; @@ -493,4 +512,42 @@ ORDER BY a.object_id return $ids; } + /** + * Load deny acl IDs + * + * @param int $contactID + * @param string $tableName + * @param int $type + * @param array $allGroups + * + * @return array + */ + private static function loadDenyIDs(int $contactID, string $tableName, int $type, $allGroups): array { + $ids = []; + $acls = CRM_ACL_BAO_Cache::build($contactID); + $aclKeys = array_keys($acls); + $aclKeys = implode(',', $aclKeys); + $query = " +SELECT a.operation, a.object_id + FROM civicrm_acl_cache c, civicrm_acl a + WHERE c.acl_id = a.id + AND a.is_active = 1 + AND a.object_table = %1 + AND a.id IN ( $aclKeys ) + AND a.deny = 1 +GROUP BY a.operation,a.object_id +ORDER BY a.object_id +"; + $params = [1 => [$tableName, 'String']]; + $dao = CRM_Core_DAO::executeQuery($query, $params); + while ($dao->fetch()) { + if ($dao->object_id) { + if (self::matchType($type, $dao->operation)) { + $ids[] = $dao->object_id; + } + } + } + return $ids; + } + } diff --git a/CRM/ACL/Form/ACL.php b/CRM/ACL/Form/ACL.php index 809793014a..10e6369f50 100644 --- a/CRM/ACL/Form/ACL.php +++ b/CRM/ACL/Form/ACL.php @@ -160,6 +160,10 @@ class CRM_ACL_Form_ACL extends CRM_Admin_Form { $this->add('select', 'event_id', ts('Event'), $event); $this->add('checkbox', 'is_active', ts('Enabled?')); + $this->addRadio('deny', ts('Mode'), [ + 0 => ts('Allow'), + 1 => ts('Deny'), + ]); $this->addFormRule(['CRM_ACL_Form_ACL', 'formRule']); } @@ -253,7 +257,6 @@ class CRM_ACL_Form_ACL extends CRM_Admin_Form { else { $params = $this->controller->exportValues($this->_name); $params['is_active'] = CRM_Utils_Array::value('is_active', $params, FALSE); - $params['deny'] = 0; $params['entity_table'] = 'civicrm_acl_role'; // Figure out which type of object we're permissioning on and set object_table and object_id. diff --git a/CRM/ACL/Page/ACL.php b/CRM/ACL/Page/ACL.php index 5e4e6e971a..8c3b3b79a1 100644 --- a/CRM/ACL/Page/ACL.php +++ b/CRM/ACL/Page/ACL.php @@ -136,6 +136,7 @@ ORDER BY entity_id $acl[$dao->id]['object_table'] = $dao->object_table; $acl[$dao->id]['object_id'] = $dao->object_id; $acl[$dao->id]['is_active'] = $dao->is_active; + $acl[$dao->id]['deny'] = $dao->deny; if ($acl[$dao->id]['entity_id']) { $acl[$dao->id]['entity'] = $roles[$acl[$dao->id]['entity_id']] ?? NULL; diff --git a/templates/CRM/ACL/Form/ACL.tpl b/templates/CRM/ACL/Form/ACL.tpl index f27f8974bd..583be52ac0 100644 --- a/templates/CRM/ACL/Form/ACL.tpl +++ b/templates/CRM/ACL/Form/ACL.tpl @@ -32,7 +32,7 @@ {$form.operation.label} {$form.operation.html}
- {ts}What type of operation (action) is being permitted?{/ts} + {ts}What type of operation (action) is being referenced?{/ts} @@ -45,6 +45,10 @@
{ts}IMPORTANT: The Drupal permissions for 'access all custom data' and 'profile listings and forms' override and disable specific ACL settings for custom field groups and profiles respectively. Do not enable those Drupal permissions for a Drupal role if you want to use CiviCRM ACL's to control access.{/ts}
{/if} + + {$form.deny.label} + {$form.deny.html} +
diff --git a/templates/CRM/ACL/Page/ACL.tpl b/templates/CRM/ACL/Page/ACL.tpl index d1e07327ce..5d8e168b3f 100644 --- a/templates/CRM/ACL/Page/ACL.tpl +++ b/templates/CRM/ACL/Page/ACL.tpl @@ -30,6 +30,7 @@ + @@ -42,6 +43,7 @@ + {/foreach} -- 2.25.1
{ts}Which Data{/ts} {ts}Description{/ts} {ts}Enabled?{/ts}{ts}Mode{/ts}
{$row.object} {$row.name} {if $row.is_active eq 1} {ts}Yes{/ts} {else} {ts}No{/ts} {/if}{if $row.deny eq 1} {ts}Deny{/ts} {else} {ts}allow{/ts} {/if} {$row.action|replace:'xx':$aclID}