public function __construct(ExportFormatter $exportFormatter)
{
$this->exportFormatter = $exportFormatter;
+ $this->middleware('can:content-export');
}
/**
public function __construct(ExportFormatter $exportFormatter)
{
$this->exportFormatter = $exportFormatter;
+ $this->middleware('can:content-export');
}
/**
public function __construct(ExportFormatter $exportFormatter)
{
$this->exportFormatter = $exportFormatter;
+ $this->middleware('can:content-export');
}
/**
{
$this->bookRepo = $bookRepo;
$this->exportFormatter = $exportFormatter;
+ $this->middleware('can:content-export');
}
/**
{
$this->chapterRepo = $chapterRepo;
$this->exportFormatter = $exportFormatter;
+ $this->middleware('can:content-export');
}
/**
{
$this->pageRepo = $pageRepo;
$this->exportFormatter = $exportFormatter;
+ $this->middleware('can:content-export');
}
/**
*/
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,
];
--- /dev/null
+<?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('/');
+ }
+}
+++ /dev/null
-<?php
-
-namespace BookStack\Http\Middleware;
-
-use Closure;
-
-class PermissionMiddleware
-{
- /**
- * Handle an incoming request.
- *
- * @param \Illuminate\Http\Request $request
- * @param \Closure $next
- * @param $permission
- *
- * @return mixed
- */
- public function handle($request, Closure $next, $permission)
- {
- if (!$request->user() || !$request->user()->can($permission)) {
- session()->flash('error', trans('errors.permission'));
-
- return redirect()->back();
- }
-
- return $next($request);
- }
-}
--- /dev/null
+<?php
+
+use Carbon\Carbon;
+use Illuminate\Database\Migrations\Migration;
+
+class AddExportRolePermission extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ // Create new templates-manage permission and assign to admin role
+ $roles = \Illuminate\Support\Facades\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();
+ }
+}
'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.',
@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>
@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
@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>
<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>
<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">
$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);
+ }
+ }
}
$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);
+ }
+ }
}
$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);
+ }
+ }
}
namespace Tests\Entity;
+use BookStack\Auth\Role;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
$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);
+ }
+ }
}
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;
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;
$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.
*/
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.'])
+ )
+ );
}
/**