Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1c53ffc
Updated entity_permissions table for user perms.
ssddanbrown Dec 7, 2022
f8c4725
Aligned logic to entity_permission role_id usage change
ssddanbrown Dec 7, 2022
7a269e7
Added users to permission form interface
ssddanbrown Dec 10, 2022
93cbd3b
Improved user-permissions adding ux
ssddanbrown Dec 10, 2022
0411185
Added, and built perm. gen for, joint_user_permissions table
ssddanbrown Dec 11, 2022
60bf838
Added joint_user_permissions handling to query system
ssddanbrown Dec 11, 2022
e8a8fed
Started aligning permission behaviour across application methods
ssddanbrown Dec 14, 2022
d54ea1b
Started more formal permission test case definitions
ssddanbrown Dec 15, 2022
f844ae0
Create additional test helper classes
ssddanbrown Dec 15, 2022
491beee
Added additional entity_role_permission scenario tests
ssddanbrown Dec 17, 2022
6a6f5e4
Added a bunch of role content permissions
ssddanbrown Dec 17, 2022
5ffc10e
Added entity user permission scenarios
ssddanbrown Dec 20, 2022
adabf06
Added more inter-method permissions test cases
ssddanbrown Dec 20, 2022
2d9d2bb
Added additional case thats known to currently fail
ssddanbrown Dec 21, 2022
39acbea
Started new permission-caching/querying model
ssddanbrown Dec 22, 2022
7330139
Created big scary query to apply permissions via new format
ssddanbrown Dec 22, 2022
451e4ac
Fixed collapsed perm. gen for book sub-items.
ssddanbrown Dec 23, 2022
026e903
Reworked userCan permission check to follow defined logic.
ssddanbrown Dec 23, 2022
2f1491c
Split out 'restrictEntityQuery' function components
ssddanbrown Jan 13, 2023
932e1d7
Got entity relation query permission application working
ssddanbrown Jan 13, 2023
a825f27
Updated additional relation queries to apply permissions correctly
ssddanbrown Jan 13, 2023
93ba572
Aligned admin permission check restriction ignore
ssddanbrown Jan 13, 2023
55642a3
Attempted fix of issues, realised new query system is a failure
ssddanbrown Jan 14, 2023
3083979
Added method for using enity ownership in relation queries
ssddanbrown Jan 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions app/Actions/ActivityQueries.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ class ActivityQueries
{
protected PermissionApplicator $permissions;

protected array $fieldsForLists = ['id', 'type', 'detail', 'activities.entity_type', 'activities.entity_id', 'user_id', 'created_at'];

public function __construct(PermissionApplicator $permissions)
{
$this->permissions = $permissions;
Expand All @@ -25,9 +27,11 @@ public function __construct(PermissionApplicator $permissions)
*/
public function latest(int $count = 20, int $page = 0): array
{
$query = Activity::query()->select($this->fieldsForLists);
$activityList = $this->permissions
->restrictEntityRelationQuery(Activity::query(), 'activities', 'entity_id', 'entity_type')
->restrictEntityRelationQuery($query, 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')
->whereNotNull('activities.entity_id')
->with(['user', 'entity'])
->skip($count * $page)
->take($count)
Expand Down Expand Up @@ -78,10 +82,12 @@ public function entityActivity(Entity $entity, int $count = 20, int $page = 1):
*/
public function userActivity(User $user, int $count = 20, int $page = 0): array
{
$query = Activity::query()->select($this->fieldsForLists);
$activityList = $this->permissions
->restrictEntityRelationQuery(Activity::query(), 'activities', 'entity_id', 'entity_type')
->restrictEntityRelationQuery($query, 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')
->where('user_id', '=', $user->id)
->whereNotNull('activities.entity_id')
->skip($count * $page)
->take($count)
->get();
Expand Down
9 changes: 5 additions & 4 deletions app/Actions/TagRepo.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,16 @@ public function queryWithTotals(SimpleListOptions $listOptions, string $nameFilt
$sort = 'value';
}

$entityTypeCol = DB::getTablePrefix() . 'tags.entity_type';
$query = Tag::query()
->select([
'name',
($searchTerm || $nameFilter) ? 'value' : DB::raw('COUNT(distinct value) as `values`'),
DB::raw('COUNT(id) as usages'),
DB::raw('SUM(IF(entity_type = \'page\', 1, 0)) as page_count'),
DB::raw('SUM(IF(entity_type = \'chapter\', 1, 0)) as chapter_count'),
DB::raw('SUM(IF(entity_type = \'book\', 1, 0)) as book_count'),
DB::raw('SUM(IF(entity_type = \'bookshelf\', 1, 0)) as shelf_count'),
DB::raw("SUM(IF({$entityTypeCol} = 'page', 1, 0)) as page_count"),
DB::raw("SUM(IF({$entityTypeCol} = 'chapter', 1, 0)) as chapter_count"),
DB::raw("SUM(IF({$entityTypeCol} = 'book', 1, 0)) as book_count"),
DB::raw("SUM(IF({$entityTypeCol} = 'bookshelf', 1, 0)) as shelf_count"),
])
->orderBy($sort, $listOptions->getOrder());

Expand Down
18 changes: 18 additions & 0 deletions app/Auth/Permissions/CollapsedPermission.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace BookStack\Auth\Permissions;

use BookStack\Model;

/**
* @property int $id
* @property ?int $role_id
* @property ?int $user_id
* @property string $entity_type
* @property int $entity_id
* @property bool $view
*/
class CollapsedPermission extends Model
{
protected $table = 'entity_permissions_collapsed';
}
278 changes: 278 additions & 0 deletions app/Auth/Permissions/CollapsedPermissionBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
<?php

namespace BookStack\Auth\Permissions;

use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Facades\DB;

/**
* Collapsed permissions act as a "flattened" view of entity-level permissions in the system
* so inheritance does not have to managed as part of permission querying.
*/
class CollapsedPermissionBuilder
{
/**
* Re-generate all collapsed permissions from scratch.
*/
public function rebuildForAll()
{
DB::table('entity_permissions_collapsed')->truncate();

// Chunk through all books
$this->bookFetchQuery()->chunk(5, function (EloquentCollection $books) {
$this->buildForBooks($books, false);
});

// Chunk through all bookshelves
Bookshelf::query()->withTrashed()
->select(['id'])
->chunk(50, function (EloquentCollection $shelves) {
$this->generateCollapsedPermissions($shelves->all());
});
}

/**
* Rebuild the collapsed permissions for a particular entity.
*/
public function rebuildForEntity(Entity $entity)
{
$entities = [$entity];
if ($entity instanceof Book) {
$books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get();
$this->buildForBooks($books, true);

return;
}

/** @var BookChild $entity */
if ($entity->book) {
$entities[] = $entity->book;
}

if ($entity instanceof Page && $entity->chapter_id) {
$entities[] = $entity->chapter;
}

if ($entity instanceof Chapter) {
foreach ($entity->pages as $page) {
$entities[] = $page;
}
}

$this->buildForEntities($entities);
}

/**
* Get a query for fetching a book with its children.
*/
protected function bookFetchQuery(): Builder
{
return Book::query()->withTrashed()
->select(['id'])->with([
'chapters' => function ($query) {
$query->withTrashed()->select(['id', 'book_id']);
},
'pages' => function ($query) {
$query->withTrashed()->select(['id', 'book_id', 'chapter_id']);
},
]);
}

/**
* Build collapsed permissions for the given books.
*/
protected function buildForBooks(EloquentCollection $books, bool $deleteOld)
{
$entities = clone $books;

/** @var Book $book */
foreach ($books->all() as $book) {
foreach ($book->getRelation('chapters') as $chapter) {
$entities->push($chapter);
}
foreach ($book->getRelation('pages') as $page) {
$entities->push($page);
}
}

if ($deleteOld) {
$this->deleteForEntities($entities->all());
}

$this->generateCollapsedPermissions($entities->all());
}

/**
* Rebuild the collapsed permissions for a collection of entities.
*/
protected function buildForEntities(array $entities)
{
$this->deleteForEntities($entities);
$this->generateCollapsedPermissions($entities);
}

/**
* Delete the stored collapsed permissions for a list of entities.
*
* @param Entity[] $entities
*/
protected function deleteForEntities(array $entities)
{
$simpleEntities = $this->entitiesToSimpleEntities($entities);
$idsByType = $this->entitiesToTypeIdMap($simpleEntities);

DB::transaction(function () use ($idsByType) {
foreach ($idsByType as $type => $ids) {
foreach (array_chunk($ids, 1000) as $idChunk) {
DB::table('entity_permissions_collapsed')
->where('entity_type', '=', $type)
->whereIn('entity_id', $idChunk)
->delete();
}
}
});
}

/**
* Convert the given list of entities into "SimpleEntityData" representations
* for faster usage and property access.
*
* @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->book_id = $attrs['book_id'] ?? null;
$simple->chapter_id = $attrs['chapter_id'] ?? null;
$simpleEntities[] = $simple;
}

return $simpleEntities;
}

/**
* Create & Save collapsed entity permissions.
*
* @param Entity[] $originalEntities
*/
protected function generateCollapsedPermissions(array $originalEntities)
{
$entities = $this->entitiesToSimpleEntities($originalEntities);
$collapsedPermData = [];

// Fetch related entity permissions
$permissions = $this->getEntityPermissionsForEntities($entities);

// Create a mapping of explicit entity permissions
$permissionMap = new EntityPermissionMap($permissions);

// Create Joint Permission Data
foreach ($entities as $entity) {
array_push($collapsedPermData, ...$this->createCollapsedPermissionData($entity, $permissionMap));
}

DB::transaction(function () use ($collapsedPermData) {
foreach (array_chunk($collapsedPermData, 1000) as $dataChunk) {
DB::table('entity_permissions_collapsed')->insert($dataChunk);
}
});
}

/**
* Create collapsed permission data for the given entity using the given permission map.
*/
protected function createCollapsedPermissionData(SimpleEntityData $entity, EntityPermissionMap $permissionMap): array
{
$chain = [
$entity->type . ':' . $entity->id,
$entity->chapter_id ? ('chapter:' . $entity->chapter_id) : null,
$entity->book_id ? ('book:' . $entity->book_id) : null,
];

$permissionData = [];
$overridesApplied = [];

foreach ($chain as $entityTypeId) {
if ($entityTypeId === null) {
continue;
}

$permissions = $permissionMap->getForEntity($entityTypeId);
foreach ($permissions as $permission) {
$related = $permission->getAssignedType() . ':' . $permission->getAssignedTypeId();
if (!isset($overridesApplied[$related])) {
$permissionData[] = [
'role_id' => $permission->role_id,
'user_id' => $permission->user_id,
'view' => $permission->view,
'entity_type' => $entity->type,
'entity_id' => $entity->id,
];
$overridesApplied[$related] = true;
}
}
}

return $permissionData;
}

/**
* 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 SimpleEntityData[] $entities
*
* @return array<string, int[]>
*/
protected function entitiesToTypeIdMap(array $entities): array
{
$idsByType = [];

foreach ($entities as $entity) {
if (!isset($idsByType[$entity->type])) {
$idsByType[$entity->type] = [];
}

$idsByType[$entity->type][] = $entity->id;
}

return $idsByType;
}

/**
* Get the entity permissions for all the given entities.
*
* @param SimpleEntityData[] $entities
*
* @return EntityPermission[]
*/
protected function getEntityPermissionsForEntities(array $entities): array
{
$idsByType = $this->entitiesToTypeIdMap($entities);
$permissionFetch = EntityPermission::query()
->where(function (Builder $query) use ($idsByType) {
foreach ($idsByType as $type => $ids) {
$query->orWhere(function (Builder $query) use ($type, $ids) {
$query->where('entity_type', '=', $type)->whereIn('entity_id', $ids);
});
}
});

return $permissionFetch->get()->all();
}
}
Loading