Add permission support for fields, columns and filter scopes (#4520)

Credit to @Samuell1. Fixes #1837.
This commit is contained in:
Samuell 2019-10-10 00:41:53 +02:00 committed by Luke Towers
parent e246427463
commit 348040a4e4
11 changed files with 650 additions and 5 deletions

View File

@ -51,6 +51,8 @@
},
"autoload-dev": {
"classmap": [
"tests/concerns/InteractsWithAuthentication.php",
"tests/fixtures/backend/models/UserFixture.php",
"tests/TestCase.php",
"tests/UiTestCase.php",
"tests/PluginTestCase.php"

View File

@ -9,6 +9,7 @@ use Carbon\Carbon;
use Backend\Classes\WidgetBase;
use Backend\Classes\FilterScope;
use ApplicationException;
use BackendAuth;
/**
* Filter Widget
@ -559,6 +560,14 @@ class Filter extends WidgetBase
public function addScopes(array $scopes)
{
foreach ($scopes as $name => $config) {
/*
* Check if user has permissions to show this filter
*/
$permissions = array_get($config, 'permissions');
if (!empty($permissions) && !BackendAuth::getUser()->hasAccess($permissions, false)) {
continue;
}
$scopeObj = $this->makeFilterScope($name, $config);
/*

View File

@ -11,6 +11,7 @@ use October\Rain\Database\Model;
use October\Rain\Html\Helper as HtmlHelper;
use ApplicationException;
use Exception;
use BackendAuth;
/**
* Form Widget
@ -682,6 +683,12 @@ class Form extends WidgetBase
public function addFields(array $fields, $addToArea = null)
{
foreach ($fields as $name => $config) {
// Check if user has permissions to show this field
$permissions = array_get($config, 'permissions');
if (!empty($permissions) && !BackendAuth::getUser()->hasAccess($permissions, false)) {
continue;
}
$fieldObj = $this->makeFormField($name, $config);
$fieldTab = is_array($config) ? array_get($config, 'tab') : null;

View File

@ -14,6 +14,7 @@ use Backend\Classes\ListColumn;
use Backend\Classes\WidgetBase;
use October\Rain\Database\Model;
use ApplicationException;
use BackendAuth;
/**
* List Widget
@ -709,6 +710,10 @@ class Lists extends WidgetBase
*/
public function getColumn($column)
{
if (!isset($this->allColumns[$column])) {
throw new ApplicationException('No definition for column ' . $column);
}
return $this->allColumns[$column];
}
@ -852,6 +857,12 @@ class Lists extends WidgetBase
* Build a final collection of list column objects
*/
foreach ($columns as $columnName => $config) {
// Check if user has permissions to show this column
$permissions = array_get($config, 'permissions');
if (!empty($permissions) && !BackendAuth::getUser()->hasAccess($permissions, false)) {
continue;
}
$this->allColumns[$columnName] = $this->makeListColumn($columnName, $config);
}
}
@ -1150,7 +1161,7 @@ class Lists extends WidgetBase
return call_user_func_array($callback, [$value, $column, $record]);
}
}
$customMessage = '';
if ($type === 'relation') {
$customMessage = 'Type: relation is not supported, instead use the relation property to specify a relationship to pull the value from and set the type to the type of the value expected.';

View File

@ -1,11 +1,15 @@
<?php
use Backend\Classes\AuthManager;
use System\Classes\UpdateManager;
use System\Classes\PluginManager;
use October\Rain\Database\Model as ActiveRecord;
use October\Tests\Concerns\InteractsWithAuthentication;
abstract class PluginTestCase extends Illuminate\Foundation\Testing\TestCase
abstract class PluginTestCase extends TestCase
{
use InteractsWithAuthentication;
/**
* @var array Cache for storing which plugins have been loaded
* and refreshed.
@ -24,6 +28,12 @@ abstract class PluginTestCase extends Illuminate\Foundation\Testing\TestCase
$app['cache']->setDefaultDriver('array');
$app->setLocale('en');
$app->singleton('auth', function ($app) {
$app['auth.loaded'] = true;
return AuthManager::instance();
});
/*
* Store database in memory by default, if not specified otherwise
*/

View File

@ -1,8 +1,6 @@
<?php
class TestCase extends Illuminate\Foundation\Testing\TestCase
{
/**
* Creates the application.
*
@ -11,6 +9,7 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
public function createApplication()
{
$app = require __DIR__.'/../bootstrap/app.php';
$app->make('Illuminate\Contracts\Console\Kernel')->bootstrap();
$app['cache']->setDefaultDriver('array');

View File

@ -0,0 +1,149 @@
<?php
namespace October\Tests\Concerns;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;
trait InteractsWithAuthentication
{
/**
* Set the currently logged in user for the application.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param string|null $driver
* @return $this
*/
public function actingAs(UserContract $user, $driver = null)
{
$this->be($user, $driver);
return $this;
}
/**
* Set the currently logged in user for the application.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param string|null $driver
* @return void
*/
public function be(UserContract $user, $driver = null)
{
$this->app['auth']->setUser($user);
}
/**
* Assert that the user is authenticated.
*
* @param string|null $guard
* @return $this
*/
public function assertAuthenticated($guard = null)
{
$this->assertTrue($this->isAuthenticated($guard), 'The user is not authenticated');
return $this;
}
/**
* Assert that the user is not authenticated.
*
* @param string|null $guard
* @return $this
*/
public function assertGuest($guard = null)
{
$this->assertFalse($this->isAuthenticated($guard), 'The user is authenticated');
return $this;
}
/**
* Return true if the user is authenticated, false otherwise.
*
* @param string|null $guard
* @return bool
*/
protected function isAuthenticated($guard = null)
{
return $this->app->make('auth')->guard($guard)->check();
}
/**
* Assert that the user is authenticated as the given user.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param string|null $guard
* @return $this
*/
public function assertAuthenticatedAs($user, $guard = null)
{
$expected = $this->app->make('auth')->guard($guard)->user();
$this->assertNotNull($expected, 'The current user is not authenticated.');
$this->assertInstanceOf(
get_class($expected),
$user,
'The currently authenticated user is not who was expected'
);
$this->assertSame(
$expected->getAuthIdentifier(),
$user->getAuthIdentifier(),
'The currently authenticated user is not who was expected'
);
return $this;
}
/**
* Assert that the given credentials are valid.
*
* @param array $credentials
* @param string|null $guard
* @return $this
*/
public function assertCredentials(array $credentials, $guard = null)
{
$this->assertTrue(
$this->hasCredentials($credentials, $guard),
'The given credentials are invalid.'
);
return $this;
}
/**
* Assert that the given credentials are invalid.
*
* @param array $credentials
* @param string|null $guard
* @return $this
*/
public function assertInvalidCredentials(array $credentials, $guard = null)
{
$this->assertFalse(
$this->hasCredentials($credentials, $guard),
'The given credentials are valid.'
);
return $this;
}
/**
* Return true if the credentials are valid, false otherwise.
*
* @param array $credentials
* @param string|null $guard
* @return bool
*/
protected function hasCredentials(array $credentials, $guard = null)
{
$provider = $this->app->make('auth')->guard($guard)->getProvider();
$user = $provider->retrieveByCredentials($credentials);
return $user && $provider->validateCredentials($user, $credentials);
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace October\Tests\Fixtures\Backend\Models;
use Backend\Models\User;
class UserFixture extends User
{
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->fill([
'first_name' => 'Test',
'last_name' => 'User',
'login' => 'testuser',
'email' => 'testuser@test.com',
'password' => '',
'activation_code' => null,
'persist_code' => null,
'reset_password_code' => null,
'permissions' => null,
'is_activated' => true,
'role_id' => null,
'activated_at' => null,
'last_login' => '2019-09-27 12:00:00',
'created_at' => '2019-09-27 12:00:00',
'updated_at' => '2019-09-27 12:00:00',
'deleted_at' => null,
'is_superuser' => false
]);
}
public function asSuperUser()
{
$this->setAttribute('is_superuser', true);
return $this;
}
public function asDeletedUser()
{
$this->setAttribute('deleted_at', date('Y-m-d H:i:s'));
return $this;
}
public function withPermission($permission, bool $granted = true)
{
$currentPermissions = $this->getAttribute('permissions');
if (is_string($permission)) {
$permission = [
$permission => (int) $granted
];
}
if (is_array($currentPermissions)) {
$this->setAttribute('permissions', array_replace($currentPermissions, $permission));
} else {
$this->setAttribute('permissions', $permission);
}
return $this;
}
}

View File

@ -0,0 +1,139 @@
<?php
use Backend\Widgets\Filter;
use Backend\Models\User;
use October\Tests\Fixtures\Backend\Models\UserFixture;
class FilterTest extends PluginTestCase
{
public function testRestrictedScopeWithUserWithNoPermissions()
{
$user = new UserFixture;
$this->actingAs($user);
$filter = $this->restrictedFilterFixture();
$filter->render();
$this->assertNotNull($filter->getScope('id'));
// Expect an exception
$this->expectException(ApplicationException::class);
$this->expectExceptionMessage('No definition for scope email');
$scope = $filter->getScope('email');
}
public function testRestrictedScopeWithUserWithWrongPermissions()
{
$user = new UserFixture;
$this->actingAs($user->withPermission('test.wrong_permission', true));
$filter = $this->restrictedFilterFixture();
$filter->render();
$this->assertNotNull($filter->getScope('id'));
// Expect an exception
$this->expectException(ApplicationException::class);
$this->expectExceptionMessage('No definition for scope email');
$scope = $filter->getScope('email');
}
public function testRestrictedScopeWithUserWithRightPermissions()
{
$user = new UserFixture;
$this->actingAs($user->withPermission('test.access_field', true));
$filter = $this->restrictedFilterFixture();
$filter->render();
$this->assertNotNull($filter->getScope('id'));
$this->assertNotNull($filter->getScope('email'));
}
public function testRestrictedScopeWithUserWithRightWildcardPermissions()
{
$user = new UserFixture;
$this->actingAs($user->withPermission('test.access_field', true));
$filter = new Filter(null, [
'model' => new User,
'arrayName' => 'array',
'scopes' => [
'id' => [
'type' => 'text',
'label' => 'ID'
],
'email' => [
'type' => 'text',
'label' => 'Email',
'permission' => 'test.*'
]
]
]);
$filter->render();
$this->assertNotNull($filter->getScope('id'));
$this->assertNotNull($filter->getScope('email'));
}
public function testRestrictedScopeWithSuperuser()
{
$user = new UserFixture;
$this->actingAs($user->asSuperUser());
$filter = $this->restrictedFilterFixture();
$filter->render();
$this->assertNotNull($filter->getScope('id'));
$this->assertNotNull($filter->getScope('email'));
}
public function testRestrictedScopeSinglePermissionWithUserWithWrongPermissions()
{
$user = new UserFixture;
$this->actingAs($user->withPermission('test.wrong_permission', true));
$filter = $this->restrictedFilterFixture(true);
$filter->render();
$this->assertNotNull($filter->getScope('id'));
// Expect an exception
$this->expectException(ApplicationException::class);
$this->expectExceptionMessage('No definition for scope email');
$scope = $filter->getScope('email');
}
public function testRestrictedScopeSinglePermissionWithUserWithRightPermissions()
{
$user = new UserFixture;
$this->actingAs($user->withPermission('test.access_field', true));
$filter = $this->restrictedFilterFixture(true);
$filter->render();
$this->assertNotNull($filter->getScope('id'));
$this->assertNotNull($filter->getScope('email'));
}
protected function restrictedFilterFixture(bool $singlePermission = false)
{
return new Filter(null, [
'model' => new User,
'arrayName' => 'array',
'scopes' => [
'id' => [
'type' => 'text',
'label' => 'ID'
],
'email' => [
'type' => 'text',
'label' => 'Email',
'permissions' => ($singlePermission) ? 'test.access_field' : [
'test.access_field'
]
]
]
]);
}
}

View File

@ -2,14 +2,106 @@
use Backend\Widgets\Form;
use Illuminate\Database\Eloquent\Model;
use October\Tests\Fixtures\Backend\Models\UserFixture;
class FormTestModel extends Model
{
}
class FormTest extends TestCase
class FormTest extends PluginTestCase
{
public function testRestrictedFieldWithUserWithNoPermissions()
{
$user = new UserFixture;
$this->actingAs($user);
$form = $this->restrictedFormFixture();
$form->render();
$this->assertNull($form->getField('testRestricted'));
}
public function testRestrictedFieldWithUserWithWrongPermissions()
{
$user = new UserFixture;
$this->actingAs($user->withPermission('test.wrong_permission', true));
$form = $this->restrictedFormFixture();
$form->render();
$this->assertNull($form->getField('testRestricted'));
}
public function testRestrictedFieldWithUserWithRightPermissions()
{
$user = new UserFixture;
$this->actingAs($user->withPermission('test.access_field', true));
$form = $this->restrictedFormFixture();
$form->render();
$this->assertNotNull($form->getField('testRestricted'));
}
public function testRestrictedFieldWithUserWithRightWildcardPermissions()
{
$user = new UserFixture;
$this->actingAs($user->withPermission('test.access_field', true));
$form = new Form(null, [
'model' => new FormTestModel,
'arrayName' => 'array',
'fields' => [
'testField' => [
'type' => 'text',
'label' => 'Test 1'
],
'testRestricted' => [
'type' => 'text',
'label' => 'Test 2',
'permission' => 'test.*'
]
]
]);
$form->render();
$this->assertNotNull($form->getField('testRestricted'));
}
public function testRestrictedFieldWithSuperuser()
{
$user = new UserFixture;
$this->actingAs($user->asSuperUser());
$form = $this->restrictedFormFixture();
$form->render();
$this->assertNotNull($form->getField('testRestricted'));
}
public function testRestrictedFieldSinglePermissionWithUserWithWrongPermissions()
{
$user = new UserFixture;
$this->actingAs($user->withPermission('test.wrong_permission', true));
$form = $this->restrictedFormFixture(true);
$form->render();
$this->assertNull($form->getField('testRestricted'));
}
public function testRestrictedFieldSinglePermissionWithUserWithRightPermissions()
{
$user = new UserFixture;
$this->actingAs($user->withPermission('test.access_field', true));
$form = $this->restrictedFormFixture(true);
$form->render();
$this->assertNotNull($form->getField('testRestricted'));
}
public function testCheckboxlistTrigger()
{
$form = new Form(null, [
@ -38,4 +130,25 @@ class FormTest extends TestCase
$attributes = $form->getField('triggered')->getAttributes('container', false);
$this->assertEquals('[name="array[trigger][]"]', array_get($attributes, 'data-trigger'));
}
protected function restrictedFormFixture(bool $singlePermission = false)
{
return new Form(null, [
'model' => new FormTestModel,
'arrayName' => 'array',
'fields' => [
'testField' => [
'type' => 'text',
'label' => 'Test 1'
],
'testRestricted' => [
'type' => 'text',
'label' => 'Test 2',
'permissions' => ($singlePermission) ? 'test.access_field' : [
'test.access_field'
]
]
]
]);
}
}

View File

@ -0,0 +1,140 @@
<?php
use Backend\Models\User;
use Backend\Widgets\Lists;
use October\Rain\Exception\ApplicationException;
use October\Tests\Fixtures\Backend\Models\UserFixture;
class ListsTest extends PluginTestCase
{
public function testRestrictedColumnWithUserWithNoPermissions()
{
$user = new UserFixture;
$this->actingAs($user);
$list = $this->restrictedListsFixture();
$list->render();
$this->assertNotNull($list->getColumn('id'));
// Expect an exception
$this->expectException(ApplicationException::class);
$this->expectExceptionMessage('No definition for column email');
$column = $list->getColumn('email');
}
public function testRestrictedColumnWithUserWithWrongPermissions()
{
$user = new UserFixture;
$this->actingAs($user->withPermission('test.wrong_permission', true));
$list = $this->restrictedListsFixture();
$list->render();
$this->assertNotNull($list->getColumn('id'));
// Expect an exception
$this->expectException(ApplicationException::class);
$this->expectExceptionMessage('No definition for column email');
$column = $list->getColumn('email');
}
public function testRestrictedColumnWithUserWithRightPermissions()
{
$user = new UserFixture;
$this->actingAs($user->withPermission('test.access_field', true));
$list = $this->restrictedListsFixture();
$list->render();
$this->assertNotNull($list->getColumn('id'));
$this->assertNotNull($list->getColumn('email'));
}
public function testRestrictedColumnWithUserWithRightWildcardPermissions()
{
$user = new UserFixture;
$this->actingAs($user->withPermission('test.access_field', true));
$list = new Lists(null, [
'model' => new User,
'arrayName' => 'array',
'columns' => [
'id' => [
'type' => 'text',
'label' => 'ID'
],
'email' => [
'type' => 'text',
'label' => 'Email',
'permission' => 'test.*'
]
]
]);
$list->render();
$this->assertNotNull($list->getColumn('id'));
$this->assertNotNull($list->getColumn('email'));
}
public function testRestrictedColumnWithSuperuser()
{
$user = new UserFixture;
$this->actingAs($user->asSuperUser());
$list = $this->restrictedListsFixture();
$list->render();
$this->assertNotNull($list->getColumn('id'));
$this->assertNotNull($list->getColumn('email'));
}
public function testRestrictedColumnSinglePermissionWithUserWithWrongPermissions()
{
$user = new UserFixture;
$this->actingAs($user->withPermission('test.wrong_permission', true));
$list = $this->restrictedListsFixture(true);
$list->render();
$this->assertNotNull($list->getColumn('id'));
// Expect an exception
$this->expectException(ApplicationException::class);
$this->expectExceptionMessage('No definition for column email');
$column = $list->getColumn('email');
}
public function testRestrictedColumnSinglePermissionWithUserWithRightPermissions()
{
$user = new UserFixture;
$this->actingAs($user->withPermission('test.access_field', true));
$list = $this->restrictedListsFixture(true);
$list->render();
$this->assertNotNull($list->getColumn('id'));
$this->assertNotNull($list->getColumn('email'));
}
protected function restrictedListsFixture(bool $singlePermission = false)
{
return new Lists(null, [
'model' => new User,
'arrayName' => 'array',
'columns' => [
'id' => [
'type' => 'text',
'label' => 'ID'
],
'email' => [
'type' => 'text',
'label' => 'Email',
'permissions' => ($singlePermission) ? 'test.access_field' : [
'test.access_field'
]
]
]
]);
}
}