From: Dan Brown Date: Sat, 10 Dec 2022 14:37:18 +0000 (+0000) Subject: Added users to permission form interface X-Git-Url: http://source.bookstackapp.com/bookstack/commitdiff_plain/7a269e7689d0e000dbf56f9da603e8586106e8bb Added users to permission form interface Also updated non-joint permission handling to support user permissions. --- diff --git a/app/Auth/Permissions/EntityPermission.php b/app/Auth/Permissions/EntityPermission.php index 79fd1a2db..8592d25dd 100644 --- a/app/Auth/Permissions/EntityPermission.php +++ b/app/Auth/Permissions/EntityPermission.php @@ -3,9 +3,9 @@ namespace BookStack\Auth\Permissions; use BookStack\Auth\Role; +use BookStack\Auth\User; use BookStack\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; -use Illuminate\Database\Eloquent\Relations\MorphTo; /** * @property int $id @@ -22,22 +22,22 @@ class EntityPermission extends Model { public const PERMISSIONS = ['view', 'create', 'update', 'delete']; - protected $fillable = ['role_id', 'view', 'create', 'update', 'delete']; + protected $fillable = ['role_id', 'user_id', 'view', 'create', 'update', 'delete']; public $timestamps = false; /** - * Get this restriction's attached entity. + * Get the role assigned to this entity permission. */ - public function restrictable(): MorphTo + public function role(): BelongsTo { - return $this->morphTo('restrictable'); + return $this->belongsTo(Role::class); } /** - * Get the role assigned to this entity permission. + * Get the user assigned to this entity permission. */ - public function role(): BelongsTo + public function user(): BelongsTo { - return $this->belongsTo(Role::class); + return $this->belongsTo(User::class); } } diff --git a/app/Auth/Permissions/PermissionApplicator.php b/app/Auth/Permissions/PermissionApplicator.php index 614860263..96228ead5 100644 --- a/app/Auth/Permissions/PermissionApplicator.php +++ b/app/Auth/Permissions/PermissionApplicator.php @@ -48,7 +48,7 @@ class PermissionApplicator return $hasRolePermission; } - $hasApplicableEntityPermissions = $this->hasEntityPermission($ownable, $userRoleIds, $action); + $hasApplicableEntityPermissions = $this->hasEntityPermission($ownable, $userRoleIds, $user->id, $action); return is_null($hasApplicableEntityPermissions) ? $hasRolePermission : $hasApplicableEntityPermissions; } @@ -57,7 +57,7 @@ class PermissionApplicator * Check if there are permissions that are applicable for the given entity item, action and roles. * Returns null when no entity permissions are in force. */ - protected function hasEntityPermission(Entity $entity, array $userRoleIds, string $action): ?bool + protected function hasEntityPermission(Entity $entity, array $userRoleIds, int $userId, string $action): ?bool { $this->ensureValidEntityAction($action); @@ -79,8 +79,9 @@ class PermissionApplicator foreach ($chain as $currentEntity) { $relevantPermissions = $currentEntity->permissions() - ->where(function (Builder $query) use ($userRoleIds) { + ->where(function (Builder $query) use ($userRoleIds, $userId) { $query->whereIn('role_id', $userRoleIds) + ->orWhere('user_id', '=', $userId) ->orWhere(function (Builder $query) { $query->whereNull(['role_id', 'user_id']); }); @@ -88,22 +89,17 @@ class PermissionApplicator ->get(['role_id', 'user_id', $action]) ->all(); - // TODO - Update below for user permissions - - // 1. Default fallback set and allows, no role permissions -> True - // 2. Default fallback set and prevents, no role permissions -> False - // 3. Role permission allows, fallback set and allows -> True - // 3. Role permission allows, fallback set and prevents -> True - // 3. Role permission allows, fallback not set -> True - // 3. Role permission prevents, fallback set and allows -> False - // 3. Role permission prevents, fallback set and prevents -> False - // 3. Role permission prevents, fallback not set -> False - // 4. Nothing exists -> Continue + // Permissions work on specificity, in order of: + // 1. User-specific permissions + // 2. Role-specific permissions + // 3. Fallback-specific permissions + // For role permissions, the system tries to be fairly permissive, in that if the user has two roles, + // one lacking and one permitting an action, they will be permitted. // If the default is set, we have to return something here. $allowedById = []; foreach ($relevantPermissions as $permission) { - $allowedById[$permission->role_id . ':' . $permission->user_id] = $permission->$action; + $allowedById[($permission->role_id ?? '') . ':' . ($permission->user_id ?? '')] = $permission->$action; } // Continue up the chain if no applicable entity permission overrides. @@ -111,7 +107,14 @@ class PermissionApplicator continue; } - // If we have user-role-specific permissions set, allow if any of those + // If we have user-specific permissions set, return the status of that + // since it's the most specific possible. + $userKey = ':' . $userId; + if (isset($allowedById[$userKey])) { + return $allowedById[$userKey]; + } + + // If we have role-specific permissions set, allow if any of those // role permissions allow access. $hasDefault = isset($allowedById[':']); if (!$hasDefault || count($allowedById) > 1) { @@ -140,8 +143,10 @@ class PermissionApplicator $permissionQuery = EntityPermission::query() ->where($action, '=', true) - ->whereIn('role_id', $this->getCurrentUserRoleIds()); - // TODO - Update for user permission + ->where(function (Builder $query) { + $query->whereIn('role_id', $this->getCurrentUserRoleIds()) + ->orWhere('user_id', '=', $this->currentUser()->id); + }); if (!empty($entityClass)) { /** @var Entity $entityInstance */ diff --git a/app/Auth/Permissions/PermissionFormData.php b/app/Auth/Permissions/PermissionFormData.php index 18d45591f..9cf7ba916 100644 --- a/app/Auth/Permissions/PermissionFormData.php +++ b/app/Auth/Permissions/PermissionFormData.php @@ -27,6 +27,19 @@ class PermissionFormData ->all(); } + /** + * Get the permissions with assigned users. + */ + public function permissionsWithUsers(): array + { + return $this->entity->permissions() + ->with('user') + ->whereNotNull('user_id') + ->get() + ->sortBy('user.name') + ->all(); + } + /** * Get the roles that don't yet have specific permissions for the * entity we're managing permissions for. diff --git a/app/Entities/Tools/PermissionsUpdater.php b/app/Entities/Tools/PermissionsUpdater.php index f13323ba6..de06c8149 100644 --- a/app/Entities/Tools/PermissionsUpdater.php +++ b/app/Entities/Tools/PermissionsUpdater.php @@ -10,7 +10,6 @@ use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Entity; use BookStack\Facades\Activity; use Illuminate\Http\Request; -use Illuminate\Support\Collection; class PermissionsUpdater { diff --git a/app/Http/Controllers/PermissionsController.php b/app/Http/Controllers/PermissionsController.php index 562e8305b..453a230af 100644 --- a/app/Http/Controllers/PermissionsController.php +++ b/app/Http/Controllers/PermissionsController.php @@ -5,6 +5,7 @@ namespace BookStack\Http\Controllers; use BookStack\Auth\Permissions\EntityPermission; use BookStack\Auth\Permissions\PermissionFormData; use BookStack\Auth\Role; +use BookStack\Auth\User; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Chapter; @@ -175,4 +176,25 @@ class PermissionsController extends Controller 'inheriting' => false, ]); } + + /** + * Get an empty entity permissions form row for the given user. + */ + public function formRowForUser(string $entityType, string $userId) + { + $this->checkPermissionOr('restrictions-manage-all', fn() => userCan('restrictions-manage-own')); + + /** @var User $user */ + $user = User::query()->findOrFail($userId); + + return view('form.entity-permissions-row', [ + 'modelType' => 'user', + 'modelId' => $user->id, + 'modelName' => $user->name, + 'modelDescription' => '', + 'permission' => new EntityPermission(), + 'entityType' => $entityType, + 'inheriting' => false, + ]); + } } diff --git a/resources/js/components/entity-permissions.js b/resources/js/components/entity-permissions.js index b1b6c5084..189b859b7 100644 --- a/resources/js/components/entity-permissions.js +++ b/resources/js/components/entity-permissions.js @@ -10,6 +10,8 @@ export class EntityPermissions extends Component { this.everyoneInheritToggle = this.$refs.everyoneInherit; this.roleSelect = this.$refs.roleSelect; this.roleContainer = this.$refs.roleContainer; + this.userContainer = this.$refs.userContainer; + this.userSelectContainer = this.$refs.userSelectContainer; this.setupListeners(); } @@ -40,6 +42,14 @@ export class EntityPermissions extends Component { this.addRoleRow(roleId); } }); + + // User select change + this.userSelectContainer.querySelector('input[name="user_select"]').addEventListener('change', event => { + const userId = event.target.value; + if (userId) { + this.addUserRow(userId); + } + }); } async addRoleRow(roleId) { @@ -52,13 +62,32 @@ export class EntityPermissions extends Component { } // Get and insert new row - const resp = await window.$http.get(`/permissions/form-row/${this.entityType}/${roleId}`); + const resp = await window.$http.get(`/permissions/role-form-row/${this.entityType}/${roleId}`); const row = htmlToDom(resp.data); this.roleContainer.append(row); this.roleSelect.disabled = false; } + async addUserRow(userId) { + const exists = this.userContainer.querySelector(`[name^="permissions[user][${userId}]"]`) !== null; + if (exists) { + return; + } + + const toggle = this.userSelectContainer.querySelector('.dropdown-search-toggle-select'); + toggle.classList.add('disabled'); + this.userContainer.style.pointerEvents = 'none'; + + // Get and insert new row + const resp = await window.$http.get(`/permissions/user-form-row/${this.entityType}/${userId}`); + const row = htmlToDom(resp.data); + this.userContainer.append(row); + + toggle.classList.remove('disabled'); + this.userContainer.style.pointerEvents = null; + } + removeRowOnButtonClick(button) { const row = button.closest('.item-list-row'); const modelId = button.dataset.modelId; @@ -72,7 +101,7 @@ export class EntityPermissions extends Component { if (modelType === 'role') { this.roleSelect.append(option); } - // TODO - User role! + row.remove(); } diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index fa2586f8d..4605c0c2f 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -50,6 +50,7 @@ return [ 'permissions_role_everyone_else' => 'Everyone Else', 'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.', 'permissions_role_override' => 'Override permissions for role', + 'permissions_user_override' => 'Override permissions for user', 'permissions_inherit_defaults' => 'Inherit defaults', // Search diff --git a/resources/views/form/entity-permissions-row.blade.php b/resources/views/form/entity-permissions-row.blade.php index 1d028d7f6..bb9f204a0 100644 --- a/resources/views/form/entity-permissions-row.blade.php +++ b/resources/views/form/entity-permissions-row.blade.php @@ -11,7 +11,7 @@ $inheriting - Boolean if the current row should be marked as inheriting default
- @icon($modelType === 'fallback' ? 'groups' : 'role') + @icon($modelType === 'fallback' ? 'groups' : ($modelType === 'role' ? 'role' : 'user'))
{{ $modelName }}
diff --git a/resources/views/form/entity-permissions.blade.php b/resources/views/form/entity-permissions.blade.php index f56740562..dad941573 100644 --- a/resources/views/form/entity-permissions.blade.php +++ b/resources/views/form/entity-permissions.blade.php @@ -35,6 +35,27 @@
+
+ @foreach($data->permissionsWithUsers() as $permission) + @include('form.entity-permissions-row', [ + 'permission' => $permission, + 'modelType' => 'user', + 'modelId' => $permission->user->id, + 'modelName' => $permission->user->name, + 'modelDescription' => '', + 'entityType' => $model->getType(), + 'inheriting' => false, + ]) + @endforeach +
+ +
+
+ + @include('form.user-select', ['name' => 'user_select', 'user' => null]) +
+
+
@foreach($data->permissionsWithRoles() as $permission) @include('form.entity-permissions-row', [ diff --git a/routes/web.php b/routes/web.php index 95b4ae535..cf4bfbe36 100644 --- a/routes/web.php +++ b/routes/web.php @@ -217,7 +217,8 @@ Route::middleware('auth')->group(function () { Route::get('/home', [HomeController::class, 'index']); // Permissions - Route::get('/permissions/form-row/{entityType}/{roleId}', [PermissionsController::class, 'formRowForRole']); + Route::get('/permissions/role-form-row/{entityType}/{roleId}', [PermissionsController::class, 'formRowForRole']); + Route::get('/permissions/user-form-row/{entityType}/{userId}', [PermissionsController::class, 'formRowForUser']); // Maintenance Route::get('/settings/maintenance', [MaintenanceController::class, 'index']);