Skip to content

Commit

Permalink
Merge pull request #26041 from seamuslee001/negative_acls
Browse files Browse the repository at this point in the history
Permit ACL rules that negate (deny) access
  • Loading branch information
seamuslee001 authored Jun 19, 2023
2 parents d30704b + f6b845d commit b224423
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 3 deletions.
57 changes: 57 additions & 0 deletions CRM/ACL/BAO/ACL.php
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ public static function whereClause($type, &$tables, &$whereTables, $contactID =
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
";

Expand All @@ -254,6 +255,21 @@ public static function whereClause($type, &$tables, &$whereTables, $contactID =
$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);
Expand Down Expand Up @@ -347,6 +363,8 @@ public static function group(
$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);
}
}
Expand Down Expand Up @@ -468,6 +486,7 @@ protected static function loadPermittedIDs(int $contactID, string $tableName, in
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
";
Expand All @@ -493,4 +512,42 @@ protected static function loadPermittedIDs(int $contactID, string $tableName, in
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;
}

}
5 changes: 4 additions & 1 deletion CRM/ACL/Form/ACL.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ public function buildQuickForm() {
$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']);
}
Expand Down Expand Up @@ -253,7 +257,6 @@ public function postProcess() {
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.
Expand Down
1 change: 1 addition & 0 deletions CRM/ACL/Page/ACL.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ public function browse() {
$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;
Expand Down
6 changes: 5 additions & 1 deletion templates/CRM/ACL/Form/ACL.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
<tr class="crm-acl-form-block-operation">
<td class="label">{$form.operation.label}</td>
<td>{$form.operation.html}<br />
<span class="description">{ts}What type of operation (action) is being permitted?{/ts}</span>
<span class="description">{ts}What type of operation (action) is being referenced?{/ts}</span>
</td>
</tr>
<tr class="crm-acl-form-block-object_type">
Expand All @@ -45,6 +45,10 @@
<div class="status description">{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}</div></td>
{/if}
</tr>
<tr class="crm-acl-form-block-deny">
<td class="label">{$form.deny.label}</td>
<td>{$form.deny.html}</td>
</tr>
</table>
<div id="id-group-acl">
<table class="form-layout-compressed">
Expand Down
2 changes: 2 additions & 0 deletions templates/CRM/ACL/Page/ACL.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<th>{ts}Which Data{/ts}</th>
<th>{ts}Description{/ts}</th>
<th>{ts}Enabled?{/ts}</th>
<th>{ts}Mode{/ts}</th>
<th></th>
</tr>
</thead>
Expand All @@ -42,6 +43,7 @@
<td class="crm-acl-object" >{$row.object}</td>
<td class="crm-acl-name crm-editable" data-field="name">{$row.name}</td>
<td class="crm-acl-is_active" id="row_{$aclID}_status">{if $row.is_active eq 1} {ts}Yes{/ts} {else} {ts}No{/ts} {/if}</td>
<td class="crm-acl-deny" id="row_{$aclID}_deny">{if $row.deny}{ts}Deny{/ts}{else}{ts}Allow{/ts}{/if}</td>
<td>{$row.action|replace:'xx':$aclID}</td>
</tr>
{/foreach}
Expand Down
74 changes: 73 additions & 1 deletion tests/phpunit/api/v3/ACLPermissionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
+--------------------------------------------------------------------+
*/

use Civi\Api4\ACLEntityRole;
use Civi\Api4\Contact;
use Civi\Api4\CustomField;
use Civi\Api4\CustomGroup;
Expand Down Expand Up @@ -1187,7 +1188,6 @@ public function testApi4CustomGroupACL(): void {
)
->execute()->single()['id'];
}

$this->createLoggedInUser();
$this->aclGroupHookType = 'civicrm_custom_group';
CRM_Core_Config::singleton()->userPermissionClass->permissions = [
Expand Down Expand Up @@ -1244,6 +1244,78 @@ public function testApi4CustomGroupACL(): void {
$this->assertEquals('Custom_extra_group_4', $getEntities[0]['name']);
}

/**
* @throws \CRM_Core_Exception
*/
public function testNegativeCustomGroupACL(): void {
// Create 2 multi-record custom entities and 2 regular custom fields
$customGroups = [];
foreach ([1, 2, 3, 4] as $i) {
$customGroups[$i] = CustomGroup::create(FALSE)
->addValue('title', "negative_extra_group_$i")
->addValue('extends', 'Contact')
->addValue('is_multiple', $i >= 3)
->addChain('field', CustomField::create()
->addValue('label', "negative_extra_field_$i")
->addValue('custom_group_id', '$id')
->addValue('html_type', 'Text')
->addValue('data_type', 'String')
)
->execute()->single()['id'];
$this->callAPISuccess('Acl', 'create', [
'name' => 'Permit everyone to access custom group ' . $customGroups[$i],
'deny' => 0,
'entity_table' => 'civicrm_acl_role',
'entity_id' => 0,
'operation' => 'Edit',
'object_table' => 'civicrm_custom_group',
'object_id' => $customGroups[$i],
]);
}

$this->callAPISuccess('OptionValue', 'create', [
'option_group_id' => 'acl_role',
'label' => 'Test Negative ACL Role',
'value' => 4,
'is_active' => 1,
]);
$aclGroup = $this->groupCreate();
ACLEntityRole::create(FALSE)->setValues([
'acl_role_id' => 4,
'entity_table' => 'civicrm_group',
'entity_id' => $aclGroup,
'is_active' => 1,
])->execute();
$this->callAPISuccess('Acl', 'create', [
'name' => 'Test Negative ACL',
'deny' => 1,
'entity_table' => 'civicrm_acl_role',
'entity_id' => 4,
'operation' => 'Edit',
'object_table' => 'civicrm_custom_group',
'object_id' => $customGroups[2],
]);
$userID = $this->createLoggedInUser();
CRM_Core_Config::singleton()->userPermissionClass->permissions = [
'access CiviCRM',
'view my contact',
];
$this->callAPISuccess('GroupContact', 'create', [
'contact_id' => $userID,
'group_id' => $aclGroup,
'status' => 'Added',
]);
Civi::cache('metadata')->clear();
Civi::$statics['CRM_ACL_BAO_ACL'] = [];
$getFields = Contact::getFields()
->addWhere('name', 'LIKE', 'negative_extra_group_%.negative_extra_field_%')
->execute();
$this->assertCount(1, $getFields);

Civi::cache('metadata')->clear();
Civi::$statics['CRM_ACL_BAO_ACL'] = [];
}

public function aclGroupHookAllResults($action, $contactID, $tableName, &$allGroups, &$currentGroups) {
if ($tableName === $this->aclGroupHookType) {
$currentGroups = array_keys($allGroups);
Expand Down

0 comments on commit b224423

Please sign in to comment.