Dan Brown

Refactored some permission controls and increased testing for roles system

1 +<?php namespace BookStack\Exceptions;
2 +
3 +
4 +use Exception;
5 +
6 +class PermissionsException extends Exception {}
...\ No newline at end of file ...\ No newline at end of file
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 }
......
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
......