]> BookStack Code Mirror - bookstack/commitdiff
Updated userCan logic to meet expectations in tests
authorDan Brown <redacted>
Mon, 23 Jan 2023 12:40:11 +0000 (12:40 +0000)
committerDan Brown <redacted>
Mon, 23 Jan 2023 12:40:11 +0000 (12:40 +0000)
Updated with similar logic to that used in the user_permissions branch,
but all extracted to a seperate class for doing all fetch and collapse
work.

app/Auth/Permissions/EntityPermissionEvaluator.php [new file with mode: 0644]
app/Auth/Permissions/PermissionApplicator.php

diff --git a/app/Auth/Permissions/EntityPermissionEvaluator.php b/app/Auth/Permissions/EntityPermissionEvaluator.php
new file mode 100644 (file)
index 0000000..91596d0
--- /dev/null
@@ -0,0 +1,131 @@
+<?php
+
+namespace BookStack\Auth\Permissions;
+
+use BookStack\Auth\Role;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\Page;
+use Illuminate\Database\Eloquent\Builder;
+
+class EntityPermissionEvaluator
+{
+    protected Entity $entity;
+    protected array $userRoleIds;
+    protected string $action;
+    protected int $userId;
+
+    public function __construct(Entity $entity, int $userId, array $userRoleIds, string $action)
+    {
+        $this->entity = $entity;
+        $this->userId = $userId;
+        $this->userRoleIds = $userRoleIds;
+        $this->action = $action;
+    }
+
+    public function evaluate(): ?bool
+    {
+        if ($this->isUserSystemAdmin()) {
+            return true;
+        }
+
+        $typeIdChain = $this->gatherEntityChainTypeIds();
+        $relevantPermissions = $this->getRelevantPermissionsMapByTypeId($typeIdChain);
+        $permitsByType = $this->collapseAndCategorisePermissions($typeIdChain, $relevantPermissions);
+
+        // Return grant or reject from role-level if exists
+        if (count($permitsByType['role']) > 0) {
+            return boolval(max($permitsByType['role']));
+        }
+
+        // Return fallback permission if exists
+        if (count($permitsByType['fallback']) > 0) {
+            return boolval($permitsByType['fallback'][0]);
+        }
+
+        return null;
+    }
+
+    /**
+     * @param string[] $typeIdChain
+     * @param array<string, EntityPermission[]> $permissionMapByTypeId
+     * @return array<string, array<string, int>>
+     */
+    protected function collapseAndCategorisePermissions(array $typeIdChain, array $permissionMapByTypeId): array
+    {
+        $permitsByType = ['fallback' => [], 'role' => []];
+
+        foreach ($typeIdChain as $typeId) {
+            $permissions = $permissionMapByTypeId[$typeId] ?? [];
+            foreach ($permissions as $permission) {
+                $roleId = $permission->role_id;
+                $type = $roleId === 0 ? 'fallback' : 'role';
+                if (!isset($permitsByType[$type][$roleId])) {
+                    $permitsByType[$type][$roleId] = $permission->{$this->action};
+                }
+            }
+        }
+
+        return $permitsByType;
+    }
+
+    /**
+     * @param string[] $typeIdChain
+     * @return array<string, EntityPermission[]>
+     */
+    protected function getRelevantPermissionsMapByTypeId(array $typeIdChain): array
+    {
+        $relevantPermissions = EntityPermission::query()
+            ->where(function (Builder $query) use ($typeIdChain) {
+                foreach ($typeIdChain as $typeId) {
+                    $query->orWhere(function (Builder $query) use ($typeId) {
+                        [$type, $id] = explode(':', $typeId);
+                        $query->where('entity_type', '=', $type)
+                            ->where('entity_id', '=', $id);
+                    });
+                }
+            })->where(function (Builder $query) {
+                $query->whereIn('role_id', [...$this->userRoleIds, 0]);
+            })->get(['entity_id', 'entity_type', 'role_id', $this->action])
+            ->all();
+
+        $map = [];
+        foreach ($relevantPermissions as $permission) {
+            $key = $permission->entity_type . ':' . $permission->entity_id;
+            if (!isset($map[$key])) {
+                $map[$key] = [];
+            }
+
+            $map[$key][] = $permission;
+        }
+
+        return $map;
+    }
+
+    /**
+     * @return string[]
+     */
+    protected function gatherEntityChainTypeIds(): array
+    {
+        // The array order here is very important due to the fact we walk up the chain
+        // elsewhere in the class. Earlier items in the chain have higher priority.
+
+        $chain = [$this->entity->getMorphClass() . ':' . $this->entity->id];
+
+        if ($this->entity instanceof Page && $this->entity->chapter_id) {
+            $chain[] = 'chapter:' . $this->entity->chapter_id;
+        }
+
+        if ($this->entity instanceof Page || $this->entity instanceof Chapter) {
+            $chain[] = 'book:' . $this->entity->book_id;
+        }
+
+        return $chain;
+    }
+
+    protected function isUserSystemAdmin(): bool
+    {
+        $adminRoleId = Role::getSystemRole('admin')->id;
+        return in_array($adminRoleId, $this->userRoleIds);
+    }
+}
index af372cb74002e264a950cfdaaa67cbde2e1f3e7c..3855a283bbf9ddcc08ed3593a0dce2498e5e4b70 100644 (file)
@@ -4,7 +4,6 @@ namespace BookStack\Auth\Permissions;
 
 use BookStack\Auth\Role;
 use BookStack\Auth\User;
-use BookStack\Entities\Models\Chapter;
 use BookStack\Entities\Models\Entity;
 use BookStack\Entities\Models\Page;
 use BookStack\Model;
@@ -48,7 +47,7 @@ class PermissionApplicator
             return $hasRolePermission;
         }
 
-        $hasApplicableEntityPermissions = $this->hasEntityPermission($ownable, $userRoleIds, $action);
+        $hasApplicableEntityPermissions = $this->hasEntityPermission($ownable, $user->id, $userRoleIds, $action);
 
         return is_null($hasApplicableEntityPermissions) ? $hasRolePermission : $hasApplicableEntityPermissions;
     }
@@ -57,50 +56,11 @@ 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, int $userId, array $userRoleIds, string $action): ?bool
     {
         $this->ensureValidEntityAction($action);
 
-        $adminRoleId = Role::getSystemRole('admin')->id;
-        if (in_array($adminRoleId, $userRoleIds)) {
-            return true;
-        }
-
-        // The chain order here is very important due to the fact we walk up the chain
-        // in the loop below. Earlier items in the chain have higher priority.
-        $chain = [$entity];
-        if ($entity instanceof Page && $entity->chapter_id) {
-            $chain[] = $entity->chapter;
-        }
-
-        if ($entity instanceof Page || $entity instanceof Chapter) {
-            $chain[] = $entity->book;
-        }
-
-        foreach ($chain as $currentEntity) {
-            $allowedByRoleId = $currentEntity->permissions()
-                ->whereIn('role_id', [0, ...$userRoleIds])
-                ->pluck($action, 'role_id');
-
-            // Continue up the chain if no applicable entity permission overrides.
-            if ($allowedByRoleId->isEmpty()) {
-                continue;
-            }
-
-            // If we have user-role-specific permissions set, allow if any of those
-            // role permissions allow access.
-            $hasDefault = $allowedByRoleId->has(0);
-            if (!$hasDefault || $allowedByRoleId->count() > 1) {
-                return $allowedByRoleId->search(function (bool $allowed, int $roleId) {
-                        return $roleId !== 0 && $allowed;
-                }) !== false;
-            }
-
-            // Otherwise, return the default "Other roles" fallback value.
-            return $allowedByRoleId->get(0);
-        }
-
-        return null;
+        return (new EntityPermissionEvaluator($entity, $userId, $userRoleIds, $action))->evaluate();
     }
 
     /**