From 02af69ddf29e0a2d03cc3a8660bfca9a65175afc Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 11 Dec 2019 21:21:19 +0000 Subject: [PATCH] Added command to copy shelf permissions Has options to run for all or to specify a slug for a specific shelf. Closes #1091 --- app/Console/Commands/CopyShelfPermissions.php | 88 +++++++++++++++++++ app/Entities/Repos/BookshelfRepo.php | 6 +- tests/CommandsTest.php | 48 ++++++++++ 3 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 app/Console/Commands/CopyShelfPermissions.php diff --git a/app/Console/Commands/CopyShelfPermissions.php b/app/Console/Commands/CopyShelfPermissions.php new file mode 100644 index 000000000..d9a1c1d72 --- /dev/null +++ b/app/Console/Commands/CopyShelfPermissions.php @@ -0,0 +1,88 @@ +bookshelfRepo = $repo; + parent::__construct(); + } + + /** + * Execute the console command. + * + * @return mixed + */ + public function handle() + { + $shelfSlug = $this->option('slug'); + $cascadeAll = $this->option('all'); + $shelves = null; + + if (!$cascadeAll && !$shelfSlug) { + $this->error('Either a --slug or --all option must be provided.'); + return; + } + + if ($cascadeAll) { + $continue = $this->confirm( + 'Permission settings for all shelves will be cascaded. '. + 'Books assigned to multiple shelves will receive only the permissions of it\'s last processed shelf. '. + 'Are you sure you want to proceed?' + ); + + if (!$continue && !$this->hasOption('no-interaction')) { + return; + } + + $shelves = Bookshelf::query()->get(['id', 'restricted']); + } + + if ($shelfSlug) { + $shelves = Bookshelf::query()->where('slug', '=', $shelfSlug)->get(['id', 'restricted']); + if ($shelves->count() === 0) { + $this->info('No shelves found with the given slug.'); + } + } + + foreach ($shelves as $shelf) { + $this->bookshelfRepo->copyDownPermissions($shelf, false); + $this->info('Copied permissions for shelf [' . $shelf->id . ']'); + } + + $this->info('Permissions copied for ' . $shelves->count() . ' shelves.'); + } +} diff --git a/app/Entities/Repos/BookshelfRepo.php b/app/Entities/Repos/BookshelfRepo.php index ab4a51805..03b54f009 100644 --- a/app/Entities/Repos/BookshelfRepo.php +++ b/app/Entities/Repos/BookshelfRepo.php @@ -139,15 +139,15 @@ class BookshelfRepo /** * Copy down the permissions of the given shelf to all child books. */ - public function copyDownPermissions(Bookshelf $shelf): int + public function copyDownPermissions(Bookshelf $shelf, $checkUserPermissions = true): int { $shelfPermissions = $shelf->permissions()->get(['role_id', 'action'])->toArray(); - $shelfBooks = $shelf->books()->get(); + $shelfBooks = $shelf->books()->get(['id', 'restricted']); $updatedBookCount = 0; /** @var Book $book */ foreach ($shelfBooks as $book) { - if (!userCan('restrictions-manage', $book)) { + if ($checkUserPermissions && !userCan('restrictions-manage', $book)) { continue; } $book->permissions()->delete(); diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index 4aef0ed26..099af2939 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -1,6 +1,7 @@ assertTrue(User::where('email', '=', 'admintest@example.com')->first()->hasSystemRole('admin'), 'User has admin role as expected'); $this->assertTrue(\Auth::attempt(['email' => 'admintest@example.com', 'password' => 'testing-4']), 'Password stored as expected'); } + + public function test_copy_shelf_permissions_command_shows_error_when_no_required_option_given() + { + $this->artisan('bookstack:copy-shelf-permissions') + ->expectsOutput('Either a --slug or --all option must be provided.') + ->assertExitCode(0); + } + + public function test_copy_shelf_permissions_command_using_slug() + { + $shelf = Bookshelf::first(); + $child = $shelf->books()->first(); + $editorRole = $this->getEditor()->roles()->first(); + $this->assertFalse(boolval($child->restricted), "Child book should not be restricted by default"); + $this->assertTrue($child->permissions()->count() === 0, "Child book should have no permissions by default"); + + $this->setEntityRestrictions($shelf, ['view', 'update'], [$editorRole]); + $this->artisan('bookstack:copy-shelf-permissions', [ + '--slug' => $shelf->slug, + ]); + $child = $shelf->books()->first(); + + $this->assertTrue(boolval($child->restricted), "Child book should now be restricted"); + $this->assertTrue($child->permissions()->count() === 2, "Child book should have copied permissions"); + $this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'view', 'role_id' => $editorRole->id]); + $this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'update', 'role_id' => $editorRole->id]); + } + + public function test_copy_shelf_permissions_command_using_all() + { + $shelf = Bookshelf::query()->first(); + Bookshelf::query()->where('id', '!=', $shelf->id)->delete(); + $child = $shelf->books()->first(); + $editorRole = $this->getEditor()->roles()->first(); + $this->assertFalse(boolval($child->restricted), "Child book should not be restricted by default"); + $this->assertTrue($child->permissions()->count() === 0, "Child book should have no permissions by default"); + + $this->setEntityRestrictions($shelf, ['view', 'update'], [$editorRole]); + $this->artisan('bookstack:copy-shelf-permissions --all') + ->expectsQuestion('Permission settings for all shelves will be cascaded. Books assigned to multiple shelves will receive only the permissions of it\'s last processed shelf. Are you sure you want to proceed?', 'y'); + $child = $shelf->books()->first(); + + $this->assertTrue(boolval($child->restricted), "Child book should now be restricted"); + $this->assertTrue($child->permissions()->count() === 2, "Child book should have copied permissions"); + $this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'view', 'role_id' => $editorRole->id]); + $this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'update', 'role_id' => $editorRole->id]); + } }