From f5e786908ee1b322447fb0a9f68c621c0b5240a5 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Tue, 11 Apr 2023 20:47:04 -0400 Subject: [PATCH] APIv4 - Add unit test and documentation for custom group permissions --- CRM/Core/Permission.php | 16 ++-- CRM/Utils/Hook.php | 16 ++-- tests/phpunit/api/v3/ACLPermissionTest.php | 98 ++++++++++++++++++++++ 3 files changed, 118 insertions(+), 12 deletions(-) diff --git a/CRM/Core/Permission.php b/CRM/Core/Permission.php index 0144a06abf..2a412f526a 100644 --- a/CRM/Core/Permission.php +++ b/CRM/Core/Permission.php @@ -242,24 +242,30 @@ class CRM_Core_Permission { } /** + * Returns the ids of all custom groups the user is permitted to perform action of "$type" + * * @param int $type + * Type of action e.g. CRM_Core_Permission::VIEW or CRM_Core_Permission::EDIT * @param bool $reset + * Flush cache * @param int $userId * - * @return array + * @return int[] */ public static function customGroup($type = CRM_Core_Permission::VIEW, $reset = FALSE, $userId = NULL) { $customGroups = CRM_Core_PseudoConstant::get('CRM_Core_DAO_CustomField', 'custom_group_id', ['fresh' => $reset]); - $defaultGroups = []; - // check if user has all powerful permission - // or administer civicrm permission (CRM-1905) + // Administrators and users with 'access all custom data' can see all custom groups. if (self::customGroupAdmin($userId)) { return array_keys($customGroups); } - return CRM_ACL_API::group($type, $userId, 'civicrm_custom_group', $customGroups, $defaultGroups); + // By default, users without 'access all custom data' are permitted to see no groups. + $allowedGroups = []; + + // Allow ACLs and hooks to grant permissions to certain groups. + return CRM_ACL_API::group($type, $userId, 'civicrm_custom_group', $customGroups, $allowedGroups); } /** diff --git a/CRM/Utils/Hook.php b/CRM/Utils/Hook.php index 60bc4a4931..9119a0810b 100644 --- a/CRM/Utils/Hook.php +++ b/CRM/Utils/Hook.php @@ -591,26 +591,28 @@ abstract class CRM_Utils_Hook { /** * Called when restricting access to contact-groups or custom_field-groups or profile-groups. * - * @param int $action - * Current action e.g. CRM_ACL_API::VIEW or CRM_ACL_API::EDIT + * Hook subscribers should alter the array of $currentGroups by reference. + * + * @param int $type + * Action type being performed e.g. CRM_ACL_API::VIEW or CRM_ACL_API::EDIT * @param int $contactID * User contactID for whom the check is made. * @param string $tableName * Table name of group, e.g. `civicrm_uf_group` or `civicrm_custom_group`. * Note: for some weird reason when this hook is called for contact groups, this * value will be `civicrm_saved_search` instead of `civicrm_group` as you'd expect. - * @param int[] $allGroups - * The ids of all groups from the above table. + * @param array $allGroups + * All groups from the above table, keyed by id. * @param int[] $currentGroups - * The ids of allowed groups which may be altered by reference. + * Ids of allowed groups (corresponding to array keys of $allGroups) to be altered by reference. * * @return null * the return value is ignored */ - public static function aclGroup($action, $contactID, $tableName, &$allGroups, &$currentGroups) { + public static function aclGroup($type, $contactID, $tableName, &$allGroups, &$currentGroups) { $null = NULL; return self::singleton() - ->invoke(['type', 'contactID', 'tableName', 'allGroups', 'currentGroups'], $action, $contactID, $tableName, $allGroups, $currentGroups, $null, 'civicrm_aclGroup'); + ->invoke(['type', 'contactID', 'tableName', 'allGroups', 'currentGroups'], $type, $contactID, $tableName, $allGroups, $currentGroups, $null, 'civicrm_aclGroup'); } /** diff --git a/tests/phpunit/api/v3/ACLPermissionTest.php b/tests/phpunit/api/v3/ACLPermissionTest.php index 3e0c2a4bdb..0182f7717a 100644 --- a/tests/phpunit/api/v3/ACLPermissionTest.php +++ b/tests/phpunit/api/v3/ACLPermissionTest.php @@ -13,6 +13,7 @@ use Civi\Api4\Contact; use Civi\Api4\CustomField; use Civi\Api4\CustomGroup; use Civi\Api4\CustomValue; +use Civi\Api4\Entity; /** * This class is intended to test ACL permission using the multisite module @@ -47,6 +48,11 @@ class api_v3_ACLPermissionTest extends CiviUnitTestCase { */ protected $_permissionedDisabledGroup; + /** + * @var string + */ + protected $aclGroupHookType; + public function setUp(): void { parent::setUp(); CRM_Core_DAO::createTestObject('CRM_Pledge_BAO_Pledge', [], 1, 0); @@ -1162,4 +1168,96 @@ class api_v3_ACLPermissionTest extends CiviUnitTestCase { $this->assertEquals('2', $customValues[0]['cf.' . $textField]); } + /** + * @throws \CRM_Core_Exception + */ + public function testApi4CustomGroupACL(): 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', "extra_group_$i") + ->addValue('extends', 'Contact') + ->addValue('is_multiple', $i >= 3) + ->addChain('field', CustomField::create() + ->addValue('label', "extra_field_$i") + ->addValue('custom_group_id', '$id') + ->addValue('html_type', 'Text') + ->addValue('data_type', 'String') + ) + ->execute()->single()['id']; + } + + $this->createLoggedInUser(); + $this->aclGroupHookType = 'civicrm_custom_group'; + CRM_Core_Config::singleton()->userPermissionClass->permissions = [ + 'access CiviCRM', + 'view my contact', + ]; + + Civi::$statics['CRM_ACL_BAO_ACL'] = []; + + // Unrestricted + $this->hookClass->setHook('civicrm_aclGroup', [$this, 'aclGroupHookAllResults']); + $getFields = Contact::getFields() + ->addWhere('name', 'LIKE', 'extra_group_%.extra_field_%') + ->execute(); + $this->assertCount(2, $getFields); + + Civi::cache('metadata')->clear(); + Civi::$statics['CRM_ACL_BAO_ACL'] = []; + + // Restricted to no groups + $this->hookClass->setHook('civicrm_aclGroup', [$this, 'aclGroupHookNoResults']); + $getFields = Contact::getFields() + ->addWhere('name', 'LIKE', 'extra_group_%.extra_field_%') + ->execute(); + $this->assertCount(0, $getFields); + + Civi::cache('metadata')->clear(); + Civi::$statics['CRM_ACL_BAO_ACL'] = []; + + // Restricted to group 2 + $this->hookClass->setHook('civicrm_aclGroup', [$this, 'aclGroupHookOneResult']); + $this->_permissionedGroup = $customGroups[2]; + $getFields = Contact::getFields() + ->addWhere('name', 'LIKE', 'extra_group_%.extra_field_%') + ->execute(); + $this->assertCount(1, $getFields); + $this->assertEquals('extra_group_2.extra_field_2', $getFields[0]['name']); + + // Group 2 is not multi-valued, so no custom entities are visible + $getEntities = Entity::get() + ->addWhere('type', 'CONTAINS', 'CustomValue') + ->execute(); + $this->assertCount(0, $getEntities); + + Civi::cache('metadata')->clear(); + Civi::$statics['CRM_ACL_BAO_ACL'] = []; + + // Restricted to group 4 (multi-valued entity) + $this->_permissionedGroup = $customGroups[4]; + $getEntities = Entity::get() + ->addWhere('type', 'CONTAINS', 'CustomValue') + ->execute(); + $this->assertCount(1, $getEntities); + $this->assertEquals('Custom_extra_group_4', $getEntities[0]['name']); + } + + public function aclGroupHookAllResults($action, $contactID, $tableName, &$allGroups, &$currentGroups) { + if ($tableName === $this->aclGroupHookType) { + $currentGroups = array_keys($allGroups); + } + } + + public function aclGroupHookOneResult($action, $contactID, $tableName, &$allGroups, &$currentGroups) { + if ($tableName === $this->aclGroupHookType) { + $currentGroups = [$this->_permissionedGroup]; + } + } + + public function aclGroupHookNoResults($action, $contactID, $tableName, &$allGroups, &$currentGroups) { + // No change + } + } -- 2.25.1