3 namespace BookStack\Auth\Permissions;
5 use BookStack\Auth\Role;
6 use BookStack\Entities\Models\Book;
7 use BookStack\Entities\Models\BookChild;
8 use BookStack\Entities\Models\Bookshelf;
9 use BookStack\Entities\Models\Chapter;
10 use BookStack\Entities\Models\Entity;
11 use BookStack\Entities\Models\Page;
12 use Illuminate\Database\Eloquent\Builder;
13 use Illuminate\Database\Eloquent\Collection as EloquentCollection;
14 use Illuminate\Support\Facades\DB;
17 * Joint permissions provide a pre-query "cached" table of view permissions for all core entity
18 * types for all roles in the system. This class generates out that table for different scenarios.
20 class JointPermissionBuilder
23 * Re-generate all entity permission from scratch.
25 public function rebuildForAll()
27 DB::table('entity_permissions_collapsed')->truncate();
29 // Chunk through all books
30 $this->bookFetchQuery()->chunk(5, function (EloquentCollection $books) {
31 $this->buildJointPermissionsForBooks($books);
34 // Chunk through all bookshelves
35 Bookshelf::query()->withTrashed()
36 ->select(['id', 'owned_by'])
37 ->chunk(50, function (EloquentCollection $shelves) {
38 $this->generateCollapsedPermissions($shelves->all());
43 * Rebuild the entity jointPermissions for a particular entity.
45 public function rebuildForEntity(Entity $entity)
47 $entities = [$entity];
48 if ($entity instanceof Book) {
49 $books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get();
50 $this->buildJointPermissionsForBooks($books, true);
55 /** @var BookChild $entity */
57 $entities[] = $entity->book;
60 if ($entity instanceof Page && $entity->chapter_id) {
61 $entities[] = $entity->chapter;
64 if ($entity instanceof Chapter) {
65 foreach ($entity->pages as $page) {
70 $this->buildJointPermissionsForEntities($entities);
74 * Get a query for fetching a book with its children.
76 protected function bookFetchQuery(): Builder
78 return Book::query()->withTrashed()
79 ->select(['id', 'owned_by'])->with([
80 'chapters' => function ($query) {
81 $query->withTrashed()->select(['id', 'owned_by', 'book_id']);
83 'pages' => function ($query) {
84 $query->withTrashed()->select(['id', 'owned_by', 'book_id', 'chapter_id']);
90 * Build joint permissions for the given book and role combinations.
92 protected function buildJointPermissionsForBooks(EloquentCollection $books, bool $deleteOld = false)
94 $entities = clone $books;
96 /** @var Book $book */
97 foreach ($books->all() as $book) {
98 foreach ($book->getRelation('chapters') as $chapter) {
99 $entities->push($chapter);
101 foreach ($book->getRelation('pages') as $page) {
102 $entities->push($page);
107 $this->deleteManyJointPermissionsForEntities($entities->all());
110 $this->generateCollapsedPermissions($entities->all());
114 * Rebuild the entity jointPermissions for a collection of entities.
116 protected function buildJointPermissionsForEntities(array $entities)
118 $this->deleteManyJointPermissionsForEntities($entities);
119 $this->generateCollapsedPermissions($entities);
123 * Delete all the entity jointPermissions for a list of entities.
125 * @param Entity[] $entities
127 protected function deleteManyJointPermissionsForEntities(array $entities)
129 $simpleEntities = $this->entitiesToSimpleEntities($entities);
130 $idsByType = $this->entitiesToTypeIdMap($simpleEntities);
132 DB::transaction(function () use ($idsByType) {
133 foreach ($idsByType as $type => $ids) {
134 foreach (array_chunk($ids, 1000) as $idChunk) {
135 DB::table('entity_permissions_collapsed')
136 ->where('entity_type', '=', $type)
137 ->whereIn('entity_id', $idChunk)
145 * @param Entity[] $entities
147 * @return SimpleEntityData[]
149 protected function entitiesToSimpleEntities(array $entities): array
151 $simpleEntities = [];
153 foreach ($entities as $entity) {
154 $attrs = $entity->getAttributes();
155 $simple = new SimpleEntityData();
156 $simple->id = $attrs['id'];
157 $simple->type = $entity->getMorphClass();
158 $simple->owned_by = $attrs['owned_by'] ?? 0;
159 $simple->book_id = $attrs['book_id'] ?? null;
160 $simple->chapter_id = $attrs['chapter_id'] ?? null;
161 $simpleEntities[] = $simple;
164 return $simpleEntities;
168 * Create & Save collapsed entity permissions.
170 * @param Entity[] $originalEntities
172 protected function generateCollapsedPermissions(array $originalEntities)
174 $entities = $this->entitiesToSimpleEntities($originalEntities);
175 $jointPermissions = [];
177 // Fetch related entity permissions
178 $permissions = $this->getEntityPermissionsForEntities($entities);
180 // Create a mapping of explicit entity permissions
181 $permissionMap = new EntityPermissionMap($permissions);
183 // Create Joint Permission Data
184 foreach ($entities as $entity) {
185 array_push($jointPermissions, ...$this->createCollapsedPermissionData($entity, $permissionMap));
188 DB::transaction(function () use ($jointPermissions) {
189 foreach (array_chunk($jointPermissions, 1000) as $jointPermissionChunk) {
190 DB::table('entity_permissions_collapsed')->insert($jointPermissionChunk);
196 * Create collapsed permission data for the given entity using the given permission map.
198 protected function createCollapsedPermissionData(SimpleEntityData $entity, EntityPermissionMap $permissionMap): array
201 $entity->type . ':' . $entity->id,
202 $entity->chapter_id ? null : ('chapter:' . $entity->chapter_id),
203 $entity->book_id ? null : ('book:' . $entity->book_id),
206 $permissionData = [];
207 $overridesApplied = [];
209 foreach ($chain as $entityTypeId) {
210 if ($entityTypeId === null) {
214 $permissions = $permissionMap->getForEntity($entityTypeId);
215 foreach ($permissions as $permission) {
216 $related = $permission->getAssignedType() . ':' . $permission->getAssignedTypeId();
217 if (!isset($overridesApplied[$related])) {
218 $permissionData[] = [
219 'role_id' => $permission->role_id,
220 'user_id' => $permission->user_id,
221 'view' => $permission->view,
222 'entity_type' => $entity->type,
223 'entity_id' => $entity->id,
225 $overridesApplied[$related] = true;
230 return $permissionData;
234 * From the given entity list, provide back a mapping of entity types to
235 * the ids of that given type. The type used is the DB morph class.
237 * @param SimpleEntityData[] $entities
239 * @return array<string, int[]>
241 protected function entitiesToTypeIdMap(array $entities): array
245 foreach ($entities as $entity) {
246 if (!isset($idsByType[$entity->type])) {
247 $idsByType[$entity->type] = [];
250 $idsByType[$entity->type][] = $entity->id;
257 * Get the entity permissions for all the given entities.
259 * @param SimpleEntityData[] $entities
261 * @return EntityPermission[]
263 protected function getEntityPermissionsForEntities(array $entities): array
265 $idsByType = $this->entitiesToTypeIdMap($entities);
266 $permissionFetch = EntityPermission::query()
267 ->where(function (Builder $query) use ($idsByType) {
268 foreach ($idsByType as $type => $ids) {
269 $query->orWhere(function (Builder $query) use ($type, $ids) {
270 $query->where('entity_type', '=', $type)->whereIn('entity_id', $ids);
275 return $permissionFetch->get()->all();