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' => [