]> BookStack Code Mirror - bookstack/commitdiff
Permissions: Removed unused role-perm columns, added permission enum
authorDan Brown <redacted>
Mon, 8 Sep 2025 14:59:25 +0000 (15:59 +0100)
committerDan Brown <redacted>
Mon, 8 Sep 2025 14:59:25 +0000 (15:59 +0100)
Updated main permission check methods to support our new enum.

app/App/helpers.php
app/Entities/Tools/PermissionsUpdater.php
app/Permissions/Models/EntityPermission.php
app/Permissions/Models/RolePermission.php
app/Permissions/Permission.php [new file with mode: 0644]
app/Permissions/PermissionApplicator.php
app/Users/Models/Role.php
app/Users/Models/User.php
database/migrations/2025_09_02_111542_remove_unused_columns.php [moved from database/migrations/2025_09_02_111542_remove_comments_text_column.php with 76% similarity]
tests/Helpers/PermissionsProvider.php

index 2305c2d72a19f41f240de42f2234fc6ee24c55db..0e26d9974a9016fc941a67080b78b377f09e0c42 100644 (file)
@@ -3,6 +3,7 @@
 use BookStack\App\AppVersion;
 use BookStack\App\Model;
 use BookStack\Facades\Theme;
+use BookStack\Permissions\Permission;
 use BookStack\Permissions\PermissionApplicator;
 use BookStack\Settings\SettingService;
 use BookStack\Users\Models\User;
@@ -39,7 +40,7 @@ function user(): User
  * Check if the current user has a permission. If an ownable element
  * is passed in the jointPermissions are checked against that particular item.
  */
-function userCan(string $permission, ?Model $ownable = null): bool
+function userCan(string|Permission $permission, ?Model $ownable = null): bool
 {
     if (is_null($ownable)) {
         return user()->can($permission);
index 9f3b8f952777d8c1e9658f96fd2f5c65acb6eb95..4ca53982a451e790635716be4dc4088e6a7d0121 100644 (file)
@@ -8,6 +8,7 @@ use BookStack\Entities\Models\Bookshelf;
 use BookStack\Entities\Models\Entity;
 use BookStack\Facades\Activity;
 use BookStack\Permissions\Models\EntityPermission;
+use BookStack\Permissions\Permission;
 use BookStack\Users\Models\Role;
 use BookStack\Users\Models\User;
 use Illuminate\Http\Request;
@@ -93,8 +94,9 @@ class PermissionsUpdater
 
         foreach ($permissions as $roleId => $info) {
             $entityPermissionData = ['role_id' => $roleId];
-            foreach (EntityPermission::PERMISSIONS as $permission) {
-                $entityPermissionData[$permission] = (($info[$permission] ?? false) === "true");
+            foreach (Permission::genericForEntity() as $permission) {
+                $permName = $permission->value;
+                $entityPermissionData[$permName] = (($info[$permName] ?? false) === "true");
             }
             $formatted[] = $entityPermissionData;
         }
@@ -108,8 +110,9 @@ class PermissionsUpdater
 
         foreach ($permissions as $requestPermissionData) {
             $entityPermissionData = ['role_id' => $requestPermissionData['role_id']];
-            foreach (EntityPermission::PERMISSIONS as $permission) {
-                $entityPermissionData[$permission] = boolval($requestPermissionData[$permission] ?? false);
+            foreach (Permission::genericForEntity() as $permission) {
+                $permName = $permission->value;
+                $entityPermissionData[$permName] = boolval($requestPermissionData[$permName] ?? false);
             }
             $formatted[] = $entityPermissionData;
         }
index 1ef9c2886f5bacb43b5c03dba1f6b958e4017254..efad2ba39a0ff6df882ca44271ae6da777c94611 100644 (file)
@@ -18,8 +18,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
  */
 class EntityPermission extends Model
 {
-    public const PERMISSIONS = ['view', 'create', 'update', 'delete'];
-
     protected $fillable = ['role_id', 'view', 'create', 'update', 'delete'];
     public $timestamps = false;
     protected $hidden = ['entity_id', 'entity_type', 'id'];
index d43313ea0d8ca274d12623d9af23bd2ff3fa6b89..67b1c55f55d8c1a669f6ce80474670adc921df22 100644 (file)
@@ -9,7 +9,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 /**
  * @property int $id
  * @property string $name
- * @property string $display_name
  */
 class RolePermission extends Model
 {
diff --git a/app/Permissions/Permission.php b/app/Permissions/Permission.php
new file mode 100644 (file)
index 0000000..539bbe8
--- /dev/null
@@ -0,0 +1,151 @@
+<?php
+
+namespace BookStack\Permissions;
+
+/**
+ * Enum to represent the permissions which may be used in checks.
+ * These generally align with RolePermission names, although some are abstract or truncated as some checks
+ * are performed across a range of different items which may be subject to inheritance and other complications.
+ *
+ * We use and still allow the string values in usage to allow for compatibility with scenarios where
+ * users have customised their instance with additional permissions via the theme system.
+ * This enum primarily exists for alignment within the codebase.
+ */
+enum Permission: string
+{
+    // Generic Actions
+    // Used for more abstract entity permission checks
+    case View = 'view';
+    case Create = 'create';
+    case Update = 'update';
+    case Delete = 'delete';
+
+    // System Permissions
+    case AccessApi = 'access-api';
+    case ContentExport = 'content-export';
+    case ContentImport = 'content-import';
+    case EditorChange = 'editor-change';
+    case ReceiveNotifications = 'receive-notifications';
+    case RestrictionsManageAll = 'restrictions-manage-all';
+    case RestrictionsManageOwn = 'restrictions-manage-own';
+    case SettingsManage = 'settings-manage';
+    case TemplatesManage = 'templates-manage';
+    case UserRolesManage = 'user-roles-manage';
+    case UsersManage = 'users-manage';
+
+    // Entity permissions
+    // Each has 'all' or 'own' in it's RolePermission, with the base non-suffix name being used
+    // in actual checking logic, with the permission system handling the assessment of the underlying RolePermission.
+    case AttachmentCreate = 'attachment-create';
+    case AttachmentCreateAll = 'attachment-create-all';
+    case AttachmentCreateOwn = 'attachment-create-own';
+
+    case AttachmentDelete = 'attachment-delete';
+    case AttachmentDeleteAll = 'attachment-delete-all';
+    case AttachmentDeleteOwn = 'attachment-delete-own';
+
+    case AttachmentUpdate = 'attachment-update';
+    case AttachmentUpdateAll = 'attachment-update-all';
+    case AttachmentUpdateOwn = 'attachment-update-own';
+
+    case BookCreate = 'book-create';
+    case BookCreateAll = 'book-create-all';
+    case BookCreateOwn = 'book-create-own';
+
+    case BookDelete = 'book-delete';
+    case BookDeleteAll = 'book-delete-all';
+    case BookDeleteOwn = 'book-delete-own';
+
+    case BookUpdate = 'book-update';
+    case BookUpdateAll = 'book-update-all';
+    case BookUpdateOwn = 'book-update-own';
+
+    case BookView = 'book-view';
+    case BookViewAll = 'book-view-all';
+    case BookViewOwn = 'book-view-own';
+
+    case BookshelfCreate = 'bookshelf-create';
+    case BookshelfCreateAll = 'bookshelf-create-all';
+    case BookshelfCreateOwn = 'bookshelf-create-own';
+
+    case BookshelfDelete = 'bookshelf-delete';
+    case BookshelfDeleteAll = 'bookshelf-delete-all';
+    case BookshelfDeleteOwn = 'bookshelf-delete-own';
+
+    case BookshelfUpdate = 'bookshelf-update';
+    case BookshelfUpdateAll = 'bookshelf-update-all';
+    case BookshelfUpdateOwn = 'bookshelf-update-own';
+
+    case BookshelfView = 'bookshelf-view';
+    case BookshelfViewAll = 'bookshelf-view-all';
+    case BookshelfViewOwn = 'bookshelf-view-own';
+
+    case ChapterCreate = 'chapter-create';
+    case ChapterCreateAll = 'chapter-create-all';
+    case ChapterCreateOwn = 'chapter-create-own';
+
+    case ChapterDelete = 'chapter-delete';
+    case ChapterDeleteAll = 'chapter-delete-all';
+    case ChapterDeleteOwn = 'chapter-delete-own';
+
+    case ChapterUpdate = 'chapter-update';
+    case ChapterUpdateAll = 'chapter-update-all';
+    case ChapterUpdateOwn = 'chapter-update-own';
+
+    case ChapterView = 'chapter-view';
+    case ChapterViewAll = 'chapter-view-all';
+    case ChapterViewOwn = 'chapter-view-own';
+
+    case CommentCreate = 'comment-create';
+    case CommentCreateAll = 'comment-create-all';
+    case CommentCreateOwn = 'comment-create-own';
+
+    case CommentDelete = 'comment-delete';
+    case CommentDeleteAll = 'comment-delete-all';
+    case CommentDeleteOwn = 'comment-delete-own';
+
+    case CommentUpdate = 'comment-update';
+    case CommentUpdateAll = 'comment-update-all';
+    case CommentUpdateOwn = 'comment-update-own';
+
+    case ImageCreate = 'image-create';
+    case ImageCreateAll = 'image-create-all';
+    case ImageCreateOwn = 'image-create-own';
+
+    case ImageDelete = 'image-delete';
+    case ImageDeleteAll = 'image-delete-all';
+    case ImageDeleteOwn = 'image-delete-own';
+
+    case ImageUpdate = 'image-update';
+    case ImageUpdateAll = 'image-update-all';
+    case ImageUpdateOwn = 'image-update-own';
+
+    case PageCreate = 'page-create';
+    case PageCreateAll = 'page-create-all';
+    case PageCreateOwn = 'page-create-own';
+
+    case PageDelete = 'page-delete';
+    case PageDeleteAll = 'page-delete-all';
+    case PageDeleteOwn = 'page-delete-own';
+
+    case PageUpdate = 'page-update';
+    case PageUpdateAll = 'page-update-all';
+    case PageUpdateOwn = 'page-update-own';
+
+    case PageView = 'page-view';
+    case PageViewAll = 'page-view-all';
+    case PageViewOwn = 'page-view-own';
+
+    /**
+     * Get the generic permissions which may be queried for entities.
+     */
+    public static function genericForEntity(): array
+    {
+        return [
+            self::View,
+            self::Create,
+            self::Update,
+            self::Delete,
+        ];
+    }
+}
index e59b8ab675915bb7b39de1b2d44e92e822b0f313..47f30c8f949376fd8fbdea3b33c765e980391513 100644 (file)
@@ -24,11 +24,12 @@ class PermissionApplicator
     /**
      * Checks if an entity has a restriction set upon it.
      */
-    public function checkOwnableUserAccess(Model&OwnableInterface $ownable, string $permission): bool
+    public function checkOwnableUserAccess(Model&OwnableInterface $ownable, string|Permission $permission): bool
     {
-        $explodedPermission = explode('-', $permission);
+        $permissionName = is_string($permission) ? $permission : $permission->value;
+        $explodedPermission = explode('-', $permissionName);
         $action = $explodedPermission[1] ?? $explodedPermission[0];
-        $fullPermission = count($explodedPermission) > 1 ? $permission : $ownable->getMorphClass() . '-' . $permission;
+        $fullPermission = count($explodedPermission) > 1 ? $permissionName : $ownable->getMorphClass() . '-' . $permissionName;
 
         $user = $this->currentUser();
         $userRoleIds = $this->getCurrentUserRoleIds();
@@ -235,8 +236,13 @@ class PermissionApplicator
      */
     protected function ensureValidEntityAction(string $action): void
     {
-        if (!in_array($action, EntityPermission::PERMISSIONS)) {
-            throw new InvalidArgumentException('Action should be a simple entity permission action, not a role permission');
+        $allowed = Permission::genericForEntity();
+        foreach ($allowed as $permission) {
+            if ($permission->value === $action) {
+                return;
+            }
         }
+
+        throw new InvalidArgumentException('Action should be a simple entity permission action, not a role permission');
     }
 }
index 912ae81c9804463e071d421a1e4ea03b4b1c9452..92be224580f7ff5b3accbb2c1248f27fc6a4cae2 100644 (file)
@@ -57,7 +57,8 @@ class Role extends Model implements Loggable
      */
     public function permissions(): BelongsToMany
     {
-        return $this->belongsToMany(RolePermission::class, 'permission_role', 'role_id', 'permission_id');
+        return $this->belongsToMany(RolePermission::class, 'permission_role', 'role_id', 'permission_id')
+            ->select(['id', 'name']);
     }
 
     /**
index f83e120887b1728d4853bb219fff4c8e9ef787ec..0017653b53d6e152571d22143edc2673a3737c20 100644 (file)
@@ -12,6 +12,7 @@ use BookStack\Api\ApiToken;
 use BookStack\App\Model;
 use BookStack\App\SluggableInterface;
 use BookStack\Entities\Tools\SlugGenerator;
+use BookStack\Permissions\Permission;
 use BookStack\Translation\LocaleDefinition;
 use BookStack\Translation\LocaleManager;
 use BookStack\Uploads\Image;
@@ -26,7 +27,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
 use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 use Illuminate\Database\Eloquent\Relations\HasMany;
-use Illuminate\Database\Eloquent\Relations\Relation;
 use Illuminate\Notifications\Notifiable;
 use Illuminate\Support\Collection;
 
@@ -156,8 +156,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
     /**
      * Check if the user has a particular permission.
      */
-    public function can(string $permissionName): bool
+    public function can(string|Permission $permission): bool
     {
+        $permissionName = is_string($permission) ? $permission : $permission->value;
         return $this->permissions()->contains($permissionName);
     }
 
@@ -181,9 +182,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
     }
 
     /**
-     * Clear any cached permissions on this instance.
+     * Clear any cached permissions in this instance.
      */
-    public function clearPermissionCache()
+    public function clearPermissionCache(): void
     {
         $this->permissions = null;
     }
@@ -191,7 +192,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
     /**
      * Attach a role to this user.
      */
-    public function attachRole(Role $role)
+    public function attachRole(Role $role): void
     {
         $this->roles()->attach($role->id);
         $this->unsetRelation('roles');
@@ -207,15 +208,11 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
 
     /**
      * Check if the user has a social account,
-     * If a driver is passed it checks for that single account type.
-     *
-     * @param bool|string $socialDriver
-     *
-     * @return bool
+     * If a driver is passed, it checks for that single account type.
      */
-    public function hasSocialAccount($socialDriver = false)
+    public function hasSocialAccount(string $socialDriver = ''): bool
     {
-        if ($socialDriver === false) {
+        if (empty($socialDriver)) {
             return $this->socialAccounts()->count() > 0;
         }
 
similarity index 76%
rename from database/migrations/2025_09_02_111542_remove_comments_text_column.php
rename to database/migrations/2025_09_02_111542_remove_unused_columns.php
index 8caa05e5443cc4050818f45ad584c808d2147758..1ec2dfdf991de4118a0004e1835d631f2f47fff3 100644 (file)
@@ -14,6 +14,11 @@ return new class extends Migration
         Schema::table('comments', function (Blueprint $table) {
             $table->dropColumn('text');
         });
+
+        Schema::table('role_permissions', function (Blueprint $table) {
+            $table->dropColumn('display_name');
+            $table->dropColumn('description');
+        });
     }
 
     /**
index cb036fe97c71c964bfdb6c8b018364014f9f02b9..c9ae309196e3adbac737977a5fb817bfa87361ae 100644 (file)
@@ -5,6 +5,7 @@ namespace Tests\Helpers;
 use BookStack\Entities\Models\Entity;
 use BookStack\Permissions\Models\EntityPermission;
 use BookStack\Permissions\Models\RolePermission;
+use BookStack\Permissions\Permission;
 use BookStack\Settings\SettingService;
 use BookStack\Users\Models\Role;
 use BookStack\Users\Models\User;
@@ -139,8 +140,8 @@ class PermissionsProvider
     protected function actionListToEntityPermissionData(array $actionList, int $roleId = 0): array
     {
         $permissionData = ['role_id' => $roleId];
-        foreach (EntityPermission::PERMISSIONS as $possibleAction) {
-            $permissionData[$possibleAction] = in_array($possibleAction, $actionList);
+        foreach (Permission::genericForEntity() as $permission) {
+            $permissionData[$permission->value] = in_array($permission->value, $actionList);
         }
 
         return $permissionData;