376 lines
13 KiB
PHP
376 lines
13 KiB
PHP
|
<?php
|
||
|
/*
|
||
|
+--------------------------------------------------------------------+
|
||
|
| CiviCRM version 4.7 |
|
||
|
+--------------------------------------------------------------------+
|
||
|
| Copyright CiviCRM LLC (c) 2004-2017 |
|
||
|
+--------------------------------------------------------------------+
|
||
|
| This file is a part of CiviCRM. |
|
||
|
| |
|
||
|
| CiviCRM is free software; you can copy, modify, and distribute it |
|
||
|
| under the terms of the GNU Affero General Public License |
|
||
|
| Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
|
||
|
| |
|
||
|
| CiviCRM is distributed in the hope that it will be useful, but |
|
||
|
| WITHOUT ANY WARRANTY; without even the implied warranty of |
|
||
|
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
|
||
|
| See the GNU Affero General Public License for more details. |
|
||
|
| |
|
||
|
| You should have received a copy of the GNU Affero General Public |
|
||
|
| License and the CiviCRM Licensing Exception along |
|
||
|
| with this program; if not, contact CiviCRM LLC |
|
||
|
| at info[AT]civicrm[DOT]org. If you have questions about the |
|
||
|
| GNU Affero General Public License or the licensing of CiviCRM, |
|
||
|
| see the CiviCRM license FAQ at http://civicrm.org/licensing |
|
||
|
+--------------------------------------------------------------------+
|
||
|
*/
|
||
|
|
||
|
namespace Civi\API\Subscriber;
|
||
|
|
||
|
use Civi\API\Events;
|
||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||
|
|
||
|
/**
|
||
|
* Given an entity which dynamically attaches itself to another entity,
|
||
|
* determine if one has permission to the other entity.
|
||
|
*
|
||
|
* Example: Suppose one tries to manipulate a File which is attached to a
|
||
|
* Mailing. DynamicFKAuthorization will enforce permissions on the File by
|
||
|
* imitating the permissions of the Mailing.
|
||
|
*
|
||
|
* Note: This enforces a constraint: all matching API calls must define
|
||
|
* "id" (e.g. for the file) or "entity_table+entity_id" or
|
||
|
* "field_name+entity_id".
|
||
|
*
|
||
|
* Note: The permission guard does not exactly authorize the request, but it
|
||
|
* may veto authorization.
|
||
|
*/
|
||
|
class DynamicFKAuthorization implements EventSubscriberInterface {
|
||
|
|
||
|
/**
|
||
|
* @return array
|
||
|
*/
|
||
|
public static function getSubscribedEvents() {
|
||
|
return array(
|
||
|
Events::AUTHORIZE => array(
|
||
|
array('onApiAuthorize', Events::W_EARLY),
|
||
|
),
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @var \Civi\API\Kernel
|
||
|
*
|
||
|
* Treat as private. Marked public due to PHP 5.3-compatibility issues.
|
||
|
*/
|
||
|
public $kernel;
|
||
|
|
||
|
/**
|
||
|
* @var string, the entity for which we want to manage permissions
|
||
|
*/
|
||
|
protected $entityName;
|
||
|
|
||
|
/**
|
||
|
* @var array <string> the actions for which we want to manage permissions
|
||
|
*/
|
||
|
protected $actions;
|
||
|
|
||
|
/**
|
||
|
* @var string, SQL. Given a file ID, determine the entity+table it's attached to.
|
||
|
*
|
||
|
* ex: "SELECT if(cf.id,1,0) as is_valid, cef.entity_table, cef.entity_id
|
||
|
* FROM civicrm_file cf
|
||
|
* INNER JOIN civicrm_entity_file cef ON cf.id = cef.file_id
|
||
|
* WHERE cf.id = %1"
|
||
|
*
|
||
|
* Note: %1 is a parameter
|
||
|
* Note: There are three parameters
|
||
|
* - is_valid: "1" if %1 identifies an actual record; otherwise "0"
|
||
|
* - entity_table: NULL or the name of a related table
|
||
|
* - entity_id: NULL or the ID of a row in the related table
|
||
|
*/
|
||
|
protected $lookupDelegateSql;
|
||
|
|
||
|
/**
|
||
|
* @var string, SQL. Get a list of (field_name, table_name, extends) tuples.
|
||
|
*
|
||
|
* For example, one tuple might be ("custom_123", "civicrm_value_mygroup_4",
|
||
|
* "Activity").
|
||
|
*/
|
||
|
protected $lookupCustomFieldSql;
|
||
|
|
||
|
/**
|
||
|
* @var array
|
||
|
*
|
||
|
* Each item is an array(field_name => $, table_name => $, extends => $)
|
||
|
*/
|
||
|
protected $lookupCustomFieldCache;
|
||
|
|
||
|
/**
|
||
|
* @var array list of related tables for which FKs are allowed
|
||
|
*/
|
||
|
protected $allowedDelegates;
|
||
|
|
||
|
/**
|
||
|
* @param \Civi\API\Kernel $kernel
|
||
|
* The API kernel.
|
||
|
* @param string $entityName
|
||
|
* The entity for which we want to manage permissions (e.g. "File" or
|
||
|
* "Note").
|
||
|
* @param array $actions
|
||
|
* The actions for which we want to manage permissions (e.g. "create",
|
||
|
* "get", "delete").
|
||
|
* @param string $lookupDelegateSql
|
||
|
* See docblock in DynamicFKAuthorization::$lookupDelegateSql.
|
||
|
* @param string $lookupCustomFieldSql
|
||
|
* See docblock in DynamicFKAuthorization::$lookupCustomFieldSql.
|
||
|
* @param array|NULL $allowedDelegates
|
||
|
* e.g. "civicrm_mailing","civicrm_activity"; NULL to allow any.
|
||
|
*/
|
||
|
public function __construct($kernel, $entityName, $actions, $lookupDelegateSql, $lookupCustomFieldSql, $allowedDelegates = NULL) {
|
||
|
$this->kernel = $kernel;
|
||
|
$this->entityName = \CRM_Utils_String::convertStringToCamel($entityName);
|
||
|
$this->actions = $actions;
|
||
|
$this->lookupDelegateSql = $lookupDelegateSql;
|
||
|
$this->lookupCustomFieldSql = $lookupCustomFieldSql;
|
||
|
$this->allowedDelegates = $allowedDelegates;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param \Civi\API\Event\AuthorizeEvent $event
|
||
|
* API authorization event.
|
||
|
* @throws \API_Exception
|
||
|
* @throws \Civi\API\Exception\UnauthorizedException
|
||
|
*/
|
||
|
public function onApiAuthorize(\Civi\API\Event\AuthorizeEvent $event) {
|
||
|
$apiRequest = $event->getApiRequest();
|
||
|
if ($apiRequest['version'] == 3 && \CRM_Utils_String::convertStringToCamel($apiRequest['entity']) == $this->entityName && in_array(strtolower($apiRequest['action']), $this->actions)) {
|
||
|
if (isset($apiRequest['params']['field_name'])) {
|
||
|
$fldIdx = \CRM_Utils_Array::index(array('field_name'), $this->getCustomFields());
|
||
|
if (empty($fldIdx[$apiRequest['params']['field_name']])) {
|
||
|
throw new \Exception("Failed to map custom field to entity table");
|
||
|
}
|
||
|
$apiRequest['params']['entity_table'] = $fldIdx[$apiRequest['params']['field_name']]['entity_table'];
|
||
|
unset($apiRequest['params']['field_name']);
|
||
|
}
|
||
|
|
||
|
if (/*!$isTrusted */
|
||
|
empty($apiRequest['params']['id']) && empty($apiRequest['params']['entity_table'])
|
||
|
) {
|
||
|
throw new \API_Exception("Mandatory key(s) missing from params array: 'id' or 'entity_table'");
|
||
|
}
|
||
|
|
||
|
if (isset($apiRequest['params']['id'])) {
|
||
|
list($isValidId, $entityTable, $entityId) = $this->getDelegate($apiRequest['params']['id']);
|
||
|
if ($isValidId && $entityTable && $entityId) {
|
||
|
$this->authorizeDelegate($apiRequest['action'], $entityTable, $entityId, $apiRequest);
|
||
|
$this->preventReassignment($apiRequest['params']['id'], $entityTable, $entityId, $apiRequest);
|
||
|
return;
|
||
|
}
|
||
|
elseif ($isValidId) {
|
||
|
throw new \API_Exception("Failed to match record to related entity");
|
||
|
}
|
||
|
elseif (!$isValidId && strtolower($apiRequest['action']) == 'get') {
|
||
|
// The matches will be an empty set; doesn't make a difference if we
|
||
|
// reject or accept.
|
||
|
// To pass SyntaxConformanceTest, we won't veto "get" on empty-set.
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (isset($apiRequest['params']['entity_table'])) {
|
||
|
$this->authorizeDelegate(
|
||
|
$apiRequest['action'],
|
||
|
$apiRequest['params']['entity_table'],
|
||
|
\CRM_Utils_Array::value('entity_id', $apiRequest['params'], NULL),
|
||
|
$apiRequest
|
||
|
);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
throw new \API_Exception("Failed to run permission check");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param string $action
|
||
|
* The API action (e.g. "create").
|
||
|
* @param string $entityTable
|
||
|
* The target entity table (e.g. "civicrm_mailing").
|
||
|
* @param int|NULL $entityId
|
||
|
* The target entity ID.
|
||
|
* @param array $apiRequest
|
||
|
* The full API request.
|
||
|
* @throws \Exception
|
||
|
* @throws \API_Exception
|
||
|
* @throws \Civi\API\Exception\UnauthorizedException
|
||
|
*/
|
||
|
public function authorizeDelegate($action, $entityTable, $entityId, $apiRequest) {
|
||
|
$entity = $this->getDelegatedEntityName($entityTable);
|
||
|
if (!$entity) {
|
||
|
throw new \API_Exception("Failed to run permission check: Unrecognized target entity table ($entityTable)");
|
||
|
}
|
||
|
if (!$entityId) {
|
||
|
throw new \Civi\API\Exception\UnauthorizedException("Authorization failed on ($entity): Missing entity_id");
|
||
|
}
|
||
|
|
||
|
if ($this->isTrusted($apiRequest)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @var \Exception $exception
|
||
|
*/
|
||
|
$exception = NULL;
|
||
|
$self = $this;
|
||
|
\CRM_Core_Transaction::create(TRUE)->run(function($tx) use ($entity, $action, $entityId, &$exception, $self) {
|
||
|
$tx->rollback(); // Just to be safe.
|
||
|
|
||
|
$params = array(
|
||
|
'version' => 3,
|
||
|
'check_permissions' => 1,
|
||
|
'id' => $entityId,
|
||
|
);
|
||
|
|
||
|
$result = $self->kernel->run($entity, $self->getDelegatedAction($action), $params);
|
||
|
if ($result['is_error'] || empty($result['values'])) {
|
||
|
$exception = new \Civi\API\Exception\UnauthorizedException("Authorization failed on ($entity,$entityId)", array(
|
||
|
'cause' => $result,
|
||
|
));
|
||
|
}
|
||
|
});
|
||
|
|
||
|
if ($exception) {
|
||
|
throw $exception;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* If the request attempts to change the entity_table/entity_id of an
|
||
|
* existing record, then generate an error.
|
||
|
*
|
||
|
* @param int $fileId
|
||
|
* The main record being changed.
|
||
|
* @param string $entityTable
|
||
|
* The saved FK.
|
||
|
* @param int $entityId
|
||
|
* The saved FK.
|
||
|
* @param array $apiRequest
|
||
|
* The full API request.
|
||
|
* @throws \API_Exception
|
||
|
*/
|
||
|
public function preventReassignment($fileId, $entityTable, $entityId, $apiRequest) {
|
||
|
if (strtolower($apiRequest['action']) == 'create' && $fileId && !$this->isTrusted($apiRequest)) {
|
||
|
// TODO: no change in field_name?
|
||
|
if (isset($apiRequest['params']['entity_table']) && $entityTable != $apiRequest['params']['entity_table']) {
|
||
|
throw new \API_Exception("Cannot modify entity_table");
|
||
|
}
|
||
|
if (isset($apiRequest['params']['entity_id']) && $entityId != $apiRequest['params']['entity_id']) {
|
||
|
throw new \API_Exception("Cannot modify entity_id");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param string $entityTable
|
||
|
* The target entity table (e.g. "civicrm_mailing" or "civicrm_activity").
|
||
|
* @return string|NULL
|
||
|
* The target entity name (e.g. "Mailing" or "Activity").
|
||
|
*/
|
||
|
public function getDelegatedEntityName($entityTable) {
|
||
|
if ($this->allowedDelegates === NULL || in_array($entityTable, $this->allowedDelegates)) {
|
||
|
$className = \CRM_Core_DAO_AllCoreTables::getClassForTable($entityTable);
|
||
|
if ($className) {
|
||
|
$entityName = \CRM_Core_DAO_AllCoreTables::getBriefName($className);
|
||
|
if ($entityName) {
|
||
|
return $entityName;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param string $action
|
||
|
* API action name -- e.g. "create" ("When running *create* on a file...").
|
||
|
* @return string
|
||
|
* e.g. "create" ("Check for *create* permission on the mailing to which
|
||
|
* it is attached.")
|
||
|
*/
|
||
|
public function getDelegatedAction($action) {
|
||
|
switch ($action) {
|
||
|
case 'get':
|
||
|
// reading attachments requires reading the other entity
|
||
|
return 'get';
|
||
|
|
||
|
case 'create':
|
||
|
case 'delete':
|
||
|
// creating/updating/deleting an attachment requires editing
|
||
|
// the other entity
|
||
|
return 'create';
|
||
|
|
||
|
default:
|
||
|
return $action;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param int $id
|
||
|
* e.g. file ID.
|
||
|
* @return array
|
||
|
* (0 => bool $isValid, 1 => string $entityTable, 2 => int $entityId)
|
||
|
* @throws \Exception
|
||
|
*/
|
||
|
public function getDelegate($id) {
|
||
|
$query = \CRM_Core_DAO::executeQuery($this->lookupDelegateSql, array(
|
||
|
1 => array($id, 'Positive'),
|
||
|
));
|
||
|
if ($query->fetch()) {
|
||
|
if (!preg_match('/^civicrm_value_/', $query->entity_table)) {
|
||
|
// A normal attachment directly on its entity.
|
||
|
return array($query->is_valid, $query->entity_table, $query->entity_id);
|
||
|
}
|
||
|
|
||
|
// Ex: Translate custom-field table ("civicrm_value_foo_4") to
|
||
|
// entity table ("civicrm_activity").
|
||
|
$tblIdx = \CRM_Utils_Array::index(array('table_name'), $this->getCustomFields());
|
||
|
if (isset($tblIdx[$query->entity_table])) {
|
||
|
return array($query->is_valid, $tblIdx[$query->entity_table]['entity_table'], $query->entity_id);
|
||
|
}
|
||
|
throw new \Exception('Failed to lookup entity table for custom field.');
|
||
|
}
|
||
|
else {
|
||
|
return array(FALSE, NULL, NULL);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param array $apiRequest
|
||
|
* The full API request.
|
||
|
* @return bool
|
||
|
*/
|
||
|
public function isTrusted($apiRequest) {
|
||
|
// isn't this redundant?
|
||
|
return empty($apiRequest['params']['check_permissions']) or $apiRequest['params']['check_permissions'] == FALSE;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return array
|
||
|
* Each item has keys 'field_name', 'table_name', 'extends', 'entity_table'
|
||
|
*/
|
||
|
public function getCustomFields() {
|
||
|
$query = \CRM_Core_DAO::executeQuery($this->lookupCustomFieldSql);
|
||
|
$rows = array();
|
||
|
while ($query->fetch()) {
|
||
|
$rows[] = array(
|
||
|
'field_name' => $query->field_name,
|
||
|
'table_name' => $query->table_name,
|
||
|
'extends' => $query->extends,
|
||
|
'entity_table' => \CRM_Core_BAO_CustomGroup::getTableNameByEntityName($query->extends),
|
||
|
);
|
||
|
}
|
||
|
return $rows;
|
||
|
}
|
||
|
|
||
|
}
|