Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce civi.api4.authorizeRecord and civi.api4.validate #20533

Merged
merged 27 commits into from
Jun 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
cc69f30
(REF) Consolidate the 'dispatchSubevent()' methods
totten May 18, 2021
8047ede
(REF) APIv4 BatchAction - Split 'getBatchRecords()' in two
totten May 19, 2021
a9aac3b
(REF) APIv4 UpdateAction - Add skeletal 'validateValues()` method
totten May 19, 2021
48872a5
LazyArray - Add helper class for lazily-loaded lists
totten May 20, 2021
7645fa5
(REF) Civi/API/Event - Extract RequestTrait
totten Jun 4, 2021
4bf9210
APIv4 - When running validateValues(), fire an event
totten Apr 27, 2021
9f04ea3
ValidateValues - Provide optional access to complete records
totten May 20, 2021
61e8985
Add ValidateValuesTest
totten May 20, 2021
3f0340c
FinancialItem - Provide defaults so that stricter ConformanceTest wil…
totten Jun 4, 2021
572b221
DAOCreateAction - Fill defaults before validating write
totten Jun 4, 2021
d291b36
UFJoin - Update addSelectWhereClause
colemanw Jun 4, 2021
9cef606
Add BAO function and hook for checkAccess
eileenmcnaughton Apr 11, 2021
929a958
APIv4 - Add checkAccess action
colemanw Apr 27, 2021
1d3cbc3
Implement _checkAccess for Contact BAO and related entities (email, p…
colemanw May 5, 2021
01c65aa
Implement checkAccess for custom entities
colemanw May 6, 2021
3d3a716
Implement checkAccess for EntityTags and Notes
colemanw May 8, 2021
63ee667
ConformanceTest - Add coverage for checkAccess
totten Jun 4, 2021
cb72c80
ConformanceTest - Add support for read-only entities
totten Jun 7, 2021
63a2492
(REF) AuthorizeEvent - Extract AuthorizedTrait
totten Jun 7, 2021
399eff1
CoreUtil::checkAccess() - Accept optional argument $userID
totten Jun 4, 2021
6ea81ac
(REF) Isolate calls to $bao::checkAccess. Prefer CoreUtil::checkAcces…
totten Jun 7, 2021
a5d0f31
(REF) Consistently pass `string $entity` to all flavors of checkAccess
totten Jun 7, 2021
849354a
(REF) Change CoreUtil::checkAccess() to CoreUtil::checkAccessRecord()
totten Jun 7, 2021
e294ceb
(REF) Consolidate calls to `Hook::checkAccess()`. Define initial valu…
totten Jun 7, 2021
af4cccf
Convert hook_civicrm_checkAccess to civi.api4.authorizeRecord
totten Jun 4, 2021
70da392
Partially rollback changes to `$userID`. Merely lay groundwork for fu…
totten Jun 9, 2021
b87406e
Expand CustomValue::_checkAccess()
totten Jun 9, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions CRM/Contact/AccessTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php
/*
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC. All rights reserved. |
| |
| This work is published under the GNU AGPLv3 license with some |
| permitted exceptions and without any warranty. For full license |
| and copyright information, see https://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/

/**
*
* @package CRM
* @copyright CiviCRM LLC https://civicrm.org/licensing
*/

/**
* Trait shared with entities attached to the contact record.
*/
trait CRM_Contact_AccessTrait {

/**
* @param string $entityName
* @param string $action
* @param array $record
* @param int $userID
* @return bool
* @see CRM_Core_DAO::checkAccess
*/
public static function _checkAccess(string $entityName, string $action, array $record, int $userID) {
$cid = $record['contact_id'] ?? NULL;
if (!$cid && !empty($record['id'])) {
$cid = CRM_Core_DAO::getFieldValue(__CLASS__, $record['id'], 'contact_id');
}
if (!$cid) {
// With no contact id this must be part of an event locblock
return in_array(__CLASS__, ['CRM_Core_BAO_Phone', 'CRM_Core_BAO_Email', 'CRM_Core_BAO_Address']) &&
CRM_Core_Permission::check('edit all events', $userID);
}
return \Civi\Api4\Utils\CoreUtil::checkAccessDelegated('Contact', 'update', ['id' => $cid], $userID);
}

}
29 changes: 29 additions & 0 deletions CRM/Contact/BAO/Contact.php
Original file line number Diff line number Diff line change
Expand Up @@ -3728,4 +3728,33 @@ public static function getEntityRefFilters() {
];
}

/**
* @param string $entityName
* @param string $action
* @param array $record
* @param $userID
* @return bool
* @see CRM_Core_DAO::checkAccess
*/
public static function _checkAccess(string $entityName, string $action, array $record, $userID): bool {
switch ($action) {
case 'create':
return CRM_Core_Permission::check('add contacts', $userID);

case 'get':
$actionType = CRM_Core_Permission::VIEW;
break;

case 'delete':
$actionType = CRM_Core_Permission::DELETE;
break;

default:
$actionType = CRM_Core_Permission::EDIT;
break;
}

return CRM_Contact_BAO_Contact_Permission::allow($record['id'], $actionType, $userID);
}

}
43 changes: 21 additions & 22 deletions CRM/Contact/BAO/Contact/Permission.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,37 +136,39 @@ public static function allowList($contact_ids, $type = CRM_Core_Permission::VIEW
* @param int $id
* Contact id.
* @param int|string $type the type of operation (view|edit)
* @param int $userID
* Contact id of user to check (defaults to current logged-in user)
*
* @return bool
* true if the user has permission, false otherwise
*/
public static function allow($id, $type = CRM_Core_Permission::VIEW) {
// get logged in user
$contactID = CRM_Core_Session::getLoggedInContactID();
public static function allow($id, $type = CRM_Core_Permission::VIEW, $userID = NULL) {
// Default to logged in user if not supplied
$userID = $userID ?? CRM_Core_Session::getLoggedInContactID();

// first: check if contact is trying to view own contact
if ($contactID == $id && ($type == CRM_Core_Permission::VIEW && CRM_Core_Permission::check('view my contact')
|| $type == CRM_Core_Permission::EDIT && CRM_Core_Permission::check('edit my contact'))
if ($userID == $id && ($type == CRM_Core_Permission::VIEW && CRM_Core_Permission::check('view my contact')
|| $type == CRM_Core_Permission::EDIT && CRM_Core_Permission::check('edit my contact', $userID))
) {
return TRUE;
}

// FIXME: push this somewhere below, to not give this permission so many rights
$isDeleted = (bool) CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact', $id, 'is_deleted');
if (CRM_Core_Permission::check('access deleted contacts') && $isDeleted) {
if (CRM_Core_Permission::check('access deleted contacts', $userID) && $isDeleted) {
return TRUE;
}

// short circuit for admin rights here so we avoid unneeeded queries
// some duplication of code, but we skip 3-5 queries
if (CRM_Core_Permission::check('edit all contacts') ||
($type == CRM_ACL_API::VIEW && CRM_Core_Permission::check('view all contacts'))
if (CRM_Core_Permission::check('edit all contacts', $userID) ||
($type == CRM_Core_Permission::VIEW && CRM_Core_Permission::check('view all contacts', $userID))
) {
return TRUE;
}

// check permission based on relationship, CRM-2963
if (self::relationshipList([$id], $type)) {
if (self::relationshipList([$id], $type, $userID)) {
return TRUE;
}

Expand All @@ -175,7 +177,7 @@ public static function allow($id, $type = CRM_Core_Permission::VIEW) {
$tables = [];
$whereTables = [];

$permission = CRM_ACL_API::whereClause($type, $tables, $whereTables, NULL, FALSE, FALSE, TRUE);
$permission = CRM_ACL_API::whereClause($type, $tables, $whereTables, $userID, FALSE, FALSE, TRUE);
$from = CRM_Contact_BAO_Query::fromClause($whereTables);

$query = "
Expand All @@ -185,10 +187,7 @@ public static function allow($id, $type = CRM_Core_Permission::VIEW) {
LIMIT 1
";

if (CRM_Core_DAO::singleValueQuery($query, [1 => [$id, 'Integer']])) {
return TRUE;
}
return FALSE;
return (bool) CRM_Core_DAO::singleValueQuery($query, [1 => [$id, 'Integer']]);
}

/**
Expand Down Expand Up @@ -362,28 +361,28 @@ public static function cacheSubquery() {

/**
* Filter a list of contact_ids by the ones that the
* currently active user as a permissioned relationship with
* user as a permissioned relationship with
*
* @param array $contact_ids
* List of contact IDs to be filtered
*
* @param int $type
* access type CRM_Core_Permission::VIEW or CRM_Core_Permission::EDIT
* @param int $userID
*
* @return array
* List of contact IDs that the user has permissions for
*/
public static function relationshipList($contact_ids, $type) {
public static function relationshipList($contact_ids, $type, $userID = NULL) {
$result_set = [];

// no processing empty lists (avoid SQL errors as well)
if (empty($contact_ids)) {
return [];
}

// get the currently logged in user
$contactID = CRM_Core_Session::getLoggedInContactID();
if (empty($contactID)) {
// Default to currently logged in user
$userID = $userID ?? CRM_Core_Session::getLoggedInContactID();
if (empty($userID)) {
return [];
}

Expand Down Expand Up @@ -418,7 +417,7 @@ public static function relationshipList($contact_ids, $type) {
SELECT civicrm_relationship.{$contact_id_column} AS contact_id
FROM civicrm_relationship
{$LEFT_JOIN_DELETED}
WHERE civicrm_relationship.{$user_id_column} = {$contactID}
WHERE civicrm_relationship.{$user_id_column} = {$userID}
AND civicrm_relationship.{$contact_id_column} IN ({$contact_id_list})
AND civicrm_relationship.is_active = 1
AND civicrm_relationship.is_permission_{$direction['from']}_{$direction['to']} {$is_perm_condition}
Expand All @@ -444,7 +443,7 @@ public static function relationshipList($contact_ids, $type) {
FROM civicrm_relationship first_degree_relationship
LEFT JOIN civicrm_relationship second_degree_relationship ON first_degree_relationship.contact_id_{$first_direction['to']} = second_degree_relationship.contact_id_{$second_direction['from']}
{$LEFT_JOIN_DELETED}
WHERE first_degree_relationship.contact_id_{$first_direction['from']} = {$contactID}
WHERE first_degree_relationship.contact_id_{$first_direction['from']} = {$userID}
AND second_degree_relationship.contact_id_{$second_direction['to']} IN ({$contact_id_list})
AND first_degree_relationship.is_active = 1
AND first_degree_relationship.is_permission_{$first_direction['from']}_{$first_direction['to']} {$is_perm_condition}
Expand Down
1 change: 1 addition & 0 deletions CRM/Core/BAO/Address.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* This is class to handle address related functions.
*/
class CRM_Core_BAO_Address extends CRM_Core_DAO_Address {
use CRM_Contact_AccessTrait;

/**
* Takes an associative array and creates a address.
Expand Down
55 changes: 55 additions & 0 deletions CRM/Core/BAO/CustomValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -221,4 +221,59 @@ public function addSelectWhereClause() {
return $clauses;
}

/**
* Special checkAccess function for multi-record custom pseudo-entities
*
* @param string $entityName
* Ex: 'Contact' or 'Custom_Foobar'
* @param string $action
* @param array $record
* @param int $userID
* Contact ID of the active user (whose access we must check). 0 for anonymous.
* @return bool
* TRUE if granted. FALSE if prohibited. NULL if indeterminate.
*/
public static function _checkAccess(string $entityName, string $action, array $record, int $userID): ?bool {
// This check implements two rules: you must have access to the specific custom-data-group - and to the underlying record (e.g. Contact).

$groupName = substr($entityName, 0, 7) === 'Custom_' ? substr($entityName, 7) : NULL;
$extends = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $groupName, 'extends', 'name');
$id = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $groupName, 'id', 'name');
if (!$groupName) {
// $groupName is required but the function signature has to match the parent.
throw new CRM_Core_Exception('Missing required group-name in CustomValue::checkAccess');
}

if (empty($extends) || empty($id)) {
throw new CRM_Core_Exception('Received invalid group-name in CustomValue::checkAccess');
}

$customGroups = [$id => $id];
$defaultGroups = CRM_Core_Permission::customGroupAdmin() ? [$id] : [];
// FIXME: Per current onscreen help (Admin=>ACLs=>Add ACLs), CustomGroup ACLs treat VIEW and EDIT as the same. Skimming code, it appears that existing checks use VIEW.
$accessList = CRM_ACL_API::group(CRM_Core_Permission::VIEW, $userID, 'civicrm_custom_group', $customGroups, $defaultGroups);
if (empty($accessList)) {
return FALSE;
}

$eid = $record['entity_id'] ?? NULL;
if (!$eid) {
$tableName = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', $groupName, 'table_name', 'name');
$eid = CRM_Core_DAO::singleValueQuery("SELECT entity_id FROM `$tableName` WHERE id = " . (int) $record['id']);
}

// Do we have access to the target record?
if (in_array($extends, ['Contact', 'Individual', 'Organization', 'Household'])) {
return \Civi\Api4\Utils\CoreUtil::checkAccessDelegated('Contact', 'update', ['id' => $eid], $userID);
}
elseif (\Civi\Api4\Utils\CoreUtil::getApiClass($extends)) {
// For most entities (Activity, Relationship, Contribution, ad nauseum), we acn just use an eponymous API.
return \Civi\Api4\Utils\CoreUtil::checkAccessDelegated($extends, 'update', ['id' => $eid], $userID);
}
else {
// Do you need to add a special case for some oddball custom-group type?
throw new CRM_Core_Exception("Cannot assess delegated permissions for group {$groupName}.");
}
}

}
1 change: 1 addition & 0 deletions CRM/Core/BAO/Email.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* This class contains functions for email handling.
*/
class CRM_Core_BAO_Email extends CRM_Core_DAO_Email {
use CRM_Contact_AccessTrait;

/**
* Create email address.
Expand Down
1 change: 1 addition & 0 deletions CRM/Core/BAO/EntityTag.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* @copyright CiviCRM LLC https://civicrm.org/licensing
*/
class CRM_Core_BAO_EntityTag extends CRM_Core_DAO_EntityTag {
use CRM_Core_DynamicFKAccessTrait;

/**
* Given a contact id, it returns an array of tag id's the contact belongs to.
Expand Down
1 change: 1 addition & 0 deletions CRM/Core/BAO/IM.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* This class contain function for IM handling
*/
class CRM_Core_BAO_IM extends CRM_Core_DAO_IM {
use CRM_Contact_AccessTrait;

/**
* Create or update IM record.
Expand Down
1 change: 1 addition & 0 deletions CRM/Core/BAO/Note.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* BAO object for crm_note table.
*/
class CRM_Core_BAO_Note extends CRM_Core_DAO_Note {
use CRM_Core_DynamicFKAccessTrait;

/**
* Const the max number of notes we display at any given time.
Expand Down
1 change: 1 addition & 0 deletions CRM/Core/BAO/OpenID.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* This class contains function for Open Id
*/
class CRM_Core_BAO_OpenID extends CRM_Core_DAO_OpenID {
use CRM_Contact_AccessTrait;

/**
* Create or update OpenID record.
Expand Down
1 change: 1 addition & 0 deletions CRM/Core/BAO/Phone.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* Class contains functions for phone.
*/
class CRM_Core_BAO_Phone extends CRM_Core_DAO_Phone {
use CRM_Contact_AccessTrait;

/**
* Create phone object - note that the create function calls 'add' but
Expand Down
11 changes: 11 additions & 0 deletions CRM/Core/BAO/UFJoin.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,15 @@ public static function entityTables() {
];
}

/**
* Override base method which assumes permissions should be based on entity_table.
*
* @return array
*/
public function addSelectWhereClause() {
$clauses = [];
CRM_Utils_Hook::selectWhereClause($this, $clauses);
return $clauses;
}

}
1 change: 1 addition & 0 deletions CRM/Core/BAO/Website.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* This class contain function for Website handling.
*/
class CRM_Core_BAO_Website extends CRM_Core_DAO_Website {
use CRM_Contact_AccessTrait;

/**
* Create or update Website record.
Expand Down
4 changes: 2 additions & 2 deletions CRM/Core/DAO.php
Original file line number Diff line number Diff line change
Expand Up @@ -2993,7 +2993,7 @@ public function addSelectWhereClause() {
$clauses[$fieldName] = CRM_Utils_SQL::mergeSubquery('Contact');
}
// Clause for an entity_table/entity_id combo
if ($fieldName == 'entity_id' && isset($fields['entity_table'])) {
if ($fieldName === 'entity_id' && isset($fields['entity_table'])) {
$relatedClauses = [];
$relatedEntities = $this->buildOptions('entity_table', 'get');
foreach ((array) $relatedEntities as $table => $ent) {
Expand Down Expand Up @@ -3042,7 +3042,7 @@ public static function getSelectWhereClause($tableAlias = NULL) {

/**
* ensure database name is 'safe', i.e. only contains word characters (includes underscores)
* and dashes, and contains at least one [a-z] case insenstive.
* and dashes, and contains at least one [a-z] case insensitive.
*
* @param $database
*
Expand Down
2 changes: 1 addition & 1 deletion CRM/Core/DAO/AllCoreTables.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ public static function getCanonicalClassName($baoName) {
*/
public static function getBAOClassName($daoName) {
$baoName = str_replace('_DAO_', '_BAO_', $daoName);
return class_exists($baoName) ? $baoName : $daoName;
return $daoName === $baoName || class_exists($baoName) ? $baoName : $daoName;
}

/**
Expand Down
Loading