APIv4 - Deprecate and stop using PreSaveSubscriber
[civicrm-core.git] / tests / phpunit / api / v3 / ACLPermissionTest.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 use Civi\Api4\Contact;
13 use Civi\Api4\CustomField;
14 use Civi\Api4\CustomGroup;
15 use Civi\Api4\CustomValue;
16
17 /**
18 * This class is intended to test ACL permission using the multisite module
19 *
20 * @package CiviCRM_APIv3
21 * @subpackage API_Contact
22 * @group headless
23 */
24 class api_v3_ACLPermissionTest extends CiviUnitTestCase {
25
26 use Civi\Test\ACLPermissionTrait;
27
28 /**
29 * Should financials be checked after the test but before tear down.
30 *
31 * The setup methodology in this class bypasses valid financial creation
32 * so we don't check.
33 *
34 * @var bool
35 */
36 protected $isValidateFinancialsOnPostAssert = FALSE;
37
38 public $DBResetRequired = FALSE;
39 protected $_entity;
40
41 public function setUp(): void {
42 parent::setUp();
43 CRM_Core_DAO::createTestObject('CRM_Pledge_BAO_Pledge', [], 1, 0);
44 $this->callAPISuccess('Phone', 'create', ['id' => $this->individualCreate(['email' => '']), 'phone' => '911', 'location_type_id' => 'Home']);
45 $this->prepareForACLs();
46 }
47
48 /**
49 * (non-PHPdoc)
50 * @see CiviUnitTestCase::tearDown()
51 */
52 public function tearDown(): void {
53 $this->cleanUpAfterACLs();
54 $tablesToTruncate = [
55 'civicrm_contact',
56 'civicrm_address',
57 'civicrm_group_contact',
58 'civicrm_group',
59 'civicrm_acl',
60 'civicrm_acl_cache',
61 'civicrm_acl_entity_role',
62 'civicrm_acl_contact_cache',
63 'civicrm_contribution',
64 'civicrm_line_item',
65 'civicrm_participant',
66 'civicrm_uf_match',
67 'civicrm_activity',
68 'civicrm_activity_contact',
69 'civicrm_note',
70 'civicrm_entity_tag',
71 'civicrm_tag',
72 'civicrm_membership',
73 ];
74 $this->quickCleanup($tablesToTruncate);
75 }
76
77 /**
78 * Function tests that an empty where hook returns no results.
79 *
80 * @param int $version
81 *
82 * @dataProvider versionThreeAndFour
83 */
84 public function testContactGetNoResultsHook(int $version): void {
85 $this->_apiversion = $version;
86 $this->hookClass->setHook('civicrm_aclWhereClause', [
87 $this,
88 'aclWhereHookNoResults',
89 ]);
90 $result = $this->callAPISuccess('contact', 'get', [
91 'check_permissions' => 1,
92 'return' => 'display_name',
93 ]);
94 $this->assertEquals(0, $result['count']);
95 }
96
97 /**
98 * Function tests that an empty where hook returns exactly 1 result with "view my contact".
99 *
100 * CRM-16512 caused contacts with Edit my contact to be able to view all records.
101 *
102 * @param int $version
103 *
104 * @dataProvider versionThreeAndFour
105 */
106 public function testContactGetOneResultHookWithViewMyContact(int $version): void {
107 $this->_apiversion = $version;
108 $this->createLoggedInUser();
109 $this->hookClass->setHook('civicrm_aclWhereClause', [
110 $this,
111 'aclWhereHookNoResults',
112 ]);
113 CRM_Core_Config::singleton()->userPermissionClass->permissions = [
114 'access CiviCRM',
115 'view my contact',
116 ];
117 $result = $this->callAPISuccess('contact', 'get', [
118 'check_permissions' => 1,
119 'return' => 'display_name',
120 ]);
121 $this->assertEquals(1, $result['count']);
122 }
123
124 /**
125 * Function tests that a user with "edit my contact" can edit themselves.
126 *
127 * @param int $version
128 *
129 * @dataProvider versionThreeAndFour
130 */
131 public function testContactEditHookWithEditMyContact(int $version): void {
132 $this->_apiversion = $version;
133 $cid = $this->createLoggedInUser();
134 $this->hookClass->setHook('civicrm_aclWhereClause', [
135 $this,
136 'aclWhereHookNoResults',
137 ]);
138 CRM_Core_Config::singleton()->userPermissionClass->permissions = [
139 'access CiviCRM',
140 'edit my contact',
141 ];
142 $this->callAPISuccess('contact', 'create', [
143 'check_permissions' => 1,
144 'id' => $cid,
145 'first_name' => 'NewName',
146 ]);
147 }
148
149 /**
150 * Ensure contact permissions do not block contact-less location entities.
151 *
152 * @param int $version
153 *
154 * @dataProvider versionThreeAndFour
155 */
156 public function testAddressWithoutContactIDAccess(int $version): void {
157 $this->_apiversion = $version;
158 $ownID = $this->createLoggedInUser();
159 CRM_Core_Config::singleton()->userPermissionClass->permissions = [
160 'access CiviCRM',
161 'view all contacts',
162 ];
163 $this->callAPISuccess('Address', 'create', [
164 'city' => 'Mouseville',
165 'location_type_id' => 'Main',
166 'api.LocBlock.create' => 1,
167 'contact_id' => $ownID,
168 ]);
169 $this->callAPISuccessGetSingle('Address', [
170 'city' => 'Mouseville',
171 'check_permissions' => 1,
172 ]);
173 CRM_Core_DAO::executeQuery('UPDATE civicrm_address SET contact_id = NULL WHERE contact_id = %1', [
174 1 => [
175 $ownID,
176 'Integer',
177 ],
178 ]);
179 $this->callAPISuccessGetSingle('Address', [
180 'city' => 'Mouseville',
181 'check_permissions' => 1,
182 ]);
183 }
184
185 /**
186 * Ensure contact permissions extend to related entities like email
187 *
188 * @param int $version
189 *
190 * @throws \CiviCRM_API3_Exception
191 * @dataProvider versionThreeAndFour
192 */
193 public function testRelatedEntityPermissions(int $version): void {
194 $this->_apiversion = $version;
195 $this->createLoggedInUser();
196 $disallowedContact = $this->individualCreate([]);
197 $this->allowedContactId = $this->individualCreate([], 1);
198 $this->hookClass->setHook('civicrm_aclWhereClause', [
199 $this,
200 'aclWhereOnlyOne',
201 ]);
202 CRM_Core_Config::singleton()->userPermissionClass->permissions = ['access CiviCRM'];
203 $testEntities = [
204 'Email' => ['email' => 'null@nothing', 'location_type_id' => 1],
205 'Phone' => ['phone' => '123456', 'location_type_id' => 1],
206 'IM' => ['name' => 'hello', 'location_type_id' => 1],
207 'Website' => ['url' => 'http://test'],
208 'Address' => [
209 'street_address' => '123 Sesame St.',
210 'location_type_id' => 1,
211 ],
212 ];
213 foreach ($testEntities as $entity => $params) {
214 $params += [
215 'contact_id' => $disallowedContact,
216 'check_permissions' => 1,
217 ];
218 // We should be prevented from getting or creating entities for a contact we don't have permission for
219 $this->callAPIFailure($entity, 'create', $params);
220 $this->callAPISuccess($entity, 'create', ['check_permissions' => 0] + $params);
221 $results = $this->callAPISuccess($entity, 'get', [
222 'contact_id' => $disallowedContact,
223 'check_permissions' => 1,
224 ]);
225 $this->assertEquals(0, $results['count']);
226
227 // We should be allowed to create and get for contacts we do have permission on
228 $params['contact_id'] = $this->allowedContactId;
229 $this->callAPISuccess($entity, 'create', $params);
230 $results = $this->callAPISuccess($entity, 'get', [
231 'contact_id' => $this->allowedContactId,
232 'check_permissions' => 1,
233 ]);
234 $this->assertGreaterThan(0, $results['count']);
235 }
236 $newTag = civicrm_api3('Tag', 'create', [
237 'name' => 'Foo123',
238 ]);
239 $relatedEntities = [
240 'Note' => ['note' => 'abc'],
241 'EntityTag' => ['tag_id' => $newTag['id']],
242 ];
243 foreach ($relatedEntities as $entity => $params) {
244 $params += [
245 'entity_id' => $disallowedContact,
246 'entity_table' => 'civicrm_contact',
247 'check_permissions' => 1,
248 ];
249 // We should be prevented from getting or creating entities for a contact we don't have permission for
250 $this->callAPIFailure($entity, 'create', $params);
251 $this->callAPISuccess($entity, 'create', ['check_permissions' => 0] + $params);
252 $results = $this->callAPISuccess($entity, 'get', [
253 'entity_id' => $disallowedContact,
254 'entity_table' => 'civicrm_contact',
255 'check_permissions' => 1,
256 ]);
257 $this->assertEquals(0, $results['count']);
258
259 // We should be allowed to create and get for entities we do have permission on
260 $params['entity_id'] = $this->allowedContactId;
261 $this->callAPISuccess($entity, 'create', $params);
262 $results = $this->callAPISuccess($entity, 'get', [
263 'entity_id' => $this->allowedContactId,
264 'entity_table' => 'civicrm_contact',
265 'check_permissions' => 1,
266 ]);
267 $this->assertGreaterThan(0, $results['count']);
268 }
269 }
270
271 /**
272 * Function tests all results are returned.
273 *
274 * @param int $version
275 *
276 * @dataProvider versionThreeAndFour
277 */
278 public function testContactGetAllResultsHook(int $version): void {
279 $this->_apiversion = $version;
280 $this->hookClass->setHook('civicrm_aclWhereClause', [
281 $this,
282 'aclWhereHookAllResults',
283 ]);
284 $result = $this->callAPISuccess('contact', 'get', [
285 'check_permissions' => 1,
286 'return' => 'display_name',
287 ]);
288
289 $this->assertEquals(2, $result['count']);
290 }
291
292 /**
293 * Function tests that deleted contacts are not returned.
294 *
295 * @param int $version
296 *
297 * @dataProvider versionThreeAndFour
298 */
299 public function testContactGetPermissionHookNoDeleted(int $version): void {
300 $this->_apiversion = $version;
301 $this->callAPISuccess('contact', 'create', ['id' => 2, 'is_deleted' => 1]);
302 $this->hookClass->setHook('civicrm_aclWhereClause', [
303 $this,
304 'aclWhereHookAllResults',
305 ]);
306 $result = $this->callAPISuccess('contact', 'get', [
307 'check_permissions' => 1,
308 'return' => 'display_name',
309 ]);
310 $this->assertEquals(1, $result['count']);
311 }
312
313 /**
314 * Test permissions limited by hook.
315 *
316 * @param int $version
317 *
318 * @dataProvider versionThreeAndFour
319 */
320 public function testContactGetHookLimitingHook(int $version): void {
321 $this->_apiversion = $version;
322 $this->hookClass->setHook('civicrm_aclWhereClause', [
323 $this,
324 'aclWhereOnlySecond',
325 ]);
326
327 $result = $this->callAPISuccess('contact', 'get', [
328 'check_permissions' => 1,
329 'return' => 'display_name',
330 ]);
331 $this->assertEquals(1, $result['count']);
332 }
333
334 /**
335 * Confirm that without check permissions we still get 2 contacts returned.
336 *
337 * @param int $version
338 *
339 * @dataProvider versionThreeAndFour
340 */
341 public function testContactGetHookLimitingHookDontCheck(int $version): void {
342 $this->_apiversion = $version;
343 $result = $this->callAPISuccess('contact', 'get', [
344 'check_permissions' => 0,
345 'return' => 'display_name',
346 ]);
347 $this->assertEquals(2, $result['count']);
348 }
349
350 /**
351 * Check that id works as a filter.
352 *
353 * @param int $version
354 *
355 * @dataProvider versionThreeAndFour
356 */
357 public function testContactGetIDFilter(int $version): void {
358 $this->_apiversion = $version;
359 $this->hookClass->setHook('civicrm_aclWhereClause', [
360 $this,
361 'aclWhereHookAllResults',
362 ]);
363 $result = $this->callAPISuccess('contact', 'get', [
364 'sequential' => 1,
365 'id' => 2,
366 'check_permissions' => 1,
367 ]);
368
369 $this->assertEquals(1, $result['count']);
370 $this->assertEquals(2, $result['id']);
371 }
372
373 /**
374 * Check that address IS returned.
375 */
376 public function testContactGetAddressReturned(): void {
377 $this->hookClass->setHook('civicrm_aclWhereClause', [
378 $this,
379 'aclWhereOnlySecond',
380 ]);
381 $fullresult = $this->callAPISuccess('contact', 'get', [
382 'sequential' => 1,
383 ]);
384 //return doesn't work for all keys - can't fix that here so let's skip ...
385 //prefix & suffix are inconsistent due to CRM-7929
386 // unsure about others but return doesn't work on them
387 $elementsReturnDoesntSupport = [
388 'prefix',
389 'suffix',
390 'gender',
391 'current_employer',
392 'phone_id',
393 'phone_type_id',
394 'phone',
395 'worldregion_id',
396 'world_region',
397 ];
398 $expectedReturnElements = array_diff(array_keys($fullresult['values'][0]), $elementsReturnDoesntSupport);
399 $result = $this->callAPISuccess('contact', 'get', [
400 'check_permissions' => 1,
401 'return' => $expectedReturnElements,
402 'sequential' => 1,
403 ]);
404 $this->assertEquals(1, $result['count']);
405 foreach ($expectedReturnElements as $element) {
406 $this->assertArrayHasKey($element, $result['values'][0]);
407 }
408 }
409
410 /**
411 * Check that pledge IS not returned.
412 *
413 * @param int $version
414 *
415 * @dataProvider versionThreeAndFour
416 */
417 public function testContactGetPledgeIDNotReturned(int $version): void {
418 $this->_apiversion = $version;
419 $this->hookClass->setHook('civicrm_aclWhereClause', [
420 $this,
421 'aclWhereHookAllResults',
422 ]);
423 $this->callAPISuccess('contact', 'get', [
424 'sequential' => 1,
425 ]);
426 $result = $this->callAPISuccess('contact', 'get', [
427 'check_permissions' => 1,
428 'return' => 'pledge_id',
429 'sequential' => 1,
430 ]);
431 $this->assertArrayNotHasKey('pledge_id', $result['values'][0]);
432 }
433
434 /**
435 * Check that pledge IS not an allowable filter.
436 */
437 public function testContactGetPledgeIDNotFiltered(): void {
438 $this->hookClass->setHook('civicrm_aclWhereClause', [
439 $this,
440 'aclWhereHookAllResults',
441 ]);
442 $this->callAPISuccess('contact', 'get', [
443 'sequential' => 1,
444 ]);
445 $result = $this->callAPISuccess('contact', 'get', [
446 'check_permissions' => 1,
447 'pledge_id' => 1,
448 'sequential' => 1,
449 ]);
450 $this->assertEquals(2, $result['count']);
451 }
452
453 /**
454 * Check that chaining doesn't bypass permissions
455 *
456 * @param int $version
457 *
458 * @dataProvider versionThreeAndFour
459 */
460 public function testContactGetPledgeNotChainable(int $version): void {
461 $this->_apiversion = $version;
462 $this->hookClass->setHook('civicrm_aclWhereClause', [
463 $this,
464 'aclWhereOnlySecond',
465 ]);
466 $this->callAPISuccess('contact', 'get', [
467 'sequential' => 1,
468 ]);
469 $this->callAPIFailure('contact', 'get', [
470 'check_permissions' => 1,
471 'api.pledge.get' => 1,
472 'sequential' => 1,
473 ],
474 'Error in call to Pledge_get : API permission check failed for Pledge/get call; insufficient permission: require access CiviCRM and access CiviPledge'
475 );
476 }
477
478 public function setupCoreACL(): void {
479 $this->createLoggedInUser();
480 $this->_permissionedDisabledGroup = $this->groupCreate([
481 'title' => 'pick-me-disabled',
482 'is_active' => 0,
483 'name' => 'pick-me-disabled',
484 ]);
485 $this->_permissionedGroup = $this->groupCreate([
486 'title' => 'pick-me-active',
487 'is_active' => 1,
488 'name' => 'pick-me-active',
489 ]);
490 $this->setupACL();
491 }
492
493 /**
494 * @dataProvider entities
495 * confirm that without check permissions we still get 2 contacts returned
496 *
497 * @param string $entity
498 * @param int $apiVersion
499 */
500 public function testEntitiesGetHookLimitingHookNoCheck(string $entity, int $apiVersion): void {
501 $this->_apiversion = $apiVersion;
502 CRM_Core_Config::singleton()->userPermissionClass->permissions = [];
503 $this->setUpEntities($entity);
504 $this->hookClass->setHook('civicrm_aclWhereClause', [
505 $this,
506 'aclWhereHookNoResults',
507 ]);
508 $result = $this->callAPISuccess($entity, 'get', [
509 'check_permissions' => 0,
510 'return' => 'contact_id',
511 ]);
512 $this->assertEquals(2, $result['count'], "failed with entity : $entity and api version $apiVersion");
513 }
514
515 /**
516 * @dataProvider entities
517 * confirm that without check permissions we still get 2 entities returned
518 *
519 * @param string $entity
520 * @param int $apiVersion
521 */
522 public function testEntitiesGetCoreACLLimitingHookNoCheck(string $entity, int $apiVersion): void {
523 $this->_apiversion = $apiVersion;
524 $this->setupCoreACL();
525 //CRM_Core_Config::singleton()->userPermissionClass->permissions = array();
526 $this->setUpEntities($entity);
527 $this->hookClass->setHook('civicrm_aclWhereClause', [
528 $this,
529 'aclWhereHookNoResults',
530 ]);
531 $result = $this->callAPISuccess($entity, 'get', [
532 'check_permissions' => 0,
533 'return' => 'contact_id',
534 ]);
535 $this->assertEquals(2, $result['count'], "failed with entity : $entity and api version $apiVersion");
536 }
537
538 /**
539 * @dataProvider entities
540 * confirm that with check permissions we don't get entities
541 *
542 * @param $entity
543 * @param $apiVersion
544 */
545 public function testEntitiesGetCoreACLLimitingCheck($entity, $apiVersion): void {
546 $this->_apiversion = $apiVersion;
547 $this->setupCoreACL();
548 $this->setUpEntities($entity);
549 $result = $this->callAPISuccess($entity, 'get', [
550 'check_permissions' => 1,
551 'return' => 'contact_id',
552 ]);
553 $this->assertEquals(0, $result['count'], "failed with entity : $entity and api version $apiVersion");
554 }
555
556 /**
557 * @dataProvider entities
558 * Function tests that an empty where hook returns no results
559 *
560 * @param string $entity
561 * @param int $apiVersion
562 */
563 public function testEntityGetNoResultsHook(string $entity, int $apiVersion): void {
564 $this->_apiversion = $apiVersion;
565 $this->markTestIncomplete('hook acls only work with contacts so far');
566 CRM_Core_Config::singleton()->userPermissionClass->permissions = [];
567 $this->setUpEntities($entity);
568 $this->hookClass->setHook('civicrm_aclWhereClause', [
569 $this,
570 'aclWhereHookNoResults',
571 ]);
572 $result = $this->callAPISuccess($entity, 'get', [
573 'check_permission' => 1,
574 ]);
575 $this->assertEquals(0, $result['count']);
576 }
577
578 /**
579 * @return array
580 */
581 public static function entities(): array {
582 return [
583 ['contribution', 3],
584 ['participant', 3],
585 ];
586 }
587
588 /**
589 * Create 2 entities.
590 *
591 * @param string $entity
592 */
593 public function setUpEntities(string $entity): void {
594 CRM_Core_DAO::createTestObject(_civicrm_api3_get_BAO($entity), [], 2, 0);
595 CRM_Core_Config::singleton()->userPermissionClass->permissions = [
596 'access CiviCRM',
597 'access CiviContribute',
598 'access CiviEvent',
599 'view event participants',
600 'access CiviMember',
601 ];
602 }
603
604 /**
605 * Basic check that an un-permissioned call keeps working and permissioned call fails.
606 *
607 * @param int $version
608 *
609 * @dataProvider versionThreeAndFour
610 */
611 public function testGetActivityNoPermissions(int $version): void {
612 $this->_apiversion = $version;
613 $this->setPermissions([]);
614 $this->callAPISuccess('Activity', 'get');
615 $this->callAPIFailure('Activity', 'get', ['check_permissions' => 1]);
616 }
617
618 /**
619 * View all activities is enough regardless of contact ACLs.
620 *
621 * @param int $version
622 *
623 * @throws \CRM_Core_Exception
624 * @throws \CiviCRM_API3_Exception
625 * @dataProvider versionThreeAndFour
626 */
627 public function testGetActivityViewAllActivitiesDoesntCutItAnymore(int $version): void {
628 $this->_apiversion = $version;
629 $activity = $this->activityCreate();
630 $this->setPermissions(['view all activities', 'access CiviCRM']);
631 $this->callAPISuccessGetCount('Activity', [
632 'check_permissions' => 1,
633 'id' => $activity['id'],
634 ], 0);
635 }
636
637 /**
638 * View all activities is required unless id is passed in.
639 *
640 * @param int $version
641 *
642 * @dataProvider versionThreeAndFour
643 */
644 public function testGetActivityViewAllContactsEnoughWithoutID(int $version): void {
645 $this->_apiversion = $version;
646 $this->setPermissions(['view all contacts', 'access CiviCRM']);
647 $this->callAPISuccess('Activity', 'get', ['check_permissions' => 1]);
648 }
649
650 /**
651 * Without view all activities contact level acls are used.
652 *
653 * @param int $version
654 *
655 * @throws \CRM_Core_Exception
656 * @throws \CiviCRM_API3_Exception
657 * @dataProvider versionThreeAndFour
658 */
659 public function testGetActivityViewAllContactsEnoughWIthID(int $version): void {
660 $this->_apiversion = $version;
661 $activity = $this->activityCreate();
662 $this->setPermissions(['view all contacts', 'access CiviCRM']);
663 $this->callAPISuccess('Activity', 'getsingle', [
664 'check_permissions' => 1,
665 'id' => $activity['id'],
666 ]);
667 }
668
669 /**
670 * Check the error message is not a permission error.
671 *
672 * @param int $version
673 *
674 * @throws \CRM_Core_Exception
675 * @throws \CiviCRM_API3_Exception
676 * @dataProvider versionThreeAndFour
677 */
678 public function testGetActivityAccessCiviCRMEnough(int $version): void {
679 $this->_apiversion = $version;
680 $activity = $this->activityCreate();
681 $this->setPermissions(['access CiviCRM']);
682 $this->callAPIFailure('Activity', 'getsingle', [
683 'check_permissions' => 1,
684 'id' => $activity['id'],
685 'return' => 'id',
686 ], 'Expected one Activity but found 0');
687 $this->callAPISuccessGetCount('Activity', [
688 'check_permissions' => 1,
689 'id' => $activity['id'],
690 ], 0);
691 }
692
693 /**
694 * Check that component related activity filtering.
695 *
696 * If the contact does NOT have permission to 'view all contacts' but they DO have permission
697 * to view the contact in question they will only see the activities of components they have access too.
698 *
699 * (logically the same component limit should apply when they have access to view all too but....
700 * adding test for 'how it is at the moment.)
701 *
702 * @param int $version
703 *
704 * @throws \CRM_Core_Exception
705 * @throws \CiviCRM_API3_Exception
706 * @dataProvider versionThreeAndFour
707 */
708 public function testGetActivityCheckPermissionsByComponent(int $version): void {
709 $this->_apiversion = $version;
710 $activity = $this->activityCreate(['activity_type_id' => 'Contribution']);
711 $activity2 = $this->activityCreate(['activity_type_id' => 'Pledge Reminder']);
712 $this->hookClass->setHook('civicrm_aclWhereClause', [
713 $this,
714 'aclWhereHookAllResults',
715 ]);
716 $this->setPermissions(['access CiviCRM', 'access CiviContribute']);
717 $this->callAPISuccessGetCount('Activity', [
718 'check_permissions' => 1,
719 'id' => ['IN' => [$activity['id'], $activity2['id']]],
720 ], 1);
721 $this->callAPISuccessGetCount('Activity', [
722 'check_permissions' => 1,
723 'id' => ['IN' => [$activity['id'], $activity2['id']]],
724 ], 1);
725 }
726
727 /**
728 * Check that component related activity filtering works for CiviCase.
729 *
730 * @param int $version
731 *
732 * @throws \CRM_Core_Exception
733 * @throws \CiviCRM_API3_Exception
734 * @dataProvider versionThreeAndFour
735 */
736 public function testGetActivityCheckPermissionsByCaseComponent(int $version): void {
737 $this->_apiversion = $version;
738 CRM_Core_BAO_ConfigSetting::enableComponent('CiviCase');
739 $activity = $this->activityCreate(['activity_type_id' => 'Open Case']);
740 $activity2 = $this->activityCreate(['activity_type_id' => 'Pledge Reminder']);
741 $this->hookClass->setHook('civicrm_aclWhereClause', [
742 $this,
743 'aclWhereHookAllResults',
744 ]);
745 $this->setPermissions([
746 'access CiviCRM',
747 'access CiviContribute',
748 'access all cases and activities',
749 ]);
750 $this->callAPISuccessGetCount('Activity', [
751 'check_permissions' => 1,
752 'id' => ['IN' => [$activity['id'], $activity2['id']]],
753 ], 1);
754 $this->callAPISuccessGetCount('Activity', [
755 'check_permissions' => 1,
756 'id' => ['IN' => [$activity['id'], $activity2['id']]],
757 ], 1);
758 }
759
760 /**
761 * Check that activities can be retrieved by ACL.
762 *
763 * The activities api applies ACLs in a very limited circumstance, if id is passed in.
764 * Otherwise it sticks with the blunt original permissions.
765 *
766 * @param int $version
767 *
768 * @throws \CRM_Core_Exception
769 * @throws \CiviCRM_API3_Exception
770 * @dataProvider versionThreeAndFour
771 */
772 public function testGetActivityByACL(int $version): void {
773 $this->_apiversion = $version;
774 $this->setPermissions(['access CiviCRM']);
775 $activity = $this->activityCreate();
776
777 $this->hookClass->setHook('civicrm_aclWhereClause', [
778 $this,
779 'aclWhereHookAllResults',
780 ]);
781 $this->callAPISuccessGetCount('Activity', [
782 'check_permissions' => 1,
783 'id' => $activity['id'],
784 ], 1);
785 $this->callAPISuccessGetCount('Activity', [
786 'check_permissions' => 1,
787 'id' => $activity['id'],
788 ]);
789 }
790
791 /**
792 * To leverage ACL permission to view an activity you must be able to see any
793 * of the contacts. FIXME: Api4
794 *
795 * @throws \CRM_Core_Exception|\CiviCRM_API3_Exception
796 */
797 public function testGetActivityByAclCannotViewAllContacts(): void {
798 $activity = $this->activityCreate(['assignee_contact_id' => $this->individualCreate()]);
799 $contacts = $this->getActivityContacts($activity);
800 $this->setPermissions(['access CiviCRM']);
801
802 foreach ($contacts as $role => $contact_id) {
803 $this->allowedContactId = $contact_id;
804 $this->hookClass->setHook('civicrm_aclWhereClause', [
805 $this,
806 'aclWhereOnlyOne',
807 ]);
808 $this->cleanupCachedPermissions();
809 $result = $this->callAPISuccessGetSingle('Activity', [
810 'check_permissions' => 1,
811 'id' => $activity['id'],
812 'return' => [
813 'source_contact_id',
814 'target_contact_id',
815 'assignee_contact_id',
816 ],
817 ]);
818 foreach ([
819 'source_contact',
820 'target_contact',
821 'assignee_contact',
822 ] as $roleName) {
823 $roleKey = $roleName . '_id';
824 if ($role !== $roleKey) {
825 $this->assertTrue(empty($result[$roleKey]), "Only contact in $role is permissioned to be returned, not $roleKey");
826 }
827 else {
828 $this->assertEquals([$contact_id], (array) $result[$roleKey]);
829 $this->assertNotEmpty($result[$roleName . '_name']);
830 }
831 }
832 }
833 }
834
835 /**
836 * To leverage ACL permission to view an activity you must be able to see any of the contacts.
837 *
838 * @param int $version
839 *
840 * @throws \CRM_Core_Exception
841 * @throws \CiviCRM_API3_Exception
842 * @dataProvider versionThreeAndFour
843 */
844 public function testGetActivityByAclCannotViewAnyContacts(int $version): void {
845 $this->_apiversion = $version;
846 $activity = $this->activityCreate();
847 $contacts = $this->getActivityContacts($activity);
848 $this->setPermissions(['access CiviCRM']);
849
850 foreach ($contacts as $contact_id) {
851 $this->callAPIFailure('Activity', 'getsingle', [
852 'check_permissions' => 1,
853 'id' => $activity['id'],
854 ]);
855 }
856 }
857
858 /**
859 * Check that if the source contact is deleted but we can view the others we can see the activity.
860 *
861 * CRM-18409.
862 *
863 * @param int $version
864 *
865 * @dataProvider versionThreeAndFour
866 * @throws \CiviCRM_API3_Exception
867 * @throws \CRM_Core_Exception
868 */
869 public function testGetActivityACLSourceContactDeleted($version): void {
870 $this->_apiversion = $version;
871 $this->setPermissions(['access CiviCRM', 'delete contacts']);
872 $activity = $this->activityCreate();
873 $contacts = $this->getActivityContacts($activity);
874
875 $this->hookClass->setHook('civicrm_aclWhereClause', [
876 $this,
877 'aclWhereHookAllResults',
878 ]);
879 $this->contactDelete($contacts['source_contact_id']);
880 $this->callAPISuccessGetCount('Activity', [
881 'check_permissions' => 1,
882 'id' => $activity['id'],
883 ], 1);
884 }
885
886 /**
887 * Test get activities multiple ids with check permissions
888 *
889 * @see https://issues.civicrm.org/jira/browse/CRM-20441
890 *
891 * @param int $version
892 *
893 * @throws \CRM_Core_Exception
894 * @throws \CiviCRM_API3_Exception
895 * @dataProvider versionThreeAndFour
896 */
897 public function testActivitiesGetMultipleIdsCheckPermissions(int $version): void {
898 $this->_apiversion = $version;
899 $this->createLoggedInUser();
900 $activity = $this->activityCreate();
901 $activity2 = $this->activityCreate();
902 $this->setPermissions(['access CiviCRM']);
903 $this->hookClass->setHook('civicrm_aclWhereClause', [
904 $this,
905 'aclWhereHookAllResults',
906 ]);
907 // Get activities associated with contact $this->_contactID.
908 $params = [
909 'id' => ['IN' => [$activity['id'], $activity2['id']]],
910 'check_permissions' => TRUE,
911 ];
912 $this->callAPISuccessGetCount('Activity', $params, 2);
913 }
914
915 /**
916 * Test get activities multiple ids with check permissions
917 * Limit access to One contact
918 *
919 * @see https://issues.civicrm.org/jira/browse/CRM-20441
920 *
921 * @param int $version
922 *
923 * @throws \CRM_Core_Exception
924 * @throws \CiviCRM_API3_Exception
925 * @dataProvider versionThreeAndFour
926 */
927 public function testActivitiesGetMultipleIdsCheckPermissionsLimitedACL(int $version): void {
928 $this->_apiversion = $version;
929 $this->createLoggedInUser();
930 $activity = $this->activityCreate();
931 $contacts = $this->getActivityContacts($activity);
932 $this->setPermissions(['access CiviCRM']);
933 foreach ($contacts as $contact_id) {
934 $this->allowedContacts[] = $contact_id;
935 }
936 $this->hookClass->setHook('civicrm_aclWhereClause', [
937 $this,
938 'aclWhereMultipleContacts',
939 ]);
940 $contact2 = $this->individualCreate();
941 $activity2 = $this->activityCreate(['source_contact_id' => $contact2]);
942 // Get activities associated with contact $this->_contactID.
943 $params = [
944 'id' => ['IN' => [$activity['id']]],
945 'check_permissions' => TRUE,
946 ];
947 $result = $this->callAPISuccess('activity', 'get', $params);
948 $this->assertEquals(1, $result['count']);
949 $this->callAPIFailure('activity', 'getsingle', array_merge($params, [
950 'id' => [
951 'IN' => [$activity2['id']],
952 ],
953 ]));
954 }
955
956 /**
957 * Test get activities multiple ids with check permissions
958 *
959 * @see https://issues.civicrm.org/jira/browse/CRM-20441
960 *
961 * @param int $version
962 *
963 * @throws \CRM_Core_Exception
964 * @throws \CiviCRM_API3_Exception
965 * @dataProvider versionThreeAndFour
966 */
967 public function testActivitiesGetMultipleIdsCheckPermissionsNotIN(int $version): void {
968 $this->_apiversion = $version;
969 $this->createLoggedInUser();
970 $activity = $this->activityCreate();
971 $activity2 = $this->activityCreate();
972 $this->setPermissions(['access CiviCRM']);
973 $this->hookClass->setHook('civicrm_aclWhereClause', [
974 $this,
975 'aclWhereHookAllResults',
976 ]);
977 // Get activities associated with contact $this->_contactID.
978 $params = [
979 'id' => ['NOT IN' => [$activity['id'], $activity2['id']]],
980 'check_permissions' => TRUE,
981 ];
982 $result = $this->callAPISuccess('activity', 'get', $params);
983 $this->assertEquals(0, $result['count']);
984 }
985
986 /**
987 * Get the contacts for the activity.
988 *
989 * @param $activity
990 *
991 * @return array
992 */
993 protected function getActivityContacts($activity): array {
994 $contacts = [];
995
996 $activityContacts = $this->callAPISuccess('ActivityContact', 'get', [
997 'activity_id' => $activity['id'],
998 'return' => ['record_type_id', 'contact_id'],
999 ])['values'];
1000
1001 $activityRecordTypes = $this->callAPISuccess('ActivityContact', 'getoptions', ['field' => 'record_type_id']);
1002 foreach ($activityContacts as $activityContact) {
1003 $type = $activityRecordTypes['values'][$activityContact['record_type_id']];
1004 switch ($type) {
1005 case 'Activity Source':
1006 $contacts['source_contact_id'] = $activityContact['contact_id'];
1007 break;
1008
1009 case 'Activity Targets':
1010 $contacts['target_contact_id'] = $activityContact['contact_id'];
1011 break;
1012
1013 case 'Activity Assignees':
1014 $contacts['assignee_contact_id'] = $activityContact['contact_id'];
1015 break;
1016
1017 }
1018 }
1019 return $contacts;
1020 }
1021
1022 /**
1023 * Test that the 'everyone' group can be given access to a contact.
1024 * FIXME: Api4
1025 */
1026 public function testGetACLEveryonePermittedEntity(): void {
1027 $this->setupScenarioCoreACLEveryonePermittedToGroup();
1028 $this->callAPISuccessGetCount('Contact', [
1029 'id' => $this->scenarioIDs['Contact']['permitted_contact'],
1030 'check_permissions' => 1,
1031 ], 1);
1032
1033 $this->callAPISuccessGetCount('Contact', [
1034 'id' => $this->scenarioIDs['Contact']['non_permitted_contact'],
1035 'check_permissions' => 1,
1036 ], 0);
1037
1038 // Also check that we can access ACLs through a path that uses the acl_contact_cache table.
1039 // historically this has caused errors due to the key_constraint on that table.
1040 // This is a bit of an artificial check as we have to amp up permissions to access this api.
1041 // However, the lower level function is more directly accessed through the Contribution & Event & Profile
1042 $dupes = $this->callAPISuccess('Contact', 'duplicatecheck', [
1043 'match' => [
1044 'first_name' => 'Anthony',
1045 'last_name' => 'Anderson',
1046 'contact_type' => 'Individual',
1047 'email' => 'anthony_anderson@civicrm.org',
1048 ],
1049 'check_permissions' => 0,
1050 ]);
1051 $this->assertEquals(2, $dupes['count']);
1052 CRM_Core_Config::singleton()->userPermissionClass->permissions = ['access CiviCRM'];
1053
1054 $dupes = $this->callAPISuccess('Contact', 'duplicatecheck', [
1055 'match' => [
1056 'first_name' => 'Anthony',
1057 'last_name' => 'Anderson',
1058 'contact_type' => 'Individual',
1059 'email' => 'anthony_anderson@civicrm.org',
1060 ],
1061 'check_permissions' => 1,
1062 ]);
1063 $this->assertEquals(1, $dupes['count']);
1064
1065 }
1066
1067 /**
1068 * @param int $version
1069 *
1070 * @dataProvider versionThreeAndFour
1071 */
1072 public function testContactGetViaJoin(int $version): void {
1073 $this->_apiversion = $version;
1074 $this->createLoggedInUser();
1075 $main = $this->individualCreate(['first_name' => 'Main']);
1076 $other = $this->individualCreate(['first_name' => 'Other'], 1);
1077 $tag1 = $this->tagCreate(['name' => 'tag_1', 'created_id' => $main])['id'];
1078 $tag2 = $this->tagCreate(['name' => 'tag_2', 'created_id' => $other])['id'];
1079 $this->setPermissions(['access CiviCRM']);
1080 $this->hookClass->setHook('civicrm_aclWhereClause', [$this, 'aclWhereHookAllResults']);
1081 $createdFirstName = 'created_id.first_name';
1082 $result = $this->callAPISuccess('Tag', 'get', [
1083 'check_permissions' => 1,
1084 'return' => ['id', $createdFirstName],
1085 'id' => ['IN' => [$tag1, $tag2]],
1086 ]);
1087 $this->assertEquals('Main', $result['values'][$tag1][$createdFirstName]);
1088 $this->assertEquals('Other', $result['values'][$tag2][$createdFirstName]);
1089 $this->allowedContactId = $main;
1090 $this->hookClass->setHook('civicrm_aclWhereClause', [$this, 'aclWhereOnlyOne']);
1091 $this->cleanupCachedPermissions();
1092 $result = $this->callAPISuccess('Tag', 'get', [
1093 'check_permissions' => 1,
1094 'return' => ['id', $createdFirstName],
1095 'id' => ['IN' => [$tag1, $tag2]],
1096 ]);
1097 $this->assertEquals('Main', $result['values'][$tag1][$createdFirstName]);
1098 $this->assertEquals($tag2, $result['values'][$tag2]['id']);
1099 $this->assertFalse(isset($result['values'][$tag2][$createdFirstName]));
1100 }
1101
1102 /**
1103 * @throws \API_Exception
1104 */
1105 public function testApi4CustomEntityACL(): void {
1106 $group = 'test_group';
1107 $textField = 'text_field';
1108
1109 CustomGroup::create(FALSE)
1110 ->addValue('title', $group)
1111 ->addValue('extends', 'Contact')
1112 ->addValue('is_multiple', TRUE)
1113 ->addChain('field', CustomField::create()
1114 ->addValue('label', $textField)
1115 ->addValue('custom_group_id', '$id')
1116 ->addValue('html_type', 'Text')
1117 ->addValue('data_type', 'String')
1118 )
1119 ->execute();
1120
1121 $this->createLoggedInUser();
1122 $c1 = $this->individualCreate(['first_name' => 'C1']);
1123 $c2 = $this->individualCreate(['first_name' => 'C2', 'is_deleted' => 1], 1);
1124
1125 CustomValue::save($group)->setCheckPermissions(FALSE)
1126 ->addRecord(['entity_id' => $c1, $textField => '1'])
1127 ->addRecord(['entity_id' => $c2, $textField => '2'])
1128 ->execute();
1129
1130 $this->setPermissions(['access CiviCRM', 'view debug output', 'access all custom data']);
1131 $this->hookClass->setHook('civicrm_aclWhereClause', [$this, 'aclWhereHookAllResults']);
1132
1133 // Without "access deleted contacts" we won't see C2
1134 $vals = CustomValue::get($group)->setDebug(TRUE)->execute();
1135 $this->assertCount(1, $vals);
1136 $this->assertEquals($c1, $vals[0]['entity_id']);
1137
1138 $this->setPermissions(['access CiviCRM', 'access deleted contacts', 'view debug output', 'access all custom data']);
1139 $this->hookClass->setHook('civicrm_aclWhereClause', [$this, 'aclWhereHookAllResults']);
1140 $this->cleanupCachedPermissions();
1141
1142 $vals = CustomValue::get($group)->execute();
1143 $this->assertCount(2, $vals);
1144
1145 $this->allowedContactId = $c2;
1146 $this->hookClass->setHook('civicrm_aclWhereClause', [$this, 'aclWhereOnlyOne']);
1147 $this->cleanupCachedPermissions();
1148
1149 $vals = CustomValue::get($group)->addSelect('*', 'contact.first_name')->execute();
1150 $this->assertCount(1, $vals);
1151 $this->assertEquals($c2, $vals[0]['entity_id']);
1152 $this->assertEquals('C2', $vals[0]['contact.first_name']);
1153
1154 $vals = Contact::get()
1155 ->addJoin('Custom_' . $group . ' AS cf')
1156 ->addSelect('first_name', 'cf.' . $textField)
1157 ->addWhere('is_deleted', '=', TRUE)
1158 ->execute();
1159 $this->assertCount(1, $vals);
1160 $this->assertEquals('C2', $vals[0]['first_name']);
1161 $this->assertEquals('2', $vals[0]['cf.' . $textField]);
1162 }
1163
1164 }