]> BookStack Code Mirror - bookstack/commitdiff
Added users to permission form interface
authorDan Brown <redacted>
Sat, 10 Dec 2022 14:37:18 +0000 (14:37 +0000)
committerDan Brown <redacted>
Sat, 10 Dec 2022 14:37:18 +0000 (14:37 +0000)
Also updated non-joint permission handling to support user permissions.

app/Auth/Permissions/EntityPermission.php
app/Auth/Permissions/PermissionApplicator.php
app/Auth/Permissions/PermissionFormData.php
app/Entities/Tools/PermissionsUpdater.php
app/Http/Controllers/PermissionsController.php
resources/js/components/entity-permissions.js
resources/lang/en/entities.php
resources/views/form/entity-permissions-row.blade.php
resources/views/form/entity-permissions.blade.php
routes/web.php

index 79fd1a2db66d86d4c038f7c87097f5379a75af4d..8592d25dd2c1bdc3ab4429ee59c6ddd6baaa2c5b 100644 (file)
@@ -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);
     }
 }
index 61486026359a448762e00aba70013c4f790541f9..96228ead591ba9264beb5ed0f6e0d3a22444340e 100644 (file)
@@ -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 */
index 18d45591fd2948bf7602069ba3a226d0cc3fbb77..9cf7ba9163521263081933608b4449560b253a1a 100644 (file)
@@ -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.
index f13323ba6bb30e5f1af44ce62b78b97d8669aeb0..de06c8149f3ee21a1d4061089b5795ca402ceadd 100644 (file)
@@ -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
 {
index 562e8305b13b3e0bd78aff0cb273c5696e015d2b..453a230af9a466fc897ae88b8e8e9b749185fa01 100644 (file)
@@ -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,
+        ]);
+    }
 }
index b1b6c508489f69382772375d98d0e30fdd6d41b5..189b859b7119c58c3db7d3ce0c1ba6c1eed9ad10 100644 (file)
@@ -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();
     }
 
index fa2586f8d75cab171ec753065eaf73f4efcafe5d..4605c0c2f720e46ee84dffb9cf6220009d410624 100644 (file)
@@ -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
index 1d028d7f65ae4f006ff1729086d4124d783d1105..bb9f204a09d85062425eb837d6620b2b17b192a7 100644 (file)
@@ -11,7 +11,7 @@ $inheriting - Boolean if the current row should be marked as inheriting default
 <div component="permissions-table" class="item-list-row flex-container-row justify-space-between wrap">
     <div class="gap-x-m flex-container-row items-center px-l py-m flex">
         <div class="text-large" title="{{  $modelType === 'fallback' ? trans('entities.permissions_role_everyone_else') : trans('common.role') }}">
-            @icon($modelType === 'fallback' ? 'groups' : 'role')
+            @icon($modelType === 'fallback' ? 'groups' : ($modelType === 'role' ? 'role' : 'user'))
         </div>
         <span>
             <strong>{{ $modelName }}</strong> <br>
index f5674056236d8b61b08d65302fb186e9377e338b..dad941573d7c970552987d0d4892c5eba8936cc9 100644 (file)
 
     <hr>
 
+    <div refs="entity-permissions@user-container" class="item-list mt-m mb-m">
+        @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
+    </div>
+
+    <div class="flex-container-row justify-flex-end mb-xl">
+        <div refs="entity-permissions@user-select-container" class="flex-container-row items-center gap-m">
+            <label for="user_select" class="m-none p-none"><span class="bold">{{ trans('entities.permissions_user_override') }}</span></label>
+            @include('form.user-select', ['name' => 'user_select', 'user' => null])
+        </div>
+    </div>
+
     <div refs="entity-permissions@role-container" class="item-list mt-m mb-m">
         @foreach($data->permissionsWithRoles() as $permission)
             @include('form.entity-permissions-row', [
index 95b4ae535236b879e9c9aa83ac2e486251a6d318..cf4bfbe36eb6c596260eedda5f1112bc7558caea 100644 (file)
@@ -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']);