APIv4 - Make LineItem, EntityFinancialTrxn and FinancialTrxn read-only
authorColeman Watts <coleman@civicrm.org>
Tue, 15 Jun 2021 19:04:06 +0000 (15:04 -0400)
committerColeman Watts <coleman@civicrm.org>
Tue, 15 Jun 2021 22:06:12 +0000 (18:06 -0400)
Adds a new ReadOnly trait which annotates write methods as @internal
and sets write permissions to ALWAYS_DENY.

This effectively hides the write actions from the API Explorer,
and restricts use of the write methods to when `checkPermissions = FALSE`.

Civi/Api4/EntityFinancialTrxn.php
Civi/Api4/FinancialItem.php
Civi/Api4/FinancialTrxn.php
Civi/Api4/Generic/Traits/ReadOnly.php [new file with mode: 0644]
Civi/Api4/LineItem.php
tests/phpunit/api/v4/Entity/ConformanceTest.php

index f63803793a66ec0599a414deb36672068e894e1a..568f57d4823bd7b8b74017614a7330484e60e507 100644 (file)
@@ -29,6 +29,7 @@ namespace Civi\Api4;
  */
 class EntityFinancialTrxn extends Generic\DAOEntity {
   use Generic\Traits\EntityBridge;
+  use Generic\Traits\ReadOnly;
 
   /**
    * @return array
index 7f1c3966e93ebf6e28735ede17e55d07667f5eab..ced033b3c445ca51c82712fca7c53e8dee0fe437 100644 (file)
@@ -30,19 +30,6 @@ namespace Civi\Api4;
  * @package Civi\Api4
  */
 class FinancialItem extends Generic\DAOEntity {
-
-  /**
-   * @see \Civi\Api4\Generic\AbstractEntity::permissions()
-   * @return array
-   */
-  public static function permissions() {
-    $permissions = \CRM_Core_Permission::getEntityActionPermissions()['financial_item'] ?? [];
-
-    // Merge permissions for this entity with the defaults
-    return array_merge($permissions, [
-      'create' => [\CRM_Core_Permission::ALWAYS_DENY_PERMISSION],
-      'update' => [\CRM_Core_Permission::ALWAYS_DENY_PERMISSION],
-    ]);
-  }
+  use Generic\Traits\ReadOnly;
 
 }
index f04a37e399e7da98bef2a9e908b5d666c79631f3..f96f515196f9aa9282cc32ac61b594047b4203b7 100644 (file)
@@ -32,5 +32,6 @@ namespace Civi\Api4;
  * @package Civi\Api4
  */
 class FinancialTrxn extends Generic\DAOEntity {
+  use Generic\Traits\ReadOnly;
 
 }
diff --git a/Civi/Api4/Generic/Traits/ReadOnly.php b/Civi/Api4/Generic/Traits/ReadOnly.php
new file mode 100644 (file)
index 0000000..5c9abe2
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved.                        |
+ |                                                                    |
+ | This work is published under the GNU AGPLv3 license with some      |
+ | permitted exceptions and without any warranty. For full license    |
+ | and copyright information, see https://civicrm.org/licensing       |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Api4\Generic\Traits;
+
+/**
+ * Trait for Entities not intended to be publicly writable.
+ */
+trait ReadOnly {
+
+  /**
+   * Not intended to be used outside CiviCRM core code.
+   *
+   * @inheritDoc
+   * @internal
+   */
+  public static function save($checkPermissions = TRUE) {
+    return parent::save($checkPermissions);
+  }
+
+  /**
+   * Not intended to be used outside CiviCRM core code.
+   *
+   * @inheritDoc
+   * @internal
+   */
+  public static function create($checkPermissions = TRUE) {
+    return parent::create($checkPermissions);
+  }
+
+  /**
+   * Not intended to be used outside CiviCRM core code.
+   *
+   * @inheritDoc
+   * @internal
+   */
+  public static function update($checkPermissions = TRUE) {
+    return parent::update($checkPermissions);
+  }
+
+  /**
+   * Not intended to be used outside CiviCRM core code.
+   *
+   * @inheritDoc
+   * @internal
+   */
+  public static function delete($checkPermissions = TRUE) {
+    return parent::delete($checkPermissions);
+  }
+
+  /**
+   * Not intended to be used outside CiviCRM core code.
+   *
+   * @inheritDoc
+   * @internal
+   */
+  public static function replace($checkPermissions = TRUE) {
+    return parent::replace($checkPermissions);
+  }
+
+  /**
+   * @return array
+   */
+  public static function permissions() {
+    $permissions = parent::permissions();
+    $permissions['create'] = $permissions['update'] = $permissions['delete'] = \CRM_Core_Permission::ALWAYS_DENY_PERMISSION;
+    return $permissions;
+  }
+
+}
index 724484be1eecf92eca7f7ad9c71d763e345d0447..ca425f9278e13513cfcf10877db06a28a47fb260 100644 (file)
@@ -25,5 +25,6 @@ namespace Civi\Api4;
  * @package Civi\Api4
  */
 class LineItem extends Generic\DAOEntity {
+  use Generic\Traits\ReadOnly;
 
 }
index 893af3b00b00ce7c5f5cc96a250197cb3ec30395..9c1408a4009d2c398c570d8fa2c66d613977dff8 100644 (file)
@@ -31,8 +31,6 @@ use Civi\Test\HookInterface;
  */
 class ConformanceTest extends UnitTestCase implements HookInterface {
 
-  const READ_ONLY_ENTITIES = '/^(FinancialItem)$/';
-
   use \api\v4\Traits\CheckAccessTrait;
   use \api\v4\Traits\TableDropperTrait;
   use \api\v4\Traits\OptionCleanupTrait {
@@ -215,7 +213,7 @@ class ConformanceTest extends UnitTestCase implements HookInterface {
    * @return mixed
    */
   protected function checkCreation($entity, $entityClass) {
-    $isReadOnly = preg_match(static::READ_ONLY_ENTITIES, $entity);
+    $isReadOnly = $this->isReadOnly($entityClass);
 
     $hookLog = [];
     $onValidate = function(ValidateValuesEvent $e) use (&$hookLog) {
@@ -272,7 +270,7 @@ class ConformanceTest extends UnitTestCase implements HookInterface {
     catch (UnauthorizedException $e) {
       // OK, expected exception
     }
-    if (!preg_match(static::READ_ONLY_ENTITIES, $entity)) {
+    if (!$this->isReadOnly($entityClass)) {
       $this->assertEquals(1, $this->checkAccessCounts["{$entity}::create"]);
     }
     $this->resetCheckAccess();
@@ -353,15 +351,14 @@ class ConformanceTest extends UnitTestCase implements HookInterface {
    * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
    */
   protected function checkDeleteWithNoId($entityClass) {
-    $exceptionThrown = '';
     try {
       $entityClass::delete()
         ->execute();
+      $this->fail("$entityClass should require ID to delete.");
     }
     catch (\API_Exception $e) {
-      $exceptionThrown = $e->getMessage();
+      // OK
     }
-    $this->assertStringContainsString('required', $exceptionThrown);
   }
 
   /**
@@ -391,14 +388,18 @@ class ConformanceTest extends UnitTestCase implements HookInterface {
   protected function checkDeletionAllowed($entityClass, $id, $entity) {
     $this->setCheckAccessGrants(["{$entity}::delete" => TRUE]);
     $this->assertEquals(0, $this->checkAccessCounts["{$entity}::delete"]);
+    $isReadOnly = $this->isReadOnly($entityClass);
 
     $deleteResult = $entityClass::delete()
+      ->setCheckPermissions(!$isReadOnly)
       ->addWhere('id', '=', $id)
       ->execute();
 
     // should get back an array of deleted id
     $this->assertEquals([['id' => $id]], (array) $deleteResult);
-    $this->assertEquals(1, $this->checkAccessCounts["{$entity}::delete"]);
+    if (!$isReadOnly) {
+      $this->assertEquals(1, $this->checkAccessCounts["{$entity}::delete"]);
+    }
     $this->resetCheckAccess();
   }
 
@@ -423,7 +424,9 @@ class ConformanceTest extends UnitTestCase implements HookInterface {
       // OK
     }
 
-    $this->assertEquals(1, $this->checkAccessCounts["{$entity}::delete"]);
+    if (!$this->isReadOnly($entityClass)) {
+      $this->assertEquals(1, $this->checkAccessCounts["{$entity}::delete"]);
+    }
     $this->resetCheckAccess();
   }
 
@@ -459,4 +462,12 @@ class ConformanceTest extends UnitTestCase implements HookInterface {
     return $result;
   }
 
+  /**
+   * @param \Civi\Api4\Generic\AbstractEntity|string $entityClass
+   * @return bool
+   */
+  protected function isReadOnly($entityClass) {
+    return in_array('ReadOnly', $entityClass::getInfo()['type'], TRUE);
+  }
+
 }