3 namespace Tests\Permissions;
5 use BookStack\Auth\User;
6 use BookStack\Entities\Models\Book;
7 use BookStack\Entities\Models\Bookshelf;
8 use BookStack\Entities\Models\Chapter;
9 use BookStack\Entities\Models\Entity;
10 use BookStack\Entities\Models\Page;
12 use Illuminate\Support\Str;
15 class EntityPermissionsTest extends TestCase
18 protected User $viewer;
20 protected function setUp(): void
23 $this->user = $this->getEditor();
24 $this->viewer = $this->getViewer();
27 protected function setRestrictionsForTestRoles(Entity $entity, array $actions = [])
30 $this->user->roles->first(),
31 $this->viewer->roles->first(),
33 $this->entities->setPermissions($entity, $actions, $roles);
36 public function test_bookshelf_view_restriction()
38 $shelf = $this->entities->shelf();
40 $this->actingAs($this->user)
41 ->get($shelf->getUrl())
44 $this->setRestrictionsForTestRoles($shelf, []);
46 $this->followingRedirects()->get($shelf->getUrl())
47 ->assertSee('Shelf not found');
49 $this->setRestrictionsForTestRoles($shelf, ['view']);
51 $this->get($shelf->getUrl())
52 ->assertSee($shelf->name);
55 public function test_bookshelf_update_restriction()
57 $shelf = $this->entities->shelf();
59 $this->actingAs($this->user)
60 ->get($shelf->getUrl('/edit'))
61 ->assertSee('Edit Shelf');
63 $this->setRestrictionsForTestRoles($shelf, ['view', 'delete']);
65 $resp = $this->get($shelf->getUrl('/edit'))
66 ->assertRedirect('/');
67 $this->followRedirects($resp)->assertSee('You do not have permission');
69 $this->setRestrictionsForTestRoles($shelf, ['view', 'update']);
71 $this->get($shelf->getUrl('/edit'))
75 public function test_bookshelf_delete_restriction()
77 $shelf = $this->entities->shelf();
79 $this->actingAs($this->user)
80 ->get($shelf->getUrl('/delete'))
81 ->assertSee('Delete Shelf');
83 $this->setRestrictionsForTestRoles($shelf, ['view', 'update']);
85 $this->get($shelf->getUrl('/delete'))->assertRedirect('/');
86 $this->get('/')->assertSee('You do not have permission');
88 $this->setRestrictionsForTestRoles($shelf, ['view', 'delete']);
90 $this->get($shelf->getUrl('/delete'))
92 ->assertSee('Delete Shelf');
95 public function test_book_view_restriction()
97 $book = $this->entities->book();
98 $bookPage = $book->pages->first();
99 $bookChapter = $book->chapters->first();
101 $bookUrl = $book->getUrl();
102 $this->actingAs($this->user)
106 $this->setRestrictionsForTestRoles($book, []);
108 $this->followingRedirects()->get($bookUrl)
109 ->assertSee('Book not found');
110 $this->followingRedirects()->get($bookPage->getUrl())
111 ->assertSee('Page not found');
112 $this->followingRedirects()->get($bookChapter->getUrl())
113 ->assertSee('Chapter not found');
115 $this->setRestrictionsForTestRoles($book, ['view']);
118 ->assertSee($book->name);
119 $this->get($bookPage->getUrl())
120 ->assertSee($bookPage->name);
121 $this->get($bookChapter->getUrl())
122 ->assertSee($bookChapter->name);
125 public function test_book_create_restriction()
127 $book = $this->entities->book();
129 $bookUrl = $book->getUrl();
130 $resp = $this->actingAs($this->viewer)->get($bookUrl);
131 $this->withHtml($resp)->assertElementNotContains('.actions', 'New Page')
132 ->assertElementNotContains('.actions', 'New Chapter');
133 $resp = $this->actingAs($this->user)->get($bookUrl);
134 $this->withHtml($resp)->assertElementContains('.actions', 'New Page')
135 ->assertElementContains('.actions', 'New Chapter');
137 $this->setRestrictionsForTestRoles($book, ['view', 'delete', 'update']);
139 $this->get($bookUrl . '/create-chapter')->assertRedirect('/');
140 $this->get('/')->assertSee('You do not have permission');
142 $this->get($bookUrl . '/create-page')->assertRedirect('/');
143 $this->get('/')->assertSee('You do not have permission');
145 $resp = $this->get($bookUrl);
146 $this->withHtml($resp)->assertElementNotContains('.actions', 'New Page')
147 ->assertElementNotContains('.actions', 'New Chapter');
149 $this->setRestrictionsForTestRoles($book, ['view', 'create']);
151 $resp = $this->post($book->getUrl('/create-chapter'), [
152 'name' => 'test chapter',
153 'description' => 'desc',
155 $resp->assertRedirect($book->getUrl('/chapter/test-chapter'));
157 $this->get($book->getUrl('/create-page'));
158 /** @var Page $page */
159 $page = Page::query()->where('draft', '=', true)->orderBy('id', 'desc')->first();
160 $resp = $this->post($page->getUrl(), [
161 'name' => 'test page',
162 'html' => 'test content',
164 $resp->assertRedirect($book->getUrl('/page/test-page'));
166 $resp = $this->get($bookUrl);
167 $this->withHtml($resp)->assertElementContains('.actions', 'New Page')
168 ->assertElementContains('.actions', 'New Chapter');
171 public function test_book_update_restriction()
173 $book = $this->entities->book();
174 $bookPage = $book->pages->first();
175 $bookChapter = $book->chapters->first();
177 $bookUrl = $book->getUrl();
178 $this->actingAs($this->user)
179 ->get($bookUrl . '/edit')
180 ->assertSee('Edit Book');
182 $this->setRestrictionsForTestRoles($book, ['view', 'delete']);
184 $this->get($bookUrl . '/edit')->assertRedirect('/');
185 $this->get('/')->assertSee('You do not have permission');
186 $this->get($bookPage->getUrl() . '/edit')->assertRedirect('/');
187 $this->get('/')->assertSee('You do not have permission');
188 $this->get($bookChapter->getUrl() . '/edit')->assertRedirect('/');
189 $this->get('/')->assertSee('You do not have permission');
191 $this->setRestrictionsForTestRoles($book, ['view', 'update']);
193 $this->get($bookUrl . '/edit')->assertOk();
194 $this->get($bookPage->getUrl() . '/edit')->assertOk();
195 $this->get($bookChapter->getUrl() . '/edit')->assertSee('Edit Chapter');
198 public function test_book_delete_restriction()
200 $book = $this->entities->book();
201 $bookPage = $book->pages->first();
202 $bookChapter = $book->chapters->first();
204 $bookUrl = $book->getUrl();
205 $this->actingAs($this->user)->get($bookUrl . '/delete')
206 ->assertSee('Delete Book');
208 $this->setRestrictionsForTestRoles($book, ['view', 'update']);
210 $this->get($bookUrl . '/delete')->assertRedirect('/');
211 $this->get('/')->assertSee('You do not have permission');
212 $this->get($bookPage->getUrl() . '/delete')->assertRedirect('/');
213 $this->get('/')->assertSee('You do not have permission');
214 $this->get($bookChapter->getUrl() . '/delete')->assertRedirect('/');
215 $this->get('/')->assertSee('You do not have permission');
217 $this->setRestrictionsForTestRoles($book, ['view', 'delete']);
219 $this->get($bookUrl . '/delete')->assertOk()->assertSee('Delete Book');
220 $this->get($bookPage->getUrl('/delete'))->assertOk()->assertSee('Delete Page');
221 $this->get($bookChapter->getUrl('/delete'))->assertSee('Delete Chapter');
224 public function test_chapter_view_restriction()
226 $chapter = $this->entities->chapter();
227 $chapterPage = $chapter->pages->first();
229 $chapterUrl = $chapter->getUrl();
230 $this->actingAs($this->user)->get($chapterUrl)->assertOk();
232 $this->setRestrictionsForTestRoles($chapter, []);
234 $this->followingRedirects()->get($chapterUrl)->assertSee('Chapter not found');
235 $this->followingRedirects()->get($chapterPage->getUrl())->assertSee('Page not found');
237 $this->setRestrictionsForTestRoles($chapter, ['view']);
239 $this->get($chapterUrl)->assertSee($chapter->name);
240 $this->get($chapterPage->getUrl())->assertSee($chapterPage->name);
243 public function test_chapter_create_restriction()
245 $chapter = $this->entities->chapter();
247 $chapterUrl = $chapter->getUrl();
248 $resp = $this->actingAs($this->user)->get($chapterUrl);
249 $this->withHtml($resp)->assertElementContains('.actions', 'New Page');
251 $this->setRestrictionsForTestRoles($chapter, ['view', 'delete', 'update']);
253 $this->get($chapterUrl . '/create-page')->assertRedirect('/');
254 $this->get('/')->assertSee('You do not have permission');
255 $this->withHtml($this->get($chapterUrl))->assertElementNotContains('.actions', 'New Page');
257 $this->setRestrictionsForTestRoles($chapter, ['view', 'create']);
259 $this->get($chapter->getUrl('/create-page'));
260 /** @var Page $page */
261 $page = Page::query()->where('draft', '=', true)->orderBy('id', 'desc')->first();
262 $resp = $this->post($page->getUrl(), [
263 'name' => 'test page',
264 'html' => 'test content',
266 $resp->assertRedirect($chapter->book->getUrl('/page/test-page'));
268 $this->withHtml($this->get($chapterUrl))->assertElementContains('.actions', 'New Page');
271 public function test_chapter_update_restriction()
273 $chapter = $this->entities->chapter();
274 $chapterPage = $chapter->pages->first();
276 $chapterUrl = $chapter->getUrl();
277 $this->actingAs($this->user)->get($chapterUrl . '/edit')
278 ->assertSee('Edit Chapter');
280 $this->setRestrictionsForTestRoles($chapter, ['view', 'delete']);
282 $this->get($chapterUrl . '/edit')->assertRedirect('/');
283 $this->get('/')->assertSee('You do not have permission');
284 $this->get($chapterPage->getUrl() . '/edit')->assertRedirect('/');
285 $this->get('/')->assertSee('You do not have permission');
287 $this->setRestrictionsForTestRoles($chapter, ['view', 'update']);
289 $this->get($chapterUrl . '/edit')->assertOk()->assertSee('Edit Chapter');
290 $this->get($chapterPage->getUrl() . '/edit')->assertOk();
293 public function test_chapter_delete_restriction()
295 $chapter = $this->entities->chapter();
296 $chapterPage = $chapter->pages->first();
298 $chapterUrl = $chapter->getUrl();
299 $this->actingAs($this->user)
300 ->get($chapterUrl . '/delete')
301 ->assertSee('Delete Chapter');
303 $this->setRestrictionsForTestRoles($chapter, ['view', 'update']);
305 $this->get($chapterUrl . '/delete')->assertRedirect('/');
306 $this->get('/')->assertSee('You do not have permission');
307 $this->get($chapterPage->getUrl() . '/delete')->assertRedirect('/');
308 $this->get('/')->assertSee('You do not have permission');
310 $this->setRestrictionsForTestRoles($chapter, ['view', 'delete']);
312 $this->get($chapterUrl . '/delete')->assertOk()->assertSee('Delete Chapter');
313 $this->get($chapterPage->getUrl() . '/delete')->assertOk()->assertSee('Delete Page');
316 public function test_page_view_restriction()
318 $page = $this->entities->page();
320 $pageUrl = $page->getUrl();
321 $this->actingAs($this->user)->get($pageUrl)->assertOk();
323 $this->setRestrictionsForTestRoles($page, ['update', 'delete']);
325 $this->get($pageUrl)->assertSee('Page not found');
327 $this->setRestrictionsForTestRoles($page, ['view']);
329 $this->get($pageUrl)->assertSee($page->name);
332 public function test_page_update_restriction()
334 $page = $this->entities->page();
336 $pageUrl = $page->getUrl();
337 $resp = $this->actingAs($this->user)
338 ->get($pageUrl . '/edit');
339 $this->withHtml($resp)->assertElementExists('input[name="name"][value="' . $page->name . '"]');
341 $this->setRestrictionsForTestRoles($page, ['view', 'delete']);
343 $this->get($pageUrl . '/edit')->assertRedirect('/');
344 $this->get('/')->assertSee('You do not have permission');
346 $this->setRestrictionsForTestRoles($page, ['view', 'update']);
348 $resp = $this->get($pageUrl . '/edit')
350 $this->withHtml($resp)->assertElementExists('input[name="name"][value="' . $page->name . '"]');
353 public function test_page_delete_restriction()
355 $page = $this->entities->page();
357 $pageUrl = $page->getUrl();
358 $this->actingAs($this->user)
359 ->get($pageUrl . '/delete')
360 ->assertSee('Delete Page');
362 $this->setRestrictionsForTestRoles($page, ['view', 'update']);
364 $this->get($pageUrl . '/delete')->assertRedirect('/');
365 $this->get('/')->assertSee('You do not have permission');
367 $this->setRestrictionsForTestRoles($page, ['view', 'delete']);
369 $this->get($pageUrl . '/delete')->assertOk()->assertSee('Delete Page');
372 protected function entityRestrictionFormTest(string $model, string $title, string $permission, string $roleId)
374 /** @var Entity $modelInstance */
375 $modelInstance = $model::query()->first();
376 $this->asAdmin()->get($modelInstance->getUrl('/permissions'))
379 $this->put($modelInstance->getUrl('/permissions'), [
383 $permission => 'true',
389 $this->assertDatabaseHas('entity_permissions', [
390 'entity_id' => $modelInstance->id,
391 'entity_type' => $modelInstance->getMorphClass(),
392 'role_id' => $roleId,
397 public function test_bookshelf_restriction_form()
399 $this->entityRestrictionFormTest(Bookshelf::class, 'Shelf Permissions', 'view', '2');
402 public function test_book_restriction_form()
404 $this->entityRestrictionFormTest(Book::class, 'Book Permissions', 'view', '2');
407 public function test_chapter_restriction_form()
409 $this->entityRestrictionFormTest(Chapter::class, 'Chapter Permissions', 'update', '2');
412 public function test_page_restriction_form()
414 $this->entityRestrictionFormTest(Page::class, 'Page Permissions', 'delete', '2');
417 public function test_restricted_pages_not_visible_in_book_navigation_on_pages()
419 $chapter = $this->entities->chapter();
420 $page = $chapter->pages->first();
421 $page2 = $chapter->pages[2];
423 $this->setRestrictionsForTestRoles($page, []);
425 $resp = $this->actingAs($this->user)->get($page2->getUrl());
426 $this->withHtml($resp)->assertElementNotContains('.sidebar-page-list', $page->name);
429 public function test_restricted_pages_not_visible_in_book_navigation_on_chapters()
431 $chapter = $this->entities->chapter();
432 $page = $chapter->pages->first();
434 $this->setRestrictionsForTestRoles($page, []);
436 $resp = $this->actingAs($this->user)->get($chapter->getUrl());
437 $this->withHtml($resp)->assertElementNotContains('.sidebar-page-list', $page->name);
440 public function test_restricted_pages_not_visible_on_chapter_pages()
442 $chapter = $this->entities->chapter();
443 $page = $chapter->pages->first();
445 $this->setRestrictionsForTestRoles($page, []);
447 $this->actingAs($this->user)
448 ->get($chapter->getUrl())
449 ->assertDontSee($page->name);
452 public function test_restricted_chapter_pages_not_visible_on_book_page()
454 $chapter = $this->entities->chapter();
455 $this->actingAs($this->user)
456 ->get($chapter->book->getUrl())
457 ->assertSee($chapter->pages->first()->name);
459 foreach ($chapter->pages as $page) {
460 $this->setRestrictionsForTestRoles($page, []);
463 $this->actingAs($this->user)
464 ->get($chapter->book->getUrl())
465 ->assertDontSee($chapter->pages->first()->name);
468 public function test_bookshelf_update_restriction_override()
470 $shelf = $this->entities->shelf();
472 $this->actingAs($this->viewer)
473 ->get($shelf->getUrl('/edit'))
474 ->assertDontSee('Edit Book');
476 $this->setRestrictionsForTestRoles($shelf, ['view', 'delete']);
478 $this->get($shelf->getUrl('/edit'))->assertRedirect('/');
479 $this->get('/')->assertSee('You do not have permission');
481 $this->setRestrictionsForTestRoles($shelf, ['view', 'update']);
483 $this->get($shelf->getUrl('/edit'))->assertOk();
486 public function test_bookshelf_delete_restriction_override()
488 $shelf = $this->entities->shelf();
490 $this->actingAs($this->viewer)
491 ->get($shelf->getUrl('/delete'))
492 ->assertDontSee('Delete Book');
494 $this->setRestrictionsForTestRoles($shelf, ['view', 'update']);
496 $this->get($shelf->getUrl('/delete'))->assertRedirect('/');
497 $this->get('/')->assertSee('You do not have permission');
499 $this->setRestrictionsForTestRoles($shelf, ['view', 'delete']);
501 $this->get($shelf->getUrl('/delete'))->assertOk()->assertSee('Delete Shelf');
504 public function test_book_create_restriction_override()
506 $book = $this->entities->book();
508 $bookUrl = $book->getUrl();
509 $resp = $this->actingAs($this->viewer)->get($bookUrl);
510 $this->withHtml($resp)->assertElementNotContains('.actions', 'New Page')
511 ->assertElementNotContains('.actions', 'New Chapter');
513 $this->setRestrictionsForTestRoles($book, ['view', 'delete', 'update']);
515 $this->get($bookUrl . '/create-chapter')->assertRedirect('/');
516 $this->get('/')->assertSee('You do not have permission');
517 $this->get($bookUrl . '/create-page')->assertRedirect('/');
518 $this->get('/')->assertSee('You do not have permission');
519 $resp = $this->get($bookUrl);
520 $this->withHtml($resp)->assertElementNotContains('.actions', 'New Page')
521 ->assertElementNotContains('.actions', 'New Chapter');
523 $this->setRestrictionsForTestRoles($book, ['view', 'create']);
525 $resp = $this->post($book->getUrl('/create-chapter'), [
526 'name' => 'test chapter',
527 'description' => 'test desc',
529 $resp->assertRedirect($book->getUrl('/chapter/test-chapter'));
531 $this->get($book->getUrl('/create-page'));
532 /** @var Page $page */
533 $page = Page::query()->where('draft', '=', true)->orderByDesc('id')->first();
534 $resp = $this->post($page->getUrl(), [
535 'name' => 'test page',
536 'html' => 'test desc',
538 $resp->assertRedirect($book->getUrl('/page/test-page'));
540 $resp = $this->get($bookUrl);
541 $this->withHtml($resp)->assertElementContains('.actions', 'New Page')
542 ->assertElementContains('.actions', 'New Chapter');
545 public function test_book_update_restriction_override()
547 $book = $this->entities->book();
548 $bookPage = $book->pages->first();
549 $bookChapter = $book->chapters->first();
551 $bookUrl = $book->getUrl();
552 $this->actingAs($this->viewer)->get($bookUrl . '/edit')
553 ->assertDontSee('Edit Book');
555 $this->setRestrictionsForTestRoles($book, ['view', 'delete']);
557 $this->get($bookUrl . '/edit')->assertRedirect('/');
558 $this->get('/')->assertSee('You do not have permission');
559 $this->get($bookPage->getUrl() . '/edit')->assertRedirect('/');
560 $this->get('/')->assertSee('You do not have permission');
561 $this->get($bookChapter->getUrl() . '/edit')->assertRedirect('/');
562 $this->get('/')->assertSee('You do not have permission');
564 $this->setRestrictionsForTestRoles($book, ['view', 'update']);
566 $this->get($bookUrl . '/edit')->assertOk();
567 $this->get($bookPage->getUrl() . '/edit')->assertOk();
568 $this->get($bookChapter->getUrl() . '/edit')->assertSee('Edit Chapter');
571 public function test_book_delete_restriction_override()
573 $book = $this->entities->book();
574 $bookPage = $book->pages->first();
575 $bookChapter = $book->chapters->first();
577 $bookUrl = $book->getUrl();
578 $this->actingAs($this->viewer)
579 ->get($bookUrl . '/delete')
580 ->assertDontSee('Delete Book');
582 $this->setRestrictionsForTestRoles($book, ['view', 'update']);
584 $this->get($bookUrl . '/delete')->assertRedirect('/');
585 $this->get('/')->assertSee('You do not have permission');
586 $this->get($bookPage->getUrl() . '/delete')->assertRedirect('/');
587 $this->get('/')->assertSee('You do not have permission');
588 $this->get($bookChapter->getUrl() . '/delete')->assertRedirect('/');
589 $this->get('/')->assertSee('You do not have permission');
591 $this->setRestrictionsForTestRoles($book, ['view', 'delete']);
593 $this->get($bookUrl . '/delete')->assertOk()->assertSee('Delete Book');
594 $this->get($bookPage->getUrl() . '/delete')->assertOk()->assertSee('Delete Page');
595 $this->get($bookChapter->getUrl() . '/delete')->assertSee('Delete Chapter');
598 public function test_page_visible_if_has_permissions_when_book_not_visible()
600 $book = $this->entities->book();
601 $bookChapter = $book->chapters->first();
602 $bookPage = $bookChapter->pages->first();
604 foreach ([$book, $bookChapter, $bookPage] as $entity) {
605 $entity->name = Str::random(24);
609 $this->setRestrictionsForTestRoles($book, []);
610 $this->setRestrictionsForTestRoles($bookPage, ['view']);
612 $this->actingAs($this->viewer);
613 $resp = $this->get($bookPage->getUrl());
615 $resp->assertSee($bookPage->name);
616 $resp->assertDontSee(substr($book->name, 0, 15));
617 $resp->assertDontSee(substr($bookChapter->name, 0, 15));
620 public function test_book_sort_view_permission()
622 /** @var Book $firstBook */
623 $firstBook = Book::query()->first();
624 /** @var Book $secondBook */
625 $secondBook = Book::query()->find(2);
627 $this->setRestrictionsForTestRoles($firstBook, ['view', 'update']);
628 $this->setRestrictionsForTestRoles($secondBook, ['view']);
630 // Test sort page visibility
631 $this->actingAs($this->user)->get($secondBook->getUrl('/sort'))->assertRedirect('/');
632 $this->get('/')->assertSee('You do not have permission');
634 // Check sort page on first book
635 $this->actingAs($this->user)->get($firstBook->getUrl('/sort'));
638 public function test_can_create_page_if_chapter_has_permissions_when_book_not_visible()
640 $book = $this->entities->book();
641 $this->setRestrictionsForTestRoles($book, []);
642 $bookChapter = $book->chapters->first();
643 $this->setRestrictionsForTestRoles($bookChapter, ['view']);
645 $this->actingAs($this->user)->get($bookChapter->getUrl())
646 ->assertDontSee('New Page');
648 $this->setRestrictionsForTestRoles($bookChapter, ['view', 'create']);
650 $this->get($bookChapter->getUrl('/create-page'));
651 /** @var Page $page */
652 $page = Page::query()->where('draft', '=', true)->orderByDesc('id')->first();
653 $resp = $this->post($page->getUrl(), [
654 'name' => 'test page',
655 'html' => 'test content',
657 $resp->assertRedirect($book->getUrl('/page/test-page'));
660 public function test_book_permissions_can_be_generated_without_error_if_child_chapter_is_in_recycle_bin()
662 $book = $this->entities->bookHasChaptersAndPages();
663 /** @var Chapter $chapter */
664 $chapter = $book->chapters()->first();
666 $this->asAdmin()->delete($chapter->getUrl());
670 $this->entities->setPermissions($book, ['view'], []);
671 } catch (Exception $e) {
675 $this->assertNull($error);