Dan Brown

Continued with database work for permissions overhaul

Added to the entity_permissions table with further required fields and indexes.
Wrote the code for checking permissions.
1 +<?php
2 +
3 +namespace BookStack\Console\Commands;
4 +
5 +use BookStack\Services\RestrictionService;
6 +use Illuminate\Console\Command;
7 +
8 +class RegeneratePermissions extends Command
9 +{
10 + /**
11 + * The name and signature of the console command.
12 + *
13 + * @var string
14 + */
15 + protected $signature = 'permissions:regen';
16 +
17 + /**
18 + * The console command description.
19 + *
20 + * @var string
21 + */
22 + protected $description = 'Regenerate all system permissions';
23 +
24 + /**
25 + * The service to handle the permission system.
26 + *
27 + * @var RestrictionService
28 + */
29 + protected $restrictionService;
30 +
31 + /**
32 + * Create a new command instance.
33 + *
34 + * @param RestrictionService $restrictionService
35 + */
36 + public function __construct(RestrictionService $restrictionService)
37 + {
38 + $this->restrictionService = $restrictionService;
39 + parent::__construct();
40 + }
41 +
42 + /**
43 + * Execute the console command.
44 + *
45 + * @return mixed
46 + */
47 + public function handle()
48 + {
49 + $this->restrictionService->buildEntityPermissions();
50 + }
51 +}
...@@ -15,6 +15,7 @@ class Kernel extends ConsoleKernel ...@@ -15,6 +15,7 @@ class Kernel extends ConsoleKernel
15 protected $commands = [ 15 protected $commands = [
16 \BookStack\Console\Commands\Inspire::class, 16 \BookStack\Console\Commands\Inspire::class,
17 \BookStack\Console\Commands\ResetViews::class, 17 \BookStack\Console\Commands\ResetViews::class,
18 + \BookStack\Console\Commands\RegeneratePermissions::class,
18 ]; 19 ];
19 20
20 /** 21 /**
......
...@@ -74,6 +74,15 @@ abstract class Entity extends Ownable ...@@ -74,6 +74,15 @@ abstract class Entity extends Ownable
74 } 74 }
75 75
76 /** 76 /**
77 + * Get the entity permissions this is connected to.
78 + * @return \Illuminate\Database\Eloquent\Relations\MorphMany
79 + */
80 + public function permissions()
81 + {
82 + return $this->morphMany(EntityPermission::class, 'entity');
83 + }
84 +
85 + /**
77 * Allows checking of the exact class, Used to check entity type. 86 * Allows checking of the exact class, Used to check entity type.
78 * Cleaner method for is_a. 87 * Cleaner method for is_a.
79 * @param $type 88 * @param $type
...@@ -81,7 +90,16 @@ abstract class Entity extends Ownable ...@@ -81,7 +90,16 @@ abstract class Entity extends Ownable
81 */ 90 */
82 public static function isA($type) 91 public static function isA($type)
83 { 92 {
84 - return static::getClassName() === strtolower($type); 93 + return static::getType() === strtolower($type);
94 + }
95 +
96 + /**
97 + * Get entity type.
98 + * @return mixed
99 + */
100 + public static function getType()
101 + {
102 + return strtolower(static::getClassName());
85 } 103 }
86 104
87 /** 105 /**
......
...@@ -5,7 +5,6 @@ use BookStack\Chapter; ...@@ -5,7 +5,6 @@ use BookStack\Chapter;
5 use BookStack\Entity; 5 use BookStack\Entity;
6 use BookStack\EntityPermission; 6 use BookStack\EntityPermission;
7 use BookStack\Page; 7 use BookStack\Page;
8 -use BookStack\Permission;
9 use BookStack\Role; 8 use BookStack\Role;
10 use Illuminate\Database\Eloquent\Collection; 9 use Illuminate\Database\Eloquent\Collection;
11 10
...@@ -23,18 +22,19 @@ class RestrictionService ...@@ -23,18 +22,19 @@ class RestrictionService
23 22
24 protected $entityPermission; 23 protected $entityPermission;
25 protected $role; 24 protected $role;
26 - protected $permission; 25 +
26 + protected $actions = ['view', 'create', 'update', 'delete'];
27 27
28 /** 28 /**
29 * RestrictionService constructor. 29 * RestrictionService constructor.
30 + * TODO - Handle events when roles or entities change.
30 * @param EntityPermission $entityPermission 31 * @param EntityPermission $entityPermission
31 * @param Book $book 32 * @param Book $book
32 * @param Chapter $chapter 33 * @param Chapter $chapter
33 * @param Page $page 34 * @param Page $page
34 * @param Role $role 35 * @param Role $role
35 - * @param Permission $permission
36 */ 36 */
37 - public function __construct(EntityPermission $entityPermission, Book $book, Chapter $chapter, Page $page, Role $role, Permission $permission) 37 + public function __construct(EntityPermission $entityPermission, Book $book, Chapter $chapter, Page $page, Role $role)
38 { 38 {
39 $this->currentUser = auth()->user(); 39 $this->currentUser = auth()->user();
40 $this->userRoles = $this->currentUser ? $this->currentUser->roles->pluck('id') : []; 40 $this->userRoles = $this->currentUser ? $this->currentUser->roles->pluck('id') : [];
...@@ -42,13 +42,11 @@ class RestrictionService ...@@ -42,13 +42,11 @@ class RestrictionService
42 42
43 $this->entityPermission = $entityPermission; 43 $this->entityPermission = $entityPermission;
44 $this->role = $role; 44 $this->role = $role;
45 - $this->permission = $permission;
46 $this->book = $book; 45 $this->book = $book;
47 $this->chapter = $chapter; 46 $this->chapter = $chapter;
48 $this->page = $page; 47 $this->page = $page;
49 } 48 }
50 49
51 -
52 /** 50 /**
53 * Re-generate all entity permission from scratch. 51 * Re-generate all entity permission from scratch.
54 */ 52 */
...@@ -65,12 +63,12 @@ class RestrictionService ...@@ -65,12 +63,12 @@ class RestrictionService
65 }); 63 });
66 64
67 // Chunk through all chapters 65 // Chunk through all chapters
68 - $this->chapter->chunk(500, function ($books) use ($roles) { 66 + $this->chapter->with('book')->chunk(500, function ($books) use ($roles) {
69 $this->createManyEntityPermissions($books, $roles); 67 $this->createManyEntityPermissions($books, $roles);
70 }); 68 });
71 69
72 // Chunk through all pages 70 // Chunk through all pages
73 - $this->page->chunk(500, function ($books) use ($roles) { 71 + $this->page->with('book', 'chapter')->chunk(500, function ($books) use ($roles) {
74 $this->createManyEntityPermissions($books, $roles); 72 $this->createManyEntityPermissions($books, $roles);
75 }); 73 });
76 } 74 }
...@@ -85,16 +83,69 @@ class RestrictionService ...@@ -85,16 +83,69 @@ class RestrictionService
85 $entityPermissions = []; 83 $entityPermissions = [];
86 foreach ($entities as $entity) { 84 foreach ($entities as $entity) {
87 foreach ($roles as $role) { 85 foreach ($roles as $role) {
88 - $entityPermissions[] = $this->createEntityPermission($entity, $role); 86 + foreach ($this->actions as $action) {
87 + $entityPermissions[] = $this->createEntityPermissionData($entity, $role, $action);
88 + }
89 } 89 }
90 } 90 }
91 $this->entityPermission->insert($entityPermissions); 91 $this->entityPermission->insert($entityPermissions);
92 } 92 }
93 93
94 94
95 - protected function createEntityPermissionData(Entity $entity, Role $role) 95 + protected function createEntityPermissionData(Entity $entity, Role $role, $action)
96 { 96 {
97 - // TODO - Check the permission values and return an EntityPermission 97 + $permissionPrefix = $entity->getType() . '-' . $action;
98 + $roleHasPermission = $role->hasPermission($permissionPrefix . '-all');
99 + $roleHasPermissionOwn = $role->hasPermission($permissionPrefix . '-own');
100 +
101 + if ($entity->isA('book')) {
102 +
103 + if (!$entity->restricted) {
104 + return $this->createEntityPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
105 + } else {
106 + $hasAccess = $entity->hasRestriction($role->id, $action);
107 + return $this->createEntityPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
108 + }
109 +
110 + } elseif ($entity->isA('chapter')) {
111 +
112 + if (!$entity->restricted) {
113 + $hasAccessToBook = $entity->book->hasRestriction($role->id, $action);
114 + return $this->createEntityPermissionDataArray($entity, $role, $action,
115 + ($roleHasPermission && $hasAccessToBook), ($roleHasPermissionOwn && $hasAccessToBook));
116 + } else {
117 + $hasAccess = $entity->hasRestriction($role->id, $action);
118 + return $this->createEntityPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
119 + }
120 +
121 + } elseif ($entity->isA('page')) {
122 +
123 + if (!$entity->restricted) {
124 + $hasAccessToBook = $entity->book->hasRestriction($role->id, $action);
125 + $hasAccessToChapter = $entity->chapter ? ($entity->chapter->hasRestriction($role->id, $action)) : true;
126 + return $this->createEntityPermissionDataArray($entity, $role, $action,
127 + ($roleHasPermission && $hasAccessToBook && $hasAccessToChapter),
128 + ($roleHasPermissionOwn && $hasAccessToBook && $hasAccessToChapter));
129 + } else {
130 + $hasAccess = $entity->hasRestriction($role->id, $action);
131 + return $this->createEntityPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
132 + }
133 +
134 + }
135 + }
136 +
137 + protected function createEntityPermissionDataArray(Entity $entity, Role $role, $action, $permissionAll, $permissionOwn)
138 + {
139 + $entityClass = get_class($entity);
140 + return [
141 + 'role_id' => $role->id,
142 + 'entity_id' => $entity->id,
143 + 'entity_type' => $entityClass,
144 + 'action' => $action,
145 + 'has_permission' => $permissionAll,
146 + 'has_permission_own' => $permissionOwn,
147 + 'created_by' => $entity->created_by
148 + ];
98 } 149 }
99 150
100 /** 151 /**
...@@ -157,86 +208,29 @@ class RestrictionService ...@@ -157,86 +208,29 @@ class RestrictionService
157 208
158 if ($this->isAdmin) return $query; 209 if ($this->isAdmin) return $query;
159 $this->currentAction = $action; 210 $this->currentAction = $action;
160 - return $this->pageRestrictionQuery($query); 211 + return $this->entityRestrictionQuery($query);
161 } 212 }
162 213
163 /** 214 /**
164 - * The base query for restricting pages. 215 + * The general query filter to remove all entities
216 + * that the current user does not have access to.
165 * @param $query 217 * @param $query
166 * @return mixed 218 * @return mixed
167 */ 219 */
168 - private function pageRestrictionQuery($query) 220 + protected function entityRestrictionQuery($query)
169 { 221 {
170 - return $query->where(function ($parentWhereQuery) { 222 + return $query->where(function ($parentQuery) {
171 - 223 + $parentQuery->whereHas('permissions', function ($permissionQuery) {
172 - $parentWhereQuery 224 + $permissionQuery->whereIn('role_id', $this->userRoles)
173 - // (Book & chapter & page) or (Book & page & NO CHAPTER) unrestricted 225 + ->where('action', '=', $this->currentAction)
174 - ->where(function ($query) { 226 + ->where(function ($query) {
175 - $query->where(function ($query) { 227 + $query->where('has_permission', '=', true)
176 - $query->whereExists(function ($query) { 228 + ->orWhere(function ($query) {
177 - $query->select('*')->from('chapters') 229 + $query->where('has_permission_own', '=', true)
178 - ->whereRaw('chapters.id=pages.chapter_id') 230 + ->where('created_by', '=', $this->currentUser->id);
179 - ->where('restricted', '=', false);
180 - })->whereExists(function ($query) {
181 - $query->select('*')->from('books')
182 - ->whereRaw('books.id=pages.book_id')
183 - ->where('restricted', '=', false);
184 - })->where('restricted', '=', false);
185 - })->orWhere(function ($query) {
186 - $query->where('restricted', '=', false)->where('chapter_id', '=', 0)
187 - ->whereExists(function ($query) {
188 - $query->select('*')->from('books')
189 - ->whereRaw('books.id=pages.book_id')
190 - ->where('restricted', '=', false);
191 }); 231 });
192 }); 232 });
193 - }) 233 + });
194 - // Page unrestricted, Has no chapter & book has accepted restrictions
195 - ->orWhere(function ($query) {
196 - $query->where('restricted', '=', false)
197 - ->whereExists(function ($query) {
198 - $query->select('*')->from('chapters')
199 - ->whereRaw('chapters.id=pages.chapter_id');
200 - }, 'and', true)
201 - ->whereExists(function ($query) {
202 - $query->select('*')->from('books')
203 - ->whereRaw('books.id=pages.book_id')
204 - ->whereExists(function ($query) {
205 - $this->checkRestrictionsQuery($query, 'books', 'Book');
206 - });
207 - });
208 - })
209 - // Page unrestricted, Has an unrestricted chapter & book has accepted restrictions
210 - ->orWhere(function ($query) {
211 - $query->where('restricted', '=', false)
212 - ->whereExists(function ($query) {
213 - $query->select('*')->from('chapters')
214 - ->whereRaw('chapters.id=pages.chapter_id')->where('restricted', '=', false);
215 - })
216 - ->whereExists(function ($query) {
217 - $query->select('*')->from('books')
218 - ->whereRaw('books.id=pages.book_id')
219 - ->whereExists(function ($query) {
220 - $this->checkRestrictionsQuery($query, 'books', 'Book');
221 - });
222 - });
223 - })
224 - // Page unrestricted, Has a chapter with accepted permissions
225 - ->orWhere(function ($query) {
226 - $query->where('restricted', '=', false)
227 - ->whereExists(function ($query) {
228 - $query->select('*')->from('chapters')
229 - ->whereRaw('chapters.id=pages.chapter_id')
230 - ->where('restricted', '=', true)
231 - ->whereExists(function ($query) {
232 - $this->checkRestrictionsQuery($query, 'chapters', 'Chapter');
233 - });
234 - });
235 - })
236 - // Page has accepted permissions
237 - ->orWhereExists(function ($query) {
238 - $this->checkRestrictionsQuery($query, 'pages', 'Page');
239 - });
240 }); 234 });
241 } 235 }
242 236
...@@ -250,43 +244,7 @@ class RestrictionService ...@@ -250,43 +244,7 @@ class RestrictionService
250 { 244 {
251 if ($this->isAdmin) return $query; 245 if ($this->isAdmin) return $query;
252 $this->currentAction = $action; 246 $this->currentAction = $action;
253 - return $this->chapterRestrictionQuery($query); 247 + return $this->entityRestrictionQuery($query);
254 - }
255 -
256 - /**
257 - * The base query for restricting chapters.
258 - * @param $query
259 - * @return mixed
260 - */
261 - private function chapterRestrictionQuery($query)
262 - {
263 - return $query->where(function ($parentWhereQuery) {
264 -
265 - $parentWhereQuery
266 - // Book & chapter unrestricted
267 - ->where(function ($query) {
268 - $query->where('restricted', '=', false)->whereExists(function ($query) {
269 - $query->select('*')->from('books')
270 - ->whereRaw('books.id=chapters.book_id')
271 - ->where('restricted', '=', false);
272 - });
273 - })
274 - // Chapter unrestricted & book has accepted restrictions
275 - ->orWhere(function ($query) {
276 - $query->where('restricted', '=', false)
277 - ->whereExists(function ($query) {
278 - $query->select('*')->from('books')
279 - ->whereRaw('books.id=chapters.book_id')
280 - ->whereExists(function ($query) {
281 - $this->checkRestrictionsQuery($query, 'books', 'Book');
282 - });
283 - });
284 - })
285 - // Chapter has accepted permissions
286 - ->orWhereExists(function ($query) {
287 - $this->checkRestrictionsQuery($query, 'chapters', 'Chapter');
288 - });
289 - });
290 } 248 }
291 249
292 /** 250 /**
...@@ -299,25 +257,7 @@ class RestrictionService ...@@ -299,25 +257,7 @@ class RestrictionService
299 { 257 {
300 if ($this->isAdmin) return $query; 258 if ($this->isAdmin) return $query;
301 $this->currentAction = $action; 259 $this->currentAction = $action;
302 - return $this->bookRestrictionQuery($query); 260 + return $this->entityRestrictionQuery($query);
303 - }
304 -
305 - /**
306 - * The base query for restricting books.
307 - * @param $query
308 - * @return mixed
309 - */
310 - private function bookRestrictionQuery($query)
311 - {
312 - return $query->where(function ($parentWhereQuery) {
313 - $parentWhereQuery
314 - ->where('restricted', '=', false)
315 - ->orWhere(function ($query) {
316 - $query->where('restricted', '=', true)->whereExists(function ($query) {
317 - $this->checkRestrictionsQuery($query, 'books', 'Book');
318 - });
319 - });
320 - });
321 } 261 }
322 262
323 /** 263 /**
...@@ -333,31 +273,23 @@ class RestrictionService ...@@ -333,31 +273,23 @@ class RestrictionService
333 if ($this->isAdmin) return $query; 273 if ($this->isAdmin) return $query;
334 $this->currentAction = 'view'; 274 $this->currentAction = 'view';
335 $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn]; 275 $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
276 +
336 return $query->where(function ($query) use ($tableDetails) { 277 return $query->where(function ($query) use ($tableDetails) {
337 - $query->where(function ($query) use (&$tableDetails) { 278 + $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
338 - $query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Page') 279 + $permissionQuery->select('id')->from('entity_permissions')
339 - ->whereExists(function ($query) use (&$tableDetails) { 280 + ->whereRaw('entity_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
340 - $query->select('*')->from('pages')->whereRaw('pages.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) 281 + ->whereRaw('entity_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
341 - ->where(function ($query) { 282 + ->where('action', '=', $this->currentAction)
342 - $this->pageRestrictionQuery($query); 283 + ->whereIn('role_id', $this->userRoles)
343 - }); 284 + ->where(function ($query) {
344 - }); 285 + $query->where('has_permission', '=', true)->orWhere(function ($query) {
345 - })->orWhere(function ($query) use (&$tableDetails) { 286 + $query->where('has_permission_own', '=', true)
346 - $query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Book')->whereExists(function ($query) use (&$tableDetails) { 287 + ->where('created_by', '=', $this->currentUser->id);
347 - $query->select('*')->from('books')->whereRaw('books.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
348 - ->where(function ($query) {
349 - $this->bookRestrictionQuery($query);
350 - });
351 - });
352 - })->orWhere(function ($query) use (&$tableDetails) {
353 - $query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Chapter')->whereExists(function ($query) use (&$tableDetails) {
354 - $query->select('*')->from('chapters')->whereRaw('chapters.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
355 - ->where(function ($query) {
356 - $this->chapterRestrictionQuery($query);
357 }); 288 });
358 - }); 289 + });
359 }); 290 });
360 }); 291 });
292 +
361 } 293 }
362 294
363 /** 295 /**
...@@ -372,32 +304,24 @@ class RestrictionService ...@@ -372,32 +304,24 @@ class RestrictionService
372 if ($this->isAdmin) return $query; 304 if ($this->isAdmin) return $query;
373 $this->currentAction = 'view'; 305 $this->currentAction = 'view';
374 $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn]; 306 $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
375 - return $query->where(function ($query) use (&$tableDetails) { 307 +
308 + return $query->where(function ($query) use ($tableDetails) {
376 $query->where(function ($query) use (&$tableDetails) { 309 $query->where(function ($query) use (&$tableDetails) {
377 - $query->whereExists(function ($query) use (&$tableDetails) { 310 + $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
378 - $query->select('*')->from('pages')->whereRaw('pages.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) 311 + $permissionQuery->select('id')->from('entity_permissions')
312 + ->whereRaw('entity_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
313 + ->where('entity_type', '=', 'Bookstack\\Page')
314 + ->where('action', '=', $this->currentAction)
315 + ->whereIn('role_id', $this->userRoles)
379 ->where(function ($query) { 316 ->where(function ($query) {
380 - $this->pageRestrictionQuery($query); 317 + $query->where('has_permission', '=', true)->orWhere(function ($query) {
318 + $query->where('has_permission_own', '=', true)
319 + ->where('created_by', '=', $this->currentUser->id);
320 + });
381 }); 321 });
382 - })->orWhere($tableDetails['entityIdColumn'], '=', 0); 322 + });
383 - }); 323 + })->orWhere($tableDetails['entityIdColumn'], '=', 0);
384 }); 324 });
385 } 325 }
386 326
387 - /**
388 - * The query to check the restrictions on an entity.
389 - * @param $query
390 - * @param $tableName
391 - * @param $modelName
392 - */
393 - private function checkRestrictionsQuery($query, $tableName, $modelName)
394 - {
395 - $query->select('*')->from('restrictions')
396 - ->whereRaw('restrictions.restrictable_id=' . $tableName . '.id')
397 - ->where('restrictions.restrictable_type', '=', 'BookStack\\' . $modelName)
398 - ->where('restrictions.action', '=', $this->currentAction)
399 - ->whereIn('restrictions.role_id', $this->userRoles);
400 - }
401 -
402 -
403 } 327 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -19,7 +19,16 @@ class CreateEntityPermissionsTable extends Migration ...@@ -19,7 +19,16 @@ class CreateEntityPermissionsTable extends Migration
19 $table->integer('entity_id'); 19 $table->integer('entity_id');
20 $table->string('action'); 20 $table->string('action');
21 $table->boolean('has_permission')->default(false); 21 $table->boolean('has_permission')->default(false);
22 + $table->boolean('has_permission_own')->default(false);
23 + $table->integer('created_by');
24 + $table->index(['entity_id', 'entity_type']);
25 + $table->index('role_id');
26 + $table->index('action');
27 + $table->index('created_by');
22 }); 28 });
29 +
30 + $restrictionService = app(\BookStack\Services\RestrictionService::class);
31 + $restrictionService->buildEntityPermissions();
23 } 32 }
24 33
25 /** 34 /**
......
...@@ -33,6 +33,7 @@ ...@@ -33,6 +33,7 @@
33 <tr> 33 <tr>
34 <th></th> 34 <th></th>
35 <th>Create</th> 35 <th>Create</th>
36 + <th>View</th>
36 <th>Edit</th> 37 <th>Edit</th>
37 <th>Delete</th> 38 <th>Delete</th>
38 </tr> 39 </tr>
......