]> BookStack Code Mirror - bookstack/commitdiff
Removed role 'name' field from database
authorDan Brown <redacted>
Tue, 4 Aug 2020 13:55:01 +0000 (14:55 +0100)
committerDan Brown <redacted>
Tue, 4 Aug 2020 13:55:01 +0000 (14:55 +0100)
The 'name' field was really redundant and caused confusion in the
codebase, since the 'Display' name is often used and we have a
'system_name' for the admin and public role.

This fixes #2032, Where external auth group matching has confusing
behaviour as matching was done against the display_name, if no
external_auth field is set, but only roles with a match 'name' field
would be considered.

This also fixes and error where the role users migration, on role
delete, would not actually fire due to mis-matching http body keys.
Looks like this has been an issue from the start. Added some testing to
cover. Fixes #2211.

Also converted phpdoc to typehints in many areas of the reviewed code
during the above.

15 files changed:
app/Auth/Access/ExternalAuthService.php
app/Auth/Permissions/PermissionsRepo.php
app/Auth/Permissions/RolePermission.php
app/Auth/Role.php
app/Auth/User.php
app/Auth/UserRepo.php
app/Http/Controllers/PermissionController.php
app/Http/Controllers/UserController.php
database/migrations/2020_08_04_131052_remove_role_name_field.php [new file with mode: 0644]
resources/views/form/role-checkboxes.blade.php
resources/views/settings/index.blade.php
resources/views/settings/roles/delete.blade.php
tests/Auth/AuthTest.php
tests/Auth/LdapTest.php
tests/Permissions/RolesTest.php

index db8bd2dfb7c2f83cd855d4622b818f17581045f8..4c71db21adff7a559b929353ec4974512c16df14 100644 (file)
@@ -3,6 +3,8 @@
 use BookStack\Auth\Role;
 use BookStack\Auth\User;
 use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\DB;
 
 class ExternalAuthService
 {
@@ -39,22 +41,14 @@ class ExternalAuthService
     /**
      * Match an array of group names to BookStack system roles.
      * Formats group names to be lower-case and hyphenated.
-     * @param array $groupNames
-     * @return \Illuminate\Support\Collection
      */
-    protected function matchGroupsToSystemsRoles(array $groupNames)
+    protected function matchGroupsToSystemsRoles(array $groupNames): Collection
     {
         foreach ($groupNames as $i => $groupName) {
             $groupNames[$i] = str_replace(' ', '-', trim(strtolower($groupName)));
         }
 
-        $roles = Role::query()->where(function (Builder $query) use ($groupNames) {
-            $query->whereIn('name', $groupNames);
-            foreach ($groupNames as $groupName) {
-                $query->orWhere('external_auth_id', 'LIKE', '%' . $groupName . '%');
-            }
-        })->get();
-
+        $roles = Role::query()->get(['id', 'external_auth_id', 'display_name']);
         $matchedRoles = $roles->filter(function (Role $role) use ($groupNames) {
             return $this->roleMatchesGroupNames($role, $groupNames);
         });
index 56ef193015f7103a98bb440ec60498e5bf765c25..ce61093cc5ed26d4f81ebb01671e4a1e91eac6e1 100644 (file)
@@ -1,8 +1,9 @@
 <?php namespace BookStack\Auth\Permissions;
 
-use BookStack\Auth\Permissions;
 use BookStack\Auth\Role;
 use BookStack\Exceptions\PermissionsException;
+use Exception;
+use Illuminate\Database\Eloquent\Collection;
 use Illuminate\Support\Str;
 
 class PermissionsRepo
@@ -16,11 +17,8 @@ class PermissionsRepo
 
     /**
      * PermissionsRepo constructor.
-     * @param RolePermission $permission
-     * @param Role $role
-     * @param \BookStack\Auth\Permissions\PermissionService $permissionService
      */
-    public function __construct(RolePermission $permission, Role $role, Permissions\PermissionService $permissionService)
+    public function __construct(RolePermission $permission, Role $role, PermissionService $permissionService)
     {
         $this->permission = $permission;
         $this->role = $role;
@@ -29,46 +27,34 @@ class PermissionsRepo
 
     /**
      * Get all the user roles from the system.
-     * @return \Illuminate\Database\Eloquent\Collection|static[]
      */
-    public function getAllRoles()
+    public function getAllRoles(): Collection
     {
         return $this->role->all();
     }
 
     /**
      * Get all the roles except for the provided one.
-     * @param Role $role
-     * @return mixed
      */
-    public function getAllRolesExcept(Role $role)
+    public function getAllRolesExcept(Role $role): Collection
     {
         return $this->role->where('id', '!=', $role->id)->get();
     }
 
     /**
      * Get a role via its ID.
-     * @param $id
-     * @return mixed
      */
-    public function getRoleById($id)
+    public function getRoleById($id): Role
     {
-        return $this->role->findOrFail($id);
+        return $this->role->newQuery()->findOrFail($id);
     }
 
     /**
      * Save a new role into the system.
-     * @param array $roleData
-     * @return Role
      */
-    public function saveNewRole($roleData)
+    public function saveNewRole(array $roleData): Role
     {
         $role = $this->role->newInstance($roleData);
-        $role->name = str_replace(' ', '-', strtolower($roleData['display_name']));
-        // Prevent duplicate names
-        while ($this->role->where('name', '=', $role->name)->count() > 0) {
-            $role->name .= strtolower(Str::random(2));
-        }
         $role->save();
 
         $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
@@ -80,13 +66,11 @@ class PermissionsRepo
     /**
      * Updates an existing role.
      * Ensure Admin role always have core permissions.
-     * @param $roleId
-     * @param $roleData
-     * @throws PermissionsException
      */
-    public function updateRole($roleId, $roleData)
+    public function updateRole($roleId, array $roleData)
     {
-        $role = $this->role->findOrFail($roleId);
+        /** @var Role $role */
+        $role = $this->role->newQuery()->findOrFail($roleId);
 
         $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
         if ($role->system_name === 'admin') {
@@ -108,16 +92,19 @@ class PermissionsRepo
 
     /**
      * Assign an list of permission names to an role.
-     * @param Role $role
-     * @param array $permissionNameArray
      */
-    public function assignRolePermissions(Role $role, $permissionNameArray = [])
+    public function assignRolePermissions(Role $role, array $permissionNameArray = [])
     {
         $permissions = [];
         $permissionNameArray = array_values($permissionNameArray);
-        if ($permissionNameArray && count($permissionNameArray) > 0) {
-            $permissions = $this->permission->whereIn('name', $permissionNameArray)->pluck('id')->toArray();
+
+        if ($permissionNameArray) {
+            $permissions = $this->permission->newQuery()
+                ->whereIn('name', $permissionNameArray)
+                ->pluck('id')
+                ->toArray();
         }
+
         $role->permissions()->sync($permissions);
     }
 
@@ -126,13 +113,13 @@ class PermissionsRepo
      * Check it's not an admin role or set as default before deleting.
      * If an migration Role ID is specified the users assign to the current role
      * will be added to the role of the specified id.
-     * @param $roleId
-     * @param $migrateRoleId
      * @throws PermissionsException
+     * @throws Exception
      */
     public function deleteRole($roleId, $migrateRoleId)
     {
-        $role = $this->role->findOrFail($roleId);
+        /** @var Role $role */
+        $role = $this->role->newQuery()->findOrFail($roleId);
 
         // Prevent deleting admin role or default registration role.
         if ($role->system_name && in_array($role->system_name, $this->systemRoles)) {
@@ -142,9 +129,9 @@ class PermissionsRepo
         }
 
         if ($migrateRoleId) {
-            $newRole = $this->role->find($migrateRoleId);
+            $newRole = $this->role->newQuery()->find($migrateRoleId);
             if ($newRole) {
-                $users = $role->users->pluck('id')->toArray();
+                $users = $role->users()->pluck('id')->toArray();
                 $newRole->users()->sync($users);
             }
         }
index 8b07b3073bf9add0eb1f4a216844c69e05235832..7f44ff8152b5a23b759b5fce3a83238da01855f9 100644 (file)
@@ -3,6 +3,9 @@
 use BookStack\Auth\Role;
 use BookStack\Model;
 
+/**
+ * @property int $id
+ */
 class RolePermission extends Model
 {
     /**
index df9b1cea93faa7d413800e966248fd6f99eb62b1..13ec6df16b8488c23120d9a129e617fcad99182b 100644 (file)
@@ -3,13 +3,16 @@
 use BookStack\Auth\Permissions\JointPermission;
 use BookStack\Auth\Permissions\RolePermission;
 use BookStack\Model;
+use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Database\Eloquent\Relations\HasMany;
 
 /**
  * Class Role
+ * @property int $id
  * @property string $display_name
  * @property string $description
  * @property string $external_auth_id
- * @package BookStack\Auth
+ * @property string $system_name
  */
 class Role extends Model
 {
@@ -26,9 +29,8 @@ class Role extends Model
 
     /**
      * Get all related JointPermissions.
-     * @return \Illuminate\Database\Eloquent\Relations\HasMany
      */
-    public function jointPermissions()
+    public function jointPermissions(): HasMany
     {
         return $this->hasMany(JointPermission::class);
     }
@@ -43,10 +45,8 @@ class Role extends Model
 
     /**
      * Check if this role has a permission.
-     * @param $permissionName
-     * @return bool
      */
-    public function hasPermission($permissionName)
+    public function hasPermission(string $permissionName): bool
     {
         $permissions = $this->getRelationValue('permissions');
         foreach ($permissions as $permission) {
@@ -59,7 +59,6 @@ class Role extends Model
 
     /**
      * Add a permission to this role.
-     * @param RolePermission $permission
      */
     public function attachPermission(RolePermission $permission)
     {
@@ -68,7 +67,6 @@ class Role extends Model
 
     /**
      * Detach a single permission from this role.
-     * @param RolePermission $permission
      */
     public function detachPermission(RolePermission $permission)
     {
@@ -76,39 +74,33 @@ class Role extends Model
     }
 
     /**
-     * Get the role object for the specified role.
-     * @param $roleName
-     * @return Role
+     * Get the role of the specified display name.
      */
-    public static function getRole($roleName)
+    public static function getRole(string $displayName): ?Role
     {
-        return static::query()->where('name', '=', $roleName)->first();
+        return static::query()->where('display_name', '=', $displayName)->first();
     }
 
     /**
      * Get the role object for the specified system role.
-     * @param $roleName
-     * @return Role
      */
-    public static function getSystemRole($roleName)
+    public static function getSystemRole(string $systemName): ?Role
     {
-        return static::query()->where('system_name', '=', $roleName)->first();
+        return static::query()->where('system_name', '=', $systemName)->first();
     }
 
     /**
      * Get all visible roles
-     * @return mixed
      */
-    public static function visible()
+    public static function visible(): Collection
     {
         return static::query()->where('hidden', '=', false)->orderBy('name')->get();
     }
 
     /**
      * Get the roles that can be restricted.
-     * @return \Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection
      */
-    public static function restrictable()
+    public static function restrictable(): Collection
     {
         return static::query()->where('system_name', '!=', 'admin')->get();
     }
index 40718beb6bfdccc1f7fb53770c7600fffd43b2ed..f65ef5316f67bfe3c68f40a6e2f7925ac7aa8f27 100644 (file)
@@ -101,12 +101,10 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
 
     /**
      * Check if the user has a role.
-     * @param $role
-     * @return mixed
      */
-    public function hasRole($role)
+    public function hasRole($roleId): bool
     {
-        return $this->roles->pluck('name')->contains($role);
+        return $this->roles->pluck('id')->contains($roleId);
     }
 
     /**
@@ -163,7 +161,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
 
     /**
      * Attach a role to this user.
-     * @param Role $role
      */
     public function attachRole(Role $role)
     {
index cfa7bfce1f1c836ffde1beddd0f58e66d07bd99a..fdb8c0923882cabe08e03f4db68e6b89609e1468 100644 (file)
@@ -238,7 +238,7 @@ class UserRepo
      */
     public function getAllRoles()
     {
-        return $this->role->newQuery()->orderBy('name', 'asc')->get();
+        return $this->role->newQuery()->orderBy('display_name', 'asc')->get();
     }
 
     /**
index 148ae5cd65a3049e421ea2b331f3bfa34f31ed4d..1200d44ab69092ddcd3e498a2abf9ac961386f7a 100644 (file)
@@ -2,7 +2,9 @@
 
 use BookStack\Auth\Permissions\PermissionsRepo;
 use BookStack\Exceptions\PermissionsException;
+use Exception;
 use Illuminate\Http\Request;
+use Illuminate\Validation\ValidationException;
 
 class PermissionController extends Controller
 {
@@ -11,7 +13,6 @@ class PermissionController extends Controller
 
     /**
      * PermissionController constructor.
-     * @param \BookStack\Auth\Permissions\PermissionsRepo $permissionsRepo
      */
     public function __construct(PermissionsRepo $permissionsRepo)
     {
@@ -31,7 +32,6 @@ class PermissionController extends Controller
 
     /**
      * Show the form to create a new role
-     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
      */
     public function createRole()
     {
@@ -41,15 +41,13 @@ class PermissionController extends Controller
 
     /**
      * Store a new role in the system.
-     * @param Request $request
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
      */
     public function storeRole(Request $request)
     {
         $this->checkPermission('user-roles-manage');
         $this->validate($request, [
-            'display_name' => 'required|min:3|max:200',
-            'description' => 'max:250'
+            'display_name' => 'required|min:3|max:180',
+            'description' => 'max:180'
         ]);
 
         $this->permissionsRepo->saveNewRole($request->all());
@@ -59,11 +57,9 @@ class PermissionController extends Controller
 
     /**
      * Show the form for editing a user role.
-     * @param $id
-     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
      * @throws PermissionsException
      */
-    public function editRole($id)
+    public function editRole(string $id)
     {
         $this->checkPermission('user-roles-manage');
         $role = $this->permissionsRepo->getRoleById($id);
@@ -75,18 +71,14 @@ class PermissionController extends Controller
 
     /**
      * Updates a user role.
-     * @param Request $request
-     * @param $id
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
-     * @throws PermissionsException
-     * @throws \Illuminate\Validation\ValidationException
+     * @throws ValidationException
      */
-    public function updateRole(Request $request, $id)
+    public function updateRole(Request $request, string $id)
     {
         $this->checkPermission('user-roles-manage');
         $this->validate($request, [
-            'display_name' => 'required|min:3|max:200',
-            'description' => 'max:250'
+            'display_name' => 'required|min:3|max:180',
+            'description' => 'max:180'
         ]);
 
         $this->permissionsRepo->updateRole($id, $request->all());
@@ -97,10 +89,8 @@ class PermissionController extends Controller
     /**
      * Show the view to delete a role.
      * Offers the chance to migrate users.
-     * @param $id
-     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
      */
-    public function showDeleteRole($id)
+    public function showDeleteRole(string $id)
     {
         $this->checkPermission('user-roles-manage');
         $role = $this->permissionsRepo->getRoleById($id);
@@ -113,11 +103,9 @@ class PermissionController extends Controller
     /**
      * Delete a role from the system,
      * Migrate from a previous role if set.
-     * @param Request $request
-     * @param $id
-     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+     * @throws Exception
      */
-    public function deleteRole(Request $request, $id)
+    public function deleteRole(Request $request, string $id)
     {
         $this->checkPermission('user-roles-manage');
 
index 97ec31dcce0b2dfa2a539319afe126f23eb79788..651dedc0d855d5f54ecc2aba6b2def4e2feced1c 100644 (file)
@@ -66,8 +66,8 @@ class UserController extends Controller
     {
         $this->checkPermission('users-manage');
         $validationRules = [
-            'name'             => 'required',
-            'email'            => 'required|email|unique:users,email'
+            'name'  => 'required',
+            'email' => 'required|email|unique:users,email'
         ];
 
         $authMethod = config('auth.method');
diff --git a/database/migrations/2020_08_04_131052_remove_role_name_field.php b/database/migrations/2020_08_04_131052_remove_role_name_field.php
new file mode 100644 (file)
index 0000000..f3cafb7
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Support\Facades\DB;
+
+class RemoveRoleNameField extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('roles', function (Blueprint $table) {
+            $table->dropColumn('name');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('roles', function (Blueprint $table) {
+            $table->string('name')->index();
+        });
+
+        DB::table('roles')->update([
+            "name" => DB::raw("lower(replace(`display_name`, ' ', '-'))"),
+        ]);
+    }
+}
index 580b332f39c7e1fc993c670693ba08a8ad818192..fc6ad93a8d13284680fd7fc5b267c2ce6453d89d 100644 (file)
@@ -3,10 +3,10 @@
     @foreach($roles as $role)
         <div>
             @include('components.custom-checkbox', [
-                'name' => $name . '[' . str_replace('.', 'DOT', $role->name) . ']',
+                'name' => $name . '[' . strval($role->id) . ']',
                 'label' => $role->display_name,
                 'value' => $role->id,
-                'checked' => old($name . '.' . str_replace('.', 'DOT', $role->name)) || (!old('name') && isset($model) && $model->hasRole($role->name))
+                'checked' => old($name . '.' . strval($role->id)) || (!old('name') && isset($model) && $model->hasRole($role->id))
             ])
         </div>
     @endforeach
index 0c8ff843a5f00f4fe7e94de69c7e33be41df992a..b688d96469a7df900be90cc1587f20192aec3478 100644 (file)
                             <label for="setting-registration-role">{{ trans('settings.reg_default_role') }}</label>
                             <select id="setting-registration-role" name="setting-registration-role" @if($errors->has('setting-registration-role')) class="neg" @endif>
                                 @foreach(\BookStack\Auth\Role::all() as $role)
-                                    <option value="{{$role->id}}" data-role-name="{{ $role->name }}"
+                                    <option value="{{$role->id}}"
+                                            data-system-role-name="{{ $role->system_name ?? '' }}"
                                             @if(setting('registration-role', \BookStack\Auth\Role::first()->id) == $role->id) selected @endif
                                     >
                                         {{ $role->display_name }}
index 4f40345df99947dec5de11fa2d44cf88dcf382e0..fa7c12b0a2aafab0ec401e4a951a24966a89afa6 100644 (file)
@@ -19,7 +19,7 @@
                 @if($role->users->count() > 0)
                     <div class="form-group">
                         <p>{{ trans('settings.role_delete_users_assigned', ['userCount' => $role->users->count()]) }}</p>
-                        @include('form.role-select', ['options' => $roles, 'name' => 'migration_role_id'])
+                        @include('form.role-select', ['options' => $roles, 'name' => 'migrate_role_id'])
                     </div>
                 @endif
 
index 6257f841f9351db6d1eb4ffc2e961de6f4388b4b..92dd22ac47265be32b19817e5ef17e0482b95b36 100644 (file)
@@ -213,13 +213,14 @@ class AuthTest extends BrowserKitTest
     public function test_user_creation()
     {
         $user = factory(User::class)->make();
+        $adminRole = Role::getRole('admin');
 
         $this->asAdmin()
             ->visit('/settings/users')
             ->click('Add New User')
             ->type($user->name, '#name')
             ->type($user->email, '#email')
-            ->check('roles[admin]')
+            ->check("roles[{$adminRole->id}]")
             ->type($user->password, '#password')
             ->type($user->password, '#password-confirm')
             ->press('Save')
index 0d92241bbe30c59acc93a9c089bcfb2b06a43053..df3fd8d21e5053140a66e23298251a3a7725f3d1 100644 (file)
@@ -237,9 +237,9 @@ class LdapTest extends BrowserKitTest
 
     public function test_login_maps_roles_and_retains_existing_roles()
     {
-        $roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'display_name' => 'LdapTester']);
-        $roleToReceive2 = factory(Role::class)->create(['name' => 'ldaptester-second', 'display_name' => 'LdapTester Second']);
-        $existingRole = factory(Role::class)->create(['name' => 'ldaptester-existing']);
+        $roleToReceive = factory(Role::class)->create(['display_name' => 'LdapTester']);
+        $roleToReceive2 = factory(Role::class)->create(['display_name' => 'LdapTester Second']);
+        $existingRole = factory(Role::class)->create(['display_name' => 'ldaptester-existing']);
         $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
         $this->mockUser->attachRole($existingRole);
 
@@ -283,8 +283,8 @@ class LdapTest extends BrowserKitTest
 
     public function test_login_maps_roles_and_removes_old_roles_if_set()
     {
-        $roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'display_name' => 'LdapTester']);
-        $existingRole = factory(Role::class)->create(['name' => 'ldaptester-existing']);
+        $roleToReceive = factory(Role::class)->create(['display_name' => 'LdapTester']);
+        $existingRole = factory(Role::class)->create(['display_name' => 'ldaptester-existing']);
         $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
         $this->mockUser->attachRole($existingRole);
 
@@ -323,15 +323,15 @@ class LdapTest extends BrowserKitTest
 
     public function test_external_auth_id_visible_in_roles_page_when_ldap_active()
     {
-        $role = factory(Role::class)->create(['name' => 'ldaptester', 'external_auth_id' => 'ex-auth-a, test-second-param']);
+        $role = factory(Role::class)->create(['display_name' => 'ldaptester', 'external_auth_id' => 'ex-auth-a, test-second-param']);
         $this->asAdmin()->visit('/settings/roles/' . $role->id)
             ->see('ex-auth-a');
     }
 
     public function test_login_maps_roles_using_external_auth_ids_if_set()
     {
-        $roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'external_auth_id' => 'test-second-param, ex-auth-a']);
-        $roleToNotReceive = factory(Role::class)->create(['name' => 'ldaptester-not-receive', 'display_name' => 'ex-auth-a', 'external_auth_id' => 'test-second-param']);
+        $roleToReceive = factory(Role::class)->create(['display_name' => 'ldaptester', 'external_auth_id' => 'test-second-param, ex-auth-a']);
+        $roleToNotReceive = factory(Role::class)->create(['display_name' => 'ex-auth-a', 'external_auth_id' => 'test-second-param']);
 
         app('config')->set([
             'services.ldap.user_to_groups' => true,
@@ -368,8 +368,8 @@ class LdapTest extends BrowserKitTest
 
     public function test_login_group_mapping_does_not_conflict_with_default_role()
     {
-        $roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'display_name' => 'LdapTester']);
-        $roleToReceive2 = factory(Role::class)->create(['name' => 'ldaptester-second', 'display_name' => 'LdapTester Second']);
+        $roleToReceive = factory(Role::class)->create(['display_name' => 'LdapTester']);
+        $roleToReceive2 = factory(Role::class)->create(['display_name' => 'LdapTester Second']);
         $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
 
         setting()->put('registration-role', $roleToReceive->id);
index dff052891a1a9e9769cfe4c47701358576b6718d..73060c834383726a138fa8ce180980123070104a 100644 (file)
@@ -57,7 +57,7 @@ class RolesTest extends BrowserKitTest
             ->type('Test Role', 'display_name')
             ->type('A little test description', 'description')
             ->press('Save Role')
-            ->seeInDatabase('roles', ['display_name' => $testRoleName, 'name' => 'test-role', 'description' => $testRoleDesc])
+            ->seeInDatabase('roles', ['display_name' => $testRoleName, 'description' => $testRoleDesc])
             ->seePageIs('/settings/roles');
         // Updating
         $this->asAdmin()->visit('/settings/roles')
@@ -65,7 +65,7 @@ class RolesTest extends BrowserKitTest
             ->click($testRoleName)
             ->type($testRoleUpdateName, '#display_name')
             ->press('Save Role')
-            ->seeInDatabase('roles', ['display_name' => $testRoleUpdateName, 'name' => 'test-role', 'description' => $testRoleDesc])
+            ->seeInDatabase('roles', ['display_name' => $testRoleUpdateName, 'description' => $testRoleDesc])
             ->seePageIs('/settings/roles');
         // Deleting
         $this->asAdmin()->visit('/settings/roles')
@@ -99,6 +99,25 @@ class RolesTest extends BrowserKitTest
         $this->see('This user is the only user assigned to the administrator role');
     }
 
+    public function test_migrate_users_on_delete_works()
+    {
+        $roleA = Role::query()->create(['display_name' => 'Delete Test A']);
+        $roleB = Role::query()->create(['display_name' => 'Delete Test B']);
+        $this->user->attachRole($roleB);
+
+        $this->assertCount(0, $roleA->users()->get());
+        $this->assertCount(1, $roleB->users()->get());
+
+        $deletePage = $this->asAdmin()->get("/settings/roles/delete/{$roleB->id}");
+        $deletePage->seeElement('select[name=migrate_role_id]');
+        $this->asAdmin()->delete("/settings/roles/delete/{$roleB->id}", [
+            'migrate_role_id' => $roleA->id,
+        ]);
+
+        $this->assertCount(1, $roleA->users()->get());
+        $this->assertEquals($this->user->id, $roleA->users()->first()->id);
+    }
+
     public function test_manage_user_permission()
     {
         $this->actingAs($this->user)->visit('/settings/users')
@@ -667,9 +686,11 @@ class RolesTest extends BrowserKitTest
     public function test_public_role_visible_in_user_edit_screen()
     {
         $user = \BookStack\Auth\User::first();
+        $adminRole = Role::getSystemRole('admin');
+        $publicRole = Role::getSystemRole('public');
         $this->asAdmin()->visit('/settings/users/' . $user->id)
-            ->seeElement('[name="roles[admin]"]')
-            ->seeElement('[name="roles[public]"]');
+            ->seeElement('[name="roles['.$adminRole->id.']"]')
+            ->seeElement('[name="roles['.$publicRole->id.']"]');
     }
 
     public function test_public_role_visible_in_role_listing()
@@ -682,9 +703,8 @@ class RolesTest extends BrowserKitTest
     public function test_public_role_visible_in_default_role_setting()
     {
         $this->asAdmin()->visit('/settings')
-            ->seeElement('[data-role-name="admin"]')
-            ->seeElement('[data-role-name="public"]');
-
+            ->seeElement('[data-system-role-name="admin"]')
+            ->seeElement('[data-system-role-name="public"]');
     }
 
     public function test_public_role_not_deleteable()