From: Tim Otten Date: Thu, 11 Feb 2021 02:25:23 +0000 (-0800) Subject: Afform - Delegated API calls should use a security helper X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=0fd2d6af2c865a13875021fbfbc81470495d28dc;p=civicrm-core.git Afform - Delegated API calls should use a security helper Before ------ The `Prefill`, `Submit`, and `AbstractProcessor` have various calls to `civicrm_api4()` which are meant to read/write data for a specific entity. These calls may or may not have `checkPermissions` sprinkled in. After ----- Those calls to `civicrm_api4()` now go through a wrapper. For example: ```php $formDataModel->getSecureApi4('spouse')('Contact', 'get', [...]); ``` In this call, we use the settings for the `spouse` entity to pick a security policy. Then, we execute the `Contact.get` API within that security policy. --- diff --git a/ext/afform/core/Civi/Afform/FormDataModel.php b/ext/afform/core/Civi/Afform/FormDataModel.php index fc89bb267f..26eb41fc2d 100644 --- a/ext/afform/core/Civi/Afform/FormDataModel.php +++ b/ext/afform/core/Civi/Afform/FormDataModel.php @@ -2,6 +2,7 @@ namespace Civi\Afform; +use Civi\API\Exception\UnauthorizedException; use Civi\Api4\Afform; /** @@ -23,6 +24,12 @@ class FormDataModel { */ protected $blocks = []; + /** + * @var array + * Ex: $secureApi4s['spouse'] = function($entity, $action, $params){...}; + */ + protected $secureApi4s = []; + public function __construct($layout) { $root = AHQ::makeRoot($layout); $this->entities = array_column(AHQ::getTags($root, 'af-entity'), NULL, 'name'); @@ -34,6 +41,35 @@ class FormDataModel { $this->parseFields($layout); } + /** + * Prepare to access APIv4 on behalf of a particular entity. This will enforce + * any security options associated with that entity. + * + * $formDataModel->getSecureApi4('me')('Contact', 'get', ['where'=>[...]]); + * $formDataModel->getSecureApi4('me')('Email', 'create', [...]); + * + * @param string $entityName + * Ex: 'Individual1', 'Individual2', 'me', 'spouse', 'children', 'theMeeting' + * + * @return callable + * API4-style + */ + public function getSecureApi4($entityName) { + if (!isset($this->secureApi4s[$entityName])) { + if (!isset($this->entities[$entityName])) { + throw new UnauthorizedException("Cannot delegate APIv4 calls on behalf of unrecognized entity ($entityName)"); + } + $this->secureApi4s[$entityName] = function(string $entity, string $action, $params = [], $index = NULL) use ($entityName) { + // FIXME Pick real value of checkPermissions. Possibly limit by ID. + // \Civi::log()->info("secureApi4($entityName): call($entity, $action)"); + // $params['checkPermissions'] = FALSE; + $params['checkPermissions'] = TRUE; + return civicrm_api4($entity, $action, $params, $index); + }; + } + return $this->secureApi4s[$entityName]; + } + /** * @param array $nodes * @param string $entity diff --git a/ext/afform/core/Civi/Api4/Action/Afform/AbstractProcessor.php b/ext/afform/core/Civi/Api4/Action/Afform/AbstractProcessor.php index baa12b91e5..077e73cee7 100644 --- a/ext/afform/core/Civi/Api4/Action/Afform/AbstractProcessor.php +++ b/ext/afform/core/Civi/Api4/Action/Afform/AbstractProcessor.php @@ -39,13 +39,8 @@ abstract class AbstractProcessor extends \Civi\Api4\Generic\AbstractAction { public function _run(Result $result) { // This will throw an exception if the form doesn't exist $this->_afform = (array) civicrm_api4('Afform', 'get', ['checkPermissions' => FALSE, 'where' => [['name', '=', $this->name]]], 0); - if ($this->getCheckPermissions()) { - if (!\CRM_Core_Permission::check("@afform:" . $this->_afform['name'])) { - throw new \Civi\API\Exception\UnauthorizedException("Authorization failed: Cannot process form " . $this->_afform['name']); - } - } - $this->_formDataModel = new FormDataModel($this->_afform['layout']); + $this->checkPermissions(); $this->validateArgs(); $result->exchangeArray($this->processForm()); } @@ -64,6 +59,19 @@ abstract class AbstractProcessor extends \Civi\Api4\Generic\AbstractAction { } } + /** + * Assert that the current form submission is authorized. + * + * @throws \Civi\API\Exception\UnauthorizedException + */ + protected function checkPermissions() { + if ($this->getCheckPermissions()) { + if (!\CRM_Core_Permission::check("@afform:" . $this->_afform['name'])) { + throw new \Civi\API\Exception\UnauthorizedException("Authorization failed: Cannot process form " . $this->_afform['name']); + } + } + } + /** * @return array */ diff --git a/ext/afform/core/Civi/Api4/Action/Afform/Prefill.php b/ext/afform/core/Civi/Api4/Action/Afform/Prefill.php index e4b93be85f..0b69d39576 100644 --- a/ext/afform/core/Civi/Api4/Action/Afform/Prefill.php +++ b/ext/afform/core/Civi/Api4/Action/Afform/Prefill.php @@ -36,27 +36,18 @@ class Prefill extends AbstractProcessor { * @throws \API_Exception */ private function loadEntity($entity, $id) { - $checkPermissions = TRUE; - if ($entity['type'] == 'Contact' && !empty($this->args[$entity['name'] . '-cs'])) { - $checkSum = civicrm_api4('Contact', 'validateChecksum', [ - 'checksum' => $this->args[$entity['name'] . '-cs'], - 'contactId' => $id, - ]); - $checkPermissions = empty($checkSum[0]['valid']); - } - $result = civicrm_api4($entity['type'], 'get', [ + $api4 = $this->_formDataModel->getSecureApi4($entity['name']); + $result = $api4($entity['type'], 'get', [ 'where' => [['id', '=', $id]], 'select' => array_keys($entity['fields']), - 'checkPermissions' => $checkPermissions, ]); foreach ($result as $item) { $data = ['fields' => $item]; foreach ($entity['joins'] ?? [] as $joinEntity => $join) { - $data['joins'][$joinEntity] = (array) civicrm_api4($joinEntity, 'get', [ + $data['joins'][$joinEntity] = (array) $api4($joinEntity, 'get', [ 'where' => self::getJoinWhereClause($entity['type'], $joinEntity, $item['id']), 'limit' => !empty($join['af-repeat']) ? $join['max'] ?? 0 : 1, 'select' => array_keys($join['fields']), - 'checkPermissions' => $checkPermissions, 'orderBy' => self::fieldExists($joinEntity, 'is_primary') ? ['is_primary' => 'DESC'] : [], ]); } diff --git a/ext/afform/core/Civi/Api4/Action/Afform/Submit.php b/ext/afform/core/Civi/Api4/Action/Afform/Submit.php index dda6a41739..8c81828147 100644 --- a/ext/afform/core/Civi/Api4/Action/Afform/Submit.php +++ b/ext/afform/core/Civi/Api4/Action/Afform/Submit.php @@ -51,9 +51,10 @@ class Submit extends AbstractProcessor { */ public static function processContacts(AfformSubmitEvent $event) { foreach ($event->entityValues['Contact'] ?? [] as $entityName => $contacts) { + $api4 = $event->formDataModel->getSecureApi4($entityName); foreach ($contacts as $contact) { - $saved = civicrm_api4('Contact', 'save', ['records' => [$contact['fields']]])->first(); - self::saveJoins('Contact', $saved['id'], $contact['joins'] ?? []); + $saved = $api4('Contact', 'save', ['records' => [$contact['fields']]])->first(); + self::saveJoins($api4, 'Contact', $saved['id'], $contact['joins'] ?? []); } } unset($event->entityValues['Contact']); @@ -68,28 +69,29 @@ class Submit extends AbstractProcessor { foreach ($event->entityValues as $entityType => $entities) { // Each record is an array of one or more items (can be > 1 if af-repeat is used) foreach ($entities as $entityName => $records) { + $api4 = $event->formDataModel->getSecureApi4($entityName); foreach ($records as $record) { - $saved = civicrm_api4($entityType, 'save', ['records' => [$record['fields']]])->first(); - self::saveJoins($entityType, $saved['id'], $record['joins'] ?? []); + $saved = $api4($entityType, 'save', ['records' => [$record['fields']]])->first(); + self::saveJoins($api4, $entityType, $saved['id'], $record['joins'] ?? []); } } unset($event->entityValues[$entityType]); } } - protected static function saveJoins($mainEntityName, $entityId, $joins) { + protected static function saveJoins($api4, $mainEntityName, $entityId, $joins) { foreach ($joins as $joinEntityName => $join) { $values = self::filterEmptyJoins($joinEntityName, $join); // FIXME: Replace/delete should only be done to known contacts if ($values) { - civicrm_api4($joinEntityName, 'replace', [ + $api4($joinEntityName, 'replace', [ 'where' => self::getJoinWhereClause($mainEntityName, $joinEntityName, $entityId), 'records' => $values, ]); } else { try { - civicrm_api4($joinEntityName, 'delete', [ + $api4($joinEntityName, 'delete', [ 'where' => self::getJoinWhereClause($mainEntityName, $joinEntityName, $entityId), ]); }