Added simple data model for faster permission generation

This commit is contained in:
Dan Brown 2022-07-12 21:13:02 +01:00
parent b0a4d3d059
commit 2989852520
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
3 changed files with 95 additions and 73 deletions

View File

@ -16,7 +16,7 @@ use Illuminate\Support\Facades\DB;
class JointPermissionBuilder class JointPermissionBuilder
{ {
/** /**
* @var array<string, array<int, Entity>> * @var array<string, array<int, SimpleEntityData>>
*/ */
protected $entityCache; protected $entityCache;
@ -26,7 +26,6 @@ class JointPermissionBuilder
public function rebuildForAll() public function rebuildForAll()
{ {
JointPermission::query()->truncate(); JointPermission::query()->truncate();
$this->readyEntityCache();
// Get all roles (Should be the most limited dimension) // Get all roles (Should be the most limited dimension)
$roles = Role::query()->with('permissions')->get()->all(); $roles = Role::query()->with('permissions')->get()->all();
@ -45,8 +44,6 @@ class JointPermissionBuilder
/** /**
* Rebuild the entity jointPermissions for a particular entity. * Rebuild the entity jointPermissions for a particular entity.
*
* @throws Throwable
*/ */
public function rebuildForEntity(Entity $entity) public function rebuildForEntity(Entity $entity)
{ {
@ -99,51 +96,39 @@ class JointPermissionBuilder
/** /**
* Prepare the local entity cache and ensure it's empty. * 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 = []; $this->entityCache = [];
foreach ($entities as $entity) { foreach ($entities as $entity) {
$class = get_class($entity); if (!isset($this->entityCache[$entity->type])) {
$this->entityCache[$entity->type] = [];
if (!isset($this->entityCache[$class])) {
$this->entityCache[$class] = [];
} }
$this->entityCache[$class][$entity->getRawAttribute('id')] = $entity; $this->entityCache[$entity->type][$entity->id] = $entity;
} }
} }
/** /**
* Get a book via ID, Checks local cache. * 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'][$bookId];
return $this->entityCache[Book::class][$bookId];
}
return Book::query()->withTrashed()->find($bookId);
} }
/** /**
* Get a chapter via ID, Checks local cache. * 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'][$chapterId];
return $this->entityCache[Chapter::class][$chapterId];
}
return Chapter::query()
->withTrashed()
->find($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 protected function bookFetchQuery(): Builder
{ {
@ -161,8 +146,6 @@ class JointPermissionBuilder
/** /**
* Build joint permissions for the given book and role combinations. * Build joint permissions for the given book and role combinations.
*
* @throws Throwable
*/ */
protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false) 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. * Rebuild the entity jointPermissions for a collection of entities.
*
* @throws Throwable
*/ */
protected function buildJointPermissionsForEntities(array $entities) protected function buildJointPermissionsForEntities(array $entities)
{ {
@ -201,12 +182,11 @@ class JointPermissionBuilder
* Delete all the entity jointPermissions for a list of entities. * Delete all the entity jointPermissions for a list of entities.
* *
* @param Entity[] $entities * @param Entity[] $entities
*
* @throws Throwable
*/ */
protected function deleteManyJointPermissionsForEntities(array $entities) protected function deleteManyJointPermissionsForEntities(array $entities)
{ {
$idsByType = $this->entitiesToTypeIdMap($entities); $simpleEntities = $this->entitiesToSimpleEntities($entities);
$idsByType = $this->entitiesToTypeIdMap($simpleEntities);
DB::transaction(function () use ($idsByType) { DB::transaction(function () use ($idsByType) {
foreach ($idsByType as $type => $ids) { 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. * Create & Save entity jointPermissions for many entities and roles.
* *
* @param Entity[] $entities * @param Entity[] $entities
* @param Role[] $roles * @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); $this->readyEntityCache($entities);
$jointPermissions = []; $jointPermissions = [];
// Create a mapping of entity restricted statuses // Create a mapping of entity restricted statuses
$entityRestrictedMap = []; $entityRestrictedMap = [];
foreach ($entities as $entity) { 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 // Fetch related entity permissions
@ -262,7 +264,14 @@ class JointPermissionBuilder
foreach ($entities as $entity) { foreach ($entities as $entity) {
foreach ($roles as $role) { foreach ($roles as $role) {
foreach ($this->getActions($entity) as $action) { 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 * 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. * the ids of that given type. The type used is the DB morph class.
* @param Entity[] $entities * @param SimpleEntityData[] $entities
* @return array<string, int[]> * @return array<string, int[]>
*/ */
protected function entitiesToTypeIdMap(array $entities): array protected function entitiesToTypeIdMap(array $entities): array
@ -285,13 +294,11 @@ class JointPermissionBuilder
$idsByType = []; $idsByType = [];
foreach ($entities as $entity) { foreach ($entities as $entity) {
$type = $entity->getMorphClass(); if (!isset($idsByType[$entity->type])) {
$idsByType[$entity->type] = [];
if (!isset($idsByType[$type])) {
$idsByType[$type] = [];
} }
$idsByType[$type][] = $entity->getRawAttribute('id'); $idsByType[$entity->type][] = $entity->id;
} }
return $idsByType; return $idsByType;
@ -299,10 +306,10 @@ class JointPermissionBuilder
/** /**
* Get the entity permissions for all the given entities * Get the entity permissions for all the given entities
* @param Entity[] $entities * @param SimpleEntityData[] $entities
* @return EloquentCollection * @return EntityPermission[]
*/ */
protected function getEntityPermissionsForEntities(array $entities) protected function getEntityPermissionsForEntities(array $entities): array
{ {
$idsByType = $this->entitiesToTypeIdMap($entities); $idsByType = $this->entitiesToTypeIdMap($entities);
$permissionFetch = EntityPermission::query(); $permissionFetch = EntityPermission::query();
@ -313,19 +320,21 @@ class JointPermissionBuilder
}); });
} }
return $permissionFetch->get(); return $permissionFetch->get()->all();
} }
/** /**
* Get the actions related to an entity. * Get the actions related to an entity.
*/ */
protected function getActions(Entity $entity): array protected function getActions(SimpleEntityData $entity): array
{ {
$baseActions = ['view', 'update', 'delete']; $baseActions = ['view', 'update', 'delete'];
if ($entity instanceof Chapter || $entity instanceof Book) {
if ($entity->type === 'chapter' || $entity->type === 'book') {
$baseActions[] = 'page-create'; $baseActions[] = 'page-create';
} }
if ($entity instanceof Book) {
if ($entity->type === 'book') {
$baseActions[] = 'chapter-create'; $baseActions[] = 'chapter-create';
} }
@ -336,45 +345,45 @@ class JointPermissionBuilder
* Create entity permission data for an entity and role * Create entity permission data for an entity and role
* for a particular action. * 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; $permissionPrefix = (strpos($action, '-') === false ? ($entity->type . '-') : '') . $action;
$roleHasPermission = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-all']); $roleHasPermission = isset($rolePermissionMap[$roleId . ':' . $permissionPrefix . '-all']);
$roleHasPermissionOwn = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-own']); $roleHasPermissionOwn = isset($rolePermissionMap[$roleId . ':' . $permissionPrefix . '-own']);
$explodedAction = explode('-', $action); $explodedAction = explode('-', $action);
$restrictionAction = end($explodedAction); $restrictionAction = end($explodedAction);
if ($role->system_name === 'admin') { if ($isAdminRole) {
return $this->createJointPermissionDataArray($entity, $role, $action, true, true); return $this->createJointPermissionDataArray($entity, $roleId, $action, true, true);
} }
if ($entity->restricted) { 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) { if ($entity->type === 'book' || $entity->type === 'bookshelf') {
return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn); return $this->createJointPermissionDataArray($entity, $roleId, $action, $roleHasPermission, $roleHasPermissionOwn);
} }
// For chapters and pages, Check if explicit permissions are set on the Book. // For chapters and pages, Check if explicit permissions are set on the Book.
$book = $this->getBook($entity->book_id); $book = $this->getBook($entity->book_id);
$hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $book, $role, $restrictionAction); $hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $book, $roleId, $restrictionAction);
$hasPermissiveAccessToParents = !$book->restricted; $hasPermissiveAccessToParents = !$book->restricted;
// For pages with a chapter, Check if explicit permissions are set on the Chapter // 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); $chapter = $this->getChapter($entity->chapter_id);
$hasPermissiveAccessToParents = $hasPermissiveAccessToParents && !$chapter->restricted; $hasPermissiveAccessToParents = $hasPermissiveAccessToParents && !$chapter->restricted;
if ($chapter->restricted) { if ($chapter->restricted) {
$hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $chapter, $role, $restrictionAction); $hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $chapter, $roleId, $restrictionAction);
} }
} }
return $this->createJointPermissionDataArray( return $this->createJointPermissionDataArray(
$entity, $entity,
$role, $roleId,
$action, $action,
($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)), ($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents)) ($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
@ -384,9 +393,9 @@ class JointPermissionBuilder
/** /**
* Check for an active restriction in an entity map. * 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; return $entityMap[$key] ?? false;
} }
@ -395,16 +404,16 @@ class JointPermissionBuilder
* Create an array of data with the information of an entity jointPermissions. * Create an array of data with the information of an entity jointPermissions.
* Used to build data for bulk insertion. * 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 [ return [
'action' => $action, 'action' => $action,
'entity_id' => $entity->getRawAttribute('id'), 'entity_id' => $entity->id,
'entity_type' => $entity->getMorphClass(), 'entity_type' => $entity->type,
'has_permission' => $permissionAll, 'has_permission' => $permissionAll,
'has_permission_own' => $permissionOwn, 'has_permission_own' => $permissionOwn,
'owned_by' => $entity->getRawAttribute('owned_by'), 'owned_by' => $entity->owned_by,
'role_id' => $role->getRawAttribute('id'), 'role_id' => $roleId,
]; ];
} }

View File

@ -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;
}

View File

@ -262,7 +262,7 @@ class AttachmentsApiTest extends TestCase
/** @var Page $page */ /** @var Page $page */
$page = Page::query()->first(); $page = Page::query()->first();
$page->draft = true; $page->draft = true;
$page->owned_by = $editor; $page->owned_by = $editor->id;
$page->save(); $page->save();
$this->regenEntityPermissions($page); $this->regenEntityPermissions($page);