]> BookStack Code Mirror - bookstack/commitdiff
Added restriction tests and fixed any bugs in the process
authorDan Brown <redacted>
Sat, 5 Mar 2016 18:09:21 +0000 (18:09 +0000)
committerDan Brown <redacted>
Sat, 5 Mar 2016 18:09:21 +0000 (18:09 +0000)
Also updated many styles within areas affected by the new permission and roles system.

26 files changed:
app/Exceptions/Handler.php
app/Exceptions/NotFoundException.php [new file with mode: 0644]
app/Http/Controllers/ChapterController.php
app/Http/Controllers/PageController.php
app/Repos/BookRepo.php
app/Repos/ChapterRepo.php
app/Repos/PageRepo.php
app/Services/RestrictionService.php
app/helpers.php
database/seeds/DummyContentSeeder.php
phpunit.xml
resources/assets/sass/_header.scss
resources/assets/sass/_lists.scss
resources/views/books/index.blade.php
resources/views/books/restrictions.blade.php
resources/views/books/show.blade.php
resources/views/chapters/restrictions.blade.php
resources/views/chapters/show.blade.php
resources/views/errors/404.blade.php
resources/views/form/restriction-form.blade.php
resources/views/pages/restrictions.blade.php
resources/views/pages/show.blade.php
resources/views/settings/roles/form.blade.php
resources/views/settings/roles/index.blade.php
tests/RestrictionsTest.php [new file with mode: 0644]
tests/TestCase.php

index 73a3169531c82c8080d13cb822315b1380ff4618..14d553ed0bb359b200dfc91a95bdc05ff92501ba 100644 (file)
@@ -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);
diff --git a/app/Exceptions/NotFoundException.php b/app/Exceptions/NotFoundException.php
new file mode 100644 (file)
index 0000000..3c027dc
--- /dev/null
@@ -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);
+    }
+}
\ No newline at end of file
index 2ad08c7da6b979332fc7260640fccc6cf337b54f..6b8a2f18fc8aa9af8a1fb15f0decb239754f49ad 100644 (file)
@@ -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
+        ]);
     }
 
     /**
index b469f51ddccfce7d2f61d51cbe4aae006509cc09..19e4744ea5643e5412a1b24689b4ad03de545314 100644 (file)
@@ -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());
index 73572f25eb7807011eeeeae329787d5d85ed06df..4ae7cc0622a18ff5205f33eac8ab9300c4ef77c7 100644 (file)
@@ -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;
index 90f2f8c543bf26a5c755afc66fa6d237c2f0ddf5..095596a608931f10ac1912e1e017fe403f72de96 100644 (file)
@@ -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();
     }
 
index c4cf00e7c7b28d5519a138b024cc4abb3a43e86b..f3933af69da8cd3cce6ddcdceedf69b0fe64c869 100644 (file)
@@ -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();
     }
 
index 0ef80b229002d5e137c9f9d4ec60290a648c7604..f7838bf88b0284d0a45bb69927d902f0b8d52243 100644 (file)
@@ -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');
+                    });
                 });
         });
     }
index 8f080c5e157f14e619059084abe7b811877c251c..ead6b300810130111fc146e6923cdfe86aed4e15 100644 (file)
@@ -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');
index aa70eaa0a6139eeab12abf1a50bddcae66d3eab2..328971f260b787baac504f8687b6883bce3da7fc 100644 (file)
@@ -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);
 
 
index 762fc2da70c061dde6eed4de437cefc875780572..66196e8cf1bdda4df84d062beb08a9721ff97048 100644 (file)
@@ -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"/>
index 1edfc0037ea399b6f77696272ed88afb0f0b14ff..87aa20046dc6f4d9ec09e59b642f212bf70dd684 100644 (file)
@@ -87,6 +87,9 @@ header {
       padding-top: $-s;
     }
   }
+  .dropdown-container {
+    font-size: 0.9em;
+  }
 }
 
 form.search-box {
index f0bd3b1eaffdca3a4ce6b73c8c480ba2484975e0..09707ebc42196c75974872130751638e78ae628e 100644 (file)
 
 // 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;
index d5d7cb139813976a58a3b0c646d4ea987713584c..7b5c92b5a9dfd0d5f0f9b399c925ff507737f813 100644 (file)
@@ -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">
index 826f218ce0aa195c501028f9f2cd46c349fbb538..60b126a7b5ce8adacce446cda91a75fecb086aec 100644 (file)
@@ -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])
index f8a22ada846f343a77440839d76480347e66e7f5..cd32a406b65d6173323c892fe761a5c49c973369 100644 (file)
@@ -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">
                         @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>
 
             <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">
index 3b19b55c8069e1dd629051f007dee26d6844000d..1f2f9c8faa646ac043830b575efbb59b3b510681 100644 (file)
@@ -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">&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>
         <h1>Chapter Restrictions</h1>
         @include('form/restriction-form', ['model' => $chapter])
index ac36a6b3ee6d3a04d5b3faa34b15df61d6974e53..f053edc1c68e39d0441c2f0764802ed347ac3050 100644 (file)
                 <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
                 </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>
index 191cf758faeabe7f18d83a728d2e389304006614..a9294c7cf4cf80c0f909e03cc011c29e257611f9 100644 (file)
@@ -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>
index cb42497b4dd2aeb660a1787398b8f0357003c26b..d2fa239826b6d7a346bd66c554362e3a62e89286 100644 (file)
@@ -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>
\ No newline at end of file
index 63ad1fade616f1c578c66150558ad2a81e85a570..d094abc7184adeca9ea61bb40c93b47ea4ef2d06 100644 (file)
@@ -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">&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>
         <h1>Page Restrictions</h1>
         @include('form/restriction-form', ['model' => $page])
index f84d1963dd561474dc567d7c500aab2a47bbf136..286d443874001fd7c22bb2017574f193790bb163 100644 (file)
                 </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>
index ed0e3dd913470a86b0bd2ba691702caa37c8518e..fafb9bed28f34923c22cac6529aafb71655453a5 100644 (file)
@@ -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>
         <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>
     </div>
 
 </div>
+
+<a href="/settings/roles" class="button muted">Cancel</a>
 <button type="submit" class="button pos">Save Role</button>
\ No newline at end of file
index 601c6533e53a2640f8f5df03ff840e1271457827..8f92a5eba4a12c45ef4713d4325be6769ec1fc7f 100644 (file)
@@ -4,7 +4,7 @@
 
     @include('settings/navbar', ['selected' => 'roles'])
 
-    <div class="container">
+    <div class="container small">
 
         <h1>User Roles</h1>
 
diff --git a/tests/RestrictionsTest.php b/tests/RestrictionsTest.php
new file mode 100644 (file)
index 0000000..40b5a76
--- /dev/null
@@ -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);
+    }
+
+}
index 840fe0d0894c62a648e2694ce1e17497f9f9571c..567dc93eca876bc7426bfee781e80f35c1526ca5 100644 (file)
@@ -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