Skip to content

Commit

Permalink
Convert hook_civicrm_checkAccess to civi.api4.authorizeRecord
Browse files Browse the repository at this point in the history
  • Loading branch information
totten committed Jun 7, 2021
1 parent 6270355 commit eccc78b
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 87 deletions.
33 changes: 0 additions & 33 deletions CRM/Core/DAO.php
Original file line number Diff line number Diff line change
Expand Up @@ -3040,39 +3040,6 @@ public static function getSelectWhereClause($tableAlias = NULL) {
return $clauses;
}

/**
* Check whether action can be performed on a given record.
*
* Dispatches to internal BAO function ('static::_checkAccess())` and `hook_civicrm_checkAccess`.
*
* @param string $entityName
* Ex: 'Contact' or 'Custom_Foobar'
* @param string $action
* APIv4 action name.
* Ex: 'create', 'get', 'delete'
* @param array $record
* All (known/loaded) values of individual record being accessed.
* The record should provide an 'id' but may otherwise be incomplete; guard accordingly.
* @param int|null $userID
* Contact ID of the active user (whose access we must check). NULL for anonymous.
* @return bool
* TRUE if granted. FALSE if prohibited. NULL if indeterminate.
*/
public static function checkAccess(string $entityName, string $action, array $record, $userID): ?bool {
// Ensure this function was either called on a BAO class or a DAO that has no BAO
if (!$entityName ||
(!strpos(static::class, '_BAO_') && CRM_Core_DAO_AllCoreTables::getBAOClassName(static::class) !== static::class)
) {
throw new CRM_Core_Exception('Function checkAccess must be called on a BAO class');
}
if (method_exists(static::class, '_checkAccess')) {
return static::_checkAccess($entityName, $action, $record, $userID);
}
else {
return TRUE;
}
}

/**
* ensure database name is 'safe', i.e. only contains word characters (includes underscores)
* and dashes, and contains at least one [a-z] case insensitive.
Expand Down
26 changes: 0 additions & 26 deletions CRM/Utils/Hook.php
Original file line number Diff line number Diff line change
Expand Up @@ -2100,32 +2100,6 @@ public static function permission_check($permission, &$granted, $contactId) {
);
}

/**
* Check whether the given contact has access to perform the given action.
*
* If the contact cannot perform the action the
*
* @param string $entity
* APIv4 entity name.
* Ex: 'Contact', 'Email', 'Event'
* @param string $action
* APIv4 action name.
* Ex: 'create', 'get', 'delete'
* @param array $record
* All (known/loaded) values of individual record being accessed.
* The record should provide an 'id' but may otherwise be incomplete; guard accordingly.
* @param int|null $contactID
* Contact ID of the active user (whose access we must check).
* @param bool|null $granted
* TRUE if granted. FALSE if prohibited. NULL if indeterminate.
*/
public static function checkAccess(string $entity, string $action, array $record, ?int $contactID, ?bool &$granted) {
self::singleton()->invoke(['entity', 'action', 'record', 'contactID', 'granted'], $entity, $action, $record,
$contactID, $granted, self::$_nullObject,
'civicrm_checkAccess'
);
}

/**
* Rotate the cryptographic key used in the database.
*
Expand Down
91 changes: 91 additions & 0 deletions Civi/Api4/Event/AuthorizeRecordEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?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\Event;

use Civi\API\Event\AuthorizedTrait;
use Civi\API\Event\RequestTrait;
use Civi\Core\Event\GenericHookEvent;

/**
* Determine if the a user has access to a given record.
*
* Event name: 'civi.api4.authorizeRecord'
*/
class AuthorizeRecordEvent extends GenericHookEvent {

use RequestTrait;
use AuthorizedTrait;

/**
* All (known/loaded) values of individual record being accessed.
* The record should provide an 'id' but may otherwise be incomplete; guard accordingly.
*
* @var array
*/
private $record;

/**
* Contact ID of the active/target user (whose access we must check).
* NULL for anonymous.
*
* @var int|null
*/
private $userID;

/**
* CheckAccessEvent constructor.
*
* @param \Civi\Api4\Generic\AbstractAction $apiRequest
* @param array $record
* All (known/loaded) values of individual record being accessed.
* The record should provide an 'id' but may otherwise be incomplete; guard accordingly.
* @param int|null $userID
* Contact ID of the active/target user (whose access we must check).
* NULL for anonymous.
*/
public function __construct($apiRequest, array $record, ?int $userID) {
$this->setApiRequest($apiRequest);
$this->record = $record;
$this->userID = $userID;
}

/**
* @inheritDoc
*/
public function getHookValues() {
return [$this->getApiRequest(), $this->record, &$this->authorized];
}

/**
* @return array
*/
public function getRecord(): array {
return $this->record;
}

/**
* @return int|null
* Contact ID of the active/target user (whose access we must check).
* NULL for anonymous.
*/
public function getUserID(): ?int {
return $this->userID;
}

}
20 changes: 14 additions & 6 deletions Civi/Api4/Utils/CoreUtil.php
Original file line number Diff line number Diff line change
Expand Up @@ -173,13 +173,21 @@ public static function checkAccessRecord(\Civi\Api4\Generic\AbstractAction $apiR
return (bool) $apiRequest->addSelect('id')->addWhere('id', '=', $record['id'])->execute()->count();
}

$granted = NULL;
\CRM_Utils_Hook::checkAccess($apiRequest->getEntityName(), $apiRequest->getActionName(), $record, $userID, $granted);
$baoName = self::getBAOFromApiName($apiRequest->getEntityName());
if ($granted === NULL && $baoName) {
$granted = $baoName::checkAccess($apiRequest->getEntityName(), $apiRequest->getActionName(), $record, $userID);
$event = new \Civi\Api4\Event\AuthorizeRecordEvent($apiRequest, $record, $userID);
\Civi::dispatcher()->dispatch('civi.api4.authorizeRecord', $event);

// Note: $bao::_checkAccess() is a quasi-listener. TODO: Convert to straight-up listener.
if ($event->isAuthorized() === NULL) {
$baoName = self::getBAOFromApiName($apiRequest->getEntityName());
if ($baoName && method_exists($baoName, '_checkAccess')) {
$authorized = $baoName::_checkAccess($event->getEntityName(), $event->getActionName(), $event->getRecord(), $event->getUserID());
$event->setAuthorized($authorized);
}
else {
$event->setAuthorized(TRUE);
}
}
return $granted;
return $event->isAuthorized();
}

/**
Expand Down
1 change: 1 addition & 0 deletions Civi/Core/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ public function createEventDispatcher() {
};

$dispatcher->addListener('civi.api4.validate', $aliasMethodEvent('civi.api4.validate', 'getEntityName'), 100);
$dispatcher->addListener('civi.api4.authorizeRecord', $aliasMethodEvent('civi.api4.authorizeRecord', 'getEntityName'), 100);

$dispatcher->addListener('civi.core.install', ['\Civi\Core\InstallationCanary', 'check']);
$dispatcher->addListener('civi.core.install', ['\Civi\Core\DatabaseInitializer', 'initialize']);
Expand Down
27 changes: 15 additions & 12 deletions ext/financialacls/financialacls.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@
*/
function financialacls_civicrm_config(&$config) {
_financialacls_civix_civicrm_config($config);

static $configured = FALSE;
if ($configured) {
return;
}
$configured = TRUE;

Civi::dispatcher()->addListener('civi.api4.authorizeRecord', '_financialacls_civi_api4_authorizeRecord');
}

/**
Expand Down Expand Up @@ -287,27 +295,22 @@ function financialacls_civicrm_permission(&$permissions) {
}

/**
* @param string $entity
* @param string $action
* @param array $record
* @param int|null $contactID
* @param bool|null $granted
*
* @param \Civi\Api4\Event\AuthorizeRecordEvent $e
* @throws \CRM_Core_Exception
*/
function financialacls_civicrm_checkAccess(string $entity, string $action, array $record, ?int $contactID, ?bool &$granted) {
function _financialacls_civi_api4_authorizeRecord(\Civi\Api4\Event\AuthorizeRecordEvent $e) {
if (!financialacls_is_acl_limiting_enabled()) {
return;
}
if ($action === 'delete' && $entity === 'Contribution') {
$contributionID = $record['id'];
if ($e->getActionName() === 'delete' && $e->getEntityName() === 'Contribution') {
$contributionID = $e->getRecord()['id'];
// First check contribution financial type
$financialType = CRM_Core_PseudoConstant::getName('CRM_Contribute_DAO_Contribution', 'financial_type_id', CRM_Core_DAO::getFieldValue('CRM_Contribute_DAO_Contribution', $contributionID, 'financial_type_id'));
// Now check permissioned line items & permissioned contribution
if (!CRM_Core_Permission::check('delete contributions of type ' . $financialType, $contactID) ||
!CRM_Financial_BAO_FinancialType::checkPermissionedLineItems($contributionID, 'delete', FALSE, $contactID)
if (!CRM_Core_Permission::check('delete contributions of type ' . $financialType, $e->getUserID()) ||
!CRM_Financial_BAO_FinancialType::checkPermissionedLineItems($contributionID, 'delete', FALSE, $e->getUserID())
) {
$granted = FALSE;
$e->setAuthorized(FALSE);
}
}
}
Expand Down
19 changes: 9 additions & 10 deletions tests/phpunit/api/v4/Traits/CheckAccessTrait.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<?php
namespace api\v4\Traits;

use Civi\Api4\Event\AuthorizeRecordEvent;

/**
* Define an implementation of `hook_civicrm_checkAccess` in which access-control decisions are
* Define an implementation of `civi.api4.authorizeRecord` in which access-control decisions are
* based on a predefined list. For example:
*
* $this->setCheckAccessGrants(['Contact::create' => TRUE]);
Expand All @@ -28,17 +30,14 @@ trait CheckAccessTrait {
protected $checkAccessCounts = [];

/**
* @param string $entity
* @param string $action
* @param array $record
* @param int|null $contactID
* @param bool $granted
* @see \CRM_Utils_Hook::checkAccess()
* Listen to 'civi.api4.authorizeRecord'. Override decisions with specified grants.
*
* @param \Civi\Api4\Event\AuthorizeRecordEvent $e
*/
public function hook_civicrm_checkAccess(string $entity, string $action, array $record, ?int $contactID, ?bool &$granted) {
$key = "{$entity}::{$action}";
public function on_civi_api4_authorizeRecord(AuthorizeRecordEvent $e): void {
$key = $e->getEntityName() . '::' . $e->getActionName();
if (isset($this->checkAccessGrants[$key])) {
$granted = $this->checkAccessGrants[$key];
$e->setAuthorized($this->checkAccessGrants[$key]);
$this->checkAccessCounts[$key]++;
}
}
Expand Down

0 comments on commit eccc78b

Please sign in to comment.