Books
- @if(isset($currentUser) && $currentUser->can('settings-update'))
+ @if(isset($currentUser) && $currentUser->can('settings-manage'))
Settings
@endif
@if(!isset($signedIn) || !$signedIn)
diff --git a/resources/views/books/index.blade.php b/resources/views/books/index.blade.php
index 9fa483735..7b5c92b5a 100644
--- a/resources/views/books/index.blade.php
+++ b/resources/views/books/index.blade.php
@@ -8,7 +8,7 @@
@@ -30,7 +30,9 @@
{!! $books->render() !!}
@else
No books have been created.
-
Create one now
+ @if(userCan('books-create-all'))
+
Create one now
+ @endif
@endif
diff --git a/resources/views/books/restrictions.blade.php b/resources/views/books/restrictions.blade.php
new file mode 100644
index 000000000..60b126a7b
--- /dev/null
+++ b/resources/views/books/restrictions.blade.php
@@ -0,0 +1,23 @@
+@extends('base')
+
+@section('content')
+
+
+
+
+
+
Book Restrictions
+ @include('form/restriction-form', ['model' => $book])
+
+
+@stop
diff --git a/resources/views/books/show.blade.php b/resources/views/books/show.blade.php
index db89bed9e..cd32a406b 100644
--- a/resources/views/books/show.blade.php
+++ b/resources/views/books/show.blade.php
@@ -2,23 +2,35 @@
@section('content')
-
diff --git a/resources/views/settings/roles/checkbox.blade.php b/resources/views/settings/roles/checkbox.blade.php
new file mode 100644
index 000000000..35aa61ef5
--- /dev/null
+++ b/resources/views/settings/roles/checkbox.blade.php
@@ -0,0 +1,3 @@
+
hasPermission($permission)))) checked="checked" @endif
+ value="true">
\ No newline at end of file
diff --git a/resources/views/settings/roles/create.blade.php b/resources/views/settings/roles/create.blade.php
new file mode 100644
index 000000000..f7d39f454
--- /dev/null
+++ b/resources/views/settings/roles/create.blade.php
@@ -0,0 +1,15 @@
+@extends('base')
+
+@section('content')
+
+ @include('settings/navbar', ['selected' => 'roles'])
+
+
+
Create New Role
+
+
+
+
+@stop
diff --git a/resources/views/settings/roles/delete.blade.php b/resources/views/settings/roles/delete.blade.php
new file mode 100644
index 000000000..5d1ffe259
--- /dev/null
+++ b/resources/views/settings/roles/delete.blade.php
@@ -0,0 +1,28 @@
+@extends('base')
+
+@section('content')
+
+ @include('settings/navbar', ['selected' => 'roles'])
+
+
+
Delete Role
+
This will delete the role with the name '{{$role->display_name}}'.
+
+
+
+
+@stop
diff --git a/resources/views/settings/roles/edit.blade.php b/resources/views/settings/roles/edit.blade.php
new file mode 100644
index 000000000..f98e6e0b3
--- /dev/null
+++ b/resources/views/settings/roles/edit.blade.php
@@ -0,0 +1,24 @@
+@extends('base')
+
+@section('content')
+
+ @include('settings/navbar', ['selected' => 'roles'])
+
+
+
+
+
Edit Role {{ $role->display_name }}
+
+
+
+
+
+
+
+@stop
diff --git a/resources/views/settings/roles/form.blade.php b/resources/views/settings/roles/form.blade.php
new file mode 100644
index 000000000..fafb9bed2
--- /dev/null
+++ b/resources/views/settings/roles/form.blade.php
@@ -0,0 +1,117 @@
+{!! csrf_field() !!}
+
+
+
+
+
Role Details
+
+ Role Name
+ @include('form/text', ['name' => 'display_name'])
+
+
+ Short Role Description
+ @include('form/text', ['name' => 'description'])
+
+
System Permissions
+
+
+ @include('settings/roles/checkbox', ['permission' => 'users-manage']) Manage users
+
+
+ @include('settings/roles/checkbox', ['permission' => 'user-roles-manage']) Manage user roles
+
+
+
+
+
+ @include('settings/roles/checkbox', ['permission' => 'restrictions-manage-all']) Manage all restrictions
+
+
+ @include('settings/roles/checkbox', ['permission' => 'restrictions-manage-own']) Manage restrictions on own content
+
+
+
+
+ @include('settings/roles/checkbox', ['permission' => 'settings-manage']) Manage app settings
+
+
+
+
+
+
+
+
Asset Permissions
+
+ These permissions control default access to the assets within the system.
+ Restrictions on Books, Chapters and Pages will override these permissions.
+
+
+
+
+ Create
+ Edit
+ Delete
+
+
+ Books
+
+ @include('settings/roles/checkbox', ['permission' => 'book-create-all']) All
+
+
+ @include('settings/roles/checkbox', ['permission' => 'book-update-own']) Own
+ @include('settings/roles/checkbox', ['permission' => 'book-update-all']) All
+
+
+ @include('settings/roles/checkbox', ['permission' => 'book-delete-own']) Own
+ @include('settings/roles/checkbox', ['permission' => 'book-delete-all']) All
+
+
+
+ Chapters
+
+ @include('settings/roles/checkbox', ['permission' => 'chapter-create-own']) Own
+ @include('settings/roles/checkbox', ['permission' => 'chapter-create-all']) All
+
+
+ @include('settings/roles/checkbox', ['permission' => 'chapter-update-own']) Own
+ @include('settings/roles/checkbox', ['permission' => 'chapter-update-all']) All
+
+
+ @include('settings/roles/checkbox', ['permission' => 'chapter-delete-own']) Own
+ @include('settings/roles/checkbox', ['permission' => 'chapter-delete-all']) All
+
+
+
+ Pages
+
+ @include('settings/roles/checkbox', ['permission' => 'page-create-own']) Own
+ @include('settings/roles/checkbox', ['permission' => 'page-create-all']) All
+
+
+ @include('settings/roles/checkbox', ['permission' => 'page-update-own']) Own
+ @include('settings/roles/checkbox', ['permission' => 'page-update-all']) All
+
+
+ @include('settings/roles/checkbox', ['permission' => 'page-delete-own']) Own
+ @include('settings/roles/checkbox', ['permission' => 'page-delete-all']) All
+
+
+
+ Images
+ @include('settings/roles/checkbox', ['permission' => 'image-create-all'])
+
+ @include('settings/roles/checkbox', ['permission' => 'image-update-own']) Own
+ @include('settings/roles/checkbox', ['permission' => 'image-update-all']) All
+
+
+ @include('settings/roles/checkbox', ['permission' => 'image-delete-own']) Own
+ @include('settings/roles/checkbox', ['permission' => 'image-delete-all']) All
+
+
+
+
+
+
+
+
Cancel
+
Save Role
\ No newline at end of file
diff --git a/resources/views/settings/roles/index.blade.php b/resources/views/settings/roles/index.blade.php
new file mode 100644
index 000000000..8f92a5eba
--- /dev/null
+++ b/resources/views/settings/roles/index.blade.php
@@ -0,0 +1,31 @@
+@extends('base')
+
+@section('content')
+
+ @include('settings/navbar', ['selected' => 'roles'])
+
+
+
+
User Roles
+
+
+ Add new role
+
+
+
+
+ Role Name
+
+ Users
+
+ @foreach($roles as $role)
+
+ {{ $role->display_name }}
+ {{ $role->description }}
+ {{ $role->users->count() }}
+
+ @endforeach
+
+
+
+@stop
diff --git a/resources/views/users/forms/ldap.blade.php b/resources/views/users/forms/ldap.blade.php
index 4a6673dc2..47edb211b 100644
--- a/resources/views/users/forms/ldap.blade.php
+++ b/resources/views/users/forms/ldap.blade.php
@@ -3,21 +3,21 @@
@include('form.text', ['name' => 'name'])
@endif
-@if($currentUser->can('user-update'))
+@if(userCan('users-manage'))
@endif
-@if($currentUser->can('user-update'))
+@if(userCan('users-manage'))
@endif
diff --git a/resources/views/users/index.blade.php b/resources/views/users/index.blade.php
index 6e5d10c5f..f06630714 100644
--- a/resources/views/users/index.blade.php
+++ b/resources/views/users/index.blade.php
@@ -8,7 +8,7 @@
Users
- @if($currentUser->can('user-create'))
+ @if(userCan('users-manage'))
Add new user
@@ -18,30 +18,32 @@
Name
Email
-
User Type
+
User Roles
@foreach($users as $user)
- @if($currentUser->can('user-update') || $currentUser->id == $user->id)
+ @if(userCan('users-manage') || $currentUser->id == $user->id)
@endif
{{ $user->name }}
- @if($currentUser->can('user-update') || $currentUser->id == $user->id)
+ @if(userCan('users-manage') || $currentUser->id == $user->id)
@endif
- @if($currentUser->can('user-update') || $currentUser->id == $user->id)
+ @if(userCan('users-manage') || $currentUser->id == $user->id)
@endif
{{ $user->email }}
- @if($currentUser->can('user-update') || $currentUser->id == $user->id)
+ @if(userCan('users-manage') || $currentUser->id == $user->id)
@endif
- {{ $user->role->display_name }}
+
+ {{ $user->roles->implode('display_name', ', ') }}
+
@endforeach
diff --git a/tests/Auth/AuthTest.php b/tests/Auth/AuthTest.php
index 694022666..067840841 100644
--- a/tests/Auth/AuthTest.php
+++ b/tests/Auth/AuthTest.php
@@ -133,12 +133,12 @@ class AuthTest extends TestCase
->click('Add new user')
->type($user->name, '#name')
->type($user->email, '#email')
- ->select(2, '#role')
+ ->check('roles[admin]')
->type($user->password, '#password')
->type($user->password, '#password-confirm')
->press('Save')
- ->seeInDatabase('users', $user->toArray())
->seePageIs('/settings/users')
+ ->seeInDatabase('users', $user->toArray())
->see($user->name);
}
diff --git a/tests/RestrictionsTest.php b/tests/RestrictionsTest.php
new file mode 100644
index 000000000..40b5a7647
--- /dev/null
+++ b/tests/RestrictionsTest.php
@@ -0,0 +1,407 @@
+user = $this->getNewUser();
+ }
+
+ /**
+ * Manually set some restrictions on an entity.
+ * @param \BookStack\Entity $entity
+ * @param $actions
+ */
+ protected function setEntityRestrictions(\BookStack\Entity $entity, $actions)
+ {
+ $entity->restricted = true;
+ $entity->restrictions()->delete();
+ $role = $this->user->roles->first();
+ foreach ($actions as $action) {
+ $entity->restrictions()->create([
+ 'role_id' => $role->id,
+ 'action' => strtolower($action)
+ ]);
+ }
+ $entity->save();
+ $entity->load('restrictions');
+ }
+
+ public function test_book_view_restriction()
+ {
+ $book = \BookStack\Book::first();
+ $bookPage = $book->pages->first();
+ $bookChapter = $book->chapters->first();
+
+ $bookUrl = $book->getUrl();
+ $this->actingAs($this->user)
+ ->visit($bookUrl)
+ ->seePageIs($bookUrl);
+
+ $this->setEntityRestrictions($book, []);
+
+ $this->forceVisit($bookUrl)
+ ->see('Book not found');
+ $this->forceVisit($bookPage->getUrl())
+ ->see('Book not found');
+ $this->forceVisit($bookChapter->getUrl())
+ ->see('Book not found');
+
+ $this->setEntityRestrictions($book, ['view']);
+
+ $this->visit($bookUrl)
+ ->see($book->name);
+ $this->visit($bookPage->getUrl())
+ ->see($bookPage->name);
+ $this->visit($bookChapter->getUrl())
+ ->see($bookChapter->name);
+ }
+
+ public function test_book_create_restriction()
+ {
+ $book = \BookStack\Book::first();
+
+ $bookUrl = $book->getUrl();
+ $this->actingAs($this->user)
+ ->visit($bookUrl)
+ ->seeInElement('.action-buttons', 'New Page')
+ ->seeInElement('.action-buttons', 'New Chapter');
+
+ $this->setEntityRestrictions($book, ['view', 'delete', 'update']);
+
+ $this->forceVisit($bookUrl . '/chapter/create')
+ ->see('You do not have permission')->seePageIs('/');
+ $this->forceVisit($bookUrl . '/page/create')
+ ->see('You do not have permission')->seePageIs('/');
+ $this->visit($bookUrl)->dontSeeInElement('.action-buttons', 'New Page')
+ ->dontSeeInElement('.action-buttons', 'New Chapter');
+
+ $this->setEntityRestrictions($book, ['view', 'create']);
+
+ $this->visit($bookUrl . '/chapter/create')
+ ->type('test chapter', 'name')
+ ->type('test description for chapter', 'description')
+ ->press('Save Chapter')
+ ->seePageIs($bookUrl . '/chapter/test-chapter');
+ $this->visit($bookUrl . '/page/create')
+ ->type('test page', 'name')
+ ->type('test content', 'html')
+ ->press('Save Page')
+ ->seePageIs($bookUrl . '/page/test-page');
+ $this->visit($bookUrl)->seeInElement('.action-buttons', 'New Page')
+ ->seeInElement('.action-buttons', 'New Chapter');
+ }
+
+ public function test_book_update_restriction()
+ {
+ $book = \BookStack\Book::first();
+ $bookPage = $book->pages->first();
+ $bookChapter = $book->chapters->first();
+
+ $bookUrl = $book->getUrl();
+ $this->actingAs($this->user)
+ ->visit($bookUrl . '/edit')
+ ->see('Edit Book');
+
+ $this->setEntityRestrictions($book, ['view', 'delete']);
+
+ $this->forceVisit($bookUrl . '/edit')
+ ->see('You do not have permission')->seePageIs('/');
+ $this->forceVisit($bookPage->getUrl() . '/edit')
+ ->see('You do not have permission')->seePageIs('/');
+ $this->forceVisit($bookChapter->getUrl() . '/edit')
+ ->see('You do not have permission')->seePageIs('/');
+
+ $this->setEntityRestrictions($book, ['view', 'update']);
+
+ $this->visit($bookUrl . '/edit')
+ ->seePageIs($bookUrl . '/edit');
+ $this->visit($bookPage->getUrl() . '/edit')
+ ->seePageIs($bookPage->getUrl() . '/edit');
+ $this->visit($bookChapter->getUrl() . '/edit')
+ ->see('Edit Chapter');
+ }
+
+ public function test_book_delete_restriction()
+ {
+ $book = \BookStack\Book::first();
+ $bookPage = $book->pages->first();
+ $bookChapter = $book->chapters->first();
+
+ $bookUrl = $book->getUrl();
+ $this->actingAs($this->user)
+ ->visit($bookUrl . '/delete')
+ ->see('Delete Book');
+
+ $this->setEntityRestrictions($book, ['view', 'update']);
+
+ $this->forceVisit($bookUrl . '/delete')
+ ->see('You do not have permission')->seePageIs('/');
+ $this->forceVisit($bookPage->getUrl() . '/delete')
+ ->see('You do not have permission')->seePageIs('/');
+ $this->forceVisit($bookChapter->getUrl() . '/delete')
+ ->see('You do not have permission')->seePageIs('/');
+
+ $this->setEntityRestrictions($book, ['view', 'delete']);
+
+ $this->visit($bookUrl . '/delete')
+ ->seePageIs($bookUrl . '/delete')->see('Delete Book');
+ $this->visit($bookPage->getUrl() . '/delete')
+ ->seePageIs($bookPage->getUrl() . '/delete')->see('Delete Page');
+ $this->visit($bookChapter->getUrl() . '/delete')
+ ->see('Delete Chapter');
+ }
+
+ public function test_chapter_view_restriction()
+ {
+ $chapter = \BookStack\Chapter::first();
+ $chapterPage = $chapter->pages->first();
+
+ $chapterUrl = $chapter->getUrl();
+ $this->actingAs($this->user)
+ ->visit($chapterUrl)
+ ->seePageIs($chapterUrl);
+
+ $this->setEntityRestrictions($chapter, []);
+
+ $this->forceVisit($chapterUrl)
+ ->see('Chapter not found');
+ $this->forceVisit($chapterPage->getUrl())
+ ->see('Page not found');
+
+ $this->setEntityRestrictions($chapter, ['view']);
+
+ $this->visit($chapterUrl)
+ ->see($chapter->name);
+ $this->visit($chapterPage->getUrl())
+ ->see($chapterPage->name);
+ }
+
+ public function test_chapter_create_restriction()
+ {
+ $chapter = \BookStack\Chapter::first();
+
+ $chapterUrl = $chapter->getUrl();
+ $this->actingAs($this->user)
+ ->visit($chapterUrl)
+ ->seeInElement('.action-buttons', 'New Page');
+
+ $this->setEntityRestrictions($chapter, ['view', 'delete', 'update']);
+
+ $this->forceVisit($chapterUrl . '/create-page')
+ ->see('You do not have permission')->seePageIs('/');
+ $this->visit($chapterUrl)->dontSeeInElement('.action-buttons', 'New Page');
+
+ $this->setEntityRestrictions($chapter, ['view', 'create']);
+
+
+ $this->visit($chapterUrl . '/create-page')
+ ->type('test page', 'name')
+ ->type('test content', 'html')
+ ->press('Save Page')
+ ->seePageIs($chapter->book->getUrl() . '/page/test-page');
+ $this->visit($chapterUrl)->seeInElement('.action-buttons', 'New Page');
+ }
+
+ public function test_chapter_update_restriction()
+ {
+ $chapter = \BookStack\Chapter::first();
+ $chapterPage = $chapter->pages->first();
+
+ $chapterUrl = $chapter->getUrl();
+ $this->actingAs($this->user)
+ ->visit($chapterUrl . '/edit')
+ ->see('Edit Chapter');
+
+ $this->setEntityRestrictions($chapter, ['view', 'delete']);
+
+ $this->forceVisit($chapterUrl . '/edit')
+ ->see('You do not have permission')->seePageIs('/');
+ $this->forceVisit($chapterPage->getUrl() . '/edit')
+ ->see('You do not have permission')->seePageIs('/');
+
+ $this->setEntityRestrictions($chapter, ['view', 'update']);
+
+ $this->visit($chapterUrl . '/edit')
+ ->seePageIs($chapterUrl . '/edit')->see('Edit Chapter');
+ $this->visit($chapterPage->getUrl() . '/edit')
+ ->seePageIs($chapterPage->getUrl() . '/edit');
+ }
+
+ public function test_chapter_delete_restriction()
+ {
+ $chapter = \BookStack\Chapter::first();
+ $chapterPage = $chapter->pages->first();
+
+ $chapterUrl = $chapter->getUrl();
+ $this->actingAs($this->user)
+ ->visit($chapterUrl . '/delete')
+ ->see('Delete Chapter');
+
+ $this->setEntityRestrictions($chapter, ['view', 'update']);
+
+ $this->forceVisit($chapterUrl . '/delete')
+ ->see('You do not have permission')->seePageIs('/');
+ $this->forceVisit($chapterPage->getUrl() . '/delete')
+ ->see('You do not have permission')->seePageIs('/');
+
+ $this->setEntityRestrictions($chapter, ['view', 'delete']);
+
+ $this->visit($chapterUrl . '/delete')
+ ->seePageIs($chapterUrl . '/delete')->see('Delete Chapter');
+ $this->visit($chapterPage->getUrl() . '/delete')
+ ->seePageIs($chapterPage->getUrl() . '/delete')->see('Delete Page');
+ }
+
+ public function test_page_view_restriction()
+ {
+ $page = \BookStack\Page::first();
+
+ $pageUrl = $page->getUrl();
+ $this->actingAs($this->user)
+ ->visit($pageUrl)
+ ->seePageIs($pageUrl);
+
+ $this->setEntityRestrictions($page, ['update', 'delete']);
+
+ $this->forceVisit($pageUrl)
+ ->see('Page not found');
+
+ $this->setEntityRestrictions($page, ['view']);
+
+ $this->visit($pageUrl)
+ ->see($page->name);
+ }
+
+ public function test_page_update_restriction()
+ {
+ $page = \BookStack\Chapter::first();
+
+ $pageUrl = $page->getUrl();
+ $this->actingAs($this->user)
+ ->visit($pageUrl . '/edit')
+ ->seeInField('name', $page->name);
+
+ $this->setEntityRestrictions($page, ['view', 'delete']);
+
+ $this->forceVisit($pageUrl . '/edit')
+ ->see('You do not have permission')->seePageIs('/');
+
+ $this->setEntityRestrictions($page, ['view', 'update']);
+
+ $this->visit($pageUrl . '/edit')
+ ->seePageIs($pageUrl . '/edit')->seeInField('name', $page->name);
+ }
+
+ public function test_page_delete_restriction()
+ {
+ $page = \BookStack\Page::first();
+
+ $pageUrl = $page->getUrl();
+ $this->actingAs($this->user)
+ ->visit($pageUrl . '/delete')
+ ->see('Delete Page');
+
+ $this->setEntityRestrictions($page, ['view', 'update']);
+
+ $this->forceVisit($pageUrl . '/delete')
+ ->see('You do not have permission')->seePageIs('/');
+
+ $this->setEntityRestrictions($page, ['view', 'delete']);
+
+ $this->visit($pageUrl . '/delete')
+ ->seePageIs($pageUrl . '/delete')->see('Delete Page');
+ }
+
+ public function test_book_restriction_form()
+ {
+ $book = \BookStack\Book::first();
+ $this->asAdmin()->visit($book->getUrl() . '/restrict')
+ ->see('Book Restrictions')
+ ->check('restricted')
+ ->check('restrictions[2][view]')
+ ->press('Save Restrictions')
+ ->seeInDatabase('books', ['id' => $book->id, 'restricted' => true])
+ ->seeInDatabase('restrictions', [
+ 'restrictable_id' => $book->id,
+ 'restrictable_type' => 'BookStack\Book',
+ 'role_id' => '2',
+ 'action' => 'view'
+ ]);
+ }
+
+ public function test_chapter_restriction_form()
+ {
+ $chapter = \BookStack\Chapter::first();
+ $this->asAdmin()->visit($chapter->getUrl() . '/restrict')
+ ->see('Chapter Restrictions')
+ ->check('restricted')
+ ->check('restrictions[2][update]')
+ ->press('Save Restrictions')
+ ->seeInDatabase('chapters', ['id' => $chapter->id, 'restricted' => true])
+ ->seeInDatabase('restrictions', [
+ 'restrictable_id' => $chapter->id,
+ 'restrictable_type' => 'BookStack\Chapter',
+ 'role_id' => '2',
+ 'action' => 'update'
+ ]);
+ }
+
+ public function test_page_restriction_form()
+ {
+ $page = \BookStack\Page::first();
+ $this->asAdmin()->visit($page->getUrl() . '/restrict')
+ ->see('Page Restrictions')
+ ->check('restricted')
+ ->check('restrictions[2][delete]')
+ ->press('Save Restrictions')
+ ->seeInDatabase('pages', ['id' => $page->id, 'restricted' => true])
+ ->seeInDatabase('restrictions', [
+ 'restrictable_id' => $page->id,
+ 'restrictable_type' => 'BookStack\Page',
+ 'role_id' => '2',
+ 'action' => 'delete'
+ ]);
+ }
+
+ public function test_restricted_pages_not_visible_in_book_navigation_on_pages()
+ {
+ $chapter = \BookStack\Chapter::first();
+ $page = $chapter->pages->first();
+ $page2 = $chapter->pages[2];
+
+ $this->setEntityRestrictions($page, []);
+
+ $this->actingAs($this->user)
+ ->visit($page2->getUrl())
+ ->dontSeeInElement('.sidebar-page-list', $page->name);
+ }
+
+ public function test_restricted_pages_not_visible_in_book_navigation_on_chapters()
+ {
+ $chapter = \BookStack\Chapter::first();
+ $page = $chapter->pages->first();
+
+ $this->setEntityRestrictions($page, []);
+
+ $this->actingAs($this->user)
+ ->visit($chapter->getUrl())
+ ->dontSeeInElement('.sidebar-page-list', $page->name);
+ }
+
+ public function test_restricted_pages_not_visible_on_chapter_pages()
+ {
+ $chapter = \BookStack\Chapter::first();
+ $page = $chapter->pages->first();
+
+ $this->setEntityRestrictions($page, []);
+
+ $this->actingAs($this->user)
+ ->visit($chapter->getUrl())
+ ->dontSee($page->name);
+ }
+
+}
diff --git a/tests/RolesTest.php b/tests/RolesTest.php
new file mode 100644
index 000000000..baba208f1
--- /dev/null
+++ b/tests/RolesTest.php
@@ -0,0 +1,510 @@
+user = $this->getNewBlankUser();
+ }
+
+ /**
+ * Give the given user some permissions.
+ * @param \BookStack\User $user
+ * @param array $permissions
+ */
+ protected function giveUserPermissions(\BookStack\User $user, $permissions = [])
+ {
+ $newRole = $this->createNewRole($permissions);
+ $user->attachRole($newRole);
+ $user->load('roles');
+ $user->permissions(false);
+ }
+
+ /**
+ * Create a new basic role for testing purposes.
+ * @param array $permissions
+ * @return static
+ */
+ protected function createNewRole($permissions = [])
+ {
+ $permissionRepo = app('BookStack\Repos\PermissionsRepo');
+ $roleData = factory(\BookStack\Role::class)->make()->toArray();
+ $roleData['permissions'] = array_flip($permissions);
+ return $permissionRepo->saveNewRole($roleData);
+ }
+
+ public function test_admin_can_see_settings()
+ {
+ $this->asAdmin()->visit('/settings')->see('Settings');
+ }
+
+ public function test_cannot_delete_admin_role()
+ {
+ $adminRole = \BookStack\Role::getRole('admin');
+ $deletePageUrl = '/settings/roles/delete/' . $adminRole->id;
+ $this->asAdmin()->visit($deletePageUrl)
+ ->press('Confirm')
+ ->seePageIs($deletePageUrl)
+ ->see('cannot be deleted');
+ }
+
+ public function test_role_cannot_be_deleted_if_default()
+ {
+ $newRole = $this->createNewRole();
+ $this->setSettings(['registration-role' => $newRole->id]);
+
+ $deletePageUrl = '/settings/roles/delete/' . $newRole->id;
+ $this->asAdmin()->visit($deletePageUrl)
+ ->press('Confirm')
+ ->seePageIs($deletePageUrl)
+ ->see('cannot be deleted');
+ }
+
+ public function test_role_create_update_delete_flow()
+ {
+ $testRoleName = 'Test Role';
+ $testRoleDesc = 'a little test description';
+ $testRoleUpdateName = 'An Super Updated role';
+
+ // Creation
+ $this->asAdmin()->visit('/settings')
+ ->click('Roles')
+ ->seePageIs('/settings/roles')
+ ->click('Add new role')
+ ->type('Test Role', 'display_name')
+ ->type('A little test description', 'description')
+ ->press('Save Role')
+ ->seeInDatabase('roles', ['display_name' => $testRoleName, 'name' => 'test-role', 'description' => $testRoleDesc])
+ ->seePageIs('/settings/roles');
+ // Updating
+ $this->asAdmin()->visit('/settings/roles')
+ ->see($testRoleDesc)
+ ->click($testRoleName)
+ ->type($testRoleUpdateName, '#display_name')
+ ->press('Save Role')
+ ->seeInDatabase('roles', ['display_name' => $testRoleUpdateName, 'name' => 'test-role', 'description' => $testRoleDesc])
+ ->seePageIs('/settings/roles');
+ // Deleting
+ $this->asAdmin()->visit('/settings/roles')
+ ->click($testRoleUpdateName)
+ ->click('Delete Role')
+ ->see($testRoleUpdateName)
+ ->press('Confirm')
+ ->seePageIs('/settings/roles')
+ ->dontSee($testRoleUpdateName);
+ }
+
+ public function test_manage_user_permission()
+ {
+ $this->actingAs($this->user)->visit('/')->visit('/settings/users')
+ ->seePageIs('/');
+ $this->giveUserPermissions($this->user, ['users-manage']);
+ $this->actingAs($this->user)->visit('/')->visit('/settings/users')
+ ->seePageIs('/settings/users');
+ }
+
+ public function test_user_roles_manage_permission()
+ {
+ $this->actingAs($this->user)->visit('/')->visit('/settings/roles')
+ ->seePageIs('/')->visit('/settings/roles/1')->seePageIs('/');
+ $this->giveUserPermissions($this->user, ['user-roles-manage']);
+ $this->actingAs($this->user)->visit('/settings/roles')
+ ->seePageIs('/settings/roles')->click('Admin')
+ ->see('Edit Role');
+ }
+
+ public function test_settings_manage_permission()
+ {
+ $this->actingAs($this->user)->visit('/')->visit('/settings')
+ ->seePageIs('/');
+ $this->giveUserPermissions($this->user, ['settings-manage']);
+ $this->actingAs($this->user)->visit('/')->visit('/settings')
+ ->seePageIs('/settings')->press('Save Settings')->see('Settings Saved');
+ }
+
+ public function test_restrictions_manage_all_permission()
+ {
+ $page = \BookStack\Page::take(1)->get()->first();
+ $this->actingAs($this->user)->visit($page->getUrl())
+ ->dontSee('Restrict')
+ ->visit($page->getUrl() . '/restrict')
+ ->seePageIs('/');
+ $this->giveUserPermissions($this->user, ['restrictions-manage-all']);
+ $this->actingAs($this->user)->visit($page->getUrl())
+ ->see('Restrict')
+ ->click('Restrict')
+ ->see('Page Restrictions')->seePageIs($page->getUrl() . '/restrict');
+ }
+
+ public function test_restrictions_manage_own_permission()
+ {
+ $otherUsersPage = \BookStack\Page::take(1)->get()->first();
+ $content = $this->createEntityChainBelongingToUser($this->user);
+ // Check can't restrict other's content
+ $this->actingAs($this->user)->visit($otherUsersPage->getUrl())
+ ->dontSee('Restrict')
+ ->visit($otherUsersPage->getUrl() . '/restrict')
+ ->seePageIs('/');
+ // Check can't restrict own content
+ $this->actingAs($this->user)->visit($content['page']->getUrl())
+ ->dontSee('Restrict')
+ ->visit($content['page']->getUrl() . '/restrict')
+ ->seePageIs('/');
+
+ $this->giveUserPermissions($this->user, ['restrictions-manage-own']);
+
+ // Check can't restrict other's content
+ $this->actingAs($this->user)->visit($otherUsersPage->getUrl())
+ ->dontSee('Restrict')
+ ->visit($otherUsersPage->getUrl() . '/restrict')
+ ->seePageIs('/');
+ // Check can restrict own content
+ $this->actingAs($this->user)->visit($content['page']->getUrl())
+ ->see('Restrict')
+ ->click('Restrict')
+ ->seePageIs($content['page']->getUrl() . '/restrict');
+ }
+
+ /**
+ * Check a standard entity access permission
+ * @param string $permission
+ * @param array $accessUrls Urls that are only accessible after having the permission
+ * @param array $visibles Check this text, In the buttons toolbar, is only visible with the permission
+ * @param null $callback
+ */
+ private function checkAccessPermission($permission, $accessUrls = [], $visibles = [])
+ {
+ foreach ($accessUrls as $url) {
+ $this->actingAs($this->user)->visit('/')->visit($url)
+ ->seePageIs('/');
+ }
+ foreach ($visibles as $url => $text) {
+ $this->actingAs($this->user)->visit('/')->visit($url)
+ ->dontSeeInElement('.action-buttons',$text);
+ }
+
+ $this->giveUserPermissions($this->user, [$permission]);
+
+ foreach ($accessUrls as $url) {
+ $this->actingAs($this->user)->visit('/')->visit($url)
+ ->seePageIs($url);
+ }
+ foreach ($visibles as $url => $text) {
+ $this->actingAs($this->user)->visit('/')->visit($url)
+ ->see($text);
+ }
+ }
+
+ public function test_books_create_all_permissions()
+ {
+ $this->checkAccessPermission('book-create-all', [
+ '/books/create'
+ ], [
+ '/books' => 'Add new book'
+ ]);
+
+ $this->visit('/books/create')
+ ->type('test book', 'name')
+ ->type('book desc', 'description')
+ ->press('Save Book')
+ ->seePageIs('/books/test-book');
+ }
+
+ public function test_books_edit_own_permission()
+ {
+ $otherBook = \BookStack\Book::take(1)->get()->first();
+ $ownBook = $this->createEntityChainBelongingToUser($this->user)['book'];
+ $this->checkAccessPermission('book-update-own', [
+ $ownBook->getUrl() . '/edit'
+ ], [
+ $ownBook->getUrl() => 'Edit'
+ ]);
+
+ $this->visit($otherBook->getUrl())
+ ->dontSeeInElement('.action-buttons', 'Edit')
+ ->visit($otherBook->getUrl() . '/edit')
+ ->seePageIs('/');
+ }
+
+ public function test_books_edit_all_permission()
+ {
+ $otherBook = \BookStack\Book::take(1)->get()->first();
+ $this->checkAccessPermission('book-update-all', [
+ $otherBook->getUrl() . '/edit'
+ ], [
+ $otherBook->getUrl() => 'Edit'
+ ]);
+ }
+
+ public function test_books_delete_own_permission()
+ {
+ $this->giveUserPermissions($this->user, ['book-update-all']);
+ $otherBook = \BookStack\Book::take(1)->get()->first();
+ $ownBook = $this->createEntityChainBelongingToUser($this->user)['book'];
+ $this->checkAccessPermission('book-delete-own', [
+ $ownBook->getUrl() . '/delete'
+ ], [
+ $ownBook->getUrl() => 'Delete'
+ ]);
+
+ $this->visit($otherBook->getUrl())
+ ->dontSeeInElement('.action-buttons', 'Delete')
+ ->visit($otherBook->getUrl() . '/delete')
+ ->seePageIs('/');
+ $this->visit($ownBook->getUrl())->visit($ownBook->getUrl() . '/delete')
+ ->press('Confirm')
+ ->seePageIs('/books')
+ ->dontSee($ownBook->name);
+ }
+
+ public function test_books_delete_all_permission()
+ {
+ $this->giveUserPermissions($this->user, ['book-update-all']);
+ $otherBook = \BookStack\Book::take(1)->get()->first();
+ $this->checkAccessPermission('book-delete-all', [
+ $otherBook->getUrl() . '/delete'
+ ], [
+ $otherBook->getUrl() => 'Delete'
+ ]);
+
+ $this->visit($otherBook->getUrl())->visit($otherBook->getUrl() . '/delete')
+ ->press('Confirm')
+ ->seePageIs('/books')
+ ->dontSee($otherBook->name);
+ }
+
+ public function test_chapter_create_own_permissions()
+ {
+ $book = \BookStack\Book::take(1)->get()->first();
+ $ownBook = $this->createEntityChainBelongingToUser($this->user)['book'];
+ $baseUrl = $ownBook->getUrl() . '/chapter';
+ $this->checkAccessPermission('chapter-create-own', [
+ $baseUrl . '/create'
+ ], [
+ $ownBook->getUrl() => 'New Chapter'
+ ]);
+
+ $this->visit($baseUrl . '/create')
+ ->type('test chapter', 'name')
+ ->type('chapter desc', 'description')
+ ->press('Save Chapter')
+ ->seePageIs($baseUrl . '/test-chapter');
+
+ $this->visit($book->getUrl())
+ ->dontSeeInElement('.action-buttons', 'New Chapter')
+ ->visit($book->getUrl() . '/chapter/create')
+ ->seePageIs('/');
+ }
+
+ public function test_chapter_create_all_permissions()
+ {
+ $book = \BookStack\Book::take(1)->get()->first();
+ $baseUrl = $book->getUrl() . '/chapter';
+ $this->checkAccessPermission('chapter-create-all', [
+ $baseUrl . '/create'
+ ], [
+ $book->getUrl() => 'New Chapter'
+ ]);
+
+ $this->visit($baseUrl . '/create')
+ ->type('test chapter', 'name')
+ ->type('chapter desc', 'description')
+ ->press('Save Chapter')
+ ->seePageIs($baseUrl . '/test-chapter');
+ }
+
+ public function test_chapter_edit_own_permission()
+ {
+ $otherChapter = \BookStack\Chapter::take(1)->get()->first();
+ $ownChapter = $this->createEntityChainBelongingToUser($this->user)['chapter'];
+ $this->checkAccessPermission('chapter-update-own', [
+ $ownChapter->getUrl() . '/edit'
+ ], [
+ $ownChapter->getUrl() => 'Edit'
+ ]);
+
+ $this->visit($otherChapter->getUrl())
+ ->dontSeeInElement('.action-buttons', 'Edit')
+ ->visit($otherChapter->getUrl() . '/edit')
+ ->seePageIs('/');
+ }
+
+ public function test_chapter_edit_all_permission()
+ {
+ $otherChapter = \BookStack\Chapter::take(1)->get()->first();
+ $this->checkAccessPermission('chapter-update-all', [
+ $otherChapter->getUrl() . '/edit'
+ ], [
+ $otherChapter->getUrl() => 'Edit'
+ ]);
+ }
+
+ public function test_chapter_delete_own_permission()
+ {
+ $this->giveUserPermissions($this->user, ['chapter-update-all']);
+ $otherChapter = \BookStack\Chapter::take(1)->get()->first();
+ $ownChapter = $this->createEntityChainBelongingToUser($this->user)['chapter'];
+ $this->checkAccessPermission('chapter-delete-own', [
+ $ownChapter->getUrl() . '/delete'
+ ], [
+ $ownChapter->getUrl() => 'Delete'
+ ]);
+
+ $bookUrl = $ownChapter->book->getUrl();
+ $this->visit($otherChapter->getUrl())
+ ->dontSeeInElement('.action-buttons', 'Delete')
+ ->visit($otherChapter->getUrl() . '/delete')
+ ->seePageIs('/');
+ $this->visit($ownChapter->getUrl())->visit($ownChapter->getUrl() . '/delete')
+ ->press('Confirm')
+ ->seePageIs($bookUrl)
+ ->dontSeeInElement('.book-content', $ownChapter->name);
+ }
+
+ public function test_chapter_delete_all_permission()
+ {
+ $this->giveUserPermissions($this->user, ['chapter-update-all']);
+ $otherChapter = \BookStack\Chapter::take(1)->get()->first();
+ $this->checkAccessPermission('chapter-delete-all', [
+ $otherChapter->getUrl() . '/delete'
+ ], [
+ $otherChapter->getUrl() => 'Delete'
+ ]);
+
+ $bookUrl = $otherChapter->book->getUrl();
+ $this->visit($otherChapter->getUrl())->visit($otherChapter->getUrl() . '/delete')
+ ->press('Confirm')
+ ->seePageIs($bookUrl)
+ ->dontSeeInElement('.book-content', $otherChapter->name);
+ }
+
+ public function test_page_create_own_permissions()
+ {
+ $book = \BookStack\Book::take(1)->get()->first();
+ $chapter = \BookStack\Chapter::take(1)->get()->first();
+
+ $entities = $this->createEntityChainBelongingToUser($this->user);
+ $ownBook = $entities['book'];
+ $ownChapter = $entities['chapter'];
+
+ $baseUrl = $ownBook->getUrl() . '/page';
+
+ $this->checkAccessPermission('page-create-own', [
+ $baseUrl . '/create',
+ $ownChapter->getUrl() . '/create-page'
+ ], [
+ $ownBook->getUrl() => 'New Page',
+ $ownChapter->getUrl() => 'New Page'
+ ]);
+
+ $this->visit($baseUrl . '/create')
+ ->type('test page', 'name')
+ ->type('page desc', 'html')
+ ->press('Save Page')
+ ->seePageIs($baseUrl . '/test-page');
+
+ $this->visit($book->getUrl())
+ ->dontSeeInElement('.action-buttons', 'New Page')
+ ->visit($book->getUrl() . '/page/create')
+ ->seePageIs('/');
+ $this->visit($chapter->getUrl())
+ ->dontSeeInElement('.action-buttons', 'New Page')
+ ->visit($chapter->getUrl() . '/create-page')
+ ->seePageIs('/');
+ }
+
+ public function test_page_create_all_permissions()
+ {
+ $book = \BookStack\Book::take(1)->get()->first();
+ $chapter = \BookStack\Chapter::take(1)->get()->first();
+ $baseUrl = $book->getUrl() . '/page';
+ $this->checkAccessPermission('page-create-all', [
+ $baseUrl . '/create',
+ $chapter->getUrl() . '/create-page'
+ ], [
+ $book->getUrl() => 'New Page',
+ $chapter->getUrl() => 'New Page'
+ ]);
+
+ $this->visit($baseUrl . '/create')
+ ->type('test page', 'name')
+ ->type('page desc', 'html')
+ ->press('Save Page')
+ ->seePageIs($baseUrl . '/test-page');
+
+ $this->visit($chapter->getUrl() . '/create-page')
+ ->type('new test page', 'name')
+ ->type('page desc', 'html')
+ ->press('Save Page')
+ ->seePageIs($baseUrl . '/new-test-page');
+ }
+
+ public function test_page_edit_own_permission()
+ {
+ $otherPage = \BookStack\Page::take(1)->get()->first();
+ $ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
+ $this->checkAccessPermission('page-update-own', [
+ $ownPage->getUrl() . '/edit'
+ ], [
+ $ownPage->getUrl() => 'Edit'
+ ]);
+
+ $this->visit($otherPage->getUrl())
+ ->dontSeeInElement('.action-buttons', 'Edit')
+ ->visit($otherPage->getUrl() . '/edit')
+ ->seePageIs('/');
+ }
+
+ public function test_page_edit_all_permission()
+ {
+ $otherPage = \BookStack\Page::take(1)->get()->first();
+ $this->checkAccessPermission('page-update-all', [
+ $otherPage->getUrl() . '/edit'
+ ], [
+ $otherPage->getUrl() => 'Edit'
+ ]);
+ }
+
+ public function test_page_delete_own_permission()
+ {
+ $this->giveUserPermissions($this->user, ['page-update-all']);
+ $otherPage = \BookStack\Page::take(1)->get()->first();
+ $ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
+ $this->checkAccessPermission('page-delete-own', [
+ $ownPage->getUrl() . '/delete'
+ ], [
+ $ownPage->getUrl() => 'Delete'
+ ]);
+
+ $bookUrl = $ownPage->book->getUrl();
+ $this->visit($otherPage->getUrl())
+ ->dontSeeInElement('.action-buttons', 'Delete')
+ ->visit($otherPage->getUrl() . '/delete')
+ ->seePageIs('/');
+ $this->visit($ownPage->getUrl())->visit($ownPage->getUrl() . '/delete')
+ ->press('Confirm')
+ ->seePageIs($bookUrl)
+ ->dontSeeInElement('.book-content', $ownPage->name);
+ }
+
+ public function test_page_delete_all_permission()
+ {
+ $this->giveUserPermissions($this->user, ['page-update-all']);
+ $otherPage = \BookStack\Page::take(1)->get()->first();
+ $this->checkAccessPermission('page-delete-all', [
+ $otherPage->getUrl() . '/delete'
+ ], [
+ $otherPage->getUrl() => 'Delete'
+ ]);
+
+ $bookUrl = $otherPage->book->getUrl();
+ $this->visit($otherPage->getUrl())->visit($otherPage->getUrl() . '/delete')
+ ->press('Confirm')
+ ->seePageIs($bookUrl)
+ ->dontSeeInElement('.book-content', $otherPage->name);
+ }
+
+}
diff --git a/tests/TestCase.php b/tests/TestCase.php
index 4b8578a43..567dc93ec 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -1,6 +1,7 @@
admin === null) {
- $this->admin = \BookStack\User::find(1);
+ $adminRole = \BookStack\Role::getRole('admin');
+ $this->admin = $adminRole->users->first();
}
return $this->actingAs($this->admin);
}
@@ -78,8 +80,19 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
protected function getNewUser($attributes = [])
{
$user = factory(\BookStack\User::class)->create($attributes);
- $userRepo = app('BookStack\Repos\UserRepo');
- $userRepo->attachDefaultRole($user);
+ $role = \BookStack\Role::getRole('editor');
+ $user->attachRole($role);;
+ return $user;
+ }
+
+ /**
+ * Quick way to create a new user without any permissions
+ * @param array $attributes
+ * @return mixed
+ */
+ protected function getNewBlankUser($attributes = [])
+ {
+ $user = factory(\BookStack\User::class)->create($attributes);
return $user;
}
@@ -110,6 +123,40 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
return $this;
}
+ /**
+ * Assert that the current page matches a given URI.
+ *
+ * @param string $uri
+ * @return $this
+ */
+ protected function seePageUrlIs($uri)
+ {
+ $this->assertEquals(
+ $uri, $this->currentUri, "Did not land on expected page [{$uri}].\n"
+ );
+
+ return $this;
+ }
+
+ /**
+ * Do a forced visit that does not error out on exception.
+ * @param string $uri
+ * @param array $parameters
+ * @param array $cookies
+ * @param array $files
+ * @return $this
+ */
+ protected function forceVisit($uri, $parameters = [], $cookies = [], $files = [])
+ {
+ $method = 'GET';
+ $uri = $this->prepareUrlForRequest($uri);
+ $this->call($method, $uri, $parameters, $cookies, $files);
+ $this->clearInputs()->followRedirects();
+ $this->currentUri = $this->app->make('request')->fullUrl();
+ $this->crawler = new Crawler($this->response->getContent(), $uri);
+ return $this;
+ }
+
/**
* Click the text within the selected element.
* @param $parentElement