diff --git a/composer.json b/composer.json index 2050781e1..e08f1cdde 100644 --- a/composer.json +++ b/composer.json @@ -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" diff --git a/modules/backend/widgets/Filter.php b/modules/backend/widgets/Filter.php index 54789b9a3..5deb97210 100644 --- a/modules/backend/widgets/Filter.php +++ b/modules/backend/widgets/Filter.php @@ -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); /* diff --git a/modules/backend/widgets/Form.php b/modules/backend/widgets/Form.php index c16e75bdd..ce42ad24b 100644 --- a/modules/backend/widgets/Form.php +++ b/modules/backend/widgets/Form.php @@ -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; diff --git a/modules/backend/widgets/Lists.php b/modules/backend/widgets/Lists.php index 6fddd5ca3..17d45631f 100644 --- a/modules/backend/widgets/Lists.php +++ b/modules/backend/widgets/Lists.php @@ -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.'; diff --git a/tests/PluginTestCase.php b/tests/PluginTestCase.php index 9689c8790..6618b17e0 100644 --- a/tests/PluginTestCase.php +++ b/tests/PluginTestCase.php @@ -1,11 +1,15 @@ 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 */ diff --git a/tests/TestCase.php b/tests/TestCase.php index ee25a6f42..062f67062 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -1,8 +1,6 @@ make('Illuminate\Contracts\Console\Kernel')->bootstrap(); $app['cache']->setDefaultDriver('array'); diff --git a/tests/concerns/InteractsWithAuthentication.php b/tests/concerns/InteractsWithAuthentication.php new file mode 100644 index 000000000..a7950f230 --- /dev/null +++ b/tests/concerns/InteractsWithAuthentication.php @@ -0,0 +1,149 @@ +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); + } +} diff --git a/tests/fixtures/backend/models/UserFixture.php b/tests/fixtures/backend/models/UserFixture.php new file mode 100644 index 000000000..a444cd597 --- /dev/null +++ b/tests/fixtures/backend/models/UserFixture.php @@ -0,0 +1,66 @@ +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; + } +} diff --git a/tests/unit/backend/widgets/FilterTest.php b/tests/unit/backend/widgets/FilterTest.php new file mode 100644 index 000000000..05d0f0d78 --- /dev/null +++ b/tests/unit/backend/widgets/FilterTest.php @@ -0,0 +1,139 @@ +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' + ] + ] + ] + ]); + } +} diff --git a/tests/unit/backend/widgets/FormTest.php b/tests/unit/backend/widgets/FormTest.php index 8fdaa7e4c..d373a1d86 100644 --- a/tests/unit/backend/widgets/FormTest.php +++ b/tests/unit/backend/widgets/FormTest.php @@ -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' + ] + ] + ] + ]); + } } diff --git a/tests/unit/backend/widgets/ListsTest.php b/tests/unit/backend/widgets/ListsTest.php new file mode 100644 index 000000000..e74aa877d --- /dev/null +++ b/tests/unit/backend/widgets/ListsTest.php @@ -0,0 +1,140 @@ +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' + ] + ] + ] + ]); + } +}