Permissions: Updated generation querying to be more efficient

Query of existing entity permissions during view permission generation
could cause timeouts or SQL placeholder limits due to massive whereOr
query generation, where an "or where" clause would be created for each
entity type/id combo involved, which could be all within 20 books.

This updates the query handling to use a query per type involved, with
no "or where"s, and to be chunked at large entity counts.

Also tweaked role-specific permission regen to chunk books at
half-previous rate to prevent such a large scope being involved on each
chunk.

For #4695
This commit is contained in:
Dan Brown 2023-12-23 13:35:57 +00:00
parent 88ee33ee49
commit 02d94c8798
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
3 changed files with 50 additions and 24 deletions

View File

@ -9,11 +9,9 @@ use Illuminate\Database\Eloquent\Builder;
class EntityPermissionEvaluator
{
protected string $action;
public function __construct(string $action)
{
$this->action = $action;
public function __construct(
protected string $action
) {
}
public function evaluateEntityForUser(Entity $entity, array $userRoleIds): ?bool
@ -82,23 +80,25 @@ class EntityPermissionEvaluator
*/
protected function getPermissionsMapByTypeId(array $typeIdChain, array $filterRoleIds): array
{
$query = 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);
});
$idsByType = [];
foreach ($typeIdChain as $typeId) {
[$type, $id] = explode(':', $typeId);
if (!isset($idsByType[$type])) {
$idsByType[$type] = [];
}
});
if (!empty($filterRoleIds)) {
$query->where(function (Builder $query) use ($filterRoleIds) {
$query->whereIn('role_id', [...$filterRoleIds, 0]);
});
$idsByType[$type][] = $id;
}
$relevantPermissions = $query->get(['entity_id', 'entity_type', 'role_id', $this->action])->all();
$relevantPermissions = [];
foreach ($idsByType as $type => $ids) {
$idsChunked = array_chunk($ids, 10000);
foreach ($idsChunked as $idChunk) {
$permissions = $this->getPermissionsForEntityIdsOfType($type, $idChunk, $filterRoleIds);
array_push($relevantPermissions, ...$permissions);
}
}
$map = [];
foreach ($relevantPermissions as $permission) {
@ -113,6 +113,26 @@ class EntityPermissionEvaluator
return $map;
}
/**
* @param string[] $ids
* @param int[] $filterRoleIds
* @return EntityPermission[]
*/
protected function getPermissionsForEntityIdsOfType(string $type, array $ids, array $filterRoleIds): array
{
$query = EntityPermission::query()
->where('entity_type', '=', $type)
->whereIn('entity_id', $ids);
if (!empty($filterRoleIds)) {
$query->where(function (Builder $query) use ($filterRoleIds) {
$query->whereIn('role_id', [...$filterRoleIds, 0]);
});
}
return $query->get(['entity_id', 'entity_type', 'role_id', $this->action])->all();
}
/**
* @return string[]
*/

View File

@ -83,13 +83,13 @@ class JointPermissionBuilder
$role->load('permissions');
// Chunk through all books
$this->bookFetchQuery()->chunk(20, function ($books) use ($roles) {
$this->bookFetchQuery()->chunk(10, function ($books) use ($roles) {
$this->buildJointPermissionsForBooks($books, $roles);
});
// Chunk through all bookshelves
Bookshelf::query()->select(['id', 'owned_by'])
->chunk(50, function ($shelves) use ($roles) {
->chunk(100, function ($shelves) use ($roles) {
$this->createManyJointPermissions($shelves->all(), $roles);
});
}

View File

@ -28,12 +28,18 @@ class LargeContentSeeder extends Seeder
/** @var Book $largeBook */
$largeBook = Book::factory()->create(['name' => 'Large book' . Str::random(10), 'created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
$pages = Page::factory()->count(200)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
$chapters = Chapter::factory()->count(50)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
$largeBook->pages()->saveMany($pages);
$largeBook->chapters()->saveMany($chapters);
$all = array_merge([$largeBook], array_values($pages->all()), array_values($chapters->all()));
$allPages = [];
foreach ($chapters as $chapter) {
$pages = Page::factory()->count(100)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id, 'chapter_id' => $chapter->id]);
$largeBook->pages()->saveMany($pages);
array_push($allPages, ...$pages->all());
}
$all = array_merge([$largeBook], $allPages, array_values($chapters->all()));
app()->make(JointPermissionBuilder::class)->rebuildForEntity($largeBook);
app()->make(SearchIndex::class)->indexEntities($all);