Updated entity restrictions to allow permissions, Not just restrict

Also changed wording from 'Restrictions' to 'Permissions' to keep things more familiar and to better reflect what they do.

Referenced in issue #89.
This commit is contained in:
Dan Brown 2016-03-30 20:15:44 +01:00
parent 491f73e0cd
commit 097d9c9f3c
15 changed files with 201 additions and 56 deletions

View File

@ -19,8 +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('/{bookSlug}/permissions', 'BookController@showRestrict');
Route::put('/{bookSlug}/permissions', 'BookController@restrict');
Route::get('/{slug}/delete', 'BookController@showDelete');
Route::get('/{bookSlug}/sort', 'BookController@sort');
Route::put('/{bookSlug}/sort', 'BookController@saveSort');
@ -36,8 +36,8 @@ Route::group(['middleware' => 'auth'], function () {
Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit');
Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete');
Route::get('/{bookSlug}/draft/{pageId}/delete', 'PageController@showDeleteDraft');
Route::get('/{bookSlug}/page/{pageSlug}/restrict', 'PageController@showRestrict');
Route::put('/{bookSlug}/page/{pageSlug}/restrict', 'PageController@restrict');
Route::get('/{bookSlug}/page/{pageSlug}/permissions', 'PageController@showRestrict');
Route::put('/{bookSlug}/page/{pageSlug}/permissions', 'PageController@restrict');
Route::put('/{bookSlug}/page/{pageSlug}', 'PageController@update');
Route::delete('/{bookSlug}/page/{pageSlug}', 'PageController@destroy');
Route::delete('/{bookSlug}/draft/{pageId}', 'PageController@destroyDraft');
@ -54,8 +54,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}/permissions', 'ChapterController@showRestrict');
Route::put('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@restrict');
Route::get('/{bookSlug}/chapter/{chapterSlug}/delete', 'ChapterController@showDelete');
Route::delete('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@destroy');

View File

@ -41,6 +41,25 @@ class RestrictionService
return false;
}
/**
* Check if an entity has restrictions set on itself or its
* parent tree.
* @param Entity $entity
* @param $action
* @return bool|mixed
*/
public function checkIfRestrictionsSet(Entity $entity, $action)
{
$this->currentAction = $action;
if ($entity->isA('page')) {
return $entity->restricted || ($entity->chapter && $entity->chapter->restricted) || $entity->book->restricted;
} elseif ($entity->isA('chapter')) {
return $entity->restricted || $entity->book->restricted;
} elseif ($entity->isA('book')) {
return $entity->restricted;
}
}
/**
* Add restrictions for a page query
* @param $query

View File

@ -52,12 +52,13 @@ function userCan($permission, \BookStack\Ownable $ownable = null)
if (!$ownable instanceof \BookStack\Entity) return $hasPermission;
// Check restrictions on the entitiy
// Check restrictions on the entity
$restrictionService = app('BookStack\Services\RestrictionService');
$explodedPermission = explode('-', $permission);
$action = end($explodedPermission);
$hasAccess = $restrictionService->checkIfEntityRestricted($ownable, $action);
return $hasAccess && $hasPermission;
$restrictionsSet = $restrictionService->checkIfRestrictionsSet($ownable, $action);
return ($hasAccess && $restrictionsSet) || (!$restrictionsSet && $hasPermission);
}
/**

View File

@ -16,7 +16,7 @@
<div class="container" ng-non-bindable>
<h1>Book Restrictions</h1>
<h1>Book Permissions</h1>
@include('form/restriction-form', ['model' => $book])
</div>

View File

@ -24,7 +24,7 @@
<li><a href="{{ $book->getUrl() }}/sort" class="text-primary"><i class="zmdi zmdi-sort"></i>Sort</a></li>
@endif
@if(userCan('restrictions-manage', $book))
<li><a href="{{$book->getUrl()}}/restrict" class="text-primary"><i class="zmdi zmdi-lock-outline"></i>Restrict</a></li>
<li><a href="{{$book->getUrl()}}/permissions" class="text-primary"><i class="zmdi zmdi-lock-outline"></i>Permissions</a></li>
@endif
@if(userCan('book-delete', $book))
<li><a href="{{ $book->getUrl() }}/delete" class="text-neg"><i class="zmdi zmdi-delete"></i>Delete</a></li>
@ -90,9 +90,9 @@
@if($book->restricted)
<p class="text-muted">
@if(userCan('restrictions-manage', $book))
<a href="{{ $book->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Book Restricted</a>
<a href="{{ $book->getUrl() }}/permissions"><i class="zmdi zmdi-lock-outline"></i>Book Permissions Active</a>
@else
<i class="zmdi zmdi-lock-outline"></i>Book Restricted
<i class="zmdi zmdi-lock-outline"></i>Book Permissions Active
@endif
</p>
@endif

View File

@ -17,7 +17,7 @@
</div>
<div class="container" ng-non-bindable>
<h1>Chapter Restrictions</h1>
<h1>Chapter Permissions</h1>
@include('form/restriction-form', ['model' => $chapter])
</div>

View File

@ -19,7 +19,7 @@
<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>
<a href="{{$chapter->getUrl()}}/permissions" class="text-primary text-button"><i class="zmdi zmdi-lock-outline"></i>Permissions</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>
@ -69,18 +69,18 @@
@if($book->restricted)
@if(userCan('restrictions-manage', $book))
<a href="{{ $book->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Book Restricted</a>
<a href="{{ $book->getUrl() }}/permissions"><i class="zmdi zmdi-lock-outline"></i>Book Permissions Active</a>
@else
<i class="zmdi zmdi-lock-outline"></i>Book Restricted
<i class="zmdi zmdi-lock-outline"></i>Book Permissions Active
@endif
<br>
@endif
@if($chapter->restricted)
@if(userCan('restrictions-manage', $chapter))
<a href="{{ $chapter->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Chapter Restricted</a>
<a href="{{ $chapter->getUrl() }}/permissions"><i class="zmdi zmdi-lock-outline"></i>Chapter Permissions Active</a>
@else
<i class="zmdi zmdi-lock-outline"></i>Chapter Restricted
<i class="zmdi zmdi-lock-outline"></i>Chapter Permissions Active
@endif
@endif
</div>

View File

@ -1,11 +1,14 @@
<form action="{{ $model->getUrl() }}/restrict" method="POST">
<form action="{{ $model->getUrl() }}/permissions" method="POST">
{!! csrf_field() !!}
<input type="hidden" name="_method" value="PUT">
<p>Once enabled, These permissions will take priority over any set role permissions.</p>
<div class="form-group">
@include('form/checkbox', ['name' => 'restricted', 'label' => 'Restrict this ' . $model->getClassName()])
@include('form/checkbox', ['name' => 'restricted', 'label' => 'Enable custom permissions'])
</div>
<table class="table">
<tr>
<th>Role</th>
@ -25,5 +28,5 @@
</table>
<a href="{{ $model->getUrl() }}" class="button muted">Cancel</a>
<button type="submit" class="button pos">Save Restrictions</button>
<button type="submit" class="button pos">Save Permissions</button>
</form>

View File

@ -24,7 +24,7 @@
</div>
<div class="container" ng-non-bindable>
<h1>Page Restrictions</h1>
<h1>Page Permissions</h1>
@include('form/restriction-form', ['model' => $page])
</div>

View File

@ -32,7 +32,7 @@
<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>
<a href="{{$page->getUrl()}}/permissions" class="text-primary text-button"><i class="zmdi zmdi-lock-outline"></i>Permissions</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>
@ -76,27 +76,27 @@
@if($book->restricted)
@if(userCan('restrictions-manage', $book))
<a href="{{ $book->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Book restricted</a>
<a href="{{ $book->getUrl() }}/permissions"><i class="zmdi zmdi-lock-outline"></i>Book Permissions Active</a>
@else
<i class="zmdi zmdi-lock-outline"></i>Book restricted
<i class="zmdi zmdi-lock-outline"></i>Book Permissions Active
@endif
<br>
@endif
@if($page->chapter && $page->chapter->restricted)
@if(userCan('restrictions-manage', $page->chapter))
<a href="{{ $page->chapter->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Chapter restricted</a>
<a href="{{ $page->chapter->getUrl() }}/permissions"><i class="zmdi zmdi-lock-outline"></i>Chapter Permissions Active</a>
@else
<i class="zmdi zmdi-lock-outline"></i>Chapter restricted
<i class="zmdi zmdi-lock-outline"></i>Chapter Permissions Active
@endif
<br>
@endif
@if($page->restricted)
@if(userCan('restrictions-manage', $page))
<a href="{{ $page->getUrl() }}/restrict"><i class="zmdi zmdi-lock-outline"></i>Page restricted</a>
<a href="{{ $page->getUrl() }}/permissions"><i class="zmdi zmdi-lock-outline"></i>Page Permissions Active</a>
@else
<i class="zmdi zmdi-lock-outline"></i>Page restricted
<i class="zmdi zmdi-lock-outline"></i>Page Permissions Active
@endif
<br>
@endif

View File

@ -24,10 +24,10 @@
<hr class="even">
<div class="row">
<div class="col-md-6">
<label>@include('settings/roles/checkbox', ['permission' => 'restrictions-manage-all']) Manage all restrictions</label>
<label>@include('settings/roles/checkbox', ['permission' => 'restrictions-manage-all']) Manage all Book, Chapter & Page permissions</label>
</div>
<div class="col-md-6">
<label>@include('settings/roles/checkbox', ['permission' => 'restrictions-manage-own']) Manage restrictions on own content</label>
<label>@include('settings/roles/checkbox', ['permission' => 'restrictions-manage-own']) Manage permissions on own Book, Chapter & Pages</label>
</div>
</div>
<hr class="even">
@ -43,7 +43,7 @@
<h3>Asset Permissions</h3>
<p>
These permissions control default access to the assets within the system. <br>
Restrictions on Books, Chapters and Pages will override these permissions.
Permissions on Books, Chapters and Pages will override these permissions.
</p>
<table class="table">
<tr>

View File

@ -10,7 +10,7 @@
<form action="/settings/users/{{$user->id}}" method="POST">
{!! csrf_field() !!}
<input type="hidden" name="_method" value="DELETE">
<a href="/users/{{$user->id}}" class="button muted">Cancel</a>
<a href="/settings/users/{{$user->id}}" class="button muted">Cancel</a>
<button type="submit" class="button neg">Confirm</button>
</form>
</div>

View File

@ -3,11 +3,21 @@
class RestrictionsTest extends TestCase
{
protected $user;
protected $viewer;
public function setUp()
{
parent::setUp();
$this->user = $this->getNewUser();
$this->viewer = $this->getViewer();
}
protected function getViewer()
{
$role = \BookStack\Role::getRole('viewer');
$viewer = $this->getNewBlankUser();
$viewer->attachRole($role);;
return $viewer;
}
/**
@ -20,11 +30,16 @@ class RestrictionsTest extends TestCase
$entity->restricted = true;
$entity->restrictions()->delete();
$role = $this->user->roles->first();
$viewerRole = $this->viewer->roles->first();
foreach ($actions as $action) {
$entity->restrictions()->create([
'role_id' => $role->id,
'action' => strtolower($action)
]);
$entity->restrictions()->create([
'role_id' => $viewerRole->id,
'action' => strtolower($action)
]);
}
$entity->save();
$entity->load('restrictions');
@ -65,6 +80,10 @@ class RestrictionsTest extends TestCase
$book = \BookStack\Book::first();
$bookUrl = $book->getUrl();
$this->actingAs($this->viewer)
->visit($bookUrl)
->dontSeeInElement('.action-buttons', 'New Page')
->dontSeeInElement('.action-buttons', 'New Chapter');
$this->actingAs($this->user)
->visit($bookUrl)
->seeInElement('.action-buttons', 'New Page')
@ -319,11 +338,11 @@ class RestrictionsTest extends TestCase
public function test_book_restriction_form()
{
$book = \BookStack\Book::first();
$this->asAdmin()->visit($book->getUrl() . '/restrict')
->see('Book Restrictions')
$this->asAdmin()->visit($book->getUrl() . '/permissions')
->see('Book Permissions')
->check('restricted')
->check('restrictions[2][view]')
->press('Save Restrictions')
->press('Save Permissions')
->seeInDatabase('books', ['id' => $book->id, 'restricted' => true])
->seeInDatabase('restrictions', [
'restrictable_id' => $book->id,
@ -336,11 +355,11 @@ class RestrictionsTest extends TestCase
public function test_chapter_restriction_form()
{
$chapter = \BookStack\Chapter::first();
$this->asAdmin()->visit($chapter->getUrl() . '/restrict')
->see('Chapter Restrictions')
$this->asAdmin()->visit($chapter->getUrl() . '/permissions')
->see('Chapter Permissions')
->check('restricted')
->check('restrictions[2][update]')
->press('Save Restrictions')
->press('Save Permissions')
->seeInDatabase('chapters', ['id' => $chapter->id, 'restricted' => true])
->seeInDatabase('restrictions', [
'restrictable_id' => $chapter->id,
@ -353,11 +372,11 @@ class RestrictionsTest extends TestCase
public function test_page_restriction_form()
{
$page = \BookStack\Page::first();
$this->asAdmin()->visit($page->getUrl() . '/restrict')
->see('Page Restrictions')
$this->asAdmin()->visit($page->getUrl() . '/permissions')
->see('Page Permissions')
->check('restricted')
->check('restrictions[2][delete]')
->press('Save Restrictions')
->press('Save Permissions')
->seeInDatabase('pages', ['id' => $page->id, 'restricted' => true])
->seeInDatabase('restrictions', [
'restrictable_id' => $page->id,
@ -404,4 +423,99 @@ class RestrictionsTest extends TestCase
->dontSee($page->name);
}
public function test_book_create_restriction_override()
{
$book = \BookStack\Book::first();
$bookUrl = $book->getUrl();
$this->actingAs($this->viewer)
->visit($bookUrl)
->dontSeeInElement('.action-buttons', 'New Page')
->dontSeeInElement('.action-buttons', 'New Chapter');
$this->setEntityRestrictions($book, ['view', 'delete', 'update']);
$this->forceVisit($bookUrl . '/chapter/create')
->see('You do not have permission')->seePageIs('/');
$this->forceVisit($bookUrl . '/page/create')
->see('You do not have permission')->seePageIs('/');
$this->visit($bookUrl)->dontSeeInElement('.action-buttons', 'New Page')
->dontSeeInElement('.action-buttons', 'New Chapter');
$this->setEntityRestrictions($book, ['view', 'create']);
$this->visit($bookUrl . '/chapter/create')
->type('test chapter', 'name')
->type('test description for chapter', 'description')
->press('Save Chapter')
->seePageIs($bookUrl . '/chapter/test-chapter');
$this->visit($bookUrl . '/page/create')
->type('test page', 'name')
->type('test content', 'html')
->press('Save Page')
->seePageIs($bookUrl . '/page/test-page');
$this->visit($bookUrl)->seeInElement('.action-buttons', 'New Page')
->seeInElement('.action-buttons', 'New Chapter');
}
public function test_book_update_restriction_override()
{
$book = \BookStack\Book::first();
$bookPage = $book->pages->first();
$bookChapter = $book->chapters->first();
$bookUrl = $book->getUrl();
$this->actingAs($this->viewer)
->visit($bookUrl . '/edit')
->dontSee('Edit Book');
$this->setEntityRestrictions($book, ['view', 'delete']);
$this->forceVisit($bookUrl . '/edit')
->see('You do not have permission')->seePageIs('/');
$this->forceVisit($bookPage->getUrl() . '/edit')
->see('You do not have permission')->seePageIs('/');
$this->forceVisit($bookChapter->getUrl() . '/edit')
->see('You do not have permission')->seePageIs('/');
$this->setEntityRestrictions($book, ['view', 'update']);
$this->visit($bookUrl . '/edit')
->seePageIs($bookUrl . '/edit');
$this->visit($bookPage->getUrl() . '/edit')
->seePageIs($bookPage->getUrl() . '/edit');
$this->visit($bookChapter->getUrl() . '/edit')
->see('Edit Chapter');
}
public function test_book_delete_restriction_override()
{
$book = \BookStack\Book::first();
$bookPage = $book->pages->first();
$bookChapter = $book->chapters->first();
$bookUrl = $book->getUrl();
$this->actingAs($this->viewer)
->visit($bookUrl . '/delete')
->dontSee('Delete Book');
$this->setEntityRestrictions($book, ['view', 'update']);
$this->forceVisit($bookUrl . '/delete')
->see('You do not have permission')->seePageIs('/');
$this->forceVisit($bookPage->getUrl() . '/delete')
->see('You do not have permission')->seePageIs('/');
$this->forceVisit($bookChapter->getUrl() . '/delete')
->see('You do not have permission')->seePageIs('/');
$this->setEntityRestrictions($book, ['view', 'delete']);
$this->visit($bookUrl . '/delete')
->seePageIs($bookUrl . '/delete')->see('Delete Book');
$this->visit($bookPage->getUrl() . '/delete')
->seePageIs($bookPage->getUrl() . '/delete')->see('Delete Page');
$this->visit($bookChapter->getUrl() . '/delete')
->see('Delete Chapter');
}
}

View File

@ -129,14 +129,14 @@ class RolesTest extends TestCase
{
$page = \BookStack\Page::take(1)->get()->first();
$this->actingAs($this->user)->visit($page->getUrl())
->dontSee('Restrict')
->visit($page->getUrl() . '/restrict')
->dontSee('Permissions')
->visit($page->getUrl() . '/permissions')
->seePageIs('/');
$this->giveUserPermissions($this->user, ['restrictions-manage-all']);
$this->actingAs($this->user)->visit($page->getUrl())
->see('Restrict')
->click('Restrict')
->see('Page Restrictions')->seePageIs($page->getUrl() . '/restrict');
->see('Permissions')
->click('Permissions')
->see('Page Permissions')->seePageIs($page->getUrl() . '/permissions');
}
public function test_restrictions_manage_own_permission()
@ -145,27 +145,27 @@ class RolesTest extends TestCase
$content = $this->createEntityChainBelongingToUser($this->user);
// Check can't restrict other's content
$this->actingAs($this->user)->visit($otherUsersPage->getUrl())
->dontSee('Restrict')
->visit($otherUsersPage->getUrl() . '/restrict')
->dontSee('Permissions')
->visit($otherUsersPage->getUrl() . '/permissions')
->seePageIs('/');
// Check can't restrict own content
$this->actingAs($this->user)->visit($content['page']->getUrl())
->dontSee('Restrict')
->visit($content['page']->getUrl() . '/restrict')
->dontSee('Permissions')
->visit($content['page']->getUrl() . '/permissions')
->seePageIs('/');
$this->giveUserPermissions($this->user, ['restrictions-manage-own']);
// Check can't restrict other's content
$this->actingAs($this->user)->visit($otherUsersPage->getUrl())
->dontSee('Restrict')
->visit($otherUsersPage->getUrl() . '/restrict')
->dontSee('Permissions')
->visit($otherUsersPage->getUrl() . '/permissions')
->seePageIs('/');
// Check can restrict own content
$this->actingAs($this->user)->visit($content['page']->getUrl())
->see('Restrict')
->click('Restrict')
->seePageIs($content['page']->getUrl() . '/restrict');
->see('Permissions')
->click('Permissions')
->seePageIs($content['page']->getUrl() . '/permissions');
}
/**

View File

@ -170,4 +170,12 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
$this->visit($link->link()->getUri());
return $this;
}
protected function actingAsUsers($usersArray, $callback)
{
foreach ($usersArray as $user) {
$this->actingAs($user);
$callback($user);
}
}
}