X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/f459a68535fc42ef5079e2514b82bff28504fc50..refs/pull/3918/head:/app/Auth/Permissions/PermissionApplicator.php diff --git a/app/Auth/Permissions/PermissionApplicator.php b/app/Auth/Permissions/PermissionApplicator.php index e73db5157..af372cb74 100644 --- a/app/Auth/Permissions/PermissionApplicator.php +++ b/app/Auth/Permissions/PermissionApplicator.php @@ -25,14 +25,22 @@ class PermissionApplicator { $explodedPermission = explode('-', $permission); $action = $explodedPermission[1] ?? $explodedPermission[0]; + $fullPermission = count($explodedPermission) > 1 ? $permission : $ownable->getMorphClass() . '-' . $permission; + $user = $this->currentUser(); $userRoleIds = $this->getCurrentUserRoleIds(); - $allRolePermission = $user->can($permission . '-all'); - $ownRolePermission = $user->can($permission . '-own'); + $allRolePermission = $user->can($fullPermission . '-all'); + $ownRolePermission = $user->can($fullPermission . '-own'); $nonJointPermissions = ['restrictions', 'image', 'attachment', 'comment']; $ownerField = ($ownable instanceof Entity) ? 'owned_by' : 'created_by'; - $isOwner = $user->id === $ownable->getAttribute($ownerField); + $ownableFieldVal = $ownable->getAttribute($ownerField); + + if (is_null($ownableFieldVal)) { + throw new InvalidArgumentException("{$ownerField} field used but has not been loaded"); + } + + $isOwner = $user->id === $ownableFieldVal; $hasRolePermission = $allRolePermission || ($isOwner && $ownRolePermission); // Handle non entity specific jointPermissions @@ -40,23 +48,26 @@ class PermissionApplicator return $hasRolePermission; } - $entityPermissions = $this->getApplicableEntityPermissions($ownable, $userRoleIds, $action); - if (is_null($entityPermissions)) { - return $hasRolePermission; - } + $hasApplicableEntityPermissions = $this->hasEntityPermission($ownable, $userRoleIds, $action); - return count($entityPermissions) > 0; + return is_null($hasApplicableEntityPermissions) ? $hasRolePermission : $hasApplicableEntityPermissions; } /** - * Get the permissions that are applicable for the given entity item. - * Returns null when no entity permissions apply otherwise entity permissions - * are active, even if the returned array is empty. - * - * @returns EntityPermission[] + * Check if there are permissions that are applicable for the given entity item, action and roles. + * Returns null when no entity permissions are in force. */ - protected function getApplicableEntityPermissions(Entity $entity, array $userRoleIds, string $action): ?array + protected function hasEntityPermission(Entity $entity, array $userRoleIds, string $action): ?bool { + $this->ensureValidEntityAction($action); + + $adminRoleId = Role::getSystemRole('admin')->id; + if (in_array($adminRoleId, $userRoleIds)) { + return true; + } + + // The chain order here is very important due to the fact we walk up the chain + // in the loop below. Earlier items in the chain have higher priority. $chain = [$entity]; if ($entity instanceof Page && $entity->chapter_id) { $chain[] = $entity->chapter; @@ -67,13 +78,26 @@ class PermissionApplicator } foreach ($chain as $currentEntity) { - if ($currentEntity->restricted) { - return $currentEntity->permissions() - ->whereIn('role_id', $userRoleIds) - ->where('action', '=', $action) - ->get() - ->all(); + $allowedByRoleId = $currentEntity->permissions() + ->whereIn('role_id', [0, ...$userRoleIds]) + ->pluck($action, 'role_id'); + + // Continue up the chain if no applicable entity permission overrides. + if ($allowedByRoleId->isEmpty()) { + continue; + } + + // If we have user-role-specific permissions set, allow if any of those + // role permissions allow access. + $hasDefault = $allowedByRoleId->has(0); + if (!$hasDefault || $allowedByRoleId->count() > 1) { + return $allowedByRoleId->search(function (bool $allowed, int $roleId) { + return $roleId !== 0 && $allowed; + }) !== false; } + + // Otherwise, return the default "Other roles" fallback value. + return $allowedByRoleId->get(0); } return null; @@ -85,18 +109,16 @@ class PermissionApplicator */ public function checkUserHasEntityPermissionOnAny(string $action, string $entityClass = ''): bool { - if (strpos($action, '-') !== false) { - throw new InvalidArgumentException("Action should be a simple entity permission action, not a role permission"); - } + $this->ensureValidEntityAction($action); $permissionQuery = EntityPermission::query() - ->where('action', '=', $action) + ->where($action, '=', true) ->whereIn('role_id', $this->getCurrentUserRoleIds()); if (!empty($entityClass)) { /** @var Entity $entityInstance */ $entityInstance = app()->make($entityClass); - $permissionQuery = $permissionQuery->where('restrictable_type', '=', $entityInstance->getMorphClass()); + $permissionQuery = $permissionQuery->where('entity_type', '=', $entityInstance->getMorphClass()); } $hasPermission = $permissionQuery->count() > 0; @@ -105,7 +127,7 @@ class PermissionApplicator } /** - * Limited the given entity query so that the query will only + * Limit the given entity query so that the query will only * return items that the user has view permission for. */ public function restrictEntityQuery(Builder $query): Builder @@ -113,8 +135,6 @@ class PermissionApplicator return $query->where(function (Builder $parentQuery) { $parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) { $permissionQuery->whereIn('role_id', $this->getCurrentUserRoleIds()) - // TODO - Delete line once only views - ->where('action', '=', 'view') ->where(function (Builder $query) { $this->addJointHasPermissionCheck($query, $this->currentUser()->id); }); @@ -126,7 +146,7 @@ class PermissionApplicator * Extend the given page query to ensure draft items are not visible * unless created by the given user. */ - public function enforceDraftVisibilityOnQuery(Builder $query): Builder + public function restrictDraftsOnPageQuery(Builder $query): Builder { return $query->where(function (Builder $query) { $query->where('draft', '=', false) @@ -137,39 +157,6 @@ class PermissionApplicator }); } - /** - * Add restrictions for a generic entity. - */ - public function enforceEntityRestrictions(Entity $entity, Builder $query): Builder - { - if ($entity instanceof Page) { - // Prevent drafts being visible to others. - $this->enforceDraftVisibilityOnQuery($query); - } - - return $this->entityRestrictionQuery($query); - } - - /** - * The general query filter to remove all entities - * that the current user does not have access to. - */ - protected function entityRestrictionQuery(Builder $query): Builder - { - $q = $query->where(function ($parentQuery) { - $parentQuery->whereHas('jointPermissions', function ($permissionQuery) { - $permissionQuery->whereIn('role_id', $this->getCurrentUserRoleIds()) - // TODO - Delete line once only views - ->where('action', '=', 'view') - ->where(function (Builder $query) { - $this->addJointHasPermissionCheck($query, $this->currentUser()->id); - }); - }); - }); - - return $q; - } - /** * Filter items that have entities set as a polymorphic relation. * For simplicity, this will not return results attached to draft pages. @@ -177,7 +164,7 @@ class PermissionApplicator * * @param Builder|QueryBuilder $query */ - public function filterRestrictedEntityRelations($query, string $tableName, string $entityIdColumn, string $entityTypeColumn) + public function restrictEntityRelationQuery($query, string $tableName, string $entityIdColumn, string $entityTypeColumn) { $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn]; $pageMorphClass = (new Page())->getMorphClass(); @@ -187,7 +174,6 @@ class PermissionApplicator $permissionQuery->select(['role_id'])->from('joint_permissions') ->whereColumn('joint_permissions.entity_id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) ->whereColumn('joint_permissions.entity_type', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn']) - ->where('joint_permissions.action', '=', 'view') ->whereIn('joint_permissions.role_id', $this->getCurrentUserRoleIds()) ->where(function (QueryBuilder $query) { $this->addJointHasPermissionCheck($query, $this->currentUser()->id); @@ -207,43 +193,41 @@ class PermissionApplicator } /** - * Add conditions to a query to filter the selection to related entities - * where view permissions are granted. + * Add conditions to a query for a model that's a relation of a page, so only the model results + * on visible pages are returned by the query. + * Is effectively the same as "restrictEntityRelationQuery" but takes into account page drafts + * while not expecting a polymorphic relation, Just a simpler one-page-to-many-relations set-up. */ - public function filterRelatedEntity(string $entityClass, Builder $query, string $tableName, string $entityIdColumn): Builder + public function restrictPageRelationQuery(Builder $query, string $tableName, string $pageIdColumn): Builder { - $fullEntityIdColumn = $tableName . '.' . $entityIdColumn; - $instance = new $entityClass(); - $morphClass = $instance->getMorphClass(); + $fullPageIdColumn = $tableName . '.' . $pageIdColumn; + $morphClass = (new Page())->getMorphClass(); - $existsQuery = function ($permissionQuery) use ($fullEntityIdColumn, $morphClass) { + $existsQuery = function ($permissionQuery) use ($fullPageIdColumn, $morphClass) { /** @var Builder $permissionQuery */ $permissionQuery->select('joint_permissions.role_id')->from('joint_permissions') - ->whereColumn('joint_permissions.entity_id', '=', $fullEntityIdColumn) + ->whereColumn('joint_permissions.entity_id', '=', $fullPageIdColumn) ->where('joint_permissions.entity_type', '=', $morphClass) - ->where('joint_permissions.action', '=', 'view') ->whereIn('joint_permissions.role_id', $this->getCurrentUserRoleIds()) ->where(function (QueryBuilder $query) { $this->addJointHasPermissionCheck($query, $this->currentUser()->id); }); }; - $q = $query->where(function ($query) use ($existsQuery, $fullEntityIdColumn) { + $q = $query->where(function ($query) use ($existsQuery, $fullPageIdColumn) { $query->whereExists($existsQuery) - ->orWhere($fullEntityIdColumn, '=', 0); + ->orWhere($fullPageIdColumn, '=', 0); }); - if ($instance instanceof Page) { - // Prevent visibility of non-owned draft pages - $q->whereExists(function (QueryBuilder $query) use ($fullEntityIdColumn) { - $query->select('id')->from('pages') - ->whereColumn('pages.id', '=', $fullEntityIdColumn) - ->where(function (QueryBuilder $query) { - $query->where('pages.draft', '=', false) - ->orWhere('pages.owned_by', '=', $this->currentUser()->id); - }); - }); - } + // Prevent visibility of non-owned draft pages + $q->whereExists(function (QueryBuilder $query) use ($fullPageIdColumn) { + $query->select('id')->from('pages') + ->whereColumn('pages.id', '=', $fullPageIdColumn) + ->where(function (QueryBuilder $query) { + $query->where('pages.draft', '=', false) + ->orWhere('pages.owned_by', '=', $this->currentUser()->id); + }); + }); return $q; } @@ -283,4 +267,16 @@ class PermissionApplicator return $this->currentUser()->roles->pluck('id')->values()->all(); } + + /** + * Ensure the given action is a valid and expected entity action. + * Throws an exception if invalid otherwise does nothing. + * @throws InvalidArgumentException + */ + protected function ensureValidEntityAction(string $action): void + { + if (!in_array($action, EntityPermission::PERMISSIONS)) { + throw new InvalidArgumentException('Action should be a simple entity permission action, not a role permission'); + } + } }