]> BookStack Code Mirror - bookstack/commitdiff
Finished initial implementation of custom role system
authorDan Brown <redacted>
Sat, 27 Feb 2016 19:24:42 +0000 (19:24 +0000)
committerDan Brown <redacted>
Sat, 27 Feb 2016 19:24:42 +0000 (19:24 +0000)
37 files changed:
app/Entity.php
app/Http/Controllers/BookController.php
app/Http/Controllers/ChapterController.php
app/Http/Controllers/Controller.php
app/Http/Controllers/ImageController.php
app/Http/Controllers/PageController.php
app/Http/Controllers/PermissionController.php
app/Http/Controllers/SettingController.php
app/Http/Controllers/UserController.php
app/Http/routes.php
app/Image.php
app/Ownable.php
app/Permission.php
app/Repos/UserRepo.php
app/Role.php
app/User.php
app/helpers.php
database/migrations/2016_02_27_120329_update_permissions_and_roles.php [new file with mode: 0644]
resources/views/base.blade.php
resources/views/books/index.blade.php
resources/views/books/show.blade.php
resources/views/chapters/show.blade.php
resources/views/form/role-checkboxes.blade.php [new file with mode: 0644]
resources/views/pages/show.blade.php
resources/views/settings/index.blade.php
resources/views/settings/roles/checkbox.blade.php [new file with mode: 0644]
resources/views/settings/roles/create.blade.php [new file with mode: 0644]
resources/views/settings/roles/delete.blade.php [new file with mode: 0644]
resources/views/settings/roles/edit.blade.php
resources/views/settings/roles/form.blade.php [new file with mode: 0644]
resources/views/settings/roles/index.blade.php
resources/views/users/forms/ldap.blade.php
resources/views/users/forms/standard.blade.php
resources/views/users/index.blade.php
tests/Auth/AuthTest.php
tests/RolesTest.php [new file with mode: 0644]
tests/TestCase.php

index 42323628ac8c3f09319ac34aa681a6580ff34339..08aa14638a4bdbe9272584ca6ca2e4fa37ac6097 100644 (file)
@@ -1,14 +1,9 @@
-<?php
+<?php namespace BookStack;
 
-namespace BookStack;
 
-use Illuminate\Database\Eloquent\Model;
-
-abstract class Entity extends Model
+abstract class Entity extends Ownable
 {
 
-    use Ownable;
-
     /**
      * Compares this entity to another given entity.
      * Matches by comparing class and id.
@@ -72,16 +67,7 @@ abstract class Entity extends Model
     }
 
     /**
-     * Gets the class name.
-     * @return string
-     */
-    public static function getClassName()
-    {
-        return strtolower(array_slice(explode('\\', static::class), -1, 1)[0]);
-    }
-
-    /**
-     *Gets a limited-length version of the entities name.
+     * Gets a limited-length version of the entities name.
      * @param int $length
      * @return string
      */
index d577a85b134dcd060eebf72b2be744bc8914766e..c1320088dc68efa9e57ad4f8bb41da907f4b7fdb 100644 (file)
@@ -22,8 +22,8 @@ class BookController extends Controller
 
     /**
      * BookController constructor.
-     * @param BookRepo    $bookRepo
-     * @param PageRepo    $pageRepo
+     * @param BookRepo $bookRepo
+     * @param PageRepo $pageRepo
      * @param ChapterRepo $chapterRepo
      */
     public function __construct(BookRepo $bookRepo, PageRepo $pageRepo, ChapterRepo $chapterRepo)
@@ -55,7 +55,7 @@ class BookController extends Controller
      */
     public function create()
     {
-        $this->checkPermission('book-create');
+        $this->checkPermission('book-create-all');
         $this->setPageTitle('Create New Book');
         return view('books/create');
     }
@@ -68,9 +68,9 @@ class BookController extends Controller
      */
     public function store(Request $request)
     {
-        $this->checkPermission('book-create');
+        $this->checkPermission('book-create-all');
         $this->validate($request, [
-            'name'        => 'required|string|max:255',
+            'name' => 'required|string|max:255',
             'description' => 'string|max:1000'
         ]);
         $book = $this->bookRepo->newFromInput($request->all());
@@ -105,8 +105,8 @@ class BookController extends Controller
      */
     public function edit($slug)
     {
-        $this->checkPermission('book-update');
         $book = $this->bookRepo->getBySlug($slug);
+        $this->checkOwnablePermission('book-update', $book);
         $this->setPageTitle('Edit Book ' . $book->getShortName());
         return view('books/edit', ['book' => $book, 'current' => $book]);
     }
@@ -120,10 +120,10 @@ class BookController extends Controller
      */
     public function update(Request $request, $slug)
     {
-        $this->checkPermission('book-update');
         $book = $this->bookRepo->getBySlug($slug);
+        $this->checkOwnablePermission('book-update', $book);
         $this->validate($request, [
-            'name'        => 'required|string|max:255',
+            'name' => 'required|string|max:255',
             'description' => 'string|max:1000'
         ]);
         $book->fill($request->all());
@@ -141,8 +141,8 @@ class BookController extends Controller
      */
     public function showDelete($bookSlug)
     {
-        $this->checkPermission('book-delete');
         $book = $this->bookRepo->getBySlug($bookSlug);
+        $this->checkOwnablePermission('book-delete', $book);
         $this->setPageTitle('Delete Book ' . $book->getShortName());
         return view('books/delete', ['book' => $book, 'current' => $book]);
     }
@@ -154,8 +154,8 @@ class BookController extends Controller
      */
     public function sort($bookSlug)
     {
-        $this->checkPermission('book-update');
         $book = $this->bookRepo->getBySlug($bookSlug);
+        $this->checkOwnablePermission('book-update', $book);
         $bookChildren = $this->bookRepo->getChildren($book);
         $books = $this->bookRepo->getAll(false);
         $this->setPageTitle('Sort Book ' . $book->getShortName());
@@ -184,8 +184,8 @@ class BookController extends Controller
      */
     public function saveSort($bookSlug, Request $request)
     {
-        $this->checkPermission('book-update');
         $book = $this->bookRepo->getBySlug($bookSlug);
+        $this->checkOwnablePermission('book-update', $book);
 
         // Return if no map sent
         if (!$request->has('sort-tree')) {
@@ -229,8 +229,8 @@ class BookController extends Controller
      */
     public function destroy($bookSlug)
     {
-        $this->checkPermission('book-delete');
         $book = $this->bookRepo->getBySlug($bookSlug);
+        $this->checkOwnablePermission('book-delete', $book);
         Activity::addMessage('book_delete', 0, $book->name);
         Activity::removeEntity($book);
         $this->bookRepo->destroyBySlug($bookSlug);
index fc13e8b58b777a24584cd59f01a6020fa74eee95..3b4780f8dfa1204fdb4178f2204b81cf8e1fee64 100644 (file)
@@ -1,13 +1,8 @@
-<?php
-
-namespace BookStack\Http\Controllers;
+<?php namespace BookStack\Http\Controllers;
 
 use Activity;
 use Illuminate\Http\Request;
-
-use Illuminate\Support\Facades\Auth;
 use BookStack\Http\Requests;
-use BookStack\Http\Controllers\Controller;
 use BookStack\Repos\BookRepo;
 use BookStack\Repos\ChapterRepo;
 use Views;
@@ -30,7 +25,6 @@ class ChapterController extends Controller
         parent::__construct();
     }
 
-
     /**
      * Show the form for creating a new chapter.
      * @param $bookSlug
@@ -38,8 +32,8 @@ class ChapterController extends Controller
      */
     public function create($bookSlug)
     {
-        $this->checkPermission('chapter-create');
         $book = $this->bookRepo->getBySlug($bookSlug);
+        $this->checkOwnablePermission('chapter-create', $book);
         $this->setPageTitle('Create New Chapter');
         return view('chapters/create', ['book' => $book, 'current' => $book]);
     }
@@ -52,12 +46,13 @@ class ChapterController extends Controller
      */
     public function store($bookSlug, Request $request)
     {
-        $this->checkPermission('chapter-create');
         $this->validate($request, [
             'name' => 'required|string|max:255'
         ]);
 
         $book = $this->bookRepo->getBySlug($bookSlug);
+        $this->checkOwnablePermission('chapter-create', $book);
+
         $chapter = $this->chapterRepo->newFromInput($request->all());
         $chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id);
         $chapter->priority = $this->bookRepo->getNewPriority($book);
@@ -92,9 +87,9 @@ class ChapterController extends Controller
      */
     public function edit($bookSlug, $chapterSlug)
     {
-        $this->checkPermission('chapter-update');
         $book = $this->bookRepo->getBySlug($bookSlug);
         $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
+        $this->checkOwnablePermission('chapter-update', $chapter);
         $this->setPageTitle('Edit Chapter' . $chapter->getShortName());
         return view('chapters/edit', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]);
     }
@@ -108,9 +103,9 @@ class ChapterController extends Controller
      */
     public function update(Request $request, $bookSlug, $chapterSlug)
     {
-        $this->checkPermission('chapter-update');
         $book = $this->bookRepo->getBySlug($bookSlug);
         $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
+        $this->checkOwnablePermission('chapter-update', $chapter);
         $chapter->fill($request->all());
         $chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id, $chapter->id);
         $chapter->updated_by = auth()->user()->id;
@@ -127,9 +122,9 @@ class ChapterController extends Controller
      */
     public function showDelete($bookSlug, $chapterSlug)
     {
-        $this->checkPermission('chapter-delete');
         $book = $this->bookRepo->getBySlug($bookSlug);
         $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
+        $this->checkOwnablePermission('chapter-delete', $chapter);
         $this->setPageTitle('Delete Chapter' . $chapter->getShortName());
         return view('chapters/delete', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]);
     }
@@ -142,9 +137,9 @@ class ChapterController extends Controller
      */
     public function destroy($bookSlug, $chapterSlug)
     {
-        $this->checkPermission('chapter-delete');
         $book = $this->bookRepo->getBySlug($bookSlug);
         $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
+        $this->checkOwnablePermission('chapter-delete', $chapter);
         Activity::addMessage('chapter_delete', $book->id, $chapter->name);
         $this->chapterRepo->destroy($chapter);
         return redirect($book->getUrl());
index 654fed5380af518e538b0c3850f0634b784a729f..fce479af04e7d1294c2cf17eeb31b2bc40f6ec9d 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace BookStack\Http\Controllers;
 
+use BookStack\Ownable;
 use HttpRequestException;
 use Illuminate\Foundation\Bus\DispatchesJobs;
 use Illuminate\Http\Exception\HttpResponseException;
@@ -61,7 +62,7 @@ abstract class Controller extends BaseController
     }
 
     /**
-     * On a permission error redirect to home and display
+     * On a permission error redirect to home and display.
      * the error as a notification.
      */
     protected function showPermissionError()
@@ -74,20 +75,31 @@ abstract class Controller extends BaseController
 
     /**
      * Checks for a permission.
-     *
-     * @param $permissionName
+     * @param string $permissionName
      * @return bool|\Illuminate\Http\RedirectResponse
      */
     protected function checkPermission($permissionName)
     {
         if (!$this->currentUser || !$this->currentUser->can($permissionName)) {
-            dd($this->currentUser);
             $this->showPermissionError();
         }
-
         return true;
     }
 
+    /**
+     * Check the current user's permissions against an ownable item.
+     * @param $permission
+     * @param Ownable $ownable
+     * @return bool
+     */
+    protected function checkOwnablePermission($permission, Ownable $ownable)
+    {
+        $permissionBaseName = strtolower($permission) . '-';
+        if (userCan($permissionBaseName . 'all')) return true;
+        if (userCan($permissionBaseName . 'own') && $ownable->createdBy->id === $this->currentUser->id) return true;
+        $this->showPermissionError();
+    }
+
     /**
      * Check if a user has a permission or bypass if the callback is true.
      * @param $permissionName
index 3fff28d3ba493cd31ef4acc06ac32f91ab78ae11..48e89ee41d7d8858f72e8585deb6096c5881c22b 100644 (file)
@@ -64,7 +64,7 @@ class ImageController extends Controller
      */
     public function uploadByType($type, Request $request)
     {
-        $this->checkPermission('image-create');
+        $this->checkPermission('image-create-all');
         $this->validate($request, [
             'file' => 'image|mimes:jpeg,gif,png'
         ]);
@@ -90,7 +90,7 @@ class ImageController extends Controller
      */
     public function getThumbnail($id, $width, $height, $crop)
     {
-        $this->checkPermission('image-create');
+        $this->checkPermission('image-create-all');
         $image = $this->imageRepo->getById($id);
         $thumbnailUrl = $this->imageRepo->getThumbnail($image, $width, $height, $crop == 'false');
         return response()->json(['url' => $thumbnailUrl]);
@@ -104,11 +104,11 @@ class ImageController extends Controller
      */
     public function update($imageId, Request $request)
     {
-        $this->checkPermission('image-update');
         $this->validate($request, [
             'name' => 'required|min:2|string'
         ]);
         $image = $this->imageRepo->getById($imageId);
+        $this->checkOwnablePermission('image-update', $image);
         $image = $this->imageRepo->updateImageDetails($image, $request->all());
         return response()->json($image);
     }
@@ -123,8 +123,8 @@ class ImageController extends Controller
      */
     public function destroy(PageRepo $pageRepo, Request $request, $id)
     {
-        $this->checkPermission('image-delete');
         $image = $this->imageRepo->getById($id);
+        $this->checkOwnablePermission('image-delete', $image);
 
         // Check if this image is used on any pages
         $isForced = ($request->has('force') && ($request->get('force') === 'true') || $request->get('force') === true);
index e78ae13e4c2972067ef9ed4232f6771426ab852d..ac968159bd780280aaf882e11e58487cb860728d 100644 (file)
@@ -1,12 +1,8 @@
-<?php
-
-namespace BookStack\Http\Controllers;
+<?php namespace BookStack\Http\Controllers;
 
 use Activity;
 use BookStack\Services\ExportService;
 use Illuminate\Http\Request;
-
-use Illuminate\Support\Facades\Auth;
 use BookStack\Http\Requests;
 use BookStack\Repos\BookRepo;
 use BookStack\Repos\ChapterRepo;
@@ -40,7 +36,6 @@ class PageController extends Controller
 
     /**
      * Show the form for creating a new page.
-     *
      * @param      $bookSlug
      * @param bool $chapterSlug
      * @return Response
@@ -48,23 +43,22 @@ class PageController extends Controller
      */
     public function create($bookSlug, $chapterSlug = false)
     {
-        $this->checkPermission('page-create');
         $book = $this->bookRepo->getBySlug($bookSlug);
         $chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : false;
+        $parent = $chapter ? $chapter : $book;
+        $this->checkOwnablePermission('page-create', $parent);
         $this->setPageTitle('Create New Page');
         return view('pages/create', ['book' => $book, 'chapter' => $chapter]);
     }
 
     /**
      * Store a newly created page in storage.
-     *
      * @param  Request $request
      * @param          $bookSlug
      * @return Response
      */
     public function store(Request $request, $bookSlug)
     {
-        $this->checkPermission('page-create');
         $this->validate($request, [
             'name'   => 'required|string|max:255'
         ]);
@@ -72,6 +66,8 @@ class PageController extends Controller
         $input = $request->all();
         $book = $this->bookRepo->getBySlug($bookSlug);
         $chapterId = ($request->has('chapter') && $this->chapterRepo->idExists($request->get('chapter'))) ? $request->get('chapter') : null;
+        $parent = $chapterId !== null ? $this->chapterRepo->getById($chapterId) : $book;
+        $this->checkOwnablePermission('page-create', $parent);
         $input['priority'] = $this->bookRepo->getNewPriority($book);
 
         $page = $this->pageRepo->saveNew($input, $book, $chapterId);
@@ -84,7 +80,6 @@ class PageController extends Controller
      * Display the specified page.
      * If the page is not found via the slug the
      * revisions are searched for a match.
-     *
      * @param $bookSlug
      * @param $pageSlug
      * @return Response
@@ -109,23 +104,21 @@ class PageController extends Controller
 
     /**
      * Show the form for editing the specified page.
-     *
      * @param $bookSlug
      * @param $pageSlug
      * @return Response
      */
     public function edit($bookSlug, $pageSlug)
     {
-        $this->checkPermission('page-update');
         $book = $this->bookRepo->getBySlug($bookSlug);
         $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
+        $this->checkOwnablePermission('page-update', $page);
         $this->setPageTitle('Editing Page ' . $page->getShortName());
         return view('pages/edit', ['page' => $page, 'book' => $book, 'current' => $page]);
     }
 
     /**
      * Update the specified page in storage.
-     *
      * @param  Request $request
      * @param          $bookSlug
      * @param          $pageSlug
@@ -133,12 +126,12 @@ class PageController extends Controller
      */
     public function update(Request $request, $bookSlug, $pageSlug)
     {
-        $this->checkPermission('page-update');
         $this->validate($request, [
             'name'   => 'required|string|max:255'
         ]);
         $book = $this->bookRepo->getBySlug($bookSlug);
         $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
+        $this->checkOwnablePermission('page-update', $page);
         $this->pageRepo->updatePage($page, $book->id, $request->all());
         Activity::add($page, 'page_update', $book->id);
         return redirect($page->getUrl());
@@ -164,9 +157,9 @@ class PageController extends Controller
      */
     public function showDelete($bookSlug, $pageSlug)
     {
-        $this->checkPermission('page-delete');
         $book = $this->bookRepo->getBySlug($bookSlug);
         $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
+        $this->checkOwnablePermission('page-delete', $page);
         $this->setPageTitle('Delete Page ' . $page->getShortName());
         return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]);
     }
@@ -181,9 +174,9 @@ class PageController extends Controller
      */
     public function destroy($bookSlug, $pageSlug)
     {
-        $this->checkPermission('page-delete');
         $book = $this->bookRepo->getBySlug($bookSlug);
         $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
+        $this->checkOwnablePermission('page-delete', $page);
         Activity::addMessage('page_delete', $book->id, $page->name);
         $this->pageRepo->destroy($page);
         return redirect($book->getUrl());
@@ -229,9 +222,9 @@ class PageController extends Controller
      */
     public function restoreRevision($bookSlug, $pageSlug, $revisionId)
     {
-        $this->checkPermission('page-update');
         $book = $this->bookRepo->getBySlug($bookSlug);
         $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
+        $this->checkOwnablePermission('page-update', $page);
         $page = $this->pageRepo->restoreRevision($page, $book, $revisionId);
         Activity::add($page, 'page_restore', $book->id);
         return redirect($page->getUrl());
index 69e2619b69aa6186b364f9630af4af35e0bb66b0..8cc14fc7aa32624fdc8a4d8c6e2413c0aac23822 100644 (file)
@@ -2,26 +2,27 @@
 
 namespace BookStack\Http\Controllers;
 
+use BookStack\Permission;
 use BookStack\Role;
-use BookStack\User;
 use Illuminate\Http\Request;
-
 use BookStack\Http\Requests;
-use BookStack\Http\Controllers\Controller;
 
 class PermissionController extends Controller
 {
 
     protected $role;
+    protected $permission;
 
     /**
      * PermissionController constructor.
-     * @param $role
-     * @param $user
+     * @param Role $role
+     * @param Permission $permission
+     * @internal param $user
      */
-    public function __construct(Role $role)
+    public function __construct(Role $role, Permission $permission)
     {
         $this->role = $role;
+        $this->permission = $permission;
         parent::__construct();
     }
 
@@ -30,11 +31,54 @@ class PermissionController extends Controller
      */
     public function listRoles()
     {
-        $this->checkPermission('settings-update');
+        $this->checkPermission('user-roles-manage');
         $roles = $this->role->all();
         return view('settings/roles/index', ['roles' => $roles]);
     }
 
+    /**
+     * Show the form to create a new role
+     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+     */
+    public function createRole()
+    {
+        $this->checkPermission('user-roles-manage');
+        return view('settings/roles/create');
+    }
+
+    /**
+     * Store a new role in the system.
+     * @param Request $request
+     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+     */
+    public function storeRole(Request $request)
+    {
+        $this->checkPermission('user-roles-manage');
+        $this->validate($request, [
+            'display_name' => 'required|min:3|max:200',
+            'description' => 'max:250'
+        ]);
+
+        $role = $this->role->newInstance($request->all());
+        $role->name = str_replace(' ', '-', strtolower($request->get('display_name')));
+        // Prevent duplicate names
+        while ($this->role->where('name', '=', $role->name)->count() > 0) {
+            $role->name .= strtolower(str_random(2));
+        }
+        $role->save();
+
+        if ($request->has('permissions')) {
+            $permissionsNames = array_keys($request->get('permissions'));
+            $permissions = $this->permission->whereIn('name', $permissionsNames)->pluck('id')->toArray();
+            $role->permissions()->sync($permissions);
+        } else {
+            $role->permissions()->sync([]);
+        }
+
+        session()->flash('success', 'Role successfully created');
+        return redirect('/settings/roles');
+    }
+
     /**
      * Show the form for editing a user role.
      * @param $id
@@ -42,8 +86,97 @@ class PermissionController extends Controller
      */
     public function editRole($id)
     {
-        $this->checkPermission('settings-update');
+        $this->checkPermission('user-roles-manage');
         $role = $this->role->findOrFail($id);
         return view('settings/roles/edit', ['role' => $role]);
     }
+
+    /**
+     * Updates a user role.
+     * @param $id
+     * @param Request $request
+     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+     */
+    public function updateRole($id, Request $request)
+    {
+        $this->checkPermission('user-roles-manage');
+        $this->validate($request, [
+            'display_name' => 'required|min:3|max:200',
+            'description' => 'max:250'
+        ]);
+
+        $role = $this->role->findOrFail($id);
+        if ($request->has('permissions')) {
+            $permissionsNames = array_keys($request->get('permissions'));
+            $permissions = $this->permission->whereIn('name', $permissionsNames)->pluck('id')->toArray();
+            $role->permissions()->sync($permissions);
+        } else {
+            $role->permissions()->sync([]);
+        }
+
+        // Ensure admin account always has all permissions
+        if ($role->name === 'admin') {
+            $permissions = $this->permission->all()->pluck('id')->toArray();
+            $role->permissions()->sync($permissions);
+        }
+
+        $role->fill($request->all());
+        $role->save();
+
+        session()->flash('success', 'Role successfully updated');
+        return redirect('/settings/roles');
+    }
+
+    /**
+     * Show the view to delete a role.
+     * Offers the chance to migrate users.
+     * @param $id
+     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+     */
+    public function showDeleteRole($id)
+    {
+        $this->checkPermission('user-roles-manage');
+        $role = $this->role->findOrFail($id);
+        $roles = $this->role->where('id', '!=', $id)->get();
+        $blankRole = $this->role->newInstance(['display_name' => 'Don\'t migrate users']);
+        $roles->prepend($blankRole);
+        return view('settings/roles/delete', ['role' => $role, 'roles' => $roles]);
+    }
+
+    /**
+     * Delete a role from the system,
+     * Migrate from a previous role if set.
+     * @param $id
+     * @param Request $request
+     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+     */
+    public function deleteRole($id, Request $request)
+    {
+        $this->checkPermission('user-roles-manage');
+        $role = $this->role->findOrFail($id);
+
+        // Prevent deleting admin role
+        if ($role->name === 'admin') {
+            session()->flash('error', 'The admin role cannot be deleted');
+            return redirect()->back();
+        }
+
+        if ($role->id == \Setting::get('registration-role')) {
+            session()->flash('error', 'This role cannot be deleted while set as the default registration role.');
+            return redirect()->back();
+        }
+
+        if ($request->has('migration_role_id')) {
+            $newRole = $this->role->find($request->get('migration_role_id'));
+            if ($newRole) {
+                $users = $role->users->pluck('id')->toArray();
+                $newRole->users()->sync($users);
+            }
+        }
+
+        $role->delete();
+
+        session()->flash('success', 'Role successfully deleted');
+        return redirect('/settings/roles');
+    }
 }
index 1739e0b530cb9dac9793ff4a91fd2d3ae3c7cb05..c43e6e399df24ec369bb792f777adc663b64b58c 100644 (file)
@@ -17,7 +17,7 @@ class SettingController extends Controller
      */
     public function index()
     {
-        $this->checkPermission('settings-update');
+        $this->checkPermission('settings-manage');
         $this->setPageTitle('Settings');
         return view('settings/index');
     }
@@ -32,7 +32,7 @@ class SettingController extends Controller
     public function update(Request $request)
     {
         $this->preventAccessForDemoUsers();
-        $this->checkPermission('settings-update');
+        $this->checkPermission('settings-manage');
 
         // Cycles through posted settings and update them
         foreach($request->all() as $name => $value) {
index 55ca5be19899bbdbc93927a7354548478a973ec9..1207c87f1b02330dea430291c4afc7d13d5d4e87 100644 (file)
@@ -35,7 +35,7 @@ class UserController extends Controller
      */
     public function index()
     {
-        $users = $this->user->all();
+        $users = $this->userRepo->getAllUsers();
         $this->setPageTitle('Users');
         return view('users/index', ['users' => $users]);
     }
@@ -46,7 +46,7 @@ class UserController extends Controller
      */
     public function create()
     {
-        $this->checkPermission('user-create');
+        $this->checkPermission('users-manage');
         $authMethod = config('auth.method');
         return view('users/create', ['authMethod' => $authMethod]);
     }
@@ -58,11 +58,10 @@ class UserController extends Controller
      */
     public function store(Request $request)
     {
-        $this->checkPermission('user-create');
+        $this->checkPermission('users-manage');
         $validationRules = [
             'name'             => 'required',
-            'email'            => 'required|email|unique:users,email',
-            'role'             => 'required|exists:roles,id'
+            'email'            => 'required|email|unique:users,email'
         ];
 
         $authMethod = config('auth.method');
@@ -84,7 +83,11 @@ class UserController extends Controller
         }
 
         $user->save();
-        $user->attachRoleId($request->get('role'));
+
+        if ($request->has('roles')) {
+            $roles = $request->get('roles');
+            $user->roles()->sync($roles);
+        }
 
         // Get avatar from gravatar and save
         if (!config('services.disable_services')) {
@@ -104,7 +107,7 @@ class UserController extends Controller
      */
     public function edit($id, SocialAuthService $socialAuthService)
     {
-        $this->checkPermissionOr('user-update', function () use ($id) {
+        $this->checkPermissionOr('users-manage', function () use ($id) {
             return $this->currentUser->id == $id;
         });
 
@@ -125,7 +128,7 @@ class UserController extends Controller
     public function update(Request $request, $id)
     {
         $this->preventAccessForDemoUsers();
-        $this->checkPermissionOr('user-update', function () use ($id) {
+        $this->checkPermissionOr('users-manage', function () use ($id) {
             return $this->currentUser->id == $id;
         });
 
@@ -133,8 +136,7 @@ class UserController extends Controller
             'name'             => 'min:2',
             'email'            => 'min:2|email|unique:users,email,' . $id,
             'password'         => 'min:5|required_with:password_confirm',
-            'password-confirm' => 'same:password|required_with:password',
-            'role'             => 'exists:roles,id'
+            'password-confirm' => 'same:password|required_with:password'
         ], [
             'password-confirm.required_with' => 'Password confirmation required'
         ]);
@@ -143,8 +145,9 @@ class UserController extends Controller
         $user->fill($request->all());
 
         // Role updates
-        if ($this->currentUser->can('user-update') && $request->has('role')) {
-            $user->attachRoleId($request->get('role'));
+        if (userCan('users-manage') && $request->has('roles')) {
+            $roles = $request->get('roles');
+            $user->roles()->sync($roles);
         }
 
         // Password updates
@@ -154,11 +157,12 @@ class UserController extends Controller
         }
 
         // External auth id updates
-        if ($this->currentUser->can('user-update') && $request->has('external_auth_id')) {
+        if ($this->currentUser->can('users-manage') && $request->has('external_auth_id')) {
             $user->external_auth_id = $request->get('external_auth_id');
         }
 
         $user->save();
+        session()->flash('success', 'User successfully updated');
         return redirect('/settings/users');
     }
 
@@ -169,7 +173,7 @@ class UserController extends Controller
      */
     public function delete($id)
     {
-        $this->checkPermissionOr('user-delete', function () use ($id) {
+        $this->checkPermissionOr('users-manage', function () use ($id) {
             return $this->currentUser->id == $id;
         });
 
@@ -186,7 +190,7 @@ class UserController extends Controller
     public function destroy($id)
     {
         $this->preventAccessForDemoUsers();
-        $this->checkPermissionOr('user-delete', function () use ($id) {
+        $this->checkPermissionOr('users-manage', function () use ($id) {
             return $this->currentUser->id == $id;
         });
 
index eea0a03370f293985e76246e8a8cacd5b5737c2f..a1c73764246373be546f0af0c40625a495727de9 100644 (file)
@@ -99,7 +99,12 @@ Route::group(['middleware' => 'auth'], function () {
 
         // Roles
         Route::get('/roles', 'PermissionController@listRoles');
+        Route::get('/roles/new', 'PermissionController@createRole');
+        Route::post('/roles/new', 'PermissionController@storeRole');
+        Route::get('/roles/delete/{id}', 'PermissionController@showDeleteRole');
+        Route::delete('/roles/delete/{id}', 'PermissionController@deleteRole');
         Route::get('/roles/{id}', 'PermissionController@editRole');
+        Route::put('/roles/{id}', 'PermissionController@updateRole');
     });
 
 });
index 3ac084d8fab08d41a637aa9ba6747c495c2fd037..ad23a077adc5ba12cb652ad55043fa3289101f4d 100644 (file)
@@ -1,14 +1,9 @@
-<?php
+<?php namespace BookStack;
 
-namespace BookStack;
-
-
-use Illuminate\Database\Eloquent\Model;
 use Images;
 
-class Image extends Model
+class Image extends Ownable
 {
-    use Ownable;
 
     protected $fillable = ['name'];
 
index d6505b7460440f95f1bf9e8eecd09ae6775129bd..28d55c2bb98156cb08a4b6e2e44842270f4bbe99 100644 (file)
@@ -1,7 +1,8 @@
 <?php namespace BookStack;
 
+use Illuminate\Database\Eloquent\Model;
 
-trait Ownable
+abstract class Ownable extends Model
 {
     /**
      * Relation for the user that created this entity.
@@ -20,4 +21,14 @@ trait Ownable
     {
         return $this->belongsTo('BookStack\User', 'updated_by');
     }
+
+    /**
+     * Gets the class name.
+     * @return string
+     */
+    public static function getClassName()
+    {
+        return strtolower(array_slice(explode('\\', static::class), -1, 1)[0]);
+    }
+
 }
\ No newline at end of file
index 6859ed56ed9c26e0134b2696b88f9aaa02d91a9a..794df01ab8338e624f3f3b03c6521d6b7e3217ae 100644 (file)
@@ -13,4 +13,14 @@ class Permission extends Model
     {
         return $this->belongsToMany('BookStack\Permissions');
     }
+
+    /**
+     * Get the permission object by name.
+     * @param $roleName
+     * @return mixed
+     */
+    public static function getByName($name)
+    {
+        return static::where('name', '=', $name)->first();
+    }
 }
index 48541a51aa341aeb576dab3ef91056bee8d1b39b..15813b3e18b4e204d4a6e45e84cab56477754f7b 100644 (file)
@@ -42,6 +42,15 @@ class UserRepo
         return $this->user->findOrFail($id);
     }
 
+    /**
+     * Get all the users with their permissions.
+     * @return \Illuminate\Database\Eloquent\Builder|static
+     */
+    public function getAllUsers()
+    {
+        return $this->user->with('roles', 'avatar')->orderBy('name', 'asc')->get();
+    }
+
     /**
      * Creates a new user and attaches a role to them.
      * @param array $data
@@ -69,7 +78,7 @@ class UserRepo
     public function attachDefaultRole($user)
     {
         $roleId = Setting::get('registration-role');
-        if ($roleId === false) $roleId = $this->role->getDefault()->id;
+        if ($roleId === false) $roleId = $this->role->first()->id;
         $user->attachRoleId($roleId);
     }
 
@@ -80,15 +89,10 @@ class UserRepo
      */
     public function isOnlyAdmin(User $user)
     {
-        if ($user->role->name != 'admin') {
-            return false;
-        }
-
-        $adminRole = $this->role->where('name', '=', 'admin')->first();
-        if (count($adminRole->users) > 1) {
-            return false;
-        }
+        if (!$user->roles->pluck('name')->contains('admin')) return false;
 
+        $adminRole = $this->role->getRole('admin');
+        if ($adminRole->users->count() > 1) return false;
         return true;
     }
 
index 3d93bf7702b6300ee784949aeab989e949981858..8d5ed7d6678e4266ff94e8666150cd13d0d32c7b 100644 (file)
@@ -6,6 +6,8 @@ use Illuminate\Database\Eloquent\Model;
 
 class Role extends Model
 {
+
+    protected $fillable = ['display_name', 'description'];
     /**
      * Sets the default role name for newly registered users.
      * @var string
@@ -28,6 +30,15 @@ class Role extends Model
         return $this->belongsToMany('BookStack\Permission');
     }
 
+    /**
+     * Check if this role has a permission.
+     * @param $permission
+     */
+    public function hasPermission($permission)
+    {
+        return $this->permissions->pluck('name')->contains($permission);
+    }
+
     /**
      * Add a permission to this role.
      * @param Permission $permission
index c551020788153a32594752c7e327c965f82d9c3e..b062aa78f20dc11a05409132c82547c2ba57b12a 100644 (file)
@@ -14,21 +14,18 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
 
     /**
      * The database table used by the model.
-     *
      * @var string
      */
     protected $table = 'users';
 
     /**
      * The attributes that are mass assignable.
-     *
      * @var array
      */
     protected $fillable = ['name', 'email', 'image_id'];
 
     /**
      * The attributes excluded from the model's JSON form.
-     *
      * @var array
      */
     protected $hidden = ['password', 'remember_token'];
@@ -50,10 +47,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
         ]);
     }
 
-    /**
-     * Permissions and roles
-     */
-
     /**
      * The roles that belong to the user.
      */
@@ -62,21 +55,29 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
         return $this->belongsToMany('BookStack\Role');
     }
 
-    public function getRoleAttribute()
+    /**
+     * Check if the user has a role.
+     * @param $role
+     * @return mixed
+     */
+    public function hasRole($role)
     {
-        return $this->roles()->with('permissions')->first();
+        return $this->roles->pluck('name')->contains($role);
     }
 
     /**
-     * Loads the user's permissions from their role.
+     * Get all permissions belonging to a the current user.
+     * @return \Illuminate\Database\Eloquent\Relations\HasManyThrough
      */
-    private function loadPermissions()
+    public function permissions()
     {
-        if (isset($this->permissions)) return;
+        if(isset($this->permissions)) return $this->permissions;
         $this->load('roles.permissions');
-        $permissions = $this->roles[0]->permissions;
-        $permissionsArray = $permissions->pluck('name')->all();
-        $this->permissions = $permissionsArray;
+        $permissions = $this->roles->map(function($role) {
+            return $role->permissions;
+        })->flatten()->unique();
+        $this->permissions = $permissions;
+        return $permissions;
     }
 
     /**
@@ -86,11 +87,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
      */
     public function can($permissionName)
     {
-        if ($this->email == 'guest') {
-            return false;
-        }
-        $this->loadPermissions();
-        return array_search($permissionName, $this->permissions) !== false;
+        if ($this->email === 'guest') return false;
+        return $this->permissions()->pluck('name')->contains($permissionName);
     }
 
     /**
@@ -113,7 +111,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
 
     /**
      * Get the social account associated with this user.
-     *
      * @return \Illuminate\Database\Eloquent\Relations\HasMany
      */
     public function socialAccounts()
@@ -138,8 +135,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
 
     /**
      * Returns the user's avatar,
-     * Uses Gravatar as the avatar service.
-     *
      * @param int $size
      * @return string
      */
index f25a8f765d4e7694d92b276e36a2e4a0b96c356f..db65407c73729a9c85669c7fd7e57fdc2b3eedeb 100644 (file)
@@ -27,4 +27,24 @@ if (! function_exists('versioned_asset')) {
 
         throw new InvalidArgumentException("File {$file} not defined in asset manifest.");
     }
+}
+
+/**
+ * Check if the current user has a permission.
+ * If an ownable element is passed in the permissions are checked against
+ * that particular item.
+ * @param $permission
+ * @param \BookStack\Ownable $ownable
+ * @return mixed
+ */
+function userCan($permission, \BookStack\Ownable $ownable = null)
+{
+    if ($ownable === null) {
+        return auth()->user() && auth()->user()->can($permission);
+    }
+
+    $permissionBaseName = strtolower($permission) . '-';
+    if (userCan($permissionBaseName . 'all')) return true;
+    if (userCan($permissionBaseName . 'own') && $ownable->createdBy->id === auth()->user()->id) return true;
+    return false;
 }
\ No newline at end of file
diff --git a/database/migrations/2016_02_27_120329_update_permissions_and_roles.php b/database/migrations/2016_02_27_120329_update_permissions_and_roles.php
new file mode 100644 (file)
index 0000000..9fb2e98
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+class UpdatePermissionsAndRoles extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        // Get roles with permissions we need to change
+        $adminRole = \BookStack\Role::getRole('admin');
+        $editorRole = \BookStack\Role::getRole('editor');
+
+        // Delete old permissions
+        $permissions = \BookStack\Permission::all();
+        $permissions->each(function ($permission) {
+            $permission->delete();
+        });
+
+        // Create & attach new admin permissions
+        $permissionsToCreate = [
+            'settings-manage' => 'Manage Settings',
+            'users-manage' => 'Manage Users',
+            'user-roles-manage' => 'Manage Roles & Permissions'
+        ];
+        foreach ($permissionsToCreate as $name => $displayName) {
+            $newPermission = new \BookStack\Permission();
+            $newPermission->name = $name;
+            $newPermission->display_name = $displayName;
+            $newPermission->save();
+            $adminRole->attachPermission($newPermission);
+        }
+
+        // Create & attach new entity permissions
+        $entities = ['Book', 'Page', 'Chapter', 'Image'];
+        $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
+        foreach ($entities as $entity) {
+            foreach ($ops as $op) {
+                $newPermission = new \BookStack\Permission();
+                $newPermission->name = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op));
+                $newPermission->display_name = $op . ' ' . $entity . 's';
+                $newPermission->save();
+                $adminRole->attachPermission($newPermission);
+                if ($editorRole !== null) $editorRole->attachPermission($newPermission);
+            }
+        }
+
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        // Get roles with permissions we need to change
+        $adminRole = \BookStack\Role::getRole('admin');
+
+        // Delete old permissions
+        $permissions = \BookStack\Permission::all();
+        $permissions->each(function ($permission) {
+            $permission->delete();
+        });
+
+        // Create default CRUD permissions and allocate to admins and editors
+        $entities = ['Book', 'Page', 'Chapter', 'Image'];
+        $ops = ['Create', 'Update', 'Delete'];
+        foreach ($entities as $entity) {
+            foreach ($ops as $op) {
+                $newPermission = new \BookStack\Permission();
+                $newPermission->name = strtolower($entity) . '-' . strtolower($op);
+                $newPermission->display_name = $op . ' ' . $entity . 's';
+                $newPermission->save();
+                $adminRole->attachPermission($newPermission);
+            }
+        }
+
+        // Create admin permissions
+        $entities = ['Settings', 'User'];
+        $ops = ['Create', 'Update', 'Delete'];
+        foreach ($entities as $entity) {
+            foreach ($ops as $op) {
+                $newPermission = new \BookStack\Permission();
+                $newPermission->name = strtolower($entity) . '-' . strtolower($op);
+                $newPermission->display_name = $op . ' ' . $entity;
+                $newPermission->save();
+                $adminRole->attachPermission($newPermission);
+            }
+        }
+    }
+}
index 0df49485efd400f813f028b768b4ea432971045f..e3cac3621b113e4e818e866dde739d8fecd2cec8 100644 (file)
@@ -43,7 +43,7 @@
                     <div class="float right">
                         <div class="links text-center">
                             <a href="/books"><i class="zmdi zmdi-book"></i>Books</a>
-                            @if(isset($currentUser) && $currentUser->can('settings-update'))
+                            @if(isset($currentUser) && $currentUser->can('settings-manage'))
                                 <a href="/settings"><i class="zmdi zmdi-settings"></i>Settings</a>
                             @endif
                             @if(!isset($signedIn) || !$signedIn)
index 9fa48373558b259753a8855bd54069f39684654a..d5d7cb139813976a58a3b0c646d4ea987713584c 100644 (file)
@@ -8,7 +8,7 @@
                 <div class="col-xs-1"></div>
                 <div class="col-xs-11 faded">
                     <div class="action-buttons">
-                        @if($currentUser->can('book-create'))
+                        @if($currentUser->can('book-create-all'))
                             <a href="/books/create" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>Add new book</a>
                         @endif
                     </div>
index db89bed9ece04eb2a6840b20f34a5402b04603a0..12ebae0d6b820484805c081101ac746698454fca 100644 (file)
@@ -7,17 +7,17 @@
             <div class="row">
                 <div class="col-md-12">
                     <div class="action-buttons faded">
-                        @if($currentUser->can('page-create'))
+                        @if(userCan('page-create', $book))
                             <a href="{{$book->getUrl() . '/page/create'}}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i> New Page</a>
                         @endif
-                        @if($currentUser->can('chapter-create'))
+                        @if(userCan('chapter-create', $book))
                             <a href="{{$book->getUrl() . '/chapter/create'}}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i> New Chapter</a>
                         @endif
-                        @if($currentUser->can('book-update'))
+                        @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($currentUser->can('book-delete'))
+                        @if(userCan('book-delete', $book))
                             <a href="{{ $book->getUrl() }}/delete" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete</a>
                         @endif
                     </div>
index 7033093e5675c863f47db6d92343ad2c48fb9d77..18a115ea5c3c03755304049aa1bee0bbf6ae3901 100644 (file)
                 </div>
                 <div class="col-md-8 faded">
                     <div class="action-buttons">
-                        @if($currentUser->can('chapter-create'))
+                        @if(userCan('page-create', $chapter))
                             <a href="{{$chapter->getUrl() . '/create-page'}}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>New Page</a>
                         @endif
-                        @if($currentUser->can('chapter-update'))
+                        @if(userCan('chapter-update', $chapter))
                             <a href="{{$chapter->getUrl() . '/edit'}}" class="text-primary text-button"><i class="zmdi zmdi-edit"></i>Edit</a>
                         @endif
-                        @if($currentUser->can('chapter-delete'))
+                        @if(userCan('chapter-delete', $chapter))
                             <a href="{{$chapter->getUrl() . '/delete'}}" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete</a>
                         @endif
                     </div>
diff --git a/resources/views/form/role-checkboxes.blade.php b/resources/views/form/role-checkboxes.blade.php
new file mode 100644 (file)
index 0000000..df868ee
--- /dev/null
@@ -0,0 +1,14 @@
+
+@foreach($roles as $role)
+    <label>
+        <input value="{{ $role->id }}" id="{{$name}}-{{$role->name}}" type="checkbox" name="{{$name}}[{{$role->name}}]"
+               @if($errors->has($name)) class="neg" @endif
+               @if(old($name . '.' . $role->name) || (!old('name') && isset($model) && $model->hasRole($role->name))) checked="checked" @endif
+        >
+        {{ $role->display_name }}
+    </label>
+@endforeach
+
+@if($errors->has($name))
+    <div class="text-neg text-small">{{ $errors->first($name) }}</div>
+@endif
\ No newline at end of file
index 004a240d0290bdbc8e4272e67d0a6593c9ead29c..52b9daee67e974821da43fd4ae63f0279fbf42eb 100644 (file)
                                 <li><a href="{{$page->getUrl() . '/export/plaintext'}}" target="_blank">Plain Text File <span class="text-muted float right">.txt</span></a></li>
                             </ul>
                         </span>
-                        @if($currentUser->can('page-update'))
+                        @if(userCan('page-update', $page))
                             <a href="{{$page->getUrl() . '/revisions'}}" class="text-primary text-button"><i class="zmdi zmdi-replay"></i>Revisions</a>
                             <a href="{{$page->getUrl() . '/edit'}}" class="text-primary text-button" ><i class="zmdi zmdi-edit"></i>Edit</a>
                         @endif
-                        @if($currentUser->can('page-delete'))
+                        @if(userCan('page-delete', $page))
                             <a href="{{$page->getUrl() . '/delete'}}" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete</a>
                         @endif
                     </div>
index b5b5d0a4f1ecf4589df977ac953fd94496519e47..4274378e62bf1a07f730473eefe4a4ea72e87884 100644 (file)
@@ -54,7 +54,7 @@
                     <select id="setting-registration-role" name="setting-registration-role" @if($errors->has('setting-registration-role')) class="neg" @endif>
                         @foreach(\BookStack\Role::all() as $role)
                             <option value="{{$role->id}}"
-                                    @if(\Setting::get('registration-role', \BookStack\Role::getDefault()->id) == $role->id) selected @endif
+                                    @if(\Setting::get('registration-role', \BookStack\Role::first()->id) == $role->id) selected @endif
                                     >
                                 {{ $role->display_name }}
                             </option>
diff --git a/resources/views/settings/roles/checkbox.blade.php b/resources/views/settings/roles/checkbox.blade.php
new file mode 100644 (file)
index 0000000..35aa61e
--- /dev/null
@@ -0,0 +1,3 @@
+<input type="checkbox" name="permissions[{{ $permission }}]"
+       @if(old('permissions.'.$permission, false)|| (!old('display_name', false) && (isset($role) && $role->hasPermission($permission)))) checked="checked" @endif
+       value="true">
\ No newline at end of file
diff --git a/resources/views/settings/roles/create.blade.php b/resources/views/settings/roles/create.blade.php
new file mode 100644 (file)
index 0000000..f7d39f4
--- /dev/null
@@ -0,0 +1,15 @@
+@extends('base')
+
+@section('content')
+
+    @include('settings/navbar', ['selected' => 'roles'])
+
+    <div class="container">
+        <h1>Create New Role</h1>
+
+        <form action="/settings/roles/new" method="POST">
+            @include('settings/roles/form')
+        </form>
+    </div>
+
+@stop
diff --git a/resources/views/settings/roles/delete.blade.php b/resources/views/settings/roles/delete.blade.php
new file mode 100644 (file)
index 0000000..5d1ffe2
--- /dev/null
@@ -0,0 +1,28 @@
+@extends('base')
+
+@section('content')
+
+    @include('settings/navbar', ['selected' => 'roles'])
+
+    <div class="container small" ng-non-bindable>
+        <h1>Delete Role</h1>
+        <p>This will delete the role with the name '{{$role->display_name}}'.</p>
+
+        <form action="/settings/roles/delete/{{$role->id}}" method="POST">
+            {!! csrf_field() !!}
+            <input type="hidden" name="_method" value="DELETE">
+
+            @if($role->users->count() > 0)
+            <div class="form-group">
+                    <p>This role has {{$role->users->count()}} users assigned to it. If you would like to migrate the users from this role select a new role below.</p>
+                    @include('form/role-select', ['options' => $roles, 'name' => 'migration_role_id'])
+            </div>
+            @endif
+
+            <p class="text-neg">Are you sure you want to delete this role?</p>
+            <a href="/settings/roles/{{ $role->id }}" class="button">Cancel</a>
+            <button type="submit" class="button neg">Confirm</button>
+        </form>
+    </div>
+
+@stop
index ae2d01538fe6024684023cdb4fe32f7c44b8b92f..f98e6e0b34b954860e57e84ba3619aa9898c3166 100644 (file)
@@ -5,59 +5,19 @@
     @include('settings/navbar', ['selected' => 'roles'])
 
     <div class="container">
-        <h1>Edit Role <small> {{ $role->display_name }}</small></h1>
-
-        <form action="">
-            <div class="row">
-
-                <div class="col-md-6">
-                    <table class="table">
-                        <tr>
-                            <th></th>
-                            <th>Create</th>
-                            <th>Edit</th>
-                            <th>Delete</th>
-                        </tr>
-                        <tr>
-                            <td>Books</td>
-                            <td></td>
-                            <td></td>
-                            <td></td>
-                        </tr>
-                        <tr>
-                            <td>Chapters</td>
-                            <td></td>
-                            <td></td>
-                            <td></td>
-                        </tr>
-                        <tr>
-                            <td>Pages</td>
-                            <td></td>
-                            <td></td>
-                            <td></td>
-                        </tr>
-                        <tr>
-                            <td>Images</td>
-                            <td></td>
-                            <td></td>
-                            <td></td>
-                        </tr>
-                    </table>
-                </div>
-                <div class="col-md-6">
-                    <div class="form-group">
-                        <label for="">Can only edit own content</label>
-                        <hr class="even">
-                        <label for="">Manage users</label>
-                        <hr class="even">
-                        <label for="">Manage user roles</label>
-                        <hr class="even">
-                        <label for="">Manage app settings</label>
-                    </div>
-                </div>
-
+        <div class="row">
+            <div class="col-sm-6">
+                <h1>Edit Role <small> {{ $role->display_name }}</small></h1>
             </div>
-            <button type="submit" class="button pos">Save Role</button>
+            <div class="col-sm-6">
+                <p></p>
+                <a href="/settings/roles/delete/{{ $role->id }}" class="button neg float right">Delete Role</a>
+            </div>
+        </div>
+
+        <form action="/settings/roles/{{ $role->id }}" method="POST">
+            <input type="hidden" name="_method" value="PUT">
+            @include('settings/roles/form', ['model' => $role])
         </form>
     </div>
 
diff --git a/resources/views/settings/roles/form.blade.php b/resources/views/settings/roles/form.blade.php
new file mode 100644 (file)
index 0000000..b9c4705
--- /dev/null
@@ -0,0 +1,84 @@
+{!! csrf_field() !!}
+
+<div class="row">
+
+    <div class="col-md-6">
+        <div class="form-group">
+            <label for="name">Role Name</label>
+            @include('form/text', ['name' => 'display_name'])
+        </div>
+        <div class="form-group">
+            <label for="name">Short Role Description</label>
+            @include('form/text', ['name' => 'description'])
+        </div>
+        <hr class="even">
+        <div class="form-group">
+            <label>Manage users @include('settings/roles/checkbox', ['permission' => 'users-manage'])</label>
+            <hr class="even">
+            <label>Manage user roles & Permissions @include('settings/roles/checkbox', ['permission' => 'user-roles-manage'])</label>
+            <hr class="even">
+            <label>Manage app settings @include('settings/roles/checkbox', ['permission' => 'settings-manage'])</label>
+        </div>
+    </div>
+
+    <div class="col-md-6">
+        <table class="table">
+            <tr>
+                <th></th>
+                <th>Create</th>
+                <th>Edit</th>
+                <th>Delete</th>
+            </tr>
+            <tr>
+                <td>Books</td>
+                <td>@include('settings/roles/checkbox', ['permission' => 'book-create-all'])</td>
+                <td>
+                    <label>@include('settings/roles/checkbox', ['permission' => 'book-update-own']) Own</label>
+                    <label>@include('settings/roles/checkbox', ['permission' => 'book-update-all']) All</label>
+                </td>
+                <td>
+                    <label>@include('settings/roles/checkbox', ['permission' => 'book-delete-own']) Own</label>
+                    <label>@include('settings/roles/checkbox', ['permission' => 'book-delete-all']) All</label>
+                </td>
+            </tr>
+            <tr>
+                <td>Chapters</td>
+                <td>@include('settings/roles/checkbox', ['permission' => 'chapter-create-all'])</td>
+                <td>
+                    <label>@include('settings/roles/checkbox', ['permission' => 'chapter-update-own']) Own</label>
+                    <label>@include('settings/roles/checkbox', ['permission' => 'chapter-update-all']) All</label>
+                </td>
+                <td>
+                    <label>@include('settings/roles/checkbox', ['permission' => 'chapter-delete-own']) Own</label>
+                    <label>@include('settings/roles/checkbox', ['permission' => 'chapter-delete-all']) All</label>
+                </td>
+            </tr>
+            <tr>
+                <td>Pages</td>
+                <td>@include('settings/roles/checkbox', ['permission' => 'page-create-all'])</td>
+                <td>
+                    <label>@include('settings/roles/checkbox', ['permission' => 'page-update-own']) Own</label>
+                    <label>@include('settings/roles/checkbox', ['permission' => 'page-update-all']) All</label>
+                </td>
+                <td>
+                    <label>@include('settings/roles/checkbox', ['permission' => 'page-delete-own']) Own</label>
+                    <label>@include('settings/roles/checkbox', ['permission' => 'page-delete-all']) All</label>
+                </td>
+            </tr>
+            <tr>
+                <td>Images</td>
+                <td>@include('settings/roles/checkbox', ['permission' => 'image-create-all'])</td>
+                <td>
+                    <label>@include('settings/roles/checkbox', ['permission' => 'image-update-own']) Own</label>
+                    <label>@include('settings/roles/checkbox', ['permission' => 'image-update-all']) All</label>
+                </td>
+                <td>
+                    <label>@include('settings/roles/checkbox', ['permission' => 'image-delete-own']) Own</label>
+                    <label>@include('settings/roles/checkbox', ['permission' => 'image-delete-all']) All</label>
+                </td>
+            </tr>
+        </table>
+    </div>
+
+</div>
+<button type="submit" class="button pos">Save Role</button>
\ No newline at end of file
index 661d66f63586e5045790d4afd43cd06a26c9aaac..601c6533e53a2640f8f5df03ff840e1271457827 100644 (file)
@@ -7,6 +7,11 @@
     <div class="container">
 
         <h1>User Roles</h1>
+
+        <p>
+            <a href="/settings/roles/new" class="text-pos"><i class="zmdi zmdi-lock-open"></i>Add new role</a>
+        </p>
+
         <table class="table">
             <tr>
                 <th>Role Name</th>
index 4a6673dc2ebedcbd36fb75c44f16cd474b12be0f..47edb211b4ce1f2b352e018676578a2a9b4b0292 100644 (file)
@@ -3,21 +3,21 @@
     @include('form.text', ['name' => 'name'])
 </div>
 
-@if($currentUser->can('user-update'))
+@if(userCan('users-manage'))
 <div class="form-group">
     <label for="email">Email</label>
     @include('form.text', ['name' => 'email'])
 </div>
 @endif
 
-@if($currentUser->can('user-update'))
+@if(userCan('users-manage'))
     <div class="form-group">
         <label for="role">User Role</label>
-        @include('form.role-select', ['name' => 'role', 'options' => \BookStack\Role::all(), 'displayKey' => 'display_name'])
+        @include('form/role-checkboxes', ['name' => 'roles', 'roles' => \BookStack\Role::all()])
     </div>
 @endif
 
-@if($currentUser->can('user-update'))
+@if(userCan('users-manage'))
     <div class="form-group">
         <label for="external_auth_id">External Authentication ID</label>
         @include('form.text', ['name' => 'external_auth_id'])
index c20e2955d05b52b1beab19c0014ccee262dfa040..9bd70b43c4805d4671f0f626b69e30495a3e129d 100644 (file)
@@ -8,10 +8,10 @@
     @include('form.text', ['name' => 'email'])
 </div>
 
-@if($currentUser->can('user-update'))
+@if(userCan('users-manage'))
     <div class="form-group">
         <label for="role">User Role</label>
-        @include('form.role-select', ['name' => 'role', 'options' => \BookStack\Role::all(), 'displayKey' => 'display_name'])
+        @include('form/role-checkboxes', ['name' => 'roles', 'roles' => \BookStack\Role::all()])
     </div>
 @endif
 
index 6e5d10c5faa6a504bd618e48d04c6b61894d5a34..f0663071472349fc88ad5ea149ec41419ba2a325 100644 (file)
@@ -8,7 +8,7 @@
 
     <div class="container small" ng-non-bindable>
         <h1>Users</h1>
-        @if($currentUser->can('user-create'))
+        @if(userCan('users-manage'))
             <p>
                 <a href="/settings/users/create" class="text-pos"><i class="zmdi zmdi-account-add"></i>Add new user</a>
             </p>
                 <th></th>
                 <th>Name</th>
                 <th>Email</th>
-                <th>User Type</th>
+                <th>User Roles</th>
             </tr>
             @foreach($users as $user)
                 <tr>
                     <td style="line-height: 0;"><img class="avatar med" src="{{$user->getAvatar(40)}}" alt="{{$user->name}}"></td>
                     <td>
-                        @if($currentUser->can('user-update') || $currentUser->id == $user->id)
+                        @if(userCan('users-manage') || $currentUser->id == $user->id)
                             <a href="/settings/users/{{$user->id}}">
                                 @endif
                                 {{ $user->name }}
-                                @if($currentUser->can('user-update') || $currentUser->id == $user->id)
+                                @if(userCan('users-manage') || $currentUser->id == $user->id)
                             </a>
                         @endif
                     </td>
                     <td>
-                        @if($currentUser->can('user-update') || $currentUser->id == $user->id)
+                        @if(userCan('users-manage') || $currentUser->id == $user->id)
                             <a href="/settings/users/{{$user->id}}">
                                 @endif
                                 {{ $user->email }}
-                                @if($currentUser->can('user-update') || $currentUser->id == $user->id)
+                                @if(userCan('users-manage') || $currentUser->id == $user->id)
                             </a>
                         @endif
                     </td>
-                    <td>{{ $user->role->display_name }}</td>
+                    <td>
+                       <small> {{ $user->roles->implode('display_name', ', ') }}</small>
+                    </td>
                 </tr>
             @endforeach
         </table>
index 69402266684522b2cb0b882f66dfef04df77bd8d..067840841c8938aed53cba08a2c7c30356ac27f8 100644 (file)
@@ -133,12 +133,12 @@ class AuthTest extends TestCase
             ->click('Add new user')
             ->type($user->name, '#name')
             ->type($user->email, '#email')
-            ->select(2, '#role')
+            ->check('roles[admin]')
             ->type($user->password, '#password')
             ->type($user->password, '#password-confirm')
             ->press('Save')
-            ->seeInDatabase('users', $user->toArray())
             ->seePageIs('/settings/users')
+            ->seeInDatabase('users', $user->toArray())
             ->see($user->name);
     }
 
diff --git a/tests/RolesTest.php b/tests/RolesTest.php
new file mode 100644 (file)
index 0000000..140290b
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+
+class RolesTest extends TestCase
+{
+    protected $user;
+
+    public function setUp()
+    {
+        parent::setUp();
+    }
+
+    protected function createNewRole()
+    {
+        return \BookStack\Role::forceCreate([
+            'name' => 'test-role',
+            'display_name' => 'Test Role',
+            'description' => 'This is a role for testing'
+        ]);
+    }
+
+    public function test_admin_can_see_settings()
+    {
+        $this->asAdmin()->visit('/settings')->see('Settings');
+    }
+
+    public function test_cannot_delete_admin_role()
+    {
+        $adminRole = \BookStack\Role::getRole('admin');
+        $deletePageUrl = '/settings/roles/delete/' . $adminRole->id;
+        $this->asAdmin()->visit($deletePageUrl)
+            ->press('Confirm')
+            ->seePageIs($deletePageUrl)
+            ->see('cannot be deleted');
+    }
+
+    public function test_role_cannot_be_deleted_if_default()
+    {
+        $newRole = $this->createNewRole();
+        $this->setSettings(['registration-role' => $newRole->id]);
+
+        $deletePageUrl = '/settings/roles/delete/' . $newRole->id;
+        $this->asAdmin()->visit($deletePageUrl)
+            ->press('Confirm')
+            ->seePageIs($deletePageUrl)
+            ->see('cannot be deleted');
+    }
+
+}
index 4b8578a43280aae82c51e57bdafeee670c157eef..ce4e93b122864dd266b6b60bee375ec3ca88f6f3 100644 (file)
@@ -32,7 +32,8 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
     public function asAdmin()
     {
         if($this->admin === null) {
-            $this->admin = \BookStack\User::find(1);
+            $adminRole = \BookStack\Role::getRole('admin');
+            $this->admin = $adminRole->users->first();
         }
         return $this->actingAs($this->admin);
     }