From be6a7e247011e796274eb8ffcce0a4cf361d2f4f Mon Sep 17 00:00:00 2001 From: colemanw Date: Wed, 4 Oct 2023 22:22:53 -0400 Subject: [PATCH] CiviCase - Add selectWhereClause for activities --- CRM/Case/BAO/Case.php | 46 ++++++--- ext/civi_case/civi_case.php | 19 ++++ tests/phpunit/api/v4/Entity/CaseTest.php | 118 +++++++++++++++++++++++ 3 files changed, 167 insertions(+), 16 deletions(-) diff --git a/CRM/Case/BAO/Case.php b/CRM/Case/BAO/Case.php index d8aa69d2e8..d6d3c55cd9 100644 --- a/CRM/Case/BAO/Case.php +++ b/CRM/Case/BAO/Case.php @@ -3019,33 +3019,47 @@ WHERE id IN (' . implode(',', $copiedActivityIds) . ')'; * @inheritDoc */ public function addSelectWhereClause(string $entityName = NULL, int $userId = NULL, array $conditions = []): array { + $administerCases = CRM_Core_Permission::check('administer CiviCase', $userId); + $viewMyCases = CRM_Core_Permission::check('access my cases and activities', $userId); + $viewAllCases = CRM_Core_Permission::check('access all cases and activities', $userId); + // We always return an array with these keys, even if they are empty, // because this tells the query builder that we have considered these fields for acls $clauses = [ 'id' => [], // Only case admins can view deleted cases - 'is_deleted' => CRM_Core_Permission::check('administer CiviCase') ? [] : ["= 0"], + 'is_deleted' => $administerCases ? [] : ['= 0'], ]; - // Ensure the user has permission to view the case client - $contactClause = CRM_Utils_SQL::mergeSubquery('Contact'); - if ($contactClause) { - $contactClause = implode(' AND contact_id ', $contactClause); - $clauses['id'][] = "IN (SELECT case_id FROM civicrm_case_contact WHERE contact_id $contactClause)"; - } - // The api gatekeeper ensures the user has at least "access my cases and activities" - // so if they do not have permission to see all cases we'll assume they can only access their own - if (!CRM_Core_Permission::check('access all cases and activities')) { - $user = (int) CRM_Core_Session::getLoggedInContactID(); - $clauses['id'][] = "IN ( - SELECT r.case_id FROM civicrm_relationship r, civicrm_case_contact cc WHERE r.is_active = 1 AND cc.case_id = r.case_id AND ( - (r.contact_id_a = cc.contact_id AND r.contact_id_b = $user) OR (r.contact_id_b = cc.contact_id AND r.contact_id_a = $user) - ) - )"; + + // No CiviCase access + if (!$viewAllCases && !$viewMyCases) { + $clauses['id'][] = 'IS NULL'; + } + else { + // Enforce permission to view the case client + $contactClause = CRM_Utils_SQL::mergeSubquery('Contact'); + if ($contactClause) { + $contactClause = implode(' AND contact_id ', $contactClause); + $clauses['id'][] = "IN (SELECT case_id FROM civicrm_case_contact WHERE contact_id $contactClause)"; + } + // User can only access their own cases + if (!$viewAllCases) { + $clauses['id'][] = self::getAccessMyCasesClause($userId); + } } CRM_Utils_Hook::selectWhereClause($this, $clauses, $userId, $conditions); return $clauses; } + private static function getAccessMyCasesClause(int $userId = NULL): string { + $user = $userId ?? (int) CRM_Core_Session::getLoggedInContactID(); + return "IN ( + SELECT r.case_id FROM civicrm_relationship r, civicrm_case_contact cc WHERE r.is_active = 1 AND cc.case_id = r.case_id AND ( + (r.contact_id_a = cc.contact_id AND r.contact_id_b = $user) OR (r.contact_id_b = cc.contact_id AND r.contact_id_a = $user) + ) + )"; + } + /** * CRM-20308: Method to get the contact id to use as from contact for email copy * 1. Activity Added by Contact's email address diff --git a/ext/civi_case/civi_case.php b/ext/civi_case/civi_case.php index d35d421086..43d5b67706 100644 --- a/ext/civi_case/civi_case.php +++ b/ext/civi_case/civi_case.php @@ -17,3 +17,22 @@ function civi_case_civicrm_managed(&$entities, $modules) { ); } } + +/** + * Applies Case permissions to Activities + * + * @implements CRM_Utils_Hook::selectWhereClause + */ +function civi_case_civicrm_selectWhereClause($entityName, &$clauses, $userId, $conditions) { + if ($entityName === 'Activity') { + // OR group: either it's a non-case activity OR case permissions apply + $orGroup = [ + 'NOT IN (SELECT activity_id FROM civicrm_case_activity)', + ]; + $casePerms = CRM_Utils_SQL::mergeSubquery('Case'); + if ($casePerms) { + $orGroup[] = 'IN (SELECT activity_id FROM civicrm_case_activity WHERE case_id ' . implode(' AND case_id ', $casePerms) . ')'; + } + $clauses['id'][] = $orGroup; + } +} diff --git a/tests/phpunit/api/v4/Entity/CaseTest.php b/tests/phpunit/api/v4/Entity/CaseTest.php index 7feff704de..39cd8d3028 100644 --- a/tests/phpunit/api/v4/Entity/CaseTest.php +++ b/tests/phpunit/api/v4/Entity/CaseTest.php @@ -20,8 +20,10 @@ namespace api\v4\Entity; use api\v4\Api4TestBase; +use Civi\API\Exception\UnauthorizedException; use Civi\Api4\Activity; use Civi\Api4\CaseActivity; +use Civi\Api4\CiviCase; use Civi\Api4\Relationship; /** @@ -164,4 +166,120 @@ class CaseTest extends Api4TestBase { $this->assertCount(0, $get1); } + public function testCaseActivityPermission(): void { + $case1 = $this->createTestRecord('Case')['id']; + $userId = $this->createLoggedInUser(); + $case2 = $this->createTestRecord('Case', [ + 'creator_id' => $userId, + ])['id']; + + $act1 = $this->createTestRecord('Activity', [ + 'case_id' => $case1, + ])['id']; + $act2 = $this->createTestRecord('Activity', [ + 'case_id' => $case2, + ])['id']; + $act3 = $this->createTestRecord('Activity')['id']; + + // No CiviCase permission + \CRM_Core_Config::singleton()->userPermissionClass->permissions = [ + 'access CiviCRM', + 'view all contacts', + ]; + + $result = Activity::get() + ->addWhere('id', 'IN', [$act1, $act2, $act3]) + ->execute()->column('id'); + $this->assertCount(1, $result); + $this->assertEquals($act3, $result[0]); + try { + CiviCase::get()->execute(); + $this->fail('Expected UnauthorizedException'); + } + catch (UnauthorizedException $e) { + } + + // Without any CiviCase permission, ensure `case_id` in the where clause doesn't cause errors + $result = Activity::get() + ->addWhere('id', 'IN', [$act1, $act2, $act3]) + ->addWhere('case_id', 'IS EMPTY') + ->execute()->column('id'); + $this->assertCount(1, $result); + $this->assertEquals($act3, $result[0]); + + // CiviCase permission limited to "my cases" + \CRM_Core_Config::singleton()->userPermissionClass->permissions = [ + 'access CiviCRM', + 'view all contacts', + 'access my cases and activities', + ]; + + $result = Activity::get() + ->addWhere('id', 'IN', [$act1, $act2]) + ->execute()->column('id'); + $this->assertCount(1, $result); + $this->assertEquals($act2, $result[0]); + $result = CiviCase::get() + ->addWhere('id', 'IN', [$case1, $case2]) + ->execute()->column('id'); + $this->assertCount(1, $result); + $this->assertEquals($case2, $result[0]); + + // CiviCase permission for all non-deleted cases + \CRM_Core_Config::singleton()->userPermissionClass->permissions = [ + 'access CiviCRM', + 'view all contacts', + 'access all cases and activities', + ]; + + $result = Activity::get() + ->addWhere('id', 'IN', [$act1, $act2]) + ->execute()->column('id'); + $this->assertCount(2, $result); + $result = CiviCase::get() + ->addWhere('id', 'IN', [$case1, $case2]) + ->execute()->column('id'); + $this->assertCount(2, $result); + + CiviCase::update(FALSE) + ->addWhere('id', '=', $case2) + ->addValue('is_deleted', TRUE) + ->execute(); + + $result = Activity::get() + ->addWhere('id', 'IN', [$act1, $act2]) + ->execute()->column('id'); + $this->assertCount(1, $result); + $this->assertEquals($act1, $result[0]); + $result = CiviCase::get() + ->addWhere('id', 'IN', [$case1, $case2]) + ->execute()->column('id'); + $this->assertCount(1, $result); + $this->assertEquals($case1, $result[0]); + + // CiviCase permission for all cases + \CRM_Core_Config::singleton()->userPermissionClass->permissions = [ + 'access CiviCRM', + 'view all contacts', + 'access all cases and activities', + 'administer CiviCase', + ]; + + $result = Activity::get() + ->addWhere('id', 'IN', [$act1, $act2]) + ->execute()->column('id'); + $this->assertCount(2, $result); + $result = CiviCase::get() + ->addWhere('id', 'IN', [$case1, $case2]) + ->execute()->column('id'); + $this->assertCount(2, $result); + + $result = Activity::get() + ->addWhere('id', 'IN', [$act1, $act2, $act3]) + ->addWhere('case_id', 'IS EMPTY') + ->execute()->column('id'); + $this->assertCount(1, $result); + $this->assertEquals($act3, $result[0]); + } + } -- 2.25.1