Refactored some permission controls and increased testing for roles system
Showing
8 changed files
with
216 additions
and
93 deletions
app/Exceptions/PermissionsException.php
0 → 100644
| 1 | -<?php | 1 | +<?php namespace BookStack\Http\Controllers; |
| 2 | 2 | ||
| 3 | -namespace BookStack\Http\Controllers; | 3 | +use BookStack\Exceptions\PermissionsException; |
| 4 | - | 4 | +use BookStack\Repos\PermissionsRepo; |
| 5 | -use BookStack\Permission; | ||
| 6 | -use BookStack\Role; | ||
| 7 | use Illuminate\Http\Request; | 5 | use Illuminate\Http\Request; |
| 8 | use BookStack\Http\Requests; | 6 | use BookStack\Http\Requests; |
| 9 | 7 | ||
| 10 | class PermissionController extends Controller | 8 | class PermissionController extends Controller |
| 11 | { | 9 | { |
| 12 | 10 | ||
| 13 | - protected $role; | 11 | + protected $permissionsRepo; |
| 14 | - protected $permission; | ||
| 15 | 12 | ||
| 16 | /** | 13 | /** |
| 17 | * PermissionController constructor. | 14 | * PermissionController constructor. |
| 18 | - * @param Role $role | 15 | + * @param PermissionsRepo $permissionsRepo |
| 19 | - * @param Permission $permission | ||
| 20 | - * @internal param $user | ||
| 21 | */ | 16 | */ |
| 22 | - public function __construct(Role $role, Permission $permission) | 17 | + public function __construct(PermissionsRepo $permissionsRepo) |
| 23 | { | 18 | { |
| 24 | - $this->role = $role; | 19 | + $this->permissionsRepo = $permissionsRepo; |
| 25 | - $this->permission = $permission; | ||
| 26 | parent::__construct(); | 20 | parent::__construct(); |
| 27 | } | 21 | } |
| 28 | 22 | ||
| ... | @@ -32,7 +26,7 @@ class PermissionController extends Controller | ... | @@ -32,7 +26,7 @@ class PermissionController extends Controller |
| 32 | public function listRoles() | 26 | public function listRoles() |
| 33 | { | 27 | { |
| 34 | $this->checkPermission('user-roles-manage'); | 28 | $this->checkPermission('user-roles-manage'); |
| 35 | - $roles = $this->role->all(); | 29 | + $roles = $this->permissionsRepo->getAllRoles(); |
| 36 | return view('settings/roles/index', ['roles' => $roles]); | 30 | return view('settings/roles/index', ['roles' => $roles]); |
| 37 | } | 31 | } |
| 38 | 32 | ||
| ... | @@ -59,22 +53,7 @@ class PermissionController extends Controller | ... | @@ -59,22 +53,7 @@ class PermissionController extends Controller |
| 59 | 'description' => 'max:250' | 53 | 'description' => 'max:250' |
| 60 | ]); | 54 | ]); |
| 61 | 55 | ||
| 62 | - $role = $this->role->newInstance($request->all()); | 56 | + $this->permissionsRepo->saveNewRole($request->all()); |
| 63 | - $role->name = str_replace(' ', '-', strtolower($request->get('display_name'))); | ||
| 64 | - // Prevent duplicate names | ||
| 65 | - while ($this->role->where('name', '=', $role->name)->count() > 0) { | ||
| 66 | - $role->name .= strtolower(str_random(2)); | ||
| 67 | - } | ||
| 68 | - $role->save(); | ||
| 69 | - | ||
| 70 | - if ($request->has('permissions')) { | ||
| 71 | - $permissionsNames = array_keys($request->get('permissions')); | ||
| 72 | - $permissions = $this->permission->whereIn('name', $permissionsNames)->pluck('id')->toArray(); | ||
| 73 | - $role->permissions()->sync($permissions); | ||
| 74 | - } else { | ||
| 75 | - $role->permissions()->sync([]); | ||
| 76 | - } | ||
| 77 | - | ||
| 78 | session()->flash('success', 'Role successfully created'); | 57 | session()->flash('success', 'Role successfully created'); |
| 79 | return redirect('/settings/roles'); | 58 | return redirect('/settings/roles'); |
| 80 | } | 59 | } |
| ... | @@ -87,7 +66,7 @@ class PermissionController extends Controller | ... | @@ -87,7 +66,7 @@ class PermissionController extends Controller |
| 87 | public function editRole($id) | 66 | public function editRole($id) |
| 88 | { | 67 | { |
| 89 | $this->checkPermission('user-roles-manage'); | 68 | $this->checkPermission('user-roles-manage'); |
| 90 | - $role = $this->role->findOrFail($id); | 69 | + $role = $this->permissionsRepo->getRoleById($id); |
| 91 | return view('settings/roles/edit', ['role' => $role]); | 70 | return view('settings/roles/edit', ['role' => $role]); |
| 92 | } | 71 | } |
| 93 | 72 | ||
| ... | @@ -105,24 +84,7 @@ class PermissionController extends Controller | ... | @@ -105,24 +84,7 @@ class PermissionController extends Controller |
| 105 | 'description' => 'max:250' | 84 | 'description' => 'max:250' |
| 106 | ]); | 85 | ]); |
| 107 | 86 | ||
| 108 | - $role = $this->role->findOrFail($id); | 87 | + $this->permissionsRepo->updateRole($id, $request->all()); |
| 109 | - if ($request->has('permissions')) { | ||
| 110 | - $permissionsNames = array_keys($request->get('permissions')); | ||
| 111 | - $permissions = $this->permission->whereIn('name', $permissionsNames)->pluck('id')->toArray(); | ||
| 112 | - $role->permissions()->sync($permissions); | ||
| 113 | - } else { | ||
| 114 | - $role->permissions()->sync([]); | ||
| 115 | - } | ||
| 116 | - | ||
| 117 | - // Ensure admin account always has all permissions | ||
| 118 | - if ($role->name === 'admin') { | ||
| 119 | - $permissions = $this->permission->all()->pluck('id')->toArray(); | ||
| 120 | - $role->permissions()->sync($permissions); | ||
| 121 | - } | ||
| 122 | - | ||
| 123 | - $role->fill($request->all()); | ||
| 124 | - $role->save(); | ||
| 125 | - | ||
| 126 | session()->flash('success', 'Role successfully updated'); | 88 | session()->flash('success', 'Role successfully updated'); |
| 127 | return redirect('/settings/roles'); | 89 | return redirect('/settings/roles'); |
| 128 | } | 90 | } |
| ... | @@ -136,9 +98,9 @@ class PermissionController extends Controller | ... | @@ -136,9 +98,9 @@ class PermissionController extends Controller |
| 136 | public function showDeleteRole($id) | 98 | public function showDeleteRole($id) |
| 137 | { | 99 | { |
| 138 | $this->checkPermission('user-roles-manage'); | 100 | $this->checkPermission('user-roles-manage'); |
| 139 | - $role = $this->role->findOrFail($id); | 101 | + $role = $this->permissionsRepo->getRoleById($id); |
| 140 | - $roles = $this->role->where('id', '!=', $id)->get(); | 102 | + $roles = $this->permissionsRepo->getAllRolesExcept($role); |
| 141 | - $blankRole = $this->role->newInstance(['display_name' => 'Don\'t migrate users']); | 103 | + $blankRole = $role->newInstance(['display_name' => 'Don\'t migrate users']); |
| 142 | $roles->prepend($blankRole); | 104 | $roles->prepend($blankRole); |
| 143 | return view('settings/roles/delete', ['role' => $role, 'roles' => $roles]); | 105 | return view('settings/roles/delete', ['role' => $role, 'roles' => $roles]); |
| 144 | } | 106 | } |
| ... | @@ -153,29 +115,14 @@ class PermissionController extends Controller | ... | @@ -153,29 +115,14 @@ class PermissionController extends Controller |
| 153 | public function deleteRole($id, Request $request) | 115 | public function deleteRole($id, Request $request) |
| 154 | { | 116 | { |
| 155 | $this->checkPermission('user-roles-manage'); | 117 | $this->checkPermission('user-roles-manage'); |
| 156 | - $role = $this->role->findOrFail($id); | ||
| 157 | - | ||
| 158 | - // Prevent deleting admin role | ||
| 159 | - if ($role->name === 'admin') { | ||
| 160 | - session()->flash('error', 'The admin role cannot be deleted'); | ||
| 161 | - return redirect()->back(); | ||
| 162 | - } | ||
| 163 | 118 | ||
| 164 | - if ($role->id == \Setting::get('registration-role')) { | 119 | + try { |
| 165 | - session()->flash('error', 'This role cannot be deleted while set as the default registration role.'); | 120 | + $this->permissionsRepo->deleteRole($id, $request->get('migrate_role_id')); |
| 121 | + } catch (PermissionsException $e) { | ||
| 122 | + session()->flash('error', $e->getMessage()); | ||
| 166 | return redirect()->back(); | 123 | return redirect()->back(); |
| 167 | } | 124 | } |
| 168 | 125 | ||
| 169 | - if ($request->has('migration_role_id')) { | ||
| 170 | - $newRole = $this->role->find($request->get('migration_role_id')); | ||
| 171 | - if ($newRole) { | ||
| 172 | - $users = $role->users->pluck('id')->toArray(); | ||
| 173 | - $newRole->users()->sync($users); | ||
| 174 | - } | ||
| 175 | - } | ||
| 176 | - | ||
| 177 | - $role->delete(); | ||
| 178 | - | ||
| 179 | session()->flash('success', 'Role successfully deleted'); | 126 | session()->flash('success', 'Role successfully deleted'); |
| 180 | return redirect('/settings/roles'); | 127 | return redirect('/settings/roles'); |
| 181 | } | 128 | } | ... | ... |
app/Repos/PermissionsRepo.php
0 → 100644
| 1 | +<?php namespace BookStack\Repos; | ||
| 2 | + | ||
| 3 | + | ||
| 4 | +use BookStack\Exceptions\PermissionsException; | ||
| 5 | +use BookStack\Permission; | ||
| 6 | +use BookStack\Role; | ||
| 7 | +use Setting; | ||
| 8 | + | ||
| 9 | +class PermissionsRepo | ||
| 10 | +{ | ||
| 11 | + | ||
| 12 | + protected $permission; | ||
| 13 | + protected $role; | ||
| 14 | + | ||
| 15 | + /** | ||
| 16 | + * PermissionsRepo constructor. | ||
| 17 | + * @param $permission | ||
| 18 | + * @param $role | ||
| 19 | + */ | ||
| 20 | + public function __construct(Permission $permission, Role $role) | ||
| 21 | + { | ||
| 22 | + $this->permission = $permission; | ||
| 23 | + $this->role = $role; | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + /** | ||
| 27 | + * Get all the user roles from the system. | ||
| 28 | + * @return \Illuminate\Database\Eloquent\Collection|static[] | ||
| 29 | + */ | ||
| 30 | + public function getAllRoles() | ||
| 31 | + { | ||
| 32 | + return $this->role->all(); | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + /** | ||
| 36 | + * Get all the roles except for the provided one. | ||
| 37 | + * @param Role $role | ||
| 38 | + * @return mixed | ||
| 39 | + */ | ||
| 40 | + public function getAllRolesExcept(Role $role) | ||
| 41 | + { | ||
| 42 | + return $this->role->where('id', '!=', $role->id)->get(); | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + /** | ||
| 46 | + * Get a role via its ID. | ||
| 47 | + * @param $id | ||
| 48 | + * @return mixed | ||
| 49 | + */ | ||
| 50 | + public function getRoleById($id) | ||
| 51 | + { | ||
| 52 | + return $this->role->findOrFail($id); | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + /** | ||
| 56 | + * Save a new role into the system. | ||
| 57 | + * @param array $roleData | ||
| 58 | + * @return Role | ||
| 59 | + */ | ||
| 60 | + public function saveNewRole($roleData) | ||
| 61 | + { | ||
| 62 | + $role = $this->role->newInstance($roleData); | ||
| 63 | + $role->name = str_replace(' ', '-', strtolower($roleData['display_name'])); | ||
| 64 | + // Prevent duplicate names | ||
| 65 | + while ($this->role->where('name', '=', $role->name)->count() > 0) { | ||
| 66 | + $role->name .= strtolower(str_random(2)); | ||
| 67 | + } | ||
| 68 | + $role->save(); | ||
| 69 | + | ||
| 70 | + $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : []; | ||
| 71 | + $this->assignRolePermissions($role, $permissions); | ||
| 72 | + return $role; | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | + /** | ||
| 76 | + * Updates an existing role. | ||
| 77 | + * Ensure Admin role always has all permissions. | ||
| 78 | + * @param $roleId | ||
| 79 | + * @param $roleData | ||
| 80 | + */ | ||
| 81 | + public function updateRole($roleId, $roleData) | ||
| 82 | + { | ||
| 83 | + $role = $this->role->findOrFail($roleId); | ||
| 84 | + $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : []; | ||
| 85 | + $this->assignRolePermissions($role, $permissions); | ||
| 86 | + | ||
| 87 | + if ($role->name === 'admin') { | ||
| 88 | + $permissions = $this->permission->all()->pluck('id')->toArray(); | ||
| 89 | + $role->permissions()->sync($permissions); | ||
| 90 | + } | ||
| 91 | + | ||
| 92 | + $role->fill($roleData); | ||
| 93 | + $role->save(); | ||
| 94 | + } | ||
| 95 | + | ||
| 96 | + /** | ||
| 97 | + * Assign an list of permission names to an role. | ||
| 98 | + * @param Role $role | ||
| 99 | + * @param array $permissionNameArray | ||
| 100 | + */ | ||
| 101 | + public function assignRolePermissions(Role $role, $permissionNameArray = []) | ||
| 102 | + { | ||
| 103 | + $permissions = []; | ||
| 104 | + if ($permissionNameArray && count($permissionNameArray) > 0) { | ||
| 105 | + $permissions = $this->permission->whereIn('name', $permissionNameArray)->pluck('id')->toArray(); | ||
| 106 | + } | ||
| 107 | + $role->permissions()->sync($permissions); | ||
| 108 | + } | ||
| 109 | + | ||
| 110 | + /** | ||
| 111 | + * Delete a role from the system. | ||
| 112 | + * Check it's not an admin role or set as default before deleting. | ||
| 113 | + * If an migration Role ID is specified the users assign to the current role | ||
| 114 | + * will be added to the role of the specified id. | ||
| 115 | + * @param $roleId | ||
| 116 | + * @param $migrateRoleId | ||
| 117 | + * @throws PermissionsException | ||
| 118 | + */ | ||
| 119 | + public function deleteRole($roleId, $migrateRoleId) | ||
| 120 | + { | ||
| 121 | + $role = $this->role->findOrFail($roleId); | ||
| 122 | + | ||
| 123 | + // Prevent deleting admin role or default registration role. | ||
| 124 | + if ($role->name === 'admin') { | ||
| 125 | + throw new PermissionsException('The admin role cannot be deleted'); | ||
| 126 | + } else if ($role->id == Setting::get('registration-role')) { | ||
| 127 | + throw new PermissionsException('This role cannot be deleted while set as the default registration role.'); | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + if ($migrateRoleId) { | ||
| 131 | + $newRole = $this->role->find($migrateRoleId); | ||
| 132 | + if ($newRole) { | ||
| 133 | + $users = $role->users->pluck('id')->toArray(); | ||
| 134 | + $newRole->users()->sync($users); | ||
| 135 | + } | ||
| 136 | + } | ||
| 137 | + | ||
| 138 | + $role->delete(); | ||
| 139 | + } | ||
| 140 | + | ||
| 141 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| ... | @@ -8,11 +8,6 @@ class Role extends Model | ... | @@ -8,11 +8,6 @@ class Role extends Model |
| 8 | { | 8 | { |
| 9 | 9 | ||
| 10 | protected $fillable = ['display_name', 'description']; | 10 | protected $fillable = ['display_name', 'description']; |
| 11 | - /** | ||
| 12 | - * Sets the default role name for newly registered users. | ||
| 13 | - * @var string | ||
| 14 | - */ | ||
| 15 | - protected static $default = 'viewer'; | ||
| 16 | 11 | ||
| 17 | /** | 12 | /** |
| 18 | * The roles that belong to the role. | 13 | * The roles that belong to the role. |
| ... | @@ -49,15 +44,6 @@ class Role extends Model | ... | @@ -49,15 +44,6 @@ class Role extends Model |
| 49 | } | 44 | } |
| 50 | 45 | ||
| 51 | /** | 46 | /** |
| 52 | - * Get an instance of the default role. | ||
| 53 | - * @return Role | ||
| 54 | - */ | ||
| 55 | - public static function getDefault() | ||
| 56 | - { | ||
| 57 | - return static::getRole(static::$default); | ||
| 58 | - } | ||
| 59 | - | ||
| 60 | - /** | ||
| 61 | * Get the role object for the specified role. | 47 | * Get the role object for the specified role. |
| 62 | * @param $roleName | 48 | * @param $roleName |
| 63 | * @return mixed | 49 | * @return mixed | ... | ... |
| ... | @@ -106,7 +106,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon | ... | @@ -106,7 +106,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon |
| 106 | */ | 106 | */ |
| 107 | public function attachRoleId($id) | 107 | public function attachRoleId($id) |
| 108 | { | 108 | { |
| 109 | - $this->roles()->sync([$id]); | 109 | + $this->roles()->attach([$id]); |
| 110 | } | 110 | } |
| 111 | 111 | ||
| 112 | /** | 112 | /** | ... | ... |
| ... | @@ -17,6 +17,7 @@ $factory->define(BookStack\User::class, function ($faker) { | ... | @@ -17,6 +17,7 @@ $factory->define(BookStack\User::class, function ($faker) { |
| 17 | 'email' => $faker->email, | 17 | 'email' => $faker->email, |
| 18 | 'password' => str_random(10), | 18 | 'password' => str_random(10), |
| 19 | 'remember_token' => str_random(10), | 19 | 'remember_token' => str_random(10), |
| 20 | + 'email_confirmed' => 1 | ||
| 20 | ]; | 21 | ]; |
| 21 | }); | 22 | }); |
| 22 | 23 | ||
| ... | @@ -45,3 +46,10 @@ $factory->define(BookStack\Page::class, function ($faker) { | ... | @@ -45,3 +46,10 @@ $factory->define(BookStack\Page::class, function ($faker) { |
| 45 | 'text' => strip_tags($html) | 46 | 'text' => strip_tags($html) |
| 46 | ]; | 47 | ]; |
| 47 | }); | 48 | }); |
| 49 | + | ||
| 50 | +$factory->define(BookStack\Role::class, function ($faker) { | ||
| 51 | + return [ | ||
| 52 | + 'display_name' => $faker->sentence(3), | ||
| 53 | + 'description' => $faker->sentence(10) | ||
| 54 | + ]; | ||
| 55 | +}); | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| ... | @@ -9,13 +9,14 @@ class RolesTest extends TestCase | ... | @@ -9,13 +9,14 @@ class RolesTest extends TestCase |
| 9 | parent::setUp(); | 9 | parent::setUp(); |
| 10 | } | 10 | } |
| 11 | 11 | ||
| 12 | + /** | ||
| 13 | + * Create a new basic role for testing purposes. | ||
| 14 | + * @return static | ||
| 15 | + */ | ||
| 12 | protected function createNewRole() | 16 | protected function createNewRole() |
| 13 | { | 17 | { |
| 14 | - return \BookStack\Role::forceCreate([ | 18 | + $permissionRepo = app('BookStack\Repos\PermissionsRepo'); |
| 15 | - 'name' => 'test-role', | 19 | + return $permissionRepo->saveNewRole(factory(\BookStack\Role::class)->make()->toArray()); |
| 16 | - 'display_name' => 'Test Role', | ||
| 17 | - 'description' => 'This is a role for testing' | ||
| 18 | - ]); | ||
| 19 | } | 20 | } |
| 20 | 21 | ||
| 21 | public function test_admin_can_see_settings() | 22 | public function test_admin_can_see_settings() |
| ... | @@ -45,4 +46,38 @@ class RolesTest extends TestCase | ... | @@ -45,4 +46,38 @@ class RolesTest extends TestCase |
| 45 | ->see('cannot be deleted'); | 46 | ->see('cannot be deleted'); |
| 46 | } | 47 | } |
| 47 | 48 | ||
| 49 | + public function test_role_create_update_delete_flow() | ||
| 50 | + { | ||
| 51 | + $testRoleName = 'Test Role'; | ||
| 52 | + $testRoleDesc = 'a little test description'; | ||
| 53 | + $testRoleUpdateName = 'An Super Updated role'; | ||
| 54 | + | ||
| 55 | + // Creation | ||
| 56 | + $this->asAdmin()->visit('/settings') | ||
| 57 | + ->click('Roles') | ||
| 58 | + ->seePageIs('/settings/roles') | ||
| 59 | + ->click('Add new role') | ||
| 60 | + ->type('Test Role', 'display_name') | ||
| 61 | + ->type('A little test description', 'description') | ||
| 62 | + ->press('Save Role') | ||
| 63 | + ->seeInDatabase('roles', ['display_name' => $testRoleName, 'name' => 'test-role', 'description' => $testRoleDesc]) | ||
| 64 | + ->seePageIs('/settings/roles'); | ||
| 65 | + // Updating | ||
| 66 | + $this->asAdmin()->visit('/settings/roles') | ||
| 67 | + ->see($testRoleDesc) | ||
| 68 | + ->click($testRoleName) | ||
| 69 | + ->type($testRoleUpdateName, '#display_name') | ||
| 70 | + ->press('Save Role') | ||
| 71 | + ->seeInDatabase('roles', ['display_name' => $testRoleUpdateName, 'name' => 'test-role', 'description' => $testRoleDesc]) | ||
| 72 | + ->seePageIs('/settings/roles'); | ||
| 73 | + // Deleting | ||
| 74 | + $this->asAdmin()->visit('/settings/roles') | ||
| 75 | + ->click($testRoleUpdateName) | ||
| 76 | + ->click('Delete Role') | ||
| 77 | + ->see($testRoleUpdateName) | ||
| 78 | + ->press('Confirm') | ||
| 79 | + ->seePageIs('/settings/roles') | ||
| 80 | + ->dontSee($testRoleUpdateName); | ||
| 81 | + } | ||
| 82 | + | ||
| 48 | } | 83 | } | ... | ... |
| ... | @@ -79,8 +79,8 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase | ... | @@ -79,8 +79,8 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase |
| 79 | protected function getNewUser($attributes = []) | 79 | protected function getNewUser($attributes = []) |
| 80 | { | 80 | { |
| 81 | $user = factory(\BookStack\User::class)->create($attributes); | 81 | $user = factory(\BookStack\User::class)->create($attributes); |
| 82 | - $userRepo = app('BookStack\Repos\UserRepo'); | 82 | + $role = \BookStack\Role::getRole('editor'); |
| 83 | - $userRepo->attachDefaultRole($user); | 83 | + $user->attachRole($role);; |
| 84 | return $user; | 84 | return $user; |
| 85 | } | 85 | } |
| 86 | 86 | ... | ... |
-
Please register or sign in to post a comment