3 +--------------------------------------------------------------------+
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2018 |
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 +--------------------------------------------------------------------+
29 * This class is intended to test ACL permission using the multisite module
31 * @package CiviCRM_APIv3
32 * @subpackage API_Contact
35 class api_v3_ACLPermissionTest
extends CiviUnitTestCase
{
36 protected $_apiversion = 3;
37 public $DBResetRequired = FALSE;
39 protected $allowedContactId = 0;
40 protected $allowedContacts = array();
42 public function setUp() {
44 $baoObj = new CRM_Core_DAO();
45 $baoObj->createTestObject('CRM_Pledge_BAO_Pledge', array(), 1, 0);
46 $baoObj->createTestObject('CRM_Core_BAO_Phone', array(), 1, 0);
47 $this->prepareForACLs();
52 * @see CiviUnitTestCase::tearDown()
54 public function tearDown() {
55 $this->cleanUpAfterACLs();
56 $tablesToTruncate = array(
58 'civicrm_group_contact',
62 'civicrm_acl_entity_role',
63 'civicrm_acl_contact_cache',
64 'civicrm_contribution',
65 'civicrm_participant',
68 'civicrm_activity_contact',
73 $this->quickCleanup($tablesToTruncate);
77 * Function tests that an empty where hook returns no results.
79 public function testContactGetNoResultsHook() {
80 $this->hookClass
->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookNoResults'));
81 $result = $this->callAPISuccess('contact', 'get', array(
82 'check_permissions' => 1,
83 'return' => 'display_name',
85 $this->assertEquals(0, $result['count']);
89 * Function tests that an empty where hook returns exactly 1 result with "view my contact".
91 * CRM-16512 caused contacts with Edit my contact to be able to view all records.
93 public function testContactGetOneResultHookWithViewMyContact() {
94 $this->createLoggedInUser();
95 $this->hookClass
->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookNoResults'));
96 CRM_Core_Config
::singleton()->userPermissionClass
->permissions
= array('access CiviCRM', 'view my contact');
97 $result = $this->callAPISuccess('contact', 'get', array(
98 'check_permissions' => 1,
99 'return' => 'display_name',
101 $this->assertEquals(1, $result['count']);
105 * Function tests that a user with "edit my contact" can edit themselves.
107 public function testContactEditHookWithEditMyContact() {
108 $cid = $this->createLoggedInUser();
109 $this->hookClass
->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookNoResults'));
110 CRM_Core_Config
::singleton()->userPermissionClass
->permissions
= array('access CiviCRM', 'edit my contact');
111 $this->callAPISuccess('contact', 'create', array(
112 'check_permissions' => 1,
118 * Ensure contact permissions do not block contact-less location entities.
120 public function testAddressWithoutContactIDAccess() {
121 $ownID = $this->createLoggedInUser();
122 CRM_Core_Config
::singleton()->userPermissionClass
->permissions
= array('access CiviCRM', 'view all contacts');
123 $this->callAPISuccess('Address', 'create', array(
124 'city' => 'Mouseville',
125 'location_type_id' => 'Main',
126 'api.LocBlock.create' => 1,
127 'contact_id' => $ownID,
129 $this->callAPISuccessGetSingle('Address', array('city' => 'Mouseville', 'check_permissions' => 1));
130 CRM_Core_DAO
::executeQuery('UPDATE civicrm_address SET contact_id = NULL WHERE contact_id = %1', array(1 => array($ownID, 'Integer')));
131 $this->callAPISuccessGetSingle('Address', array('city' => 'Mouseville', 'check_permissions' => 1));
135 * Ensure contact permissions extend to related entities like email
137 public function testRelatedEntityPermissions() {
138 $this->createLoggedInUser();
139 $disallowedContact = $this->individualCreate(array(), 0);
140 $this->allowedContactId
= $this->individualCreate(array(), 1);
141 $this->hookClass
->setHook('civicrm_aclWhereClause', array($this, 'aclWhereOnlyOne'));
142 CRM_Core_Config
::singleton()->userPermissionClass
->permissions
= array('access CiviCRM');
143 $testEntities = array(
144 'Email' => array('email' => 'null@nothing', 'location_type_id' => 1),
145 'Phone' => array('phone' => '123456', 'location_type_id' => 1),
146 'IM' => array('name' => 'hello', 'location_type_id' => 1),
147 'Website' => array('url' => 'http://test'),
148 'Address' => array('street_address' => '123 Sesame St.', 'location_type_id' => 1),
150 foreach ($testEntities as $entity => $params) {
152 'contact_id' => $disallowedContact,
153 'check_permissions' => 1,
155 // We should be prevented from getting or creating entities for a contact we don't have permission for
156 $this->callAPIFailure($entity, 'create', $params);
157 $this->callAPISuccess($entity, 'create', array('check_permissions' => 0) +
$params);
158 $results = $this->callAPISuccess($entity, 'get', array('contact_id' => $disallowedContact, 'check_permissions' => 1));
159 $this->assertEquals(0, $results['count']);
161 // We should be allowed to create and get for contacts we do have permission on
162 $params['contact_id'] = $this->allowedContactId
;
163 $this->callAPISuccess($entity, 'create', $params);
164 $results = $this->callAPISuccess($entity, 'get', array('contact_id' => $this->allowedContactId
, 'check_permissions' => 1));
165 $this->assertGreaterThan(0, $results['count']);
167 $newTag = civicrm_api3('Tag', 'create', array(
170 $relatedEntities = array(
171 'Note' => array('note' => 'abc'),
172 'EntityTag' => array('tag_id' => $newTag['id']),
174 foreach ($relatedEntities as $entity => $params) {
176 'entity_id' => $disallowedContact,
177 'entity_table' => 'civicrm_contact',
178 'check_permissions' => 1,
180 // We should be prevented from getting or creating entities for a contact we don't have permission for
181 $this->callAPIFailure($entity, 'create', $params);
182 $this->callAPISuccess($entity, 'create', array('check_permissions' => 0) +
$params);
183 $results = $this->callAPISuccess($entity, 'get', array('entity_id' => $disallowedContact, 'entity_table' => 'civicrm_contact', 'check_permissions' => 1));
184 $this->assertEquals(0, $results['count']);
186 // We should be allowed to create and get for entities we do have permission on
187 $params['entity_id'] = $this->allowedContactId
;
188 $this->callAPISuccess($entity, 'create', $params);
189 $results = $this->callAPISuccess($entity, 'get', array('entity_id' => $this->allowedContactId
, 'entity_table' => 'civicrm_contact', 'check_permissions' => 1));
190 $this->assertGreaterThan(0, $results['count']);
195 * Function tests all results are returned.
197 public function testContactGetAllResultsHook() {
198 $this->hookClass
->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookAllResults'));
199 $result = $this->callAPISuccess('contact', 'get', array(
200 'check_permissions' => 1,
201 'return' => 'display_name',
204 $this->assertEquals(2, $result['count']);
208 * Function tests that deleted contacts are not returned.
210 public function testContactGetPermissionHookNoDeleted() {
211 $this->callAPISuccess('contact', 'create', array('id' => 2, 'is_deleted' => 1));
212 $this->hookClass
->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookAllResults'));
213 $result = $this->callAPISuccess('contact', 'get', array(
214 'check_permissions' => 1,
215 'return' => 'display_name',
217 $this->assertEquals(1, $result['count']);
221 * Test permissions limited by hook.
223 public function testContactGetHookLimitingHook() {
224 $this->hookClass
->setHook('civicrm_aclWhereClause', array($this, 'aclWhereOnlySecond'));
226 $result = $this->callAPISuccess('contact', 'get', array(
227 'check_permissions' => 1,
228 'return' => 'display_name',
230 $this->assertEquals(1, $result['count']);
234 * Confirm that without check permissions we still get 2 contacts returned.
236 public function testContactGetHookLimitingHookDontCheck() {
237 $result = $this->callAPISuccess('contact', 'get', array(
238 'check_permissions' => 0,
239 'return' => 'display_name',
241 $this->assertEquals(2, $result['count']);
245 * Check that id works as a filter.
247 public function testContactGetIDFilter() {
248 $this->hookClass
->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookAllResults'));
249 $result = $this->callAPISuccess('contact', 'get', array(
252 'check_permissions' => 1,
255 $this->assertEquals(1, $result['count']);
256 $this->assertEquals(2, $result['id']);
260 * Check that address IS returned.
262 public function testContactGetAddressReturned() {
263 $this->hookClass
->setHook('civicrm_aclWhereClause', array($this, 'aclWhereOnlySecond'));
264 $fullresult = $this->callAPISuccess('contact', 'get', array(
267 //return doesn't work for all keys - can't fix that here so let's skip ...
268 //prefix & suffix are inconsistent due to CRM-7929
269 // unsure about others but return doesn't work on them
270 $elementsReturnDoesntSupport = array(
281 $expectedReturnElements = array_diff(array_keys($fullresult['values'][0]), $elementsReturnDoesntSupport);
282 $result = $this->callAPISuccess('contact', 'get', array(
283 'check_permissions' => 1,
284 'return' => $expectedReturnElements,
287 $this->assertEquals(1, $result['count']);
288 foreach ($expectedReturnElements as $element) {
289 $this->assertArrayHasKey($element, $result['values'][0]);
294 * Check that pledge IS not returned.
296 public function testContactGetPledgeIDNotReturned() {
297 $this->hookClass
->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookAllResults'));
298 $this->callAPISuccess('contact', 'get', array(
301 $result = $this->callAPISuccess('contact', 'get', array(
302 'check_permissions' => 1,
303 'return' => 'pledge_id',
306 $this->assertArrayNotHasKey('pledge_id', $result['values'][0]);
310 * Check that pledge IS not an allowable filter.
312 public function testContactGetPledgeIDNotFiltered() {
313 $this->hookClass
->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookAllResults'));
314 $this->callAPISuccess('contact', 'get', array(
317 $result = $this->callAPISuccess('contact', 'get', array(
318 'check_permissions' => 1,
322 $this->assertEquals(2, $result['count']);
326 * Check that chaining doesn't bypass permissions
328 public function testContactGetPledgeNotChainable() {
329 $this->hookClass
->setHook('civicrm_aclWhereClause', array($this, 'aclWhereOnlySecond'));
330 $this->callAPISuccess('contact', 'get', array(
333 $this->callAPIFailure('contact', 'get', array(
334 'check_permissions' => 1,
335 'api.pledge.get' => 1,
338 'Error in call to Pledge_get : API permission check failed for Pledge/get call; insufficient permission: require access CiviCRM and access CiviPledge'
342 public function setupCoreACL() {
343 $this->createLoggedInUser();
344 $this->_permissionedDisabledGroup
= $this->groupCreate(array(
345 'title' => 'pick-me-disabled',
347 'name' => 'pick-me-disabled',
349 $this->_permissionedGroup
= $this->groupCreate(array(
350 'title' => 'pick-me-active',
352 'name' => 'pick-me-active',
358 * @dataProvider entities
359 * confirm that without check permissions we still get 2 contacts returned
362 public function testEntitiesGetHookLimitingHookNoCheck($entity) {
363 CRM_Core_Config
::singleton()->userPermissionClass
->permissions
= array();
364 $this->setUpEntities($entity);
365 $this->hookClass
->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookNoResults'));
366 $result = $this->callAPISuccess($entity, 'get', array(
367 'check_permissions' => 0,
368 'return' => 'contact_id',
370 $this->assertEquals(2, $result['count']);
374 * @dataProvider entities
375 * confirm that without check permissions we still get 2 entities returned
378 public function testEntitiesGetCoreACLLimitingHookNoCheck($entity) {
379 $this->setupCoreACL();
380 //CRM_Core_Config::singleton()->userPermissionClass->permissions = array();
381 $this->setUpEntities($entity);
382 $this->hookClass
->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookNoResults'));
383 $result = $this->callAPISuccess($entity, 'get', array(
384 'check_permissions' => 0,
385 'return' => 'contact_id',
387 $this->assertEquals(2, $result['count']);
391 * @dataProvider entities
392 * confirm that with check permissions we don't get entities
394 * @throws \PHPUnit_Framework_IncompleteTestError
396 public function testEntitiesGetCoreACLLimitingCheck($entity) {
397 $this->setupCoreACL();
398 $this->setUpEntities($entity);
399 $result = $this->callAPISuccess($entity, 'get', array(
400 'check_permissions' => 1,
401 'return' => 'contact_id',
403 $this->assertEquals(0, $result['count']);
407 * @dataProvider entities
408 * Function tests that an empty where hook returns no results
409 * @param string $entity
410 * @throws \PHPUnit_Framework_IncompleteTestError
412 public function testEntityGetNoResultsHook($entity) {
413 $this->markTestIncomplete('hook acls only work with contacts so far');
414 CRM_Core_Config
::singleton()->userPermissionClass
->permissions
= array();
415 $this->setUpEntities($entity);
416 $this->hookClass
->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookNoResults'));
417 $result = $this->callAPISuccess($entity, 'get', array(
418 'check_permission' => 1,
420 $this->assertEquals(0, $result['count']);
426 public static function entities() {
427 return array(array('contribution'), array('participant'));// @todo array('pledge' => 'pledge')
434 public function setUpEntities($entity) {
435 $baoObj = new CRM_Core_DAO();
436 $baoObj->createTestObject(_civicrm_api3_get_BAO($entity), array(), 2, 0);
437 CRM_Core_Config
::singleton()->userPermissionClass
->permissions
= array(
439 'access CiviContribute',
441 'view event participants',
446 * All results returned.
448 * @implements CRM_Utils_Hook::aclWhereClause
450 * @param string $type
451 * @param array $tables
452 * @param array $whereTables
453 * @param int $contactID
454 * @param string $where
456 public function aclWhereHookAllResults($type, &$tables, &$whereTables, &$contactID, &$where) {
461 * All but first results returned.
462 * @implements CRM_Utils_Hook::aclWhereClause
465 * @param $whereTables
469 public function aclWhereOnlySecond($type, &$tables, &$whereTables, &$contactID, &$where) {
470 $where = " contact_a.id > 1";
474 * Only specified contact returned.
475 * @implements CRM_Utils_Hook::aclWhereClause
478 * @param $whereTables
482 public function aclWhereOnlyOne($type, &$tables, &$whereTables, &$contactID, &$where) {
483 $where = " contact_a.id = " . $this->allowedContactId
;
487 * Basic check that an unpermissioned call keeps working and permissioned call fails.
489 public function testGetActivityNoPermissions() {
490 $this->setPermissions(array());
491 $this->callAPISuccess('Activity', 'get', array());
492 $this->callAPIFailure('Activity', 'get', array('check_permissions' => 1));
496 * View all activities is enough regardless of contact ACLs.
498 public function testGetActivityViewAllActivitiesEnoughWithOrWithoutID() {
499 $activity = $this->activityCreate();
500 $this->setPermissions(array('view all activities', 'access CiviCRM'));
501 $this->callAPISuccess('Activity', 'getsingle', array('check_permissions' => 1, 'id' => $activity['id']));
502 $this->callAPISuccess('Activity', 'getsingle', array('check_permissions' => 1));
506 * View all activities is required unless id is passed in.
508 public function testGetActivityViewAllContactsEnoughWIthoutID() {
509 $this->setPermissions(array('view all contacts', 'access CiviCRM'));
510 $this->callAPISuccess('Activity', 'get', array('check_permissions' => 1));
514 * View all activities is required unless id is passed in, in which case ACLs are used.
516 public function testGetActivityViewAllContactsEnoughWIthID() {
517 $activity = $this->activityCreate();
518 $this->setPermissions(array('view all contacts', 'access CiviCRM'));
519 $this->callAPISuccess('Activity', 'getsingle', array('check_permissions' => 1, 'id' => $activity['id']));
523 * View all activities is required unless id is passed in, in which case ACLs are used.
525 public function testGetActivityAccessCiviCRMNotEnough() {
526 $activity = $this->activityCreate();
527 $this->setPermissions(array('access CiviCRM'));
528 $this->callAPIFailure('Activity', 'getsingle', array('check_permissions' => 1, 'id' => $activity['id']));
532 * Check that activities can be retrieved by ACL.
534 * The activities api applies ACLs in a very limited circumstance, if id is passed in.
535 * Otherwise it sticks with the blunt original permissions.
537 public function testGetActivityByACL() {
538 $this->setPermissions(array('access CiviCRM'));
539 $activity = $this->activityCreate();
541 $this->hookClass
->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookAllResults'));
542 $this->callAPISuccess('Activity', 'getsingle', array('check_permissions' => 1, 'id' => $activity['id']));
546 * To leverage ACL permission to view an activity you must be able to see all of the contacts.
548 public function testGetActivityByAclCannotViewAllContacts() {
549 $activity = $this->activityCreate();
550 $contacts = $this->getActivityContacts($activity);
551 $this->setPermissions(array('access CiviCRM'));
553 foreach ($contacts as $contact_id) {
554 $this->allowedContactId
= $contact_id;
555 $this->hookClass
->setHook('civicrm_aclWhereClause', array($this, 'aclWhereOnlyOne'));
556 $this->callAPIFailure('Activity', 'getsingle', array('check_permissions' => 1, 'id' => $activity['id']));
561 * Check that if the source contact is deleted but we can view the others we can see the activity.
565 * @throws \CRM_Core_Exception
567 public function testGetActivityACLSourceContactDeleted() {
568 $this->setPermissions(array('access CiviCRM', 'delete contacts'));
569 $activity = $this->activityCreate();
570 $contacts = $this->getActivityContacts($activity);
572 $this->hookClass
->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookAllResults'));
573 $this->contactDelete($contacts['source_contact_id']);
574 $this->callAPISuccess('Activity', 'getsingle', array('check_permissions' => 1, 'id' => $activity['id']));
578 * Test get activities multiple ids with check permissions
581 public function testActivitiesGetMultipleIdsCheckPermissions() {
582 $this->createLoggedInUser();
583 $activity = $this->activityCreate();
584 $activity2 = $this->activityCreate();
585 $this->setPermissions(array('access CiviCRM'));
586 $this->hookClass
->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookAllResults'));
587 // Get activities associated with contact $this->_contactID.
589 'id' => array('IN' => array($activity['id'], $activity2['id'])),
590 'check_permissions' => TRUE,
592 $result = $this->callAPISuccess('activity', 'get', $params);
593 $this->assertEquals(2, $result['count']);
597 * Test get activities multiple ids with check permissions
598 * Limit access to One contact
601 public function testActivitiesGetMultipleIdsCheckPermissionsLimitedACL() {
602 $this->createLoggedInUser();
603 $activity = $this->activityCreate();
604 $contacts = $this->getActivityContacts($activity);
605 $this->setPermissions(array('access CiviCRM'));
606 foreach ($contacts as $contact_id) {
607 $this->allowedContacts
[] = $contact_id;
609 $this->hookClass
->setHook('civicrm_aclWhereClause', array($this, 'aclWhereMultipleContacts'));
610 $contact2 = $this->individualCreate();
611 $activity2 = $this->activityCreate(array('source_contact_id' => $contact2));
612 // Get activities associated with contact $this->_contactID.
614 'id' => array('IN' => array($activity['id'])),
615 'check_permissions' => TRUE,
617 $result = $this->callAPISuccess('activity', 'get', $params);
618 $this->assertEquals(1, $result['count']);
619 $this->callAPIFailure('activity', 'get', array_merge($params, array('id' => array('IN', array($activity2['id'])))));
623 * Test get activities multiple ids with check permissions
626 public function testActivitiesGetMultipleIdsCheckPermissionsNotIN() {
627 $this->createLoggedInUser();
628 $activity = $this->activityCreate();
629 $activity2 = $this->activityCreate();
630 $this->setPermissions(array('access CiviCRM'));
631 $this->hookClass
->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookAllResults'));
632 // Get activities associated with contact $this->_contactID.
634 'id' => array('NOT IN' => array($activity['id'], $activity2['id'])),
635 'check_permissions' => TRUE,
637 $result = $this->callAPISuccess('activity', 'get', $params);
638 $this->assertEquals(0, $result['count']);
642 * Get the contacts for the activity.
647 * @throws \CRM_Core_Exception
649 protected function getActivityContacts($activity) {
652 $activityContacts = $this->callAPISuccess('ActivityContact', 'get', array(
653 'activity_id' => $activity['id'],
657 $activityRecordTypes = $this->callAPISuccess('ActivityContact', 'getoptions', array('field' => 'record_type_id'));
658 foreach ($activityContacts['values'] as $activityContact) {
659 $type = $activityRecordTypes['values'][$activityContact['record_type_id']];
661 case 'Activity Source':
662 $contacts['source_contact_id'] = $activityContact['contact_id'];
665 case 'Activity Targets':
666 $contacts['target_contact_id'] = $activityContact['contact_id'];
669 case 'Activity Assignees':
670 $contacts['assignee_contact_id'] = $activityContact['contact_id'];