From befa3a8fbb23cb8026fc646b9c8e8617e095b91e Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 19 Apr 2026 12:41:11 +0100 Subject: [PATCH 1/5] Permissions: Started addition of revision-view permission --- .../Controllers/PageRevisionController.php | 7 ++++++ app/Permissions/Permission.php | 2 ++ lang/en/settings.php | 1 + resources/views/entities/meta.blade.php | 2 +- .../show-sidebar-section-actions.blade.php | 10 +++++---- .../views/settings/roles/parts/form.blade.php | 1 + .../parts/revisions-permissions-row.blade.php | 22 +++++++++++++++++++ 7 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 resources/views/settings/roles/parts/revisions-permissions-row.blade.php diff --git a/app/Entities/Controllers/PageRevisionController.php b/app/Entities/Controllers/PageRevisionController.php index 4bc15e6e967..0d690cb2c33 100644 --- a/app/Entities/Controllers/PageRevisionController.php +++ b/app/Entities/Controllers/PageRevisionController.php @@ -34,6 +34,7 @@ public function __construct( */ public function index(Request $request, string $bookSlug, string $pageSlug) { + $this->checkPermission(Permission::RevisionViewAll); $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); $listOptions = SimpleListOptions::fromRequest($request, 'page_revisions', true)->withSortOptions([ 'id' => trans('entities.pages_revisions_sort_number') @@ -65,6 +66,8 @@ public function index(Request $request, string $bookSlug, string $pageSlug) */ public function show(string $bookSlug, string $pageSlug, int $revisionId) { + $this->checkPermission(Permission::RevisionViewAll); + $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); /** @var ?PageRevision $revision */ $revision = $page->revisions()->where('id', '=', $revisionId)->first(); @@ -94,6 +97,8 @@ public function show(string $bookSlug, string $pageSlug, int $revisionId) */ public function changes(string $bookSlug, string $pageSlug, int $revisionId) { + $this->checkPermission(Permission::RevisionViewAll); + $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); /** @var ?PageRevision $revision */ $revision = $page->revisions()->where('id', '=', $revisionId)->first(); @@ -130,6 +135,7 @@ public function changes(string $bookSlug, string $pageSlug, int $revisionId) public function restore(string $bookSlug, string $pageSlug, int $revisionId) { $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); + $this->checkPermission(Permission::RevisionViewAll); $this->checkOwnablePermission(Permission::PageUpdate, $page); $page = $this->pageRepo->restoreRevision($page, $revisionId); @@ -145,6 +151,7 @@ public function restore(string $bookSlug, string $pageSlug, int $revisionId) public function destroy(string $bookSlug, string $pageSlug, int $revId) { $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); + $this->checkPermission(Permission::RevisionViewAll); $this->checkOwnablePermission(Permission::PageDelete, $page); $revision = $page->revisions()->where('id', '=', $revId)->first(); diff --git a/app/Permissions/Permission.php b/app/Permissions/Permission.php index 04878ada01f..0fbe9693dcb 100644 --- a/app/Permissions/Permission.php +++ b/app/Permissions/Permission.php @@ -118,6 +118,8 @@ enum Permission: string case PageViewAll = 'page-view-all'; case PageViewOwn = 'page-view-own'; + case RevisionViewAll = 'revision-view-all'; + /** * Get the generic permissions which may be queried for entities. */ diff --git a/lang/en/settings.php b/lang/en/settings.php index c4d1eb136eb..3937c650f86 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -207,6 +207,7 @@ 'role_all' => 'All', 'role_own' => 'Own', 'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Save Role', 'role_users' => 'Users in this role', 'role_users_none' => 'No users are currently assigned to this role', diff --git a/resources/views/entities/meta.blade.php b/resources/views/entities/meta.blade.php index 060c197a466..6c425a2401b 100644 --- a/resources/views/entities/meta.blade.php +++ b/resources/views/entities/meta.blade.php @@ -9,7 +9,7 @@ @endif - @if ($entity->isA('page')) + @if ($entity->isA('page') && userCan(\BookStack\Permissions\Permission::RevisionViewAll)) @icon('history'){{ trans('entities.meta_revision', ['revisionCount' => $entity->revision_count]) }} diff --git a/resources/views/pages/parts/show-sidebar-section-actions.blade.php b/resources/views/pages/parts/show-sidebar-section-actions.blade.php index ae115b69e23..94061ecb3a8 100644 --- a/resources/views/pages/parts/show-sidebar-section-actions.blade.php +++ b/resources/views/pages/parts/show-sidebar-section-actions.blade.php @@ -24,10 +24,12 @@ @endif @endif - - @icon('history') - {{ trans('entities.revisions') }} - + @if(userCan(\BookStack\Permissions\Permission::RevisionViewAll)) + + @icon('history') + {{ trans('entities.revisions') }} + + @endif @if(userCan(\BookStack\Permissions\Permission::RestrictionsManage, $page)) @icon('lock') diff --git a/resources/views/settings/roles/parts/form.blade.php b/resources/views/settings/roles/parts/form.blade.php index 5a9eca7d2cd..890f790574e 100644 --- a/resources/views/settings/roles/parts/form.blade.php +++ b/resources/views/settings/roles/parts/form.blade.php @@ -79,6 +79,7 @@ class="item-list toggle-switch-list"> @include('settings.roles.parts.asset-permissions-row', ['title' => trans('entities.books'), 'permissionPrefix' => 'book']) @include('settings.roles.parts.asset-permissions-row', ['title' => trans('entities.chapters'), 'permissionPrefix' => 'chapter']) @include('settings.roles.parts.asset-permissions-row', ['title' => trans('entities.pages'), 'permissionPrefix' => 'page']) + @include('settings.roles.parts.revisions-permissions-row', ['title' => trans('entities.revisions'), 'permissionPrefix' => 'revision']) @include('settings.roles.parts.related-asset-permissions-row', ['title' => trans('entities.images'), 'permissionPrefix' => 'image']) @include('settings.roles.parts.related-asset-permissions-row', ['title' => trans('entities.attachments'), 'permissionPrefix' => 'attachment']) @include('settings.roles.parts.related-asset-permissions-row', ['title' => trans('entities.comments'), 'permissionPrefix' => 'comment']) diff --git a/resources/views/settings/roles/parts/revisions-permissions-row.blade.php b/resources/views/settings/roles/parts/revisions-permissions-row.blade.php new file mode 100644 index 00000000000..fe886a5d0e1 --- /dev/null +++ b/resources/views/settings/roles/parts/revisions-permissions-row.blade.php @@ -0,0 +1,22 @@ +
+ +
+ {{ trans('common.create') }}
+ - +
+
+ {{ trans('common.view') }}
+ @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-view-all', 'label' => trans('settings.role_all')]) +
+
+ {{ trans('common.edit') }}
+ - +
+
+ {{ trans('common.delete') }}
+ {{ trans('settings.role_controlled_by_page_delete') }} +
+
\ No newline at end of file From 1339f668ebfd0155c15c122f4257e435d23f9a11 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 19 Apr 2026 15:32:10 +0100 Subject: [PATCH 2/5] Permissions: Added revision-view-all addition migration --- ...41616_add_revision_view_all_permission.php | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 database/migrations/2026_04_19_141616_add_revision_view_all_permission.php diff --git a/database/migrations/2026_04_19_141616_add_revision_view_all_permission.php b/database/migrations/2026_04_19_141616_add_revision_view_all_permission.php new file mode 100644 index 00000000000..5a0b9a09b40 --- /dev/null +++ b/database/migrations/2026_04_19_141616_add_revision_view_all_permission.php @@ -0,0 +1,67 @@ +insertGetId([ + 'name' => 'revision-view-all', + 'created_at' => Carbon::now()->toDateTimeString(), + 'updated_at' => Carbon::now()->toDateTimeString(), + ]); + + // Get ids of page view permissions + $pageViewPermissions = DB::table('role_permissions') + ->whereIn('name', [ + 'page-view-own', + 'page-view-all', + ])->get(); + + if (!$pageViewPermissions->count() === 0) { + return; + } + + // Get role ids which have page view permission + $applicableRoleIds = DB::table('permission_role') + ->whereIn('permission_id', $pageViewPermissions->pluck('id')) + ->pluck('role_id') + ->unique() + ->all(); + + // Assign the new permission to relevant roles + $newPermissionRoles = array_values(array_map(function (int $roleId) use ($permissionId) { + return [ + 'role_id' => $roleId, + 'permission_id' => $permissionId, + ]; + }, $applicableRoleIds)); + + DB::table('permission_role')->insert($newPermissionRoles); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // Get the permission to remove + $revisionViewPermission = DB::table('role_permissions') + ->where('name', '=', 'revision-view-all') + ->first(); + + if (!$revisionViewPermission) { + return; + } + + // Remove the permission, and its use on roles, from the database + DB::table('permission_role')->where('permission_id', '=', $revisionViewPermission->id)->delete(); + DB::table('role_permissions')->where('id', '=', $revisionViewPermission->id)->delete(); + } +}; From e7e019d3d44b263031d4a63f91e4c6bdf3b424eb Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 19 Apr 2026 15:56:54 +0100 Subject: [PATCH 3/5] Permissions: Added testing coverage for revision-view-all --- tests/Entity/PageRevisionTest.php | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/Entity/PageRevisionTest.php b/tests/Entity/PageRevisionTest.php index 132a10fa4da..8b46e84a634 100644 --- a/tests/Entity/PageRevisionTest.php +++ b/tests/Entity/PageRevisionTest.php @@ -4,6 +4,8 @@ use BookStack\Activity\ActivityType; use BookStack\Entities\Models\Page; +use BookStack\Entities\Models\PageRevision; +use BookStack\Permissions\Permission; use Tests\TestCase; class PageRevisionTest extends TestCase @@ -257,6 +259,33 @@ public function test_revision_changes_view_filters_html_content() $revisionView->assertDontSee('dontwantthishere'); } + public function test_access_to_revision_operation_requires_revision_view_all_permission() + { + $editor = $this->users->editor(); + $this->actingAs($editor); + + $page = $this->entities->page(); + $this->createRevisions($page, 3); + /** @var PageRevision $revision */ + $revision = $page->revisions()->orderBy('id', 'desc')->first(); + + $this->get($page->getUrl())->assertSee($page->getUrl('/revisions'), false); + $this->get($page->getUrl('/revisions'))->assertOk(); + $this->get($revision->getUrl())->assertOk(); + $this->get($revision->getUrl('/changes'))->assertOk(); + $this->put($revision->getUrl('/restore'))->assertRedirect($page->getUrl()); + $this->delete($revision->getUrl('/delete'))->assertRedirect($page->getUrl('/revisions')); + + $this->permissions->removeUserRolePermissions($editor, [Permission::RevisionViewAll]); + + $this->get($page->getUrl())->assertDontSee($page->getUrl('/revisions'), false); + $this->assertPermissionError($this->get($page->getUrl('/revisions'))); + $this->assertPermissionError($this->get($revision->getUrl())); + $this->assertPermissionError($this->get($revision->getUrl('/changes'))); + $this->assertPermissionError($this->put($revision->getUrl('/restore'))); + $this->assertPermissionError($this->delete($revision->getUrl('/delete'))); + } + public function test_revision_restore_action_only_visible_with_permission() { $page = $this->entities->page(); From ec0b0384a20f10a5ec44197a3fd5ca8f9fc543aa Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 19 Apr 2026 16:06:31 +0100 Subject: [PATCH 4/5] Permissions: Tweaks/fixed during review of revision-view-all changes --- app/Entities/Controllers/PageRevisionController.php | 4 ++-- .../2026_04_19_141616_add_revision_view_all_permission.php | 2 +- .../settings/roles/parts/revisions-permissions-row.blade.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Entities/Controllers/PageRevisionController.php b/app/Entities/Controllers/PageRevisionController.php index 0d690cb2c33..cc6b79bfe45 100644 --- a/app/Entities/Controllers/PageRevisionController.php +++ b/app/Entities/Controllers/PageRevisionController.php @@ -134,8 +134,8 @@ public function changes(string $bookSlug, string $pageSlug, int $revisionId) */ public function restore(string $bookSlug, string $pageSlug, int $revisionId) { - $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); $this->checkPermission(Permission::RevisionViewAll); + $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); $this->checkOwnablePermission(Permission::PageUpdate, $page); $page = $this->pageRepo->restoreRevision($page, $revisionId); @@ -150,8 +150,8 @@ public function restore(string $bookSlug, string $pageSlug, int $revisionId) */ public function destroy(string $bookSlug, string $pageSlug, int $revId) { - $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); $this->checkPermission(Permission::RevisionViewAll); + $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); $this->checkOwnablePermission(Permission::PageDelete, $page); $revision = $page->revisions()->where('id', '=', $revId)->first(); diff --git a/database/migrations/2026_04_19_141616_add_revision_view_all_permission.php b/database/migrations/2026_04_19_141616_add_revision_view_all_permission.php index 5a0b9a09b40..e4b51ff7026 100644 --- a/database/migrations/2026_04_19_141616_add_revision_view_all_permission.php +++ b/database/migrations/2026_04_19_141616_add_revision_view_all_permission.php @@ -24,7 +24,7 @@ public function up(): void 'page-view-all', ])->get(); - if (!$pageViewPermissions->count() === 0) { + if ($pageViewPermissions->count() === 0) { return; } diff --git a/resources/views/settings/roles/parts/revisions-permissions-row.blade.php b/resources/views/settings/roles/parts/revisions-permissions-row.blade.php index fe886a5d0e1..326925ef93c 100644 --- a/resources/views/settings/roles/parts/revisions-permissions-row.blade.php +++ b/resources/views/settings/roles/parts/revisions-permissions-row.blade.php @@ -19,4 +19,4 @@ {{ trans('common.delete') }}
{{ trans('settings.role_controlled_by_page_delete') }} - \ No newline at end of file + From 426f9ac4934308da9f57580bc0e2fe399346cbb1 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 19 Apr 2026 16:23:16 +0100 Subject: [PATCH 5/5] Permissions: Prevent export revision metadata view without permission --- resources/views/exports/parts/meta.blade.php | 2 +- tests/Exports/HtmlExportTest.php | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/resources/views/exports/parts/meta.blade.php b/resources/views/exports/parts/meta.blade.php index 00117f4a157..07eff14a470 100644 --- a/resources/views/exports/parts/meta.blade.php +++ b/resources/views/exports/parts/meta.blade.php @@ -1,5 +1,5 @@
- @if ($entity->isA('page')) + @if ($entity->isA('page') && userCan(\BookStack\Permissions\Permission::RevisionViewAll)) @icon('history'){{ trans('entities.meta_revision', ['revisionCount' => $entity->revision_count]) }}
@endif diff --git a/tests/Exports/HtmlExportTest.php b/tests/Exports/HtmlExportTest.php index f23352e0eb9..223a8c92285 100644 --- a/tests/Exports/HtmlExportTest.php +++ b/tests/Exports/HtmlExportTest.php @@ -5,6 +5,7 @@ use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Page; +use BookStack\Permissions\Permission; use Illuminate\Support\Facades\Storage; use Tests\TestCase; @@ -229,6 +230,20 @@ public function test_page_export_with_deleted_creator_and_updater() $resp->assertDontSee('ExportWizardTheFifth'); } + public function test_page_export_only_includes_revision_count_if_user_has_revision_view_permissions() + { + $editor = $this->users->editor(); + $page = $this->entities->page(); + + $resp = $this->actingAs($editor)->get($page->getUrl('/export/html')); + $resp->assertSee('Revision #'); + + $this->permissions->removeUserRolePermissions($editor, [Permission::RevisionViewAll]); + + $resp = $this->actingAs($editor)->get($page->getUrl('/export/html')); + $resp->assertDontSee('Revision #'); + } + public function test_html_exports_contain_csp_meta_tag() { $entities = [