diff --git a/app/Activity.php b/app/Activity.php index a1fe608f0..ac7c1d749 100644 --- a/app/Activity.php +++ b/app/Activity.php @@ -15,10 +15,10 @@ class Activity extends Model /** * Get the entity for this activity. - * @return bool */ public function entity() { + if ($this->entity_type === '') $this->entity_type = null; return $this->morphTo('entity'); } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 1207c87f1..9f6a4105f 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -35,6 +35,7 @@ class UserController extends Controller */ public function index() { + $this->checkPermission('users-manage'); $users = $this->userRepo->getAllUsers(); $this->setPageTitle('Users'); return view('users/index', ['users' => $users]); diff --git a/app/Repos/BookRepo.php b/app/Repos/BookRepo.php index 572030d43..73572f25e 100644 --- a/app/Repos/BookRepo.php +++ b/app/Repos/BookRepo.php @@ -136,7 +136,7 @@ class BookRepo */ public function newFromInput($input) { - return $this->bookQuery()->fill($input); + return $this->book->newInstance($input); } /** diff --git a/app/Repos/PermissionsRepo.php b/app/Repos/PermissionsRepo.php index c35f29d10..2d497b76a 100644 --- a/app/Repos/PermissionsRepo.php +++ b/app/Repos/PermissionsRepo.php @@ -101,6 +101,7 @@ class PermissionsRepo public function assignRolePermissions(Role $role, $permissionNameArray = []) { $permissions = []; + $permissionNameArray = array_values($permissionNameArray); if ($permissionNameArray && count($permissionNameArray) > 0) { $permissions = $this->permission->whereIn('name', $permissionNameArray)->pluck('id')->toArray(); } diff --git a/app/User.php b/app/User.php index 2d14c6e6e..e1b7c143b 100644 --- a/app/User.php +++ b/app/User.php @@ -67,11 +67,12 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon /** * Get all permissions belonging to a the current user. + * @param bool $cache * @return \Illuminate\Database\Eloquent\Relations\HasManyThrough */ - public function permissions() + public function permissions($cache = true) { - if(isset($this->permissions)) return $this->permissions; + if(isset($this->permissions) && $cache) return $this->permissions; $this->load('roles.permissions'); $permissions = $this->roles->map(function($role) { return $role->permissions; @@ -106,7 +107,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon */ public function attachRoleId($id) { - $this->roles()->attach([$id]); + $this->roles()->attach($id); } /** diff --git a/resources/views/partials/activity-item.blade.php b/resources/views/partials/activity-item.blade.php index 00ca574dd..ff0d74586 100644 --- a/resources/views/partials/activity-item.blade.php +++ b/resources/views/partials/activity-item.blade.php @@ -16,7 +16,7 @@ {{ $activity->getText() }} - @if($activity->entity()) + @if($activity->entity) {{ $activity->entity->name }} @endif diff --git a/resources/views/settings/roles/form.blade.php b/resources/views/settings/roles/form.blade.php index 0758f317a..ed0e3dd91 100644 --- a/resources/views/settings/roles/form.blade.php +++ b/resources/views/settings/roles/form.blade.php @@ -17,7 +17,7 @@
- +

diff --git a/tests/RolesTest.php b/tests/RolesTest.php index 7349c2968..baba208f1 100644 --- a/tests/RolesTest.php +++ b/tests/RolesTest.php @@ -7,16 +7,33 @@ class RolesTest extends TestCase public function setUp() { parent::setUp(); + $this->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() + protected function createNewRole($permissions = []) { $permissionRepo = app('BookStack\Repos\PermissionsRepo'); - return $permissionRepo->saveNewRole(factory(\BookStack\Role::class)->make()->toArray()); + $roleData = factory(\BookStack\Role::class)->make()->toArray(); + $roleData['permissions'] = array_flip($permissions); + return $permissionRepo->saveNewRole($roleData); } public function test_admin_can_see_settings() @@ -80,4 +97,414 @@ class RolesTest extends TestCase ->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 a521fd076..840fe0d08 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -84,6 +84,17 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase 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; + } + /** * Assert that a given string is seen inside an element. *