From 29468114469beefe8f88946215d768d0483f9305 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Mon, 9 Feb 2015 09:51:28 -0800 Subject: [PATCH] Attachment API, DynamicFKAuthorization - Support for custom fields and contacts. This commit makes the entity_id a required field. This restricts search abilities but allows support for ACL'd entities (e.g. contacts). --- .../API/Subscriber/DynamicFKAuthorization.php | 98 +++++++++++++++++-- Civi/Core/Container.php | 10 +- .../Subscriber/DynamicFKAuthorizationTest.php | 6 +- 3 files changed, 102 insertions(+), 12 deletions(-) diff --git a/Civi/API/Subscriber/DynamicFKAuthorization.php b/Civi/API/Subscriber/DynamicFKAuthorization.php index 4f94fb4ac6..922d2d806d 100644 --- a/Civi/API/Subscriber/DynamicFKAuthorization.php +++ b/Civi/API/Subscriber/DynamicFKAuthorization.php @@ -39,7 +39,8 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; * imitating the permissions of the Mailing. * * Note: This enforces a constraint: all matching API calls must define - * either "id" (e.g. for the file) or "entity_table". + * "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. @@ -88,6 +89,21 @@ class DynamicFKAuthorization implements EventSubscriberInterface { */ 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 */ @@ -104,14 +120,17 @@ class DynamicFKAuthorization implements EventSubscriberInterface { * "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, $allowedDelegates = NULL) { + public function __construct($kernel, $entityName, $actions, $lookupDelegateSql, $lookupCustomFieldSql, $allowedDelegates = NULL) { $this->kernel = $kernel; $this->entityName = $entityName; $this->actions = $actions; $this->lookupDelegateSql = $lookupDelegateSql; + $this->lookupCustomFieldSql = $lookupCustomFieldSql; $this->allowedDelegates = $allowedDelegates; } @@ -124,6 +143,15 @@ class DynamicFKAuthorization implements EventSubscriberInterface { public function onApiAuthorize(\Civi\API\Event\AuthorizeEvent $event) { $apiRequest = $event->getApiRequest(); if ($apiRequest['version'] == 3 && strtolower($apiRequest['entity']) == strtolower($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']) ) { @@ -177,20 +205,39 @@ class DynamicFKAuthorization implements EventSubscriberInterface { 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 ($entityTable)"); + 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; } - $params = array('check_permissions' => 1); - if ($entityId) { - $params['id'] = $entityId; - } + /** + * @var \Exception $exception + */ + $exception = NULL; + \CRM_Core_Transaction::create(TRUE)->run(function($tx) use ($entity, $action, $entityId, &$exception) { + $tx->rollback(); // Just to be safe. - if (!$this->kernel->runAuthorize($entity, $this->getDelegatedAction($action), $params)) { - throw new \Civi\API\Exception\UnauthorizedException("Authorization failed on ($entity,$entityId)"); + $params = array( + 'version' => 3, + 'check_permissions' => 1, + 'id' => $entityId, + ); + + $result = $this->kernel->run($entity, $this->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; } } @@ -210,6 +257,7 @@ class DynamicFKAuthorization implements EventSubscriberInterface { */ 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"); } @@ -267,13 +315,25 @@ class DynamicFKAuthorization implements EventSubscriberInterface { * 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()) { - return array($query->is_valid, $query->entity_table, $query->entity_id); + 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); @@ -290,4 +350,22 @@ class DynamicFKAuthorization implements EventSubscriberInterface { 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; + } + } diff --git a/Civi/Core/Container.php b/Civi/Core/Container.php index 182737e712..d4aa0ebfe0 100644 --- a/Civi/Core/Container.php +++ b/Civi/Core/Container.php @@ -167,7 +167,15 @@ class Container { FROM civicrm_file cf LEFT JOIN civicrm_entity_file cef ON cf.id = cef.file_id WHERE cf.id = %1', - array('civicrm_activity', 'civicrm_mailing') + // Get a list of custom fields (field_name,table_name,extends) + 'SELECT concat("custom_",fld.id) as field_name, + grp.table_name as table_name, + grp.extends as extends + FROM civicrm_custom_field fld + INNER JOIN civicrm_custom_group grp ON fld.custom_group_id = grp.id + WHERE fld.data_type = "File" + ', + array('civicrm_activity', 'civicrm_mailing', 'civicrm_contact') )); $kernel->setApiProviders(array( diff --git a/tests/phpunit/Civi/API/Subscriber/DynamicFKAuthorizationTest.php b/tests/phpunit/Civi/API/Subscriber/DynamicFKAuthorizationTest.php index c6ca9ea4e2..fbbe190e05 100644 --- a/tests/phpunit/Civi/API/Subscriber/DynamicFKAuthorizationTest.php +++ b/tests/phpunit/Civi/API/Subscriber/DynamicFKAuthorizationTest.php @@ -95,6 +95,8 @@ class DynamicFKAuthorizationTest extends \CiviUnitTestCase { else null end as entity_id ", + // Get a list of custom fields (field_name,table_name,extends) + "select", array('fake_widget', 'fake_forbidden') )); } @@ -120,7 +122,6 @@ class DynamicFKAuthorizationTest extends \CiviUnitTestCase { 'create', array('entity_table' => 'fake_widget', 'entity_id' => self::WIDGET_ID), ); - $cases[] = array('FakeFile', 'get', array('entity_table' => 'fake_widget')); return $cases; } @@ -169,6 +170,9 @@ class DynamicFKAuthorizationTest extends \CiviUnitTestCase { $cases[] = array('FakeFile', 'create', array('entity_table' => 'unknown'), '/Unrecognized target entity/'); $cases[] = array('FakeFile', 'get', array('entity_table' => 'unknown'), '/Unrecognized target entity/'); + // We should be allowed to lookup files for fake_widgets, but we need an ID. + $cases[] = array('FakeFile', 'get', array('entity_table' => 'fake_widget'), '/Missing entity_id/'); + return $cases; } -- 2.25.1