Create migrate script to split permissions out

Permissions have been moved from Groups to a new Role model
Refs #2367
This commit is contained in:
Samuel Georges 2017-07-11 19:17:44 +10:00
parent 1db67af8e8
commit 8002c1010b
21 changed files with 473 additions and 35 deletions

View File

@ -31,30 +31,4 @@ class UserGroups extends Controller
BackendMenu::setContext('October.System', 'system', 'users');
SettingsManager::setContext('October.System', 'administrators');
}
/**
* Add available permission fields to the Group form.
*/
public function formExtendFields($form)
{
/*
* Add permissions tab
*/
$form->addTabFields($this->generatePermissionsField());
}
/**
* Adds the permissions editor widget to the form.
* @return array
*/
protected function generatePermissionsField()
{
return [
'permissions' => [
'tab' => 'backend::lang.user.permissions',
'type' => 'Backend\FormWidgets\PermissionEditor',
'mode' => 'checkbox'
]
];
}
}

View File

@ -0,0 +1,60 @@
<?php namespace Backend\Controllers;
use BackendMenu;
use BackendAuth;
use Backend\Classes\Controller;
use System\Classes\SettingsManager;
/**
* Backend user groups controller
*
* @package october\backend
* @author Alexey Bobkov, Samuel Georges
*
*/
class UserRoles extends Controller
{
public $implement = [
'Backend.Behaviors.FormController',
'Backend.Behaviors.ListController'
];
public $formConfig = 'config_form.yaml';
public $listConfig = 'config_list.yaml';
public $requiredPermissions = ['backend.manage_users'];
public function __construct()
{
parent::__construct();
BackendMenu::setContext('October.System', 'system', 'users');
SettingsManager::setContext('October.System', 'administrators');
}
/**
* Add available permission fields to the Role form.
*/
public function formExtendFields($form)
{
/*
* Add permissions tab
*/
$form->addTabFields($this->generatePermissionsField());
}
/**
* Adds the permissions editor widget to the form.
* @return array
*/
protected function generatePermissionsField()
{
return [
'permissions' => [
'tab' => 'backend::lang.user.permissions',
'type' => 'Backend\FormWidgets\PermissionEditor',
'mode' => 'checkbox'
]
];
}
}

View File

@ -41,4 +41,4 @@
<?php else: ?>
<p class="flash-message static error"><?= e(trans($this->fatalError)) ?></p>
<p><a href="<?= Backend::url('backend/usergroups') ?>" class="btn btn-default"><?= e(trans('backend::lang.user.group.return')) ?></a></p>
<?php endif ?>
<?php endif ?>

View File

@ -50,4 +50,4 @@
<?php else: ?>
<p class="flash-message static error"><?= e(trans($this->fatalError)) ?></p>
<p><a href="<?= Backend::url('backend/usergroups') ?>" class="btn btn-default"><?= e(trans('backend::lang.user.group.return')) ?></a></p>
<?php endif ?>
<?php endif ?>

View File

@ -0,0 +1,8 @@
<div data-control="toolbar">
<a href="<?= Backend::url('backend/users') ?>" class="btn btn-default oc-icon-chevron-left">
<?= e(trans('backend::lang.user.return')) ?>
</a>
<a href="<?= Backend::url('backend/userroles/create') ?>" class="btn btn-primary oc-icon-plus">
<?= e(trans('backend::lang.user.role.new')) ?>
</a>
</div>

View File

@ -0,0 +1,16 @@
# ===================================
# Form Behavior Config
# ===================================
name: backend::lang.user.role.name
form: ~/modules/backend/models/userrole/fields.yaml
modelClass: Backend\Models\UserRole
defaultRedirect: backend/userroles
create:
redirect: backend/userroles/update/:id
redirectClose: backend/userroles
update:
redirect: backend/userroles
redirectClose: backend/userroles

View File

@ -0,0 +1,15 @@
# ===================================
# List Behavior Config
# ===================================
title: backend::lang.user.role.list_title
list: ~/modules/backend/models/userrole/columns.yaml
modelClass: Backend\Models\UserRole
recordUrl: backend/userroles/update/:id
noRecordsMessage: backend::lang.list.no_records
recordsPerPage: 5
toolbar:
buttons: list_toolbar
search:
prompt: backend::lang.list.search_prompt

View File

@ -0,0 +1,44 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('backend/users') ?>"><?= e(trans('backend::lang.user.menu_label')) ?></a></li>
<li><a href="<?= Backend::url('backend/userroles') ?>"><?= e(trans('backend::lang.user.role.menu_label')) ?></a></li>
<li><?= e(trans($this->pageTitle)) ?></li>
</ul>
<?php Block::endPut() ?>
<?php if (!$this->fatalError): ?>
<?= Form::open(['class'=>'layout']) ?>
<div class="layout-row">
<?= $this->formRender() ?>
</div>
<div class="form-buttons">
<div class="loading-indicator-container">
<button
type="submit"
data-request="onSave"
data-hotkey="ctrl+s, cmd+s"
data-load-indicator="<?= e(trans('backend::lang.form.creating')) ?>"
class="btn btn-primary">
<?= e(trans('backend::lang.form.create')) ?>
</button>
<button
type="button"
data-request="onSave"
data-request-data="close:1"
data-hotkey="ctrl+s, cmd+s"
data-load-indicator="<?= e(trans('backend::lang.form.creating')) ?>"
class="btn btn-default">
<?= e(trans('backend::lang.form.create_and_close')) ?>
</button>
</div>
</div>
<?= Form::close() ?>
<?php else: ?>
<p class="flash-message static error"><?= e(trans($this->fatalError)) ?></p>
<p><a href="<?= Backend::url('backend/userroles') ?>" class="btn btn-default"><?= e(trans('backend::lang.user.role.return')) ?></a></p>
<?php endif ?>

View File

@ -0,0 +1,8 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('backend/users') ?>"><?= e(trans('backend::lang.user.menu_label')) ?></a></li>
<li><?= e(trans($this->pageTitle)) ?></li>
</ul>
<?php Block::endPut() ?>
<?= $this->listRender() ?>

View File

@ -0,0 +1,53 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('backend/users') ?>"><?= e(trans('backend::lang.user.menu_label')) ?></a></li>
<li><a href="<?= Backend::url('backend/userroles') ?>"><?= e(trans('backend::lang.user.role.menu_label')) ?></a></li>
<li><?= e(trans($this->pageTitle)) ?></li>
</ul>
<?php Block::endPut() ?>
<?php if (!$this->fatalError): ?>
<?= Form::open(['class'=>'layout']) ?>
<div class="layout-row">
<?= $this->formRender() ?>
</div>
<div class="form-buttons">
<div class="loading-indicator-container">
<button
type="submit"
data-request="onSave"
data-request-data="redirect:0"
data-hotkey="ctrl+s, cmd+s"
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
class="btn btn-primary">
<?= e(trans('backend::lang.form.save')) ?>
</button>
<button
type="button"
data-request="onSave"
data-request-data="close:1"
data-hotkey="ctrl+enter, cmd+enter"
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
class="btn btn-default">
<?= e(trans('backend::lang.form.save_and_close')) ?>
</button>
<button
type="button"
class="btn btn-danger pull-right"
data-request="onDelete"
data-load-indicator="<?= e(trans('backend::lang.form.deleting')) ?>"
data-request-confirm="<?= e(trans('backend::lang.user.role.delete_confirm')) ?>">
<?= e(trans('backend::lang.form.delete')) ?>
</button>
</div>
</div>
<?= Form::close() ?>
<?php else: ?>
<p class="flash-message static error"><?= e(trans($this->fatalError)) ?></p>
<p><a href="<?= Backend::url('backend/userroles') ?>" class="btn btn-default"><?= e(trans('backend::lang.user.role.return')) ?></a></p>
<?php endif ?>

View File

@ -2,6 +2,9 @@
<a href="<?= Backend::url('backend/users/create') ?>" class="btn btn-primary oc-icon-plus">
<?= e(trans('backend::lang.user.new')) ?>
</a>
<a href="<?= Backend::url('backend/userroles') ?>" class="btn btn-default oc-icon-address-card">
<?= e(trans('backend::lang.user.role.list_title')) ?>
</a>
<a href="<?= Backend::url('backend/usergroups') ?>" class="btn btn-default oc-icon-group">
<?= e(trans('backend::lang.user.group.list_title')) ?>
</a>

View File

@ -20,6 +20,7 @@ class DbBackendUsers extends Migration
$table->string('reset_password_code')->nullable()->index('reset_code_index');
$table->text('permissions')->nullable();
$table->boolean('is_activated')->default(0);
$table->integer('role_id')->unsigned()->nullable()->index('admin_role_index');
$table->timestamp('activated_at')->nullable();
$table->timestamp('last_login')->nullable();
$table->timestamps();

View File

@ -11,7 +11,6 @@ class DbBackendUserGroups extends Migration
$table->engine = 'InnoDB';
$table->increments('id');
$table->string('name')->unique('name_unique');
$table->text('permissions')->nullable();
$table->timestamps();
});
}

View File

@ -0,0 +1,143 @@
<?php
use October\Rain\Database\Schema\Blueprint;
use October\Rain\Database\Updates\Migration;
class DbBackendUserRoles extends Migration
{
public function up()
{
Schema::create('backend_user_roles', function (Blueprint $table) {
$table->engine = 'InnoDB';
$table->increments('id');
$table->string('name')->unique('role_unique');
$table->string('code')->nullable()->index('role_code_index');
$table->text('description')->nullable();
$table->text('permissions')->nullable();
$table->timestamps();
});
// This detects older builds and performs a migration to include
// the new role system. This column will exist for new installs
// so this heavy migration process does not need to execute.
$this->migratePreviousBuild();
}
public function down()
{
Schema::dropIfExists('backend_user_roles');
}
protected function migratePreviousBuild()
{
// 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')) {
Schema::table('backend_users', function (Blueprint $table) {
$table->integer('role_id')->unsigned()->nullable()->index('admin_role_index');
});
$this->migratePermissionsFromGroupsToRoles();
}
// Drop permissions column on groups table as it is no longer needed.
if (Schema::hasColumn('backend_user_groups', 'permissions')) {
Schema::table('backend_user_groups', function (Blueprint $table) {
$table->dropColumn('permissions');
});
}
}
protected function migratePermissionsFromGroupsToRoles()
{
$groups = Db::table('backend_user_groups')->get();
$roles = [];
$permissions = [];
/*
* Carbon copy groups to roles
*/
foreach ($groups as $group) {
if (!isset($group->name) || !$group->name) {
continue;
}
try {
$roles[$group->id] = Db::table('backend_user_roles')->insertGetId([
'name' => $group->name,
'permissions' => $group->permissions ?? null
]);
}
catch (Exception $ex) {}
$permissions[$group->id] = $group->permissions ?? null;
}
/*
* Assign a user with the first available role
*/
$found = [];
$joins = Db::table('backend_users_groups')->get();
foreach ($joins as $join) {
if (!$roleId = array_get($roles, $join->user_group_id)) {
continue;
}
$userId = $join->user_id;
if (!isset($found[$userId])) {
Db::table('backend_users')->where('id', $userId)->update([
'role_id' => $roleId
]);
}
$found[$userId][] = $join->user_group_id;
}
/*
* Merge group permissions in to user
*/
foreach ($found as $userId => $groups) {
$userPerms = [];
foreach ($groups as $groupId) {
if (!$permString = array_get($permissions, $groupId)) {
continue;
}
try {
$perms = json_decode($permString, true);
$userPerms = array_merge($userPerms, $perms);
}
catch (Exception $ex) {}
}
if (count($userPerms) > 0) {
$this->splicePermissionsForUser($userId, $userPerms);
}
}
}
protected function splicePermissionsForUser($userId, $permissions)
{
/*
* Look up user and splice the provided permissions in
*/
$user = Db::table('backend_users')->where('id', $userId)->first();
if (!$user) {
return;
}
try {
$currentPerms = $user->permissions ? json_decode($user->permissions, true) : [];
$newPerms = array_merge($permissions, $currentPerms);
Db::table('backend_users')->where('id', $userId)->update([
'permissions' => json_encode($newPerms)
]);
}
catch (Exception $ex) {}
}
}

View File

@ -116,8 +116,10 @@ return [
'last_name' => 'Last Name',
'full_name' => 'Full Name',
'email' => 'Email',
'role_field' => 'Role',
'role_comment' => 'Roles define user permissions, which can be overriden on the user level, on the Permissions tab.',
'groups' => 'Groups',
'groups_comment' => 'Specify which groups the account should belong to. Groups define user permissions, which can be overriden on the user level, on the Permissions tab.',
'groups_comment' => 'Specify which groups this account should belong to.',
'avatar' => 'Avatar',
'password' => 'Password',
'password_confirmation' => 'Confirm Password',
@ -138,8 +140,8 @@ return [
'updated_at' => 'Updated at',
'group' => [
'name' => 'Group',
'name_comment' => 'The name is displayed in the group list on the Create/Edit Administrator form.',
'name_field' => 'Name',
'name_comment' => 'The name is displayed in the group list on the Create/Edit 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',
@ -152,6 +154,20 @@ return [
'return' => 'Return to group list',
'users_count' => 'Users'
],
'role' => [
'name' => 'Role',
'name_field' => 'Name',
'name_comment' => 'The name is displayed in the role list on the Create/Edit 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.',
'menu_label' => 'Manage Roles',
'list_title' => 'Manage Roles',
'new' => 'New Role',
'delete_confirm' => 'Delete this administrator role?',
'return' => 'Return to role list',
'users_count' => 'Users'
],
'preferences' => [
'not_authenticated' => 'There is no an authenticated user to load or save preferences for.'
]

View File

@ -35,6 +35,10 @@ class User extends UserBase
'groups' => [UserGroup::class, 'table' => 'backend_users_groups']
];
public $belongsto = [
'role' => UserRole::class
];
public $attachOne = [
'avatar' => \System\Models\File::class
];
@ -144,9 +148,22 @@ class User extends UserBase
public function getGroupsOptions()
{
$result = [];
foreach (UserGroup::all() as $group) {
$result[$group->id] = [$group->name, $group->description];
}
return $result;
}
public function getRoleOptions()
{
$result = [];
foreach (UserRole::all() as $role) {
$result[$role->id] = [$role->name, $role->description];
}
return $result;
}
}

View File

@ -0,0 +1,37 @@
<?php namespace Backend\Models;
use October\Rain\Auth\Models\Role as RoleBase;
/**
* Administrator role
*
* @package october\backend
* @author Alexey Bobkov, Samuel Georges
*/
class UserRole extends RoleBase
{
/**
* @var string The default role code.
*/
const DEFAULT_CODE = 'default';
/**
* @var string The database table used by the model.
*/
protected $table = 'backend_user_roles';
/**
* @var array Validation rules
*/
public $rules = [
'name' => 'required|between:2,128|unique:backend_user_roles',
];
/**
* @var array Relations
*/
public $hasMany = [
'users' => [User::class, 'key' => 'role_id'],
'users_count' => [User::class, 'key' => 'role_id', 'count' => true]
];
}

View File

@ -46,11 +46,17 @@ tabs:
span: right
label: backend::lang.user.password_confirmation
groups:
role:
context: [create, update]
label: backend::lang.user.role_field
commentAbove: backend::lang.user.role_comment
type: radio
groups:
label: backend::lang.user.groups
commentAbove: backend::lang.user.groups_comment
type: checkboxlist
tab: backend::lang.user.groups
secondaryTabs:
fields:

View File

@ -22,6 +22,3 @@ fields:
label: backend::lang.user.group.description_field
type: textarea
size: tiny
tabs:
stretch: true

View File

@ -0,0 +1,19 @@
# ===================================
# Column Definitions
# ===================================
columns:
name:
label: backend::lang.user.role.name_field
searchable: yes
description:
label: backend::lang.user.role.description_field
searchable: yes
users_count:
label: backend::lang.user.role.users_count
relation: users_count
valueFrom: count
default: 0
sortable: false

View File

@ -0,0 +1,22 @@
# ===================================
# Field Definitions
# ===================================
fields:
name:
label: backend::lang.user.role.name_field
commentAbove: backend::lang.user.role.name_comment
span: auto
code:
label: backend::lang.user.role.code_field
commentAbove: backend::lang.user.role.code_comment
span: auto
description:
label: backend::lang.user.role.description_field
type: textarea
size: tiny
tabs:
stretch: true