3 namespace BookStack\Auth\Permissions;
5 use BookStack\Entities\Models\Book;
6 use BookStack\Entities\Models\BookChild;
7 use BookStack\Entities\Models\Bookshelf;
8 use BookStack\Entities\Models\Chapter;
9 use BookStack\Entities\Models\Entity;
10 use BookStack\Entities\Models\Page;
11 use Illuminate\Database\Eloquent\Builder;
12 use Illuminate\Database\Eloquent\Collection as EloquentCollection;
13 use Illuminate\Support\Facades\DB;
16 * Collapsed permissions act as a "flattened" view of entity-level permissions in the system
17 * so inheritance does not have to managed as part of permission querying.
19 class CollapsedPermissionBuilder
22 * Re-generate all collapsed permissions from scratch.
24 public function rebuildForAll()
26 DB::table('entity_permissions_collapsed')->truncate();
28 // Chunk through all books
29 $this->bookFetchQuery()->chunk(5, function (EloquentCollection $books) {
30 $this->buildForBooks($books, false);
33 // Chunk through all bookshelves
34 Bookshelf::query()->withTrashed()
36 ->chunk(50, function (EloquentCollection $shelves) {
37 $this->generateCollapsedPermissions($shelves->all());
42 * Rebuild the collapsed permissions for a particular entity.
44 public function rebuildForEntity(Entity $entity)
46 $entities = [$entity];
47 if ($entity instanceof Book) {
48 $books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get();
49 $this->buildForBooks($books, true);
54 /** @var BookChild $entity */
56 $entities[] = $entity->book;
59 if ($entity instanceof Page && $entity->chapter_id) {
60 $entities[] = $entity->chapter;
63 if ($entity instanceof Chapter) {
64 foreach ($entity->pages as $page) {
69 $this->buildForEntities($entities);
73 * Get a query for fetching a book with its children.
75 protected function bookFetchQuery(): Builder
77 return Book::query()->withTrashed()
78 ->select(['id'])->with([
79 'chapters' => function ($query) {
80 $query->withTrashed()->select(['id', 'book_id']);
82 'pages' => function ($query) {
83 $query->withTrashed()->select(['id', 'book_id', 'chapter_id']);
89 * Build collapsed permissions for the given books.
91 protected function buildForBooks(EloquentCollection $books, bool $deleteOld)
93 $entities = clone $books;
95 /** @var Book $book */
96 foreach ($books->all() as $book) {
97 foreach ($book->getRelation('chapters') as $chapter) {
98 $entities->push($chapter);
100 foreach ($book->getRelation('pages') as $page) {
101 $entities->push($page);
106 $this->deleteForEntities($entities->all());
109 $this->generateCollapsedPermissions($entities->all());
113 * Rebuild the collapsed permissions for a collection of entities.
115 protected function buildForEntities(array $entities)
117 $this->deleteForEntities($entities);
118 $this->generateCollapsedPermissions($entities);
122 * Delete the stored collapsed permissions for a list of entities.
124 * @param Entity[] $entities
126 protected function deleteForEntities(array $entities)
128 $simpleEntities = $this->entitiesToSimpleEntities($entities);
129 $idsByType = $this->entitiesToTypeIdMap($simpleEntities);
131 DB::transaction(function () use ($idsByType) {
132 foreach ($idsByType as $type => $ids) {
133 foreach (array_chunk($ids, 1000) as $idChunk) {
134 DB::table('entity_permissions_collapsed')
135 ->where('entity_type', '=', $type)
136 ->whereIn('entity_id', $idChunk)
144 * Convert the given list of entities into "SimpleEntityData" representations
145 * for faster usage and property access.
147 * @param Entity[] $entities
149 * @return SimpleEntityData[]
151 protected function entitiesToSimpleEntities(array $entities): array
153 $simpleEntities = [];
155 foreach ($entities as $entity) {
156 $attrs = $entity->getAttributes();
157 $simple = new SimpleEntityData();
158 $simple->id = $attrs['id'];
159 $simple->type = $entity->getMorphClass();
160 $simple->book_id = $attrs['book_id'] ?? null;
161 $simple->chapter_id = $attrs['chapter_id'] ?? null;
162 $simpleEntities[] = $simple;
165 return $simpleEntities;
169 * Create & Save collapsed entity permissions.
171 * @param Entity[] $originalEntities
173 protected function generateCollapsedPermissions(array $originalEntities)
175 $entities = $this->entitiesToSimpleEntities($originalEntities);
176 $collapsedPermData = [];
178 // Fetch related entity permissions
179 $permissions = $this->getEntityPermissionsForEntities($entities);
181 // Create a mapping of explicit entity permissions
182 $permissionMap = new EntityPermissionMap($permissions);
184 // Create Joint Permission Data
185 foreach ($entities as $entity) {
186 array_push($collapsedPermData, ...$this->createCollapsedPermissionData($entity, $permissionMap));
189 DB::transaction(function () use ($collapsedPermData) {
190 foreach (array_chunk($collapsedPermData, 1000) as $dataChunk) {
191 DB::table('entity_permissions_collapsed')->insert($dataChunk);
197 * Create collapsed permission data for the given entity using the given permission map.
199 protected function createCollapsedPermissionData(SimpleEntityData $entity, EntityPermissionMap $permissionMap): array
202 $entity->type . ':' . $entity->id,
203 $entity->chapter_id ? ('chapter:' . $entity->chapter_id) : null,
204 $entity->book_id ? ('book:' . $entity->book_id) : null,
207 $permissionData = [];
208 $overridesApplied = [];
210 foreach ($chain as $entityTypeId) {
211 if ($entityTypeId === null) {
215 $permissions = $permissionMap->getForEntity($entityTypeId);
216 foreach ($permissions as $permission) {
217 $related = $permission->getAssignedType() . ':' . $permission->getAssignedTypeId();
218 if (!isset($overridesApplied[$related])) {
219 $permissionData[] = [
220 'role_id' => $permission->role_id,
221 'user_id' => $permission->user_id,
222 'view' => $permission->view,
223 'entity_type' => $entity->type,
224 'entity_id' => $entity->id,
226 $overridesApplied[$related] = true;
231 return $permissionData;
235 * From the given entity list, provide back a mapping of entity types to
236 * the ids of that given type. The type used is the DB morph class.
238 * @param SimpleEntityData[] $entities
240 * @return array<string, int[]>
242 protected function entitiesToTypeIdMap(array $entities): array
246 foreach ($entities as $entity) {
247 if (!isset($idsByType[$entity->type])) {
248 $idsByType[$entity->type] = [];
251 $idsByType[$entity->type][] = $entity->id;
258 * Get the entity permissions for all the given entities.
260 * @param SimpleEntityData[] $entities
262 * @return EntityPermission[]
264 protected function getEntityPermissionsForEntities(array $entities): array
266 $idsByType = $this->entitiesToTypeIdMap($entities);
267 $permissionFetch = EntityPermission::query()
268 ->where(function (Builder $query) use ($idsByType) {
269 foreach ($idsByType as $type => $ids) {
270 $query->orWhere(function (Builder $query) use ($type, $ids) {
271 $query->where('entity_type', '=', $type)->whereIn('entity_id', $ids);
276 return $permissionFetch->get()->all();