Added restriction tests and fixed any bugs in the process

Also updated many styles within areas affected by the new permission and roles system.
This commit is contained in:
Dan Brown 2016-03-05 18:09:21 +00:00
parent 268db6b1d0
commit 8e6248f57f
26 changed files with 680 additions and 32 deletions

View File

@ -56,7 +56,8 @@ class Handler extends ExceptionHandler
// Which will include the basic message to point the user roughly to the cause. // Which will include the basic message to point the user roughly to the cause.
if (($e instanceof PrettyException || $e->getPrevious() instanceof PrettyException) && !config('app.debug')) { if (($e instanceof PrettyException || $e->getPrevious() instanceof PrettyException) && !config('app.debug')) {
$message = ($e instanceof PrettyException) ? $e->getMessage() : $e->getPrevious()->getMessage(); $message = ($e instanceof PrettyException) ? $e->getMessage() : $e->getPrevious()->getMessage();
return response()->view('errors/500', ['message' => $message], 500); $code = ($e->getCode() === 0) ? 500 : $e->getCode();
return response()->view('errors/' . $code, ['message' => $message], $code);
} }
return parent::render($request, $e); return parent::render($request, $e);

View File

@ -0,0 +1,14 @@
<?php namespace BookStack\Exceptions;
class NotFoundException extends PrettyException {
/**
* NotFoundException constructor.
* @param string $message
*/
public function __construct($message = 'Item not found')
{
parent::__construct($message, 404);
}
}

View File

@ -80,7 +80,14 @@ class ChapterController extends Controller
$sidebarTree = $this->bookRepo->getChildren($book); $sidebarTree = $this->bookRepo->getChildren($book);
Views::add($chapter); Views::add($chapter);
$this->setPageTitle($chapter->getShortName()); $this->setPageTitle($chapter->getShortName());
return view('chapters/show', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter, 'sidebarTree' => $sidebarTree]); $pages = $this->chapterRepo->getChildren($chapter);
return view('chapters/show', [
'book' => $book,
'chapter' => $chapter,
'current' => $chapter,
'sidebarTree' => $sidebarTree,
'pages' => $pages
]);
} }
/** /**

View File

@ -1,6 +1,7 @@
<?php namespace BookStack\Http\Controllers; <?php namespace BookStack\Http\Controllers;
use Activity; use Activity;
use BookStack\Exceptions\NotFoundException;
use BookStack\Repos\UserRepo; use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService; use BookStack\Services\ExportService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -94,7 +95,7 @@ class PageController extends Controller
try { try {
$page = $this->pageRepo->getBySlug($pageSlug, $book->id); $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
} catch (NotFoundHttpException $e) { } catch (NotFoundException $e) {
$page = $this->pageRepo->findPageUsingOldSlug($pageSlug, $bookSlug); $page = $this->pageRepo->findPageUsingOldSlug($pageSlug, $bookSlug);
if ($page === null) abort(404); if ($page === null) abort(404);
return redirect($page->getUrl()); return redirect($page->getUrl());

View File

@ -1,6 +1,7 @@
<?php namespace BookStack\Repos; <?php namespace BookStack\Repos;
use Activity; use Activity;
use BookStack\Exceptions\NotFoundException;
use BookStack\Services\RestrictionService; use BookStack\Services\RestrictionService;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use BookStack\Book; use BookStack\Book;
@ -111,11 +112,12 @@ class BookRepo
* Get a book by slug * Get a book by slug
* @param $slug * @param $slug
* @return mixed * @return mixed
* @throws NotFoundException
*/ */
public function getBySlug($slug) public function getBySlug($slug)
{ {
$book = $this->bookQuery()->where('slug', '=', $slug)->first(); $book = $this->bookQuery()->where('slug', '=', $slug)->first();
if ($book === null) abort(404); if ($book === null) throw new NotFoundException('Book not found');
return $book; return $book;
} }
@ -153,6 +155,7 @@ class BookRepo
$this->chapterRepo->destroy($chapter); $this->chapterRepo->destroy($chapter);
} }
$book->views()->delete(); $book->views()->delete();
$book->restrictions()->delete();
$book->delete(); $book->delete();
} }
@ -210,11 +213,13 @@ class BookRepo
public function getChildren(Book $book) public function getChildren(Book $book)
{ {
$pageQuery = $book->pages()->where('chapter_id', '=', 0); $pageQuery = $book->pages()->where('chapter_id', '=', 0);
$this->restrictionService->enforcePageRestrictions($pageQuery, 'view'); $pageQuery = $this->restrictionService->enforcePageRestrictions($pageQuery, 'view');
$pages = $pageQuery->get(); $pages = $pageQuery->get();
$chapterQuery = $book->chapters()->with('pages'); $chapterQuery = $book->chapters()->with(['pages' => function($query) {
$this->restrictionService->enforceChapterRestrictions($chapterQuery, 'view'); $this->restrictionService->enforcePageRestrictions($query, 'view');
}]);
$chapterQuery = $this->restrictionService->enforceChapterRestrictions($chapterQuery, 'view');
$chapters = $chapterQuery->get(); $chapters = $chapterQuery->get();
$children = $pages->merge($chapters); $children = $pages->merge($chapters);
$bookSlug = $book->slug; $bookSlug = $book->slug;

View File

@ -2,6 +2,7 @@
use Activity; use Activity;
use BookStack\Exceptions\NotFoundException;
use BookStack\Services\RestrictionService; use BookStack\Services\RestrictionService;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use BookStack\Chapter; use BookStack\Chapter;
@ -66,14 +67,24 @@ class ChapterRepo
* @param $slug * @param $slug
* @param $bookId * @param $bookId
* @return mixed * @return mixed
* @throws NotFoundException
*/ */
public function getBySlug($slug, $bookId) public function getBySlug($slug, $bookId)
{ {
$chapter = $this->chapterQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first(); $chapter = $this->chapterQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
if ($chapter === null) abort(404); if ($chapter === null) throw new NotFoundException('Chapter not found');
return $chapter; return $chapter;
} }
/**
* Get the child items for a chapter
* @param Chapter $chapter
*/
public function getChildren(Chapter $chapter)
{
return $this->restrictionService->enforcePageRestrictions($chapter->pages())->get();
}
/** /**
* Create a new chapter from request input. * Create a new chapter from request input.
* @param $input * @param $input
@ -98,6 +109,7 @@ class ChapterRepo
} }
Activity::removeEntity($chapter); Activity::removeEntity($chapter);
$chapter->views()->delete(); $chapter->views()->delete();
$chapter->restrictions()->delete();
$chapter->delete(); $chapter->delete();
} }

View File

@ -4,6 +4,7 @@
use Activity; use Activity;
use BookStack\Book; use BookStack\Book;
use BookStack\Chapter; use BookStack\Chapter;
use BookStack\Exceptions\NotFoundException;
use BookStack\Services\RestrictionService; use BookStack\Services\RestrictionService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@ -56,11 +57,12 @@ class PageRepo
* @param $slug * @param $slug
* @param $bookId * @param $bookId
* @return mixed * @return mixed
* @throws NotFoundException
*/ */
public function getBySlug($slug, $bookId) public function getBySlug($slug, $bookId)
{ {
$page = $this->pageQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first(); $page = $this->pageQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
if ($page === null) throw new NotFoundHttpException('Page not found'); if ($page === null) throw new NotFoundException('Page not found');
return $page; return $page;
} }
@ -373,6 +375,7 @@ class PageRepo
Activity::removeEntity($page); Activity::removeEntity($page);
$page->views()->delete(); $page->views()->delete();
$page->revisions()->delete(); $page->revisions()->delete();
$page->restrictions()->delete();
$page->delete(); $page->delete();
} }

View File

@ -15,10 +15,16 @@ class RestrictionService
public function __construct() public function __construct()
{ {
$user = auth()->user(); $user = auth()->user();
$this->userRoles = $user ? auth()->user()->roles->pluck('id') : false; $this->userRoles = $user ? auth()->user()->roles->pluck('id') : [];
$this->isAdmin = $user ? auth()->user()->hasRole('admin') : false; $this->isAdmin = $user ? auth()->user()->hasRole('admin') : false;
} }
/**
* Checks if an entity has a restriction set upon it.
* @param Entity $entity
* @param $action
* @return bool
*/
public function checkIfEntityRestricted(Entity $entity, $action) public function checkIfEntityRestricted(Entity $entity, $action)
{ {
if ($this->isAdmin) return true; if ($this->isAdmin) return true;
@ -93,12 +99,28 @@ class RestrictionService
}); });
}); });
}) })
// 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 // Page unrestricted, Has a chapter with accepted permissions
->orWhere(function ($query) { ->orWhere(function ($query) {
$query->where('restricted', '=', false) $query->where('restricted', '=', false)
->whereExists(function ($query) { ->whereExists(function ($query) {
$query->select('*')->from('chapters') $query->select('*')->from('chapters')
->whereRaw('chapters.id=pages.chapter_id') ->whereRaw('chapters.id=pages.chapter_id')
->where('restricted', '=', true)
->whereExists(function ($query) { ->whereExists(function ($query) {
$this->checkRestrictionsQuery($query, 'chapters', 'Chapter'); $this->checkRestrictionsQuery($query, 'chapters', 'Chapter');
}); });
@ -183,10 +205,12 @@ class RestrictionService
return $query->where(function ($parentWhereQuery) { return $query->where(function ($parentWhereQuery) {
$parentWhereQuery $parentWhereQuery
->where('restricted', '=', false) ->where('restricted', '=', false)
->orWhereExists(function ($query) { ->orWhere(function ($query) {
$query->where('restricted', '=', true)->whereExists(function ($query) {
$this->checkRestrictionsQuery($query, 'books', 'Book'); $this->checkRestrictionsQuery($query, 'books', 'Book');
}); });
}); });
});
} }
/** /**

View File

@ -1,6 +1,6 @@
<?php <?php
if (! function_exists('versioned_asset')) { if (!function_exists('versioned_asset')) {
/** /**
* Get the path to a versioned file. * Get the path to a versioned file.
* *
@ -39,6 +39,7 @@ if (! function_exists('versioned_asset')) {
*/ */
function userCan($permission, \BookStack\Ownable $ownable = null) function userCan($permission, \BookStack\Ownable $ownable = null)
{ {
if (!auth()->check()) return false;
if ($ownable === null) { if ($ownable === null) {
return auth()->user() && auth()->user()->can($permission); return auth()->user() && auth()->user()->can($permission);
} }
@ -47,9 +48,9 @@ function userCan($permission, \BookStack\Ownable $ownable = null)
$permissionBaseName = strtolower($permission) . '-'; $permissionBaseName = strtolower($permission) . '-';
$hasPermission = false; $hasPermission = false;
if (auth()->user()->can($permissionBaseName . 'all')) $hasPermission = true; if (auth()->user()->can($permissionBaseName . 'all')) $hasPermission = true;
if (auth()->user()->can($permissionBaseName . 'own') && $ownable->createdBy->id === auth()->user()->id) $hasPermission = true; if (auth()->user()->can($permissionBaseName . 'own') && $ownable->createdBy && $ownable->createdBy->id === auth()->user()->id) $hasPermission = true;
if(!$ownable instanceof \BookStack\Entity) return $hasPermission; if (!$ownable instanceof \BookStack\Entity) return $hasPermission;
// Check restrictions on the entitiy // Check restrictions on the entitiy
$restrictionService = app('BookStack\Services\RestrictionService'); $restrictionService = app('BookStack\Services\RestrictionService');

View File

@ -12,7 +12,7 @@ class DummyContentSeeder extends Seeder
public function run() public function run()
{ {
$user = factory(BookStack\User::class, 1)->create(); $user = factory(BookStack\User::class, 1)->create();
$role = \BookStack\Role::getDefault(); $role = \BookStack\Role::getRole('editor');
$user->attachRole($role); $user->attachRole($role);

View File

@ -21,6 +21,7 @@
</filter> </filter>
<php> <php>
<env name="APP_ENV" value="testing"/> <env name="APP_ENV" value="testing"/>
<env name="APP_DEBUG" value="false"/>
<env name="CACHE_DRIVER" value="array"/> <env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/> <env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/> <env name="QUEUE_DRIVER" value="sync"/>

View File

@ -87,6 +87,9 @@ header {
padding-top: $-s; padding-top: $-s;
} }
} }
.dropdown-container {
font-size: 0.9em;
}
} }
form.search-box { form.search-box {

View File

@ -95,13 +95,14 @@
// Sidebar list // Sidebar list
.book-tree { .book-tree {
padding: $-xl 0 0 0; padding: $-l 0 0 0;
position: relative; position: relative;
right: 0; right: 0;
top: 0; top: 0;
transition: ease-in-out 240ms; transition: ease-in-out 240ms;
transition-property: right, border; transition-property: right, border;
border-left: 0px solid #FFF; border-left: 0px solid #FFF;
background-color: #FFF;
&.fixed { &.fixed {
position: fixed; position: fixed;
top: 0; top: 0;

View File

@ -30,8 +30,10 @@
{!! $books->render() !!} {!! $books->render() !!}
@else @else
<p class="text-muted">No books have been created.</p> <p class="text-muted">No books have been created.</p>
@if(userCan('books-create-all'))
<a href="/books/create" class="text-pos"><i class="zmdi zmdi-edit"></i>Create one now</a> <a href="/books/create" class="text-pos"><i class="zmdi zmdi-edit"></i>Create one now</a>
@endif @endif
@endif
</div> </div>
<div class="col-sm-4 col-sm-offset-1"> <div class="col-sm-4 col-sm-offset-1">
<div id="recents"> <div id="recents">

View File

@ -2,6 +2,19 @@
@section('content') @section('content')
<div class="faded-small toolbar">
<div class="container">
<div class="row">
<div class="col-sm-12 faded">
<div class="breadcrumbs">
<a href="{{$book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->getShortName() }}</a>
</div>
</div>
</div>
</div>
</div>
<div class="container" ng-non-bindable> <div class="container" ng-non-bindable>
<h1>Book Restrictions</h1> <h1>Book Restrictions</h1>
@include('form/restriction-form', ['model' => $book]) @include('form/restriction-form', ['model' => $book])

View File

@ -2,7 +2,7 @@
@section('content') @section('content')
<div class="faded-small toolbar" ng-non-bindable> <div class="faded-small toolbar">
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
@ -15,13 +15,22 @@
@endif @endif
@if(userCan('book-update', $book)) @if(userCan('book-update', $book))
<a href="{{$book->getEditUrl()}}" class="text-primary text-button"><i class="zmdi zmdi-edit"></i>Edit</a> <a href="{{$book->getEditUrl()}}" class="text-primary text-button"><i class="zmdi zmdi-edit"></i>Edit</a>
<a href="{{ $book->getUrl() }}/sort" class="text-primary text-button"><i class="zmdi zmdi-sort"></i>Sort</a> @endif
@if(userCan('book-update', $book) || userCan('restrictions-manage', $book) || userCan('book-delete', $book))
<div dropdown class="dropdown-container">
<a dropdown-toggle class="text-primary text-button"><i class="zmdi zmdi-more-vert"></i></a>
<ul>
@if(userCan('book-update', $book))
<li><a href="{{ $book->getUrl() }}/sort" class="text-primary"><i class="zmdi zmdi-sort"></i>Sort</a></li>
@endif @endif
@if(userCan('restrictions-manage', $book)) @if(userCan('restrictions-manage', $book))
<a href="{{$book->getUrl()}}/restrict" class="text-primary text-button"><i class="zmdi zmdi-lock-outline"></i>Restrict</a> <li><a href="{{$book->getUrl()}}/restrict" class="text-primary"><i class="zmdi zmdi-lock-outline"></i>Restrict</a></li>
@endif @endif
@if(userCan('book-delete', $book)) @if(userCan('book-delete', $book))
<a href="{{ $book->getUrl() }}/delete" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete</a> <li><a href="{{ $book->getUrl() }}/delete" class="text-neg"><i class="zmdi zmdi-delete"></i>Delete</a></li>
@endif
</ul>
</div>
@endif @endif
</div> </div>
</div> </div>
@ -78,6 +87,15 @@
<div class="col-md-4 col-md-offset-1"> <div class="col-md-4 col-md-offset-1">
<div class="margin-top large"></div> <div class="margin-top large"></div>
@if($book->restricted)
<p class="text-muted">
@if(userCan('restrictions-manage', $book))
<a href="{{ $book->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Book Restricted</a>
@else
<i class="zmdi zmdi-lock-outline"></i>Book Restricted
@endif
</p>
@endif
<div class="search-box"> <div class="search-box">
<form ng-submit="searchBook($event)"> <form ng-submit="searchBook($event)">
<input ng-model="searchTerm" ng-change="checkSearchForm()" type="text" name="term" placeholder="Search This Book"> <input ng-model="searchTerm" ng-change="checkSearchForm()" type="text" name="term" placeholder="Search This Book">

View File

@ -2,6 +2,20 @@
@section('content') @section('content')
<div class="faded-small toolbar">
<div class="container">
<div class="row">
<div class="col-sm-12 faded">
<div class="breadcrumbs">
<a href="{{$chapter->book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $chapter->book->getShortName() }}</a>
<span class="sep">&raquo;</span>
<a href="{{ $chapter->getUrl() }}" class="text-chapter text-button"><i class="zmdi zmdi-collection-bookmark"></i>{{$chapter->getShortName()}}</a>
</div>
</div>
</div>
</div>
</div>
<div class="container" ng-non-bindable> <div class="container" ng-non-bindable>
<h1>Chapter Restrictions</h1> <h1>Chapter Restrictions</h1>
@include('form/restriction-form', ['model' => $chapter]) @include('form/restriction-form', ['model' => $chapter])

View File

@ -37,10 +37,10 @@
<h1>{{ $chapter->name }}</h1> <h1>{{ $chapter->name }}</h1>
<p class="text-muted">{{ $chapter->description }}</p> <p class="text-muted">{{ $chapter->description }}</p>
@if(count($chapter->pages) > 0) @if(count($pages) > 0)
<div class="page-list"> <div class="page-list">
<hr> <hr>
@foreach($chapter->pages as $page) @foreach($pages as $page)
@include('pages/list-item', ['page' => $page]) @include('pages/list-item', ['page' => $page])
<hr> <hr>
@endforeach @endforeach
@ -63,6 +63,29 @@
</p> </p>
</div> </div>
<div class="col-md-3 col-md-offset-1"> <div class="col-md-3 col-md-offset-1">
<div class="margin-top large"></div>
@if($book->restricted || $chapter->restricted)
<div class="text-muted">
@if($book->restricted)
@if(userCan('restrictions-manage', $book))
<a href="{{ $book->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Book Restricted</a>
@else
<i class="zmdi zmdi-lock-outline"></i>Book Restricted
@endif
<br>
@endif
@if($chapter->restricted)
@if(userCan('restrictions-manage', $chapter))
<a href="{{ $chapter->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Chapter Restricted</a>
@else
<i class="zmdi zmdi-lock-outline"></i>Chapter Restricted
@endif
@endif
</div>
@endif
@include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree]) @include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree])
</div> </div>
</div> </div>

View File

@ -4,7 +4,7 @@
<div class="container"> <div class="container">
<h1 class="text-muted">Page Not Found</h1> <h1 class="text-muted">{{ $message or 'Page Not Found' }}</h1>
<p>Sorry, The page you were looking for could not be found.</p> <p>Sorry, The page you were looking for could not be found.</p>
<a href="/" class="button">Return To Home</a> <a href="/" class="button">Return To Home</a>
</div> </div>

View File

@ -3,7 +3,7 @@
<input type="hidden" name="_method" value="PUT"> <input type="hidden" name="_method" value="PUT">
<div class="form-group"> <div class="form-group">
@include('form/checkbox', ['name' => 'restricted', 'label' => 'Restrict this page?']) @include('form/checkbox', ['name' => 'restricted', 'label' => 'Restrict this ' . $model->getClassName()])
</div> </div>
<table class="table"> <table class="table">
@ -24,5 +24,6 @@
@endforeach @endforeach
</table> </table>
<a href="{{ $model->getUrl() }}" class="button muted">Cancel</a>
<button type="submit" class="button pos">Save Restrictions</button> <button type="submit" class="button pos">Save Restrictions</button>
</form> </form>

View File

@ -2,6 +2,27 @@
@section('content') @section('content')
<div class="faded-small toolbar">
<div class="container">
<div class="row">
<div class="col-sm-12 faded">
<div class="breadcrumbs">
<a href="{{$page->book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $page->book->getShortName() }}</a>
@if($page->hasChapter())
<span class="sep">&raquo;</span>
<a href="{{ $page->chapter->getUrl() }}" class="text-chapter text-button">
<i class="zmdi zmdi-collection-bookmark"></i>
{{$page->chapter->getShortName()}}
</a>
@endif
<span class="sep">&raquo;</span>
<a href="{{$page->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-file"></i>{{ $page->getShortName() }}</a>
</div>
</div>
</div>
</div>
</div>
<div class="container" ng-non-bindable> <div class="container" ng-non-bindable>
<h1>Page Restrictions</h1> <h1>Page Restrictions</h1>
@include('form/restriction-form', ['model' => $page]) @include('form/restriction-form', ['model' => $page])

View File

@ -70,7 +70,38 @@
</div> </div>
</div> </div>
<div class="col-md-3 print-hidden"> <div class="col-md-3 print-hidden">
<div class="margin-top large"></div>
@if($book->restricted || ($page->chapter && $page->chapter->restricted) || $page->restricted)
<div class="text-muted">
@if($book->restricted)
@if(userCan('restrictions-manage', $book))
<a href="{{ $book->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Book restricted</a>
@else
<i class="zmdi zmdi-lock-outline"></i>Book restricted
@endif
<br>
@endif
@if($page->chapter && $page->chapter->restricted)
@if(userCan('restrictions-manage', $page->chapter))
<a href="{{ $page->chapter->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Chapter restricted</a>
@else
<i class="zmdi zmdi-lock-outline"></i>Chapter restricted
@endif
<br>
@endif
@if($page->restricted)
@if(userCan('restrictions-manage', $page))
<a href="{{ $page->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Page restricted</a>
@else
<i class="zmdi zmdi-lock-outline"></i>Page restricted
@endif
<br>
@endif
</div>
@endif
@include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree]) @include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree])
</div> </div>

View File

@ -3,6 +3,7 @@
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<h3>Role Details</h3>
<div class="form-group"> <div class="form-group">
<label for="name">Role Name</label> <label for="name">Role Name</label>
@include('form/text', ['name' => 'display_name']) @include('form/text', ['name' => 'display_name'])
@ -11,7 +12,7 @@
<label for="name">Short Role Description</label> <label for="name">Short Role Description</label>
@include('form/text', ['name' => 'description']) @include('form/text', ['name' => 'description'])
</div> </div>
<hr class="even"> <h3>System Permissions</h3>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<label> @include('settings/roles/checkbox', ['permission' => 'users-manage']) Manage users</label> <label> @include('settings/roles/checkbox', ['permission' => 'users-manage']) Manage users</label>
@ -33,10 +34,17 @@
<div class="form-group"> <div class="form-group">
<label>@include('settings/roles/checkbox', ['permission' => 'settings-manage']) Manage app settings</label> <label>@include('settings/roles/checkbox', ['permission' => 'settings-manage']) Manage app settings</label>
</div> </div>
<hr class="even">
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<h3>Asset Permissions</h3>
<p>
These permissions control default access to the assets within the system. <br>
Restrictions on Books, Chapters and Pages will override these permissions.
</p>
<table class="table"> <table class="table">
<tr> <tr>
<th></th> <th></th>
@ -104,4 +112,6 @@
</div> </div>
</div> </div>
<a href="/settings/roles" class="button muted">Cancel</a>
<button type="submit" class="button pos">Save Role</button> <button type="submit" class="button pos">Save Role</button>

View File

@ -4,7 +4,7 @@
@include('settings/navbar', ['selected' => 'roles']) @include('settings/navbar', ['selected' => 'roles'])
<div class="container"> <div class="container small">
<h1>User Roles</h1> <h1>User Roles</h1>

407
tests/RestrictionsTest.php Normal file
View File

@ -0,0 +1,407 @@
<?php
class RestrictionsTest extends TestCase
{
protected $user;
public function setUp()
{
parent::setUp();
$this->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);
}
}

View File

@ -1,6 +1,7 @@
<?php <?php
use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\DatabaseTransactions;
use Symfony\Component\DomCrawler\Crawler;
class TestCase extends Illuminate\Foundation\Testing\TestCase class TestCase extends Illuminate\Foundation\Testing\TestCase
{ {
@ -122,6 +123,40 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
return $this; 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. * Click the text within the selected element.
* @param $parentElement * @param $parentElement