Added simple data model for faster permission generation
This commit is contained in:
parent
b0a4d3d059
commit
2989852520
|
@ -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,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue