mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
Started aligning permission behaviour across application methods
This commit is contained in:
parent
60bf838a4a
commit
e8a8fedfd6
@ -40,4 +40,30 @@ class EntityPermission extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(User::class);
|
return $this->belongsTo(User::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the type of entity permission this is.
|
||||||
|
* Will be one of: user, role, fallback
|
||||||
|
*/
|
||||||
|
public function getAssignedType(): string
|
||||||
|
{
|
||||||
|
if ($this->user_id) {
|
||||||
|
return 'user';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->role_id) {
|
||||||
|
return 'role';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'fallback';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ID for the assigned type of permission.
|
||||||
|
* (Role/User ID). Defaults to 0 for fallback.
|
||||||
|
*/
|
||||||
|
public function getAssignedTypeId(): int
|
||||||
|
{
|
||||||
|
return $this->user_id ?? $this->role_id ?? 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,6 +77,11 @@ class PermissionApplicator
|
|||||||
$chain[] = $entity->book;
|
$chain[] = $entity->book;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Record role access preventions.
|
||||||
|
// Used when we encounter a negative role permission where inheritance is active and therefore
|
||||||
|
// need to check permissive status on parent items.
|
||||||
|
$blockedRoleIds = [];
|
||||||
|
|
||||||
foreach ($chain as $currentEntity) {
|
foreach ($chain as $currentEntity) {
|
||||||
$relevantPermissions = $currentEntity->permissions()
|
$relevantPermissions = $currentEntity->permissions()
|
||||||
->where(function (Builder $query) use ($userRoleIds, $userId) {
|
->where(function (Builder $query) use ($userRoleIds, $userId) {
|
||||||
@ -95,39 +100,62 @@ class PermissionApplicator
|
|||||||
// 3. Fallback-specific permissions
|
// 3. Fallback-specific permissions
|
||||||
// For role permissions, the system tries to be fairly permissive, in that if the user has two roles,
|
// For role permissions, the system tries to be fairly permissive, in that if the user has two roles,
|
||||||
// one lacking and one permitting an action, they will be permitted.
|
// one lacking and one permitting an action, they will be permitted.
|
||||||
|
// This can be complex when multiple roles and inheritance gets involved. If permission is prevented
|
||||||
|
// via "Role A" on an item, but inheritance is active and permission is granted via "Role B" on parent item,
|
||||||
|
// the user will be granted permission.
|
||||||
|
|
||||||
// If the default is set, we have to return something here.
|
$allowedByTypeById = ['fallback' => [], 'user' => [], 'role' => []];
|
||||||
$allowedById = [];
|
/** @var EntityPermission $permission */
|
||||||
foreach ($relevantPermissions as $permission) {
|
foreach ($relevantPermissions as $permission) {
|
||||||
$allowedById[($permission->role_id ?? '') . ':' . ($permission->user_id ?? '')] = $permission->$action;
|
$allowedByTypeById[$permission->getAssignedType()][$permission->getAssignedTypeId()] = $permission->$permission;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$inheriting = !isset($allowedByTypeById['fallback'][0]);
|
||||||
|
|
||||||
// Continue up the chain if no applicable entity permission overrides.
|
// Continue up the chain if no applicable entity permission overrides.
|
||||||
if (empty($allowedById)) {
|
if (count($relevantPermissions) === 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have user-specific permissions set, return the status of that
|
// If we have user-specific permissions set, return the status of that
|
||||||
// since it's the most specific possible.
|
// since it's the most specific possible.
|
||||||
$userKey = ':' . $userId;
|
if (isset($allowedByTypeById['user'][$userId])) {
|
||||||
if (isset($allowedById[$userKey])) {
|
return $allowedByTypeById['user'][$userId];
|
||||||
return $allowedById[$userKey];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have role-specific permissions set, allow if any of those
|
// If we have role-specific permissions set, allow if any of those
|
||||||
// role permissions allow access.
|
// role permissions allow access. We do not allow if the role has been previously
|
||||||
$hasDefault = isset($allowedById[':']);
|
// blocked by a high-priority inheriting level.
|
||||||
if (!$hasDefault || count($allowedById) > 1) {
|
// If we're inheriting at this level, and there's an explicit non-allow permission, we record
|
||||||
foreach ($allowedById as $key => $allowed) {
|
// it for checking up the chain.
|
||||||
if ($key !== ':' && $allowed) {
|
foreach ($allowedByTypeById['role'] as $roleId => $allowed) {
|
||||||
return true;
|
if ($allowed && !in_array($roleId, $blockedRoleIds)) {
|
||||||
}
|
return true;
|
||||||
|
} else if (!$allowed) {
|
||||||
|
$blockedRoleIds[] = $roleId;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we had role permissions, and none of them allowed (via above loop), and
|
||||||
|
// we are not inheriting, exit here since we only have role permissions in play blocking access.
|
||||||
|
if (count($allowedByTypeById['role']) > 0 && !$inheriting) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Continue up the chain if inheriting
|
||||||
|
if ($inheriting) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Otherwise, return the default "Other roles" fallback value.
|
// Otherwise, return the default "Other roles" fallback value.
|
||||||
return $allowedById[':'];
|
return $allowedByTypeById['fallback'][0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have roles that need to be assessed, but we are also inheriting, pass back the prevented
|
||||||
|
// role IDs so they can be excluded from the role permission check.
|
||||||
|
if (count($blockedRoleIds) > 0) {
|
||||||
|
// TODO - Need to use these ids in some form in outer permission check, as blockers when access
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -20,7 +20,7 @@ export class EntityPermissions extends Component {
|
|||||||
// "Everyone Else" inherit toggle
|
// "Everyone Else" inherit toggle
|
||||||
this.everyoneInheritToggle.addEventListener('change', event => {
|
this.everyoneInheritToggle.addEventListener('change', event => {
|
||||||
const inherit = event.target.checked;
|
const inherit = event.target.checked;
|
||||||
const permissions = document.querySelectorAll('input[name^="permissions[0]["]');
|
const permissions = document.querySelectorAll('input[name^="permissions[fallback]"]');
|
||||||
for (const permission of permissions) {
|
for (const permission of permissions) {
|
||||||
permission.disabled = inherit;
|
permission.disabled = inherit;
|
||||||
permission.checked = false;
|
permission.checked = false;
|
||||||
|
@ -202,14 +202,16 @@ class EntityProvider
|
|||||||
* @param string[] $actions
|
* @param string[] $actions
|
||||||
* @param Role[] $roles
|
* @param Role[] $roles
|
||||||
*/
|
*/
|
||||||
public function setPermissions(Entity $entity, array $actions = [], array $roles = []): void
|
public function setPermissions(Entity $entity, array $actions = [], array $roles = [], $inherit = false): void
|
||||||
{
|
{
|
||||||
$entity->permissions()->delete();
|
$entity->permissions()->delete();
|
||||||
|
|
||||||
$permissions = [
|
$permissions = [];
|
||||||
|
|
||||||
|
if (!$inherit) {
|
||||||
// Set default permissions to not allow actions so that only the provided role permissions are at play.
|
// Set default permissions to not allow actions so that only the provided role permissions are at play.
|
||||||
['role_id' => null, 'view' => false, 'create' => false, 'update' => false, 'delete' => false],
|
$permissions[] = ['role_id' => null, 'user_id' => null, 'view' => false, 'create' => false, 'update' => false, 'delete' => false];
|
||||||
];
|
}
|
||||||
|
|
||||||
foreach ($roles as $role) {
|
foreach ($roles as $role) {
|
||||||
$permission = ['role_id' => $role->id];
|
$permission = ['role_id' => $role->id];
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Tests\Permissions;
|
namespace Tests\Permissions;
|
||||||
|
|
||||||
|
use BookStack\Auth\Role;
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
use BookStack\Entities\Models\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Models\Bookshelf;
|
use BookStack\Entities\Models\Bookshelf;
|
||||||
@ -657,6 +658,34 @@ class EntityPermissionsTest extends TestCase
|
|||||||
$resp->assertRedirect($book->getUrl('/page/test-page'));
|
$resp->assertRedirect($book->getUrl('/page/test-page'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_access_to_item_prevented_if_inheritance_active_but_permission_prevented_via_role()
|
||||||
|
{
|
||||||
|
$user = $this->getViewer();
|
||||||
|
$viewerRole = $user->roles->first();
|
||||||
|
$chapter = $this->entities->chapter();
|
||||||
|
$book = $chapter->book;
|
||||||
|
|
||||||
|
$this->entities->setPermissions($book, ['edit'], [$viewerRole], false);
|
||||||
|
$this->entities->setPermissions($chapter, [], [$viewerRole], true);
|
||||||
|
|
||||||
|
$this->assertFalse(userCan('chapter-update', $chapter));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_access_to_item_allowed_if_inheritance_active_and_permission_prevented_via_role_but_allowed_via_parent()
|
||||||
|
{
|
||||||
|
$user = $this->getViewer();
|
||||||
|
$viewerRole = $user->roles->first();
|
||||||
|
$editorRole = Role::getRole('Editor');
|
||||||
|
$user->attachRole($editorRole);
|
||||||
|
$chapter = $this->entities->chapter();
|
||||||
|
$book = $chapter->book;
|
||||||
|
|
||||||
|
$this->entities->setPermissions($book, ['edit'], [$editorRole], false);
|
||||||
|
$this->entities->setPermissions($chapter, [], [$viewerRole], true);
|
||||||
|
|
||||||
|
$this->assertTrue(userCan('chapter-update', $chapter));
|
||||||
|
}
|
||||||
|
|
||||||
public function test_book_permissions_can_be_generated_without_error_if_child_chapter_is_in_recycle_bin()
|
public function test_book_permissions_can_be_generated_without_error_if_child_chapter_is_in_recycle_bin()
|
||||||
{
|
{
|
||||||
$book = $this->entities->bookHasChaptersAndPages();
|
$book = $this->entities->bookHasChaptersAndPages();
|
||||||
|
Loading…
Reference in New Issue
Block a user