3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.6 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2014 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
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. |
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. |
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 +--------------------------------------------------------------------+
28 namespace Civi\API\Subscriber
;
31 use Symfony\Component\EventDispatcher\EventSubscriberInterface
;
34 * Given an entity which dynamically attaches itself to another entity,
35 * determine if one has permission to the other entity.
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.
41 * Note: This enforces a constraint: all matching API calls must define
42 * either "id" (e.g. for the file) or "entity_table".
44 * Note: The permission guard does not exactly authorize the request, but it
45 * may veto authorization.
47 class DynamicFKAuthorization
implements EventSubscriberInterface
{
52 public static function getSubscribedEvents() {
54 Events
::AUTHORIZE
=> array(
55 array('onApiAuthorize', Events
::W_EARLY
),
61 * @var \Civi\API\Kernel
66 * @var string, the entity for which we want to manage permissions
68 protected $entityName;
71 * @var array <string> the actions for which we want to manage permissions
76 * @var string, SQL; a query which looks up the related entity
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
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
89 protected $lookupDelegateSql;
92 * @var array list of related tables for which FKs are allowed
94 protected $allowedDelegates;
97 * @param \Civi\API\Kernel $kernel
99 * @param string $entityName
100 * The entity for which we want to manage permissions (e.g. "File" or
102 * @param array $actions
103 * The actions for which we want to manage permissions (e.g. "create",
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.
110 public function __construct($kernel, $entityName, $actions, $lookupDelegateSql, $allowedDelegates = NULL) {
111 $this->kernel
= $kernel;
112 $this->entityName
= $entityName;
113 $this->actions
= $actions;
114 $this->lookupDelegateSql
= $lookupDelegateSql;
115 $this->allowedDelegates
= $allowedDelegates;
119 * @param \Civi\API\Event\AuthorizeEvent $event
120 * API authorization event.
121 * @throws \API_Exception
122 * @throws \Civi\API\Exception\UnauthorizedException
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
)) {
128 empty($apiRequest['params']['id']) && empty($apiRequest['params']['entity_table'])
130 throw new \
API_Exception("Mandatory key(s) missing from params array: 'id' or 'entity_table'");
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);
140 elseif ($isValidId) {
141 throw new \
API_Exception("Failed to match record to related entity");
143 elseif (!$isValidId && strtolower($apiRequest['action']) == 'get') {
144 // The matches will be an empty set; doesn't make a difference if we
146 // To pass SyntaxConformanceTest, we won't veto "get" on empty-set.
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),
161 throw new \
API_Exception("Failed to run permission check");
166 * @param string $action
167 * The API action (e.g. "create").
168 * @param string $entityTable
169 * The target entity table (e.g. "civicrm_mailing").
170 * @param int|NULL $entityId
171 * The target entity ID.
172 * @param array $apiRequest
173 * The full API request.
174 * @throws \API_Exception
175 * @throws \Civi\API\Exception\UnauthorizedException
177 public function authorizeDelegate($action, $entityTable, $entityId, $apiRequest) {
178 $entity = $this->getDelegatedEntityName($entityTable);
180 throw new \
API_Exception("Failed to run permission check: Unrecognized target entity ($entityTable)");
183 if ($this->isTrusted($apiRequest)) {
187 $params = array('check_permissions' => 1);
189 $params['id'] = $entityId;
192 if (!$this->kernel
->runAuthorize($entity, $this->getDelegatedAction($action), $params)) {
193 throw new \Civi\API\Exception\
UnauthorizedException("Authorization failed on ($entity,$entityId)");
198 * If the request attempts to change the entity_table/entity_id of an
199 * existing record, then generate an error.
202 * The main record being changed.
203 * @param string $entityTable
205 * @param int $entityId
207 * @param array $apiRequest
208 * The full API request.
209 * @throws \API_Exception
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");
216 if (isset($apiRequest['params']['entity_id']) && $entityId != $apiRequest['params']['entity_id']) {
217 throw new \
API_Exception("Cannot modify entity_id");
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").
228 public function getDelegatedEntityName($entityTable) {
229 if ($this->allowedDelegates
=== NULL ||
in_array($entityTable, $this->allowedDelegates
)) {
230 $className = \CRM_Core_DAO_AllCoreTables
::getClassForTable($entityTable);
232 $entityName = \CRM_Core_DAO_AllCoreTables
::getBriefName($className);
242 * @param string $action
243 * API action name -- e.g. "create" ("When running *create* on a file...").
245 * e.g. "create" ("Check for *create* permission on the mailing to which
248 public function getDelegatedAction($action) {
251 // reading attachments requires reading the other entity
256 // creating/updating/deleting an attachment requires editing
269 * (0 => bool $isValid, 1 => string $entityTable, 2 => int $entityId)
271 public function getDelegate($id) {
272 $query = \CRM_Core_DAO
::executeQuery($this->lookupDelegateSql
, array(
273 1 => array($id, 'Positive'),
275 if ($query->fetch()) {
276 return array($query->is_valid
, $query->entity_table
, $query->entity_id
);
279 return array(FALSE, NULL, NULL);
284 * @param array $apiRequest
285 * The full API request.
288 public function isTrusted($apiRequest) {
289 // isn't this redundant?
290 return empty($apiRequest['params']['check_permissions']) or $apiRequest['params']['check_permissions'] == FALSE;