APIv4 - Proper ACLs for relationship entity
authorcolemanw <coleman@civicrm.org>
Mon, 28 Aug 2023 02:09:19 +0000 (22:09 -0400)
committercolemanw <coleman@civicrm.org>
Mon, 28 Aug 2023 02:09:19 +0000 (22:09 -0400)
Before: APIv3 and v4 require 'edit all contacts' to create/update/delete relationships
After: APIv3 unchanged, but v4 uses ACLs instead so the permission is no longer needed.

CRM/Contact/BAO/Relationship.php
Civi/Api4/Relationship.php
Civi/Test/ACLPermissionTrait.php
tests/phpunit/CRM/Activity/BAO/ActivityTest.php
tests/phpunit/CRM/Group/Page/AjaxTest.php
tests/phpunit/CiviTest/CiviUnitTestCase.php
tests/phpunit/api/v4/Entity/RelationshipTest.php

index 03b91fbf66e267c7ba1282ed98eae45054f8a07b..6405520474f77eacfaaaaa8600bf7595a3a8e884 100644 (file)
@@ -2274,4 +2274,25 @@ SELECT count(*)
     }
   }
 
+  /**
+   * @param string $entityName
+   * @param string $action
+   * @param array $record
+   * @param int $userID
+   * @return bool
+   * @see CRM_Core_DAO::checkAccess
+   */
+  public static function _checkAccess(string $entityName, string $action, array $record, int $userID): bool {
+    // Delegate relationship permissions to contacts a & b
+    foreach (['a', 'b'] as $ab) {
+      if (empty($record["contact_id_$ab"]) && !empty($record['id'])) {
+        $record["contact_id_$ab"] = CRM_Core_DAO::getFieldValue(__CLASS__, $record['id'], "contact_id_$ab");
+      }
+      if (!\Civi\Api4\Utils\CoreUtil::checkAccessDelegated('Contact', 'update', ['id' => $record["contact_id_$ab"]], $userID)) {
+        return FALSE;
+      }
+    }
+    return TRUE;
+  }
+
 }
index 2532d3a65cce112fd98cfba60c961b3ad6caddfb..0be9f893ef4530c908353299b592aefdb68b02b6 100644 (file)
@@ -30,4 +30,16 @@ class Relationship extends Generic\DAOEntity {
       ->setCheckPermissions($checkPermissions);
   }
 
+  /**
+   * @return array
+   */
+  public static function permissions(): array {
+    return [
+      'meta' => ['access CiviCRM'],
+      // get managed by CRM_Core_BAO::addSelectWhereClause
+      // create/update/delete managed by CRM_Contact_BAO_Relationship::_checkAccess
+      'default' => [],
+    ];
+  }
+
 }
index 415a8f6aae77071d745a23c04bc5d9b2590536cc..23368a02a9b74670884fe396d67e73872593c9cd 100644 (file)
@@ -120,6 +120,21 @@ trait ACLPermissionTrait {
     $where = ' contact_a.id > ' . $this->allowedContactId;
   }
 
+  /**
+   * Only specified contact returned.
+   *
+   * @implements CRM_Utils_Hook::aclWhereClause
+   *
+   * @param $type
+   * @param $tables
+   * @param $whereTables
+   * @param $contactID
+   * @param $where
+   */
+  public function aclWhereMultipleContacts($type, &$tables, &$whereTables, &$contactID, &$where) {
+    $where = " contact_a.id IN (" . implode(', ', $this->allowedContacts) . ")";
+  }
+
   /**
    * Set up a core ACL.
    *
index 50392eb0ba4b1fce442781a1c7d6a773f5a9e29e..478f36112a4c5dadea5bd5ced9a1bf0ccb6c4946 100644 (file)
@@ -9,14 +9,14 @@ use Civi\Api4\OptionValue;
  */
 class CRM_Activity_BAO_ActivityTest extends CiviUnitTestCase {
 
+  use Civi\Test\ACLPermissionTrait;
+
   private $allowedContactsACL = [];
 
   private $loggedInUserId = NULL;
 
   private $someContacts = [];
 
-  protected $allowedContacts = [];
-
   /**
    * Set up for test.
    *
index 8ea08a099021bd3b1ab6bdef6a452a6ae6e51e1e..6b2e8225fdf9b3e767f9da12d75788b290001bd0 100644 (file)
@@ -18,11 +18,6 @@ class CRM_Group_Page_AjaxTest extends CiviUnitTestCase {
    */
   protected $_permissionedDisabledGroup;
 
-  /**
-   * @var CRM_Utils_Hook_UnitTests
-   */
-  public $hookClass;
-
   protected $_params = [];
 
   public function setUp(): void {
index b63a80c6fe09a7ea1708c39454661af0cf8e2fe7..f2fc560d362460abeb5f579a73b8c41018f5b743 100644 (file)
@@ -2871,21 +2871,6 @@ class CiviUnitTestCase extends PHPUnit\Framework\TestCase {
     $this->_ids['membership_type'] = $membershipTypeID;
   }
 
-  /**
-   * Only specified contact returned.
-   *
-   * @implements CRM_Utils_Hook::aclWhereClause
-   *
-   * @param $type
-   * @param $tables
-   * @param $whereTables
-   * @param $contactID
-   * @param $where
-   */
-  public function aclWhereMultipleContacts($type, &$tables, &$whereTables, &$contactID, &$where) {
-    $where = " contact_a.id IN (" . implode(', ', $this->allowedContacts) . ")";
-  }
-
   /**
    * @implements CRM_Utils_Hook::selectWhereClause
    *
index a73f637eb1ae3b6b035776f6d21667c1a8a34f80..84c4f75f02ab11c14b0bfba06969dda9c54f84d4 100644 (file)
@@ -18,6 +18,7 @@
 
 namespace api\v4\Entity;
 
+use Civi\API\Exception\UnauthorizedException;
 use Civi\Api4\Contact;
 use api\v4\Api4TestBase;
 use Civi\Api4\Relationship;
@@ -34,6 +35,8 @@ use DateTime;
  */
 class RelationshipTest extends Api4TestBase implements TransactionalInterface {
 
+  use \Civi\Test\ACLPermissionTrait;
+
   /**
    * Test relationship cache tracks created relationships.
    *
@@ -151,4 +154,49 @@ class RelationshipTest extends Api4TestBase implements TransactionalInterface {
     $this->assertEquals($future->format('Y-m-d'), $cacheRecord['end_date']);
   }
 
+  public function testRelationshipCheckAccess(): void {
+    $cid = $this->saveTestRecords('Contact', ['records' => 4])->column('id');
+    $this->allowedContacts = array_slice($cid, 1);
+    \CRM_Core_Config::singleton()->userPermissionClass->permissions = ['access CiviCRM'];
+    \CRM_Utils_Hook::singleton()->setHook('civicrm_aclWhereClause', [$this, 'aclWhereMultipleContacts']);
+    $check = Relationship::checkAccess()
+      ->setAction('create')
+      ->setValues([
+        'contact_id_a' => $cid[0],
+        'contact_id_b' => $cid[1],
+        'relationship_type_id' => 1,
+      ])
+      ->execute()->first();
+    $this->assertFalse($check['access']);
+
+    try {
+      Relationship::create()->setValues([
+        'contact_id_a' => $cid[1],
+        'contact_id_b' => $cid[0],
+        'relationship_type_id' => 1,
+      ])->execute();
+      $this->fail();
+    }
+    catch (UnauthorizedException $e) {
+      Relationship::create(FALSE)->setValues([
+        'contact_id_a' => $cid[1],
+        'contact_id_b' => $cid[0],
+        'relationship_type_id' => 1,
+      ])->execute();
+    }
+    Relationship::create()->setValues([
+      'contact_id_a' => $cid[1],
+      'contact_id_b' => $cid[2],
+      'relationship_type_id' => 1,
+    ])->execute();
+
+    $this->assertCount(2, Relationship::get(FALSE)->addWhere('contact_id_a', '=', $cid[1])->execute());
+    $this->assertCount(1, Relationship::get()->addWhere('contact_id_a', '=', $cid[1])->execute());
+
+    Relationship::delete()
+      ->addWhere('contact_id_a', '=', $cid[1])
+      ->execute()->single();
+
+  }
+
 }