1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2024-11-23 11:22:33 +01:00

Implemented database structure and inital interfaces for entity restrictions

This commit is contained in:
Dan Brown 2016-02-28 10:49:41 +00:00
parent a14b5c33fd
commit 201f788806
22 changed files with 436 additions and 24 deletions

View File

@ -48,13 +48,31 @@ abstract class Entity extends Ownable
/**
* 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.

View File

@ -3,6 +3,7 @@
namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Repos\UserRepo;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -19,18 +20,21 @@ class BookController extends Controller
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();
}
@ -177,7 +181,6 @@ class BookController extends Controller
/**
* Saves an array of sort mapping to pages and chapters.
*
* @param string $bookSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
@ -223,7 +226,6 @@ class BookController extends Controller
/**
* Remove the specified book from storage.
*
* @param $bookSlug
* @return Response
*/
@ -236,4 +238,36 @@ class BookController extends Controller
$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());
}
}

View File

@ -1,6 +1,7 @@
<?php namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Repos\UserRepo;
use Illuminate\Http\Request;
use BookStack\Http\Requests;
use BookStack\Repos\BookRepo;
@ -12,16 +13,19 @@ class ChapterController extends Controller
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();
}
@ -144,4 +148,39 @@ class ChapterController extends Controller
$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());
}
}

View File

@ -1,6 +1,7 @@
<?php namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService;
use Illuminate\Http\Request;
use BookStack\Http\Requests;
@ -17,20 +18,23 @@ class PageController extends Controller
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();
}
@ -308,4 +312,39 @@ class PageController extends Controller
]);
}
/**
* 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());
}
}

View File

@ -19,6 +19,8 @@ Route::group(['middleware' => 'auth'], function () {
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');
@ -32,6 +34,8 @@ Route::group(['middleware' => 'auth'], function () {
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');
@ -47,6 +51,8 @@ Route::group(['middleware' => 'auth'], function () {
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');

View File

@ -238,4 +238,27 @@ class BookRepo
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();
}
}

View File

@ -161,4 +161,27 @@ class ChapterRepo
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();
}
}

View File

@ -407,4 +407,27 @@ class PageRepo
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();
}
}

View File

@ -164,4 +164,14 @@ class UserRepo
];
}
/**
* 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();
}
}

21
app/Restriction.php Normal file
View File

@ -0,0 +1,21 @@
<?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();
}
}

View File

@ -26,7 +26,9 @@ class UpdatePermissionsAndRoles extends Migration
$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();

View File

@ -0,0 +1,73 @@
<?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');
}
}

View File

@ -0,0 +1,10 @@
@extends('base')
@section('content')
<div class="container" ng-non-bindable>
<h1>Book Restrictions</h1>
@include('form/restriction-form', ['model' => $book])
</div>
@stop

View File

@ -17,6 +17,9 @@
<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

View File

@ -0,0 +1,10 @@
@extends('base')
@section('content')
<div class="container" ng-non-bindable>
<h1>Chapter Restrictions</h1>
@include('form/restriction-form', ['model' => $chapter])
</div>
@stop

View File

@ -18,6 +18,9 @@
@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

View File

@ -0,0 +1,12 @@
<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

View File

@ -0,0 +1,7 @@
<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>

View File

@ -0,0 +1,28 @@
<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>

View File

@ -0,0 +1,10 @@
@extends('base')
@section('content')
<div class="container" ng-non-bindable>
<h1>Page Restrictions</h1>
@include('form/restriction-form', ['model' => $page])
</div>
@stop

View File

@ -22,17 +22,20 @@
<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>

View File

@ -12,13 +12,28 @@
@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 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>@include('settings/roles/checkbox', ['permission' => 'settings-manage']) Manage app settings</label>
</div>
</div>
<div class="col-md-6">