]> BookStack Code Mirror - bookstack/commitdiff
Added simple data model for faster permission generation
authorDan Brown <redacted>
Tue, 12 Jul 2022 20:13:02 +0000 (21:13 +0100)
committerDan Brown <redacted>
Tue, 12 Jul 2022 20:13:02 +0000 (21:13 +0100)
app/Auth/Permissions/JointPermissionBuilder.php
app/Auth/Permissions/SimpleEntityData.php [new file with mode: 0644]
tests/Api/AttachmentsApiTest.php

index fbf77741ac43827076c3f70598a8b1afb95d72a9..fe25e02ff1e3d3131a6b0529b0faf2e18c57f63a 100644 (file)
@@ -16,7 +16,7 @@ use Illuminate\Support\Facades\DB;
 class JointPermissionBuilder
 {
     /**
-     * @var array<string, array<int, Entity>>
+     * @var array<string, array<int, SimpleEntityData>>
      */
     protected $entityCache;
 
@@ -26,7 +26,6 @@ class JointPermissionBuilder
     public function rebuildForAll()
     {
         JointPermission::query()->truncate();
-        $this->readyEntityCache();
 
         // Get all roles (Should be the most limited dimension)
         $roles = Role::query()->with('permissions')->get()->all();
@@ -45,8 +44,6 @@ class JointPermissionBuilder
 
     /**
      * Rebuild the entity jointPermissions for a particular entity.
-     *
-     * @throws Throwable
      */
     public function rebuildForEntity(Entity $entity)
     {
@@ -99,51 +96,39 @@ class JointPermissionBuilder
     /**
      * Prepare the local entity cache and ensure it's empty.
      *
-     * @param Entity[] $entities
+     * @param SimpleEntityData[] $entities
      */
-    protected function readyEntityCache(array $entities = [])
+    protected function readyEntityCache(array $entities)
     {
         $this->entityCache = [];
 
         foreach ($entities as $entity) {
-            $class = get_class($entity);
-
-            if (!isset($this->entityCache[$class])) {
-                $this->entityCache[$class] = [];
+            if (!isset($this->entityCache[$entity->type])) {
+                $this->entityCache[$entity->type] = [];
             }
 
-            $this->entityCache[$class][$entity->getRawAttribute('id')] = $entity;
+            $this->entityCache[$entity->type][$entity->id] = $entity;
         }
     }
 
     /**
      * Get a book via ID, Checks local cache.
      */
-    protected function getBook(int $bookId): ?Book
+    protected function getBook(int $bookId): SimpleEntityData
     {
-        if ($this->entityCache[Book::class][$bookId] ?? false) {
-            return $this->entityCache[Book::class][$bookId];
-        }
-
-        return Book::query()->withTrashed()->find($bookId);
+        return $this->entityCache['book'][$bookId];
     }
 
     /**
      * Get a chapter via ID, Checks local cache.
      */
-    protected function getChapter(int $chapterId): ?Chapter
+    protected function getChapter(int $chapterId): SimpleEntityData
     {
-        if ($this->entityCache[Chapter::class][$chapterId] ?? false) {
-            return $this->entityCache[Chapter::class][$chapterId];
-        }
-
-        return Chapter::query()
-            ->withTrashed()
-            ->find($chapterId);
+        return $this->entityCache['chapter'][$chapterId];
     }
 
     /**
-     * Get a query for fetching a book with it's children.
+     * Get a query for fetching a book with its children.
      */
     protected function bookFetchQuery(): Builder
     {
@@ -161,8 +146,6 @@ class JointPermissionBuilder
 
     /**
      * Build joint permissions for the given book and role combinations.
-     *
-     * @throws Throwable
      */
     protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false)
     {
@@ -187,8 +170,6 @@ class JointPermissionBuilder
 
     /**
      * Rebuild the entity jointPermissions for a collection of entities.
-     *
-     * @throws Throwable
      */
     protected function buildJointPermissionsForEntities(array $entities)
     {
@@ -201,12 +182,11 @@ class JointPermissionBuilder
      * Delete all the entity jointPermissions for a list of entities.
      *
      * @param Entity[] $entities
-     *
-     * @throws Throwable
      */
     protected function deleteManyJointPermissionsForEntities(array $entities)
     {
-        $idsByType = $this->entitiesToTypeIdMap($entities);
+        $simpleEntities = $this->entitiesToSimpleEntities($entities);
+        $idsByType = $this->entitiesToTypeIdMap($simpleEntities);
 
         DB::transaction(function () use ($idsByType) {
             foreach ($idsByType as $type => $ids) {
@@ -220,23 +200,45 @@ class JointPermissionBuilder
         });
     }
 
+    /**
+     * @param Entity[] $entities
+     * @return SimpleEntityData[]
+     */
+    protected function entitiesToSimpleEntities(array $entities): array
+    {
+        $simpleEntities = [];
+
+        foreach ($entities as $entity) {
+            $attrs = $entity->getAttributes();
+            $simple = new SimpleEntityData();
+            $simple->id = $attrs['id'];
+            $simple->type = $entity->getMorphClass();
+            $simple->restricted = boolval($attrs['restricted'] ?? 0);
+            $simple->owned_by = $attrs['owned_by'] ?? 0;
+            $simple->book_id = $attrs['book_id'] ?? null;
+            $simple->chapter_id = $attrs['chapter_id'] ?? null;
+            $simpleEntities[] = $simple;
+        }
+
+        return $simpleEntities;
+    }
+
     /**
      * Create & Save entity jointPermissions for many entities and roles.
      *
      * @param Entity[] $entities
      * @param Role[]   $roles
-     *
-     * @throws Throwable
      */
-    protected function createManyJointPermissions(array $entities, array $roles)
+    protected function createManyJointPermissions(array $originalEntities, array $roles)
     {
+        $entities = $this->entitiesToSimpleEntities($originalEntities);
         $this->readyEntityCache($entities);
         $jointPermissions = [];
 
         // Create a mapping of entity restricted statuses
         $entityRestrictedMap = [];
         foreach ($entities as $entity) {
-            $entityRestrictedMap[$entity->getMorphClass() . ':' . $entity->getRawAttribute('id')] = boolval($entity->getRawAttribute('restricted'));
+            $entityRestrictedMap[$entity->type . ':' . $entity->id] = $entity->restricted;
         }
 
         // Fetch related entity permissions
@@ -262,7 +264,14 @@ class JointPermissionBuilder
         foreach ($entities as $entity) {
             foreach ($roles as $role) {
                 foreach ($this->getActions($entity) as $action) {
-                    $jointPermissions[] = $this->createJointPermissionData($entity, $role, $action, $permissionMap, $rolePermissionMap);
+                    $jointPermissions[] = $this->createJointPermissionData(
+                        $entity,
+                        $role->getRawAttribute('id'),
+                        $action,
+                        $permissionMap,
+                        $rolePermissionMap,
+                        $role->system_name === 'admin'
+                    );
                 }
             }
         }
@@ -277,7 +286,7 @@ class JointPermissionBuilder
     /**
      * From the given entity list, provide back a mapping of entity types to
      * the ids of that given type. The type used is the DB morph class.
-     * @param Entity[] $entities
+     * @param SimpleEntityData[] $entities
      * @return array<string, int[]>
      */
     protected function entitiesToTypeIdMap(array $entities): array
@@ -285,13 +294,11 @@ class JointPermissionBuilder
         $idsByType = [];
 
         foreach ($entities as $entity) {
-            $type = $entity->getMorphClass();
-
-            if (!isset($idsByType[$type])) {
-                $idsByType[$type] = [];
+            if (!isset($idsByType[$entity->type])) {
+                $idsByType[$entity->type] = [];
             }
 
-            $idsByType[$type][] = $entity->getRawAttribute('id');
+            $idsByType[$entity->type][] = $entity->id;
         }
 
         return $idsByType;
@@ -299,10 +306,10 @@ class JointPermissionBuilder
 
     /**
      * Get the entity permissions for all the given entities
-     * @param Entity[] $entities
-     * @return EloquentCollection
+     * @param SimpleEntityData[] $entities
+     * @return EntityPermission[]
      */
-    protected function getEntityPermissionsForEntities(array $entities)
+    protected function getEntityPermissionsForEntities(array $entities): array
     {
         $idsByType = $this->entitiesToTypeIdMap($entities);
         $permissionFetch = EntityPermission::query();
@@ -313,19 +320,21 @@ class JointPermissionBuilder
             });
         }
 
-        return $permissionFetch->get();
+        return $permissionFetch->get()->all();
     }
 
     /**
      * Get the actions related to an entity.
      */
-    protected function getActions(Entity $entity): array
+    protected function getActions(SimpleEntityData $entity): array
     {
         $baseActions = ['view', 'update', 'delete'];
-        if ($entity instanceof Chapter || $entity instanceof Book) {
+
+        if ($entity->type === 'chapter' || $entity->type === 'book') {
             $baseActions[] = 'page-create';
         }
-        if ($entity instanceof Book) {
+
+        if ($entity->type === 'book') {
             $baseActions[] = 'chapter-create';
         }
 
@@ -336,45 +345,45 @@ class JointPermissionBuilder
      * Create entity permission data for an entity and role
      * for a particular action.
      */
-    protected function createJointPermissionData(Entity $entity, Role $role, string $action, array $permissionMap, array $rolePermissionMap): array
+    protected function createJointPermissionData(SimpleEntityData $entity, int $roleId, string $action, array $permissionMap, array $rolePermissionMap, bool $isAdminRole): array
     {
-        $permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action;
-        $roleHasPermission = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-all']);
-        $roleHasPermissionOwn = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-own']);
+        $permissionPrefix = (strpos($action, '-') === false ? ($entity->type . '-') : '') . $action;
+        $roleHasPermission = isset($rolePermissionMap[$roleId . ':' . $permissionPrefix . '-all']);
+        $roleHasPermissionOwn = isset($rolePermissionMap[$roleId . ':' . $permissionPrefix . '-own']);
         $explodedAction = explode('-', $action);
         $restrictionAction = end($explodedAction);
 
-        if ($role->system_name === 'admin') {
-            return $this->createJointPermissionDataArray($entity, $role, $action, true, true);
+        if ($isAdminRole) {
+            return $this->createJointPermissionDataArray($entity, $roleId, $action, true, true);
         }
 
         if ($entity->restricted) {
-            $hasAccess = $this->mapHasActiveRestriction($permissionMap, $entity, $role, $restrictionAction);
+            $hasAccess = $this->mapHasActiveRestriction($permissionMap, $entity, $roleId, $restrictionAction);
 
-            return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
+            return $this->createJointPermissionDataArray($entity, $roleId, $action, $hasAccess, $hasAccess);
         }
 
-        if ($entity instanceof Book || $entity instanceof Bookshelf) {
-            return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
+        if ($entity->type === 'book' || $entity->type === 'bookshelf') {
+            return $this->createJointPermissionDataArray($entity, $roleId, $action, $roleHasPermission, $roleHasPermissionOwn);
         }
 
         // For chapters and pages, Check if explicit permissions are set on the Book.
         $book = $this->getBook($entity->book_id);
-        $hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $book, $role, $restrictionAction);
+        $hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $book, $roleId, $restrictionAction);
         $hasPermissiveAccessToParents = !$book->restricted;
 
         // For pages with a chapter, Check if explicit permissions are set on the Chapter
-        if ($entity instanceof Page && intval($entity->chapter_id) !== 0) {
+        if ($entity->type === 'page' && $entity->chapter_id !== 0) {
             $chapter = $this->getChapter($entity->chapter_id);
             $hasPermissiveAccessToParents = $hasPermissiveAccessToParents && !$chapter->restricted;
             if ($chapter->restricted) {
-                $hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $chapter, $role, $restrictionAction);
+                $hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $chapter, $roleId, $restrictionAction);
             }
         }
 
         return $this->createJointPermissionDataArray(
             $entity,
-            $role,
+            $roleId,
             $action,
             ($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
             ($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
@@ -384,9 +393,9 @@ class JointPermissionBuilder
     /**
      * Check for an active restriction in an entity map.
      */
-    protected function mapHasActiveRestriction(array $entityMap, Entity $entity, Role $role, string $action): bool
+    protected function mapHasActiveRestriction(array $entityMap, SimpleEntityData $entity, int $roleId, string $action): bool
     {
-        $key = $entity->getMorphClass() . ':' . $entity->getRawAttribute('id') . ':' . $role->getRawAttribute('id') . ':' . $action;
+        $key = $entity->type . ':' . $entity->id . ':' . $roleId . ':' . $action;
 
         return $entityMap[$key] ?? false;
     }
@@ -395,16 +404,16 @@ class JointPermissionBuilder
      * Create an array of data with the information of an entity jointPermissions.
      * Used to build data for bulk insertion.
      */
-    protected function createJointPermissionDataArray(Entity $entity, Role $role, string $action, bool $permissionAll, bool $permissionOwn): array
+    protected function createJointPermissionDataArray(SimpleEntityData $entity, int $roleId, string $action, bool $permissionAll, bool $permissionOwn): array
     {
         return [
             'action'             => $action,
-            'entity_id'          => $entity->getRawAttribute('id'),
-            'entity_type'        => $entity->getMorphClass(),
+            'entity_id'          => $entity->id,
+            'entity_type'        => $entity->type,
             'has_permission'     => $permissionAll,
             'has_permission_own' => $permissionOwn,
-            'owned_by'           => $entity->getRawAttribute('owned_by'),
-            'role_id'            => $role->getRawAttribute('id'),
+            'owned_by'           => $entity->owned_by,
+            'role_id'            => $roleId,
         ];
     }
 
diff --git a/app/Auth/Permissions/SimpleEntityData.php b/app/Auth/Permissions/SimpleEntityData.php
new file mode 100644 (file)
index 0000000..0d1c94b
--- /dev/null
@@ -0,0 +1,13 @@
+<?php
+
+namespace BookStack\Auth\Permissions;
+
+class SimpleEntityData
+{
+    public int $id;
+    public string $type;
+    public bool $restricted;
+    public int $owned_by;
+    public ?int $book_id;
+    public ?int $chapter_id;
+}
\ No newline at end of file
index a343370161616503938bf566cb02ee60e0474553..6077868b26cc60b907991c8b588abea3ad6c8879 100644 (file)
@@ -262,7 +262,7 @@ class AttachmentsApiTest extends TestCase
         /** @var Page $page */
         $page = Page::query()->first();
         $page->draft = true;
-        $page->owned_by = $editor;
+        $page->owned_by = $editor->id;
         $page->save();
         $this->regenEntityPermissions($page);