Add new Super-duper-no-permissions-apply permission
authoreileen <emcnaughton@wikimedia.org>
Sat, 13 Mar 2021 02:31:48 +0000 (15:31 +1300)
committereileen <emcnaughton@wikimedia.org>
Mon, 22 Mar 2021 22:59:18 +0000 (11:59 +1300)
We discussed in the context of search kit that there are 2 competing concepts of
'administer CiviCRM'
1) like drupal user 1, can do anything
2) role that has various administrative access but acls etc still apply

In search kit we have an interest in allowing users who won't do dumb things
  the ability to expose data to people who otherwise would not have access to that data
   - e.g to create a listing of event participants to expose to anonymous users.

This effectively means we are giving people the power to create displays
that set check_permissions to FALSE. This would potentially enable people
to not just bypass ACLs applied to others but also acls applied to them.
In order words it could be a privellege escallation.

To prevent any unexpected escallation we decided that this ability
should only be given to contacts who explicitly have access to everything
anyway. There is no existing permission that does this (although
there is a perception that there is)

CRM/Core/Permission.php
CRM/Core/Permission/Base.php
CRM/Core/Permission/Drupal6.php
CRM/Utils/Hook.php
tests/phpunit/CRM/Core/Permission/BaseTest.php

index 7191a8e60c6c0a8351a69d62e6cb8adb682052f7..a403c9b81f42b7890cdfb1ae2062bd6b8d92c746 100644 (file)
@@ -591,7 +591,7 @@ class CRM_Core_Permission {
     $permissions = self::getCoreAndComponentPermissions($all);
 
     // Add any permissions defined in hook_civicrm_permission implementations.
-    $module_permissions = CRM_Core_Config::singleton()->userPermissionClass->getAllModulePermissions(TRUE);
+    $module_permissions = CRM_Core_Config::singleton()->userPermissionClass->getAllModulePermissions(TRUE, $permissions);
     $permissions = array_merge($permissions, $module_permissions);
     if (!$descriptions) {
       foreach ($permissions as $name => $attr) {
@@ -874,6 +874,10 @@ class CRM_Core_Permission {
         'label' => $prefix . ts('administer CiviCRM Data'),
         'description' => ts('Permit altering all restricted data options'),
       ],
+      'all CiviCRM permissions and ACLs' => [
+        'label' => $prefix . ts('all CiviCRM permissions and ACLs'),
+        'description' => ts('Administer and use CiviCRM bypassing any other permission or ACL checks and enabling the creation of displays and forms that allow others to bypass checks. This permission should be given out with care'),
+      ],
     ];
     if (self::isMultisiteEnabled()) {
       // This could arguably be moved to the multisite extension but
@@ -883,11 +887,6 @@ class CRM_Core_Permission {
         'description' => ts('Administer multiple organizations. In practice this allows editing the group organization link'),
       ];
     }
-    foreach (self::getImpliedPermissions() as $name => $includes) {
-      foreach ($includes as $permission) {
-        $permissions[$name][] = $permissions[$permission];
-      }
-    }
     return $permissions;
   }
 
@@ -896,11 +895,11 @@ class CRM_Core_Permission {
    *
    * @return array
    */
-  public static function getImpliedPermissions() {
+  public static function getImpliedAdminPermissions(): array {
     return [
-      'administer CiviCRM' => ['administer CiviCRM system', 'administer CiviCRM data'],
-      'administer CiviCRM data' => ['edit message templates', 'administer dedupe rules'],
-      'administer CiviCRM system' => ['edit system workflow message templates'],
+      'administer CiviCRM' => ['implied_permissions' => ['administer CiviCRM system', 'administer CiviCRM data']],
+      'administer CiviCRM data' => ['implied_permissions' => ['edit message templates', 'administer dedupe rules']],
+      'administer CiviCRM system' => ['implied_permissions' => ['edit system workflow message templates']],
     ];
   }
 
@@ -911,14 +910,19 @@ class CRM_Core_Permission {
    *
    * @return array
    */
-  public static function getImpliedPermissionsFor(string $permission) {
-    $return = [];
-    foreach (self::getImpliedPermissions() as $superPermission => $components) {
-      if (in_array($permission, $components, TRUE)) {
-        $return[$superPermission] = $superPermission;
+  public static function getImpliedPermissionsFor(string $permission): array {
+    $implied = Civi::cache('metadata')->get('implied_permissions', []);
+    if (isset($implied[$permission])) {
+      return $implied[$permission];
+    }
+    $implied[$permission] = [];
+    foreach (self::basicPermissions(FALSE, TRUE) as $key => $details) {
+      if (in_array($permission, $details['implied_permissions'] ?? [], TRUE)) {
+        $implied[$permission][] = $key;
       }
     }
-    return $return;
+    Civi::cache('metadata')->set('implied_permissions', $implied);
+    return $implied[$permission];
   }
 
   /**
@@ -1728,6 +1732,7 @@ class CRM_Core_Permission {
   protected static function getCoreAndComponentPermissions(bool $all): array {
     $permissions = self::getCorePermissions();
     $permissions = array_merge($permissions, self::getComponentPermissions($all));
+    $permissions['all CiviCRM permissions and ACLs']['implied_permissions'] = array_keys($permissions);
     return $permissions;
   }
 
index 0c7bf678a315607121959ca136bf4333d261108a..60ffbe3c9ed6adc129811e0efbe3627e121560d2 100644 (file)
@@ -57,7 +57,7 @@ class CRM_Core_Permission_Base {
    *   a permission name
    */
   public function translatePermission($perm, $nativePrefix, $map) {
-    list ($civiPrefix, $name) = CRM_Utils_String::parsePrefix(':', $perm, NULL);
+    [$civiPrefix, $name] = CRM_Utils_String::parsePrefix(':', $perm, NULL);
     switch ($civiPrefix) {
       case $nativePrefix:
         return $name;
@@ -272,9 +272,12 @@ class CRM_Core_Permission_Base {
    *   The permission to check.
    * @param int $userId
    *
+   * @return bool;
+   *
    */
   public function check($str, $userId = NULL) {
     //no default behaviour
+    return FALSE;
   }
 
   /**
@@ -374,7 +377,7 @@ class CRM_Core_Permission_Base {
    *   Array of permissions, in the same format as CRM_Core_Permission::getCorePermissions().
    * @see CRM_Core_Permission::getCorePermissions
    */
-  public static function getModulePermissions($module) {
+  public function getModulePermissions($module): array {
     $return_permissions = [];
     $fn_name = "{$module}_civicrm_permission";
     if (function_exists($fn_name)) {
@@ -390,14 +393,15 @@ class CRM_Core_Permission_Base {
    * in all enabled CiviCRM module extensions.
    *
    * @param bool $descriptions
+   * @param array $permissions
    *
    * @return array
    *   Array of permissions, in the same format as CRM_Core_Permission::getCorePermissions().
    */
-  public function getAllModulePermissions($descriptions = FALSE) {
-    // Passing in false here is to be deprecated.
-    $permissions = [];
-    CRM_Utils_Hook::permission($permissions);
+  public function getAllModulePermissions($descriptions = FALSE, &$permissions): array {
+    $newPermissions = [];
+    CRM_Utils_Hook::permission($newPermissions, $permissions);
+    $permissions = array_merge($permissions, $newPermissions);
 
     if ($descriptions) {
       foreach ($permissions as $permission => $label) {
@@ -405,6 +409,7 @@ class CRM_Core_Permission_Base {
       }
     }
     else {
+      // Passing in false here is to be deprecated.
       foreach ($permissions as $permission => $label) {
         $permissions[$permission] = (is_array($label)) ? array_shift($label) : $label;
       }
index 2ff671fb09bcedc3a65909151ef3b9f3979bd905..d478f09b5dbc9754534fb3c5757652a720986aa2 100644 (file)
@@ -192,7 +192,7 @@ class CRM_Core_Permission_Drupal6 extends CRM_Core_Permission_DrupalBase {
    * @return array
    *   Array of permissions, in the same format as CRM_Core_Permission::getCorePermissions().
    */
-  public static function getModulePermissions($module) {
+  public function getModulePermissions($module):array {
     $return_permissions = [];
     $fn_name = "{$module}_civicrm_permission";
     if (function_exists($fn_name)) {
index 2c0d1958626fe36ea2832017361b5b3ba72fee3e..dc54996f1423eb42e4e474d5821cdae4d8689f1d 100644 (file)
@@ -2023,16 +2023,19 @@ abstract class CRM_Utils_Hook {
    * This hook is called when exporting Civi's permission to the CMS. Use this hook to modify
    * the array of system permissions for CiviCRM.
    *
+   * @param array $newPermissions
+   *   Array to be filled with permissions.
    * @param array $permissions
-   *   Array of permissions. See CRM_Core_Permission::getCorePermissions() for
-   *   the format of this array.
+   *   Already calculated permissions. These can be altered. Notably an
+   *   extension might want to add it's permissions to 'implied' or to
+   *   remove some permissions.
    *
    * @return null
    *   The return value is ignored
    */
-  public static function permission(&$permissions) {
-    return self::singleton()->invoke(['permissions'], $permissions,
-      self::$_nullObject, self::$_nullObject, self::$_nullObject, self::$_nullObject, self::$_nullObject,
+  public static function permission(&$newPermissions, &$permissions) {
+    return self::singleton()->invoke(['permissions', 'all_permissions'], $newPermissions, $permissions,
+      self::$_nullObject, self::$_nullObject, self::$_nullObject, self::$_nullObject,
       'civicrm_permission'
     );
   }
index 2095aba6861ac09b9fd3edd2ef4b8476f1dda1a1..08d930defad4ed37286bc2a8a7e8ae97d8d8dede 100644 (file)
@@ -12,7 +12,7 @@ class CRM_Core_Permission_BaseTest extends CiviUnitTestCase {
    * @return array
    *   (0 => input to translatePermission, 1 => expected output from translatePermission)
    */
-  public function translateData() {
+  public function translateData(): array {
     $cases = [];
 
     $cases[] = ['administer CiviCRM', 'administer CiviCRM'];
@@ -35,7 +35,7 @@ class CRM_Core_Permission_BaseTest extends CiviUnitTestCase {
    * @param string $expected
    *   The name of an actual permission (based on translation matrix for "runtime").
    */
-  public function testTranslate($input, $expected) {
+  public function testTranslate(string $input, string $expected): void {
     $perm = new CRM_Core_Permission_Base();
     $actual = $perm->translatePermission($input, 'myruntime', [
       'universal name' => 'local name',
@@ -48,7 +48,7 @@ class CRM_Core_Permission_BaseTest extends CiviUnitTestCase {
   /**
    * Test that the user has the implied permission of administer CiviCRM data by virtue of having administer CiviCRM.
    */
-  public function testImpliedPermission() {
+  public function testImpliedPermission(): void {
     $this->createLoggedInUser();
     CRM_Core_Config::singleton()->userPermissionClass->permissions = [
       'administer CiviCRM',
@@ -56,4 +56,15 @@ class CRM_Core_Permission_BaseTest extends CiviUnitTestCase {
     $this->assertTrue(CRM_Core_Permission::check('administer CiviCRM data'));
   }
 
+  /**
+   * Test that the super permission gives the implied permission of the whole shebang.
+   */
+  public function testImpliedPermissionSuperDuper(): void {
+    $this->createLoggedInUser();
+    CRM_Core_Config::singleton()->userPermissionClass->permissions = [
+      'all CiviCRM permissions and ACLs',
+    ];
+    $this->assertTrue(CRM_Core_Permission::check('view all contacts'));
+  }
+
 }