From ada7c83e96f35a6b484869d6d27939555e1942f7 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 23 Apr 2016 18:14:26 +0100 Subject: [PATCH] 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. --- .../Commands/RegeneratePermissions.php | 51 ++++ app/Console/Kernel.php | 1 + app/Entity.php | 20 +- app/Services/RestrictionService.php | 286 +++++++----------- ...192649_create_entity_permissions_table.php | 9 + resources/views/settings/roles/form.blade.php | 1 + 6 files changed, 186 insertions(+), 182 deletions(-) create mode 100644 app/Console/Commands/RegeneratePermissions.php diff --git a/app/Console/Commands/RegeneratePermissions.php b/app/Console/Commands/RegeneratePermissions.php new file mode 100644 index 000000000..bd221c138 --- /dev/null +++ b/app/Console/Commands/RegeneratePermissions.php @@ -0,0 +1,51 @@ +restrictionService = $restrictionService; + parent::__construct(); + } + + /** + * Execute the console command. + * + * @return mixed + */ + public function handle() + { + $this->restrictionService->buildEntityPermissions(); + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index e3a71bd14..b725c9e21 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -15,6 +15,7 @@ class Kernel extends ConsoleKernel protected $commands = [ \BookStack\Console\Commands\Inspire::class, \BookStack\Console\Commands\ResetViews::class, + \BookStack\Console\Commands\RegeneratePermissions::class, ]; /** diff --git a/app/Entity.php b/app/Entity.php index 4f97c6bab..35badb461 100644 --- a/app/Entity.php +++ b/app/Entity.php @@ -73,6 +73,15 @@ abstract class Entity extends Ownable return $this->restrictions->where('role_id', $role_id)->where('action', $action)->count() > 0; } + /** + * Get the entity permissions this is connected to. + * @return \Illuminate\Database\Eloquent\Relations\MorphMany + */ + public function permissions() + { + return $this->morphMany(EntityPermission::class, 'entity'); + } + /** * Allows checking of the exact class, Used to check entity type. * Cleaner method for is_a. @@ -81,7 +90,16 @@ abstract class Entity extends Ownable */ public static function isA($type) { - return static::getClassName() === strtolower($type); + return static::getType() === strtolower($type); + } + + /** + * Get entity type. + * @return mixed + */ + public static function getType() + { + return strtolower(static::getClassName()); } /** diff --git a/app/Services/RestrictionService.php b/app/Services/RestrictionService.php index 8d57b9edc..847db29fe 100644 --- a/app/Services/RestrictionService.php +++ b/app/Services/RestrictionService.php @@ -5,7 +5,6 @@ use BookStack\Chapter; use BookStack\Entity; use BookStack\EntityPermission; use BookStack\Page; -use BookStack\Permission; use BookStack\Role; use Illuminate\Database\Eloquent\Collection; @@ -23,18 +22,19 @@ class RestrictionService protected $entityPermission; protected $role; - protected $permission; + + protected $actions = ['view', 'create', 'update', 'delete']; /** * RestrictionService constructor. + * TODO - Handle events when roles or entities change. * @param EntityPermission $entityPermission * @param Book $book * @param Chapter $chapter * @param Page $page * @param Role $role - * @param Permission $permission */ - public function __construct(EntityPermission $entityPermission, Book $book, Chapter $chapter, Page $page, Role $role, Permission $permission) + public function __construct(EntityPermission $entityPermission, Book $book, Chapter $chapter, Page $page, Role $role) { $this->currentUser = auth()->user(); $this->userRoles = $this->currentUser ? $this->currentUser->roles->pluck('id') : []; @@ -42,13 +42,11 @@ class RestrictionService $this->entityPermission = $entityPermission; $this->role = $role; - $this->permission = $permission; $this->book = $book; $this->chapter = $chapter; $this->page = $page; } - /** * Re-generate all entity permission from scratch. */ @@ -65,12 +63,12 @@ class RestrictionService }); // Chunk through all chapters - $this->chapter->chunk(500, function ($books) use ($roles) { + $this->chapter->with('book')->chunk(500, function ($books) use ($roles) { $this->createManyEntityPermissions($books, $roles); }); // Chunk through all pages - $this->page->chunk(500, function ($books) use ($roles) { + $this->page->with('book', 'chapter')->chunk(500, function ($books) use ($roles) { $this->createManyEntityPermissions($books, $roles); }); } @@ -85,16 +83,69 @@ class RestrictionService $entityPermissions = []; foreach ($entities as $entity) { foreach ($roles as $role) { - $entityPermissions[] = $this->createEntityPermission($entity, $role); + foreach ($this->actions as $action) { + $entityPermissions[] = $this->createEntityPermissionData($entity, $role, $action); + } } } $this->entityPermission->insert($entityPermissions); } - protected function createEntityPermissionData(Entity $entity, Role $role) + protected function createEntityPermissionData(Entity $entity, Role $role, $action) { - // TODO - Check the permission values and return an EntityPermission + $permissionPrefix = $entity->getType() . '-' . $action; + $roleHasPermission = $role->hasPermission($permissionPrefix . '-all'); + $roleHasPermissionOwn = $role->hasPermission($permissionPrefix . '-own'); + + if ($entity->isA('book')) { + + if (!$entity->restricted) { + return $this->createEntityPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn); + } else { + $hasAccess = $entity->hasRestriction($role->id, $action); + return $this->createEntityPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess); + } + + } elseif ($entity->isA('chapter')) { + + if (!$entity->restricted) { + $hasAccessToBook = $entity->book->hasRestriction($role->id, $action); + return $this->createEntityPermissionDataArray($entity, $role, $action, + ($roleHasPermission && $hasAccessToBook), ($roleHasPermissionOwn && $hasAccessToBook)); + } else { + $hasAccess = $entity->hasRestriction($role->id, $action); + return $this->createEntityPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess); + } + + } elseif ($entity->isA('page')) { + + if (!$entity->restricted) { + $hasAccessToBook = $entity->book->hasRestriction($role->id, $action); + $hasAccessToChapter = $entity->chapter ? ($entity->chapter->hasRestriction($role->id, $action)) : true; + return $this->createEntityPermissionDataArray($entity, $role, $action, + ($roleHasPermission && $hasAccessToBook && $hasAccessToChapter), + ($roleHasPermissionOwn && $hasAccessToBook && $hasAccessToChapter)); + } else { + $hasAccess = $entity->hasRestriction($role->id, $action); + return $this->createEntityPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess); + } + + } + } + + protected function createEntityPermissionDataArray(Entity $entity, Role $role, $action, $permissionAll, $permissionOwn) + { + $entityClass = get_class($entity); + return [ + 'role_id' => $role->id, + 'entity_id' => $entity->id, + 'entity_type' => $entityClass, + 'action' => $action, + 'has_permission' => $permissionAll, + 'has_permission_own' => $permissionOwn, + 'created_by' => $entity->created_by + ]; } /** @@ -157,86 +208,29 @@ class RestrictionService if ($this->isAdmin) return $query; $this->currentAction = $action; - return $this->pageRestrictionQuery($query); + return $this->entityRestrictionQuery($query); } /** - * The base query for restricting pages. + * The general query filter to remove all entities + * that the current user does not have access to. * @param $query * @return mixed */ - private function pageRestrictionQuery($query) + protected function entityRestrictionQuery($query) { - return $query->where(function ($parentWhereQuery) { - - $parentWhereQuery - // (Book & chapter & page) or (Book & page & NO CHAPTER) unrestricted - ->where(function ($query) { - $query->where(function ($query) { - $query->whereExists(function ($query) { - $query->select('*')->from('chapters') - ->whereRaw('chapters.id=pages.chapter_id') - ->where('restricted', '=', false); - })->whereExists(function ($query) { - $query->select('*')->from('books') - ->whereRaw('books.id=pages.book_id') - ->where('restricted', '=', false); - })->where('restricted', '=', false); - })->orWhere(function ($query) { - $query->where('restricted', '=', false)->where('chapter_id', '=', 0) - ->whereExists(function ($query) { - $query->select('*')->from('books') - ->whereRaw('books.id=pages.book_id') - ->where('restricted', '=', false); + return $query->where(function ($parentQuery) { + $parentQuery->whereHas('permissions', function ($permissionQuery) { + $permissionQuery->whereIn('role_id', $this->userRoles) + ->where('action', '=', $this->currentAction) + ->where(function ($query) { + $query->where('has_permission', '=', true) + ->orWhere(function ($query) { + $query->where('has_permission_own', '=', true) + ->where('created_by', '=', $this->currentUser->id); }); }); - }) - // Page unrestricted, Has no chapter & book has accepted restrictions - ->orWhere(function ($query) { - $query->where('restricted', '=', false) - ->whereExists(function ($query) { - $query->select('*')->from('chapters') - ->whereRaw('chapters.id=pages.chapter_id'); - }, 'and', true) - ->whereExists(function ($query) { - $query->select('*')->from('books') - ->whereRaw('books.id=pages.book_id') - ->whereExists(function ($query) { - $this->checkRestrictionsQuery($query, 'books', 'Book'); - }); - }); - }) - // Page unrestricted, Has an unrestricted chapter & book has accepted restrictions - ->orWhere(function ($query) { - $query->where('restricted', '=', false) - ->whereExists(function ($query) { - $query->select('*')->from('chapters') - ->whereRaw('chapters.id=pages.chapter_id')->where('restricted', '=', false); - }) - ->whereExists(function ($query) { - $query->select('*')->from('books') - ->whereRaw('books.id=pages.book_id') - ->whereExists(function ($query) { - $this->checkRestrictionsQuery($query, 'books', 'Book'); - }); - }); - }) - // Page unrestricted, Has a chapter with accepted permissions - ->orWhere(function ($query) { - $query->where('restricted', '=', false) - ->whereExists(function ($query) { - $query->select('*')->from('chapters') - ->whereRaw('chapters.id=pages.chapter_id') - ->where('restricted', '=', true) - ->whereExists(function ($query) { - $this->checkRestrictionsQuery($query, 'chapters', 'Chapter'); - }); - }); - }) - // Page has accepted permissions - ->orWhereExists(function ($query) { - $this->checkRestrictionsQuery($query, 'pages', 'Page'); - }); + }); }); } @@ -250,43 +244,7 @@ class RestrictionService { if ($this->isAdmin) return $query; $this->currentAction = $action; - return $this->chapterRestrictionQuery($query); - } - - /** - * The base query for restricting chapters. - * @param $query - * @return mixed - */ - private function chapterRestrictionQuery($query) - { - return $query->where(function ($parentWhereQuery) { - - $parentWhereQuery - // Book & chapter unrestricted - ->where(function ($query) { - $query->where('restricted', '=', false)->whereExists(function ($query) { - $query->select('*')->from('books') - ->whereRaw('books.id=chapters.book_id') - ->where('restricted', '=', false); - }); - }) - // Chapter unrestricted & book has accepted restrictions - ->orWhere(function ($query) { - $query->where('restricted', '=', false) - ->whereExists(function ($query) { - $query->select('*')->from('books') - ->whereRaw('books.id=chapters.book_id') - ->whereExists(function ($query) { - $this->checkRestrictionsQuery($query, 'books', 'Book'); - }); - }); - }) - // Chapter has accepted permissions - ->orWhereExists(function ($query) { - $this->checkRestrictionsQuery($query, 'chapters', 'Chapter'); - }); - }); + return $this->entityRestrictionQuery($query); } /** @@ -299,25 +257,7 @@ class RestrictionService { if ($this->isAdmin) return $query; $this->currentAction = $action; - return $this->bookRestrictionQuery($query); - } - - /** - * The base query for restricting books. - * @param $query - * @return mixed - */ - private function bookRestrictionQuery($query) - { - return $query->where(function ($parentWhereQuery) { - $parentWhereQuery - ->where('restricted', '=', false) - ->orWhere(function ($query) { - $query->where('restricted', '=', true)->whereExists(function ($query) { - $this->checkRestrictionsQuery($query, 'books', 'Book'); - }); - }); - }); + return $this->entityRestrictionQuery($query); } /** @@ -333,31 +273,23 @@ class RestrictionService if ($this->isAdmin) return $query; $this->currentAction = 'view'; $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn]; + return $query->where(function ($query) use ($tableDetails) { - $query->where(function ($query) use (&$tableDetails) { - $query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Page') - ->whereExists(function ($query) use (&$tableDetails) { - $query->select('*')->from('pages')->whereRaw('pages.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) - ->where(function ($query) { - $this->pageRestrictionQuery($query); - }); + $query->whereExists(function ($permissionQuery) use (&$tableDetails) { + $permissionQuery->select('id')->from('entity_permissions') + ->whereRaw('entity_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) + ->whereRaw('entity_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn']) + ->where('action', '=', $this->currentAction) + ->whereIn('role_id', $this->userRoles) + ->where(function ($query) { + $query->where('has_permission', '=', true)->orWhere(function ($query) { + $query->where('has_permission_own', '=', true) + ->where('created_by', '=', $this->currentUser->id); + }); }); - })->orWhere(function ($query) use (&$tableDetails) { - $query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Book')->whereExists(function ($query) use (&$tableDetails) { - $query->select('*')->from('books')->whereRaw('books.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) - ->where(function ($query) { - $this->bookRestrictionQuery($query); - }); - }); - })->orWhere(function ($query) use (&$tableDetails) { - $query->where($tableDetails['entityTypeColumn'], '=', 'BookStack\Chapter')->whereExists(function ($query) use (&$tableDetails) { - $query->select('*')->from('chapters')->whereRaw('chapters.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) - ->where(function ($query) { - $this->chapterRestrictionQuery($query); - }); - }); }); }); + } /** @@ -372,32 +304,24 @@ class RestrictionService if ($this->isAdmin) return $query; $this->currentAction = 'view'; $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn]; - return $query->where(function ($query) use (&$tableDetails) { + + return $query->where(function ($query) use ($tableDetails) { $query->where(function ($query) use (&$tableDetails) { - $query->whereExists(function ($query) use (&$tableDetails) { - $query->select('*')->from('pages')->whereRaw('pages.id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) + $query->whereExists(function ($permissionQuery) use (&$tableDetails) { + $permissionQuery->select('id')->from('entity_permissions') + ->whereRaw('entity_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) + ->where('entity_type', '=', 'Bookstack\\Page') + ->where('action', '=', $this->currentAction) + ->whereIn('role_id', $this->userRoles) ->where(function ($query) { - $this->pageRestrictionQuery($query); + $query->where('has_permission', '=', true)->orWhere(function ($query) { + $query->where('has_permission_own', '=', true) + ->where('created_by', '=', $this->currentUser->id); + }); }); - })->orWhere($tableDetails['entityIdColumn'], '=', 0); - }); + }); + })->orWhere($tableDetails['entityIdColumn'], '=', 0); }); } - /** - * The query to check the restrictions on an entity. - * @param $query - * @param $tableName - * @param $modelName - */ - private function checkRestrictionsQuery($query, $tableName, $modelName) - { - $query->select('*')->from('restrictions') - ->whereRaw('restrictions.restrictable_id=' . $tableName . '.id') - ->where('restrictions.restrictable_type', '=', 'BookStack\\' . $modelName) - ->where('restrictions.action', '=', $this->currentAction) - ->whereIn('restrictions.role_id', $this->userRoles); - } - - } \ No newline at end of file diff --git a/database/migrations/2016_04_20_192649_create_entity_permissions_table.php b/database/migrations/2016_04_20_192649_create_entity_permissions_table.php index 359f25df9..6b273390b 100644 --- a/database/migrations/2016_04_20_192649_create_entity_permissions_table.php +++ b/database/migrations/2016_04_20_192649_create_entity_permissions_table.php @@ -19,7 +19,16 @@ class CreateEntityPermissionsTable extends Migration $table->integer('entity_id'); $table->string('action'); $table->boolean('has_permission')->default(false); + $table->boolean('has_permission_own')->default(false); + $table->integer('created_by'); + $table->index(['entity_id', 'entity_type']); + $table->index('role_id'); + $table->index('action'); + $table->index('created_by'); }); + + $restrictionService = app(\BookStack\Services\RestrictionService::class); + $restrictionService->buildEntityPermissions(); } /** diff --git a/resources/views/settings/roles/form.blade.php b/resources/views/settings/roles/form.blade.php index 0980d1b65..159470477 100644 --- a/resources/views/settings/roles/form.blade.php +++ b/resources/views/settings/roles/form.blade.php @@ -33,6 +33,7 @@ Create + View Edit Delete