1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-10-23 18:48:37 +03:00

Permissions: Removed unused role-perm columns, added permission enum

Updated main permission check methods to support our new enum.
This commit is contained in:
Dan Brown
2025-09-08 15:59:25 +01:00
parent 1ac74099ca
commit c8716df284
10 changed files with 190 additions and 28 deletions

View File

@@ -3,6 +3,7 @@
use BookStack\App\AppVersion; use BookStack\App\AppVersion;
use BookStack\App\Model; use BookStack\App\Model;
use BookStack\Facades\Theme; use BookStack\Facades\Theme;
use BookStack\Permissions\Permission;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
use BookStack\Settings\SettingService; use BookStack\Settings\SettingService;
use BookStack\Users\Models\User; use BookStack\Users\Models\User;
@@ -39,7 +40,7 @@ function user(): User
* Check if the current user has a permission. If an ownable element * Check if the current user has a permission. If an ownable element
* is passed in the jointPermissions are checked against that particular item. * 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)) { if (is_null($ownable)) {
return user()->can($permission); return user()->can($permission);

View File

@@ -8,6 +8,7 @@ use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Facades\Activity; use BookStack\Facades\Activity;
use BookStack\Permissions\Models\EntityPermission; use BookStack\Permissions\Models\EntityPermission;
use BookStack\Permissions\Permission;
use BookStack\Users\Models\Role; use BookStack\Users\Models\Role;
use BookStack\Users\Models\User; use BookStack\Users\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -93,8 +94,9 @@ class PermissionsUpdater
foreach ($permissions as $roleId => $info) { foreach ($permissions as $roleId => $info) {
$entityPermissionData = ['role_id' => $roleId]; $entityPermissionData = ['role_id' => $roleId];
foreach (EntityPermission::PERMISSIONS as $permission) { foreach (Permission::genericForEntity() as $permission) {
$entityPermissionData[$permission] = (($info[$permission] ?? false) === "true"); $permName = $permission->value;
$entityPermissionData[$permName] = (($info[$permName] ?? false) === "true");
} }
$formatted[] = $entityPermissionData; $formatted[] = $entityPermissionData;
} }
@@ -108,8 +110,9 @@ class PermissionsUpdater
foreach ($permissions as $requestPermissionData) { foreach ($permissions as $requestPermissionData) {
$entityPermissionData = ['role_id' => $requestPermissionData['role_id']]; $entityPermissionData = ['role_id' => $requestPermissionData['role_id']];
foreach (EntityPermission::PERMISSIONS as $permission) { foreach (Permission::genericForEntity() as $permission) {
$entityPermissionData[$permission] = boolval($requestPermissionData[$permission] ?? false); $permName = $permission->value;
$entityPermissionData[$permName] = boolval($requestPermissionData[$permName] ?? false);
} }
$formatted[] = $entityPermissionData; $formatted[] = $entityPermissionData;
} }

View File

@@ -18,8 +18,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
*/ */
class EntityPermission extends Model class EntityPermission extends Model
{ {
public const PERMISSIONS = ['view', 'create', 'update', 'delete'];
protected $fillable = ['role_id', 'view', 'create', 'update', 'delete']; protected $fillable = ['role_id', 'view', 'create', 'update', 'delete'];
public $timestamps = false; public $timestamps = false;
protected $hidden = ['entity_id', 'entity_type', 'id']; protected $hidden = ['entity_id', 'entity_type', 'id'];

View File

@@ -9,7 +9,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/** /**
* @property int $id * @property int $id
* @property string $name * @property string $name
* @property string $display_name
*/ */
class RolePermission extends Model class RolePermission extends Model
{ {

View File

@@ -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,
];
}
}

View File

@@ -24,11 +24,12 @@ class PermissionApplicator
/** /**
* Checks if an entity has a restriction set upon it. * 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]; $action = $explodedPermission[1] ?? $explodedPermission[0];
$fullPermission = count($explodedPermission) > 1 ? $permission : $ownable->getMorphClass() . '-' . $permission; $fullPermission = count($explodedPermission) > 1 ? $permissionName : $ownable->getMorphClass() . '-' . $permissionName;
$user = $this->currentUser(); $user = $this->currentUser();
$userRoleIds = $this->getCurrentUserRoleIds(); $userRoleIds = $this->getCurrentUserRoleIds();
@@ -235,8 +236,13 @@ class PermissionApplicator
*/ */
protected function ensureValidEntityAction(string $action): void protected function ensureValidEntityAction(string $action): void
{ {
if (!in_array($action, EntityPermission::PERMISSIONS)) { $allowed = Permission::genericForEntity();
throw new InvalidArgumentException('Action should be a simple entity permission action, not a role permission'); foreach ($allowed as $permission) {
if ($permission->value === $action) {
return;
}
} }
throw new InvalidArgumentException('Action should be a simple entity permission action, not a role permission');
} }
} }

View File

@@ -57,7 +57,8 @@ class Role extends Model implements Loggable
*/ */
public function permissions(): BelongsToMany 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']);
} }
/** /**

View File

@@ -12,6 +12,7 @@ use BookStack\Api\ApiToken;
use BookStack\App\Model; use BookStack\App\Model;
use BookStack\App\SluggableInterface; use BookStack\App\SluggableInterface;
use BookStack\Entities\Tools\SlugGenerator; use BookStack\Entities\Tools\SlugGenerator;
use BookStack\Permissions\Permission;
use BookStack\Translation\LocaleDefinition; use BookStack\Translation\LocaleDefinition;
use BookStack\Translation\LocaleManager; use BookStack\Translation\LocaleManager;
use BookStack\Uploads\Image; 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\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@@ -156,8 +156,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/** /**
* Check if the user has a particular permission. * 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); 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; $this->permissions = null;
} }
@@ -191,7 +192,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/** /**
* Attach a role to this user. * Attach a role to this user.
*/ */
public function attachRole(Role $role) public function attachRole(Role $role): void
{ {
$this->roles()->attach($role->id); $this->roles()->attach($role->id);
$this->unsetRelation('roles'); $this->unsetRelation('roles');
@@ -207,15 +208,11 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/** /**
* Check if the user has a social account, * Check if the user has a social account,
* If a driver is passed it checks for that single account type. * If a driver is passed, it checks for that single account type.
*
* @param bool|string $socialDriver
*
* @return bool
*/ */
public function hasSocialAccount($socialDriver = false) public function hasSocialAccount(string $socialDriver = ''): bool
{ {
if ($socialDriver === false) { if (empty($socialDriver)) {
return $this->socialAccounts()->count() > 0; return $this->socialAccounts()->count() > 0;
} }

View File

@@ -14,6 +14,11 @@ return new class extends Migration
Schema::table('comments', function (Blueprint $table) { Schema::table('comments', function (Blueprint $table) {
$table->dropColumn('text'); $table->dropColumn('text');
}); });
Schema::table('role_permissions', function (Blueprint $table) {
$table->dropColumn('display_name');
$table->dropColumn('description');
});
} }
/** /**

View File

@@ -5,6 +5,7 @@ namespace Tests\Helpers;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Permissions\Models\EntityPermission; use BookStack\Permissions\Models\EntityPermission;
use BookStack\Permissions\Models\RolePermission; use BookStack\Permissions\Models\RolePermission;
use BookStack\Permissions\Permission;
use BookStack\Settings\SettingService; use BookStack\Settings\SettingService;
use BookStack\Users\Models\Role; use BookStack\Users\Models\Role;
use BookStack\Users\Models\User; use BookStack\Users\Models\User;
@@ -139,8 +140,8 @@ class PermissionsProvider
protected function actionListToEntityPermissionData(array $actionList, int $roleId = 0): array protected function actionListToEntityPermissionData(array $actionList, int $roleId = 0): array
{ {
$permissionData = ['role_id' => $roleId]; $permissionData = ['role_id' => $roleId];
foreach (EntityPermission::PERMISSIONS as $possibleAction) { foreach (Permission::genericForEntity() as $permission) {
$permissionData[$possibleAction] = in_array($possibleAction, $actionList); $permissionData[$permission->value] = in_array($permission->value, $actionList);
} }
return $permissionData; return $permissionData;