Convert hook_civicrm_checkAccess to civi.api4.authorizeRecord
[civicrm-core.git] / Civi / Api4 / Utils / CoreUtil.php
1 <?php
2
3 /*
4 +--------------------------------------------------------------------+
5 | Copyright CiviCRM LLC. All rights reserved. |
6 | |
7 | This work is published under the GNU AGPLv3 license with some |
8 | permitted exceptions and without any warranty. For full license |
9 | and copyright information, see https://civicrm.org/licensing |
10 +--------------------------------------------------------------------+
11 */
12
13 /**
14 *
15 * @package CRM
16 * @copyright CiviCRM LLC https://civicrm.org/licensing
17 */
18
19
20 namespace Civi\Api4\Utils;
21
22 use Civi\API\Request;
23 use CRM_Core_DAO_AllCoreTables as AllCoreTables;
24
25 class CoreUtil {
26
27 /**
28 * @param $entityName
29 *
30 * @return \CRM_Core_DAO|string
31 * The BAO name for use in static calls. Return doc block is hacked to allow
32 * auto-completion of static methods
33 */
34 public static function getBAOFromApiName($entityName) {
35 if ($entityName === 'CustomValue' || strpos($entityName, 'Custom_') === 0) {
36 return 'CRM_Core_BAO_CustomValue';
37 }
38 $dao = self::getApiClass($entityName)::getInfo()['dao'] ?? NULL;
39 if (!$dao) {
40 return NULL;
41 }
42 $bao = str_replace("DAO", "BAO", $dao);
43 // Check if this entity actually has a BAO. Fall back on the DAO if not.
44 $file = strtr($bao, '_', '/') . '.php';
45 return stream_resolve_include_path($file) ? $bao : $dao;
46 }
47
48 /**
49 * @param $entityName
50 * @return string|\Civi\Api4\Generic\AbstractEntity
51 */
52 public static function getApiClass($entityName) {
53 if (strpos($entityName, 'Custom_') === 0) {
54 $groupName = substr($entityName, 7);
55 return self::isCustomEntity($groupName) ? 'Civi\Api4\CustomValue' : NULL;
56 }
57 // Because "Case" is a reserved php keyword
58 $className = 'Civi\Api4\\' . ($entityName === 'Case' ? 'CiviCase' : $entityName);
59 return class_exists($className) ? $className : NULL;
60 }
61
62 /**
63 * Get table name of given entity
64 *
65 * @param string $entityName
66 *
67 * @return string
68 */
69 public static function getTableName($entityName) {
70 if (strpos($entityName, 'Custom_') === 0) {
71 $customGroup = substr($entityName, 7);
72 return \CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $customGroup, 'table_name', 'name');
73 }
74 return AllCoreTables::getTableForEntityName($entityName);
75 }
76
77 /**
78 * Given a sql table name, return the name of the api entity.
79 *
80 * @param $tableName
81 * @return string|NULL
82 */
83 public static function getApiNameFromTableName($tableName) {
84 $entityName = AllCoreTables::getBriefName(AllCoreTables::getClassForTable($tableName));
85 // Real entities
86 if ($entityName) {
87 // Verify class exists
88 return self::getApiClass($entityName) ? $entityName : NULL;
89 }
90 // Multi-value custom group pseudo-entities
91 $customGroup = \CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $tableName, 'name', 'table_name');
92 return self::isCustomEntity($customGroup) ? "Custom_$customGroup" : NULL;
93 }
94
95 /**
96 * @return string[]
97 */
98 public static function getOperators() {
99 $operators = \CRM_Core_DAO::acceptedSQLOperators();
100 $operators[] = 'CONTAINS';
101 $operators[] = 'IS EMPTY';
102 $operators[] = 'IS NOT EMPTY';
103 return $operators;
104 }
105
106 /**
107 * For a given API Entity, return the types of custom fields it supports and the column they join to.
108 *
109 * @param string $entityName
110 * @return array|mixed|null
111 * @throws \API_Exception
112 * @throws \Civi\API\Exception\UnauthorizedException
113 */
114 public static function getCustomGroupExtends(string $entityName) {
115 // Custom_group.extends pretty much maps 1-1 with entity names, except for a couple oddballs (Contact, Participant).
116 switch ($entityName) {
117 case 'Contact':
118 return [
119 'extends' => array_merge(['Contact'], array_keys(\CRM_Core_SelectValues::contactType())),
120 'column' => 'id',
121 ];
122
123 case 'Participant':
124 return [
125 'extends' => ['Participant', 'ParticipantRole', 'ParticipantEventName', 'ParticipantEventType'],
126 'column' => 'id',
127 ];
128
129 case 'RelationshipCache':
130 return [
131 'extends' => ['Relationship'],
132 'column' => 'relationship_id',
133 ];
134 }
135 if (array_key_exists($entityName, \CRM_Core_SelectValues::customGroupExtends())) {
136 return [
137 'extends' => [$entityName],
138 'column' => 'id',
139 ];
140 }
141 return NULL;
142 }
143
144 /**
145 * Checks if a custom group exists and is multivalued
146 *
147 * @param $customGroupName
148 * @return bool
149 * @throws \CRM_Core_Exception
150 */
151 private static function isCustomEntity($customGroupName) {
152 return $customGroupName && \CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $customGroupName, 'is_multiple', 'name');
153 }
154
155 /**
156 * Check if current user is authorized to perform specified action on a given entity.
157 *
158 * @param \Civi\Api4\Generic\AbstractAction $apiRequest
159 * @param array $record
160 * @param int|null $userID
161 * Contact ID of the user we are testing, or NULL for the anonymous user.
162 * @return bool
163 * @throws \API_Exception
164 * @throws \CRM_Core_Exception
165 * @throws \Civi\API\Exception\NotImplementedException
166 * @throws \Civi\API\Exception\UnauthorizedException
167 */
168 public static function checkAccessRecord(\Civi\Api4\Generic\AbstractAction $apiRequest, array $record, ?int $userID) {
169 // For get actions, just run a get and ACLs will be applied to the query.
170 // It's a cheap trick and not as efficient as not running the query at all,
171 // but BAO::checkAccess doesn't consistently check permissions for the "get" action.
172 if (is_a($apiRequest, '\Civi\Api4\Generic\DAOGetAction')) {
173 return (bool) $apiRequest->addSelect('id')->addWhere('id', '=', $record['id'])->execute()->count();
174 }
175
176 $event = new \Civi\Api4\Event\AuthorizeRecordEvent($apiRequest, $record, $userID);
177 \Civi::dispatcher()->dispatch('civi.api4.authorizeRecord', $event);
178
179 // Note: $bao::_checkAccess() is a quasi-listener. TODO: Convert to straight-up listener.
180 if ($event->isAuthorized() === NULL) {
181 $baoName = self::getBAOFromApiName($apiRequest->getEntityName());
182 if ($baoName && method_exists($baoName, '_checkAccess')) {
183 $authorized = $baoName::_checkAccess($event->getEntityName(), $event->getActionName(), $event->getRecord(), $event->getUserID());
184 $event->setAuthorized($authorized);
185 }
186 else {
187 $event->setAuthorized(TRUE);
188 }
189 }
190 return $event->isAuthorized();
191 }
192
193 /**
194 * If the permissions of record $A are based on record $B, then use `checkAccessDelegated($B...)`
195 * to make see if access to $B is permitted.
196 *
197 * @param string $entityName
198 * @param string $actionName
199 * @param array $record
200 * @param int|null $userID
201 * Contact ID of the user we are testing, or NULL for the anonymous user.
202 *
203 * @return bool
204 * @throws \API_Exception
205 * @throws \CRM_Core_Exception
206 */
207 public static function checkAccessDelegated(string $entityName, string $actionName, array $record, ?int $userID) {
208 $apiRequest = Request::create($entityName, $actionName, ['version' => 4]);
209 // TODO: Should probably emit civi.api.authorize for checking guardian permission; but in APIv4 with std cfg, this is de-facto equivalent.
210 if (!$apiRequest->isAuthorized($userID)) {
211 return FALSE;
212 }
213 return static::checkAccessRecord($apiRequest, $record, $userID);
214 }
215
216 }