Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/Http/Controllers/Api/BookExportApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class BookExportApiController extends ApiController
public function __construct(ExportFormatter $exportFormatter)
{
$this->exportFormatter = $exportFormatter;
$this->middleware('can:content-export');
}

/**
Expand Down
1 change: 1 addition & 0 deletions app/Http/Controllers/Api/ChapterExportApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class ChapterExportApiController extends ApiController
public function __construct(ExportFormatter $exportFormatter)
{
$this->exportFormatter = $exportFormatter;
$this->middleware('can:content-export');
}

/**
Expand Down
1 change: 1 addition & 0 deletions app/Http/Controllers/Api/PageExportApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class PageExportApiController extends ApiController
public function __construct(ExportFormatter $exportFormatter)
{
$this->exportFormatter = $exportFormatter;
$this->middleware('can:content-export');
}

/**
Expand Down
1 change: 1 addition & 0 deletions app/Http/Controllers/BookExportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public function __construct(BookRepo $bookRepo, ExportFormatter $exportFormatter
{
$this->bookRepo = $bookRepo;
$this->exportFormatter = $exportFormatter;
$this->middleware('can:content-export');
}

/**
Expand Down
1 change: 1 addition & 0 deletions app/Http/Controllers/ChapterExportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public function __construct(ChapterRepo $chapterRepo, ExportFormatter $exportFor
{
$this->chapterRepo = $chapterRepo;
$this->exportFormatter = $exportFormatter;
$this->middleware('can:content-export');
}

/**
Expand Down
1 change: 1 addition & 0 deletions app/Http/Controllers/PageExportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public function __construct(PageRepo $pageRepo, ExportFormatter $exportFormatter
{
$this->pageRepo = $pageRepo;
$this->exportFormatter = $exportFormatter;
$this->middleware('can:content-export');
}

/**
Expand Down
3 changes: 1 addition & 2 deletions app/Http/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,9 @@ class Kernel extends HttpKernel
*/
protected $routeMiddleware = [
'auth' => \BookStack\Http\Middleware\Authenticate::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'can' => \BookStack\Http\Middleware\CheckUserHasPermission::class,
'guest' => \BookStack\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'perm' => \BookStack\Http\Middleware\PermissionMiddleware::class,
'guard' => \BookStack\Http\Middleware\CheckGuard::class,
'mfa-setup' => \BookStack\Http\Middleware\AuthenticatedOrPendingMfa::class,
];
Expand Down
38 changes: 38 additions & 0 deletions app/Http/Middleware/CheckUserHasPermission.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace BookStack\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class CheckUserHasPermission
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param $permission
*
* @return mixed
*/
public function handle($request, Closure $next, $permission)
{
if (!user()->can($permission)) {
return $this->errorResponse($request);
}

return $next($request);
}

protected function errorResponse(Request $request)
{
if ($request->wantsJson()) {
return response()->json(['error' => trans('errors.permissionJson')], 403);
}

session()->flash('error', trans('errors.permission'));

return redirect('/');
}
}
28 changes: 0 additions & 28 deletions app/Http/Middleware/PermissionMiddleware.php

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

use Carbon\Carbon;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;

class AddExportRolePermission extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// Create new templates-manage permission and assign to admin role
$roles = DB::table('roles')->get('id');
$permissionId = DB::table('role_permissions')->insertGetId([
'name' => 'content-export',
'display_name' => 'Export Content',
'created_at' => Carbon::now()->toDateTimeString(),
'updated_at' => Carbon::now()->toDateTimeString(),
]);

$permissionRoles = $roles->map(function ($role) use ($permissionId) {
return [
'role_id' => $role->id,
'permission_id' => $permissionId,
];
})->values()->toArray();

DB::table('permission_role')->insert($permissionRoles);
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
// Remove content-export permission
$contentExportPermission = DB::table('role_permissions')
->where('name', '=', 'content-export')->first();

DB::table('permission_role')->where('permission_id', '=', $contentExportPermission->id)->delete();
DB::table('role_permissions')->where('id', '=', 'content-export')->delete();
}
}
1 change: 1 addition & 0 deletions resources/lang/en/settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@
'role_manage_page_templates' => 'Manage page templates',
'role_access_api' => 'Access system API',
'role_manage_settings' => 'Manage app settings',
'role_export_content' => 'Export content',
'role_asset' => 'Asset Permissions',
'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.',
'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',
Expand Down
4 changes: 3 additions & 1 deletion resources/views/books/show.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,9 @@
@if(signedInUser())
@include('entities.favourite-action', ['entity' => $book])
@endif
@include('entities.export-menu', ['entity' => $book])
@if(userCan('content-export'))
@include('entities.export-menu', ['entity' => $book])
@endif
</div>
</div>

Expand Down
4 changes: 3 additions & 1 deletion resources/views/chapters/show.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@
@if(signedInUser())
@include('entities.favourite-action', ['entity' => $chapter])
@endif
@include('entities.export-menu', ['entity' => $chapter])
@if(userCan('content-export'))
@include('entities.export-menu', ['entity' => $chapter])
@endif
</div>
</div>
@stop
Expand Down
4 changes: 3 additions & 1 deletion resources/views/pages/show.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,9 @@
@if(signedInUser())
@include('entities.favourite-action', ['entity' => $page])
@endif
@include('entities.export-menu', ['entity' => $page])
@if(userCan('content-export'))
@include('entities.export-menu', ['entity' => $page])
@endif
</div>

</div>
Expand Down
3 changes: 2 additions & 1 deletion resources/views/settings/roles/parts/form.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
<div>@include('settings.roles.parts.checkbox', ['permission' => 'restrictions-manage-own', 'label' => trans('settings.role_manage_own_entity_permissions')])</div>
<div>@include('settings.roles.parts.checkbox', ['permission' => 'templates-manage', 'label' => trans('settings.role_manage_page_templates')])</div>
<div>@include('settings.roles.parts.checkbox', ['permission' => 'access-api', 'label' => trans('settings.role_access_api')])</div>
<div>@include('settings.roles.parts.checkbox', ['permission' => 'content-export', 'label' => trans('settings.role_export_content')])</div>
</div>
<div>
<div>@include('settings.roles.parts.checkbox', ['permission' => 'settings-manage', 'label' => trans('settings.role_manage_settings')])</div>
Expand Down Expand Up @@ -239,7 +240,7 @@

<div class="card content-wrap auto-height">
<h2 class="list-heading">{{ trans('settings.role_users') }}</h2>
@if(isset($role) && count($role->users) > 0)
@if(count($role->users ?? []) > 0)
<div class="grid third">
@foreach($role->users as $user)
<div class="user-list-item">
Expand Down
13 changes: 13 additions & 0 deletions tests/Api/BooksApiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,17 @@ public function test_export_markdown_endpoint()
$resp->assertSee('# ' . $book->pages()->first()->name);
$resp->assertSee('# ' . $book->chapters()->first()->name);
}

public function test_cant_export_when_not_have_permission()
{
$types = ['html', 'plaintext', 'pdf', 'markdown'];
$this->actingAsApiEditor();
$this->removePermissionFromUser($this->getEditor(), 'content-export');

$book = Book::visible()->first();
foreach ($types as $type) {
$resp = $this->get($this->baseEndpoint . "/{$book->id}/export/{$type}");
$this->assertPermissionError($resp);
}
}
}
13 changes: 13 additions & 0 deletions tests/Api/ChaptersApiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,17 @@ public function test_export_markdown_endpoint()
$resp->assertSee('# ' . $chapter->name);
$resp->assertSee('# ' . $chapter->pages()->first()->name);
}

public function test_cant_export_when_not_have_permission()
{
$types = ['html', 'plaintext', 'pdf', 'markdown'];
$this->actingAsApiEditor();
$this->removePermissionFromUser($this->getEditor(), 'content-export');

$chapter = Chapter::visible()->has('pages')->first();
foreach ($types as $type) {
$resp = $this->get($this->baseEndpoint . "/{$chapter->id}/export/{$type}");
$this->assertPermissionError($resp);
}
}
}
13 changes: 13 additions & 0 deletions tests/Api/PagesApiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -292,4 +292,17 @@ public function test_export_markdown_endpoint()
$resp->assertSee('# ' . $page->name);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.md"');
}

public function test_cant_export_when_not_have_permission()
{
$types = ['html', 'plaintext', 'pdf', 'markdown'];
$this->actingAsApiEditor();
$this->removePermissionFromUser($this->getEditor(), 'content-export');

$page = Page::visible()->first();
foreach ($types as $type) {
$resp = $this->get($this->baseEndpoint . "/{$page->id}/export/{$type}");
$this->assertPermissionError($resp);
}
}
}
26 changes: 26 additions & 0 deletions tests/Entity/ExportTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Tests\Entity;

use BookStack\Auth\Role;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
Expand Down Expand Up @@ -340,4 +341,29 @@ public function test_book_markdown_export()
$resp->assertSee('# ' . $chapter->name);
$resp->assertSee('# ' . $page->name);
}

public function test_export_option_only_visible_and_accessible_with_permission()
{
$book = Book::query()->whereHas('pages')->whereHas('chapters')->first();
$chapter = $book->chapters()->first();
$page = $chapter->pages()->first();
$entities = [$book, $chapter, $page];
$user = $this->getViewer();
$this->actingAs($user);

foreach ($entities as $entity) {
$resp = $this->get($entity->getUrl());
$resp->assertSee('/export/pdf');
}

/** @var Role $role */
$this->removePermissionFromUser($user, 'content-export');

foreach ($entities as $entity) {
$resp = $this->get($entity->getUrl());
$resp->assertDontSee('/export/pdf');
$resp = $this->get($entity->getUrl('/export/pdf'));
$this->assertPermissionError($resp);
}
}
}
28 changes: 26 additions & 2 deletions tests/SharedTestHelpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use BookStack\Auth\Permissions\PermissionService;
use BookStack\Auth\Permissions\PermissionsRepo;
use BookStack\Auth\Permissions\RolePermission;
use BookStack\Auth\Role;
use BookStack\Auth\User;
use BookStack\Entities\Models\Book;
Expand All @@ -18,6 +19,7 @@
use BookStack\Settings\SettingService;
use BookStack\Uploads\HttpFetcher;
use Illuminate\Foundation\Testing\Assert as PHPUnit;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Env;
use Illuminate\Support\Facades\Log;
use Mockery;
Expand Down Expand Up @@ -184,6 +186,19 @@ protected function giveUserPermissions(User $user, array $permissions = []): voi
$user->clearPermissionCache();
}

/**
* Completely remove the given permission name from the given user.
*/
protected function removePermissionFromUser(User $user, string $permission)
{
$permission = RolePermission::query()->where('name', '=', $permission)->first();
/** @var Role $role */
foreach ($user->roles as $role) {
$role->detachPermission($permission);
}
$user->clearPermissionCache();
}

/**
* Create a new basic role for testing purposes.
*/
Expand Down Expand Up @@ -274,8 +289,17 @@ protected function assertNotPermissionError($response)
private function isPermissionError($response): bool
{
return $response->status() === 302
&& $response->headers->get('Location') === url('/')
&& strpos(session()->pull('error', ''), 'You do not have permission to access') === 0;
&& (
(
$response->headers->get('Location') === url('/')
&& strpos(session()->pull('error', ''), 'You do not have permission to access') === 0
)
||
(
$response instanceof JsonResponse &&
$response->json(['error' => 'You do not have permission to perform the requested action.'])
)
);
}

/**
Expand Down