Merge pull request #5121 from totten/master-mailing-dflt
[civicrm-core.git] / Civi / API / Subscriber / DynamicFKAuthorization.php
CommitLineData
56154d36
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
39de6fd5 4 | CiviCRM version 4.6 |
56154d36 5 +--------------------------------------------------------------------+
39de6fd5 6 | Copyright CiviCRM LLC (c) 2004-2014 |
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
TO
41 * Note: This enforces a constraint: all matching API calls must define
42 * either "id" (e.g. for the file) or "entity_table".
56154d36 43 *
8882ff5c
TO
44 * Note: The permission guard does not exactly authorize the request, but it
45 * may veto authorization.
56154d36
TO
46 */
47class DynamicFKAuthorization implements EventSubscriberInterface {
48
49 /**
50 * @return array
51 */
52 public static function getSubscribedEvents() {
53 return array(
54 Events::AUTHORIZE => array(
55 array('onApiAuthorize', Events::W_EARLY),
56 ),
57 );
58 }
59
60 /**
61 * @var \Civi\API\Kernel
62 */
63 protected $kernel;
64
65 /**
66 * @var string, the entity for which we want to manage permissions
67 */
68 protected $entityName;
69
70 /**
71 * @var array <string> the actions for which we want to manage permissions
72 */
73 protected $actions;
74
75 /**
76 * @var string, SQL; a query which looks up the related entity
77 *
78 * ex: "SELECT if(cf.id,1,0) as is_valid, cef.entity_table, cef.entity_id
79 * FROM civicrm_file cf
80 * INNER JOIN civicrm_entity_file cef ON cf.id = cef.file_id
81 * WHERE cf.id = %1"
82 *
83 * Note: %1 is a parameter
84 * Note: There are three parameters
85 * - is_valid: "1" if %1 identifies an actual record; otherwise "0"
86 * - entity_table: NULL or the name of a related table
87 * - entity_id: NULL or the ID of a row in the related table
88 */
89 protected $lookupDelegateSql;
90
91 /**
92 * @var array list of related tables for which FKs are allowed
93 */
94 protected $allowedDelegates;
95
96 /**
97 * @param \Civi\API\Kernel $kernel
8882ff5c
TO
98 * The API kernel.
99 * @param string $entityName
100 * The entity for which we want to manage permissions (e.g. "File" or
101 * "Note").
102 * @param array $actions
103 * The actions for which we want to manage permissions (e.g. "create",
104 * "get", "delete").
105 * @param string $lookupDelegateSql
106 * See docblock in DynamicFKAuthorization::$lookupDelegateSql.
107 * @param array|NULL $allowedDelegates
108 * e.g. "civicrm_mailing","civicrm_activity"; NULL to allow any.
56154d36 109 */
8882ff5c 110 public function __construct($kernel, $entityName, $actions, $lookupDelegateSql, $allowedDelegates = NULL) {
56154d36
TO
111 $this->kernel = $kernel;
112 $this->entityName = $entityName;
113 $this->actions = $actions;
114 $this->lookupDelegateSql = $lookupDelegateSql;
115 $this->allowedDelegates = $allowedDelegates;
116 }
117
118 /**
119 * @param \Civi\API\Event\AuthorizeEvent $event
8882ff5c 120 * API authorization event.
56154d36
TO
121 * @throws \API_Exception
122 * @throws \Civi\API\Exception\UnauthorizedException
123 */
124 public function onApiAuthorize(\Civi\API\Event\AuthorizeEvent $event) {
125 $apiRequest = $event->getApiRequest();
126 if ($apiRequest['version'] == 3 && $apiRequest['entity'] == $this->entityName && in_array(strtolower($apiRequest['action']), $this->actions)) {
127 if (/*!$isTrusted */
128 empty($apiRequest['params']['id']) && empty($apiRequest['params']['entity_table'])
129 ) {
130 throw new \API_Exception("Mandatory key(s) missing from params array: 'id' or 'entity_table'");
131 }
132
133 if (isset($apiRequest['params']['id'])) {
134 list($isValidId, $entityTable, $entityId) = $this->getDelegate($apiRequest['params']['id']);
135 if ($isValidId && $entityTable && $entityId) {
136 $this->authorizeDelegate($apiRequest['action'], $entityTable, $entityId, $apiRequest);
137 $this->preventReassignment($apiRequest['params']['id'], $entityTable, $entityId, $apiRequest);
138 return;
139 }
140 elseif ($isValidId) {
141 throw new \API_Exception("Failed to match record to related entity");
8882ff5c
TO
142 }
143 elseif (!$isValidId && strtolower($apiRequest['action']) == 'get') {
144 // The matches will be an empty set; doesn't make a difference if we
145 // reject or accept.
146 // To pass SyntaxConformanceTest, we won't veto "get" on empty-set.
56154d36
TO
147 return;
148 }
149 }
150
151 if (isset($apiRequest['params']['entity_table'])) {
152 $this->authorizeDelegate(
153 $apiRequest['action'],
154 $apiRequest['params']['entity_table'],
155 \CRM_Utils_Array::value('entity_id', $apiRequest['params'], NULL),
156 $apiRequest
157 );
158 return;
159 }
160
161 throw new \API_Exception("Failed to run permission check");
162 }
163 }
164
165 /**
8882ff5c
TO
166 * @param string $action
167 * The API action (e.g. "create").
168 * @param string $entityTable
169 * The target entity table (e.g. "civicrm_mailing").
56154d36 170 * @param int|NULL $entityId
8882ff5c 171 * The target entity ID.
56154d36 172 * @param array $apiRequest
8882ff5c 173 * The full API request.
56154d36
TO
174 * @throws \API_Exception
175 * @throws \Civi\API\Exception\UnauthorizedException
176 */
177 public function authorizeDelegate($action, $entityTable, $entityId, $apiRequest) {
178 $entity = $this->getDelegatedEntityName($entityTable);
179 if (!$entity) {
180 throw new \API_Exception("Failed to run permission check: Unrecognized target entity ($entityTable)");
181 }
182
183 if ($this->isTrusted($apiRequest)) {
184 return;
185 }
186
187 $params = array('check_permissions' => 1);
188 if ($entityId) {
189 $params['id'] = $entityId;
190 }
191
192 if (!$this->kernel->runAuthorize($entity, $this->getDelegatedAction($action), $params)) {
193 throw new \Civi\API\Exception\UnauthorizedException("Authorization failed on ($entity,$entityId)");
194 }
195 }
196
197 /**
8882ff5c
TO
198 * If the request attempts to change the entity_table/entity_id of an
199 * existing record, then generate an error.
56154d36 200 *
8882ff5c
TO
201 * @param int $fileId
202 * The main record being changed.
203 * @param string $entityTable
204 * The saved FK.
205 * @param int $entityId
206 * The saved FK.
56154d36 207 * @param array $apiRequest
8882ff5c 208 * The full API request.
56154d36
TO
209 * @throws \API_Exception
210 */
211 public function preventReassignment($fileId, $entityTable, $entityId, $apiRequest) {
212 if (strtolower($apiRequest['action']) == 'create' && $fileId && !$this->isTrusted($apiRequest)) {
213 if (isset($apiRequest['params']['entity_table']) && $entityTable != $apiRequest['params']['entity_table']) {
214 throw new \API_Exception("Cannot modify entity_table");
215 }
216 if (isset($apiRequest['params']['entity_id']) && $entityId != $apiRequest['params']['entity_id']) {
217 throw new \API_Exception("Cannot modify entity_id");
218 }
219 }
220 }
221
222 /**
8882ff5c
TO
223 * @param string $entityTable
224 * The target entity table (e.g. "civicrm_mailing" or "civicrm_activity").
225 * @return string|NULL
226 * The target entity name (e.g. "Mailing" or "Activity").
56154d36
TO
227 */
228 public function getDelegatedEntityName($entityTable) {
229 if ($this->allowedDelegates === NULL || in_array($entityTable, $this->allowedDelegates)) {
230 $className = \CRM_Core_DAO_AllCoreTables::getClassForTable($entityTable);
231 if ($className) {
232 $entityName = \CRM_Core_DAO_AllCoreTables::getBriefName($className);
233 if ($entityName) {
234 return $entityName;
235 }
236 }
237 }
238 return NULL;
239 }
240
241 /**
8882ff5c
TO
242 * @param string $action
243 * API action name -- e.g. "create" ("When running *create* on a file...").
244 * @return string
245 * e.g. "create" ("Check for *create* permission on the mailing to which
246 * it is attached.")
56154d36
TO
247 */
248 public function getDelegatedAction($action) {
249 switch ($action) {
250 case 'get':
251 // reading attachments requires reading the other entity
252 return 'get';
8882ff5c 253
56154d36
TO
254 case 'create':
255 case 'delete':
8882ff5c
TO
256 // creating/updating/deleting an attachment requires editing
257 // the other entity
56154d36 258 return 'create';
8882ff5c 259
56154d36
TO
260 default:
261 return $action;
262 }
263 }
264
265 /**
266 * @param int $id
8882ff5c 267 * e.g. file ID.
a6c01b45
CW
268 * @return array
269 * (0 => bool $isValid, 1 => string $entityTable, 2 => int $entityId)
56154d36
TO
270 */
271 public function getDelegate($id) {
272 $query = \CRM_Core_DAO::executeQuery($this->lookupDelegateSql, array(
8882ff5c 273 1 => array($id, 'Positive'),
56154d36
TO
274 ));
275 if ($query->fetch()) {
276 return array($query->is_valid, $query->entity_table, $query->entity_id);
277 }
278 else {
279 return array(FALSE, NULL, NULL);
280 }
281 }
282
283 /**
284 * @param array $apiRequest
8882ff5c 285 * The full API request.
56154d36
TO
286 * @return bool
287 */
288 public function isTrusted($apiRequest) {
289 // isn't this redundant?
290 return empty($apiRequest['params']['check_permissions']) or $apiRequest['params']['check_permissions'] == FALSE;
291 }
292
293}