Merge pull request #13520 from yashodha/number_widget
[civicrm-core.git] / Civi / API / Subscriber / DynamicFKAuthorization.php
CommitLineData
56154d36
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
fee14197 4 | CiviCRM version 5 |
56154d36 5 +--------------------------------------------------------------------+
6b83d5bd 6 | Copyright CiviCRM LLC (c) 2004-2019 |
56154d36
TO
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
d25dd0ee 26 */
56154d36
TO
27
28namespace Civi\API\Subscriber;
29
30use Civi\API\Events;
31use Symfony\Component\EventDispatcher\EventSubscriberInterface;
32
33/**
8882ff5c
TO
34 * Given an entity which dynamically attaches itself to another entity,
35 * determine if one has permission to the other entity.
56154d36 36 *
8882ff5c
TO
37 * Example: Suppose one tries to manipulate a File which is attached to a
38 * Mailing. DynamicFKAuthorization will enforce permissions on the File by
39 * imitating the permissions of the Mailing.
56154d36 40 *
8882ff5c 41 * Note: This enforces a constraint: all matching API calls must define
29468114
TO
42 * "id" (e.g. for the file) or "entity_table+entity_id" or
43 * "field_name+entity_id".
56154d36 44 *
8882ff5c
TO
45 * Note: The permission guard does not exactly authorize the request, but it
46 * may veto authorization.
56154d36
TO
47 */
48class DynamicFKAuthorization implements EventSubscriberInterface {
49
50 /**
51 * @return array
52 */
53 public static function getSubscribedEvents() {
54 return array(
55 Events::AUTHORIZE => array(
56 array('onApiAuthorize', Events::W_EARLY),
57 ),
58 );
59 }
60
61 /**
62 * @var \Civi\API\Kernel
6d0ef411
TO
63 *
64 * Treat as private. Marked public due to PHP 5.3-compatibility issues.
56154d36 65 */
6d0ef411 66 public $kernel;
56154d36
TO
67
68 /**
69 * @var string, the entity for which we want to manage permissions
70 */
71 protected $entityName;
72
73 /**
74 * @var array <string> the actions for which we want to manage permissions
75 */
76 protected $actions;
77
78 /**
2e37a19f 79 * @var string, SQL. Given a file ID, determine the entity+table it's attached to.
56154d36
TO
80 *
81 * ex: "SELECT if(cf.id,1,0) as is_valid, cef.entity_table, cef.entity_id
82 * FROM civicrm_file cf
83 * INNER JOIN civicrm_entity_file cef ON cf.id = cef.file_id
84 * WHERE cf.id = %1"
85 *
86 * Note: %1 is a parameter
87 * Note: There are three parameters
88 * - is_valid: "1" if %1 identifies an actual record; otherwise "0"
89 * - entity_table: NULL or the name of a related table
90 * - entity_id: NULL or the ID of a row in the related table
91 */
92 protected $lookupDelegateSql;
93
29468114
TO
94 /**
95 * @var string, SQL. Get a list of (field_name, table_name, extends) tuples.
96 *
97 * For example, one tuple might be ("custom_123", "civicrm_value_mygroup_4",
98 * "Activity").
99 */
100 protected $lookupCustomFieldSql;
101
102 /**
103 * @var array
104 *
105 * Each item is an array(field_name => $, table_name => $, extends => $)
106 */
107 protected $lookupCustomFieldCache;
108
56154d36
TO
109 /**
110 * @var array list of related tables for which FKs are allowed
111 */
112 protected $allowedDelegates;
113
114 /**
115 * @param \Civi\API\Kernel $kernel
8882ff5c
TO
116 * The API kernel.
117 * @param string $entityName
118 * The entity for which we want to manage permissions (e.g. "File" or
119 * "Note").
120 * @param array $actions
121 * The actions for which we want to manage permissions (e.g. "create",
122 * "get", "delete").
123 * @param string $lookupDelegateSql
124 * See docblock in DynamicFKAuthorization::$lookupDelegateSql.
29468114
TO
125 * @param string $lookupCustomFieldSql
126 * See docblock in DynamicFKAuthorization::$lookupCustomFieldSql.
8882ff5c
TO
127 * @param array|NULL $allowedDelegates
128 * e.g. "civicrm_mailing","civicrm_activity"; NULL to allow any.
56154d36 129 */
29468114 130 public function __construct($kernel, $entityName, $actions, $lookupDelegateSql, $lookupCustomFieldSql, $allowedDelegates = NULL) {
56154d36 131 $this->kernel = $kernel;
9caac49e 132 $this->entityName = \CRM_Utils_String::convertStringToCamel($entityName);
56154d36
TO
133 $this->actions = $actions;
134 $this->lookupDelegateSql = $lookupDelegateSql;
29468114 135 $this->lookupCustomFieldSql = $lookupCustomFieldSql;
56154d36
TO
136 $this->allowedDelegates = $allowedDelegates;
137 }
138
139 /**
140 * @param \Civi\API\Event\AuthorizeEvent $event
8882ff5c 141 * API authorization event.
56154d36
TO
142 * @throws \API_Exception
143 * @throws \Civi\API\Exception\UnauthorizedException
144 */
145 public function onApiAuthorize(\Civi\API\Event\AuthorizeEvent $event) {
146 $apiRequest = $event->getApiRequest();
9caac49e 147 if ($apiRequest['version'] == 3 && \CRM_Utils_String::convertStringToCamel($apiRequest['entity']) == $this->entityName && in_array(strtolower($apiRequest['action']), $this->actions)) {
29468114
TO
148 if (isset($apiRequest['params']['field_name'])) {
149 $fldIdx = \CRM_Utils_Array::index(array('field_name'), $this->getCustomFields());
150 if (empty($fldIdx[$apiRequest['params']['field_name']])) {
151 throw new \Exception("Failed to map custom field to entity table");
152 }
153 $apiRequest['params']['entity_table'] = $fldIdx[$apiRequest['params']['field_name']]['entity_table'];
154 unset($apiRequest['params']['field_name']);
155 }
156
56154d36
TO
157 if (/*!$isTrusted */
158 empty($apiRequest['params']['id']) && empty($apiRequest['params']['entity_table'])
159 ) {
160 throw new \API_Exception("Mandatory key(s) missing from params array: 'id' or 'entity_table'");
161 }
162
163 if (isset($apiRequest['params']['id'])) {
164 list($isValidId, $entityTable, $entityId) = $this->getDelegate($apiRequest['params']['id']);
165 if ($isValidId && $entityTable && $entityId) {
166 $this->authorizeDelegate($apiRequest['action'], $entityTable, $entityId, $apiRequest);
167 $this->preventReassignment($apiRequest['params']['id'], $entityTable, $entityId, $apiRequest);
168 return;
169 }
170 elseif ($isValidId) {
171 throw new \API_Exception("Failed to match record to related entity");
8882ff5c
TO
172 }
173 elseif (!$isValidId && strtolower($apiRequest['action']) == 'get') {
174 // The matches will be an empty set; doesn't make a difference if we
175 // reject or accept.
176 // To pass SyntaxConformanceTest, we won't veto "get" on empty-set.
56154d36
TO
177 return;
178 }
179 }
180
181 if (isset($apiRequest['params']['entity_table'])) {
182 $this->authorizeDelegate(
183 $apiRequest['action'],
184 $apiRequest['params']['entity_table'],
185 \CRM_Utils_Array::value('entity_id', $apiRequest['params'], NULL),
186 $apiRequest
187 );
188 return;
189 }
190
191 throw new \API_Exception("Failed to run permission check");
192 }
193 }
194
195 /**
8882ff5c
TO
196 * @param string $action
197 * The API action (e.g. "create").
198 * @param string $entityTable
199 * The target entity table (e.g. "civicrm_mailing").
56154d36 200 * @param int|NULL $entityId
8882ff5c 201 * The target entity ID.
56154d36 202 * @param array $apiRequest
8882ff5c 203 * The full API request.
066c4638 204 * @throws \Exception
56154d36
TO
205 * @throws \API_Exception
206 * @throws \Civi\API\Exception\UnauthorizedException
207 */
208 public function authorizeDelegate($action, $entityTable, $entityId, $apiRequest) {
209 $entity = $this->getDelegatedEntityName($entityTable);
210 if (!$entity) {
29468114
TO
211 throw new \API_Exception("Failed to run permission check: Unrecognized target entity table ($entityTable)");
212 }
213 if (!$entityId) {
214 throw new \Civi\API\Exception\UnauthorizedException("Authorization failed on ($entity): Missing entity_id");
56154d36
TO
215 }
216
217 if ($this->isTrusted($apiRequest)) {
218 return;
219 }
220
29468114
TO
221 /**
222 * @var \Exception $exception
223 */
224 $exception = NULL;
6d0ef411
TO
225 $self = $this;
226 \CRM_Core_Transaction::create(TRUE)->run(function($tx) use ($entity, $action, $entityId, &$exception, $self) {
29468114 227 $tx->rollback(); // Just to be safe.
56154d36 228
29468114
TO
229 $params = array(
230 'version' => 3,
231 'check_permissions' => 1,
232 'id' => $entityId,
233 );
234
6d0ef411 235 $result = $self->kernel->run($entity, $self->getDelegatedAction($action), $params);
29468114
TO
236 if ($result['is_error'] || empty($result['values'])) {
237 $exception = new \Civi\API\Exception\UnauthorizedException("Authorization failed on ($entity,$entityId)", array(
238 'cause' => $result,
239 ));
240 }
241 });
242
243 if ($exception) {
244 throw $exception;
56154d36
TO
245 }
246 }
247
248 /**
8882ff5c
TO
249 * If the request attempts to change the entity_table/entity_id of an
250 * existing record, then generate an error.
56154d36 251 *
8882ff5c
TO
252 * @param int $fileId
253 * The main record being changed.
254 * @param string $entityTable
255 * The saved FK.
256 * @param int $entityId
257 * The saved FK.
56154d36 258 * @param array $apiRequest
8882ff5c 259 * The full API request.
56154d36
TO
260 * @throws \API_Exception
261 */
262 public function preventReassignment($fileId, $entityTable, $entityId, $apiRequest) {
263 if (strtolower($apiRequest['action']) == 'create' && $fileId && !$this->isTrusted($apiRequest)) {
29468114 264 // TODO: no change in field_name?
56154d36
TO
265 if (isset($apiRequest['params']['entity_table']) && $entityTable != $apiRequest['params']['entity_table']) {
266 throw new \API_Exception("Cannot modify entity_table");
267 }
268 if (isset($apiRequest['params']['entity_id']) && $entityId != $apiRequest['params']['entity_id']) {
269 throw new \API_Exception("Cannot modify entity_id");
270 }
271 }
272 }
273
274 /**
8882ff5c
TO
275 * @param string $entityTable
276 * The target entity table (e.g. "civicrm_mailing" or "civicrm_activity").
277 * @return string|NULL
278 * The target entity name (e.g. "Mailing" or "Activity").
56154d36
TO
279 */
280 public function getDelegatedEntityName($entityTable) {
281 if ($this->allowedDelegates === NULL || in_array($entityTable, $this->allowedDelegates)) {
282 $className = \CRM_Core_DAO_AllCoreTables::getClassForTable($entityTable);
283 if ($className) {
284 $entityName = \CRM_Core_DAO_AllCoreTables::getBriefName($className);
285 if ($entityName) {
286 return $entityName;
287 }
288 }
289 }
290 return NULL;
291 }
292
293 /**
8882ff5c
TO
294 * @param string $action
295 * API action name -- e.g. "create" ("When running *create* on a file...").
296 * @return string
297 * e.g. "create" ("Check for *create* permission on the mailing to which
298 * it is attached.")
56154d36
TO
299 */
300 public function getDelegatedAction($action) {
301 switch ($action) {
302 case 'get':
303 // reading attachments requires reading the other entity
304 return 'get';
8882ff5c 305
56154d36
TO
306 case 'create':
307 case 'delete':
8882ff5c
TO
308 // creating/updating/deleting an attachment requires editing
309 // the other entity
56154d36 310 return 'create';
8882ff5c 311
56154d36
TO
312 default:
313 return $action;
314 }
315 }
316
317 /**
318 * @param int $id
8882ff5c 319 * e.g. file ID.
a6c01b45
CW
320 * @return array
321 * (0 => bool $isValid, 1 => string $entityTable, 2 => int $entityId)
29468114 322 * @throws \Exception
56154d36
TO
323 */
324 public function getDelegate($id) {
325 $query = \CRM_Core_DAO::executeQuery($this->lookupDelegateSql, array(
8882ff5c 326 1 => array($id, 'Positive'),
56154d36
TO
327 ));
328 if ($query->fetch()) {
29468114
TO
329 if (!preg_match('/^civicrm_value_/', $query->entity_table)) {
330 // A normal attachment directly on its entity.
331 return array($query->is_valid, $query->entity_table, $query->entity_id);
332 }
333
334 // Ex: Translate custom-field table ("civicrm_value_foo_4") to
335 // entity table ("civicrm_activity").
336 $tblIdx = \CRM_Utils_Array::index(array('table_name'), $this->getCustomFields());
337 if (isset($tblIdx[$query->entity_table])) {
338 return array($query->is_valid, $tblIdx[$query->entity_table]['entity_table'], $query->entity_id);
339 }
340 throw new \Exception('Failed to lookup entity table for custom field.');
56154d36
TO
341 }
342 else {
343 return array(FALSE, NULL, NULL);
344 }
345 }
346
347 /**
348 * @param array $apiRequest
8882ff5c 349 * The full API request.
56154d36
TO
350 * @return bool
351 */
352 public function isTrusted($apiRequest) {
353 // isn't this redundant?
354 return empty($apiRequest['params']['check_permissions']) or $apiRequest['params']['check_permissions'] == FALSE;
355 }
356
29468114
TO
357 /**
358 * @return array
359 * Each item has keys 'field_name', 'table_name', 'extends', 'entity_table'
360 */
361 public function getCustomFields() {
362 $query = \CRM_Core_DAO::executeQuery($this->lookupCustomFieldSql);
363 $rows = array();
364 while ($query->fetch()) {
365 $rows[] = array(
366 'field_name' => $query->field_name,
367 'table_name' => $query->table_name,
368 'extends' => $query->extends,
369 'entity_table' => \CRM_Core_BAO_CustomGroup::getTableNameByEntityName($query->extends),
370 );
371 }
372 return $rows;
373 }
374
56154d36 375}