Merge pull request #8394 from yashodha/CRM-18046
[civicrm-core.git] / tests / phpunit / api / v3 / ACLPermissionTest.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.7 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2016 |
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 +--------------------------------------------------------------------+
26 */
27
28 /**
29 * This class is intended to test ACL permission using the multisite module
30 *
31 * @package CiviCRM_APIv3
32 * @subpackage API_Contact
33 * @group headless
34 */
35 class api_v3_ACLPermissionTest extends CiviUnitTestCase {
36 protected $_apiversion = 3;
37 public $DBResetRequired = FALSE;
38 protected $_entity;
39 protected $allowedContactId = 0;
40
41 public function setUp() {
42 parent::setUp();
43 $baoObj = new CRM_Core_DAO();
44 $baoObj->createTestObject('CRM_Pledge_BAO_Pledge', array(), 1, 0);
45 $baoObj->createTestObject('CRM_Core_BAO_Phone', array(), 1, 0);
46 $config = CRM_Core_Config::singleton();
47 $config->userPermissionClass->permissions = array();
48 }
49
50 /**
51 * (non-PHPdoc)
52 * @see CiviUnitTestCase::tearDown()
53 */
54 public function tearDown() {
55 CRM_Utils_Hook::singleton()->reset();
56 $tablesToTruncate = array(
57 'civicrm_contact',
58 'civicrm_group_contact',
59 'civicrm_group',
60 'civicrm_acl',
61 'civicrm_acl_cache',
62 'civicrm_acl_entity_role',
63 'civicrm_acl_contact_cache',
64 'civicrm_contribution',
65 'civicrm_participant',
66 'civicrm_uf_match',
67 'civicrm_activity',
68 'civicrm_activity_contact',
69 );
70 $this->quickCleanup($tablesToTruncate);
71 $config = CRM_Core_Config::singleton();
72 unset($config->userPermissionClass->permissions);
73 }
74
75 /**
76 * Function tests that an empty where hook returns no results.
77 */
78 public function testContactGetNoResultsHook() {
79 $this->hookClass->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookNoResults'));
80 $result = $this->callAPISuccess('contact', 'get', array(
81 'check_permissions' => 1,
82 'return' => 'display_name',
83 ));
84 $this->assertEquals(0, $result['count']);
85 }
86
87 /**
88 * Function tests that an empty where hook returns exactly 1 result with "view my contact".
89 *
90 * CRM-16512 caused contacts with Edit my contact to be able to view all records.
91 */
92 public function testContactGetOneResultHookWithViewMyContact() {
93 $this->createLoggedInUser();
94 $this->hookClass->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookNoResults'));
95 CRM_Core_Config::singleton()->userPermissionClass->permissions = array('access CiviCRM', 'view my contact');
96 $result = $this->callAPISuccess('contact', 'get', array(
97 'check_permissions' => 1,
98 'return' => 'display_name',
99 ));
100 $this->assertEquals(1, $result['count']);
101 }
102
103 /**
104 * Function tests that a user with "edit my contact" can edit themselves.
105 */
106 public function testContactEditHookWithEditMyContact() {
107 $cid = $this->createLoggedInUser();
108 $this->hookClass->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookNoResults'));
109 CRM_Core_Config::singleton()->userPermissionClass->permissions = array('access CiviCRM', 'edit my contact');
110 $this->callAPISuccess('contact', 'create', array(
111 'check_permissions' => 1,
112 'id' => $cid,
113 ));
114 }
115
116 /**
117 * Ensure contact permissions extend to related entities like email
118 */
119 public function testRelatedEntityPermissions() {
120 $this->createLoggedInUser();
121 $disallowedContact = $this->individualCreate(array(), 0);
122 $this->allowedContactId = $this->individualCreate(array(), 1);
123 $this->hookClass->setHook('civicrm_aclWhereClause', array($this, 'aclWhereOnlyOne'));
124 CRM_Core_Config::singleton()->userPermissionClass->permissions = array('access CiviCRM');
125 $testEntities = array(
126 'Email' => array('email' => 'null@nothing', 'location_type_id' => 1),
127 'Phone' => array('phone' => '123456', 'location_type_id' => 1),
128 'IM' => array('name' => 'hello', 'location_type_id' => 1),
129 'Website' => array('url' => 'http://test'),
130 'Address' => array('street_address' => '123 Sesame St.', 'location_type_id' => 1),
131 );
132 foreach ($testEntities as $entity => $params) {
133 $params += array(
134 'contact_id' => $disallowedContact,
135 'check_permissions' => 1,
136 );
137 // We should be prevented from getting or creating entities for a contact we don't have permission for
138 $this->callAPIFailure($entity, 'create', $params);
139 $results = $this->callAPISuccess($entity, 'get', array('contact_id' => $disallowedContact, 'check_permissions' => 1));
140 $this->assertEquals(0, $results['count']);
141
142 // We should be allowed to create and get for contacts we do have permission on
143 $params['contact_id'] = $this->allowedContactId;
144 $this->callAPISuccess($entity, 'create', $params);
145 $results = $this->callAPISuccess($entity, 'get', array('contact_id' => $this->allowedContactId, 'check_permissions' => 1));
146 $this->assertGreaterThan(0, $results['count']);
147 }
148 }
149
150 /**
151 * Function tests all results are returned.
152 */
153 public function testContactGetAllResultsHook() {
154 $this->hookClass->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookAllResults'));
155 $result = $this->callAPISuccess('contact', 'get', array(
156 'check_permissions' => 1,
157 'return' => 'display_name',
158 ));
159
160 $this->assertEquals(2, $result['count']);
161 }
162
163 /**
164 * Function tests that deleted contacts are not returned.
165 */
166 public function testContactGetPermissionHookNoDeleted() {
167 $this->callAPISuccess('contact', 'create', array('id' => 2, 'is_deleted' => 1));
168 $this->hookClass->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookAllResults'));
169 $result = $this->callAPISuccess('contact', 'get', array(
170 'check_permissions' => 1,
171 'return' => 'display_name',
172 ));
173 $this->assertEquals(1, $result['count']);
174 }
175
176 /**
177 * Test permissions limited by hook.
178 */
179 public function testContactGetHookLimitingHook() {
180 $this->hookClass->setHook('civicrm_aclWhereClause', array($this, 'aclWhereOnlySecond'));
181
182 $result = $this->callAPISuccess('contact', 'get', array(
183 'check_permissions' => 1,
184 'return' => 'display_name',
185 ));
186 $this->assertEquals(1, $result['count']);
187 }
188
189 /**
190 * Confirm that without check permissions we still get 2 contacts returned.
191 */
192 public function testContactGetHookLimitingHookDontCheck() {
193 $result = $this->callAPISuccess('contact', 'get', array(
194 'check_permissions' => 0,
195 'return' => 'display_name',
196 ));
197 $this->assertEquals(2, $result['count']);
198 }
199
200 /**
201 * Check that id works as a filter.
202 */
203 public function testContactGetIDFilter() {
204 $this->hookClass->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookAllResults'));
205 $result = $this->callAPISuccess('contact', 'get', array(
206 'sequential' => 1,
207 'id' => 2,
208 'check_permissions' => 1,
209 ));
210
211 $this->assertEquals(1, $result['count']);
212 $this->assertEquals(2, $result['id']);
213 }
214
215 /**
216 * Check that address IS returned.
217 */
218 public function testContactGetAddressReturned() {
219 $this->hookClass->setHook('civicrm_aclWhereClause', array($this, 'aclWhereOnlySecond'));
220 $fullresult = $this->callAPISuccess('contact', 'get', array(
221 'sequential' => 1,
222 ));
223 //return doesn't work for all keys - can't fix that here so let's skip ...
224 //prefix & suffix are inconsistent due to CRM-7929
225 // unsure about others but return doesn't work on them
226 $elementsReturnDoesntSupport = array(
227 'prefix',
228 'suffix',
229 'gender',
230 'current_employer',
231 'phone_id',
232 'phone_type_id',
233 'phone',
234 'worldregion_id',
235 'world_region',
236 );
237 $expectedReturnElements = array_diff(array_keys($fullresult['values'][0]), $elementsReturnDoesntSupport);
238 $result = $this->callAPISuccess('contact', 'get', array(
239 'check_permissions' => 1,
240 'return' => $expectedReturnElements,
241 'sequential' => 1,
242 ));
243 $this->assertEquals(1, $result['count']);
244 foreach ($expectedReturnElements as $element) {
245 $this->assertArrayHasKey($element, $result['values'][0]);
246 }
247 }
248
249 /**
250 * Check that pledge IS not returned.
251 */
252 public function testContactGetPledgeIDNotReturned() {
253 $this->hookClass->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookAllResults'));
254 $this->callAPISuccess('contact', 'get', array(
255 'sequential' => 1,
256 ));
257 $result = $this->callAPISuccess('contact', 'get', array(
258 'check_permissions' => 1,
259 'return' => 'pledge_id',
260 'sequential' => 1,
261 ));
262 $this->assertArrayNotHasKey('pledge_id', $result['values'][0]);
263 }
264
265 /**
266 * Check that pledge IS not an allowable filter.
267 */
268 public function testContactGetPledgeIDNotFiltered() {
269 $this->hookClass->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookAllResults'));
270 $this->callAPISuccess('contact', 'get', array(
271 'sequential' => 1,
272 ));
273 $result = $this->callAPISuccess('contact', 'get', array(
274 'check_permissions' => 1,
275 'pledge_id' => 1,
276 'sequential' => 1,
277 ));
278 $this->assertEquals(2, $result['count']);
279 }
280
281 /**
282 * Check that chaining doesn't bypass permissions
283 */
284 public function testContactGetPledgeNotChainable() {
285 $this->hookClass->setHook('civicrm_aclWhereClause', array($this, 'aclWhereOnlySecond'));
286 $this->callAPISuccess('contact', 'get', array(
287 'sequential' => 1,
288 ));
289 $this->callAPIFailure('contact', 'get', array(
290 'check_permissions' => 1,
291 'api.pledge.get' => 1,
292 'sequential' => 1,
293 ),
294 'Error in call to pledge_get : API permission check failed for pledge/get call; missing permission: access CiviCRM.'
295 );
296 }
297
298 public function setupCoreACL() {
299 $this->createLoggedInUser();
300 $this->_permissionedDisabledGroup = $this->groupCreate(array(
301 'title' => 'pick-me-disabled',
302 'is_active' => 0,
303 'name' => 'pick-me-disabled',
304 ));
305 $this->_permissionedGroup = $this->groupCreate(array(
306 'title' => 'pick-me-active',
307 'is_active' => 1,
308 'name' => 'pick-me-active',
309 ));
310 $this->setupACL();
311 }
312
313 /**
314 * @dataProvider entities
315 * confirm that without check permissions we still get 2 contacts returned
316 * @param $entity
317 */
318 public function testEntitiesGetHookLimitingHookNoCheck($entity) {
319 CRM_Core_Config::singleton()->userPermissionClass->permissions = array();
320 $this->setUpEntities($entity);
321 $this->hookClass->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookNoResults'));
322 $result = $this->callAPISuccess($entity, 'get', array(
323 'check_permissions' => 0,
324 'return' => 'contact_id',
325 ));
326 $this->assertEquals(2, $result['count']);
327 }
328
329 /**
330 * @dataProvider entities
331 * confirm that without check permissions we still get 2 entities returned
332 * @param $entity
333 */
334 public function testEntitiesGetCoreACLLimitingHookNoCheck($entity) {
335 $this->setupCoreACL();
336 //CRM_Core_Config::singleton()->userPermissionClass->permissions = array();
337 $this->setUpEntities($entity);
338 $this->hookClass->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookNoResults'));
339 $result = $this->callAPISuccess($entity, 'get', array(
340 'check_permissions' => 0,
341 'return' => 'contact_id',
342 ));
343 $this->assertEquals(2, $result['count']);
344 }
345
346 /**
347 * @dataProvider entities
348 * confirm that with check permissions we don't get entities
349 * @param $entity
350 * @throws \PHPUnit_Framework_IncompleteTestError
351 */
352 public function testEntitiesGetCoreACLLimitingCheck($entity) {
353 $this->setupCoreACL();
354 $this->setUpEntities($entity);
355 $result = $this->callAPISuccess($entity, 'get', array(
356 'check_permissions' => 1,
357 'return' => 'contact_id',
358 ));
359 $this->assertEquals(0, $result['count']);
360 }
361
362 /**
363 * @dataProvider entities
364 * Function tests that an empty where hook returns no results
365 * @param string $entity
366 * @throws \PHPUnit_Framework_IncompleteTestError
367 */
368 public function testEntityGetNoResultsHook($entity) {
369 $this->markTestIncomplete('hook acls only work with contacts so far');
370 CRM_Core_Config::singleton()->userPermissionClass->permissions = array();
371 $this->setUpEntities($entity);
372 $this->hookClass->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookNoResults'));
373 $result = $this->callAPISuccess($entity, 'get', array(
374 'check_permission' => 1,
375 ));
376 $this->assertEquals(0, $result['count']);
377 }
378
379 /**
380 * @return array
381 */
382 public static function entities() {
383 return array(array('contribution'), array('participant'));// @todo array('pledge' => 'pledge')
384 }
385
386 /**
387 * Create 2 entities
388 * @param $entity
389 */
390 public function setUpEntities($entity) {
391 $baoObj = new CRM_Core_DAO();
392 $baoObj->createTestObject(_civicrm_api3_get_BAO($entity), array(), 2, 0);
393 CRM_Core_Config::singleton()->userPermissionClass->permissions = array(
394 'access CiviCRM',
395 'access CiviContribute',
396 'access CiviEvent',
397 'view event participants',
398 );
399 }
400
401 /**
402 * No results returned.
403 *
404 * @implements CRM_Utils_Hook::aclWhereClause
405 *
406 * @param string $type
407 * @param array $tables
408 * @param array $whereTables
409 * @param int $contactID
410 * @param string $where
411 */
412 public function aclWhereHookNoResults($type, &$tables, &$whereTables, &$contactID, &$where) {
413 }
414
415 /**
416 * All results returned.
417 *
418 * @implements CRM_Utils_Hook::aclWhereClause
419 *
420 * @param string $type
421 * @param array $tables
422 * @param array $whereTables
423 * @param int $contactID
424 * @param string $where
425 */
426 public function aclWhereHookAllResults($type, &$tables, &$whereTables, &$contactID, &$where) {
427 $where = " (1) ";
428 }
429
430 /**
431 * All but first results returned.
432 * @implements CRM_Utils_Hook::aclWhereClause
433 * @param $type
434 * @param $tables
435 * @param $whereTables
436 * @param $contactID
437 * @param $where
438 */
439 public function aclWhereOnlySecond($type, &$tables, &$whereTables, &$contactID, &$where) {
440 $where = " contact_a.id > 1";
441 }
442
443 /**
444 * Only specified contact returned.
445 * @implements CRM_Utils_Hook::aclWhereClause
446 * @param $type
447 * @param $tables
448 * @param $whereTables
449 * @param $contactID
450 * @param $where
451 */
452 public function aclWhereOnlyOne($type, &$tables, &$whereTables, &$contactID, &$where) {
453 $where = " contact_a.id = " . $this->allowedContactId;
454 }
455
456 /**
457 * Basic check that an unpermissioned call keeps working and permissioned call fails.
458 */
459 public function testGetActivityNoPermissions() {
460 $this->setPermissions(array());
461 $this->callAPISuccess('Activity', 'get', array());
462 $this->callAPIFailure('Activity', 'get', array('check_permissions' => 1));
463 }
464
465 /**
466 * View all activities is enough regardless of contact ACLs.
467 */
468 public function testGetActivityViewAllActivitiesEnoughWithOrWithoutID() {
469 $activity = $this->activityCreate();
470 $this->setPermissions(array('view all activities', 'access CiviCRM'));
471 $this->callAPISuccess('Activity', 'getsingle', array('check_permissions' => 1, 'id' => $activity['id']));
472 $this->callAPISuccess('Activity', 'getsingle', array('check_permissions' => 1));
473 }
474
475 /**
476 * View all activities is required unless id is passed in.
477 */
478 public function testGetActivityViewAllContactsNotEnoughWIthoutID() {
479 $this->setPermissions(array('view all contacts', 'access CiviCRM'));
480 $this->callAPIFailure('Activity', 'get', array('check_permissions' => 1));
481 }
482
483 /**
484 * View all activities is required unless id is passed in, in which case ACLs are used.
485 */
486 public function testGetActivityViewAllContactsEnoughWIthID() {
487 $activity = $this->activityCreate();
488 $this->setPermissions(array('view all contacts', 'access CiviCRM'));
489 $this->callAPISuccess('Activity', 'getsingle', array('check_permissions' => 1, 'id' => $activity['id']));
490 }
491
492 /**
493 * View all activities is required unless id is passed in, in which case ACLs are used.
494 */
495 public function testGetActivityAccessCiviCRMNotEnough() {
496 $activity = $this->activityCreate();
497 $this->setPermissions(array('access CiviCRM'));
498 $this->callAPIFailure('Activity', 'getsingle', array('check_permissions' => 1, 'id' => $activity['id']));
499 }
500
501 /**
502 * Check that activities can be retrieved by ACL.
503 *
504 * The activities api applies ACLs in a very limited circumstance, if id is passed in.
505 * Otherwise it sticks with the blunt original permissions.
506 */
507 public function testGetActivityByACL() {
508 $this->setPermissions(array('access CiviCRM'));
509 $activity = $this->activityCreate();
510
511 $this->hookClass->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookAllResults'));
512 $this->callAPISuccess('Activity', 'getsingle', array('check_permissions' => 1, 'id' => $activity['id']));
513 }
514
515 /**
516 * To leverage ACL permission to view an activity you must be able to see all of the contacts.
517 */
518 public function testGetActivityByAclCannotViewAllContacts() {
519 $activity = $this->activityCreate();
520 $contacts = $this->getActivityContacts($activity);
521 $this->setPermissions(array('access CiviCRM'));
522
523 foreach ($contacts as $contact_id) {
524 $this->allowedContactId = $contact_id;
525 $this->hookClass->setHook('civicrm_aclWhereClause', array($this, 'aclWhereOnlyOne'));
526 $this->callAPIFailure('Activity', 'getsingle', array('check_permissions' => 1, 'id' => $activity['id']));
527 }
528 }
529
530 /**
531 * Check that if the source contact is deleted but we can view the others we can see the activity.
532 *
533 * CRM-18409.
534 *
535 * @throws \CRM_Core_Exception
536 */
537 public function testGetActivityACLSourceContactDeleted() {
538 $this->setPermissions(array('access CiviCRM', 'delete contacts'));
539 $activity = $this->activityCreate();
540 $contacts = $this->getActivityContacts($activity);
541
542 $this->hookClass->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookAllResults'));
543 $this->contactDelete($contacts['source_contact_id']);
544 $this->callAPISuccess('Activity', 'getsingle', array('check_permissions' => 1, 'id' => $activity['id']));
545 }
546
547 /**
548 * Get the contacts for the activity.
549 *
550 * @param $activity
551 *
552 * @return array
553 * @throws \CRM_Core_Exception
554 */
555 protected function getActivityContacts($activity) {
556 $contacts = array();
557
558 $activityContacts = $this->callAPISuccess('ActivityContact', 'get', array(
559 'activity_id' => $activity['id'],
560 )
561 );
562
563 $activityRecordTypes = $this->callAPISuccess('ActivityContact', 'getoptions', array('field' => 'record_type_id'));
564 foreach ($activityContacts['values'] as $activityContact) {
565 $type = $activityRecordTypes['values'][$activityContact['record_type_id']];
566 switch ($type) {
567 case 'Activity Source':
568 $contacts['source_contact_id'] = $activityContact['contact_id'];
569 break;
570
571 case 'Activity Targets':
572 $contacts['target_contact_id'] = $activityContact['contact_id'];
573 break;
574
575 case 'Activity Assignees':
576 $contacts['assignee_contact_id'] = $activityContact['contact_id'];
577 break;
578
579 }
580 }
581 return $contacts;
582 }
583
584 }