APIv4 - Fix gatekeeper checks across joins
authorColeman Watts <coleman@civicrm.org>
Tue, 13 Apr 2021 12:17:01 +0000 (08:17 -0400)
committerSeamus Lee <seamuslee001@gmail.com>
Mon, 19 Apr 2021 23:21:21 +0000 (09:21 +1000)
Civi/Api4/Query/Api4SelectQuery.php
Civi/Api4/Service/Schema/Joiner.php

index 8e42ed2b25e94fb349051d35e1876f00d3b0593d..83033154f827a8ac1d86ef7769613ebf2399f2ab 100644 (file)
@@ -81,6 +81,11 @@ class Api4SelectQuery {
    */
   private $explicitJoins = [];
 
+  /**
+   * @var array
+   */
+  private $entityAccess = [];
+
   /**
    * @param \Civi\Api4\Generic\DAOGetAction $apiGet
    */
@@ -100,6 +105,8 @@ class Api4SelectQuery {
     $tableName = CoreUtil::getTableName($this->getEntity());
     $this->query = \CRM_Utils_SQL_Select::from($tableName . ' ' . self::MAIN_TABLE_ALIAS);
 
+    $this->entityAccess[$this->getEntity()] = TRUE;
+
     // Add ACLs first to avoid redundant subclauses
     $baoName = CoreUtil::getBAOFromApiName($this->getEntity());
     $this->query->where($this->getAclClause(self::MAIN_TABLE_ALIAS, $baoName));
@@ -579,6 +586,25 @@ class Api4SelectQuery {
     return $field;
   }
 
+  /**
+   * Check the "gatekeeper" permissions for performing "get" on a given entity.
+   *
+   * @param $entity
+   * @return bool
+   */
+  public function checkEntityAccess($entity) {
+    if (!$this->getCheckPermissions()) {
+      return TRUE;
+    }
+    if (!isset($this->entityAccess[$entity])) {
+      $this->entityAccess[$entity] = (bool) civicrm_api4($entity, 'getActions', [
+        'where' => [['name', '=', 'get']],
+        'select' => ['name'],
+      ])->first();
+    }
+    return $this->entityAccess[$entity];
+  }
+
   /**
    * Join onto other entities as specified by the api call.
    *
@@ -591,6 +617,10 @@ class Api4SelectQuery {
       $entity = array_shift($join);
       // Which might contain an alias. Split on the keyword "AS"
       list($entity, $alias) = array_pad(explode(' AS ', $entity), 2, NULL);
+      // Ensure permissions
+      if (!$this->checkEntityAccess($entity)) {
+        continue;
+      }
       // Ensure alias is a safe string, and supply default if not given
       $alias = $alias ? \CRM_Utils_String::munge($alias, '_', 256) : strtolower($entity);
       // First item in the array is a boolean indicating if the join is required (aka INNER or LEFT).
index a412d6571c2bcd947975c84f3dc0f141169ea4dd..ce92405683f640478139721b950218231c8a225e 100644 (file)
@@ -19,7 +19,9 @@
 
 namespace Civi\Api4\Service\Schema;
 
+use Civi\API\Exception\UnauthorizedException;
 use Civi\Api4\Query\Api4SelectQuery;
+use Civi\Api4\Utils\CoreUtil;
 
 class Joiner {
   /**
@@ -70,9 +72,14 @@ class Joiner {
     foreach ($fullPath as $link) {
       $target = $link->getTargetTable();
       $alias = $link->getAlias();
-      $bao = \CRM_Core_DAO_AllCoreTables::getBAOClassName(\CRM_Core_DAO_AllCoreTables::getClassForTable($target));
+      $joinEntity = CoreUtil::getApiNameFromTableName($target);
+
+      if ($joinEntity && !$query->checkEntityAccess($joinEntity)) {
+        throw new UnauthorizedException('Cannot join to ' . $joinEntity);
+      }
+
+      $bao = $joinEntity ? CoreUtil::getBAOFromApiName($joinEntity) : NULL;
       $conditions = $link->getConditionsForJoin($baseTableAlias);
-      // Custom fields do not have a bao, and currently do not have field-specific ACLs
       if ($bao) {
         $conditions = array_merge($conditions, $query->getAclClause($alias, $bao, $joinPath));
       }