/**
* Get View objects for this entity.
- * @return mixed
*/
public function views()
{
return $this->morphMany('BookStack\View', 'viewable');
}
+ /**
+ * Get this entities restrictions.
+ */
+ public function restrictions()
+ {
+ return $this->morphMany('BookStack\Restriction', 'restrictable');
+ }
+
+ /**
+ * Check if this entity has a specific restriction set against it.
+ * @param $role_id
+ * @param $action
+ * @return bool
+ */
+ public function hasRestriction($role_id, $action)
+ {
+ return $this->restrictions->where('role_id', $role_id)->where('action', $action)->count() > 0;
+ }
+
/**
* Allows checking of the exact class, Used to check entity type.
* Cleaner method for is_a.
namespace BookStack\Http\Controllers;
use Activity;
+use BookStack\Repos\UserRepo;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
protected $bookRepo;
protected $pageRepo;
protected $chapterRepo;
+ protected $userRepo;
/**
* BookController constructor.
* @param BookRepo $bookRepo
* @param PageRepo $pageRepo
* @param ChapterRepo $chapterRepo
+ * @param UserRepo $userRepo
*/
- public function __construct(BookRepo $bookRepo, PageRepo $pageRepo, ChapterRepo $chapterRepo)
+ public function __construct(BookRepo $bookRepo, PageRepo $pageRepo, ChapterRepo $chapterRepo, UserRepo $userRepo)
{
$this->bookRepo = $bookRepo;
$this->pageRepo = $pageRepo;
$this->chapterRepo = $chapterRepo;
+ $this->userRepo = $userRepo;
parent::__construct();
}
/**
* Saves an array of sort mapping to pages and chapters.
- *
* @param string $bookSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
/**
* Remove the specified book from storage.
- *
* @param $bookSlug
* @return Response
*/
$this->bookRepo->destroyBySlug($bookSlug);
return redirect('/books');
}
+
+ /**
+ * Show the Restrictions view.
+ * @param $bookSlug
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ */
+ public function showRestrict($bookSlug)
+ {
+ $book = $this->bookRepo->getBySlug($bookSlug);
+ $this->checkOwnablePermission('restrictions-manage', $book);
+ $roles = $this->userRepo->getRestrictableRoles();
+ return view('books/restrictions', [
+ 'book' => $book,
+ 'roles' => $roles
+ ]);
+ }
+
+ /**
+ * Set the restrictions for this book.
+ * @param $bookSlug
+ * @param $bookSlug
+ * @param Request $request
+ * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+ */
+ public function restrict($bookSlug, Request $request)
+ {
+ $book = $this->bookRepo->getBySlug($bookSlug);
+ $this->checkOwnablePermission('restrictions-manage', $book);
+ $this->bookRepo->updateRestrictionsFromRequest($request, $book);
+ session()->flash('success', 'Page Restrictions Updated');
+ return redirect($book->getUrl());
+ }
}
<?php namespace BookStack\Http\Controllers;
use Activity;
+use BookStack\Repos\UserRepo;
use Illuminate\Http\Request;
use BookStack\Http\Requests;
use BookStack\Repos\BookRepo;
protected $bookRepo;
protected $chapterRepo;
+ protected $userRepo;
/**
* ChapterController constructor.
- * @param $bookRepo
- * @param $chapterRepo
+ * @param BookRepo $bookRepo
+ * @param ChapterRepo $chapterRepo
+ * @param UserRepo $userRepo
*/
- public function __construct(BookRepo $bookRepo, ChapterRepo $chapterRepo)
+ public function __construct(BookRepo $bookRepo, ChapterRepo $chapterRepo, UserRepo $userRepo)
{
$this->bookRepo = $bookRepo;
$this->chapterRepo = $chapterRepo;
+ $this->userRepo = $userRepo;
parent::__construct();
}
$this->chapterRepo->destroy($chapter);
return redirect($book->getUrl());
}
+
+ /**
+ * Show the Restrictions view.
+ * @param $bookSlug
+ * @param $chapterSlug
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ */
+ public function showRestrict($bookSlug, $chapterSlug)
+ {
+ $book = $this->bookRepo->getBySlug($bookSlug);
+ $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
+ $this->checkOwnablePermission('restrictions-manage', $chapter);
+ $roles = $this->userRepo->getRestrictableRoles();
+ return view('chapters/restrictions', [
+ 'chapter' => $chapter,
+ 'roles' => $roles
+ ]);
+ }
+
+ /**
+ * Set the restrictions for this chapter.
+ * @param $bookSlug
+ * @param $chapterSlug
+ * @param Request $request
+ * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+ */
+ public function restrict($bookSlug, $chapterSlug, Request $request)
+ {
+ $book = $this->bookRepo->getBySlug($bookSlug);
+ $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
+ $this->checkOwnablePermission('restrictions-manage', $chapter);
+ $this->chapterRepo->updateRestrictionsFromRequest($request, $chapter);
+ session()->flash('success', 'Page Restrictions Updated');
+ return redirect($chapter->getUrl());
+ }
}
<?php namespace BookStack\Http\Controllers;
use Activity;
+use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService;
use Illuminate\Http\Request;
use BookStack\Http\Requests;
protected $bookRepo;
protected $chapterRepo;
protected $exportService;
+ protected $userRepo;
/**
* PageController constructor.
- * @param PageRepo $pageRepo
- * @param BookRepo $bookRepo
- * @param ChapterRepo $chapterRepo
+ * @param PageRepo $pageRepo
+ * @param BookRepo $bookRepo
+ * @param ChapterRepo $chapterRepo
* @param ExportService $exportService
+ * @param UserRepo $userRepo
*/
- public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ExportService $exportService)
+ public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ExportService $exportService, UserRepo $userRepo)
{
$this->pageRepo = $pageRepo;
$this->bookRepo = $bookRepo;
$this->chapterRepo = $chapterRepo;
$this->exportService = $exportService;
+ $this->userRepo = $userRepo;
parent::__construct();
}
]);
}
+ /**
+ * Show the Restrictions view.
+ * @param $bookSlug
+ * @param $pageSlug
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ */
+ public function showRestrict($bookSlug, $pageSlug)
+ {
+ $book = $this->bookRepo->getBySlug($bookSlug);
+ $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
+ $this->checkOwnablePermission('restrictions-manage', $page);
+ $roles = $this->userRepo->getRestrictableRoles();
+ return view('pages/restrictions', [
+ 'page' => $page,
+ 'roles' => $roles
+ ]);
+ }
+
+ /**
+ * Set the restrictions for this page.
+ * @param $bookSlug
+ * @param $pageSlug
+ * @param Request $request
+ * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+ */
+ public function restrict($bookSlug, $pageSlug, Request $request)
+ {
+ $book = $this->bookRepo->getBySlug($bookSlug);
+ $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
+ $this->checkOwnablePermission('restrictions-manage', $page);
+ $this->pageRepo->updateRestrictionsFromRequest($request, $page);
+ session()->flash('success', 'Page Restrictions Updated');
+ return redirect($page->getUrl());
+ }
+
}
Route::delete('/{id}', 'BookController@destroy');
Route::get('/{slug}/sort-item', 'BookController@getSortItem');
Route::get('/{slug}', 'BookController@show');
+ Route::get('/{bookSlug}/restrict', 'BookController@showRestrict');
+ Route::put('/{bookSlug}/restrict', 'BookController@restrict');
Route::get('/{slug}/delete', 'BookController@showDelete');
Route::get('/{bookSlug}/sort', 'BookController@sort');
Route::put('/{bookSlug}/sort', 'BookController@saveSort');
Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageController@exportPlainText');
Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit');
Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete');
+ Route::get('/{bookSlug}/page/{pageSlug}/restrict', 'PageController@showRestrict');
+ Route::put('/{bookSlug}/page/{pageSlug}/restrict', 'PageController@restrict');
Route::put('/{bookSlug}/page/{pageSlug}', 'PageController@update');
Route::delete('/{bookSlug}/page/{pageSlug}', 'PageController@destroy');
Route::get('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@show');
Route::put('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@update');
Route::get('/{bookSlug}/chapter/{chapterSlug}/edit', 'ChapterController@edit');
+ Route::get('/{bookSlug}/chapter/{chapterSlug}/restrict', 'ChapterController@showRestrict');
+ Route::put('/{bookSlug}/chapter/{chapterSlug}/restrict', 'ChapterController@restrict');
Route::get('/{bookSlug}/chapter/{chapterSlug}/delete', 'ChapterController@showDelete');
Route::delete('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@destroy');
return $books;
}
+ /**
+ * Updates books restrictions from a request
+ * @param $request
+ * @param $book
+ */
+ public function updateRestrictionsFromRequest($request, $book)
+ {
+ // TODO - extract into shared repo
+ $book->restricted = $request->has('restricted') && $request->get('restricted') === 'true';
+ $book->restrictions()->delete();
+ if ($request->has('restrictions')) {
+ foreach($request->get('restrictions') as $roleId => $restrictions) {
+ foreach ($restrictions as $action => $value) {
+ $book->restrictions()->create([
+ 'role_id' => $roleId,
+ 'action' => strtolower($action)
+ ]);
+ }
+ }
+ }
+ $book->save();
+ }
+
}
\ No newline at end of file
return $chapter;
}
+ /**
+ * Updates pages restrictions from a request
+ * @param $request
+ * @param $chapter
+ */
+ public function updateRestrictionsFromRequest($request, $chapter)
+ {
+ // TODO - extract into shared repo
+ $chapter->restricted = $request->has('restricted') && $request->get('restricted') === 'true';
+ $chapter->restrictions()->delete();
+ if ($request->has('restrictions')) {
+ foreach($request->get('restrictions') as $roleId => $restrictions) {
+ foreach ($restrictions as $action => $value) {
+ $chapter->restrictions()->create([
+ 'role_id' => $roleId,
+ 'action' => strtolower($action)
+ ]);
+ }
+ }
+ }
+ $chapter->save();
+ }
+
}
\ No newline at end of file
return $this->page->orderBy('updated_at', 'desc')->paginate($count);
}
+ /**
+ * Updates pages restrictions from a request
+ * @param $request
+ * @param $page
+ */
+ public function updateRestrictionsFromRequest($request, $page)
+ {
+ // TODO - extract into shared repo
+ $page->restricted = $request->has('restricted') && $request->get('restricted') === 'true';
+ $page->restrictions()->delete();
+ if ($request->has('restrictions')) {
+ foreach($request->get('restrictions') as $roleId => $restrictions) {
+ foreach ($restrictions as $action => $value) {
+ $page->restrictions()->create([
+ 'role_id' => $roleId,
+ 'action' => strtolower($action)
+ ]);
+ }
+ }
+ }
+ $page->save();
+ }
+
}
];
}
+ /**
+ * Get all the roles which can be given restricted access to
+ * other entities in the system.
+ * @return mixed
+ */
+ public function getRestrictableRoles()
+ {
+ return $this->role->where('name', '!=', 'admin')->get();
+ }
+
}
\ No newline at end of file
--- /dev/null
+<?php
+
+namespace BookStack;
+
+use Illuminate\Database\Eloquent\Model;
+
+class Restriction extends Model
+{
+
+ protected $fillable = ['role_id', 'action'];
+ public $timestamps = false;
+
+ /**
+ * Get all this restriction's attached entity.
+ * @return \Illuminate\Database\Eloquent\Relations\MorphTo
+ */
+ public function restrictable()
+ {
+ return $this->morphTo();
+ }
+}
$permissionsToCreate = [
'settings-manage' => 'Manage Settings',
'users-manage' => 'Manage Users',
- 'user-roles-manage' => 'Manage Roles & Permissions'
+ 'user-roles-manage' => 'Manage Roles & Permissions',
+ 'restrictions-manage-all' => 'Manage All Entity Restrictions',
+ 'restrictions-manage-own' => 'Manage Entity Restrictions On Own Content'
];
foreach ($permissionsToCreate as $name => $displayName) {
$newPermission = new \BookStack\Permission();
--- /dev/null
+<?php
+
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+class AddEntityAccessControls extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::table('images', function (Blueprint $table) {
+ $table->integer('uploaded_to')->default(0);
+ $table->index('uploaded_to');
+ });
+
+ Schema::table('books', function (Blueprint $table) {
+ $table->boolean('restricted')->default(false);
+ $table->index('restricted');
+ });
+
+ Schema::table('chapters', function (Blueprint $table) {
+ $table->boolean('restricted')->default(false);
+ $table->index('restricted');
+ });
+
+ Schema::table('pages', function (Blueprint $table) {
+ $table->boolean('restricted')->default(false);
+ $table->index('restricted');
+ });
+
+ Schema::create('restrictions', function(Blueprint $table) {
+ $table->increments('id');
+ $table->integer('restrictable_id');
+ $table->string('restrictable_type');
+ $table->integer('role_id');
+ $table->string('action');
+ $table->index('role_id');
+ $table->index('action');
+ $table->index(['restrictable_id', 'restrictable_type']);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table('images', function (Blueprint $table) {
+ $table->dropColumn('uploaded_to');
+ });
+
+ Schema::table('books', function (Blueprint $table) {
+ $table->dropColumn('restricted');
+ });
+
+ Schema::table('chapters', function (Blueprint $table) {
+ $table->dropColumn('restricted');
+ });
+
+
+ Schema::table('pages', function (Blueprint $table) {
+ $table->dropColumn('restricted');
+ });
+
+ Schema::drop('restrictions');
+ }
+}
--- /dev/null
+@extends('base')
+
+@section('content')
+
+ <div class="container" ng-non-bindable>
+ <h1>Book Restrictions</h1>
+ @include('form/restriction-form', ['model' => $book])
+ </div>
+
+@stop
<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>
@endif
--- /dev/null
+@extends('base')
+
+@section('content')
+
+ <div class="container" ng-non-bindable>
+ <h1>Chapter Restrictions</h1>
+ @include('form/restriction-form', ['model' => $chapter])
+ </div>
+
+@stop
@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(userCan('restrictions-manage', $chapter))
+ <a href="{{$chapter->getUrl()}}/restrict" class="text-primary text-button"><i class="zmdi zmdi-lock-outline"></i>Restrict</a>
+ @endif
@if(userCan('chapter-delete', $chapter))
<a href="{{$chapter->getUrl() . '/delete'}}" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete</a>
@endif
--- /dev/null
+
+<label>
+ <input value="true" id="{{$name}}" type="checkbox" name="{{$name}}"
+ @if($errors->has($name)) class="neg" @endif
+ @if(old($name) || (!old() && isset($model) && $model->$name)) checked="checked" @endif
+ >
+ {{ $label }}
+</label>
+
+@if($errors->has($name))
+ <div class="text-neg text-small">{{ $errors->first($name) }}</div>
+@endif
\ No newline at end of file
--- /dev/null
+
+<label>
+ <input value="true" id="{{$name}}[{{$role->id}}][{{$action}}]" type="checkbox" name="{{$name}}[{{$role->id}}][{{$action}}]"
+ @if(old($name .'.'.$role->id.'.'.$action) || (!old() && isset($model) && $model->hasRestriction($role->id, $action))) checked="checked" @endif
+ >
+ {{ $label }}
+</label>
\ No newline at end of file
--- /dev/null
+<form action="{{ $model->getUrl() }}/restrict" method="POST">
+ {!! csrf_field() !!}
+ <input type="hidden" name="_method" value="PUT">
+
+ <div class="form-group">
+ @include('form/checkbox', ['name' => 'restricted', 'label' => 'Restrict this page?'])
+ </div>
+
+ <table class="table">
+ <tr>
+ <th>Role</th>
+ <th @if($model->isA('page')) colspan="3" @else colspan="4" @endif>Actions</th>
+ </tr>
+ @foreach($roles as $role)
+ <tr>
+ <td>{{ $role->display_name }}</td>
+ <td>@include('form/restriction-checkbox', ['name'=>'restrictions', 'label' => 'View', 'action' => 'view'])</td>
+ @if(!$model->isA('page'))
+ <td>@include('form/restriction-checkbox', ['name'=>'restrictions', 'label' => 'Create', 'action' => 'create'])</td>
+ @endif
+ <td>@include('form/restriction-checkbox', ['name'=>'restrictions', 'label' => 'Update', 'action' => 'update'])</td>
+ <td>@include('form/restriction-checkbox', ['name'=>'restrictions', 'label' => 'Delete', 'action' => 'delete'])</td>
+ </tr>
+ @endforeach
+ </table>
+
+ <button type="submit" class="button pos">Save Restrictions</button>
+</form>
\ No newline at end of file
--- /dev/null
+@extends('base')
+
+@section('content')
+
+ <div class="container" ng-non-bindable>
+ <h1>Page Restrictions</h1>
+ @include('form/restriction-form', ['model' => $page])
+ </div>
+
+@stop
<span dropdown class="dropdown-container">
<div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>Export</div>
<ul class="wide">
- <li><a href="{{$page->getUrl() . '/export/html'}}" target="_blank">Contained Web File <span class="text-muted float right">.html</span></a></li>
- <li><a href="{{$page->getUrl() . '/export/pdf'}}" target="_blank">PDF File <span class="text-muted float right">.pdf</span></a></li>
- <li><a href="{{$page->getUrl() . '/export/plaintext'}}" target="_blank">Plain Text File <span class="text-muted float right">.txt</span></a></li>
+ <li><a href="{{$page->getUrl()}}/export/html" target="_blank">Contained Web File <span class="text-muted float right">.html</span></a></li>
+ <li><a href="{{$page->getUrl()}}/export/pdf" target="_blank">PDF File <span class="text-muted float right">.pdf</span></a></li>
+ <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(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>
+ <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(userCan('restrictions-manage', $page))
+ <a href="{{$page->getUrl()}}/restrict" class="text-primary text-button"><i class="zmdi zmdi-lock-outline"></i>Restrict</a>
@endif
@if(userCan('page-delete', $page))
- <a href="{{$page->getUrl() . '/delete'}}" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete</a>
+ <a href="{{$page->getUrl()}}/delete" class="text-neg text-button"><i class="zmdi zmdi-delete"></i>Delete</a>
@endif
</div>
</div>
@include('form/text', ['name' => 'description'])
</div>
<hr class="even">
+ <div class="row">
+ <div class="col-md-6">
+ <label> @include('settings/roles/checkbox', ['permission' => 'users-manage']) Manage users</label>
+ </div>
+ <div class="col-md-6">
+ <label>@include('settings/roles/checkbox', ['permission' => 'user-roles-manage']) Manage user roles & Permissions</label>
+ </div>
+ </div>
+ <hr class="even">
+ <div class="row">
+ <div class="col-md-6">
+ <label>@include('settings/roles/checkbox', ['permission' => 'restrictions-manage-all']) Manage all restrictions</label>
+ </div>
+ <div class="col-md-6">
+ <label>@include('settings/roles/checkbox', ['permission' => 'restrictions-manage-own']) Manage restrictions on own content</label>
+ </div>
+ </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>
+ <label>@include('settings/roles/checkbox', ['permission' => 'settings-manage']) Manage app settings</label>
</div>
+
</div>
<div class="col-md-6">