From d343069c86cfe3bd92559f1dce6f5b4cf421ee32 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Fri, 8 Jan 2016 14:46:23 -0500 Subject: [PATCH] CRM-17795 - Add aclWhere clause to api joins --- CRM/Contact/BAO/Contact.php | 7 +++ CRM/Contact/BAO/Contact/Permission.php | 4 +- CRM/Core/BAO/UFMatch.php | 8 +++ CRM/Core/DAO.php | 16 ++++++ Civi/API/SelectQuery.php | 77 +++++++++++++++++++------- 5 files changed, 89 insertions(+), 23 deletions(-) diff --git a/CRM/Contact/BAO/Contact.php b/CRM/Contact/BAO/Contact.php index c41b27b08d..3bceb0df77 100644 --- a/CRM/Contact/BAO/Contact.php +++ b/CRM/Contact/BAO/Contact.php @@ -3425,4 +3425,11 @@ LEFT JOIN civicrm_address add2 ON ( add1.master_id = add2.id ) return TRUE; } + /** + * @inheritDoc + */ + public function apiWhereClause($tableAlias) { + return CRM_Contact_BAO_Contact_Permission::cacheSubquery("`$tableAlias`.id"); + } + } diff --git a/CRM/Contact/BAO/Contact/Permission.php b/CRM/Contact/BAO/Contact/Permission.php index 761b657252..5d63c51a56 100644 --- a/CRM/Contact/BAO/Contact/Permission.php +++ b/CRM/Contact/BAO/Contact/Permission.php @@ -229,7 +229,7 @@ AND $operationClause LIMIT 1"; * * @param string $contactIdField * Full "table_name.field_name" for the field containing a contact id - * @return string + * @return string|NULL */ public static function cacheSubquery($contactIdField) { $clauses = array(); @@ -241,7 +241,7 @@ AND $operationClause LIMIT 1"; if (!CRM_Core_Permission::check('access deleted contacts')) { $clauses[] = "$contactIdField NOT IN (SELECT id FROM civicrm_contact WHERE is_deleted = 1)"; } - return $clauses ? implode(' AND ', $clauses) : '1'; + return $clauses ? implode(' AND ', $clauses) : NULL; } /** diff --git a/CRM/Core/BAO/UFMatch.php b/CRM/Core/BAO/UFMatch.php index 84bacc1c4c..03b018838d 100644 --- a/CRM/Core/BAO/UFMatch.php +++ b/CRM/Core/BAO/UFMatch.php @@ -633,4 +633,12 @@ AND domain_id = %4 return $ufValues[$ufID]; } + /** + * @inheritDoc + */ + public function apiWhereClause($tableAlias) { + // Prevent default behavior of joining ACLs onto the contact_id field + return NULL; + } + } diff --git a/CRM/Core/DAO.php b/CRM/Core/DAO.php index a130e4c8f2..0759ba95f7 100644 --- a/CRM/Core/DAO.php +++ b/CRM/Core/DAO.php @@ -2446,9 +2446,25 @@ SELECT contact_id } /** + * @deprecated * @param array $params */ public function setApiFilter(&$params) { } + /** + * Generates a clause suitable for adding to WHERE or ON when doing an api.get for this entity + * + * @param string $tableAlias + * @return null|string + */ + public function apiWhereClause($tableAlias) { + $fields = $this->fields(); + $cidField = CRM_Utils_Array::value('contact_id', $fields); + if (CRM_Utils_Array::value('FKClassName', $cidField) == 'CRM_Contact_DAO_Contact') { + return CRM_Contact_BAO_Contact_Permission::cacheSubquery("`$tableAlias`.contact_id"); + } + return NULL; + } + } diff --git a/Civi/API/SelectQuery.php b/Civi/API/SelectQuery.php index cbc1cb0bb3..d6ad183b7f 100644 --- a/Civi/API/SelectQuery.php +++ b/Civi/API/SelectQuery.php @@ -41,6 +41,10 @@ namespace Civi\API; */ class SelectQuery { + /** + * @var \CRM_Core_DAO + */ + protected $bao; /** * @var string */ @@ -69,33 +73,44 @@ class SelectQuery { * @var array */ protected $entityFieldNames; + /** + * @var string|bool + */ + protected $checkPermissions; /** - * @param string $dao_name - * Name of DAO + * @param string $bao_name + * Name of BAO * @param array $params * As passed into api get function. * @param bool $isFillUniqueFields * Do we need to ensure unique fields continue to be populated for this api? (backward compatibility). */ - public function __construct($dao_name, $params, $isFillUniqueFields) { - /* @var \CRM_Core_DAO $dao */ - $dao = new $dao_name(); - $this->entity = _civicrm_api_get_entity_name_from_dao($dao); + public function __construct($bao_name, $params, $isFillUniqueFields) { + $this->bao = new $bao_name(); + $this->entity = _civicrm_api_get_entity_name_from_dao($this->bao); $this->params = $params; $this->isFillUniqueFields = $isFillUniqueFields; + $this->checkPermissions = \CRM_Utils_Array::value('check_permissions', $this->params, FALSE); $this->options = _civicrm_api3_get_options_from_params($this->params); - $this->entityFieldNames = _civicrm_api3_field_names(_civicrm_api3_build_fields_array($dao)); + $this->entityFieldNames = _civicrm_api3_field_names(_civicrm_api3_build_fields_array($this->bao)); // Call this function directly instead of using the api wrapper to force unique field names off require_once 'api/v3/Generic.php'; $apiSpec = \civicrm_api3_generic_getfields(array('entity' => $this->entity, 'version' => 3, 'params' => array('action' => 'get')), FALSE); $this->apiFieldSpec = $apiSpec['values']; - $this->query = \CRM_Utils_SQL_Select::from($dao->tableName() . " a"); - $dao->free(); + $this->query = \CRM_Utils_SQL_Select::from($this->bao->tableName() . " a"); } + /** + * Build & execute the query and return results array + * + * @return array + * @throws \API_Exception + * @throws \CRM_Core_Exception + * @throws \Exception + */ public function run() { // $select_fields maps column names to the field names of the result values. $select_fields = $custom_fields = array(); @@ -273,7 +288,8 @@ class SelectQuery { } // ACLs - $this->addAclClause(); + $this->query->where($this->getAclClause('a')); + $this->bao->free(); $result_entities = array(); $result_dao = \CRM_Core_DAO::executeQuery($this->query->toSQL()); @@ -319,9 +335,12 @@ class SelectQuery { * * Adds one or more joins to the query to make this field available for use in a clause. * + * Enforces permissions at the api level and by appending the acl clause for that entity to the join. + * * @param $fkFieldName * @return array|null * Returns the table and field name for adding this field to a SELECT or WHERE clause + * @throws \API_Exception */ private function addFkField($fkFieldName) { $stack = explode('.', $fkFieldName); @@ -358,9 +377,16 @@ class SelectQuery { return NULL; } $fkTable = \CRM_Core_DAO_AllCoreTables::getTableForClass($fkField['FKClassName']); - $tableAlias = "{$fk}_to_$fkTable"; + $tableAlias = implode('_to_', array_slice($stack, 0, $depth)) . "_to_$fkTable"; + $joinClause = "LEFT JOIN $fkTable $tableAlias ON $prev.$fk = $tableAlias.id"; - $this->query->join($tableAlias, "LEFT JOIN $fkTable $tableAlias ON $prev.$fk = $tableAlias.id"); + // Add acl condition + $joinCondition = $this->getAclClause($tableAlias, $fkField['FKClassName']); + if ($joinCondition !== NULL) { + $joinClause .= " AND $joinCondition"; + } + + $this->query->join($tableAlias, $joinClause); if (strpos($fieldName, 'custom_') === 0) { list($tableAlias, $fieldName) = $this->addCustomField($fieldInfo, $tableAlias); @@ -427,14 +453,14 @@ class SelectQuery { * @return bool */ private function checkPermissionToJoin($entity, $fieldStack) { - if (empty($this->params['check_permissions'])) { + if (!$this->checkPermissions) { return TRUE; } // Build an array of params that relate to the joined entity $params = array( 'version' => 3, 'return' => array(), - 'check_permissions' => \CRM_Utils_Array::value('check_permissions', $this->params, FALSE), + 'check_permissions' => $this->checkPermissions, ); $prefix = implode('.', $fieldStack) . '.'; $len = strlen($prefix); @@ -453,15 +479,24 @@ class SelectQuery { } /** - * If this entity has a `contact_id` field, add appropriate acl clause + * Get acl clause for an entity + * + * @param string $tableAlias + * @param \CRM_Core_DAO $bao + * @return null|string */ - private function addAclClause() { - if (in_array('contact_id', $this->entityFieldNames)) { - $clause = \CRM_Contact_BAO_Contact_Permission::cacheSubquery('a.contact_id'); - if ($clause !== '1') { - $this->query->where($clause); - } + private function getAclClause($tableAlias, $bao = NULL) { + if (!$this->checkPermissions) { + return NULL; + } + if (!$bao) { + $bao = $this->bao; + } + else { + $baoName = str_replace('_DAO_', '_BAO_', $bao); + $bao = new $baoName(); } + return $bao->apiWhereClause($tableAlias); } } -- 2.25.1