From 4fd1ca824f7873b540d66d8b57c7e40b2308f172 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Thu, 13 Jul 2017 19:29:50 +1000 Subject: [PATCH] Introduce concept of system roles These are roles defined by a special API code, once a system role code is detected, the role becomes locked and its permissions are sourced from the AuthManager. All permissions are granted to system roles by default, unless otherwise specified. This should make it easier to create client accounts as "Publishers", hiding developer tools like the CMS and Builder plugins by default. --- modules/backend/classes/AuthManager.php | 42 +++++++++++++ modules/backend/controllers/UserRoles.php | 11 ++++ modules/backend/controllers/Users.php | 1 + .../controllers/usergroups/config_list.yaml | 3 +- .../controllers/userroles/config_list.yaml | 3 +- .../controllers/users/_list_toolbar.htm | 8 ++- ...017_10_01_000010_Db_Backend_User_Roles.php | 21 ++++++- .../backend/database/seeds/DatabaseSeeder.php | 1 - .../backend/database/seeds/SeedSetupAdmin.php | 20 ++++-- .../backend/formwidgets/PermissionEditor.php | 63 ++++++++++++++----- .../partials/_permissioneditor.htm | 35 ++++++----- modules/backend/lang/en/lang.php | 4 +- modules/backend/models/UserGroup.php | 5 +- modules/backend/models/UserRole.php | 52 +++++++++++++-- modules/backend/models/usergroup/columns.yaml | 9 ++- modules/backend/models/userrole/columns.yaml | 7 ++- modules/cms/ServiceProvider.php | 9 ++- 17 files changed, 237 insertions(+), 57 deletions(-) diff --git a/modules/backend/classes/AuthManager.php b/modules/backend/classes/AuthManager.php index d9f8cf89d..9fd62508d 100644 --- a/modules/backend/classes/AuthManager.php +++ b/modules/backend/classes/AuthManager.php @@ -31,6 +31,7 @@ class AuthManager extends RainAuthManager 'code' => null, 'label' => null, 'comment' => null, + 'roles' => null, 'order' => 500 ]; @@ -44,6 +45,11 @@ class AuthManager extends RainAuthManager */ protected $permissions = []; + /** + * @var array List of registered permission roles. + */ + protected $permissionRoles = false; + /** * @var array Cache of registered permissions. */ @@ -157,4 +163,40 @@ class AuthManager extends RainAuthManager return $tabs; } + + /** + * Returns an array of registered permissions belonging to a given role code + * @param string $role + * @return array + */ + public function listPermissionsForRole($role, $includeOrphans = true) + { + if ($this->permissionRoles === false) { + $this->permissionRoles = []; + + foreach ($this->listPermissions() as $permission) { + if ($permission->roles) { + foreach ((array) $permission->roles as $_role) { + $this->permissionRoles[$_role][$permission->code] = 1; + } + } + else { + $this->permissionRoles['*'][$permission->code] = 1; + } + } + } + + $result = $this->permissionRoles[$role] ?? []; + + if ($includeOrphans) { + $result += $this->permissionRoles['*'] ?? []; + } + + return $result; + } + + public function hasPermissionsForRole($role) + { + return !!$this->listPermissionsForRole($role, false); + } } diff --git a/modules/backend/controllers/UserRoles.php b/modules/backend/controllers/UserRoles.php index e6b9600f9..76587d1a2 100644 --- a/modules/backend/controllers/UserRoles.php +++ b/modules/backend/controllers/UserRoles.php @@ -1,5 +1,7 @@ bindEvent('page.beforeDisplay', function() { + if (!$this->user->isSuperUser()) { + return Response::make(View::make('backend::access_denied'), 403); + } + }); } /** diff --git a/modules/backend/controllers/Users.php b/modules/backend/controllers/Users.php index cc13b3c64..fbbdcc1d5 100644 --- a/modules/backend/controllers/Users.php +++ b/modules/backend/controllers/Users.php @@ -95,6 +95,7 @@ class Users extends Controller if (!$this->user->isSuperUser()) { $form->removeField('is_superuser'); + $form->removeField('role'); } /* diff --git a/modules/backend/controllers/usergroups/config_list.yaml b/modules/backend/controllers/usergroups/config_list.yaml index 00ba20707..e88b41715 100644 --- a/modules/backend/controllers/usergroups/config_list.yaml +++ b/modules/backend/controllers/usergroups/config_list.yaml @@ -7,7 +7,8 @@ list: ~/modules/backend/models/usergroup/columns.yaml modelClass: Backend\Models\UserGroup recordUrl: backend/usergroups/update/:id noRecordsMessage: backend::lang.list.no_records -recordsPerPage: 5 +recordsPerPage: 25 +showSetup: true toolbar: buttons: list_toolbar diff --git a/modules/backend/controllers/userroles/config_list.yaml b/modules/backend/controllers/userroles/config_list.yaml index 5871e58fd..520fb6e0c 100644 --- a/modules/backend/controllers/userroles/config_list.yaml +++ b/modules/backend/controllers/userroles/config_list.yaml @@ -7,7 +7,8 @@ list: ~/modules/backend/models/userrole/columns.yaml modelClass: Backend\Models\UserRole recordUrl: backend/userroles/update/:id noRecordsMessage: backend::lang.list.no_records -recordsPerPage: 5 +recordsPerPage: 25 +showSetup: true toolbar: buttons: list_toolbar diff --git a/modules/backend/controllers/users/_list_toolbar.htm b/modules/backend/controllers/users/_list_toolbar.htm index 096c4d68c..092dcd1e4 100644 --- a/modules/backend/controllers/users/_list_toolbar.htm +++ b/modules/backend/controllers/users/_list_toolbar.htm @@ -2,9 +2,11 @@ - - - + user->isSuperUser()): ?> + + + + diff --git a/modules/backend/database/migrations/2017_10_01_000010_Db_Backend_User_Roles.php b/modules/backend/database/migrations/2017_10_01_000010_Db_Backend_User_Roles.php index 4c49c3dd7..601893a45 100644 --- a/modules/backend/database/migrations/2017_10_01_000010_Db_Backend_User_Roles.php +++ b/modules/backend/database/migrations/2017_10_01_000010_Db_Backend_User_Roles.php @@ -1,5 +1,6 @@ string('code')->nullable()->index('role_code_index'); $table->text('description')->nullable(); $table->text('permissions')->nullable(); + $table->boolean('is_system')->default(0); $table->timestamps(); }); @@ -33,11 +35,12 @@ class DbBackendUserRoles extends Migration // Role not found in the users table, perform a complete migration. // Merging group permissions with the user and assigning the user // with the first available role. - if (Schema::hasColumn('backend_users', 'role_id')) { + if (!Schema::hasColumn('backend_users', 'role_id')) { Schema::table('backend_users', function (Blueprint $table) { $table->integer('role_id')->unsigned()->nullable()->index('admin_role_index'); }); + $this->createSystemUserRoles(); $this->migratePermissionsFromGroupsToRoles(); } @@ -49,6 +52,21 @@ class DbBackendUserRoles extends Migration } } + protected function createSystemUserRoles() + { + Db::table('backend_user_roles')->insert([ + 'name' => 'Publisher', + 'code' => UserRole::CODE_PUBLISHER, + 'description' => 'Site editor with access to publishing tools.', + ]); + + Db::table('backend_user_roles')->insert([ + 'name' => 'Developer', + 'code' => UserRole::CODE_DEVELOPER, + 'description' => 'Site administrator with access to developer tools.', + ]); + } + protected function migratePermissionsFromGroupsToRoles() { $groups = Db::table('backend_user_groups')->get(); @@ -66,6 +84,7 @@ class DbBackendUserRoles extends Migration try { $roles[$group->id] = Db::table('backend_user_roles')->insertGetId([ 'name' => $group->name, + 'description' => $group->description, 'permissions' => $group->permissions ?? null ]); } diff --git a/modules/backend/database/seeds/DatabaseSeeder.php b/modules/backend/database/seeds/DatabaseSeeder.php index ce9b9dfe1..7be5f9885 100644 --- a/modules/backend/database/seeds/DatabaseSeeder.php +++ b/modules/backend/database/seeds/DatabaseSeeder.php @@ -5,7 +5,6 @@ use Eloquent; class DatabaseSeeder extends Seeder { - /** * Run the database seeds. * diff --git a/modules/backend/database/seeds/SeedSetupAdmin.php b/modules/backend/database/seeds/SeedSetupAdmin.php index 64beb4be3..e161a41d8 100644 --- a/modules/backend/database/seeds/SeedSetupAdmin.php +++ b/modules/backend/database/seeds/SeedSetupAdmin.php @@ -2,11 +2,11 @@ use Seeder; use Backend\Models\User; +use Backend\Models\UserRole; use Backend\Models\UserGroup; class SeedSetupAdmin extends Seeder { - public static $email = 'admin@domain.tld'; public static $login = 'admin'; public static $password = 'admin'; @@ -25,9 +25,21 @@ class SeedSetupAdmin extends Seeder public function run() { + UserRole::create([ + 'name' => 'Publisher', + 'code' => UserRole::CODE_PUBLISHER, + 'description' => 'Site editor with access to publishing tools.', + ]); + + $role = UserRole::create([ + 'name' => 'Developer', + 'code' => UserRole::CODE_DEVELOPER, + 'description' => 'Site administrator with access to developer tools.', + ]); + $group = UserGroup::create([ 'name' => 'Owners', - 'code' => UserGroup::DEFAULT_CODE, + 'code' => UserGroup::CODE_OWNERS, 'description' => 'Default group for website owners.', 'is_new_user_default' => false ]); @@ -41,10 +53,10 @@ class SeedSetupAdmin extends Seeder 'last_name' => static::$lastName, 'permissions' => [], 'is_superuser' => true, - 'is_activated' => true + 'is_activated' => true, + 'role_id' => $role->id ]); $user->addGroup($group); } - } diff --git a/modules/backend/formwidgets/PermissionEditor.php b/modules/backend/formwidgets/PermissionEditor.php index 565ea3e68..51c66cf8e 100644 --- a/modules/backend/formwidgets/PermissionEditor.php +++ b/modules/backend/formwidgets/PermissionEditor.php @@ -12,6 +12,8 @@ use BackendAuth; */ class PermissionEditor extends FormWidgetBase { + protected $user; + public $mode; /** @@ -22,6 +24,8 @@ class PermissionEditor extends FormWidgetBase $this->fillFromConfig([ 'mode' ]); + + $this->user = BackendAuth::getUser(); } /** @@ -38,6 +42,10 @@ class PermissionEditor extends FormWidgetBase */ public function prepareVars() { + if ($this->formField->disabled) { + $this->previewMode = true; + } + $permissionsData = $this->formField->getValueFromData($this->model); if (!is_array($permissionsData)) { $permissionsData = []; @@ -54,6 +62,36 @@ class PermissionEditor extends FormWidgetBase * @inheritDoc */ public function getSaveValue($value) + { + if ($this->user->isSuperUser()) { + return is_array($value) ? $value : []; + } + + return $this->getSaveValueSecure($value); + } + + /** + * @inheritDoc + */ + protected function loadAssets() + { + $this->addCss('css/permissioneditor.css', 'core'); + $this->addJs('js/permissioneditor.js', 'core'); + } + + protected function getControlMode() + { + return strlen($this->mode) ? $this->mode : 'radio'; + } + + /** + * Returns a safely parsed set of permissions, ensuring the user cannot elevate + * their own permissions or permissions of another user above their own. + * + * @param string $value + * @return array + */ + protected function getSaveValueSecure($value) { $newPermissions = is_array($value) ? array_map('intval', $value) : []; @@ -80,20 +118,6 @@ class PermissionEditor extends FormWidgetBase return $newPermissions; } - /** - * @inheritDoc - */ - protected function loadAssets() - { - $this->addCss('css/permissioneditor.css', 'core'); - $this->addJs('js/permissioneditor.js', 'core'); - } - - protected function getControlMode() - { - return strlen($this->mode) ? $this->mode : 'radio'; - } - /** * Returns the available permissions; removing those that the logged-in user does not have access to * @@ -102,17 +126,22 @@ class PermissionEditor extends FormWidgetBase protected function getFilteredPermissions() { $permissions = BackendAuth::listTabbedPermissions(); - $user = BackendAuth::getUser(); + + if ($this->user->isSuperUser()) { + return $permissions; + } + foreach ($permissions as $tab => $permissionsArray) { foreach ($permissionsArray as $index => $permission) { - if (!$user->hasAccess($permission->code)) { + if (!$this->user->hasAccess($permission->code)) { unset($permissionsArray[$index]); } } if (empty($permissionsArray)) { unset($permissions[$tab]); - } else { + } + else { $permissions[$tab] = $permissionsArray; } } diff --git a/modules/backend/formwidgets/permissioneditor/partials/_permissioneditor.htm b/modules/backend/formwidgets/permissioneditor/partials/_permissioneditor.htm index cd28edaf7..4a6377650 100644 --- a/modules/backend/formwidgets/permissioneditor/partials/_permissioneditor.htm +++ b/modules/backend/formwidgets/permissioneditor/partials/_permissioneditor.htm @@ -1,34 +1,38 @@ -
getAttributes() ?>> +
getAttributes() ?>> - $tabPermissions): ?> + ?> + $tabPermissions): ?> - + - - + + - $permission): + + $permission): ?> + code, $permissionsData) ? - $permissionsData[$permission->code] : 0; - } + $permissionValue = array_key_exists($permission->code, $permissionsData) + ? $permissionsData[$permission->code] + : 0; + } else { $isChecked = array_key_exists($permission->code, $permissionsData); } - ?> + ?> diff --git a/modules/backend/lang/en/lang.php b/modules/backend/lang/en/lang.php index 344e0f5a9..3d02cef33 100644 --- a/modules/backend/lang/en/lang.php +++ b/modules/backend/lang/en/lang.php @@ -141,7 +141,7 @@ return [ 'group' => [ 'name' => 'Group', 'name_field' => 'Name', - 'name_comment' => 'The name is displayed in the group list on the Create/Edit Administrator form.', + 'name_comment' => 'The name is displayed in the group list on the Administrator form.', 'description_field' => 'Description', 'is_new_user_default_field_label' => 'Default group', 'is_new_user_default_field_comment' => 'Add new administrators to this group by default', @@ -157,7 +157,7 @@ return [ 'role' => [ 'name' => 'Role', 'name_field' => 'Name', - 'name_comment' => 'The name is displayed in the role list on the Create/Edit Administrator form.', + 'name_comment' => 'The name is displayed in the role list on the Administrator form.', 'description_field' => 'Description', 'code_field' => 'Code', 'code_comment' => 'Enter a unique code if you want to access the role object with the API.', diff --git a/modules/backend/models/UserGroup.php b/modules/backend/models/UserGroup.php index 981145436..e9be8127e 100644 --- a/modules/backend/models/UserGroup.php +++ b/modules/backend/models/UserGroup.php @@ -10,10 +10,7 @@ use October\Rain\Auth\Models\Group as GroupBase; */ class UserGroup extends GroupBase { - /** - * @var string The default group code. - */ - const DEFAULT_CODE = 'owners'; + const CODE_OWNERS = 'owners'; /** * @var string The database table used by the model. diff --git a/modules/backend/models/UserRole.php b/modules/backend/models/UserRole.php index d115ebf6e..a0498ebc3 100644 --- a/modules/backend/models/UserRole.php +++ b/modules/backend/models/UserRole.php @@ -1,5 +1,6 @@ 'required|between:2,128|unique:backend_user_roles', + 'code' => 'unique:backend_user_roles', ]; /** @@ -34,4 +34,48 @@ class UserRole extends RoleBase 'users' => [User::class, 'key' => 'role_id'], 'users_count' => [User::class, 'key' => 'role_id', 'count' => true] ]; + + public function filterFields($fields) + { + if ($this->is_system) { + $fields->code->disabled = true; + $fields->permissions->disabled = true; + } + } + + public function afterFetch() + { + if ($this->is_system) { + $this->permissions = $this->getDefaultPermissions(); + } + } + + public function beforeSave() + { + if ($this->isSystemRole()) { + $this->is_system = true; + $this->permissions = []; + } + } + + public function isSystemRole() + { + if (!$this->code || !strlen(trim($this->code))) { + return false; + } + + if ($this->is_system || in_array($this->code, [ + self::CODE_DEVELOPER, + self::CODE_PUBLISHER + ])) { + return true; + } + + return AuthManager::instance()->hasPermissionsForRole($this->code); + } + + public function getDefaultPermissions() + { + return AuthManager::instance()->listPermissionsForRole($this->code); + } } diff --git a/modules/backend/models/usergroup/columns.yaml b/modules/backend/models/usergroup/columns.yaml index 0b8c3e878..0086d159e 100644 --- a/modules/backend/models/usergroup/columns.yaml +++ b/modules/backend/models/usergroup/columns.yaml @@ -5,11 +5,16 @@ columns: name: label: backend::lang.user.group.name_field - searchable: yes + searchable: true + + code: + label: backend::lang.user.group.code_field + searchable: true + invisible: true description: label: backend::lang.user.group.description_field - searchable: yes + searchable: true users_count: label: backend::lang.user.group.users_count diff --git a/modules/backend/models/userrole/columns.yaml b/modules/backend/models/userrole/columns.yaml index 392ddf6bd..20f27aa59 100644 --- a/modules/backend/models/userrole/columns.yaml +++ b/modules/backend/models/userrole/columns.yaml @@ -5,7 +5,12 @@ columns: name: label: backend::lang.user.role.name_field - searchable: yes + searchable: true + + code: + label: backend::lang.user.role.code_field + searchable: true + invisible: true description: label: backend::lang.user.role.description_field diff --git a/modules/cms/ServiceProvider.php b/modules/cms/ServiceProvider.php index 41bd533b8..a0ccdef82 100644 --- a/modules/cms/ServiceProvider.php +++ b/modules/cms/ServiceProvider.php @@ -6,6 +6,7 @@ use Event; use Backend; use BackendMenu; use BackendAuth; +use Backend\Models\UserRole; use Backend\Classes\WidgetManager; use October\Rain\Support\ModuleServiceProvider; use System\Classes\SettingsManager; @@ -123,6 +124,7 @@ class ServiceProvider extends ModuleServiceProvider 'icon' => 'icon-magic', 'iconSvg' => 'modules/cms/assets/images/cms-icon.svg', 'url' => Backend::url('cms'), + 'order' => 100, 'permissions' => [ 'cms.manage_content', 'cms.manage_assets', @@ -130,7 +132,6 @@ class ServiceProvider extends ModuleServiceProvider 'cms.manage_layouts', 'cms.manage_partials' ], - 'order' => 100, 'sideMenu' => [ 'pages' => [ 'label' => 'cms::lang.page.menu_label', @@ -216,31 +217,37 @@ class ServiceProvider extends ModuleServiceProvider 'cms.manage_content' => [ 'label' => 'cms::lang.permissions.manage_content', 'tab' => 'cms::lang.permissions.name', + 'roles' => UserRole::CODE_DEVELOPER, 'order' => 100 ], 'cms.manage_assets' => [ 'label' => 'cms::lang.permissions.manage_assets', 'tab' => 'cms::lang.permissions.name', + 'roles' => UserRole::CODE_DEVELOPER, 'order' => 100 ], 'cms.manage_pages' => [ 'label' => 'cms::lang.permissions.manage_pages', 'tab' => 'cms::lang.permissions.name', + 'roles' => UserRole::CODE_DEVELOPER, 'order' => 100 ], 'cms.manage_layouts' => [ 'label' => 'cms::lang.permissions.manage_layouts', 'tab' => 'cms::lang.permissions.name', + 'roles' => UserRole::CODE_DEVELOPER, 'order' => 100 ], 'cms.manage_partials' => [ 'label' => 'cms::lang.permissions.manage_partials', 'tab' => 'cms::lang.permissions.name', + 'roles' => UserRole::CODE_DEVELOPER, 'order' => 100 ], 'cms.manage_themes' => [ 'label' => 'cms::lang.permissions.manage_themes', 'tab' => 'cms::lang.permissions.name', + 'roles' => UserRole::CODE_DEVELOPER, 'order' => 100 ], 'media.manage_media' => [