APIv4 Autocomplete - Fix anonymous access and add tests
authorColeman Watts <coleman@civicrm.org>
Wed, 10 Aug 2022 01:18:38 +0000 (21:18 -0400)
committerColeman Watts <coleman@civicrm.org>
Wed, 10 Aug 2022 12:41:36 +0000 (08:41 -0400)
Civi/Api4/Generic/AutocompleteAction.php
ext/afform/core/afform.php
tests/phpunit/api/v4/Action/AutocompleteTest.php

index 59313b014e861e7962ffbfd1ab937692d9a47ce0..bb686d2bb796089125162a5d4ecd7cd482beae39 100644 (file)
@@ -112,6 +112,8 @@ class AutocompleteAction extends AbstractAction {
       $this->savedSearch = ['api_entity' => $entityName];
     }
     $this->loadSavedSearch();
+    // Pass-through this parameter
+    $this->_apiParams['checkPermissions'] = $this->savedSearch['api_params']['checkPermissions'] = $this->getCheckPermissions();
     // Render mode: fetch by id
     if ($this->ids) {
       $this->_apiParams['where'][] = [$idField, 'IN', $this->ids];
@@ -138,7 +140,6 @@ class AutocompleteAction extends AbstractAction {
     else {
       $this->_apiParams['select'] = array_unique(array_merge($this->_apiParams['select'], $select));
     }
-    $this->_apiParams['checkPermissions'] = $this->getCheckPermissions();
     $this->applyFilters();
     $apiResult = civicrm_api4($entityName, 'get', $this->_apiParams);
     $rawResults = array_slice((array) $apiResult, 0, $resultsPerPage);
@@ -206,19 +207,18 @@ class AutocompleteAction extends AbstractAction {
     // Proceed only if permissions are being enforced.'
     // Anonymous users in permission-bypass mode should not be allowed to set arbitrary filters.
     if ($this->getCheckPermissions()) {
-      $field = $this->getField($fieldName);
-      try {
-        civicrm_api4($field['entity'], 'getFields', [
-          'action' => 'get',
-          'where' => [['name', '=', $fieldName]],
-        ])->single();
-        return TRUE;
-      }
-      catch (\CRM_Core_Exception $e) {
-        return FALSE;
-      }
+      // This function checks field permissions
+      return (bool) $this->getField($fieldName);
     }
     return FALSE;
   }
 
+  /**
+   * @return array
+   */
+  public function getPermissions() {
+    // Permissions for this action are checked internally
+    return [];
+  }
+
 }
index a7fc1e3787e4e60d536d8243fab82be2580fdc11..711f1a43be0041e6dd39540413763923ce11b4df 100644 (file)
@@ -527,6 +527,11 @@ function afform_civicrm_alterApiRoutePermissions(&$permissions, $entity, $action
       $permissions = CRM_Core_Permission::ALWAYS_ALLOW_PERMISSION;
     }
   }
+  // This is temporarily stuck here, but probably belongs in core (until this hook is finally abolished)
+  elseif ($action === 'autocomplete') {
+    // Autocomplete widget must be accessible by anonymous users. Permissions are checked internally.
+    $permissions = CRM_Core_Permission::ALWAYS_ALLOW_PERMISSION;
+  }
 }
 
 /**
index 63dc9b444839e893c7a05631a30b803206ac9ea5..e63567af4c2429259164c9315450a24f0f61ad00 100644 (file)
@@ -31,6 +31,11 @@ use Civi\Test\TransactionalInterface;
  */
 class AutocompleteTest extends Api4TestBase implements HookInterface, TransactionalInterface {
 
+  /**
+   * @var callable
+   */
+  private $hookCallback;
+
   /**
    * Listens for civi.api4.entityTypes event to manually add this nonstandard entity
    *
@@ -40,7 +45,15 @@ class AutocompleteTest extends Api4TestBase implements HookInterface, Transactio
     $e->entities['MockBasicEntity'] = MockBasicEntity::getInfo();
   }
 
+  public function on_civi_api_prepare(\Civi\API\Event\PrepareEvent $event) {
+    $apiRequest = $event->getApiRequest();
+    if ($this->hookCallback && is_object($apiRequest) && is_a($apiRequest, 'Civi\Api4\Generic\AutocompleteAction')) {
+      ($this->hookCallback)($apiRequest);
+    }
+  }
+
   public function setUp(): void {
+    $this->hookCallback = NULL;
     // Ensure MockBasicEntity gets added via above listener
     \Civi::cache('metadata')->clear();
     MockBasicEntity::delete(FALSE)->addWhere('identifier', '>', 0)->execute();
@@ -121,4 +134,86 @@ class AutocompleteTest extends Api4TestBase implements HookInterface, Transactio
     $this->assertEquals('fa-star', $result[2]['icon']);
   }
 
+  public function testAutocompleteValidation() {
+    $lastName = uniqid(__FUNCTION__);
+    $sampleData = [
+      [
+        'first_name' => 'One',
+        'api_key' => 'secret123',
+      ],
+      [
+        'first_name' => 'Two',
+        'api_key' => 'secret456',
+      ],
+      [
+        'first_name' => 'Three',
+        'api_key' => 'secret789',
+      ],
+    ];
+    $records = $this->saveTestRecords('Contact', [
+      'records' => $sampleData,
+      'defaults' => ['last_name' => $lastName],
+    ]);
+
+    $this->createLoggedInUser();
+
+    \CRM_Core_Config::singleton()->userPermissionClass->permissions = [
+      'administer CiviCRM',
+      'view all contacts',
+    ];
+
+    // Admin can apply the api_key filter
+    $result = Contact::autocomplete()
+      ->setInput($lastName)
+      ->setClientFilters(['api_key' => 'secret789'])
+      ->execute();
+    $this->assertCount(1, $result);
+
+    \CRM_Core_Config::singleton()->userPermissionClass->permissions = [
+      'access CiviCRM',
+      'view all contacts',
+    ];
+
+    // Non-admin cannot apply filter
+    $result = Contact::autocomplete()
+      ->setInput($lastName)
+      ->setClientFilters(['api_key' => 'secret789'])
+      ->execute();
+    $this->assertCount(3, $result);
+
+    // Cannot apply filter even with permissions disabled
+    $result = Contact::autocomplete(FALSE)
+      ->setInput($lastName)
+      ->setClientFilters(['api_key' => 'secret789'])
+      ->execute();
+    $this->assertCount(3, $result);
+
+    // Assert that the end-user is not allowed to inject arbitrary savedSearch params
+    $msg = '';
+    try {
+      $result = Contact::autocomplete()
+        ->setInput($lastName)
+        ->setSavedSearch([
+          'api_entity' => 'Contact',
+          'api_params' => [],
+        ])
+        ->execute();
+    }
+    catch (\CRM_Core_Exception $e) {
+      $msg = $e->getMessage();
+    }
+    $this->assertEquals('Parameter "savedSearch" is not of the correct type. Expecting string.', $msg);
+
+    // With hook callback, permissions can be overridden by injecting a trusted filter
+    $this->hookCallback = function(\Civi\Api4\Generic\AutocompleteAction $action) {
+      $action->addFilter('api_key', 'secret456');
+      $action->setCheckPermissions(FALSE);
+    };
+
+    $result = Contact::autocomplete()
+      ->setInput($lastName)
+      ->execute();
+    $this->assertCount(1, $result);
+  }
+
 }