]> BookStack Code Mirror - bookstack/commitdiff
Added, and built perm. gen for, joint_user_permissions table
authorDan Brown <redacted>
Sun, 11 Dec 2022 14:51:53 +0000 (14:51 +0000)
committerDan Brown <redacted>
Sun, 11 Dec 2022 14:51:53 +0000 (14:51 +0000)
app/Auth/Permissions/JointPermissionBuilder.php
app/Auth/Permissions/JointUserPermission.php [new file with mode: 0644]
database/migrations/2022_12_11_104506_create_joint_user_permissions_table.php [new file with mode: 0644]

index 0d9d2394291a0c657fd403ba94c6bc24b2d6fa5f..dbef78574abbabc5dd8efb36bb1b7ddd7a70636a 100644 (file)
@@ -30,6 +30,7 @@ class JointPermissionBuilder
     public function rebuildForAll()
     {
         JointPermission::query()->truncate();
+        JointUserPermission::query()->truncate();
 
         // Get all roles (Should be the most limited dimension)
         $roles = Role::query()->with('permissions')->get()->all();
@@ -199,6 +200,10 @@ class JointPermissionBuilder
                         ->where('entity_type', '=', $type)
                         ->whereIn('entity_id', $idChunk)
                         ->delete();
+                    DB::table('joint_user_permissions')
+                        ->where('entity_type', '=', $type)
+                        ->whereIn('entity_id', $idChunk)
+                        ->delete();
                 }
             }
         });
@@ -238,16 +243,21 @@ class JointPermissionBuilder
         $entities = $this->entitiesToSimpleEntities($originalEntities);
         $this->readyEntityCache($entities);
         $jointPermissions = [];
+        $jointUserPermissions = [];
 
         // Fetch related entity permissions
         $permissions = $this->getEntityPermissionsForEntities($entities);
 
         // Create a mapping of explicit entity permissions
         $permissionMap = [];
+        $controlledUserIds = [];
         foreach ($permissions as $permission) {
             $type = $permission->role_id ? 'role' : ($permission->user_id ? 'user' : 'fallback');
             $id = $permission->role_id ?? $permission->user_id ?? '0';
             $key = $permission->entity_type . ':' . $permission->entity_id . ':' . $type  . ':' .  $id;
+            if ($type === 'user') {
+                $controlledUserIds[$id] = true;
+            }
             $permissionMap[$key] = $permission->view;
         }
 
@@ -270,6 +280,12 @@ class JointPermissionBuilder
                     $role->system_name === 'admin'
                 );
             }
+            foreach ($controlledUserIds as $userId => $exists) {
+                $userPermitted = $this->getUserPermissionOverrideStatus($entity, $userId, $permissionMap);
+                if ($userPermitted !== null) {
+                    $jointUserPermissions[] = $this->createJointUserPermissionDataArray($entity, $userId, $userPermitted);
+                }
+            }
         }
 
         DB::transaction(function () use ($jointPermissions) {
@@ -277,6 +293,12 @@ class JointPermissionBuilder
                 DB::table('joint_permissions')->insert($jointPermissionChunk);
             }
         });
+
+        DB::transaction(function () use ($jointUserPermissions) {
+            foreach (array_chunk($jointUserPermissions, 1000) as $jointUserPermissionsChunk) {
+                DB::table('joint_user_permissions')->insert($jointUserPermissionsChunk);
+            }
+        });
     }
 
     /**
@@ -371,6 +393,40 @@ class JointPermissionBuilder
         );
     }
 
+    /**
+     * Get the status of a user-specific permission override for the given entity user combo if existing.
+     * This can return null where no user-specific permission overrides are applicable.
+     */
+    protected function getUserPermissionOverrideStatus(SimpleEntityData $entity, int $userId, array $permissionMap): ?bool
+    {
+        // If direct permissions exists, return those
+        $directKey = $entity->type . ':' . $entity->id . ':user:' . $userId;
+        if (isset($permissionMap[$directKey])) {
+            return $permissionMap[$directKey];
+        }
+
+        // If a book or shelf, exit out since no parents to check
+        if ($entity->type === 'book' || $entity->type === 'bookshelf') {
+            return null;
+        }
+
+        // If a chapter or page, get the parent book permission status.
+        // defaults to null where no permission is set.
+        $bookKey = 'book:' . $entity->book_id . ':user:' . $userId;
+        $bookPermission = $permissionMap[$bookKey] ?? null;
+
+        // If a page within a chapter, return the chapter permission if existing otherwise
+        // default ot the parent book permission.
+        if ($entity->type === 'page' && $entity->chapter_id !== 0) {
+            $chapterKey = 'chapter:' . $entity->chapter_id . ':user:' . $userId;
+            $chapterPermission = $permissionMap[$chapterKey] ?? null;
+            return $chapterPermission ?? $bookPermission;
+        }
+
+        // Return the book permission status
+        return $bookPermission;
+    }
+
     /**
      * Check if entity permissions are defined within the given map, for the given entity and role.
      * Checks for the default `role_id=0` backup option as a fallback.
@@ -407,4 +463,18 @@ class JointPermissionBuilder
             'role_id'            => $roleId,
         ];
     }
+
+    /**
+     * Create an array of data with the information of an JointUserPermission.
+     * Used to build data for bulk insertion.
+     */
+    protected function createJointUserPermissionDataArray(SimpleEntityData $entity, int $userId, bool $hasPermission): array
+    {
+        return [
+            'entity_id'          => $entity->id,
+            'entity_type'        => $entity->type,
+            'has_permission'     => $hasPermission,
+            'user_id'            => $userId,
+        ];
+    }
 }
diff --git a/app/Auth/Permissions/JointUserPermission.php b/app/Auth/Permissions/JointUserPermission.php
new file mode 100644 (file)
index 0000000..2987bf5
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+
+namespace BookStack\Auth\Permissions;
+
+use BookStack\Entities\Models\Entity;
+use BookStack\Model;
+use Illuminate\Database\Eloquent\Relations\MorphOne;
+
+/**
+ * Holds the "cached" user-specific permissions for entities in the system.
+ * These only exist to indicate resolved permissions active via user-specific
+ * entity permissions, not for all permission combinations for all users.
+ *
+ * @property int $user_id
+ * @property int $entity_id
+ * @property string $entity_type
+ * @property boolean $has_permission
+ */
+class JointUserPermission extends Model
+{
+    protected $primaryKey = null;
+    public $timestamps = false;
+
+    /**
+     * Get the entity this points to.
+     */
+    public function entity(): MorphOne
+    {
+        return $this->morphOne(Entity::class, 'entity');
+    }
+}
diff --git a/database/migrations/2022_12_11_104506_create_joint_user_permissions_table.php b/database/migrations/2022_12_11_104506_create_joint_user_permissions_table.php
new file mode 100644 (file)
index 0000000..f9ce2f6
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreateJointUserPermissionsTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('joint_user_permissions', function (Blueprint $table) {
+            $table->unsignedInteger('user_id');
+            $table->string('entity_type');
+            $table->unsignedInteger('entity_id');
+            $table->boolean('has_permission')->index();
+
+            $table->primary(['user_id', 'entity_type', 'entity_id']);
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('joint_user_permissions');
+    }
+}