diff --git a/app/Auth/Permissions/JointPermissionBuilder.php b/app/Auth/Permissions/JointPermissionBuilder.php new file mode 100644 index 000000000..c87560fe0 --- /dev/null +++ b/app/Auth/Permissions/JointPermissionBuilder.php @@ -0,0 +1,423 @@ +> + */ + protected $entityCache; + + /** + * Prepare the local entity cache and ensure it's empty. + * + * @param Entity[] $entities + */ + protected function readyEntityCache(array $entities = []) + { + $this->entityCache = []; + + foreach ($entities as $entity) { + $class = get_class($entity); + + if (!isset($this->entityCache[$class])) { + $this->entityCache[$class] = []; + } + + $this->entityCache[$class][$entity->getRawAttribute('id')] = $entity; + } + } + + /** + * Get a book via ID, Checks local cache. + */ + protected function getBook(int $bookId): ?Book + { + if ($this->entityCache[Book::class][$bookId] ?? false) { + return $this->entityCache[Book::class][$bookId]; + } + + return Book::query()->withTrashed()->find($bookId); + } + + /** + * Get a chapter via ID, Checks local cache. + */ + protected function getChapter(int $chapterId): ?Chapter + { + if ($this->entityCache[Chapter::class][$chapterId] ?? false) { + return $this->entityCache[Chapter::class][$chapterId]; + } + + return Chapter::query() + ->withTrashed() + ->find($chapterId); + } + + /** + * Re-generate all entity permission from scratch. + */ + public function buildJointPermissions() + { + JointPermission::query()->truncate(); + $this->readyEntityCache(); + + // Get all roles (Should be the most limited dimension) + $roles = Role::query()->with('permissions')->get()->all(); + + // Chunk through all books + $this->bookFetchQuery()->chunk(5, function (EloquentCollection $books) use ($roles) { + $this->buildJointPermissionsForBooks($books, $roles); + }); + + // Chunk through all bookshelves + Bookshelf::query()->withTrashed()->select(['id', 'restricted', 'owned_by']) + ->chunk(50, function (EloquentCollection $shelves) use ($roles) { + $this->buildJointPermissionsForShelves($shelves, $roles); + }); + } + + /** + * Get a query for fetching a book with it's children. + */ + protected function bookFetchQuery(): Builder + { + return Book::query()->withTrashed() + ->select(['id', 'restricted', 'owned_by'])->with([ + 'chapters' => function ($query) { + $query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id']); + }, + 'pages' => function ($query) { + $query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id', 'chapter_id']); + }, + ]); + } + + /** + * Build joint permissions for the given shelf and role combinations. + * + * @throws Throwable + */ + protected function buildJointPermissionsForShelves(EloquentCollection $shelves, array $roles, bool $deleteOld = false) + { + if ($deleteOld) { + $this->deleteManyJointPermissionsForEntities($shelves->all()); + } + $this->createManyJointPermissions($shelves->all(), $roles); + } + + /** + * Build joint permissions for the given book and role combinations. + * + * @throws Throwable + */ + protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false) + { + $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->deleteManyJointPermissionsForEntities($entities->all()); + } + + $this->createManyJointPermissions($entities->all(), $roles); + } + + /** + * Rebuild the entity jointPermissions for a particular entity. + * + * @throws Throwable + */ + public function buildJointPermissionsForEntity(Entity $entity) + { + $entities = [$entity]; + if ($entity instanceof Book) { + $books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get(); + $this->buildJointPermissionsForBooks($books, Role::query()->with('permissions')->get()->all(), 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->buildJointPermissionsForEntities($entities); + } + + /** + * Rebuild the entity jointPermissions for a collection of entities. + * + * @throws Throwable + */ + protected function buildJointPermissionsForEntities(array $entities) + { + $roles = Role::query()->get()->values()->all(); + $this->deleteManyJointPermissionsForEntities($entities); + $this->createManyJointPermissions($entities, $roles); + } + + /** + * Build the entity jointPermissions for a particular role. + */ + public function buildJointPermissionForRole(Role $role) + { + $roles = [$role]; + $role->jointPermissions()->delete(); + + // Chunk through all books + $this->bookFetchQuery()->chunk(20, function ($books) use ($roles) { + $this->buildJointPermissionsForBooks($books, $roles); + }); + + // Chunk through all bookshelves + Bookshelf::query()->select(['id', 'restricted', 'owned_by']) + ->chunk(50, function ($shelves) use ($roles) { + $this->buildJointPermissionsForShelves($shelves, $roles); + }); + } + + /** + * Delete all the entity jointPermissions for a list of entities. + * + * @param Entity[] $entities + * + * @throws Throwable + */ + protected function deleteManyJointPermissionsForEntities(array $entities) + { + $idsByType = $this->entitiesToTypeIdMap($entities); + + DB::transaction(function () use ($idsByType) { + foreach ($idsByType as $type => $ids) { + foreach (array_chunk($ids, 1000) as $idChunk) { + DB::table('joint_permissions') + ->where('entity_type', '=', $type) + ->whereIn('entity_id', $idChunk) + ->delete(); + } + } + }); + } + + /** + * Create & Save entity jointPermissions for many entities and roles. + * + * @param Entity[] $entities + * @param Role[] $roles + * + * @throws Throwable + */ + protected function createManyJointPermissions(array $entities, array $roles) + { + $this->readyEntityCache($entities); + $jointPermissions = []; + + // Create a mapping of entity restricted statuses + $entityRestrictedMap = []; + foreach ($entities as $entity) { + $entityRestrictedMap[$entity->getMorphClass() . ':' . $entity->getRawAttribute('id')] = boolval($entity->getRawAttribute('restricted')); + } + + // Fetch related entity permissions + $permissions = $this->getEntityPermissionsForEntities($entities); + + // Create a mapping of explicit entity permissions + $permissionMap = []; + foreach ($permissions as $permission) { + $key = $permission->restrictable_type . ':' . $permission->restrictable_id . ':' . $permission->role_id . ':' . $permission->action; + $isRestricted = $entityRestrictedMap[$permission->restrictable_type . ':' . $permission->restrictable_id]; + $permissionMap[$key] = $isRestricted; + } + + // Create a mapping of role permissions + $rolePermissionMap = []; + foreach ($roles as $role) { + foreach ($role->permissions as $permission) { + $rolePermissionMap[$role->getRawAttribute('id') . ':' . $permission->getRawAttribute('name')] = true; + } + } + + // Create Joint Permission Data + foreach ($entities as $entity) { + foreach ($roles as $role) { + foreach ($this->getActions($entity) as $action) { + $jointPermissions[] = $this->createJointPermissionData($entity, $role, $action, $permissionMap, $rolePermissionMap); + } + } + } + + DB::transaction(function () use ($jointPermissions) { + foreach (array_chunk($jointPermissions, 1000) as $jointPermissionChunk) { + DB::table('joint_permissions')->insert($jointPermissionChunk); + } + }); + } + + /** + * 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 Entity[] $entities + * @return array + */ + protected function entitiesToTypeIdMap(array $entities): array + { + $idsByType = []; + + foreach ($entities as $entity) { + $type = $entity->getMorphClass(); + + if (!isset($idsByType[$type])) { + $idsByType[$type] = []; + } + + $idsByType[$type][] = $entity->getRawAttribute('id'); + } + + return $idsByType; + } + + /** + * Get the entity permissions for all the given entities + * @param Entity[] $entities + * @return EloquentCollection + */ + protected function getEntityPermissionsForEntities(array $entities) + { + $idsByType = $this->entitiesToTypeIdMap($entities); + $permissionFetch = EntityPermission::query(); + + foreach ($idsByType as $type => $ids) { + $permissionFetch->orWhere(function (Builder $query) use ($type, $ids) { + $query->where('restrictable_type', '=', $type)->whereIn('restrictable_id', $ids); + }); + } + + return $permissionFetch->get(); + } + + /** + * Get the actions related to an entity. + */ + protected function getActions(Entity $entity): array + { + $baseActions = ['view', 'update', 'delete']; + if ($entity instanceof Chapter || $entity instanceof Book) { + $baseActions[] = 'page-create'; + } + if ($entity instanceof Book) { + $baseActions[] = 'chapter-create'; + } + + return $baseActions; + } + + /** + * Create entity permission data for an entity and role + * for a particular action. + */ + protected function createJointPermissionData(Entity $entity, Role $role, string $action, array $permissionMap, array $rolePermissionMap): array + { + $permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action; + $roleHasPermission = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-all']); + $roleHasPermissionOwn = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-own']); + $explodedAction = explode('-', $action); + $restrictionAction = end($explodedAction); + + if ($role->system_name === 'admin') { + return $this->createJointPermissionDataArray($entity, $role, $action, true, true); + } + + if ($entity->restricted) { + $hasAccess = $this->mapHasActiveRestriction($permissionMap, $entity, $role, $restrictionAction); + + return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess); + } + + if ($entity instanceof Book || $entity instanceof Bookshelf) { + return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn); + } + + // For chapters and pages, Check if explicit permissions are set on the Book. + $book = $this->getBook($entity->book_id); + $hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $book, $role, $restrictionAction); + $hasPermissiveAccessToParents = !$book->restricted; + + // For pages with a chapter, Check if explicit permissions are set on the Chapter + if ($entity instanceof Page && intval($entity->chapter_id) !== 0) { + $chapter = $this->getChapter($entity->chapter_id); + $hasPermissiveAccessToParents = $hasPermissiveAccessToParents && !$chapter->restricted; + if ($chapter->restricted) { + $hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $chapter, $role, $restrictionAction); + } + } + + return $this->createJointPermissionDataArray( + $entity, + $role, + $action, + ($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)), + ($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents)) + ); + } + + /** + * Check for an active restriction in an entity map. + */ + protected function mapHasActiveRestriction(array $entityMap, Entity $entity, Role $role, string $action): bool + { + $key = $entity->getMorphClass() . ':' . $entity->getRawAttribute('id') . ':' . $role->getRawAttribute('id') . ':' . $action; + + return $entityMap[$key] ?? false; + } + + /** + * Create an array of data with the information of an entity jointPermissions. + * Used to build data for bulk insertion. + */ + protected function createJointPermissionDataArray(Entity $entity, Role $role, string $action, bool $permissionAll, bool $permissionOwn): array + { + return [ + 'action' => $action, + 'entity_id' => $entity->getRawAttribute('id'), + 'entity_type' => $entity->getMorphClass(), + 'has_permission' => $permissionAll, + 'has_permission_own' => $permissionOwn, + 'owned_by' => $entity->getRawAttribute('owned_by'), + 'role_id' => $role->getRawAttribute('id'), + ]; + } + +} \ No newline at end of file diff --git a/app/Auth/Permissions/PermissionService.php b/app/Auth/Permissions/PermissionService.php index 1363fe86e..bcd4a3675 100644 --- a/app/Auth/Permissions/PermissionService.php +++ b/app/Auth/Permissions/PermissionService.php @@ -4,20 +4,13 @@ namespace BookStack\Auth\Permissions; use BookStack\Auth\Role; use BookStack\Auth\User; -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 BookStack\Model; use BookStack\Traits\HasCreatorAndUpdater; use BookStack\Traits\HasOwner; -use Illuminate\Database\Connection; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Query\Builder as QueryBuilder; -use Throwable; class PermissionService { @@ -31,77 +24,6 @@ class PermissionService */ protected $currentUserModel = null; - /** - * @var Connection - */ - protected $db; - - /** - * @var array> - */ - protected $entityCache; - - /** - * PermissionService constructor. - */ - public function __construct(Connection $db) - { - $this->db = $db; - } - - /** - * Set the database connection. - */ - public function setConnection(Connection $connection) - { - $this->db = $connection; - } - - /** - * Prepare the local entity cache and ensure it's empty. - * - * @param Entity[] $entities - */ - protected function readyEntityCache(array $entities = []) - { - $this->entityCache = []; - - foreach ($entities as $entity) { - $class = get_class($entity); - - if (!isset($this->entityCache[$class])) { - $this->entityCache[$class] = []; - } - - $this->entityCache[$class][$entity->getRawAttribute('id')] = $entity; - } - } - - /** - * Get a book via ID, Checks local cache. - */ - protected function getBook(int $bookId): ?Book - { - if ($this->entityCache[Book::class][$bookId] ?? false) { - return $this->entityCache[Book::class][$bookId]; - } - - return Book::query()->withTrashed()->find($bookId); - } - - /** - * Get a chapter via ID, Checks local cache. - */ - protected function getChapter(int $chapterId): ?Chapter - { - if ($this->entityCache[Chapter::class][$chapterId] ?? false) { - return $this->entityCache[Chapter::class][$chapterId]; - } - - return Chapter::query() - ->withTrashed() - ->find($chapterId); - } /** * Get the roles for the current logged in user. @@ -121,380 +43,6 @@ class PermissionService return $this->userRoles; } - /** - * Re-generate all entity permission from scratch. - */ - public function buildJointPermissions() - { - JointPermission::query()->truncate(); - $this->readyEntityCache(); - - // Get all roles (Should be the most limited dimension) - $roles = Role::query()->with('permissions')->get()->all(); - - // Chunk through all books - $this->bookFetchQuery()->chunk(5, function (EloquentCollection $books) use ($roles) { - $this->buildJointPermissionsForBooks($books, $roles); - }); - - // Chunk through all bookshelves - Bookshelf::query()->withTrashed()->select(['id', 'restricted', 'owned_by']) - ->chunk(50, function (EloquentCollection $shelves) use ($roles) { - $this->buildJointPermissionsForShelves($shelves, $roles); - }); - } - - /** - * Get a query for fetching a book with it's children. - */ - protected function bookFetchQuery(): Builder - { - return Book::query()->withTrashed() - ->select(['id', 'restricted', 'owned_by'])->with([ - 'chapters' => function ($query) { - $query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id']); - }, - 'pages' => function ($query) { - $query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id', 'chapter_id']); - }, - ]); - } - - /** - * Build joint permissions for the given shelf and role combinations. - * - * @throws Throwable - */ - protected function buildJointPermissionsForShelves(EloquentCollection $shelves, array $roles, bool $deleteOld = false) - { - if ($deleteOld) { - $this->deleteManyJointPermissionsForEntities($shelves->all()); - } - $this->createManyJointPermissions($shelves->all(), $roles); - } - - /** - * Build joint permissions for the given book and role combinations. - * - * @throws Throwable - */ - protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false) - { - $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->deleteManyJointPermissionsForEntities($entities->all()); - } - $this->createManyJointPermissions($entities->all(), $roles); - } - - /** - * Rebuild the entity jointPermissions for a particular entity. - * - * @throws Throwable - */ - public function buildJointPermissionsForEntity(Entity $entity) - { - $entities = [$entity]; - if ($entity instanceof Book) { - $books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get(); - $this->buildJointPermissionsForBooks($books, Role::query()->with('permissions')->get()->all(), 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->buildJointPermissionsForEntities($entities); - } - - /** - * Rebuild the entity jointPermissions for a collection of entities. - * - * @throws Throwable - */ - public function buildJointPermissionsForEntities(array $entities) - { - $roles = Role::query()->get()->values()->all(); - $this->deleteManyJointPermissionsForEntities($entities); - $this->createManyJointPermissions($entities, $roles); - } - - /** - * Build the entity jointPermissions for a particular role. - */ - public function buildJointPermissionForRole(Role $role) - { - $roles = [$role]; - $this->deleteManyJointPermissionsForRoles($roles); - - // Chunk through all books - $this->bookFetchQuery()->chunk(20, function ($books) use ($roles) { - $this->buildJointPermissionsForBooks($books, $roles); - }); - - // Chunk through all bookshelves - Bookshelf::query()->select(['id', 'restricted', 'owned_by']) - ->chunk(50, function ($shelves) use ($roles) { - $this->buildJointPermissionsForShelves($shelves, $roles); - }); - } - - /** - * Delete the entity jointPermissions attached to a particular role. - */ - public function deleteJointPermissionsForRole(Role $role) - { - $this->deleteManyJointPermissionsForRoles([$role]); - } - - /** - * Delete all the entity jointPermissions for a list of entities. - * - * @param Role[] $roles - */ - protected function deleteManyJointPermissionsForRoles($roles) - { - $roleIds = array_map(function ($role) { - return $role->id; - }, $roles); - JointPermission::query()->whereIn('role_id', $roleIds)->delete(); - } - - /** - * Delete all the entity jointPermissions for a list of entities. - * - * @param Entity[] $entities - * - * @throws Throwable - */ - protected function deleteManyJointPermissionsForEntities(array $entities) - { - $idsByType = $this->entitiesToTypeIdMap($entities); - - $this->db->transaction(function () use ($idsByType) { - foreach ($idsByType as $type => $ids) { - foreach (array_chunk($ids, 1000) as $idChunk) { - $this->db->table('joint_permissions') - ->where('entity_type', '=', $type) - ->whereIn('entity_id', $idChunk) - ->delete(); - } - } - }); - } - - /** - * Create & Save entity jointPermissions for many entities and roles. - * - * @param Entity[] $entities - * @param Role[] $roles - * - * @throws Throwable - */ - protected function createManyJointPermissions(array $entities, array $roles) - { - $this->readyEntityCache($entities); - $jointPermissions = []; - - // Create a mapping of entity restricted statuses - $entityRestrictedMap = []; - foreach ($entities as $entity) { - $entityRestrictedMap[$entity->getMorphClass() . ':' . $entity->getRawAttribute('id')] = boolval($entity->getRawAttribute('restricted')); - } - - // Fetch related entity permissions - $permissions = $this->getEntityPermissionsForEntities($entities); - - // Create a mapping of explicit entity permissions - $permissionMap = []; - foreach ($permissions as $permission) { - $key = $permission->restrictable_type . ':' . $permission->restrictable_id . ':' . $permission->role_id . ':' . $permission->action; - $isRestricted = $entityRestrictedMap[$permission->restrictable_type . ':' . $permission->restrictable_id]; - $permissionMap[$key] = $isRestricted; - } - - // Create a mapping of role permissions - $rolePermissionMap = []; - foreach ($roles as $role) { - foreach ($role->permissions as $permission) { - $rolePermissionMap[$role->getRawAttribute('id') . ':' . $permission->getRawAttribute('name')] = true; - } - } - - // Create Joint Permission Data - foreach ($entities as $entity) { - foreach ($roles as $role) { - foreach ($this->getActions($entity) as $action) { - $jointPermissions[] = $this->createJointPermissionData($entity, $role, $action, $permissionMap, $rolePermissionMap); - } - } - } - - $this->db->transaction(function () use ($jointPermissions) { - foreach (array_chunk($jointPermissions, 1000) as $jointPermissionChunk) { - $this->db->table('joint_permissions')->insert($jointPermissionChunk); - } - }); - } - - /** - * 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 Entity[] $entities - * @return array - */ - protected function entitiesToTypeIdMap(array $entities): array - { - $idsByType = []; - - foreach ($entities as $entity) { - $type = $entity->getMorphClass(); - - if (!isset($idsByType[$type])) { - $idsByType[$type] = []; - } - - $idsByType[$type][] = $entity->getRawAttribute('id'); - } - - return $idsByType; - } - - /** - * Get the entity permissions for all the given entities - * @param Entity[] $entities - * @return EloquentCollection - */ - protected function getEntityPermissionsForEntities(array $entities) - { - $idsByType = $this->entitiesToTypeIdMap($entities); - $permissionFetch = EntityPermission::query(); - - foreach ($idsByType as $type => $ids) { - $permissionFetch->orWhere(function (Builder $query) use ($type, $ids) { - $query->where('restrictable_type', '=', $type)->whereIn('restrictable_id', $ids); - }); - } - - return $permissionFetch->get(); - } - - /** - * Get the actions related to an entity. - */ - protected function getActions(Entity $entity): array - { - $baseActions = ['view', 'update', 'delete']; - if ($entity instanceof Chapter || $entity instanceof Book) { - $baseActions[] = 'page-create'; - } - if ($entity instanceof Book) { - $baseActions[] = 'chapter-create'; - } - - return $baseActions; - } - - /** - * Create entity permission data for an entity and role - * for a particular action. - */ - protected function createJointPermissionData(Entity $entity, Role $role, string $action, array $permissionMap, array $rolePermissionMap): array - { - $permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action; - $roleHasPermission = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-all']); - $roleHasPermissionOwn = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-own']); - $explodedAction = explode('-', $action); - $restrictionAction = end($explodedAction); - - if ($role->system_name === 'admin') { - return $this->createJointPermissionDataArray($entity, $role, $action, true, true); - } - - if ($entity->restricted) { - $hasAccess = $this->mapHasActiveRestriction($permissionMap, $entity, $role, $restrictionAction); - - return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess); - } - - if ($entity instanceof Book || $entity instanceof Bookshelf) { - return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn); - } - - // For chapters and pages, Check if explicit permissions are set on the Book. - $book = $this->getBook($entity->book_id); - $hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $book, $role, $restrictionAction); - $hasPermissiveAccessToParents = !$book->restricted; - - // For pages with a chapter, Check if explicit permissions are set on the Chapter - if ($entity instanceof Page && intval($entity->chapter_id) !== 0) { - $chapter = $this->getChapter($entity->chapter_id); - $hasPermissiveAccessToParents = $hasPermissiveAccessToParents && !$chapter->restricted; - if ($chapter->restricted) { - $hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $chapter, $role, $restrictionAction); - } - } - - return $this->createJointPermissionDataArray( - $entity, - $role, - $action, - ($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)), - ($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents)) - ); - } - - /** - * Check for an active restriction in an entity map. - */ - protected function mapHasActiveRestriction(array $entityMap, Entity $entity, Role $role, string $action): bool - { - $key = $entity->getMorphClass() . ':' . $entity->getRawAttribute('id') . ':' . $role->getRawAttribute('id') . ':' . $action; - - return $entityMap[$key] ?? false; - } - - /** - * Create an array of data with the information of an entity jointPermissions. - * Used to build data for bulk insertion. - */ - protected function createJointPermissionDataArray(Entity $entity, Role $role, string $action, bool $permissionAll, bool $permissionOwn): array - { - return [ - 'role_id' => $role->getRawAttribute('id'), - 'entity_id' => $entity->getRawAttribute('id'), - 'entity_type' => $entity->getMorphClass(), - 'action' => $action, - 'has_permission' => $permissionAll, - 'has_permission_own' => $permissionOwn, - 'owned_by' => $entity->getRawAttribute('owned_by'), - ]; - } - /** * Checks if an entity has a restriction set upon it. * diff --git a/app/Auth/Permissions/PermissionsRepo.php b/app/Auth/Permissions/PermissionsRepo.php index 988146700..0527875ae 100644 --- a/app/Auth/Permissions/PermissionsRepo.php +++ b/app/Auth/Permissions/PermissionsRepo.php @@ -11,20 +11,15 @@ use Illuminate\Database\Eloquent\Collection; class PermissionsRepo { - protected $permission; - protected $role; - protected $permissionService; - + protected JointPermissionBuilder $permissionBuilder; protected $systemRoles = ['admin', 'public']; /** * PermissionsRepo constructor. */ - public function __construct(RolePermission $permission, Role $role, PermissionService $permissionService) + public function __construct(JointPermissionBuilder $permissionBuilder) { - $this->permission = $permission; - $this->role = $role; - $this->permissionService = $permissionService; + $this->permissionBuilder = $permissionBuilder; } /** @@ -32,7 +27,7 @@ class PermissionsRepo */ public function getAllRoles(): Collection { - return $this->role->all(); + return Role::query()->all(); } /** @@ -40,7 +35,7 @@ class PermissionsRepo */ public function getAllRolesExcept(Role $role): Collection { - return $this->role->where('id', '!=', $role->id)->get(); + return Role::query()->where('id', '!=', $role->id)->get(); } /** @@ -48,7 +43,7 @@ class PermissionsRepo */ public function getRoleById($id): Role { - return $this->role->newQuery()->findOrFail($id); + return Role::query()->findOrFail($id); } /** @@ -56,13 +51,14 @@ class PermissionsRepo */ public function saveNewRole(array $roleData): Role { - $role = $this->role->newInstance($roleData); + $role = new Role($roleData); $role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true'; $role->save(); $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : []; $this->assignRolePermissions($role, $permissions); - $this->permissionService->buildJointPermissionForRole($role); + $this->permissionBuilder->buildJointPermissionForRole($role); + Activity::add(ActivityType::ROLE_CREATE, $role); return $role; @@ -74,8 +70,7 @@ class PermissionsRepo */ public function updateRole($roleId, array $roleData) { - /** @var Role $role */ - $role = $this->role->newQuery()->findOrFail($roleId); + $role = $this->getRoleById($roleId); $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : []; if ($role->system_name === 'admin') { @@ -93,12 +88,13 @@ class PermissionsRepo $role->fill($roleData); $role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true'; $role->save(); - $this->permissionService->buildJointPermissionForRole($role); + $this->permissionBuilder->buildJointPermissionForRole($role); + Activity::add(ActivityType::ROLE_UPDATE, $role); } /** - * Assign an list of permission names to an role. + * Assign a list of permission names to a role. */ protected function assignRolePermissions(Role $role, array $permissionNameArray = []) { @@ -106,7 +102,7 @@ class PermissionsRepo $permissionNameArray = array_values($permissionNameArray); if ($permissionNameArray) { - $permissions = $this->permission->newQuery() + $permissions = EntityPermission::query() ->whereIn('name', $permissionNameArray) ->pluck('id') ->toArray(); @@ -126,8 +122,7 @@ class PermissionsRepo */ public function deleteRole($roleId, $migrateRoleId) { - /** @var Role $role */ - $role = $this->role->newQuery()->findOrFail($roleId); + $role = $this->getRoleById($roleId); // Prevent deleting admin role or default registration role. if ($role->system_name && in_array($role->system_name, $this->systemRoles)) { @@ -137,14 +132,14 @@ class PermissionsRepo } if ($migrateRoleId) { - $newRole = $this->role->newQuery()->find($migrateRoleId); + $newRole = Role::query()->find($migrateRoleId); if ($newRole) { $users = $role->users()->pluck('id')->toArray(); $newRole->users()->sync($users); } } - $this->permissionService->deleteJointPermissionsForRole($role); + $role->jointPermissions()->delete(); Activity::add(ActivityType::ROLE_DELETE, $role); $role->delete(); } diff --git a/app/Console/Commands/RegeneratePermissions.php b/app/Console/Commands/RegeneratePermissions.php index 4fde08e6b..558ae9fea 100644 --- a/app/Console/Commands/RegeneratePermissions.php +++ b/app/Console/Commands/RegeneratePermissions.php @@ -2,8 +2,9 @@ namespace BookStack\Console\Commands; -use BookStack\Auth\Permissions\PermissionService; +use BookStack\Auth\Permissions\JointPermissionBuilder; use Illuminate\Console\Command; +use Illuminate\Support\Facades\DB; class RegeneratePermissions extends Command { @@ -21,19 +22,14 @@ class RegeneratePermissions extends Command */ protected $description = 'Regenerate all system permissions'; - /** - * The service to handle the permission system. - * - * @var PermissionService - */ - protected $permissionService; + protected JointPermissionBuilder $permissionBuilder; /** * Create a new command instance. */ - public function __construct(PermissionService $permissionService) + public function __construct(JointPermissionBuilder $permissionBuilder) { - $this->permissionService = $permissionService; + $this->permissionBuilder = $permissionBuilder; parent::__construct(); } @@ -44,15 +40,15 @@ class RegeneratePermissions extends Command */ public function handle() { - $connection = \DB::getDefaultConnection(); - if ($this->option('database') !== null) { - \DB::setDefaultConnection($this->option('database')); - $this->permissionService->setConnection(\DB::connection($this->option('database'))); + $connection = DB::getDefaultConnection(); + + if ($this->hasOption('database')) { + DB::setDefaultConnection($this->option('database')); } - $this->permissionService->buildJointPermissions(); + $this->permissionBuilder->buildJointPermissions(); - \DB::setDefaultConnection($connection); + DB::setDefaultConnection($connection); $this->comment('Permissions regenerated'); } } diff --git a/database/seeders/DummyContentSeeder.php b/database/seeders/DummyContentSeeder.php index d54732b26..cb9987df7 100644 --- a/database/seeders/DummyContentSeeder.php +++ b/database/seeders/DummyContentSeeder.php @@ -3,7 +3,7 @@ namespace Database\Seeders; use BookStack\Api\ApiToken; -use BookStack\Auth\Permissions\PermissionService; +use BookStack\Auth\Permissions\JointPermissionBuilder; use BookStack\Auth\Permissions\RolePermission; use BookStack\Auth\Role; use BookStack\Auth\User; @@ -69,7 +69,7 @@ class DummyContentSeeder extends Seeder ]); $token->save(); - app(PermissionService::class)->buildJointPermissions(); + app(JointPermissionBuilder::class)->buildJointPermissions(); app(SearchIndex::class)->indexAllEntities(); } } diff --git a/database/seeders/LargeContentSeeder.php b/database/seeders/LargeContentSeeder.php index dd9165978..041b3161a 100644 --- a/database/seeders/LargeContentSeeder.php +++ b/database/seeders/LargeContentSeeder.php @@ -2,7 +2,7 @@ namespace Database\Seeders; -use BookStack\Auth\Permissions\PermissionService; +use BookStack\Auth\Permissions\JointPermissionBuilder; use BookStack\Auth\Role; use BookStack\Auth\User; use BookStack\Entities\Models\Book; @@ -35,7 +35,7 @@ class LargeContentSeeder extends Seeder $largeBook->chapters()->saveMany($chapters); $all = array_merge([$largeBook], array_values($pages->all()), array_values($chapters->all())); - app()->make(PermissionService::class)->buildJointPermissionsForEntity($largeBook); + app()->make(JointPermissionBuilder::class)->buildJointPermissionsForEntity($largeBook); app()->make(SearchIndex::class)->indexEntities($all); } } diff --git a/tests/PublicActionTest.php b/tests/PublicActionTest.php index 499c0c9f9..745fa7660 100644 --- a/tests/PublicActionTest.php +++ b/tests/PublicActionTest.php @@ -2,7 +2,7 @@ namespace Tests; -use BookStack\Auth\Permissions\PermissionService; +use BookStack\Auth\Permissions\JointPermissionBuilder; use BookStack\Auth\Permissions\RolePermission; use BookStack\Auth\Role; use BookStack\Auth\User; @@ -89,7 +89,7 @@ class PublicActionTest extends TestCase foreach (RolePermission::all() as $perm) { $publicRole->attachPermission($perm); } - $this->app[PermissionService::class]->buildJointPermissionForRole($publicRole); + $this->app->make(JointPermissionBuilder::class)->buildJointPermissionForRole($publicRole); /** @var Chapter $chapter */ $chapter = Chapter::query()->first(); diff --git a/tests/SharedTestHelpers.php b/tests/SharedTestHelpers.php index ce57d56f5..2d05b0520 100644 --- a/tests/SharedTestHelpers.php +++ b/tests/SharedTestHelpers.php @@ -2,6 +2,7 @@ namespace Tests; +use BookStack\Auth\Permissions\JointPermissionBuilder; use BookStack\Auth\Permissions\PermissionService; use BookStack\Auth\Permissions\PermissionsRepo; use BookStack\Auth\Permissions\RolePermission; @@ -176,7 +177,7 @@ trait SharedTestHelpers $entity->save(); $entity->load('permissions'); - $this->app[PermissionService::class]->buildJointPermissionsForEntity($entity); + $this->app->make(JointPermissionBuilder::class)->buildJointPermissionsForEntity($entity); $entity->load('jointPermissions'); } @@ -196,7 +197,7 @@ trait SharedTestHelpers */ protected function removePermissionFromUser(User $user, string $permissionName) { - $permissionService = app()->make(PermissionService::class); + $permissionBuilder = app()->make(JointPermissionBuilder::class); /** @var RolePermission $permission */ $permission = RolePermission::query()->where('name', '=', $permissionName)->firstOrFail(); @@ -208,7 +209,7 @@ trait SharedTestHelpers /** @var Role $role */ foreach ($roles as $role) { $role->detachPermission($permission); - $permissionService->buildJointPermissionForRole($role); + $permissionBuilder->buildJointPermissionForRole($role); } $user->clearPermissionCache(); @@ -241,8 +242,8 @@ trait SharedTestHelpers $book = Book::factory()->create($userAttrs); $chapter = Chapter::factory()->create(array_merge(['book_id' => $book->id], $userAttrs)); $page = Page::factory()->create(array_merge(['book_id' => $book->id, 'chapter_id' => $chapter->id], $userAttrs)); - $restrictionService = $this->app[PermissionService::class]; - $restrictionService->buildJointPermissionsForEntity($book); + + $this->app->make(JointPermissionBuilder::class)->buildJointPermissionsForEntity($book); return compact('book', 'chapter', 'page'); }