->setCheckPermissions($checkPermissions);
}
+ /**
+ * @return \Civi\Api4\Generic\CheckAccessAction
+ */
+ public static function checkAccess($customGroup) {
+ return new Generic\CheckAccessAction("Custom_$customGroup", __FUNCTION__);
+ }
+
/**
* @see \Civi\Api4\Generic\AbstractEntity::permissions()
* @return array
namespace Civi\Api4\Generic;
use Civi\Api4\Event\ValidateValuesEvent;
+use Civi\API\Exception\UnauthorizedException;
+use Civi\Api4\Utils\CoreUtil;
/**
* Base class for all `Create` api actions.
/**
* @throws \API_Exception
+ * @throws \Civi\API\Exception\UnauthorizedException
*/
protected function validateValues() {
// FIXME: There should be a protocol to report a full list of errors... Perhaps a subclass of API_Exception?
if ($unmatched) {
throw new \API_Exception("Mandatory values missing from Api4 {$this->getEntityName()}::{$this->getActionName()}: " . implode(", ", $unmatched), "mandatory_missing", ["fields" => $unmatched]);
}
+
+ if ($this->checkPermissions && !CoreUtil::checkAccess($this->getEntityName(), $this->getActionName(), $this->getValues())) {
+ throw new UnauthorizedException("ACL check failed");
+ }
+
$e = new ValidateValuesEvent($this, [$this->getValues()], new \CRM_Utils_LazyArray(function () {
return [['old' => NULL, 'new' => $this->getValues()]];
}));
*/
abstract public static function getFields();
+ /**
+ * @return \Civi\Api4\Generic\CheckAccessAction
+ */
+ public static function checkAccess() {
+ return new CheckAccessAction(self::getEntityName(), __FUNCTION__);
+ }
+
/**
* Returns a list of permissions needed to access the various actions in this api.
*
namespace Civi\Api4\Generic;
use Civi\Api4\Event\ValidateValuesEvent;
+use Civi\API\Exception\UnauthorizedException;
+use Civi\Api4\Utils\CoreUtil;
/**
* Create or update one or more $ENTITIES.
/**
* @throws \API_Exception
+ * @throws \Civi\API\Exception\UnauthorizedException
*/
protected function validateValues() {
// FIXME: There should be a protocol to report a full list of errors... Perhaps a subclass of API_Exception?
if ($unmatched) {
throw new \API_Exception("Mandatory values missing from Api4 {$this->getEntityName()}::{$this->getActionName()}: " . implode(", ", $unmatched), "mandatory_missing", ["fields" => $unmatched]);
}
+
+ if ($this->checkPermissions) {
+ foreach ($this->records as $record) {
+ $action = empty($record[$this->idField]) ? 'create' : 'update';
+ if (!CoreUtil::checkAccess($this->getEntityName(), $action, $record)) {
+ throw new UnauthorizedException("ACL check failed");
+ }
+ }
+ }
+
$e = new ValidateValuesEvent($this, $this->records, new \CRM_Utils_LazyArray(function() {
$existingIds = array_column($this->records, $this->idField);
$existing = civicrm_api4($this->getEntityName(), 'get', [
namespace Civi\Api4\Generic;
use Civi\API\Exception\NotImplementedException;
+use Civi\API\Exception\UnauthorizedException;
+use Civi\Api4\Utils\CoreUtil;
/**
* $ACTION one or more $ENTITIES.
*/
public function _run(Result $result) {
foreach ($this->getBatchRecords() as $item) {
+ if ($this->checkPermissions && !CoreUtil::checkAccess($this->getEntityName(), $this->getActionName(), $item)) {
+ throw new UnauthorizedException("ACL check failed");
+ }
$result[] = $this->doTask($item);
}
}
namespace Civi\Api4\Generic;
use Civi\API\Exception\NotImplementedException;
+use Civi\API\Exception\UnauthorizedException;
+use Civi\Api4\Utils\CoreUtil;
/**
* Update one or more $ENTITY with new values.
$this->formatWriteValues($this->values);
$this->validateValues();
foreach ($this->getBatchRecords() as $item) {
- $result[] = $this->writeRecord($this->values + $item);
+ $record = $this->values + $item;
+ if ($this->checkPermissions && !CoreUtil::checkAccess($this->getEntityName(), $this->getActionName(), $record)) {
+ throw new UnauthorizedException("ACL check failed");
+ }
+ $result[] = $this->writeRecord($record);
}
}
--- /dev/null
+<?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 |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ *
+ * @package CRM
+ * @copyright CiviCRM LLC https://civicrm.org/licensing
+ */
+
+
+namespace Civi\Api4\Generic;
+
+use Civi\Api4\Utils\CoreUtil;
+
+/**
+ * Check if current user is authorized to perform specified action on a given $ENTITY.
+ *
+ * @method $this setAction(string $action)
+ * @method string getAction()
+ * @method $this setValues(array $values)
+ * @method array getValues()
+ */
+class CheckAccessAction extends AbstractAction {
+
+ /**
+ * @var string
+ * @required
+ */
+ protected $action;
+
+ /**
+ * @var array
+ * @required
+ */
+ protected $values = [];
+
+ /**
+ * @param \Civi\Api4\Generic\Result $result
+ */
+ public function _run(Result $result) {
+ // Prevent circular checks
+ if ($this->action === 'checkAccess') {
+ $granted = TRUE;
+ }
+ else {
+ $granted = CoreUtil::checkAccess($this->getEntityName(), $this->action, $this->values);
+ }
+ $result->exchangeArray([['access' => $granted]]);
+ }
+
+ /**
+ * This action is always allowed
+ *
+ * @return bool
+ */
+ public function isAuthorized() {
+ return TRUE;
+ }
+
+ /**
+ * Add an item to the values array
+ * @param string $fieldName
+ * @param mixed $value
+ * @return $this
+ */
+ public function addValue(string $fieldName, $value) {
+ $this->values[$fieldName] = $value;
+ return $this;
+ }
+
+}
namespace Civi\Api4\Generic;
+use Civi\API\Exception\UnauthorizedException;
+use Civi\Api4\Utils\CoreUtil;
+
/**
* Delete one or more $ENTITIES.
*
if ($this->getCheckPermissions()) {
foreach (array_keys($items) as $key) {
+ if (!CoreUtil::checkAccess($this->getEntityName(), $this->getActionName(), $items[$key])) {
+ throw new UnauthorizedException("ACL check failed");
+ }
$items[$key]['check_permissions'] = TRUE;
$this->checkContactPermissions($baoName, $items[$key]);
}
namespace Civi\Api4\Generic;
+use Civi\API\Exception\UnauthorizedException;
+use Civi\Api4\Utils\CoreUtil;
+
/**
* Update one or more $ENTITY with new values.
*
// Update a single record by ID unless select requires more than id
if ($this->getSelect() === ['id'] && count($this->where) === 1 && $this->where[0][0] === 'id' && $this->where[0][1] === '=' && !empty($this->where[0][2])) {
$this->values['id'] = $this->where[0][2];
+ if ($this->checkPermissions && !CoreUtil::checkAccess($this->getEntityName(), $this->getActionName(), $this->values)) {
+ throw new UnauthorizedException("ACL check failed");
+ }
$items = [$this->values];
$this->validateValues();
$result->exchangeArray($this->writeObjects($items));
$items = $this->getBatchRecords();
foreach ($items as &$item) {
$item = $this->values + $item;
+ if ($this->checkPermissions && !CoreUtil::checkAccess($this->getEntityName(), $this->getActionName(), $item)) {
+ throw new UnauthorizedException("ACL check failed");
+ }
}
$this->validateValues();
namespace Civi\Api4\Utils;
+use Civi\API\Request;
use CRM_Core_DAO_AllCoreTables as AllCoreTables;
class CoreUtil {
return $customGroupName && \CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $customGroupName, 'is_multiple', 'name');
}
+ /**
+ * Check if current user is authorized to perform specified action on a given entity.
+ *
+ * @param string $entityName
+ * @param string $actionName
+ * @param array $record
+ * @return bool
+ * @throws \API_Exception
+ * @throws \CRM_Core_Exception
+ * @throws \Civi\API\Exception\NotImplementedException
+ * @throws \Civi\API\Exception\UnauthorizedException
+ */
+ public static function checkAccess(string $entityName, string $actionName, array $record) {
+ $action = Request::create($entityName, $actionName, ['version' => 4]);
+ // This checks gatekeeper permissions
+ $granted = $action->isAuthorized();
+ // For get actions, just run a get and ACLs will be applied to the query.
+ // It's a cheap trick and not as efficient as not running the query at all,
+ // but BAO::checkAccess doesn't consistently check permissions for the "get" action.
+ if (is_a($action, '\Civi\Api4\Generic\DAOGetAction')) {
+ $granted = $granted && $action->addSelect('id')->addWhere('id', '=', $record['id'])->execute()->count();
+ }
+ else {
+ $baoName = self::getBAOFromApiName($entityName);
+ // If entity has a BAO, run the BAO::checkAccess function, which will call the hook
+ if ($baoName && strpos($baoName, '_BAO_')) {
+ $baoName::checkAccess($actionName, $record, NULL, $granted);
+ }
+ // Otherwise, call the hook directly
+ else {
+ \CRM_Utils_Hook::checkAccess($entityName, $actionName, $record, NULL, $granted);
+ }
+ }
+ return $granted;
+ }
+
}
public function testEmptyAndNullOperators() {
$records = [
- [],
+ ['id' => NULL],
['color' => '', 'weight' => 0],
['color' => 'yellow', 'weight' => 100000000000],
];