mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-11-22 09:18:23 +08:00
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:
parent
268db6b1d0
commit
8e6248f57f
|
@ -56,7 +56,8 @@ class Handler extends ExceptionHandler
|
|||
// 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')) {
|
||||
$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);
|
||||
|
|
14
app/Exceptions/NotFoundException.php
Normal file
14
app/Exceptions/NotFoundException.php
Normal 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);
|
||||
}
|
||||
}
|
|
@ -80,7 +80,14 @@ class ChapterController extends Controller
|
|||
$sidebarTree = $this->bookRepo->getChildren($book);
|
||||
Views::add($chapter);
|
||||
$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
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\ExportService;
|
||||
use Illuminate\Http\Request;
|
||||
|
@ -94,7 +95,7 @@ class PageController extends Controller
|
|||
|
||||
try {
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
} catch (NotFoundHttpException $e) {
|
||||
} catch (NotFoundException $e) {
|
||||
$page = $this->pageRepo->findPageUsingOldSlug($pageSlug, $bookSlug);
|
||||
if ($page === null) abort(404);
|
||||
return redirect($page->getUrl());
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?php namespace BookStack\Repos;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Services\RestrictionService;
|
||||
use Illuminate\Support\Str;
|
||||
use BookStack\Book;
|
||||
|
@ -111,11 +112,12 @@ class BookRepo
|
|||
* Get a book by slug
|
||||
* @param $slug
|
||||
* @return mixed
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function getBySlug($slug)
|
||||
{
|
||||
$book = $this->bookQuery()->where('slug', '=', $slug)->first();
|
||||
if ($book === null) abort(404);
|
||||
if ($book === null) throw new NotFoundException('Book not found');
|
||||
return $book;
|
||||
}
|
||||
|
||||
|
@ -153,6 +155,7 @@ class BookRepo
|
|||
$this->chapterRepo->destroy($chapter);
|
||||
}
|
||||
$book->views()->delete();
|
||||
$book->restrictions()->delete();
|
||||
$book->delete();
|
||||
}
|
||||
|
||||
|
@ -210,11 +213,13 @@ class BookRepo
|
|||
public function getChildren(Book $book)
|
||||
{
|
||||
$pageQuery = $book->pages()->where('chapter_id', '=', 0);
|
||||
$this->restrictionService->enforcePageRestrictions($pageQuery, 'view');
|
||||
$pageQuery = $this->restrictionService->enforcePageRestrictions($pageQuery, 'view');
|
||||
$pages = $pageQuery->get();
|
||||
|
||||
$chapterQuery = $book->chapters()->with('pages');
|
||||
$this->restrictionService->enforceChapterRestrictions($chapterQuery, 'view');
|
||||
$chapterQuery = $book->chapters()->with(['pages' => function($query) {
|
||||
$this->restrictionService->enforcePageRestrictions($query, 'view');
|
||||
}]);
|
||||
$chapterQuery = $this->restrictionService->enforceChapterRestrictions($chapterQuery, 'view');
|
||||
$chapters = $chapterQuery->get();
|
||||
$children = $pages->merge($chapters);
|
||||
$bookSlug = $book->slug;
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
|
||||
use Activity;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Services\RestrictionService;
|
||||
use Illuminate\Support\Str;
|
||||
use BookStack\Chapter;
|
||||
|
@ -66,14 +67,24 @@ class ChapterRepo
|
|||
* @param $slug
|
||||
* @param $bookId
|
||||
* @return mixed
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function getBySlug($slug, $bookId)
|
||||
{
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param $input
|
||||
|
@ -98,6 +109,7 @@ class ChapterRepo
|
|||
}
|
||||
Activity::removeEntity($chapter);
|
||||
$chapter->views()->delete();
|
||||
$chapter->restrictions()->delete();
|
||||
$chapter->delete();
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
use Activity;
|
||||
use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Services\RestrictionService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
@ -56,11 +57,12 @@ class PageRepo
|
|||
* @param $slug
|
||||
* @param $bookId
|
||||
* @return mixed
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function getBySlug($slug, $bookId)
|
||||
{
|
||||
$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;
|
||||
}
|
||||
|
||||
|
@ -373,6 +375,7 @@ class PageRepo
|
|||
Activity::removeEntity($page);
|
||||
$page->views()->delete();
|
||||
$page->revisions()->delete();
|
||||
$page->restrictions()->delete();
|
||||
$page->delete();
|
||||
}
|
||||
|
||||
|
|
|
@ -15,10 +15,16 @@ class RestrictionService
|
|||
public function __construct()
|
||||
{
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an entity has a restriction set upon it.
|
||||
* @param Entity $entity
|
||||
* @param $action
|
||||
* @return bool
|
||||
*/
|
||||
public function checkIfEntityRestricted(Entity $entity, $action)
|
||||
{
|
||||
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
|
||||
->orWhere(function ($query) {
|
||||
$query->where('restricted', '=', false)
|
||||
->whereExists(function ($query) {
|
||||
$query->select('*')->from('chapters')
|
||||
->whereRaw('chapters.id=pages.chapter_id')
|
||||
->where('restricted', '=', true)
|
||||
->whereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'chapters', 'Chapter');
|
||||
});
|
||||
|
@ -183,8 +205,10 @@ class RestrictionService
|
|||
return $query->where(function ($parentWhereQuery) {
|
||||
$parentWhereQuery
|
||||
->where('restricted', '=', false)
|
||||
->orWhereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'books', 'Book');
|
||||
->orWhere(function ($query) {
|
||||
$query->where('restricted', '=', true)->whereExists(function ($query) {
|
||||
$this->checkRestrictionsQuery($query, 'books', 'Book');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<?php
|
||||
|
||||
if (! function_exists('versioned_asset')) {
|
||||
if (!function_exists('versioned_asset')) {
|
||||
/**
|
||||
* Get the path to a versioned file.
|
||||
*
|
||||
* @param string $file
|
||||
* @param string $file
|
||||
* @return string
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
|
@ -39,6 +39,7 @@ if (! function_exists('versioned_asset')) {
|
|||
*/
|
||||
function userCan($permission, \BookStack\Ownable $ownable = null)
|
||||
{
|
||||
if (!auth()->check()) return false;
|
||||
if ($ownable === null) {
|
||||
return auth()->user() && auth()->user()->can($permission);
|
||||
}
|
||||
|
@ -47,9 +48,9 @@ function userCan($permission, \BookStack\Ownable $ownable = null)
|
|||
$permissionBaseName = strtolower($permission) . '-';
|
||||
$hasPermission = false;
|
||||
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
|
||||
$restrictionService = app('BookStack\Services\RestrictionService');
|
||||
|
|
|
@ -12,7 +12,7 @@ class DummyContentSeeder extends Seeder
|
|||
public function run()
|
||||
{
|
||||
$user = factory(BookStack\User::class, 1)->create();
|
||||
$role = \BookStack\Role::getDefault();
|
||||
$role = \BookStack\Role::getRole('editor');
|
||||
$user->attachRole($role);
|
||||
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
</filter>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="APP_DEBUG" value="false"/>
|
||||
<env name="CACHE_DRIVER" value="array"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="QUEUE_DRIVER" value="sync"/>
|
||||
|
|
|
@ -87,6 +87,9 @@ header {
|
|||
padding-top: $-s;
|
||||
}
|
||||
}
|
||||
.dropdown-container {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
form.search-box {
|
||||
|
|
|
@ -95,13 +95,14 @@
|
|||
|
||||
// Sidebar list
|
||||
.book-tree {
|
||||
padding: $-xl 0 0 0;
|
||||
padding: $-l 0 0 0;
|
||||
position: relative;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transition: ease-in-out 240ms;
|
||||
transition-property: right, border;
|
||||
border-left: 0px solid #FFF;
|
||||
background-color: #FFF;
|
||||
&.fixed {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
|
|
@ -30,7 +30,9 @@
|
|||
{!! $books->render() !!}
|
||||
@else
|
||||
<p class="text-muted">No books have been created.</p>
|
||||
<a href="/books/create" class="text-pos"><i class="zmdi zmdi-edit"></i>Create one now</a>
|
||||
@if(userCan('books-create-all'))
|
||||
<a href="/books/create" class="text-pos"><i class="zmdi zmdi-edit"></i>Create one now</a>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
<div class="col-sm-4 col-sm-offset-1">
|
||||
|
|
|
@ -2,6 +2,19 @@
|
|||
|
||||
@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>
|
||||
<h1>Book Restrictions</h1>
|
||||
@include('form/restriction-form', ['model' => $book])
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
@section('content')
|
||||
|
||||
<div class="faded-small toolbar" ng-non-bindable>
|
||||
<div class="faded-small toolbar">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
|
@ -15,13 +15,22 @@
|
|||
@endif
|
||||
@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->getUrl() }}/sort" class="text-primary text-button"><i class="zmdi zmdi-sort"></i>Sort</a>
|
||||
@endif
|
||||
@if(userCan('restrictions-manage', $book))
|
||||
<a href="{{$book->getUrl()}}/restrict" class="text-primary text-button"><i class="zmdi zmdi-lock-outline"></i>Restrict</a>
|
||||
@endif
|
||||
@if(userCan('book-delete', $book))
|
||||
<a href="{{ $book->getUrl() }}/delete" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete</a>
|
||||
@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
|
||||
@if(userCan('restrictions-manage', $book))
|
||||
<li><a href="{{$book->getUrl()}}/restrict" class="text-primary"><i class="zmdi zmdi-lock-outline"></i>Restrict</a></li>
|
||||
@endif
|
||||
@if(userCan('book-delete', $book))
|
||||
<li><a href="{{ $book->getUrl() }}/delete" class="text-neg"><i class="zmdi zmdi-delete"></i>Delete</a></li>
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
@ -78,6 +87,15 @@
|
|||
|
||||
<div class="col-md-4 col-md-offset-1">
|
||||
<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">
|
||||
<form ng-submit="searchBook($event)">
|
||||
<input ng-model="searchTerm" ng-change="checkSearchForm()" type="text" name="term" placeholder="Search This Book">
|
||||
|
|
|
@ -2,6 +2,20 @@
|
|||
|
||||
@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">»</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>
|
||||
<h1>Chapter Restrictions</h1>
|
||||
@include('form/restriction-form', ['model' => $chapter])
|
||||
|
|
|
@ -37,10 +37,10 @@
|
|||
<h1>{{ $chapter->name }}</h1>
|
||||
<p class="text-muted">{{ $chapter->description }}</p>
|
||||
|
||||
@if(count($chapter->pages) > 0)
|
||||
@if(count($pages) > 0)
|
||||
<div class="page-list">
|
||||
<hr>
|
||||
@foreach($chapter->pages as $page)
|
||||
@foreach($pages as $page)
|
||||
@include('pages/list-item', ['page' => $page])
|
||||
<hr>
|
||||
@endforeach
|
||||
|
@ -63,6 +63,29 @@
|
|||
</p>
|
||||
</div>
|
||||
<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])
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
|
||||
<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>
|
||||
<a href="/" class="button">Return To Home</a>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<input type="hidden" name="_method" value="PUT">
|
||||
|
||||
<div class="form-group">
|
||||
@include('form/checkbox', ['name' => 'restricted', 'label' => 'Restrict this page?'])
|
||||
@include('form/checkbox', ['name' => 'restricted', 'label' => 'Restrict this ' . $model->getClassName()])
|
||||
</div>
|
||||
|
||||
<table class="table">
|
||||
|
@ -24,5 +24,6 @@
|
|||
@endforeach
|
||||
</table>
|
||||
|
||||
<a href="{{ $model->getUrl() }}" class="button muted">Cancel</a>
|
||||
<button type="submit" class="button pos">Save Restrictions</button>
|
||||
</form>
|
|
@ -2,6 +2,27 @@
|
|||
|
||||
@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">»</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">»</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>
|
||||
<h1>Page Restrictions</h1>
|
||||
@include('form/restriction-form', ['model' => $page])
|
||||
|
|
|
@ -70,7 +70,38 @@
|
|||
</div>
|
||||
</div>
|
||||
<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])
|
||||
|
||||
</div>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
<div class="row">
|
||||
|
||||
<div class="col-md-6">
|
||||
<h3>Role Details</h3>
|
||||
<div class="form-group">
|
||||
<label for="name">Role Name</label>
|
||||
@include('form/text', ['name' => 'display_name'])
|
||||
|
@ -11,7 +12,7 @@
|
|||
<label for="name">Short Role Description</label>
|
||||
@include('form/text', ['name' => 'description'])
|
||||
</div>
|
||||
<hr class="even">
|
||||
<h3>System Permissions</h3>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label> @include('settings/roles/checkbox', ['permission' => 'users-manage']) Manage users</label>
|
||||
|
@ -33,10 +34,17 @@
|
|||
<div class="form-group">
|
||||
<label>@include('settings/roles/checkbox', ['permission' => 'settings-manage']) Manage app settings</label>
|
||||
</div>
|
||||
<hr class="even">
|
||||
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<tr>
|
||||
<th></th>
|
||||
|
@ -104,4 +112,6 @@
|
|||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<a href="/settings/roles" class="button muted">Cancel</a>
|
||||
<button type="submit" class="button pos">Save Role</button>
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
@include('settings/navbar', ['selected' => 'roles'])
|
||||
|
||||
<div class="container">
|
||||
<div class="container small">
|
||||
|
||||
<h1>User Roles</h1>
|
||||
|
||||
|
|
407
tests/RestrictionsTest.php
Normal file
407
tests/RestrictionsTest.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
class TestCase extends Illuminate\Foundation\Testing\TestCase
|
||||
{
|
||||
|
@ -122,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
|
||||
|
|
Loading…
Reference in New Issue
Block a user