Commit 6c02a06e authored by colemanw's avatar colemanw
Browse files

Merge branch 'master-perm' into 'master'

Add `permission` option, which is used by API (prefill/submit) and the router

See merge request extensions/afform!17
parents 9500d0c7 7a5798d5
......@@ -18,7 +18,13 @@ pushd "$AFF_CORE" >> /dev/null
popd >> /dev/null
pushd "$AFF_MOCK" >> /dev/null
if ! phpunit6 "$@" ; then
if ! phpunit6 --group e2e "$@" ; then
EXIT=1
fi
popd >> /dev/null
pushd "$AFF_MOCK" >> /dev/null
if ! phpunit6 --group headless "$@" ; then
EXIT=1
fi
popd >> /dev/null
......
......@@ -138,6 +138,7 @@ class CRM_Afform_AfformScanner {
'title' => '',
'description' => '',
'is_public' => FALSE,
'permission' => 'access CiviCRM',
];
$metaFile = $this->findFilePath($name, self::METADATA_FILE);
......
......@@ -35,6 +35,12 @@ abstract class AbstractProcessor extends \Civi\Api4\Generic\AbstractAction {
public function _run(Result $result) {
// This will throw an exception if the form doesn't exist
$this->_afform = (array) civicrm_api4('Afform', 'get', ['checkPermissions' => FALSE, 'where' => [['name', '=', $this->name]]], 0);
if ($this->getCheckPermissions()) {
if (!\CRM_Core_Permission::check("@afform:" . $this->_afform['name'])) {
throw new \Civi\API\Exception\UnauthorizedException("Authorization failed: Cannot process form " . $this->_afform['name']);
}
}
$this->_formDataModel = FormDataModel::create($this->_afform['layout']);
$this->validateArgs();
$result->exchangeArray($this->processForm());
......
......@@ -107,6 +107,9 @@ class Afform extends AbstractEntity {
[
'name' => 'server_route',
],
[
'name' => 'permission',
],
[
'name' => 'layout',
],
......
......@@ -63,7 +63,11 @@ trait AfformSaveTrait {
// We may have changed list of files covered by the cache.
_afform_clear();
if (($item['server_route'] ?? NULL) !== ($orig['server_route'] ?? NULL)) {
$isChanged = function($field) use ($item, $orig) {
return ($item[$field] ?? NULL) !== ($orig[$field] ?? NULL);
};
// Right now, permission-checks are completely on-demand.
if ($isChanged('server_route') /* || $isChanged('permission') */) {
\CRM_Core_Menu::store();
\CRM_Core_BAO_Navigation::resetNavigation();
}
......
......@@ -452,13 +452,41 @@ function afform_civicrm_alterMenu(&$items) {
'page_callback' => 'CRM_Afform_Page_AfformBase',
'page_arguments' => 'afform=' . urlencode($name),
'title' => $meta['title'] ?? '',
'access_arguments' => [['access CiviCRM'], 'and'], // FIXME
'access_arguments' => [["@afform:$name"], 'and'],
'is_public' => $meta['is_public'],
];
}
}
}
/**
* Implements hook_civicrm_permission_check().
*
* This extends the list of permissions available in `CRM_Core_Permission:check()`
* by introducing virtual-permissions named `@afform:myForm`. The evaluation
* of these virtual-permissions is dependent on the settings for `myForm`.
* `myForm` may be exposed/integrated through multiple subsystems (routing,
* nav-menu, API, etc), and the use of virtual-permissions makes easy to enforce
* consistent permissions across any relevant subsystems.
*
* @see CRM_Utils_Hook::permission_check()
*/
function afform_civicrm_permission_check($permission, &$granted, $contactId) {
if ($permission{0} !== '@') {
// Micro-optimization - this function may get hit a lot.
return;
}
if (preg_match('/^@afform:(.*)/', $permission, $m)) {
$name = $m[1];
/** @var CRM_Afform_AfformScanner $scanner */
$scanner = \Civi::container()->get('afform_scanner');
$meta = $scanner->getMeta($name);
$granted = CRM_Core_Permission::check($meta['permission'], $contactId);
}
}
/**
* Clear any local/in-memory caches based on afform data.
*/
......
{
"title": "Afform Administration",
"server_route": "civicrm/admin/afform",
"permission": "administer CiviCRM",
"requires": [
"afGuiEditor",
"ui.sortable"
......
......@@ -10,3 +10,7 @@
{{ ts('URL:') }}
</label>
<input ng-model="afform.server_route" class="form-control" id="af_config_form_server_route" />
<label for="af_config_form_permission">
{{ ts('Permission:') }}
</label>
<input ng-model="afform.permission" class="form-control" id="af_config_form_permission" />
{
"title": "Afform HTML Administration",
"server_route": "civicrm/admin/afform-html"
"server_route": "civicrm/admin/afform-html",
"permission": "administer CiviCRM"
}
......@@ -20,7 +20,8 @@
<legend>{{ts('Properties')}}</legend>
<div><label>{{ts('Name')}}:</label> {{resultForm.name}}</div>
<div><label>{{ts('Title')}}:</label> <input ng-model="resultForm.title" type="text" /></div>
<div><label>{{ts('Server Router')}}:</label> <input ng-model="resultForm.server_route" type="text" /></div>
<div><label>{{ts('Server Route')}}:</label> <input ng-model="resultForm.server_route" type="text" /></div>
<div><label>{{ts('Permission')}}:</label> <input ng-model="resultForm.permission" type="text" /></div>
<div><label>{{ts('Description')}}:</label> <textarea ng-model="resultForm.description"></textarea></div>
</fieldset>
......
<?php
// This file declares an Angular module which can be autoloaded
// in CiviCRM. See also:
// \https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_angularModules/n
return array (
'js' =>
array (
0 => 'ang/mockBespoke.js',
1 => 'ang/mockBespoke/*.js',
2 => 'ang/mockBespoke/*/*.js',
),
'css' =>
array (
0 => 'ang/mockBespoke.css',
),
'partials' =>
array (
0 => 'ang/mockBespoke',
),
'requires' =>
array (
0 => 'crmUi',
1 => 'crmUtil',
2 => 'ngRoute',
),
'settings' =>
array (
),
);
/* Add any CSS rules for Angular module "mockBespoke" */
(function(angular, $, _) {
// Declare a list of dependencies.
angular.module('mockBespoke', CRM.angRequires('mockBespoke'));
})(angular, CRM.$, CRM._);
{"server_route": "civicrm/mock-page", "requires":["extraMock"]}
{"server_route": "civicrm/mock-page", "requires":["mockBespoke"], "permission": "access Foobar" }
<?php
/**
* Ensure that the routes created by Afform are working.
* @group e2e
*/
class api_v4_AfformRoutingTest extends \PHPUnit\Framework\TestCase implements \Civi\Test\EndToEndInterface {
protected $formName = 'mockPage';
public static function setUpBeforeClass() {
\Civi\Test::e2e()
->install(['org.civicrm.afform', 'org.civicrm.afform-mock'])
->apply();
}
public function setUp() {
parent::setUp();
Civi\Api4\Afform::revert()
->setCheckPermissions(FALSE)
->addWhere('name', '=', $this->formName)
->execute();
}
public function tearDown() {
parent::tearDown();
Civi\Api4\Afform::revert()
->setCheckPermissions(FALSE)
->addWhere('name', '=', $this->formName)
->execute();
}
public function testChangingPermissions() {
$http = new \GuzzleHttp\Client(['http_errors' => FALSE]);
$url = function ($path, $query = NULL) {
return CRM_Utils_System::url($path, $query, TRUE, NULL, FALSE);
};
$result = $http->get($url('civicrm/mock-page'));
$this->assertNotAuthorized($result);
Civi\Api4\Afform::update()
->setCheckPermissions(FALSE)
->addWhere('name', '=', $this->formName)
->addValue('permission', CRM_Core_Permission::ALWAYS_ALLOW_PERMISSION)
->execute();
$result = $http->get($url('civicrm/mock-page'));
$this->assertOpensPage($result, 'mock-page');
}
public function testChangingPath() {
$http = new \GuzzleHttp\Client(['http_errors' => FALSE]);
$url = function ($path, $query = NULL) {
return CRM_Utils_System::url($path, $query, TRUE, NULL, FALSE);
};
Civi\Api4\Afform::update()
->setCheckPermissions(FALSE)
->addWhere('name', '=', $this->formName)
->addValue('permission', CRM_Core_Permission::ALWAYS_ALLOW_PERMISSION)
->execute();
$this->assertOpensPage($http->get($url('civicrm/mock-page')), 'mock-page');
$this->assertNotAuthorized($http->get($url('civicrm/mock-page-renamed')));
Civi\Api4\Afform::update()
->setCheckPermissions(FALSE)
->addWhere('name', '=', $this->formName)
->addValue('server_route', 'civicrm/mock-page-renamed')
->execute();
$this->assertNotAuthorized($http->get($url('civicrm/mock-page')));
$this->assertOpensPage($http->get($url('civicrm/mock-page-renamed')), 'mock-page');
}
/**
* @param $result
*/
private function assertNotAuthorized(Psr\Http\Message\ResponseInterface $result) {
$contents = $result->getBody()->getContents();
$this->assertEquals(403, $result->getStatusCode());
$this->assertRegExp(';You are not authorized to access;', $contents);
$this->assertNotRegExp(';afform":\{"open":".*"\};', $contents);
}
/**
* @param $result
* @param string $directive
* The name of the directive which auto-opens.
*/
private function assertOpensPage(Psr\Http\Message\ResponseInterface $result, $directive) {
$contents = $result->getBody()->getContents();
$this->assertEquals(200, $result->getStatusCode());
$this->assertNotRegExp(';You are not authorized to access;', $contents);
$this->assertRegExp(';afform":\{"open":"' . preg_quote($directive, ';') . '"\};', $contents);
}
}
......@@ -30,10 +30,10 @@ class api_v4_AfformTest extends api_v4_AfformTestCase {
public function getBasicDirectives() {
return [
['mockPage', ['title' => '', 'description' => '', 'server_route' => 'civicrm/mock-page']],
['mockBareFile', ['title' => '', 'description' => '']],
['mockFoo', ['title' => '', 'description' => '']],
['mock-weird-name', ['title' => 'Weird Name', 'description' => '']],
['mockPage', ['title' => '', 'description' => '', 'server_route' => 'civicrm/mock-page', 'permission' => 'access Foobar']],
['mockBareFile', ['title' => '', 'description' => '', 'permission' => 'access CiviCRM']],
['mockFoo', ['title' => '', 'description' => '', 'permission' => 'access CiviCRM']],
['mock-weird-name', ['title' => 'Weird Name', 'description' => '', 'permission' => 'access CiviCRM']],
];
}
......@@ -58,6 +58,7 @@ class api_v4_AfformTest extends api_v4_AfformTestCase {
$this->assertEquals($get($originalMetadata, 'title'), $get($result[0], 'title'), $message);
$this->assertEquals($get($originalMetadata, 'description'), $get($result[0], 'description'), $message);
$this->assertEquals($get($originalMetadata, 'server_route'), $get($result[0], 'server_route'), $message);
$this->assertEquals($get($originalMetadata, 'permission'), $get($result[0], 'permission'), $message);
$this->assertTrue(is_array($result[0]['layout']), $message);
$this->assertEquals(TRUE, $get($result[0], 'has_packaged'), $message);
$this->assertEquals(FALSE, $get($result[0], 'has_local'), $message);
......@@ -66,6 +67,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')
->execute();
$this->assertEquals($formName, $result[0]['name'], $message);
$this->assertEquals('The temporary description', $result[0]['description'], $message);
......@@ -76,6 +78,7 @@ class api_v4_AfformTest extends api_v4_AfformTestCase {
$this->assertEquals($get($originalMetadata, 'title'), $get($result[0], 'title'), $message);
$this->assertEquals('The temporary description', $get($result[0], 'description'), $message);
$this->assertEquals($get($originalMetadata, 'server_route'), $get($result[0], 'server_route'), $message);
$this->assertEquals('access foo', $get($result[0], 'permission'), $message);
$this->assertTrue(is_array($result[0]['layout']), $message);
$this->assertEquals(TRUE, $get($result[0], 'has_packaged'), $message);
$this->assertEquals(TRUE, $get($result[0], 'has_local'), $message);
......@@ -87,6 +90,7 @@ class api_v4_AfformTest extends api_v4_AfformTestCase {
$this->assertEquals($get($originalMetadata, 'title'), $get($result[0], 'title'), $message);
$this->assertEquals($get($originalMetadata, 'description'), $get($result[0], 'description'), $message);
$this->assertEquals($get($originalMetadata, 'server_route'), $get($result[0], 'server_route'), $message);
$this->assertEquals($get($originalMetadata, 'permission'), $get($result[0], 'permission'), $message);
$this->assertTrue(is_array($result[0]['layout']), $message);
$this->assertEquals(TRUE, $get($result[0], 'has_packaged'), $message);
$this->assertEquals(FALSE, $get($result[0], 'has_local'), $message);
......@@ -201,9 +205,9 @@ class api_v4_AfformTest extends api_v4_AfformTestCase {
// The default mockPage has 1 explicit requirement + 2 automatic requirements.
Civi\Api4\Afform::revert()->addWhere('name', '=', $formName)->execute();
$angModule = Civi::service('angular')->getModule($formName);
$this->assertEquals(['afCore', 'extraMock', 'mockBareFile', 'mockFoo'], $angModule['requires']);
$this->assertEquals(['afCore', 'mockBespoke', 'mockBareFile', 'mockFoo'], $angModule['requires']);
$storedRequires = Civi\Api4\Afform::get()->addWhere('name', '=', $formName)->addSelect('requires')->execute();
$this->assertEquals(['extraMock'], $storedRequires[0]['requires']);
$this->assertEquals(['mockBespoke'], $storedRequires[0]['requires']);
// Knock down to 1 explicit + 1 automatic.
Civi\Api4\Afform::update()
......@@ -212,9 +216,9 @@ class api_v4_AfformTest extends api_v4_AfformTestCase {
->setValues(['layout' => '<div>The bare file says "<span mock-bare-file/>"</div>'])
->execute();
$angModule = Civi::service('angular')->getModule($formName);
$this->assertEquals(['afCore', 'extraMock', 'mockBareFile'], $angModule['requires']);
$this->assertEquals(['afCore', 'mockBespoke', 'mockBareFile'], $angModule['requires']);
$storedRequires = Civi\Api4\Afform::get()->addWhere('name', '=', $formName)->addSelect('requires')->execute();
$this->assertEquals(['extraMock'], $storedRequires[0]['requires']);
$this->assertEquals(['mockBespoke'], $storedRequires[0]['requires']);
// Remove the last explict and implicit requirements.
Civi\Api4\Afform::update()
......@@ -232,7 +236,7 @@ class api_v4_AfformTest extends api_v4_AfformTestCase {
Civi\Api4\Afform::revert()->addWhere('name', '=', $formName)->execute();
$angModule = Civi::service('angular')->getModule($formName);
$this->assertEquals(['afCore', 'extraMock', 'mockBareFile', 'mockFoo'], $angModule['requires']);
$this->assertEquals(['afCore', 'mockBespoke', 'mockBareFile', 'mockFoo'], $angModule['requires']);
}
}
......@@ -71,6 +71,42 @@ EOHTML;
$this->assertEquals('Lasty', $contact['last_name']);
}
public function testAboutMeForbidden() {
$this->useValues([
'layout' => self::$layouts['aboutMe'],
'permission' => CRM_Core_Permission::ALWAYS_DENY_PERMISSION,
]);
$this->createLoggedInUser();
CRM_Core_Config::singleton()->userPermissionTemp = new CRM_Core_Permission_Temp();
try {
Civi\Api4\Afform::prefill()
->setName($this->formName)
->setArgs([])
->execute()
->indexBy('name');
$this->fail('Expected authorization exception from Afform.prefill');
}
catch (\Civi\API\Exception\UnauthorizedException $e) {
$this->assertRegExp(';Authorization failed: Cannot process form mock\d+;', $e->getMessage());
}
try {
Civi\Api4\Afform::submit()
->setName($this->formName)
->setArgs([])
->setValues([
'does.n' => 'tmatter',
])
->execute();
$this->fail('Expected authorization exception from Afform.submit');
}
catch (\Civi\API\Exception\UnauthorizedException $e) {
$this->assertRegExp(';Authorization failed: Cannot process form mock\d+;', $e->getMessage());
}
}
protected function useValues($values) {
$defaults = [
'title' => 'My form',
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment