diff --git a/app/Auth/Permissions/JointPermissionBuilder.php b/app/Auth/Permissions/JointPermissionBuilder.php index 4d8692aab..313f5de18 100644 --- a/app/Auth/Permissions/JointPermissionBuilder.php +++ b/app/Auth/Permissions/JointPermissionBuilder.php @@ -2,7 +2,6 @@ namespace BookStack\Auth\Permissions; -use BookStack\Auth\Role; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\BookChild; use BookStack\Entities\Models\Bookshelf; diff --git a/app/Auth/Permissions/PermissionApplicator.php b/app/Auth/Permissions/PermissionApplicator.php index 58ee7d93a..ee2027e8f 100644 --- a/app/Auth/Permissions/PermissionApplicator.php +++ b/app/Auth/Permissions/PermissionApplicator.php @@ -183,20 +183,70 @@ class PermissionApplicator * 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 + public function restrictEntityQuery(Builder $query, string $morphClass): Builder { - return $query->where(function (Builder $parentQuery) { - $parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) { - $permissionQuery->whereIn('role_id', $this->getCurrentUserRoleIds()) - ->where(function (Builder $query) { - $this->addJointHasPermissionCheck($query, $this->currentUser()->id); - }); - })->orWhereHas('jointUserPermissions', function (Builder $query) { - $query->where('user_id', '=', $this->currentUser()->id)->where('has_permission', '=', true); - }); - })->whereDoesntHave('jointUserPermissions', function (Builder $query) { - $query->where('user_id', '=', $this->currentUser()->id)->where('has_permission', '=', false); + $this->getCurrentUserRoleIds(); + $this->currentUser()->id; + + $userViewAll = userCan($morphClass . '-view-all'); + $userViewOwn = userCan($morphClass . '-view-own'); + + // TODO - Leave this as the new admin workaround? + // Or auto generate collapsed role permissions for admins? + if (\user()->hasSystemRole('admin')) { + return $query; + } + + // Fallback permission join + $query->joinSub(function (QueryBuilder $joinQuery) use ($morphClass) { + $joinQuery->select(['entity_id'])->selectRaw('max(view) as perms_fallback') + ->from('entity_permissions_collapsed') + ->where('entity_type', '=', $morphClass) + ->whereNull(['role_id', 'user_id']) + ->groupBy('entity_id'); + }, 'p_f', 'id', '=', 'p_f.entity_id', 'left'); + + // Role permission join + $query->joinSub(function (QueryBuilder $joinQuery) use ($morphClass) { + $joinQuery->select(['entity_id'])->selectRaw('max(view) as perms_role') + ->from('entity_permissions_collapsed') + ->where('entity_type', '=', $morphClass) + ->whereIn('role_id', $this->getCurrentUserRoleIds()) + ->groupBy('entity_id'); + }, 'p_r', 'id', '=', 'p_r.entity_id', 'left'); + + // User permission join + $query->joinSub(function (QueryBuilder $joinQuery) use ($morphClass) { + $joinQuery->select(['entity_id'])->selectRaw('max(view) as perms_user') + ->from('entity_permissions_collapsed') + ->where('entity_type', '=', $morphClass) + ->where('user_id', '=', $this->currentUser()->id) + ->groupBy('entity_id'); + }, 'p_u', 'id', '=', 'p_u.entity_id', 'left'); + + // Where permissions apply + $query->where(function (Builder $query) use ($userViewOwn, $userViewAll) { + $query->where('perms_user', '=', 1) + ->orWhere(function (Builder $query) { + $query->whereNull('perms_user')->where('perms_role', '=', 1); + })->orWhere(function (Builder $query) { + $query->whereNull(['perms_user', 'perms_role']) + ->where('perms_fallback', '=', 1); + }); + + if ($userViewAll) { + $query->orWhere(function (Builder $query) { + $query->whereNull(['perms_user', 'perms_role', 'perms_fallback']); + }); + } else if ($userViewOwn) { + $query->orWhere(function (Builder $query) { + $query->whereNull(['perms_user', 'perms_role', 'perms_fallback']) + ->where('created_by', '=', $this->currentUser()->id); + }); + } }); + + return $query; } /** @@ -226,6 +276,9 @@ class PermissionApplicator $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn]; $pageMorphClass = (new Page())->getMorphClass(); + // TODO; + return $query; + $q = $query->where(function ($query) use ($tableDetails) { $query->whereExists(function ($permissionQuery) use ($tableDetails) { /** @var Builder $permissionQuery */ @@ -275,6 +328,9 @@ class PermissionApplicator $fullPageIdColumn = $tableName . '.' . $pageIdColumn; $morphClass = (new Page())->getMorphClass(); + // TODO + return $query; + $existsQuery = function ($permissionQuery) use ($fullPageIdColumn, $morphClass) { /** @var Builder $permissionQuery */ $permissionQuery->select('joint_permissions.role_id')->from('joint_permissions') diff --git a/app/Entities/Models/Entity.php b/app/Entities/Models/Entity.php index 4552b5659..fc83bdd7e 100644 --- a/app/Entities/Models/Entity.php +++ b/app/Entities/Models/Entity.php @@ -70,7 +70,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable */ public function scopeVisible(Builder $query): Builder { - return app()->make(PermissionApplicator::class)->restrictEntityQuery($query); + return app()->make(PermissionApplicator::class)->restrictEntityQuery($query, $this->getMorphClass()); } /** diff --git a/dev/docs/permission-scenario-testing.md b/dev/docs/permission-scenario-testing.md index bfb5e7aa3..6d0935f09 100644 --- a/dev/docs/permission-scenario-testing.md +++ b/dev/docs/permission-scenario-testing.md @@ -8,8 +8,11 @@ Tests are categorised by the most specific element involved in the scenario, whe - User entity permissions. - Role entity permissions. +- Fallback entity permissions. - Role permissions. +- TODO - Test fallback in the context of the above. + ## General Permission Logical Rules The below are some general rules we follow to standardise the behaviour of permissions in the platform: