Afform - Support multiple permissions in the GUI
authorcolemanw <coleman@civicrm.org>
Sun, 3 Sep 2023 23:36:16 +0000 (19:36 -0400)
committercolemanw <coleman@civicrm.org>
Mon, 4 Sep 2023 02:41:03 +0000 (22:41 -0400)
ext/afform/admin/Civi/Api4/Action/Afform/LoadAdminData.php
ext/afform/admin/ang/afAdminFormSubmissionList.aff.json
ext/afform/admin/ang/afGuiEditor.css
ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js
ext/afform/admin/ang/afGuiEditor/config-form.html
ext/afform/core/CRM/Afform/AfformScanner.php
ext/afform/core/Civi/Api4/Action/Afform/Get.php
ext/afform/core/Civi/Api4/Afform.php
ext/afform/core/Civi/Api4/Utils/AfformSaveTrait.php
ext/afform/core/afform.php
ext/afform/mock/tests/phpunit/api/v4/AfformTest.php

index 0828b562b2b0193bbdb545ebd00c07f0a426e9af..675bee19385da3602dabbdf45a79b931b63ebd68 100644 (file)
@@ -48,7 +48,7 @@ class LoadAdminData extends \Civi\Api4\Generic\AbstractAction {
         case 'form':
           $info['definition'] = $this->definition + [
             'title' => '',
-            'permission' => 'access CiviCRM',
+            'permission' => ['access CiviCRM'],
             'layout' => [
               [
                 '#tag' => 'af-form',
@@ -70,7 +70,7 @@ class LoadAdminData extends \Civi\Api4\Generic\AbstractAction {
         case 'search':
           $info['definition'] = $this->definition + [
             'title' => '',
-            'permission' => 'access CiviCRM',
+            'permission' => ['access CiviCRM'],
             'layout' => [
               [
                 '#tag' => 'div',
index 88d0a00294bee6099ff9a446336173068b446a3e..3584ff594892c2f9b6c83462ccaa78c625f34459 100644 (file)
@@ -2,5 +2,6 @@
     "type": "system",
     "title": "Submissions",
     "server_route": "civicrm/admin/afform/submissions",
-    "permission": [["administer CiviCRM", "administer afform"]]
+    "permission": ["administer CiviCRM", "administer afform"],
+    "permission_operator": "OR"
 }
index 3aca35836f6514835a8a414cabd391a7a91d886c..b15d5892c7e4600145951c899b1c9d0a835dde03 100644 (file)
@@ -346,6 +346,13 @@ body.af-gui-dragging {
   color: #0071bd;
 }
 
+#afGuiEditor #af_config_form_permission {
+  width: calc(100% - 55px);
+}
+#afGuiEditor #af_config_form_permission_operator {
+  width: 45px;
+}
+
 #afGuiEditor .af-gui-layout-icon {
   width: 12px;
   height: 11px;
index 73ab209afe81d90cd31a41633e2ec2df1f19e8ba..571d796006741490637a48f30cedecf474a2ef61 100644 (file)
           editor.searchDisplays = getSearchDisplaysOnForm();
         }
 
+        editor.afform.permission_operator = editor.afform.permission_operator || 'AND';
+
         // Initialize undo history
         undoAction = 'initialLoad';
         undoHistory = [{
index bc858eef3e7b7f59f3f70e8ad1178b812728f165..a154730ebbb7bdb67a0b9ab51f9e8de93dbc8960 100644 (file)
     <label for="af_config_form_permission">
       {{:: ts('Permission') }}
     </label>
-    <input ng-model="editor.afform.permission" class="form-control" id="af_config_form_permission" crm-ui-select="{data: editor.meta.permissions}" >
-    <p class="help-block">{{:: ts('What permission is required to use this form?') }}</p>
+    <div class="form-inline" >
+      <input ng-model="editor.afform.permission" class="form-control" id="af_config_form_permission" crm-ui-select="{data: editor.meta.permissions, multiple: true}" ng-list >
+      <select ng-model="editor.afform.permission_operator" class="form-control" id="af_config_form_permission_operator" >
+        <option value="AND">{{:: ts('And') }}</option>
+        <option value="OR">{{:: ts('Or') }}</option>
+      </select>
+    </div>
+    <p class="help-block">
+      {{:: ts('What permission is required to use this form?') }}
+      {{:: ts('Join multiple permissions with "And" to require all, or "Or" to require at least one.') }}
+    </p>
   </div>
 
   <!-- Placement options do not apply to blocks -->
index b7f3a5a47cc85115445a91a13963b3fedc53380f..ae136c30c509d2a7d38e2da102990064958a41bc 100644 (file)
@@ -149,7 +149,7 @@ class CRM_Afform_AfformScanner {
       'is_dashlet' => FALSE,
       'is_public' => FALSE,
       'is_token' => FALSE,
-      'permission' => 'access CiviCRM',
+      'permission' => ['access CiviCRM'],
       'type' => 'system',
     ];
 
@@ -157,7 +157,7 @@ class CRM_Afform_AfformScanner {
     if ($metaFile !== NULL) {
       $r = array_merge($defaults, json_decode(file_get_contents($metaFile), 1));
       // Previous revisions of GUI allowed permission==''. array_merge() doesn't catch all forms of missing-ness.
-      if ($r['permission'] === '') {
+      if (empty($r['permission'])) {
         $r['permission'] = $defaults['permission'];
       }
       return $r;
index aa376d8a399400bffeb56d1307791f168e2102e4..9976c73aa755935cef25a73299fb830ce376847e 100644 (file)
@@ -81,6 +81,9 @@ class Get extends \Civi\Api4\Generic\BasicGetAction {
       if ($getComputed) {
         $scanner->addComputedFields($afforms[$name]);
       }
+      if (isset($afforms[$name]['permission']) && is_string($afforms[$name]['permission'])) {
+        $afforms[$name]['permission'] = explode(',', $afforms[$name]['permission']);
+      }
       if ($getLayout || $getSearchDisplays) {
         // Autogenerated layouts will already be in values but can be overridden; scanner takes priority
         $afforms[$name]['layout'] = $scanner->getLayout($name) ?? $afforms[$name]['layout'] ?? '';
@@ -163,7 +166,7 @@ class Get extends \Civi\Api4\Generic\BasicGetAction {
         'is_dashlet' => FALSE,
         'is_public' => FALSE,
         'is_token' => FALSE,
-        'permission' => 'access CiviCRM',
+        'permission' => ['access CiviCRM'],
         'join_entity' => 'Custom_' . $custom['name'],
         'entity_type' => $custom['extends'],
       ];
index 71ce29fa65d532b1c5b52aa5338f34a6528e766a..d1316c20c6130f1887639be50b22cbd6659a15a2 100644 (file)
@@ -190,6 +190,12 @@ class Afform extends Generic\AbstractEntity {
         ],
         [
           'name' => 'permission',
+          'data_type' => 'Array',
+        ],
+        [
+          'name' => 'permission_operator',
+          'data_type' => 'String',
+          'options' => \CRM_Core_SelectValues::andOr(),
         ],
         [
           'name' => 'redirect',
index 06147458ea6c3486f9788e402bb0ab0784929d25..42416f11e2777a4351c01264b7ea969d134716f4 100644 (file)
@@ -53,6 +53,9 @@ trait AfformSaveTrait {
 
     $meta = $item + (array) $orig;
     unset($meta['layout'], $meta['name']);
+    if (isset($meta['permission']) && is_string($meta['permission'])) {
+      $meta['permission'] = explode(',', $meta['permission']);
+    }
     if (!empty($meta)) {
       $metaPath = $scanner->createSiteLocalPath($item['name'], \CRM_Afform_AfformScanner::METADATA_FILE);
       \CRM_Utils_File::createDir(dirname($metaPath));
index 9aa670c910835f306af1b60d63fa8ccbda2174c8..007e52088ab0f3156b254af523d417d5ae5d269d 100644 (file)
@@ -136,8 +136,8 @@ function afform_civicrm_managed(&$entities, $modules) {
           'values' => [
             'name' => $afform['name'],
             'label' => $afform['navigation']['label'] ?: $afform['title'],
-            'permission' => (array) $afform['permission'],
-            'permission_operator' => 'OR',
+            'permission' => $afform['permission'],
+            'permission_operator' => $afform['permission_operator'] ?? 'AND',
             'weight' => $afform['navigation']['weight'] ?? 0,
             'url' => $afform['server_route'],
             'is_active' => 1,
@@ -443,14 +443,21 @@ function afform_civicrm_permission_check($permission, &$granted, $contactId) {
   if (preg_match('/^@afform:(.*)/', $permission, $m)) {
     $name = $m[1];
 
-    $afform = \Civi\Api4\Afform::get()
-      ->setCheckPermissions(FALSE)
+    $afform = \Civi\Api4\Afform::get(FALSE)
       ->addWhere('name', '=', $name)
-      ->setSelect(['permission'])
+      ->addSelect('permission', 'permission_operator')
       ->execute()
       ->first();
+    // No permissions found... this shouldn't happen but just in case, set default.
+    if ($afform && empty($afform['permission'])) {
+      $afform['permission'] = ['access CiviCRM'];
+    }
     if ($afform) {
-      $granted = CRM_Core_Permission::check($afform['permission'], $contactId);
+      $check = (array) $afform['permission'];
+      if ($afform['permission_operator'] === 'OR') {
+        $check = [$check];
+      }
+      $granted = CRM_Core_Permission::check($check, $contactId);
     }
   }
 }
index 8f093eac42b4f2b673febd29035644232ddc8a21..a673e680a833518f1f0c06289d0b66551b7f038c 100644 (file)
@@ -31,10 +31,10 @@ class api_v4_AfformTest extends api_v4_AfformTestCase {
 
   public function getBasicDirectives() {
     return [
-      ['mockPage', ['title' => '', 'description' => '', 'server_route' => 'civicrm/mock-page', 'permission' => 'access Foobar', 'is_dashlet' => TRUE]],
-      ['mockBareFile', ['title' => '', 'description' => '', 'permission' => 'access CiviCRM', 'is_dashlet' => FALSE]],
-      ['mockFoo', ['title' => '', 'description' => '', 'permission' => 'access CiviCRM']],
-      ['mock-weird-name', ['title' => 'Weird Name', 'description' => '', 'permission' => 'access CiviCRM']],
+      ['mockPage', ['title' => '', 'description' => '', 'server_route' => 'civicrm/mock-page', 'permission' => ['access Foobar'], 'is_dashlet' => TRUE]],
+      ['mockBareFile', ['title' => '', 'description' => '', 'permission' => ['access CiviCRM'], 'is_dashlet' => FALSE]],
+      ['mockFoo', ['title' => '', 'description' => '', 'permission' => ['access CiviCRM']]],
+      ['mock-weird-name', ['title' => 'Weird Name', 'description' => '', 'permission' => ['access CiviCRM']]],
     ];
   }
 
@@ -85,7 +85,7 @@ class api_v4_AfformTest extends api_v4_AfformTestCase {
     $result = Civi\Api4\Afform::update()
       ->addWhere('name', '=', $formName)
       ->addValue('description', 'The temporary description')
-      ->addValue('permission', 'access foo')
+      ->addValue('permission', ['access foo', 'access bar'])
       ->addValue('is_dashlet', empty($originalMetadata['is_dashlet']))
       ->execute();
     $this->assertEquals($formName, $result[0]['name'], $message);
@@ -100,7 +100,7 @@ class api_v4_AfformTest extends api_v4_AfformTestCase {
     $this->assertEquals('The temporary description', $get($result[0], 'description'), $message);
     $this->assertEquals(empty($originalMetadata['is_dashlet']), $get($result[0], 'is_dashlet'), $message);
     $this->assertEquals($get($originalMetadata, 'server_route'), $get($result[0], 'server_route'), $message);
-    $this->assertEquals('access foo', $get($result[0], 'permission'), $message);
+    $this->assertEquals(['access foo', 'access bar'], $get($result[0], 'permission'), $message);
     $this->assertTrue(is_array($result[0]['layout']), $message);
     $this->assertEquals(TRUE, $get($result[0], 'has_base'), $message);
     $this->assertEquals(TRUE, $get($result[0], 'has_local'), $message);