mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-30 07:32:39 +01:00
Merge branch 'development' into bugfix/fix-being-unable-to-clear-filters
This commit is contained in:
commit
6adc642d2f
@ -2,20 +2,41 @@
|
|||||||
|
|
||||||
namespace BookStack\Auth\Permissions;
|
namespace BookStack\Auth\Permissions;
|
||||||
|
|
||||||
|
use BookStack\Auth\Role;
|
||||||
use BookStack\Model;
|
use BookStack\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property int $role_id
|
||||||
|
* @property int $entity_id
|
||||||
|
* @property string $entity_type
|
||||||
|
* @property boolean $view
|
||||||
|
* @property boolean $create
|
||||||
|
* @property boolean $update
|
||||||
|
* @property boolean $delete
|
||||||
|
*/
|
||||||
class EntityPermission extends Model
|
class EntityPermission extends Model
|
||||||
{
|
{
|
||||||
protected $fillable = ['role_id', 'action'];
|
public const PERMISSIONS = ['view', 'create', 'update', 'delete'];
|
||||||
|
|
||||||
|
protected $fillable = ['role_id', 'view', 'create', 'update', 'delete'];
|
||||||
public $timestamps = false;
|
public $timestamps = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all this restriction's attached entity.
|
* Get this restriction's attached entity.
|
||||||
*
|
|
||||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
|
||||||
*/
|
*/
|
||||||
public function restrictable()
|
public function restrictable(): MorphTo
|
||||||
{
|
{
|
||||||
return $this->morphTo('restrictable');
|
return $this->morphTo('restrictable');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the role assigned to this entity permission.
|
||||||
|
*/
|
||||||
|
public function role(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Role::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@ class JointPermissionBuilder
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Chunk through all bookshelves
|
// Chunk through all bookshelves
|
||||||
Bookshelf::query()->withTrashed()->select(['id', 'restricted', 'owned_by'])
|
Bookshelf::query()->withTrashed()->select(['id', 'owned_by'])
|
||||||
->chunk(50, function (EloquentCollection $shelves) use ($roles) {
|
->chunk(50, function (EloquentCollection $shelves) use ($roles) {
|
||||||
$this->createManyJointPermissions($shelves->all(), $roles);
|
$this->createManyJointPermissions($shelves->all(), $roles);
|
||||||
});
|
});
|
||||||
@ -92,7 +92,7 @@ class JointPermissionBuilder
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Chunk through all bookshelves
|
// Chunk through all bookshelves
|
||||||
Bookshelf::query()->select(['id', 'restricted', 'owned_by'])
|
Bookshelf::query()->select(['id', 'owned_by'])
|
||||||
->chunk(50, function ($shelves) use ($roles) {
|
->chunk(50, function ($shelves) use ($roles) {
|
||||||
$this->createManyJointPermissions($shelves->all(), $roles);
|
$this->createManyJointPermissions($shelves->all(), $roles);
|
||||||
});
|
});
|
||||||
@ -138,12 +138,11 @@ class JointPermissionBuilder
|
|||||||
protected function bookFetchQuery(): Builder
|
protected function bookFetchQuery(): Builder
|
||||||
{
|
{
|
||||||
return Book::query()->withTrashed()
|
return Book::query()->withTrashed()
|
||||||
->select(['id', 'restricted', 'owned_by'])->with([
|
->select(['id', 'owned_by'])->with([
|
||||||
'chapters' => function ($query) {
|
'chapters' => function ($query) {
|
||||||
$query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id']);
|
|
||||||
},
|
},
|
||||||
'pages' => function ($query) {
|
'pages' => function ($query) {
|
||||||
$query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id', 'chapter_id']);
|
$query->withTrashed()->select(['id', 'owned_by', 'book_id', 'chapter_id']);
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -218,7 +217,6 @@ class JointPermissionBuilder
|
|||||||
$simple = new SimpleEntityData();
|
$simple = new SimpleEntityData();
|
||||||
$simple->id = $attrs['id'];
|
$simple->id = $attrs['id'];
|
||||||
$simple->type = $entity->getMorphClass();
|
$simple->type = $entity->getMorphClass();
|
||||||
$simple->restricted = boolval($attrs['restricted'] ?? 0);
|
|
||||||
$simple->owned_by = $attrs['owned_by'] ?? 0;
|
$simple->owned_by = $attrs['owned_by'] ?? 0;
|
||||||
$simple->book_id = $attrs['book_id'] ?? null;
|
$simple->book_id = $attrs['book_id'] ?? null;
|
||||||
$simple->chapter_id = $attrs['chapter_id'] ?? null;
|
$simple->chapter_id = $attrs['chapter_id'] ?? null;
|
||||||
@ -240,21 +238,14 @@ class JointPermissionBuilder
|
|||||||
$this->readyEntityCache($entities);
|
$this->readyEntityCache($entities);
|
||||||
$jointPermissions = [];
|
$jointPermissions = [];
|
||||||
|
|
||||||
// Create a mapping of entity restricted statuses
|
|
||||||
$entityRestrictedMap = [];
|
|
||||||
foreach ($entities as $entity) {
|
|
||||||
$entityRestrictedMap[$entity->type . ':' . $entity->id] = $entity->restricted;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch related entity permissions
|
// Fetch related entity permissions
|
||||||
$permissions = $this->getEntityPermissionsForEntities($entities);
|
$permissions = $this->getEntityPermissionsForEntities($entities);
|
||||||
|
|
||||||
// Create a mapping of explicit entity permissions
|
// Create a mapping of explicit entity permissions
|
||||||
$permissionMap = [];
|
$permissionMap = [];
|
||||||
foreach ($permissions as $permission) {
|
foreach ($permissions as $permission) {
|
||||||
$key = $permission->restrictable_type . ':' . $permission->restrictable_id . ':' . $permission->role_id;
|
$key = $permission->entity_type . ':' . $permission->entity_id . ':' . $permission->role_id;
|
||||||
$isRestricted = $entityRestrictedMap[$permission->restrictable_type . ':' . $permission->restrictable_id];
|
$permissionMap[$key] = $permission->view;
|
||||||
$permissionMap[$key] = $isRestricted;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a mapping of role permissions
|
// Create a mapping of role permissions
|
||||||
@ -319,11 +310,10 @@ class JointPermissionBuilder
|
|||||||
{
|
{
|
||||||
$idsByType = $this->entitiesToTypeIdMap($entities);
|
$idsByType = $this->entitiesToTypeIdMap($entities);
|
||||||
$permissionFetch = EntityPermission::query()
|
$permissionFetch = EntityPermission::query()
|
||||||
->where('action', '=', 'view')
|
|
||||||
->where(function (Builder $query) use ($idsByType) {
|
->where(function (Builder $query) use ($idsByType) {
|
||||||
foreach ($idsByType as $type => $ids) {
|
foreach ($idsByType as $type => $ids) {
|
||||||
$query->orWhere(function (Builder $query) use ($type, $ids) {
|
$query->orWhere(function (Builder $query) use ($type, $ids) {
|
||||||
$query->where('restrictable_type', '=', $type)->whereIn('restrictable_id', $ids);
|
$query->where('entity_type', '=', $type)->whereIn('entity_id', $ids);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -345,7 +335,7 @@ class JointPermissionBuilder
|
|||||||
return $this->createJointPermissionDataArray($entity, $roleId, true, true);
|
return $this->createJointPermissionDataArray($entity, $roleId, true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($entity->restricted) {
|
if ($this->entityPermissionsActiveForRole($permissionMap, $entity, $roleId)) {
|
||||||
$hasAccess = $this->mapHasActiveRestriction($permissionMap, $entity, $roleId);
|
$hasAccess = $this->mapHasActiveRestriction($permissionMap, $entity, $roleId);
|
||||||
|
|
||||||
return $this->createJointPermissionDataArray($entity, $roleId, $hasAccess, $hasAccess);
|
return $this->createJointPermissionDataArray($entity, $roleId, $hasAccess, $hasAccess);
|
||||||
@ -358,13 +348,14 @@ class JointPermissionBuilder
|
|||||||
// For chapters and pages, Check if explicit permissions are set on the Book.
|
// For chapters and pages, Check if explicit permissions are set on the Book.
|
||||||
$book = $this->getBook($entity->book_id);
|
$book = $this->getBook($entity->book_id);
|
||||||
$hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $book, $roleId);
|
$hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $book, $roleId);
|
||||||
$hasPermissiveAccessToParents = !$book->restricted;
|
$hasPermissiveAccessToParents = !$this->entityPermissionsActiveForRole($permissionMap, $book, $roleId);
|
||||||
|
|
||||||
// For pages with a chapter, Check if explicit permissions are set on the Chapter
|
// For pages with a chapter, Check if explicit permissions are set on the Chapter
|
||||||
if ($entity->type === 'page' && $entity->chapter_id !== 0) {
|
if ($entity->type === 'page' && $entity->chapter_id !== 0) {
|
||||||
$chapter = $this->getChapter($entity->chapter_id);
|
$chapter = $this->getChapter($entity->chapter_id);
|
||||||
$hasPermissiveAccessToParents = $hasPermissiveAccessToParents && !$chapter->restricted;
|
$chapterRestricted = $this->entityPermissionsActiveForRole($permissionMap, $chapter, $roleId);
|
||||||
if ($chapter->restricted) {
|
$hasPermissiveAccessToParents = $hasPermissiveAccessToParents && !$chapterRestricted;
|
||||||
|
if ($chapterRestricted) {
|
||||||
$hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $chapter, $roleId);
|
$hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $chapter, $roleId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -377,14 +368,25 @@ class JointPermissionBuilder
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if entity permissions are defined within the given map, for the given entity and role.
|
||||||
|
* Checks for the default `role_id=0` backup option as a fallback.
|
||||||
|
*/
|
||||||
|
protected function entityPermissionsActiveForRole(array $permissionMap, SimpleEntityData $entity, int $roleId): bool
|
||||||
|
{
|
||||||
|
$keyPrefix = $entity->type . ':' . $entity->id . ':';
|
||||||
|
return isset($permissionMap[$keyPrefix . $roleId]) || isset($permissionMap[$keyPrefix . '0']);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check for an active restriction in an entity map.
|
* Check for an active restriction in an entity map.
|
||||||
*/
|
*/
|
||||||
protected function mapHasActiveRestriction(array $entityMap, SimpleEntityData $entity, int $roleId): bool
|
protected function mapHasActiveRestriction(array $entityMap, SimpleEntityData $entity, int $roleId): bool
|
||||||
{
|
{
|
||||||
$key = $entity->type . ':' . $entity->id . ':' . $roleId;
|
$roleKey = $entity->type . ':' . $entity->id . ':' . $roleId;
|
||||||
|
$defaultKey = $entity->type . ':' . $entity->id . ':0';
|
||||||
|
|
||||||
return $entityMap[$key] ?? false;
|
return $entityMap[$roleKey] ?? $entityMap[$defaultKey] ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -59,11 +59,15 @@ class PermissionApplicator
|
|||||||
*/
|
*/
|
||||||
protected function hasEntityPermission(Entity $entity, array $userRoleIds, string $action): ?bool
|
protected function hasEntityPermission(Entity $entity, array $userRoleIds, string $action): ?bool
|
||||||
{
|
{
|
||||||
|
$this->ensureValidEntityAction($action);
|
||||||
|
|
||||||
$adminRoleId = Role::getSystemRole('admin')->id;
|
$adminRoleId = Role::getSystemRole('admin')->id;
|
||||||
if (in_array($adminRoleId, $userRoleIds)) {
|
if (in_array($adminRoleId, $userRoleIds)) {
|
||||||
return true;
|
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];
|
$chain = [$entity];
|
||||||
if ($entity instanceof Page && $entity->chapter_id) {
|
if ($entity instanceof Page && $entity->chapter_id) {
|
||||||
$chain[] = $entity->chapter;
|
$chain[] = $entity->chapter;
|
||||||
@ -74,16 +78,26 @@ class PermissionApplicator
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach ($chain as $currentEntity) {
|
foreach ($chain as $currentEntity) {
|
||||||
if (is_null($currentEntity->restricted)) {
|
$allowedByRoleId = $currentEntity->permissions()
|
||||||
throw new InvalidArgumentException('Entity restricted field used but has not been loaded');
|
->whereIn('role_id', [0, ...$userRoleIds])
|
||||||
|
->pluck($action, 'role_id');
|
||||||
|
|
||||||
|
// Continue up the chain if no applicable entity permission overrides.
|
||||||
|
if ($allowedByRoleId->isEmpty()) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($currentEntity->restricted) {
|
// If we have user-role-specific permissions set, allow if any of those
|
||||||
return $currentEntity->permissions()
|
// role permissions allow access.
|
||||||
->whereIn('role_id', $userRoleIds)
|
$hasDefault = $allowedByRoleId->has(0);
|
||||||
->where('action', '=', $action)
|
if (!$hasDefault || $allowedByRoleId->count() > 1) {
|
||||||
->count() > 0;
|
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;
|
return null;
|
||||||
@ -95,18 +109,16 @@ class PermissionApplicator
|
|||||||
*/
|
*/
|
||||||
public function checkUserHasEntityPermissionOnAny(string $action, string $entityClass = ''): bool
|
public function checkUserHasEntityPermissionOnAny(string $action, string $entityClass = ''): bool
|
||||||
{
|
{
|
||||||
if (strpos($action, '-') !== false) {
|
$this->ensureValidEntityAction($action);
|
||||||
throw new InvalidArgumentException('Action should be a simple entity permission action, not a role permission');
|
|
||||||
}
|
|
||||||
|
|
||||||
$permissionQuery = EntityPermission::query()
|
$permissionQuery = EntityPermission::query()
|
||||||
->where('action', '=', $action)
|
->where($action, '=', true)
|
||||||
->whereIn('role_id', $this->getCurrentUserRoleIds());
|
->whereIn('role_id', $this->getCurrentUserRoleIds());
|
||||||
|
|
||||||
if (!empty($entityClass)) {
|
if (!empty($entityClass)) {
|
||||||
/** @var Entity $entityInstance */
|
/** @var Entity $entityInstance */
|
||||||
$entityInstance = app()->make($entityClass);
|
$entityInstance = app()->make($entityClass);
|
||||||
$permissionQuery = $permissionQuery->where('restrictable_type', '=', $entityInstance->getMorphClass());
|
$permissionQuery = $permissionQuery->where('entity_type', '=', $entityInstance->getMorphClass());
|
||||||
}
|
}
|
||||||
|
|
||||||
$hasPermission = $permissionQuery->count() > 0;
|
$hasPermission = $permissionQuery->count() > 0;
|
||||||
@ -255,4 +267,16 @@ class PermissionApplicator
|
|||||||
|
|
||||||
return $this->currentUser()->roles->pluck('id')->values()->all();
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
68
app/Auth/Permissions/PermissionFormData.php
Normal file
68
app/Auth/Permissions/PermissionFormData.php
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Auth\Permissions;
|
||||||
|
|
||||||
|
use BookStack\Auth\Role;
|
||||||
|
use BookStack\Entities\Models\Entity;
|
||||||
|
|
||||||
|
class PermissionFormData
|
||||||
|
{
|
||||||
|
protected Entity $entity;
|
||||||
|
|
||||||
|
public function __construct(Entity $entity)
|
||||||
|
{
|
||||||
|
$this->entity = $entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the permissions with assigned roles.
|
||||||
|
*/
|
||||||
|
public function permissionsWithRoles(): array
|
||||||
|
{
|
||||||
|
return $this->entity->permissions()
|
||||||
|
->with('role')
|
||||||
|
->where('role_id', '!=', 0)
|
||||||
|
->get()
|
||||||
|
->sortBy('role.display_name')
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the roles that don't yet have specific permissions for the
|
||||||
|
* entity we're managing permissions for.
|
||||||
|
*/
|
||||||
|
public function rolesNotAssigned(): array
|
||||||
|
{
|
||||||
|
$assigned = $this->entity->permissions()->pluck('role_id');
|
||||||
|
return Role::query()
|
||||||
|
->where('system_name', '!=', 'admin')
|
||||||
|
->whereNotIn('id', $assigned)
|
||||||
|
->orderBy('display_name', 'asc')
|
||||||
|
->get()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the entity permission for the "Everyone Else" option.
|
||||||
|
*/
|
||||||
|
public function everyoneElseEntityPermission(): EntityPermission
|
||||||
|
{
|
||||||
|
/** @var ?EntityPermission $permission */
|
||||||
|
$permission = $this->entity->permissions()
|
||||||
|
->where('role_id', '=', 0)
|
||||||
|
->first();
|
||||||
|
return $permission ?? (new EntityPermission());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the "Everyone Else" role entry.
|
||||||
|
*/
|
||||||
|
public function everyoneElseRole(): Role
|
||||||
|
{
|
||||||
|
return (new Role())->forceFill([
|
||||||
|
'id' => 0,
|
||||||
|
'display_name' => trans('entities.permissions_role_everyone_else'),
|
||||||
|
'description' => trans('entities.permissions_role_everyone_else_desc'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -139,6 +139,7 @@ class PermissionsRepo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$role->entityPermissions()->delete();
|
||||||
$role->jointPermissions()->delete();
|
$role->jointPermissions()->delete();
|
||||||
Activity::add(ActivityType::ROLE_DELETE, $role);
|
Activity::add(ActivityType::ROLE_DELETE, $role);
|
||||||
$role->delete();
|
$role->delete();
|
||||||
|
@ -6,7 +6,6 @@ class SimpleEntityData
|
|||||||
{
|
{
|
||||||
public int $id;
|
public int $id;
|
||||||
public string $type;
|
public string $type;
|
||||||
public bool $restricted;
|
|
||||||
public int $owned_by;
|
public int $owned_by;
|
||||||
public ?int $book_id;
|
public ?int $book_id;
|
||||||
public ?int $chapter_id;
|
public ?int $chapter_id;
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace BookStack\Auth;
|
namespace BookStack\Auth;
|
||||||
|
|
||||||
|
use BookStack\Auth\Permissions\EntityPermission;
|
||||||
use BookStack\Auth\Permissions\JointPermission;
|
use BookStack\Auth\Permissions\JointPermission;
|
||||||
use BookStack\Auth\Permissions\RolePermission;
|
use BookStack\Auth\Permissions\RolePermission;
|
||||||
use BookStack\Interfaces\Loggable;
|
use BookStack\Interfaces\Loggable;
|
||||||
@ -54,6 +55,14 @@ class Role extends Model implements Loggable
|
|||||||
return $this->belongsToMany(RolePermission::class, 'permission_role', 'role_id', 'permission_id');
|
return $this->belongsToMany(RolePermission::class, 'permission_role', 'role_id', 'permission_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the entity permissions assigned to this role.
|
||||||
|
*/
|
||||||
|
public function entityPermissions(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(EntityPermission::class);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if this role has a permission.
|
* Check if this role has a permission.
|
||||||
*/
|
*/
|
||||||
@ -109,17 +118,6 @@ class Role extends Model implements Loggable
|
|||||||
return static::query()->where('hidden', '=', false)->orderBy('name')->get();
|
return static::query()->where('hidden', '=', false)->orderBy('name')->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the roles that can be restricted.
|
|
||||||
*/
|
|
||||||
public static function restrictable(): Collection
|
|
||||||
{
|
|
||||||
return static::query()
|
|
||||||
->where('system_name', '!=', 'admin')
|
|
||||||
->orderBy('display_name', 'asc')
|
|
||||||
->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
namespace BookStack\Console\Commands;
|
namespace BookStack\Console\Commands;
|
||||||
|
|
||||||
use BookStack\Entities\Models\Bookshelf;
|
use BookStack\Entities\Models\Bookshelf;
|
||||||
use BookStack\Entities\Repos\BookshelfRepo;
|
use BookStack\Entities\Tools\PermissionsUpdater;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
class CopyShelfPermissions extends Command
|
class CopyShelfPermissions extends Command
|
||||||
@ -25,19 +25,16 @@ class CopyShelfPermissions extends Command
|
|||||||
*/
|
*/
|
||||||
protected $description = 'Copy shelf permissions to all child books';
|
protected $description = 'Copy shelf permissions to all child books';
|
||||||
|
|
||||||
/**
|
protected PermissionsUpdater $permissionsUpdater;
|
||||||
* @var BookshelfRepo
|
|
||||||
*/
|
|
||||||
protected $bookshelfRepo;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new command instance.
|
* Create a new command instance.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function __construct(BookshelfRepo $repo)
|
public function __construct(PermissionsUpdater $permissionsUpdater)
|
||||||
{
|
{
|
||||||
$this->bookshelfRepo = $repo;
|
$this->permissionsUpdater = $permissionsUpdater;
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,18 +66,18 @@ class CopyShelfPermissions extends Command
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$shelves = Bookshelf::query()->get(['id', 'restricted']);
|
$shelves = Bookshelf::query()->get(['id']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($shelfSlug) {
|
if ($shelfSlug) {
|
||||||
$shelves = Bookshelf::query()->where('slug', '=', $shelfSlug)->get(['id', 'restricted']);
|
$shelves = Bookshelf::query()->where('slug', '=', $shelfSlug)->get(['id']);
|
||||||
if ($shelves->count() === 0) {
|
if ($shelves->count() === 0) {
|
||||||
$this->info('No shelves found with the given slug.');
|
$this->info('No shelves found with the given slug.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($shelves as $shelf) {
|
foreach ($shelves as $shelf) {
|
||||||
$this->bookshelfRepo->copyDownPermissions($shelf, false);
|
$this->permissionsUpdater->updateBookPermissionsFromShelf($shelf, false);
|
||||||
$this->info('Copied permissions for shelf [' . $shelf->id . ']');
|
$this->info('Copied permissions for shelf [' . $shelf->id . ']');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ class Book extends Entity implements HasCoverImage
|
|||||||
public $searchFactor = 1.2;
|
public $searchFactor = 1.2;
|
||||||
|
|
||||||
protected $fillable = ['name', 'description'];
|
protected $fillable = ['name', 'description'];
|
||||||
protected $hidden = ['restricted', 'pivot', 'image_id', 'deleted_at'];
|
protected $hidden = ['pivot', 'image_id', 'deleted_at'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the url for this book.
|
* Get the url for this book.
|
||||||
@ -120,4 +120,13 @@ class Book extends Entity implements HasCoverImage
|
|||||||
|
|
||||||
return $pages->concat($chapters)->sortBy('priority')->sortByDesc('draft');
|
return $pages->concat($chapters)->sortBy('priority')->sortByDesc('draft');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a visible book by its slug.
|
||||||
|
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
|
||||||
|
*/
|
||||||
|
public static function getBySlug(string $slug): self
|
||||||
|
{
|
||||||
|
return static::visible()->where('slug', '=', $slug)->firstOrFail();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ class Bookshelf extends Entity implements HasCoverImage
|
|||||||
|
|
||||||
protected $fillable = ['name', 'description', 'image_id'];
|
protected $fillable = ['name', 'description', 'image_id'];
|
||||||
|
|
||||||
protected $hidden = ['restricted', 'image_id', 'deleted_at'];
|
protected $hidden = ['image_id', 'deleted_at'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the books in this shelf.
|
* Get the books in this shelf.
|
||||||
@ -109,4 +109,13 @@ class Bookshelf extends Entity implements HasCoverImage
|
|||||||
$maxOrder = $this->books()->max('order');
|
$maxOrder = $this->books()->max('order');
|
||||||
$this->books()->attach($book->id, ['order' => $maxOrder + 1]);
|
$this->books()->attach($book->id, ['order' => $maxOrder + 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a visible shelf by its slug.
|
||||||
|
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
|
||||||
|
*/
|
||||||
|
public static function getBySlug(string $slug): self
|
||||||
|
{
|
||||||
|
return static::visible()->where('slug', '=', $slug)->firstOrFail();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ class Chapter extends BookChild
|
|||||||
public $searchFactor = 1.2;
|
public $searchFactor = 1.2;
|
||||||
|
|
||||||
protected $fillable = ['name', 'description', 'priority'];
|
protected $fillable = ['name', 'description', 'priority'];
|
||||||
protected $hidden = ['restricted', 'pivot', 'deleted_at'];
|
protected $hidden = ['pivot', 'deleted_at'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the pages that this chapter contains.
|
* Get the pages that this chapter contains.
|
||||||
@ -58,4 +58,13 @@ class Chapter extends BookChild
|
|||||||
->orderBy('priority', 'asc')
|
->orderBy('priority', 'asc')
|
||||||
->get();
|
->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a visible chapter by its book and page slugs.
|
||||||
|
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
|
||||||
|
*/
|
||||||
|
public static function getBySlugs(string $bookSlug, string $chapterSlug): self
|
||||||
|
{
|
||||||
|
return static::visible()->whereSlugs($bookSlug, $chapterSlug)->firstOrFail();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,6 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||||||
* @property Carbon $deleted_at
|
* @property Carbon $deleted_at
|
||||||
* @property int $created_by
|
* @property int $created_by
|
||||||
* @property int $updated_by
|
* @property int $updated_by
|
||||||
* @property bool $restricted
|
|
||||||
* @property Collection $tags
|
* @property Collection $tags
|
||||||
*
|
*
|
||||||
* @method static Entity|Builder visible()
|
* @method static Entity|Builder visible()
|
||||||
@ -176,16 +175,15 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
|
|||||||
*/
|
*/
|
||||||
public function permissions(): MorphMany
|
public function permissions(): MorphMany
|
||||||
{
|
{
|
||||||
return $this->morphMany(EntityPermission::class, 'restrictable');
|
return $this->morphMany(EntityPermission::class, 'entity');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if this entity has a specific restriction set against it.
|
* Check if this entity has a specific restriction set against it.
|
||||||
*/
|
*/
|
||||||
public function hasRestriction(int $role_id, string $action): bool
|
public function hasPermissions(): bool
|
||||||
{
|
{
|
||||||
return $this->permissions()->where('role_id', '=', $role_id)
|
return $this->permissions()->count() > 0;
|
||||||
->where('action', '=', $action)->count() > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -39,7 +39,7 @@ class Page extends BookChild
|
|||||||
|
|
||||||
public $textField = 'text';
|
public $textField = 'text';
|
||||||
|
|
||||||
protected $hidden = ['html', 'markdown', 'text', 'restricted', 'pivot', 'deleted_at'];
|
protected $hidden = ['html', 'markdown', 'text', 'pivot', 'deleted_at'];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'draft' => 'boolean',
|
'draft' => 'boolean',
|
||||||
@ -145,4 +145,13 @@ class Page extends BookChild
|
|||||||
|
|
||||||
return $refreshed;
|
return $refreshed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a visible page by its book and page slugs.
|
||||||
|
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
|
||||||
|
*/
|
||||||
|
public static function getBySlugs(string $bookSlug, string $pageSlug): self
|
||||||
|
{
|
||||||
|
return static::visible()->whereSlugs($bookSlug, $pageSlug)->firstOrFail();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||||||
class PageRevision extends Model implements Loggable
|
class PageRevision extends Model implements Loggable
|
||||||
{
|
{
|
||||||
protected $fillable = ['name', 'text', 'summary'];
|
protected $fillable = ['name', 'text', 'summary'];
|
||||||
protected $hidden = ['html', 'markdown', 'restricted', 'text'];
|
protected $hidden = ['html', 'markdown', 'text'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the user that created the page revision.
|
* Get the user that created the page revision.
|
||||||
|
@ -134,31 +134,6 @@ class BookshelfRepo
|
|||||||
$shelf->books()->sync($syncData);
|
$shelf->books()->sync($syncData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy down the permissions of the given shelf to all child books.
|
|
||||||
*/
|
|
||||||
public function copyDownPermissions(Bookshelf $shelf, $checkUserPermissions = true): int
|
|
||||||
{
|
|
||||||
$shelfPermissions = $shelf->permissions()->get(['role_id', 'action'])->toArray();
|
|
||||||
$shelfBooks = $shelf->books()->get(['id', 'restricted', 'owned_by']);
|
|
||||||
$updatedBookCount = 0;
|
|
||||||
|
|
||||||
/** @var Book $book */
|
|
||||||
foreach ($shelfBooks as $book) {
|
|
||||||
if ($checkUserPermissions && !userCan('restrictions-manage', $book)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$book->permissions()->delete();
|
|
||||||
$book->restricted = $shelf->restricted;
|
|
||||||
$book->permissions()->createMany($shelfPermissions);
|
|
||||||
$book->save();
|
|
||||||
$book->rebuildPermissions();
|
|
||||||
$updatedBookCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $updatedBookCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a bookshelf from the system.
|
* Remove a bookshelf from the system.
|
||||||
*
|
*
|
||||||
|
@ -122,8 +122,7 @@ class Cloner
|
|||||||
*/
|
*/
|
||||||
public function copyEntityPermissions(Entity $sourceEntity, Entity $targetEntity): void
|
public function copyEntityPermissions(Entity $sourceEntity, Entity $targetEntity): void
|
||||||
{
|
{
|
||||||
$targetEntity->restricted = $sourceEntity->restricted;
|
$permissions = $sourceEntity->permissions()->get(['role_id', 'view', 'create', 'update', 'delete'])->toArray();
|
||||||
$permissions = $sourceEntity->permissions()->get(['role_id', 'action'])->toArray();
|
|
||||||
$targetEntity->permissions()->delete();
|
$targetEntity->permissions()->delete();
|
||||||
$targetEntity->permissions()->createMany($permissions);
|
$targetEntity->permissions()->createMany($permissions);
|
||||||
$targetEntity->rebuildPermissions();
|
$targetEntity->rebuildPermissions();
|
||||||
|
@ -65,7 +65,7 @@ class HierarchyTransformer
|
|||||||
foreach ($book->chapters as $index => $chapter) {
|
foreach ($book->chapters as $index => $chapter) {
|
||||||
$newBook = $this->transformChapterToBook($chapter);
|
$newBook = $this->transformChapterToBook($chapter);
|
||||||
$shelfBookSyncData[$newBook->id] = ['order' => $index];
|
$shelfBookSyncData[$newBook->id] = ['order' => $index];
|
||||||
if (!$newBook->restricted) {
|
if (!$newBook->hasPermissions()) {
|
||||||
$this->cloner->copyEntityPermissions($shelf, $newBook);
|
$this->cloner->copyEntityPermissions($shelf, $newBook);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,10 @@
|
|||||||
namespace BookStack\Entities\Tools;
|
namespace BookStack\Entities\Tools;
|
||||||
|
|
||||||
use BookStack\Actions\ActivityType;
|
use BookStack\Actions\ActivityType;
|
||||||
|
use BookStack\Auth\Permissions\EntityPermission;
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
|
use BookStack\Entities\Models\Book;
|
||||||
|
use BookStack\Entities\Models\Bookshelf;
|
||||||
use BookStack\Entities\Models\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
use BookStack\Facades\Activity;
|
use BookStack\Facades\Activity;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@ -16,11 +19,9 @@ class PermissionsUpdater
|
|||||||
*/
|
*/
|
||||||
public function updateFromPermissionsForm(Entity $entity, Request $request)
|
public function updateFromPermissionsForm(Entity $entity, Request $request)
|
||||||
{
|
{
|
||||||
$restricted = $request->get('restricted') === 'true';
|
$permissions = $request->get('permissions', null);
|
||||||
$permissions = $request->get('restrictions', null);
|
|
||||||
$ownerId = $request->get('owned_by', null);
|
$ownerId = $request->get('owned_by', null);
|
||||||
|
|
||||||
$entity->restricted = $restricted;
|
|
||||||
$entity->permissions()->delete();
|
$entity->permissions()->delete();
|
||||||
|
|
||||||
if (!is_null($permissions)) {
|
if (!is_null($permissions)) {
|
||||||
@ -52,18 +53,43 @@ class PermissionsUpdater
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format permissions provided from a permission form to be
|
* Format permissions provided from a permission form to be EntityPermission data.
|
||||||
* EntityPermission data.
|
|
||||||
*/
|
*/
|
||||||
protected function formatPermissionsFromRequestToEntityPermissions(array $permissions): Collection
|
protected function formatPermissionsFromRequestToEntityPermissions(array $permissions): array
|
||||||
{
|
{
|
||||||
return collect($permissions)->flatMap(function ($restrictions, $roleId) {
|
$formatted = [];
|
||||||
return collect($restrictions)->keys()->map(function ($action) use ($roleId) {
|
|
||||||
return [
|
foreach ($permissions as $roleId => $info) {
|
||||||
'role_id' => $roleId,
|
$entityPermissionData = ['role_id' => $roleId];
|
||||||
'action' => strtolower($action),
|
foreach (EntityPermission::PERMISSIONS as $permission) {
|
||||||
];
|
$entityPermissionData[$permission] = (($info[$permission] ?? false) === "true");
|
||||||
});
|
}
|
||||||
});
|
$formatted[] = $entityPermissionData;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy down the permissions of the given shelf to all child books.
|
||||||
|
*/
|
||||||
|
public function updateBookPermissionsFromShelf(Bookshelf $shelf, $checkUserPermissions = true): int
|
||||||
|
{
|
||||||
|
$shelfPermissions = $shelf->permissions()->get(['role_id', 'view', 'create', 'update', 'delete'])->toArray();
|
||||||
|
$shelfBooks = $shelf->books()->get(['id', 'owned_by']);
|
||||||
|
$updatedBookCount = 0;
|
||||||
|
|
||||||
|
/** @var Book $book */
|
||||||
|
foreach ($shelfBooks as $book) {
|
||||||
|
if ($checkUserPermissions && !userCan('restrictions-manage', $book)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$book->permissions()->delete();
|
||||||
|
$book->permissions()->createMany($shelfPermissions);
|
||||||
|
$book->rebuildPermissions();
|
||||||
|
$updatedBookCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $updatedBookCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,6 @@ use BookStack\Entities\Repos\BookRepo;
|
|||||||
use BookStack\Entities\Tools\BookContents;
|
use BookStack\Entities\Tools\BookContents;
|
||||||
use BookStack\Entities\Tools\Cloner;
|
use BookStack\Entities\Tools\Cloner;
|
||||||
use BookStack\Entities\Tools\HierarchyTransformer;
|
use BookStack\Entities\Tools\HierarchyTransformer;
|
||||||
use BookStack\Entities\Tools\PermissionsUpdater;
|
|
||||||
use BookStack\Entities\Tools\ShelfContext;
|
use BookStack\Entities\Tools\ShelfContext;
|
||||||
use BookStack\Exceptions\ImageUploadException;
|
use BookStack\Exceptions\ImageUploadException;
|
||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Exceptions\NotFoundException;
|
||||||
@ -209,36 +208,6 @@ class BookController extends Controller
|
|||||||
return redirect('/books');
|
return redirect('/books');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the permissions view.
|
|
||||||
*/
|
|
||||||
public function showPermissions(string $bookSlug)
|
|
||||||
{
|
|
||||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
|
||||||
$this->checkOwnablePermission('restrictions-manage', $book);
|
|
||||||
|
|
||||||
return view('books.permissions', [
|
|
||||||
'book' => $book,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the restrictions for this book.
|
|
||||||
*
|
|
||||||
* @throws Throwable
|
|
||||||
*/
|
|
||||||
public function permissions(Request $request, PermissionsUpdater $permissionsUpdater, string $bookSlug)
|
|
||||||
{
|
|
||||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
|
||||||
$this->checkOwnablePermission('restrictions-manage', $book);
|
|
||||||
|
|
||||||
$permissionsUpdater->updateFromPermissionsForm($book, $request);
|
|
||||||
|
|
||||||
$this->showSuccessNotification(trans('entities.books_permissions_updated'));
|
|
||||||
|
|
||||||
return redirect($book->getUrl());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the view to copy a book.
|
* Show the view to copy a book.
|
||||||
*
|
*
|
||||||
|
@ -6,7 +6,6 @@ use BookStack\Actions\ActivityQueries;
|
|||||||
use BookStack\Actions\View;
|
use BookStack\Actions\View;
|
||||||
use BookStack\Entities\Models\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Repos\BookshelfRepo;
|
use BookStack\Entities\Repos\BookshelfRepo;
|
||||||
use BookStack\Entities\Tools\PermissionsUpdater;
|
|
||||||
use BookStack\Entities\Tools\ShelfContext;
|
use BookStack\Entities\Tools\ShelfContext;
|
||||||
use BookStack\Exceptions\ImageUploadException;
|
use BookStack\Exceptions\ImageUploadException;
|
||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Exceptions\NotFoundException;
|
||||||
@ -207,46 +206,4 @@ class BookshelfController extends Controller
|
|||||||
|
|
||||||
return redirect('/shelves');
|
return redirect('/shelves');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the permissions view.
|
|
||||||
*/
|
|
||||||
public function showPermissions(string $slug)
|
|
||||||
{
|
|
||||||
$shelf = $this->shelfRepo->getBySlug($slug);
|
|
||||||
$this->checkOwnablePermission('restrictions-manage', $shelf);
|
|
||||||
|
|
||||||
return view('shelves.permissions', [
|
|
||||||
'shelf' => $shelf,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the permissions for this bookshelf.
|
|
||||||
*/
|
|
||||||
public function permissions(Request $request, PermissionsUpdater $permissionsUpdater, string $slug)
|
|
||||||
{
|
|
||||||
$shelf = $this->shelfRepo->getBySlug($slug);
|
|
||||||
$this->checkOwnablePermission('restrictions-manage', $shelf);
|
|
||||||
|
|
||||||
$permissionsUpdater->updateFromPermissionsForm($shelf, $request);
|
|
||||||
|
|
||||||
$this->showSuccessNotification(trans('entities.shelves_permissions_updated'));
|
|
||||||
|
|
||||||
return redirect($shelf->getUrl());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy the permissions of a bookshelf to the child books.
|
|
||||||
*/
|
|
||||||
public function copyPermissions(string $slug)
|
|
||||||
{
|
|
||||||
$shelf = $this->shelfRepo->getBySlug($slug);
|
|
||||||
$this->checkOwnablePermission('restrictions-manage', $shelf);
|
|
||||||
|
|
||||||
$updateCount = $this->shelfRepo->copyDownPermissions($shelf);
|
|
||||||
$this->showSuccessNotification(trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
|
|
||||||
|
|
||||||
return redirect($shelf->getUrl());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,6 @@ use BookStack\Entities\Tools\BookContents;
|
|||||||
use BookStack\Entities\Tools\Cloner;
|
use BookStack\Entities\Tools\Cloner;
|
||||||
use BookStack\Entities\Tools\HierarchyTransformer;
|
use BookStack\Entities\Tools\HierarchyTransformer;
|
||||||
use BookStack\Entities\Tools\NextPreviousContentLocator;
|
use BookStack\Entities\Tools\NextPreviousContentLocator;
|
||||||
use BookStack\Entities\Tools\PermissionsUpdater;
|
|
||||||
use BookStack\Exceptions\MoveOperationException;
|
use BookStack\Exceptions\MoveOperationException;
|
||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Exceptions\NotFoundException;
|
||||||
use BookStack\Exceptions\PermissionsException;
|
use BookStack\Exceptions\PermissionsException;
|
||||||
@ -243,38 +242,6 @@ class ChapterController extends Controller
|
|||||||
return redirect($chapterCopy->getUrl());
|
return redirect($chapterCopy->getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the Restrictions view.
|
|
||||||
*
|
|
||||||
* @throws NotFoundException
|
|
||||||
*/
|
|
||||||
public function showPermissions(string $bookSlug, string $chapterSlug)
|
|
||||||
{
|
|
||||||
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
|
|
||||||
$this->checkOwnablePermission('restrictions-manage', $chapter);
|
|
||||||
|
|
||||||
return view('chapters.permissions', [
|
|
||||||
'chapter' => $chapter,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the restrictions for this chapter.
|
|
||||||
*
|
|
||||||
* @throws NotFoundException
|
|
||||||
*/
|
|
||||||
public function permissions(Request $request, PermissionsUpdater $permissionsUpdater, string $bookSlug, string $chapterSlug)
|
|
||||||
{
|
|
||||||
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
|
|
||||||
$this->checkOwnablePermission('restrictions-manage', $chapter);
|
|
||||||
|
|
||||||
$permissionsUpdater->updateFromPermissionsForm($chapter, $request);
|
|
||||||
|
|
||||||
$this->showSuccessNotification(trans('entities.chapters_permissions_success'));
|
|
||||||
|
|
||||||
return redirect($chapter->getUrl());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert the chapter to a book.
|
* Convert the chapter to a book.
|
||||||
*/
|
*/
|
||||||
|
@ -87,7 +87,7 @@ class FavouriteController extends Controller
|
|||||||
|
|
||||||
$modelInstance = $model->newQuery()
|
$modelInstance = $model->newQuery()
|
||||||
->where('id', '=', $modelInfo['id'])
|
->where('id', '=', $modelInfo['id'])
|
||||||
->first(['id', 'name', 'restricted', 'owned_by']);
|
->first(['id', 'name', 'owned_by']);
|
||||||
|
|
||||||
$inaccessibleEntity = ($modelInstance instanceof Entity && !userCan('view', $modelInstance));
|
$inaccessibleEntity = ($modelInstance instanceof Entity && !userCan('view', $modelInstance));
|
||||||
if (is_null($modelInstance) || $inaccessibleEntity) {
|
if (is_null($modelInstance) || $inaccessibleEntity) {
|
||||||
|
@ -11,7 +11,6 @@ use BookStack\Entities\Tools\NextPreviousContentLocator;
|
|||||||
use BookStack\Entities\Tools\PageContent;
|
use BookStack\Entities\Tools\PageContent;
|
||||||
use BookStack\Entities\Tools\PageEditActivity;
|
use BookStack\Entities\Tools\PageEditActivity;
|
||||||
use BookStack\Entities\Tools\PageEditorData;
|
use BookStack\Entities\Tools\PageEditorData;
|
||||||
use BookStack\Entities\Tools\PermissionsUpdater;
|
|
||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Exceptions\NotFoundException;
|
||||||
use BookStack\Exceptions\PermissionsException;
|
use BookStack\Exceptions\PermissionsException;
|
||||||
use BookStack\References\ReferenceFetcher;
|
use BookStack\References\ReferenceFetcher;
|
||||||
@ -452,37 +451,4 @@ class PageController extends Controller
|
|||||||
|
|
||||||
return redirect($pageCopy->getUrl());
|
return redirect($pageCopy->getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the Permissions view.
|
|
||||||
*
|
|
||||||
* @throws NotFoundException
|
|
||||||
*/
|
|
||||||
public function showPermissions(string $bookSlug, string $pageSlug)
|
|
||||||
{
|
|
||||||
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
|
|
||||||
$this->checkOwnablePermission('restrictions-manage', $page);
|
|
||||||
|
|
||||||
return view('pages.permissions', [
|
|
||||||
'page' => $page,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the permissions for this page.
|
|
||||||
*
|
|
||||||
* @throws NotFoundException
|
|
||||||
* @throws Throwable
|
|
||||||
*/
|
|
||||||
public function permissions(Request $request, PermissionsUpdater $permissionsUpdater, string $bookSlug, string $pageSlug)
|
|
||||||
{
|
|
||||||
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
|
|
||||||
$this->checkOwnablePermission('restrictions-manage', $page);
|
|
||||||
|
|
||||||
$permissionsUpdater->updateFromPermissionsForm($page, $request);
|
|
||||||
|
|
||||||
$this->showSuccessNotification(trans('entities.pages_permissions_success'));
|
|
||||||
|
|
||||||
return redirect($page->getUrl());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
174
app/Http/Controllers/PermissionsController.php
Normal file
174
app/Http/Controllers/PermissionsController.php
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
|
use BookStack\Auth\Permissions\EntityPermission;
|
||||||
|
use BookStack\Auth\Permissions\PermissionFormData;
|
||||||
|
use BookStack\Auth\Role;
|
||||||
|
use BookStack\Entities\Models\Book;
|
||||||
|
use BookStack\Entities\Models\Bookshelf;
|
||||||
|
use BookStack\Entities\Models\Chapter;
|
||||||
|
use BookStack\Entities\Models\Page;
|
||||||
|
use BookStack\Entities\Tools\PermissionsUpdater;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class PermissionsController extends Controller
|
||||||
|
{
|
||||||
|
protected PermissionsUpdater $permissionsUpdater;
|
||||||
|
|
||||||
|
public function __construct(PermissionsUpdater $permissionsUpdater)
|
||||||
|
{
|
||||||
|
$this->permissionsUpdater = $permissionsUpdater;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the Permissions view for a page.
|
||||||
|
*/
|
||||||
|
public function showForPage(string $bookSlug, string $pageSlug)
|
||||||
|
{
|
||||||
|
$page = Page::getBySlugs($bookSlug, $pageSlug);
|
||||||
|
$this->checkOwnablePermission('restrictions-manage', $page);
|
||||||
|
|
||||||
|
$this->setPageTitle(trans('entities.pages_permissions'));
|
||||||
|
return view('pages.permissions', [
|
||||||
|
'page' => $page,
|
||||||
|
'data' => new PermissionFormData($page),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the permissions for a page.
|
||||||
|
*/
|
||||||
|
public function updateForPage(Request $request, string $bookSlug, string $pageSlug)
|
||||||
|
{
|
||||||
|
$page = Page::getBySlugs($bookSlug, $pageSlug);
|
||||||
|
$this->checkOwnablePermission('restrictions-manage', $page);
|
||||||
|
|
||||||
|
$this->permissionsUpdater->updateFromPermissionsForm($page, $request);
|
||||||
|
|
||||||
|
$this->showSuccessNotification(trans('entities.pages_permissions_success'));
|
||||||
|
|
||||||
|
return redirect($page->getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the Restrictions view for a chapter.
|
||||||
|
*/
|
||||||
|
public function showForChapter(string $bookSlug, string $chapterSlug)
|
||||||
|
{
|
||||||
|
$chapter = Chapter::getBySlugs($bookSlug, $chapterSlug);
|
||||||
|
$this->checkOwnablePermission('restrictions-manage', $chapter);
|
||||||
|
|
||||||
|
$this->setPageTitle(trans('entities.chapters_permissions'));
|
||||||
|
return view('chapters.permissions', [
|
||||||
|
'chapter' => $chapter,
|
||||||
|
'data' => new PermissionFormData($chapter),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the restrictions for a chapter.
|
||||||
|
*/
|
||||||
|
public function updateForChapter(Request $request, string $bookSlug, string $chapterSlug)
|
||||||
|
{
|
||||||
|
$chapter = Chapter::getBySlugs($bookSlug, $chapterSlug);
|
||||||
|
$this->checkOwnablePermission('restrictions-manage', $chapter);
|
||||||
|
|
||||||
|
$this->permissionsUpdater->updateFromPermissionsForm($chapter, $request);
|
||||||
|
|
||||||
|
$this->showSuccessNotification(trans('entities.chapters_permissions_success'));
|
||||||
|
|
||||||
|
return redirect($chapter->getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the permissions view for a book.
|
||||||
|
*/
|
||||||
|
public function showForBook(string $slug)
|
||||||
|
{
|
||||||
|
$book = Book::getBySlug($slug);
|
||||||
|
$this->checkOwnablePermission('restrictions-manage', $book);
|
||||||
|
|
||||||
|
$this->setPageTitle(trans('entities.books_permissions'));
|
||||||
|
return view('books.permissions', [
|
||||||
|
'book' => $book,
|
||||||
|
'data' => new PermissionFormData($book),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the restrictions for a book.
|
||||||
|
*/
|
||||||
|
public function updateForBook(Request $request, string $slug)
|
||||||
|
{
|
||||||
|
$book = Book::getBySlug($slug);
|
||||||
|
$this->checkOwnablePermission('restrictions-manage', $book);
|
||||||
|
|
||||||
|
$this->permissionsUpdater->updateFromPermissionsForm($book, $request);
|
||||||
|
|
||||||
|
$this->showSuccessNotification(trans('entities.books_permissions_updated'));
|
||||||
|
|
||||||
|
return redirect($book->getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the permissions view for a shelf.
|
||||||
|
*/
|
||||||
|
public function showForShelf(string $slug)
|
||||||
|
{
|
||||||
|
$shelf = Bookshelf::getBySlug($slug);
|
||||||
|
$this->checkOwnablePermission('restrictions-manage', $shelf);
|
||||||
|
|
||||||
|
$this->setPageTitle(trans('entities.shelves_permissions'));
|
||||||
|
return view('shelves.permissions', [
|
||||||
|
'shelf' => $shelf,
|
||||||
|
'data' => new PermissionFormData($shelf),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the permissions for a shelf.
|
||||||
|
*/
|
||||||
|
public function updateForShelf(Request $request, string $slug)
|
||||||
|
{
|
||||||
|
$shelf = Bookshelf::getBySlug($slug);
|
||||||
|
$this->checkOwnablePermission('restrictions-manage', $shelf);
|
||||||
|
|
||||||
|
$this->permissionsUpdater->updateFromPermissionsForm($shelf, $request);
|
||||||
|
|
||||||
|
$this->showSuccessNotification(trans('entities.shelves_permissions_updated'));
|
||||||
|
|
||||||
|
return redirect($shelf->getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy the permissions of a bookshelf to the child books.
|
||||||
|
*/
|
||||||
|
public function copyShelfPermissionsToBooks(string $slug)
|
||||||
|
{
|
||||||
|
$shelf = Bookshelf::getBySlug($slug);
|
||||||
|
$this->checkOwnablePermission('restrictions-manage', $shelf);
|
||||||
|
|
||||||
|
$updateCount = $this->permissionsUpdater->updateBookPermissionsFromShelf($shelf);
|
||||||
|
$this->showSuccessNotification(trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
|
||||||
|
|
||||||
|
return redirect($shelf->getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an empty entity permissions form row for the given role.
|
||||||
|
*/
|
||||||
|
public function formRowForRole(string $entityType, string $roleId)
|
||||||
|
{
|
||||||
|
$this->checkPermissionOr('restrictions-manage-all', fn() => userCan('restrictions-manage-own'));
|
||||||
|
|
||||||
|
$role = Role::query()->findOrFail($roleId);
|
||||||
|
|
||||||
|
return view('form.entity-permissions-row', [
|
||||||
|
'role' => $role,
|
||||||
|
'permission' => new EntityPermission(),
|
||||||
|
'entityType' => $entityType,
|
||||||
|
'inheriting' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -22,8 +22,7 @@ class ReferenceController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function page(string $bookSlug, string $pageSlug)
|
public function page(string $bookSlug, string $pageSlug)
|
||||||
{
|
{
|
||||||
/** @var Page $page */
|
$page = Page::getBySlugs($bookSlug, $pageSlug);
|
||||||
$page = Page::visible()->whereSlugs($bookSlug, $pageSlug)->firstOrFail();
|
|
||||||
$references = $this->referenceFetcher->getPageReferencesToEntity($page);
|
$references = $this->referenceFetcher->getPageReferencesToEntity($page);
|
||||||
|
|
||||||
return view('pages.references', [
|
return view('pages.references', [
|
||||||
@ -37,8 +36,7 @@ class ReferenceController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function chapter(string $bookSlug, string $chapterSlug)
|
public function chapter(string $bookSlug, string $chapterSlug)
|
||||||
{
|
{
|
||||||
/** @var Chapter $chapter */
|
$chapter = Chapter::getBySlugs($bookSlug, $chapterSlug);
|
||||||
$chapter = Chapter::visible()->whereSlugs($bookSlug, $chapterSlug)->firstOrFail();
|
|
||||||
$references = $this->referenceFetcher->getPageReferencesToEntity($chapter);
|
$references = $this->referenceFetcher->getPageReferencesToEntity($chapter);
|
||||||
|
|
||||||
return view('chapters.references', [
|
return view('chapters.references', [
|
||||||
@ -52,7 +50,7 @@ class ReferenceController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function book(string $slug)
|
public function book(string $slug)
|
||||||
{
|
{
|
||||||
$book = Book::visible()->where('slug', '=', $slug)->firstOrFail();
|
$book = Book::getBySlug($slug);
|
||||||
$references = $this->referenceFetcher->getPageReferencesToEntity($book);
|
$references = $this->referenceFetcher->getPageReferencesToEntity($book);
|
||||||
|
|
||||||
return view('books.references', [
|
return view('books.references', [
|
||||||
@ -66,7 +64,7 @@ class ReferenceController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function shelf(string $slug)
|
public function shelf(string $slug)
|
||||||
{
|
{
|
||||||
$shelf = Bookshelf::visible()->where('slug', '=', $slug)->firstOrFail();
|
$shelf = Bookshelf::getBySlug($slug);
|
||||||
$references = $this->referenceFetcher->getPageReferencesToEntity($shelf);
|
$references = $this->referenceFetcher->getPageReferencesToEntity($shelf);
|
||||||
|
|
||||||
return view('shelves.references', [
|
return view('shelves.references', [
|
||||||
|
@ -162,7 +162,7 @@ class SearchRunner
|
|||||||
$entityQuery = $entityModelInstance->newQuery()->scopes('visible');
|
$entityQuery = $entityModelInstance->newQuery()->scopes('visible');
|
||||||
|
|
||||||
if ($entityModelInstance instanceof Page) {
|
if ($entityModelInstance instanceof Page) {
|
||||||
$entityQuery->select(array_merge($entityModelInstance::$listAttributes, ['restricted', 'owned_by']));
|
$entityQuery->select(array_merge($entityModelInstance::$listAttributes, ['owned_by']));
|
||||||
} else {
|
} else {
|
||||||
$entityQuery->select(['*']);
|
$entityQuery->select(['*']);
|
||||||
}
|
}
|
||||||
@ -447,7 +447,7 @@ class SearchRunner
|
|||||||
|
|
||||||
protected function filterIsRestricted(EloquentBuilder $query, Entity $model, $input)
|
protected function filterIsRestricted(EloquentBuilder $query, Entity $model, $input)
|
||||||
{
|
{
|
||||||
$query->where('restricted', '=', true);
|
$query->whereHas('permissions');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function filterViewedByMe(EloquentBuilder $query, Entity $model, $input)
|
protected function filterViewedByMe(EloquentBuilder $query, Entity $model, $input)
|
||||||
|
@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Query\Builder;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class FlattenEntityPermissionsTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
// Remove entries for non-existing roles (Caused by previous lack of deletion handling)
|
||||||
|
$roleIds = DB::table('roles')->pluck('id');
|
||||||
|
DB::table('entity_permissions')->whereNotIn('role_id', $roleIds)->delete();
|
||||||
|
|
||||||
|
// Create new table structure for entity_permissions
|
||||||
|
Schema::create('new_entity_permissions', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedInteger('entity_id');
|
||||||
|
$table->string('entity_type', 25);
|
||||||
|
$table->unsignedInteger('role_id')->index();
|
||||||
|
$table->boolean('view')->default(0);
|
||||||
|
$table->boolean('create')->default(0);
|
||||||
|
$table->boolean('update')->default(0);
|
||||||
|
$table->boolean('delete')->default(0);
|
||||||
|
|
||||||
|
$table->index(['entity_id', 'entity_type']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Migrate existing entity_permission data into new table structure
|
||||||
|
|
||||||
|
$subSelect = function (Builder $query, string $action, string $subAlias) {
|
||||||
|
$sub = $query->newQuery()->select('action')->from('entity_permissions', $subAlias)
|
||||||
|
->whereColumn('a.restrictable_id', '=', $subAlias . '.restrictable_id')
|
||||||
|
->whereColumn('a.restrictable_type', '=', $subAlias . '.restrictable_type')
|
||||||
|
->whereColumn('a.role_id', '=', $subAlias . '.role_id')
|
||||||
|
->where($subAlias . '.action', '=', $action);
|
||||||
|
return $query->selectRaw("EXISTS({$sub->toSql()})", $sub->getBindings());
|
||||||
|
};
|
||||||
|
|
||||||
|
$query = DB::table('entity_permissions', 'a')->select([
|
||||||
|
'restrictable_id as entity_id',
|
||||||
|
'restrictable_type as entity_type',
|
||||||
|
'role_id',
|
||||||
|
'view' => fn(Builder $query) => $subSelect($query, 'view', 'b'),
|
||||||
|
'create' => fn(Builder $query) => $subSelect($query, 'create', 'c'),
|
||||||
|
'update' => fn(Builder $query) => $subSelect($query, 'update', 'd'),
|
||||||
|
'delete' => fn(Builder $query) => $subSelect($query, 'delete', 'e'),
|
||||||
|
])->groupBy('restrictable_id', 'restrictable_type', 'role_id');
|
||||||
|
|
||||||
|
DB::table('new_entity_permissions')->insertUsing(['entity_id', 'entity_type', 'role_id', 'view', 'create', 'update', 'delete'], $query);
|
||||||
|
|
||||||
|
// Drop old entity_permissions table and replace with new structure
|
||||||
|
Schema::dropIfExists('entity_permissions');
|
||||||
|
Schema::rename('new_entity_permissions', 'entity_permissions');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
// Create old table structure for entity_permissions
|
||||||
|
Schema::create('old_entity_permissions', function (Blueprint $table) {
|
||||||
|
$table->increments('id');
|
||||||
|
$table->integer('restrictable_id');
|
||||||
|
$table->string('restrictable_type', 191);
|
||||||
|
$table->integer('role_id')->index();
|
||||||
|
$table->string('action', 191)->index();
|
||||||
|
|
||||||
|
$table->index(['restrictable_id', 'restrictable_type']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert newer data format to old data format, and insert into old database
|
||||||
|
|
||||||
|
$actionQuery = function (Builder $query, string $action) {
|
||||||
|
return $query->select([
|
||||||
|
'entity_id as restrictable_id',
|
||||||
|
'entity_type as restrictable_type',
|
||||||
|
'role_id',
|
||||||
|
])->selectRaw("? as action", [$action])
|
||||||
|
->from('entity_permissions')
|
||||||
|
->where($action, '=', true);
|
||||||
|
};
|
||||||
|
|
||||||
|
$query = $actionQuery(DB::query(), 'view')
|
||||||
|
->union(fn(Builder $query) => $actionQuery($query, 'create'))
|
||||||
|
->union(fn(Builder $query) => $actionQuery($query, 'update'))
|
||||||
|
->union(fn(Builder $query) => $actionQuery($query, 'delete'));
|
||||||
|
|
||||||
|
DB::table('old_entity_permissions')->insertUsing(['restrictable_id', 'restrictable_type', 'role_id', 'action'], $query);
|
||||||
|
|
||||||
|
// Drop new entity_permissions table and replace with old structure
|
||||||
|
Schema::dropIfExists('entity_permissions');
|
||||||
|
Schema::rename('old_entity_permissions', 'entity_permissions');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Query\Builder;
|
||||||
|
use Illuminate\Database\Query\JoinClause;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class DropEntityRestrictedField extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
// Remove entity-permissions on non-restricted entities
|
||||||
|
$deleteInactiveEntityPermissions = function (string $table, string $morphClass) {
|
||||||
|
$permissionIds = DB::table('entity_permissions')->select('entity_permissions.id as id')
|
||||||
|
->join($table, function (JoinClause $join) use ($table, $morphClass) {
|
||||||
|
return $join->where($table . '.restricted', '=', 0)
|
||||||
|
->on($table . '.id', '=', 'entity_permissions.entity_id');
|
||||||
|
})->where('entity_type', '=', $morphClass)
|
||||||
|
->pluck('id');
|
||||||
|
DB::table('entity_permissions')->whereIn('id', $permissionIds)->delete();
|
||||||
|
};
|
||||||
|
$deleteInactiveEntityPermissions('pages', 'page');
|
||||||
|
$deleteInactiveEntityPermissions('chapters', 'chapter');
|
||||||
|
$deleteInactiveEntityPermissions('books', 'book');
|
||||||
|
$deleteInactiveEntityPermissions('bookshelves', 'bookshelf');
|
||||||
|
|
||||||
|
// Migrate restricted=1 entries to new entity_permissions (role_id=0) entries
|
||||||
|
$defaultEntityPermissionGenQuery = function (Builder $query, string $table, string $morphClass) {
|
||||||
|
return $query->select(['id as entity_id'])
|
||||||
|
->selectRaw('? as entity_type', [$morphClass])
|
||||||
|
->selectRaw('? as `role_id`', [0])
|
||||||
|
->selectRaw('? as `view`', [0])
|
||||||
|
->selectRaw('? as `create`', [0])
|
||||||
|
->selectRaw('? as `update`', [0])
|
||||||
|
->selectRaw('? as `delete`', [0])
|
||||||
|
->from($table)
|
||||||
|
->where('restricted', '=', 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
$query = $defaultEntityPermissionGenQuery(DB::query(), 'pages', 'page')
|
||||||
|
->union(fn(Builder $query) => $defaultEntityPermissionGenQuery($query, 'books', 'book'))
|
||||||
|
->union(fn(Builder $query) => $defaultEntityPermissionGenQuery($query, 'chapters', 'chapter'))
|
||||||
|
->union(fn(Builder $query) => $defaultEntityPermissionGenQuery($query, 'bookshelves', 'bookshelf'));
|
||||||
|
|
||||||
|
DB::table('entity_permissions')->insertUsing(['entity_id', 'entity_type', 'role_id', 'view', 'create', 'update', 'delete'], $query);
|
||||||
|
|
||||||
|
// Drop restricted columns
|
||||||
|
$dropRestrictedColumn = fn(Blueprint $table) => $table->dropColumn('restricted');
|
||||||
|
Schema::table('pages', $dropRestrictedColumn);
|
||||||
|
Schema::table('chapters', $dropRestrictedColumn);
|
||||||
|
Schema::table('books', $dropRestrictedColumn);
|
||||||
|
Schema::table('bookshelves', $dropRestrictedColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
// Create restricted columns
|
||||||
|
$createRestrictedColumn = fn(Blueprint $table) => $table->boolean('restricted')->index()->default(0);
|
||||||
|
Schema::table('pages', $createRestrictedColumn);
|
||||||
|
Schema::table('chapters', $createRestrictedColumn);
|
||||||
|
Schema::table('books', $createRestrictedColumn);
|
||||||
|
Schema::table('bookshelves', $createRestrictedColumn);
|
||||||
|
|
||||||
|
// Set restrictions for entities that have a default entity permission assigned
|
||||||
|
// Note: Possible loss of data where default entity permissions have been configured
|
||||||
|
$restrictEntities = function (string $table, string $morphClass) {
|
||||||
|
$toRestrictIds = DB::table('entity_permissions')
|
||||||
|
->where('role_id', '=', 0)
|
||||||
|
->where('entity_type', '=', $morphClass)
|
||||||
|
->pluck('entity_id');
|
||||||
|
DB::table($table)->whereIn('id', $toRestrictIds)->update(['restricted' => true]);
|
||||||
|
};
|
||||||
|
$restrictEntities('pages', 'page');
|
||||||
|
$restrictEntities('chapters', 'chapter');
|
||||||
|
$restrictEntities('books', 'book');
|
||||||
|
$restrictEntities('bookshelves', 'bookshelf');
|
||||||
|
|
||||||
|
// Delete default entity permissions
|
||||||
|
DB::table('entity_permissions')->where('role_id', '=', 0)->delete();
|
||||||
|
}
|
||||||
|
}
|
1
resources/icons/groups.svg
Normal file
1
resources/icons/groups.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24px"><g><path d="M12,12.75c1.63,0,3.07,0.39,4.24,0.9c1.08,0.48,1.76,1.56,1.76,2.73L18,17c0,0.55-0.45,1-1,1H7c-0.55,0-1-0.45-1-1l0-0.61 c0-1.18,0.68-2.26,1.76-2.73C8.93,13.14,10.37,12.75,12,12.75z M4,13c1.1,0,2-0.9,2-2c0-1.1-0.9-2-2-2s-2,0.9-2,2 C2,12.1,2.9,13,4,13z M5.13,14.1C4.76,14.04,4.39,14,4,14c-0.99,0-1.93,0.21-2.78,0.58C0.48,14.9,0,15.62,0,16.43L0,17 c0,0.55,0.45,1,1,1l3.5,0v-1.61C4.5,15.56,4.73,14.78,5.13,14.1z M20,13c1.1,0,2-0.9,2-2c0-1.1-0.9-2-2-2s-2,0.9-2,2 C18,12.1,18.9,13,20,13z M24,16.43c0-0.81-0.48-1.53-1.22-1.85C21.93,14.21,20.99,14,20,14c-0.39,0-0.76,0.04-1.13,0.1 c0.4,0.68,0.63,1.46,0.63,2.29V18l3.5,0c0.55,0,1-0.45,1-1L24,16.43z M12,6c1.66,0,3,1.34,3,3c0,1.66-1.34,3-3,3s-3-1.34-3-3 C9,7.34,10.34,6,12,6z"/></g></svg>
|
After Width: | Height: | Size: 811 B |
4
resources/icons/role.svg
Normal file
4
resources/icons/role.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 415 B |
@ -1,20 +0,0 @@
|
|||||||
|
|
||||||
class EntityPermissionsEditor {
|
|
||||||
|
|
||||||
constructor(elem) {
|
|
||||||
this.permissionsTable = elem.querySelector('[permissions-table]');
|
|
||||||
|
|
||||||
// Handle toggle all event
|
|
||||||
this.restrictedCheckbox = elem.querySelector('[name=restricted]');
|
|
||||||
this.restrictedCheckbox.addEventListener('change', this.updateTableVisibility.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTableVisibility() {
|
|
||||||
this.permissionsTable.style.display =
|
|
||||||
this.restrictedCheckbox.checked
|
|
||||||
? null
|
|
||||||
: 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EntityPermissionsEditor;
|
|
80
resources/js/components/entity-permissions.js
Normal file
80
resources/js/components/entity-permissions.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* @extends {Component}
|
||||||
|
*/
|
||||||
|
class EntityPermissions {
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.container = this.$el;
|
||||||
|
this.entityType = this.$opts.entityType;
|
||||||
|
|
||||||
|
this.everyoneInheritToggle = this.$refs.everyoneInherit;
|
||||||
|
this.roleSelect = this.$refs.roleSelect;
|
||||||
|
this.roleContainer = this.$refs.roleContainer;
|
||||||
|
|
||||||
|
this.setupListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupListeners() {
|
||||||
|
// "Everyone Else" inherit toggle
|
||||||
|
this.everyoneInheritToggle.addEventListener('change', event => {
|
||||||
|
const inherit = event.target.checked;
|
||||||
|
const permissions = document.querySelectorAll('input[name^="permissions[0]["]');
|
||||||
|
for (const permission of permissions) {
|
||||||
|
permission.disabled = inherit;
|
||||||
|
permission.checked = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove role row button click
|
||||||
|
this.container.addEventListener('click', event => {
|
||||||
|
const button = event.target.closest('button');
|
||||||
|
if (button && button.dataset.roleId) {
|
||||||
|
this.removeRowOnButtonClick(button)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Role select change
|
||||||
|
this.roleSelect.addEventListener('change', event => {
|
||||||
|
const roleId = this.roleSelect.value;
|
||||||
|
if (roleId) {
|
||||||
|
this.addRoleRow(roleId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async addRoleRow(roleId) {
|
||||||
|
this.roleSelect.disabled = true;
|
||||||
|
|
||||||
|
// Remove option from select
|
||||||
|
const option = this.roleSelect.querySelector(`option[value="${roleId}"]`);
|
||||||
|
if (option) {
|
||||||
|
option.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get and insert new row
|
||||||
|
const resp = await window.$http.get(`/permissions/form-row/${this.entityType}/${roleId}`);
|
||||||
|
const wrap = document.createElement('div');
|
||||||
|
wrap.innerHTML = resp.data;
|
||||||
|
const row = wrap.children[0];
|
||||||
|
this.roleContainer.append(row);
|
||||||
|
window.components.init(row);
|
||||||
|
|
||||||
|
this.roleSelect.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeRowOnButtonClick(button) {
|
||||||
|
const row = button.closest('.content-permissions-row');
|
||||||
|
const roleId = button.dataset.roleId;
|
||||||
|
const roleName = button.dataset.roleName;
|
||||||
|
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = roleId;
|
||||||
|
option.textContent = roleName;
|
||||||
|
|
||||||
|
this.roleSelect.append(option);
|
||||||
|
row.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EntityPermissions;
|
@ -18,7 +18,7 @@ import dropdown from "./dropdown.js"
|
|||||||
import dropdownSearch from "./dropdown-search.js"
|
import dropdownSearch from "./dropdown-search.js"
|
||||||
import dropzone from "./dropzone.js"
|
import dropzone from "./dropzone.js"
|
||||||
import editorToolbox from "./editor-toolbox.js"
|
import editorToolbox from "./editor-toolbox.js"
|
||||||
import entityPermissionsEditor from "./entity-permissions-editor.js"
|
import entityPermissions from "./entity-permissions";
|
||||||
import entitySearch from "./entity-search.js"
|
import entitySearch from "./entity-search.js"
|
||||||
import entitySelector from "./entity-selector.js"
|
import entitySelector from "./entity-selector.js"
|
||||||
import entitySelectorPopup from "./entity-selector-popup.js"
|
import entitySelectorPopup from "./entity-selector-popup.js"
|
||||||
@ -75,7 +75,7 @@ const componentMapping = {
|
|||||||
"dropdown-search": dropdownSearch,
|
"dropdown-search": dropdownSearch,
|
||||||
"dropzone": dropzone,
|
"dropzone": dropzone,
|
||||||
"editor-toolbox": editorToolbox,
|
"editor-toolbox": editorToolbox,
|
||||||
"entity-permissions-editor": entityPermissionsEditor,
|
"entity-permissions": entityPermissions,
|
||||||
"entity-search": entitySearch,
|
"entity-search": entitySearch,
|
||||||
"entity-selector": entitySelector,
|
"entity-selector": entitySelector,
|
||||||
"entity-selector-popup": entitySelectorPopup,
|
"entity-selector-popup": entitySelectorPopup,
|
||||||
|
@ -1,22 +1,21 @@
|
|||||||
|
|
||||||
class PermissionsTable {
|
class PermissionsTable {
|
||||||
|
|
||||||
constructor(elem) {
|
setup() {
|
||||||
this.container = elem;
|
this.container = this.$el;
|
||||||
|
|
||||||
// Handle toggle all event
|
// Handle toggle all event
|
||||||
const toggleAll = elem.querySelector('[permissions-table-toggle-all]');
|
for (const toggleAllElem of (this.$manyRefs.toggleAll || [])) {
|
||||||
toggleAll.addEventListener('click', this.toggleAllClick.bind(this));
|
toggleAllElem.addEventListener('click', this.toggleAllClick.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
// Handle toggle row event
|
// Handle toggle row event
|
||||||
const toggleRowElems = elem.querySelectorAll('[permissions-table-toggle-all-in-row]');
|
for (const toggleRowElem of (this.$manyRefs.toggleRow || [])) {
|
||||||
for (let toggleRowElem of toggleRowElems) {
|
|
||||||
toggleRowElem.addEventListener('click', this.toggleRowClick.bind(this));
|
toggleRowElem.addEventListener('click', this.toggleRowClick.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle toggle column event
|
// Handle toggle column event
|
||||||
const toggleColumnElems = elem.querySelectorAll('[permissions-table-toggle-all-in-column]');
|
for (const toggleColElem of (this.$manyRefs.toggleColumn || [])) {
|
||||||
for (let toggleColElem of toggleColumnElems) {
|
|
||||||
toggleColElem.addEventListener('click', this.toggleColumnClick.bind(this));
|
toggleColElem.addEventListener('click', this.toggleColumnClick.bind(this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,10 +42,14 @@ return [
|
|||||||
|
|
||||||
// Permissions and restrictions
|
// Permissions and restrictions
|
||||||
'permissions' => 'Permissions',
|
'permissions' => 'Permissions',
|
||||||
'permissions_intro' => 'Once enabled, These permissions will take priority over any set role permissions.',
|
'permissions_desc' => 'Set permissions here to override the default permissions provided by user roles.',
|
||||||
'permissions_enable' => 'Enable Custom Permissions',
|
'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.',
|
||||||
|
'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.',
|
||||||
'permissions_save' => 'Save Permissions',
|
'permissions_save' => 'Save Permissions',
|
||||||
'permissions_owner' => 'Owner',
|
'permissions_owner' => 'Owner',
|
||||||
|
'permissions_role_everyone_else' => 'Everyone Else',
|
||||||
|
'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
|
||||||
|
'permissions_role_override' => 'Override permissions for role',
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
'search_results' => 'Search Results',
|
'search_results' => 'Search Results',
|
||||||
|
@ -48,9 +48,10 @@ button {
|
|||||||
|
|
||||||
.button.outline {
|
.button.outline {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@include lightDark(color, #666, #aaa);
|
@include lightDark(color, #666, #AAA);
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
border: 1px solid #CCC;
|
border: 1px solid;
|
||||||
|
@include lightDark(border-color, #CCC, #666);
|
||||||
&:hover, &:focus, &:active {
|
&:hover, &:focus, &:active {
|
||||||
border: 1px solid #CCC;
|
border: 1px solid #CCC;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
@ -109,12 +110,23 @@ button {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.icon {
|
.button.icon, .icon-button {
|
||||||
.svg-icon {
|
.svg-icon {
|
||||||
margin-inline-end: 0;
|
margin-inline-end: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-button {
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
.icon-button:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
border-radius: 4px;
|
||||||
|
@include lightDark(border-color, #DDD, #444);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.button.svg {
|
.button.svg {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -798,11 +798,35 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
|||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.permissions-table [permissions-table-toggle-all-in-row] {
|
.content-permissions {
|
||||||
display: none;
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
.permissions-table tr:hover [permissions-table-toggle-all-in-row] {
|
.content-permissions-row {
|
||||||
display: inline;
|
border: 1.5px solid;
|
||||||
|
@include lightDark(border-color, #E2E2E2, #444);
|
||||||
|
border-bottom-width: 0;
|
||||||
|
label {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
@include lightDark(background-color, #F2F2F2, #333);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.content-permissions-row:first-child {
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
}
|
||||||
|
.content-permissions-row:last-child {
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
border-bottom-width: 1.5px;
|
||||||
|
}
|
||||||
|
.content-permissions-row:first-child:last-child {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.content-permissions-row-toggle-all {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.content-permissions-row:hover .content-permissions-row-toggle-all {
|
||||||
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-item {
|
.template-item {
|
||||||
@ -857,7 +881,8 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
|||||||
gap: $-s;
|
gap: $-s;
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
.svg-icon {
|
.svg-icon {
|
||||||
height: 16px;
|
height: 26px;
|
||||||
|
width: 26px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
.avatar {
|
.avatar {
|
||||||
@ -879,10 +904,11 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.dropdown-search-toggle-select-caret {
|
.dropdown-search-toggle-select-caret {
|
||||||
font-size: 1.5rem;
|
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-top: -2px;
|
margin-top: -2px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-search-dropdown {
|
.dropdown-search-dropdown {
|
||||||
|
@ -207,8 +207,8 @@ select {
|
|||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' fill='%23666666'><polygon points='0,0 100,0 50,50'/></svg>");
|
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' fill='%23666666'><polygon points='0,0 100,0 50,50'/></svg>");
|
||||||
background-size: 12px;
|
background-size: 10px 12px;
|
||||||
background-position: calc(100% - 20px) 70%;
|
background-position: calc(100% - 20px) 64%;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
|
|
||||||
@include rtl {
|
@include rtl {
|
||||||
@ -266,6 +266,15 @@ input[type=color] {
|
|||||||
background-color: rgba(0, 0, 0, 0.05);
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
input[type=checkbox][disabled] ~ * {
|
||||||
|
opacity: 0.8;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
input[type=checkbox][disabled] ~ .custom-checkbox {
|
||||||
|
border-color: #999;
|
||||||
|
color: #999 !important;
|
||||||
|
background: #f2f2f2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.toggle-switch-list {
|
.toggle-switch-list {
|
||||||
.toggle-switch {
|
.toggle-switch {
|
||||||
|
@ -158,8 +158,8 @@ body.flexbox {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.gap-m {
|
.flex-none {
|
||||||
gap: $-m;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.justify-flex-start {
|
.justify-flex-start {
|
||||||
|
@ -29,4 +29,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
@include spacing('margin', 'm');
|
@include spacing('margin', 'm');
|
||||||
@include spacing('padding', 'p');
|
@include spacing('padding', 'p');
|
||||||
|
|
||||||
|
@each $sizeLetter, $size in $spacing {
|
||||||
|
.gap-#{$sizeLetter} {
|
||||||
|
gap: $size !important;
|
||||||
|
}
|
||||||
|
.gap-x-#{$sizeLetter} {
|
||||||
|
column-gap: $size !important;
|
||||||
|
}
|
||||||
|
.gap-y-#{$sizeLetter} {
|
||||||
|
row-gap: $size !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -14,9 +14,8 @@
|
|||||||
]])
|
]])
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main class="card content-wrap">
|
<main class="card content-wrap auto-height">
|
||||||
<h1 class="list-heading">{{ trans('entities.books_permissions') }}</h1>
|
@include('form.entity-permissions', ['model' => $book, 'title' => trans('entities.books_permissions')])
|
||||||
@include('form.entity-permissions', ['model' => $book])
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@
|
|||||||
<h5>{{ trans('common.details') }}</h5>
|
<h5>{{ trans('common.details') }}</h5>
|
||||||
<div class="blended-links">
|
<div class="blended-links">
|
||||||
@include('entities.meta', ['entity' => $book])
|
@include('entities.meta', ['entity' => $book])
|
||||||
@if($book->restricted)
|
@if($book->hasPermissions())
|
||||||
<div class="active-restriction">
|
<div class="active-restriction">
|
||||||
@if(userCan('restrictions-manage', $book))
|
@if(userCan('restrictions-manage', $book))
|
||||||
<a href="{{ $book->getUrl('/permissions') }}" class="entity-meta-item">
|
<a href="{{ $book->getUrl('/permissions') }}" class="entity-meta-item">
|
||||||
|
@ -15,9 +15,8 @@
|
|||||||
]])
|
]])
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main class="card content-wrap">
|
<main class="card content-wrap auto-height">
|
||||||
<h1 class="list-heading">{{ trans('entities.chapters_permissions') }}</h1>
|
@include('form.entity-permissions', ['model' => $chapter, 'title' => trans('entities.chapters_permissions')])
|
||||||
@include('form.entity-permissions', ['model' => $chapter])
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@
|
|||||||
<div class="blended-links">
|
<div class="blended-links">
|
||||||
@include('entities.meta', ['entity' => $chapter])
|
@include('entities.meta', ['entity' => $chapter])
|
||||||
|
|
||||||
@if($book->restricted)
|
@if($book->hasPermissions())
|
||||||
<div class="active-restriction">
|
<div class="active-restriction">
|
||||||
@if(userCan('restrictions-manage', $book))
|
@if(userCan('restrictions-manage', $book))
|
||||||
<a href="{{ $book->getUrl('/permissions') }}" class="entity-meta-item">
|
<a href="{{ $book->getUrl('/permissions') }}" class="entity-meta-item">
|
||||||
@ -85,7 +85,7 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if($chapter->restricted)
|
@if($chapter->hasPermissions())
|
||||||
<div class="active-restriction">
|
<div class="active-restriction">
|
||||||
@if(userCan('restrictions-manage', $chapter))
|
@if(userCan('restrictions-manage', $chapter))
|
||||||
<a href="{{ $chapter->getUrl('/permissions') }}" class="entity-meta-item">
|
<a href="{{ $chapter->getUrl('/permissions') }}" class="entity-meta-item">
|
||||||
|
@ -5,7 +5,7 @@ $checked
|
|||||||
$label
|
$label
|
||||||
--}}
|
--}}
|
||||||
<label custom-checkbox class="toggle-switch @if($errors->has($name)) text-neg @endif">
|
<label custom-checkbox class="toggle-switch @if($errors->has($name)) text-neg @endif">
|
||||||
<input type="checkbox" name="{{$name}}" value="{{ $value }}" @if($checked) checked="checked" @endif>
|
<input type="checkbox" name="{{$name}}" value="{{ $value }}" @if($checked) checked="checked" @endif @if($disabled ?? false) disabled="disabled" @endif>
|
||||||
<span tabindex="0" role="checkbox"
|
<span tabindex="0" role="checkbox"
|
||||||
aria-checked="{{ $checked ? 'true' : 'false' }}"
|
aria-checked="{{ $checked ? 'true' : 'false' }}"
|
||||||
class="custom-checkbox text-primary">@icon('check')</span>
|
class="custom-checkbox text-primary">@icon('check')</span>
|
||||||
|
88
resources/views/form/entity-permissions-row.blade.php
Normal file
88
resources/views/form/entity-permissions-row.blade.php
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
{{--
|
||||||
|
$role - The Role to display this row for.
|
||||||
|
$entityType - String identifier for type of entity having permissions applied.
|
||||||
|
$permission - The entity permission containing the permissions.
|
||||||
|
$inheriting - Boolean if the current row should be marked as inheriting default permissions. Used for "Everyone Else" role.
|
||||||
|
--}}
|
||||||
|
|
||||||
|
<div component="permissions-table" class="content-permissions-row flex-container-row justify-space-between wrap">
|
||||||
|
<div class="gap-x-m flex-container-row items-center px-l py-m flex">
|
||||||
|
<div class="text-large" title="{{ $role->id === 0 ? trans('entities.permissions_role_everyone_else') : trans('common.role') }}">
|
||||||
|
@icon($role->id === 0 ? 'groups' : 'role')
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
<strong>{{ $role->display_name }}</strong> <br>
|
||||||
|
<small class="text-muted">{{ $role->description }}</small>
|
||||||
|
</span>
|
||||||
|
@if($role->id !== 0)
|
||||||
|
<button type="button"
|
||||||
|
class="ml-auto flex-none text-small text-primary text-button hover-underline content-permissions-row-toggle-all hide-under-s"
|
||||||
|
refs="permissions-table@toggle-all"
|
||||||
|
><strong>{{ trans('common.toggle_all') }}</strong></button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@if($role->id === 0)
|
||||||
|
<div class="px-l flex-container-row items-center" refs="entity-permissions@everyone-inherit">
|
||||||
|
@include('form.custom-checkbox', [
|
||||||
|
'name' => 'entity-permissions-inherit',
|
||||||
|
'label' => 'Inherit defaults',
|
||||||
|
'value' => 'true',
|
||||||
|
'checked' => $inheriting
|
||||||
|
])
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
<div class="flex-container-row justify-space-between gap-x-xl wrap items-center">
|
||||||
|
<input type="hidden" name="permissions[{{ $role->id }}][active]"
|
||||||
|
@if($inheriting) disabled="disabled" @endif
|
||||||
|
value="true">
|
||||||
|
<div class="px-l">
|
||||||
|
@include('form.custom-checkbox', [
|
||||||
|
'name' => 'permissions[' . $role->id . '][view]',
|
||||||
|
'label' => trans('common.view'),
|
||||||
|
'value' => 'true',
|
||||||
|
'checked' => $permission->view,
|
||||||
|
'disabled' => $inheriting
|
||||||
|
])
|
||||||
|
</div>
|
||||||
|
@if($entityType !== 'page')
|
||||||
|
<div class="px-l">
|
||||||
|
@include('form.custom-checkbox', [
|
||||||
|
'name' => 'permissions[' . $role->id . '][create]',
|
||||||
|
'label' => trans('common.create'),
|
||||||
|
'value' => 'true',
|
||||||
|
'checked' => $permission->create,
|
||||||
|
'disabled' => $inheriting
|
||||||
|
])
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
<div class="px-l">
|
||||||
|
@include('form.custom-checkbox', [
|
||||||
|
'name' => 'permissions[' . $role->id . '][update]',
|
||||||
|
'label' => trans('common.update'),
|
||||||
|
'value' => 'true',
|
||||||
|
'checked' => $permission->update,
|
||||||
|
'disabled' => $inheriting
|
||||||
|
])
|
||||||
|
</div>
|
||||||
|
<div class="px-l">
|
||||||
|
@include('form.custom-checkbox', [
|
||||||
|
'name' => 'permissions[' . $role->id . '][delete]',
|
||||||
|
'label' => trans('common.delete'),
|
||||||
|
'value' => 'true',
|
||||||
|
'checked' => $permission->delete,
|
||||||
|
'disabled' => $inheriting
|
||||||
|
])
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if($role->id !== 0)
|
||||||
|
<div class="flex-container-row items-center px-m py-s">
|
||||||
|
<button type="button"
|
||||||
|
class="text-neg p-m icon-button"
|
||||||
|
data-role-id="{{ $role->id }}"
|
||||||
|
data-role-name="{{ $role->display_name }}"
|
||||||
|
title="{{ trans('common.remove') }}">
|
||||||
|
@icon('close') <span class="hide-over-m ml-xs">{{ trans('common.remove') }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
@ -1,54 +1,73 @@
|
|||||||
<form action="{{ $model->getUrl('/permissions') }}" method="POST" entity-permissions-editor>
|
<?php
|
||||||
|
/** @var \BookStack\Auth\Permissions\PermissionFormData $data */
|
||||||
|
?>
|
||||||
|
<form component="entity-permissions"
|
||||||
|
option:entity-permissions:entity-type="{{ $model->getType() }}"
|
||||||
|
action="{{ $model->getUrl('/permissions') }}"
|
||||||
|
method="POST">
|
||||||
{!! csrf_field() !!}
|
{!! csrf_field() !!}
|
||||||
<input type="hidden" name="_method" value="PUT">
|
<input type="hidden" name="_method" value="PUT">
|
||||||
|
|
||||||
<div class="grid half left-focus v-center">
|
<div class="grid half left-focus v-end gap-m wrap">
|
||||||
<div>
|
<div>
|
||||||
<p class="mb-none mt-m">{{ trans('entities.permissions_intro') }}</p>
|
<h1 class="list-heading">{{ $title }}</h1>
|
||||||
<div>
|
<p class="text-muted mb-s">
|
||||||
@include('form.checkbox', [
|
{{ trans('entities.permissions_desc') }}
|
||||||
'name' => 'restricted',
|
|
||||||
'label' => trans('entities.permissions_enable'),
|
@if($model instanceof \BookStack\Entities\Models\Book)
|
||||||
])
|
<br> {{ trans('entities.permissions_book_cascade') }}
|
||||||
</div>
|
@elseif($model instanceof \BookStack\Entities\Models\Chapter)
|
||||||
|
<br> {{ trans('entities.permissions_chapter_cascade') }}
|
||||||
|
@endif
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@if($model instanceof \BookStack\Entities\Models\Bookshelf)
|
||||||
|
<p class="text-warn">{{ trans('entities.shelves_permissions_cascade_warning') }}</p>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="flex-container-row justify-flex-end">
|
||||||
<div class="form-group">
|
<div class="form-group mb-m">
|
||||||
<label for="owner">{{ trans('entities.permissions_owner') }}</label>
|
<label for="owner">{{ trans('entities.permissions_owner') }}</label>
|
||||||
@include('form.user-select', ['user' => $model->ownedBy, 'name' => 'owned_by'])
|
@include('form.user-select', ['user' => $model->ownedBy, 'name' => 'owned_by'])
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if($model instanceof \BookStack\Entities\Models\Bookshelf)
|
|
||||||
<p class="text-warn">{{ trans('entities.shelves_permissions_cascade_warning') }}</p>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<table permissions-table class="table permissions-table toggle-switch-list" style="{{ !$model->restricted ? 'display: none' : '' }}">
|
<div refs="entity-permissions@role-container" class="content-permissions mt-m mb-m">
|
||||||
<tr>
|
@foreach($data->permissionsWithRoles() as $permission)
|
||||||
<th>{{ trans('common.role') }}</th>
|
@include('form.entity-permissions-row', [
|
||||||
<th colspan="{{ $model->isA('page') ? '3' : '4' }}">
|
'permission' => $permission,
|
||||||
{{ trans('common.actions') }}
|
'role' => $permission->role,
|
||||||
<a href="#" permissions-table-toggle-all class="text-small ml-m text-primary">{{ trans('common.toggle_all') }}</a>
|
'entityType' => $model->getType(),
|
||||||
</th>
|
'inheriting' => false,
|
||||||
</tr>
|
])
|
||||||
@foreach(\BookStack\Auth\Role::restrictable() as $role)
|
|
||||||
<tr>
|
|
||||||
<td width="33%" class="pt-m">
|
|
||||||
{{ $role->display_name }}
|
|
||||||
<a href="#" permissions-table-toggle-all-in-row class="text-small float right ml-m text-primary">{{ trans('common.toggle_all') }}</a>
|
|
||||||
</td>
|
|
||||||
<td>@include('form.restriction-checkbox', ['name'=>'restrictions', 'label' => trans('common.view'), 'action' => 'view'])</td>
|
|
||||||
@if(!$model->isA('page'))
|
|
||||||
<td>@include('form.restriction-checkbox', ['name'=>'restrictions', 'label' => trans('common.create'), 'action' => 'create'])</td>
|
|
||||||
@endif
|
|
||||||
<td>@include('form.restriction-checkbox', ['name'=>'restrictions', 'label' => trans('common.update'), 'action' => 'update'])</td>
|
|
||||||
<td>@include('form.restriction-checkbox', ['name'=>'restrictions', 'label' => trans('common.delete'), 'action' => 'delete'])</td>
|
|
||||||
</tr>
|
|
||||||
@endforeach
|
@endforeach
|
||||||
</table>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-container-row justify-flex-end mb-xl">
|
||||||
|
<div class="flex-container-row items-center gap-m">
|
||||||
|
<label for="role_select" class="m-none p-none"><span class="bold">{{ trans('entities.permissions_role_override') }}</span></label>
|
||||||
|
<select name="role_select" id="role_select" refs="entity-permissions@role-select">
|
||||||
|
<option value="">{{ trans('common.select') }}</option>
|
||||||
|
@foreach($data->rolesNotAssigned() as $role)
|
||||||
|
<option value="{{ $role->id }}">{{ $role->display_name }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-permissions mt-m mb-xl">
|
||||||
|
@include('form.entity-permissions-row', [
|
||||||
|
'role' => $data->everyoneElseRole(),
|
||||||
|
'permission' => $data->everyoneElseEntityPermission(),
|
||||||
|
'entityType' => $model->getType(),
|
||||||
|
'inheriting' => !$model->permissions()->where('role_id', '=', 0)->exists(),
|
||||||
|
])
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="mb-m">
|
||||||
|
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<a href="{{ $model->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
|
<a href="{{ $model->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
{{--
|
|
||||||
$name
|
|
||||||
$label
|
|
||||||
$role
|
|
||||||
$action
|
|
||||||
$model?
|
|
||||||
--}}
|
|
||||||
@include('form.custom-checkbox', [
|
|
||||||
'name' => $name . '[' . $role->id . '][' . $action . ']',
|
|
||||||
'label' => $label,
|
|
||||||
'value' => 'true',
|
|
||||||
'checked' => isset($model) && $model->hasRestriction($role->id, $action)
|
|
||||||
])
|
|
@ -16,9 +16,8 @@
|
|||||||
]])
|
]])
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main class="card content-wrap">
|
<main class="card content-wrap auto-height">
|
||||||
<h1 class="list-heading">{{ trans('entities.pages_permissions') }}</h1>
|
@include('form.entity-permissions', ['model' => $page, 'title' => trans('entities.pages_permissions')])
|
||||||
@include('form.entity-permissions', ['model' => $page])
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -81,7 +81,7 @@
|
|||||||
<div class="blended-links">
|
<div class="blended-links">
|
||||||
@include('entities.meta', ['entity' => $page])
|
@include('entities.meta', ['entity' => $page])
|
||||||
|
|
||||||
@if($book->restricted)
|
@if($book->hasPermissions())
|
||||||
<div class="active-restriction">
|
<div class="active-restriction">
|
||||||
@if(userCan('restrictions-manage', $book))
|
@if(userCan('restrictions-manage', $book))
|
||||||
<a href="{{ $book->getUrl('/permissions') }}" class="entity-meta-item">
|
<a href="{{ $book->getUrl('/permissions') }}" class="entity-meta-item">
|
||||||
@ -97,7 +97,7 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if($page->chapter && $page->chapter->restricted)
|
@if($page->chapter && $page->chapter->hasPermissions())
|
||||||
<div class="active-restriction">
|
<div class="active-restriction">
|
||||||
@if(userCan('restrictions-manage', $page->chapter))
|
@if(userCan('restrictions-manage', $page->chapter))
|
||||||
<a href="{{ $page->chapter->getUrl('/permissions') }}" class="entity-meta-item">
|
<a href="{{ $page->chapter->getUrl('/permissions') }}" class="entity-meta-item">
|
||||||
@ -113,7 +113,7 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if($page->restricted)
|
@if($page->hasPermissions())
|
||||||
<div class="active-restriction">
|
<div class="active-restriction">
|
||||||
@if(userCan('restrictions-manage', $page))
|
@if(userCan('restrictions-manage', $page))
|
||||||
<a href="{{ $page->getUrl('/permissions') }}" class="entity-meta-item">
|
<a href="{{ $page->getUrl('/permissions') }}" class="entity-meta-item">
|
||||||
|
@ -26,9 +26,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div permissions-table>
|
<div component="permissions-table">
|
||||||
<label class="setting-list-label">{{ trans('settings.role_system') }}</label>
|
<label class="setting-list-label">{{ trans('settings.role_system') }}</label>
|
||||||
<a href="#" permissions-table-toggle-all class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
|
<a href="#" refs="permissions-table@toggle-all" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
|
||||||
|
|
||||||
<div class="toggle-switch-list grid half mt-m">
|
<div class="toggle-switch-list grid half mt-m">
|
||||||
<div>
|
<div>
|
||||||
@ -56,20 +56,20 @@
|
|||||||
<p class="text-warn">{{ trans('settings.role_asset_admins') }}</p>
|
<p class="text-warn">{{ trans('settings.role_asset_admins') }}</p>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<table permissions-table class="table toggle-switch-list compact permissions-table">
|
<table component="permissions-table" class="table toggle-switch-list compact permissions-table">
|
||||||
<tr>
|
<tr>
|
||||||
<th width="20%">
|
<th width="20%">
|
||||||
<a href="#" permissions-table-toggle-all class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
|
<a href="#" refs="permissions-table@toggle-all" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
|
||||||
</th>
|
</th>
|
||||||
<th width="20%" permissions-table-toggle-all-in-column>{{ trans('common.create') }}</th>
|
<th width="20%" refs="permissions-table@toggle-column">{{ trans('common.create') }}</th>
|
||||||
<th width="20%" permissions-table-toggle-all-in-column>{{ trans('common.view') }}</th>
|
<th width="20%" refs="permissions-table@toggle-column">{{ trans('common.view') }}</th>
|
||||||
<th width="20%" permissions-table-toggle-all-in-column>{{ trans('common.edit') }}</th>
|
<th width="20%" refs="permissions-table@toggle-column">{{ trans('common.edit') }}</th>
|
||||||
<th width="20%" permissions-table-toggle-all-in-column>{{ trans('common.delete') }}</th>
|
<th width="20%" refs="permissions-table@toggle-column">{{ trans('common.delete') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<div>{{ trans('entities.shelves') }}</div>
|
<div>{{ trans('entities.shelves') }}</div>
|
||||||
<a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
|
<a href="#" refs="permissions-table@toggle-row" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-create-all', 'label' => trans('settings.role_all')])
|
@include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-create-all', 'label' => trans('settings.role_all')])
|
||||||
@ -93,7 +93,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<div>{{ trans('entities.books') }}</div>
|
<div>{{ trans('entities.books') }}</div>
|
||||||
<a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
|
<a href="#" refs="permissions-table@toggle-row" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@include('settings.roles.parts.checkbox', ['permission' => 'book-create-all', 'label' => trans('settings.role_all')])
|
@include('settings.roles.parts.checkbox', ['permission' => 'book-create-all', 'label' => trans('settings.role_all')])
|
||||||
@ -117,7 +117,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<div>{{ trans('entities.chapters') }}</div>
|
<div>{{ trans('entities.chapters') }}</div>
|
||||||
<a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
|
<a href="#" refs="permissions-table@toggle-row" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@include('settings.roles.parts.checkbox', ['permission' => 'chapter-create-own', 'label' => trans('settings.role_own')])
|
@include('settings.roles.parts.checkbox', ['permission' => 'chapter-create-own', 'label' => trans('settings.role_own')])
|
||||||
@ -143,7 +143,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<div>{{ trans('entities.pages') }}</div>
|
<div>{{ trans('entities.pages') }}</div>
|
||||||
<a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
|
<a href="#" refs="permissions-table@toggle-row" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@include('settings.roles.parts.checkbox', ['permission' => 'page-create-own', 'label' => trans('settings.role_own')])
|
@include('settings.roles.parts.checkbox', ['permission' => 'page-create-own', 'label' => trans('settings.role_own')])
|
||||||
@ -169,7 +169,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<div>{{ trans('entities.images') }}</div>
|
<div>{{ trans('entities.images') }}</div>
|
||||||
<a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
|
<a href="#" refs="permissions-table@toggle-row" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>@include('settings.roles.parts.checkbox', ['permission' => 'image-create-all', 'label' => ''])</td>
|
<td>@include('settings.roles.parts.checkbox', ['permission' => 'image-create-all', 'label' => ''])</td>
|
||||||
<td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}<sup>1</sup></small></td>
|
<td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}<sup>1</sup></small></td>
|
||||||
@ -187,7 +187,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<div>{{ trans('entities.attachments') }}</div>
|
<div>{{ trans('entities.attachments') }}</div>
|
||||||
<a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
|
<a href="#" refs="permissions-table@toggle-row" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>@include('settings.roles.parts.checkbox', ['permission' => 'attachment-create-all', 'label' => ''])</td>
|
<td>@include('settings.roles.parts.checkbox', ['permission' => 'attachment-create-all', 'label' => ''])</td>
|
||||||
<td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
|
<td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
|
||||||
@ -205,7 +205,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<div>{{ trans('entities.comments') }}</div>
|
<div>{{ trans('entities.comments') }}</div>
|
||||||
<a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
|
<a href="#" refs="permissions-table@toggle-row" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>@include('settings.roles.parts.checkbox', ['permission' => 'comment-create-all', 'label' => ''])</td>
|
<td>@include('settings.roles.parts.checkbox', ['permission' => 'comment-create-all', 'label' => ''])</td>
|
||||||
<td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
|
<td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
@section('body')
|
@section('body')
|
||||||
|
|
||||||
<div class="container small">
|
<div class="container">
|
||||||
|
|
||||||
<div class="my-s">
|
<div class="my-s">
|
||||||
@include('entities.breadcrumbs', ['crumbs' => [
|
@include('entities.breadcrumbs', ['crumbs' => [
|
||||||
@ -15,14 +15,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card content-wrap auto-height">
|
<div class="card content-wrap auto-height">
|
||||||
<h1 class="list-heading">{{ trans('entities.shelves_permissions') }}</h1>
|
@include('form.entity-permissions', ['model' => $shelf, 'title' => trans('entities.shelves_permissions')])
|
||||||
@include('form.entity-permissions', ['model' => $shelf])
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card content-wrap auto-height">
|
<div class="card content-wrap auto-height flex-container-row items-center gap-x-xl wrap">
|
||||||
<h2 class="list-heading">{{ trans('entities.shelves_copy_permissions_to_books') }}</h2>
|
<div class="flex">
|
||||||
<p>{{ trans('entities.shelves_copy_permissions_explain') }}</p>
|
<h2 class="list-heading">{{ trans('entities.shelves_copy_permissions_to_books') }}</h2>
|
||||||
<form action="{{ $shelf->getUrl('/copy-permissions') }}" method="post" class="text-right">
|
<p>{{ trans('entities.shelves_copy_permissions_explain') }}</p>
|
||||||
|
</div>
|
||||||
|
<form action="{{ $shelf->getUrl('/copy-permissions') }}" method="post" class="flex text-right">
|
||||||
{{ csrf_field() }}
|
{{ csrf_field() }}
|
||||||
<button class="button">{{ trans('entities.shelves_copy_permissions') }}</button>
|
<button class="button">{{ trans('entities.shelves_copy_permissions') }}</button>
|
||||||
</form>
|
</form>
|
||||||
|
@ -85,7 +85,7 @@
|
|||||||
<h5>{{ trans('common.details') }}</h5>
|
<h5>{{ trans('common.details') }}</h5>
|
||||||
<div class="blended-links">
|
<div class="blended-links">
|
||||||
@include('entities.meta', ['entity' => $shelf])
|
@include('entities.meta', ['entity' => $shelf])
|
||||||
@if($shelf->restricted)
|
@if($shelf->hasPermissions())
|
||||||
<div class="active-restriction">
|
<div class="active-restriction">
|
||||||
@if(userCan('restrictions-manage', $shelf))
|
@if(userCan('restrictions-manage', $shelf))
|
||||||
<a href="{{ $shelf->getUrl('/permissions') }}" class="entity-meta-item">
|
<a href="{{ $shelf->getUrl('/permissions') }}" class="entity-meta-item">
|
||||||
|
@ -19,6 +19,7 @@ use BookStack\Http\Controllers\PageController;
|
|||||||
use BookStack\Http\Controllers\PageExportController;
|
use BookStack\Http\Controllers\PageExportController;
|
||||||
use BookStack\Http\Controllers\PageRevisionController;
|
use BookStack\Http\Controllers\PageRevisionController;
|
||||||
use BookStack\Http\Controllers\PageTemplateController;
|
use BookStack\Http\Controllers\PageTemplateController;
|
||||||
|
use BookStack\Http\Controllers\PermissionsController;
|
||||||
use BookStack\Http\Controllers\RecycleBinController;
|
use BookStack\Http\Controllers\RecycleBinController;
|
||||||
use BookStack\Http\Controllers\ReferenceController;
|
use BookStack\Http\Controllers\ReferenceController;
|
||||||
use BookStack\Http\Controllers\RoleController;
|
use BookStack\Http\Controllers\RoleController;
|
||||||
@ -61,9 +62,9 @@ Route::middleware('auth')->group(function () {
|
|||||||
Route::get('/shelves/{slug}', [BookshelfController::class, 'show']);
|
Route::get('/shelves/{slug}', [BookshelfController::class, 'show']);
|
||||||
Route::put('/shelves/{slug}', [BookshelfController::class, 'update']);
|
Route::put('/shelves/{slug}', [BookshelfController::class, 'update']);
|
||||||
Route::delete('/shelves/{slug}', [BookshelfController::class, 'destroy']);
|
Route::delete('/shelves/{slug}', [BookshelfController::class, 'destroy']);
|
||||||
Route::get('/shelves/{slug}/permissions', [BookshelfController::class, 'showPermissions']);
|
Route::get('/shelves/{slug}/permissions', [PermissionsController::class, 'showForShelf']);
|
||||||
Route::put('/shelves/{slug}/permissions', [BookshelfController::class, 'permissions']);
|
Route::put('/shelves/{slug}/permissions', [PermissionsController::class, 'updateForShelf']);
|
||||||
Route::post('/shelves/{slug}/copy-permissions', [BookshelfController::class, 'copyPermissions']);
|
Route::post('/shelves/{slug}/copy-permissions', [PermissionsController::class, 'copyShelfPermissionsToBooks']);
|
||||||
Route::get('/shelves/{slug}/references', [ReferenceController::class, 'shelf']);
|
Route::get('/shelves/{slug}/references', [ReferenceController::class, 'shelf']);
|
||||||
|
|
||||||
// Book Creation
|
// Book Creation
|
||||||
@ -79,8 +80,8 @@ Route::middleware('auth')->group(function () {
|
|||||||
Route::delete('/books/{id}', [BookController::class, 'destroy']);
|
Route::delete('/books/{id}', [BookController::class, 'destroy']);
|
||||||
Route::get('/books/{slug}/sort-item', [BookSortController::class, 'showItem']);
|
Route::get('/books/{slug}/sort-item', [BookSortController::class, 'showItem']);
|
||||||
Route::get('/books/{slug}', [BookController::class, 'show']);
|
Route::get('/books/{slug}', [BookController::class, 'show']);
|
||||||
Route::get('/books/{bookSlug}/permissions', [BookController::class, 'showPermissions']);
|
Route::get('/books/{bookSlug}/permissions', [PermissionsController::class, 'showForBook']);
|
||||||
Route::put('/books/{bookSlug}/permissions', [BookController::class, 'permissions']);
|
Route::put('/books/{bookSlug}/permissions', [PermissionsController::class, 'updateForBook']);
|
||||||
Route::get('/books/{slug}/delete', [BookController::class, 'showDelete']);
|
Route::get('/books/{slug}/delete', [BookController::class, 'showDelete']);
|
||||||
Route::get('/books/{bookSlug}/copy', [BookController::class, 'showCopy']);
|
Route::get('/books/{bookSlug}/copy', [BookController::class, 'showCopy']);
|
||||||
Route::post('/books/{bookSlug}/copy', [BookController::class, 'copy']);
|
Route::post('/books/{bookSlug}/copy', [BookController::class, 'copy']);
|
||||||
@ -111,8 +112,8 @@ Route::middleware('auth')->group(function () {
|
|||||||
Route::post('/books/{bookSlug}/page/{pageSlug}/copy', [PageController::class, 'copy']);
|
Route::post('/books/{bookSlug}/page/{pageSlug}/copy', [PageController::class, 'copy']);
|
||||||
Route::get('/books/{bookSlug}/page/{pageSlug}/delete', [PageController::class, 'showDelete']);
|
Route::get('/books/{bookSlug}/page/{pageSlug}/delete', [PageController::class, 'showDelete']);
|
||||||
Route::get('/books/{bookSlug}/draft/{pageId}/delete', [PageController::class, 'showDeleteDraft']);
|
Route::get('/books/{bookSlug}/draft/{pageId}/delete', [PageController::class, 'showDeleteDraft']);
|
||||||
Route::get('/books/{bookSlug}/page/{pageSlug}/permissions', [PageController::class, 'showPermissions']);
|
Route::get('/books/{bookSlug}/page/{pageSlug}/permissions', [PermissionsController::class, 'showForPage']);
|
||||||
Route::put('/books/{bookSlug}/page/{pageSlug}/permissions', [PageController::class, 'permissions']);
|
Route::put('/books/{bookSlug}/page/{pageSlug}/permissions', [PermissionsController::class, 'updateForPage']);
|
||||||
Route::get('/books/{bookSlug}/page/{pageSlug}/references', [ReferenceController::class, 'page']);
|
Route::get('/books/{bookSlug}/page/{pageSlug}/references', [ReferenceController::class, 'page']);
|
||||||
Route::put('/books/{bookSlug}/page/{pageSlug}', [PageController::class, 'update']);
|
Route::put('/books/{bookSlug}/page/{pageSlug}', [PageController::class, 'update']);
|
||||||
Route::delete('/books/{bookSlug}/page/{pageSlug}', [PageController::class, 'destroy']);
|
Route::delete('/books/{bookSlug}/page/{pageSlug}', [PageController::class, 'destroy']);
|
||||||
@ -138,12 +139,12 @@ Route::middleware('auth')->group(function () {
|
|||||||
Route::post('/books/{bookSlug}/chapter/{chapterSlug}/copy', [ChapterController::class, 'copy']);
|
Route::post('/books/{bookSlug}/chapter/{chapterSlug}/copy', [ChapterController::class, 'copy']);
|
||||||
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/edit', [ChapterController::class, 'edit']);
|
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/edit', [ChapterController::class, 'edit']);
|
||||||
Route::post('/books/{bookSlug}/chapter/{chapterSlug}/convert-to-book', [ChapterController::class, 'convertToBook']);
|
Route::post('/books/{bookSlug}/chapter/{chapterSlug}/convert-to-book', [ChapterController::class, 'convertToBook']);
|
||||||
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/permissions', [ChapterController::class, 'showPermissions']);
|
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/permissions', [PermissionsController::class, 'showForChapter']);
|
||||||
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/pdf', [ChapterExportController::class, 'pdf']);
|
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/pdf', [ChapterExportController::class, 'pdf']);
|
||||||
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/html', [ChapterExportController::class, 'html']);
|
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/html', [ChapterExportController::class, 'html']);
|
||||||
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/markdown', [ChapterExportController::class, 'markdown']);
|
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/markdown', [ChapterExportController::class, 'markdown']);
|
||||||
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/plaintext', [ChapterExportController::class, 'plainText']);
|
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/export/plaintext', [ChapterExportController::class, 'plainText']);
|
||||||
Route::put('/books/{bookSlug}/chapter/{chapterSlug}/permissions', [ChapterController::class, 'permissions']);
|
Route::put('/books/{bookSlug}/chapter/{chapterSlug}/permissions', [PermissionsController::class, 'updateForChapter']);
|
||||||
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/references', [ReferenceController::class, 'chapter']);
|
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/references', [ReferenceController::class, 'chapter']);
|
||||||
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/delete', [ChapterController::class, 'showDelete']);
|
Route::get('/books/{bookSlug}/chapter/{chapterSlug}/delete', [ChapterController::class, 'showDelete']);
|
||||||
Route::delete('/books/{bookSlug}/chapter/{chapterSlug}', [ChapterController::class, 'destroy']);
|
Route::delete('/books/{bookSlug}/chapter/{chapterSlug}', [ChapterController::class, 'destroy']);
|
||||||
@ -214,6 +215,9 @@ Route::middleware('auth')->group(function () {
|
|||||||
Route::get('/', [HomeController::class, 'index']);
|
Route::get('/', [HomeController::class, 'index']);
|
||||||
Route::get('/home', [HomeController::class, 'index']);
|
Route::get('/home', [HomeController::class, 'index']);
|
||||||
|
|
||||||
|
// Permissions
|
||||||
|
Route::get('/permissions/form-row/{entityType}/{roleId}', [PermissionsController::class, 'formRowForRole']);
|
||||||
|
|
||||||
// Maintenance
|
// Maintenance
|
||||||
Route::get('/settings/maintenance', [MaintenanceController::class, 'index']);
|
Route::get('/settings/maintenance', [MaintenanceController::class, 'index']);
|
||||||
Route::delete('/settings/maintenance/cleanup-images', [MaintenanceController::class, 'cleanupImages']);
|
Route::delete('/settings/maintenance/cleanup-images', [MaintenanceController::class, 'cleanupImages']);
|
||||||
|
@ -50,9 +50,7 @@ class AttachmentsApiTest extends TestCase
|
|||||||
],
|
],
|
||||||
]]);
|
]]);
|
||||||
|
|
||||||
$page->restricted = true;
|
$this->entities->setPermissions($page, [], []);
|
||||||
$page->save();
|
|
||||||
$this->entities->regenPermissions($page);
|
|
||||||
|
|
||||||
$resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');
|
$resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');
|
||||||
$resp->assertJsonMissing(['data' => [
|
$resp->assertJsonMissing(['data' => [
|
||||||
|
@ -19,7 +19,7 @@ class CopyShelfPermissionsCommandTest extends TestCase
|
|||||||
$shelf = $this->entities->shelf();
|
$shelf = $this->entities->shelf();
|
||||||
$child = $shelf->books()->first();
|
$child = $shelf->books()->first();
|
||||||
$editorRole = $this->getEditor()->roles()->first();
|
$editorRole = $this->getEditor()->roles()->first();
|
||||||
$this->assertFalse(boolval($child->restricted), 'Child book should not be restricted by default');
|
$this->assertFalse($child->hasPermissions(), 'Child book should not be restricted by default');
|
||||||
$this->assertTrue($child->permissions()->count() === 0, 'Child book should have no permissions by default');
|
$this->assertTrue($child->permissions()->count() === 0, 'Child book should have no permissions by default');
|
||||||
|
|
||||||
$this->entities->setPermissions($shelf, ['view', 'update'], [$editorRole]);
|
$this->entities->setPermissions($shelf, ['view', 'update'], [$editorRole]);
|
||||||
@ -28,10 +28,14 @@ class CopyShelfPermissionsCommandTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
$child = $shelf->books()->first();
|
$child = $shelf->books()->first();
|
||||||
|
|
||||||
$this->assertTrue(boolval($child->restricted), 'Child book should now be restricted');
|
$this->assertTrue($child->hasPermissions(), 'Child book should now be restricted');
|
||||||
$this->assertTrue($child->permissions()->count() === 2, 'Child book should have copied permissions');
|
$this->assertEquals(2, $child->permissions()->count(), 'Child book should have copied permissions');
|
||||||
$this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'view', 'role_id' => $editorRole->id]);
|
$this->assertDatabaseHas('entity_permissions', [
|
||||||
$this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'update', 'role_id' => $editorRole->id]);
|
'entity_type' => 'book',
|
||||||
|
'entity_id' => $child->id,
|
||||||
|
'role_id' => $editorRole->id,
|
||||||
|
'view' => true, 'update' => true, 'create' => false, 'delete' => false,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_copy_shelf_permissions_command_using_all()
|
public function test_copy_shelf_permissions_command_using_all()
|
||||||
@ -40,7 +44,7 @@ class CopyShelfPermissionsCommandTest extends TestCase
|
|||||||
Bookshelf::query()->where('id', '!=', $shelf->id)->delete();
|
Bookshelf::query()->where('id', '!=', $shelf->id)->delete();
|
||||||
$child = $shelf->books()->first();
|
$child = $shelf->books()->first();
|
||||||
$editorRole = $this->getEditor()->roles()->first();
|
$editorRole = $this->getEditor()->roles()->first();
|
||||||
$this->assertFalse(boolval($child->restricted), 'Child book should not be restricted by default');
|
$this->assertFalse($child->hasPermissions(), 'Child book should not be restricted by default');
|
||||||
$this->assertTrue($child->permissions()->count() === 0, 'Child book should have no permissions by default');
|
$this->assertTrue($child->permissions()->count() === 0, 'Child book should have no permissions by default');
|
||||||
|
|
||||||
$this->entities->setPermissions($shelf, ['view', 'update'], [$editorRole]);
|
$this->entities->setPermissions($shelf, ['view', 'update'], [$editorRole]);
|
||||||
@ -48,9 +52,13 @@ class CopyShelfPermissionsCommandTest extends TestCase
|
|||||||
->expectsQuestion('Permission settings for all shelves will be cascaded. Books assigned to multiple shelves will receive only the permissions of it\'s last processed shelf. Are you sure you want to proceed?', 'y');
|
->expectsQuestion('Permission settings for all shelves will be cascaded. Books assigned to multiple shelves will receive only the permissions of it\'s last processed shelf. Are you sure you want to proceed?', 'y');
|
||||||
$child = $shelf->books()->first();
|
$child = $shelf->books()->first();
|
||||||
|
|
||||||
$this->assertTrue(boolval($child->restricted), 'Child book should now be restricted');
|
$this->assertTrue($child->hasPermissions(), 'Child book should now be restricted');
|
||||||
$this->assertTrue($child->permissions()->count() === 2, 'Child book should have copied permissions');
|
$this->assertEquals(2, $child->permissions()->count(), 'Child book should have copied permissions');
|
||||||
$this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'view', 'role_id' => $editorRole->id]);
|
$this->assertDatabaseHas('entity_permissions', [
|
||||||
$this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'update', 'role_id' => $editorRole->id]);
|
'entity_type' => 'book',
|
||||||
|
'entity_id' => $child->id,
|
||||||
|
'role_id' => $editorRole->id,
|
||||||
|
'view' => true, 'update' => true, 'create' => false, 'delete' => false,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -295,7 +295,7 @@ class BookShelfTest extends TestCase
|
|||||||
|
|
||||||
$child = $shelf->books()->first();
|
$child = $shelf->books()->first();
|
||||||
$editorRole = $this->getEditor()->roles()->first();
|
$editorRole = $this->getEditor()->roles()->first();
|
||||||
$this->assertFalse(boolval($child->restricted), 'Child book should not be restricted by default');
|
$this->assertFalse($child->hasPermissions(), 'Child book should not be restricted by default');
|
||||||
$this->assertTrue($child->permissions()->count() === 0, 'Child book should have no permissions by default');
|
$this->assertTrue($child->permissions()->count() === 0, 'Child book should have no permissions by default');
|
||||||
|
|
||||||
$this->entities->setPermissions($shelf, ['view', 'update'], [$editorRole]);
|
$this->entities->setPermissions($shelf, ['view', 'update'], [$editorRole]);
|
||||||
@ -303,10 +303,14 @@ class BookShelfTest extends TestCase
|
|||||||
$child = $shelf->books()->first();
|
$child = $shelf->books()->first();
|
||||||
|
|
||||||
$resp->assertRedirect($shelf->getUrl());
|
$resp->assertRedirect($shelf->getUrl());
|
||||||
$this->assertTrue(boolval($child->restricted), 'Child book should now be restricted');
|
$this->assertTrue($child->hasPermissions(), 'Child book should now be restricted');
|
||||||
$this->assertTrue($child->permissions()->count() === 2, 'Child book should have copied permissions');
|
$this->assertTrue($child->permissions()->count() === 2, 'Child book should have copied permissions');
|
||||||
$this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'view', 'role_id' => $editorRole->id]);
|
$this->assertDatabaseHas('entity_permissions', [
|
||||||
$this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'update', 'role_id' => $editorRole->id]);
|
'entity_type' => 'book',
|
||||||
|
'entity_id' => $child->id,
|
||||||
|
'role_id' => $editorRole->id,
|
||||||
|
'view' => true, 'update' => true, 'create' => false, 'delete' => false,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_permission_page_has_a_warning_about_no_cascading()
|
public function test_permission_page_has_a_warning_about_no_cascading()
|
||||||
|
@ -304,9 +304,7 @@ class BookTest extends TestCase
|
|||||||
// Hide child content
|
// Hide child content
|
||||||
/** @var BookChild $page */
|
/** @var BookChild $page */
|
||||||
foreach ($book->getDirectChildren() as $child) {
|
foreach ($book->getDirectChildren() as $child) {
|
||||||
$child->restricted = true;
|
$this->entities->setPermissions($child, [], []);
|
||||||
$child->save();
|
|
||||||
$this->entities->regenPermissions($child);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);
|
$this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);
|
||||||
|
@ -101,9 +101,7 @@ class ChapterTest extends TestCase
|
|||||||
// Hide pages to all non-admin roles
|
// Hide pages to all non-admin roles
|
||||||
/** @var Page $page */
|
/** @var Page $page */
|
||||||
foreach ($chapter->pages as $page) {
|
foreach ($chapter->pages as $page) {
|
||||||
$page->restricted = true;
|
$this->entities->setPermissions($page, [], []);
|
||||||
$page->save();
|
|
||||||
$this->entities->regenPermissions($page);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->asEditor()->post($chapter->getUrl('/copy'), [
|
$this->asEditor()->post($chapter->getUrl('/copy'), [
|
||||||
|
@ -132,9 +132,8 @@ class EntitySearchTest extends TestCase
|
|||||||
public function test_search_filters()
|
public function test_search_filters()
|
||||||
{
|
{
|
||||||
$page = $this->entities->newPage(['name' => 'My new test quaffleachits', 'html' => 'this is about an orange donkey danzorbhsing']);
|
$page = $this->entities->newPage(['name' => 'My new test quaffleachits', 'html' => 'this is about an orange donkey danzorbhsing']);
|
||||||
$this->asEditor();
|
$editor = $this->getEditor();
|
||||||
$editorId = $this->getEditor()->id;
|
$this->actingAs($editor);
|
||||||
$editorSlug = $this->getEditor()->slug;
|
|
||||||
|
|
||||||
// Viewed filter searches
|
// Viewed filter searches
|
||||||
$this->get('/search?term=' . urlencode('danzorbhsing {not_viewed_by_me}'))->assertSee($page->name);
|
$this->get('/search?term=' . urlencode('danzorbhsing {not_viewed_by_me}'))->assertSee($page->name);
|
||||||
@ -147,22 +146,22 @@ class EntitySearchTest extends TestCase
|
|||||||
$this->get('/search?term=' . urlencode('danzorbhsing {created_by:me}'))->assertDontSee($page->name);
|
$this->get('/search?term=' . urlencode('danzorbhsing {created_by:me}'))->assertDontSee($page->name);
|
||||||
$this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertDontSee($page->name);
|
$this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertDontSee($page->name);
|
||||||
$this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertDontSee($page->name);
|
$this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertDontSee($page->name);
|
||||||
$this->get('/search?term=' . urlencode('danzorbhsing {updated_by:' . $editorSlug . '}'))->assertDontSee($page->name);
|
$this->get('/search?term=' . urlencode('danzorbhsing {updated_by:' . $editor->slug . '}'))->assertDontSee($page->name);
|
||||||
$page->created_by = $editorId;
|
$page->created_by = $editor->id;
|
||||||
$page->save();
|
$page->save();
|
||||||
$this->get('/search?term=' . urlencode('danzorbhsing {created_by:me}'))->assertSee($page->name);
|
$this->get('/search?term=' . urlencode('danzorbhsing {created_by:me}'))->assertSee($page->name);
|
||||||
$this->get('/search?term=' . urlencode('danzorbhsing {created_by: ' . $editorSlug . '}'))->assertSee($page->name);
|
$this->get('/search?term=' . urlencode('danzorbhsing {created_by: ' . $editor->slug . '}'))->assertSee($page->name);
|
||||||
$this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertDontSee($page->name);
|
$this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertDontSee($page->name);
|
||||||
$this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertDontSee($page->name);
|
$this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertDontSee($page->name);
|
||||||
$page->updated_by = $editorId;
|
$page->updated_by = $editor->id;
|
||||||
$page->save();
|
$page->save();
|
||||||
$this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertSee($page->name);
|
$this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertSee($page->name);
|
||||||
$this->get('/search?term=' . urlencode('danzorbhsing {updated_by:' . $editorSlug . '}'))->assertSee($page->name);
|
$this->get('/search?term=' . urlencode('danzorbhsing {updated_by:' . $editor->slug . '}'))->assertSee($page->name);
|
||||||
$this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertDontSee($page->name);
|
$this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertDontSee($page->name);
|
||||||
$page->owned_by = $editorId;
|
$page->owned_by = $editor->id;
|
||||||
$page->save();
|
$page->save();
|
||||||
$this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertSee($page->name);
|
$this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertSee($page->name);
|
||||||
$this->get('/search?term=' . urlencode('danzorbhsing {owned_by:' . $editorSlug . '}'))->assertSee($page->name);
|
$this->get('/search?term=' . urlencode('danzorbhsing {owned_by:' . $editor->slug . '}'))->assertSee($page->name);
|
||||||
|
|
||||||
// Content filters
|
// Content filters
|
||||||
$this->get('/search?term=' . urlencode('{in_name:danzorbhsing}'))->assertDontSee($page->name);
|
$this->get('/search?term=' . urlencode('{in_name:danzorbhsing}'))->assertDontSee($page->name);
|
||||||
@ -172,8 +171,7 @@ class EntitySearchTest extends TestCase
|
|||||||
|
|
||||||
// Restricted filter
|
// Restricted filter
|
||||||
$this->get('/search?term=' . urlencode('danzorbhsing {is_restricted}'))->assertDontSee($page->name);
|
$this->get('/search?term=' . urlencode('danzorbhsing {is_restricted}'))->assertDontSee($page->name);
|
||||||
$page->restricted = true;
|
$this->entities->setPermissions($page, ['view'], [$editor->roles->first()]);
|
||||||
$page->save();
|
|
||||||
$this->get('/search?term=' . urlencode('danzorbhsing {is_restricted}'))->assertSee($page->name);
|
$this->get('/search?term=' . urlencode('danzorbhsing {is_restricted}'))->assertSee($page->name);
|
||||||
|
|
||||||
// Date filters
|
// Date filters
|
||||||
|
@ -75,9 +75,7 @@ class TagTest extends TestCase
|
|||||||
$this->asEditor()->get('/ajax/tags/suggest/names?search=co')->assertSimilarJson(['color', 'country']);
|
$this->asEditor()->get('/ajax/tags/suggest/names?search=co')->assertSimilarJson(['color', 'country']);
|
||||||
|
|
||||||
// Set restricted permission the page
|
// Set restricted permission the page
|
||||||
$page->restricted = true;
|
$this->entities->setPermissions($page, [], []);
|
||||||
$page->save();
|
|
||||||
$page->rebuildPermissions();
|
|
||||||
|
|
||||||
$this->asAdmin()->get('/ajax/tags/suggest/names?search=co')->assertSimilarJson(['color', 'country']);
|
$this->asAdmin()->get('/ajax/tags/suggest/names?search=co')->assertSimilarJson(['color', 'country']);
|
||||||
$this->asEditor()->get('/ajax/tags/suggest/names?search=co')->assertSimilarJson([]);
|
$this->asEditor()->get('/ajax/tags/suggest/names?search=co')->assertSimilarJson([]);
|
||||||
@ -180,8 +178,7 @@ class TagTest extends TestCase
|
|||||||
$resp = $this->get('/tags?name=SuperCategory');
|
$resp = $this->get('/tags?name=SuperCategory');
|
||||||
$resp->assertSee('GreatTestContent');
|
$resp->assertSee('GreatTestContent');
|
||||||
|
|
||||||
$page->restricted = true;
|
$this->entities->setPermissions($page, [], []);
|
||||||
$this->entities->regenPermissions($page);
|
|
||||||
|
|
||||||
$resp = $this->asEditor()->get('/tags');
|
$resp = $this->asEditor()->get('/tags');
|
||||||
$resp->assertDontSee('SuperCategory');
|
$resp->assertDontSee('SuperCategory');
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Tests\Helpers;
|
namespace Tests\Helpers;
|
||||||
|
|
||||||
|
use BookStack\Auth\Permissions\EntityPermission;
|
||||||
use BookStack\Auth\Role;
|
use BookStack\Auth\Role;
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
use BookStack\Entities\Models\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
@ -203,21 +204,22 @@ class EntityProvider
|
|||||||
*/
|
*/
|
||||||
public function setPermissions(Entity $entity, array $actions = [], array $roles = []): void
|
public function setPermissions(Entity $entity, array $actions = [], array $roles = []): void
|
||||||
{
|
{
|
||||||
$entity->restricted = true;
|
|
||||||
$entity->permissions()->delete();
|
$entity->permissions()->delete();
|
||||||
|
|
||||||
$permissions = [];
|
$permissions = [
|
||||||
foreach ($actions as $action) {
|
// Set default permissions to not allow actions so that only the provided role permissions are at play.
|
||||||
foreach ($roles as $role) {
|
['role_id' => 0, 'view' => false, 'create' => false, 'update' => false, 'delete' => false],
|
||||||
$permissions[] = [
|
];
|
||||||
'role_id' => $role->id,
|
|
||||||
'action' => strtolower($action),
|
foreach ($roles as $role) {
|
||||||
];
|
$permission = ['role_id' => $role->id];
|
||||||
|
foreach (EntityPermission::PERMISSIONS as $possibleAction) {
|
||||||
|
$permission[$possibleAction] = in_array($possibleAction, $actions);
|
||||||
}
|
}
|
||||||
|
$permissions[] = $permission;
|
||||||
}
|
}
|
||||||
|
|
||||||
$entity->permissions()->createMany($permissions);
|
$entity->permissions()->createMany($permissions);
|
||||||
$entity->save();
|
|
||||||
$entity->load('permissions');
|
$entity->load('permissions');
|
||||||
$this->regenPermissions($entity);
|
$this->regenPermissions($entity);
|
||||||
}
|
}
|
||||||
|
@ -376,20 +376,18 @@ class EntityPermissionsTest extends TestCase
|
|||||||
->assertSee($title);
|
->assertSee($title);
|
||||||
|
|
||||||
$this->put($modelInstance->getUrl('/permissions'), [
|
$this->put($modelInstance->getUrl('/permissions'), [
|
||||||
'restricted' => 'true',
|
'permissions' => [
|
||||||
'restrictions' => [
|
|
||||||
$roleId => [
|
$roleId => [
|
||||||
$permission => 'true',
|
$permission => 'true',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas($modelInstance->getTable(), ['id' => $modelInstance->id, 'restricted' => true]);
|
|
||||||
$this->assertDatabaseHas('entity_permissions', [
|
$this->assertDatabaseHas('entity_permissions', [
|
||||||
'restrictable_id' => $modelInstance->id,
|
'entity_id' => $modelInstance->id,
|
||||||
'restrictable_type' => $modelInstance->getMorphClass(),
|
'entity_type' => $modelInstance->getMorphClass(),
|
||||||
'role_id' => $roleId,
|
'role_id' => $roleId,
|
||||||
'action' => $permission,
|
$permission => true,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,6 +163,29 @@ class RolesTest extends TestCase
|
|||||||
$this->assertEquals($this->user->id, $roleA->users()->first()->id);
|
$this->assertEquals($this->user->id, $roleA->users()->first()->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_entity_permissions_are_removed_on_delete()
|
||||||
|
{
|
||||||
|
/** @var Role $roleA */
|
||||||
|
$roleA = Role::query()->create(['display_name' => 'Entity Permissions Delete Test']);
|
||||||
|
$page = $this->entities->page();
|
||||||
|
|
||||||
|
$this->entities->setPermissions($page, ['view'], [$roleA]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('entity_permissions', [
|
||||||
|
'role_id' => $roleA->id,
|
||||||
|
'entity_id' => $page->id,
|
||||||
|
'entity_type' => $page->getMorphClass(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->asAdmin()->delete("/settings/roles/delete/$roleA->id");
|
||||||
|
|
||||||
|
$this->assertDatabaseMissing('entity_permissions', [
|
||||||
|
'role_id' => $roleA->id,
|
||||||
|
'entity_id' => $page->id,
|
||||||
|
'entity_type' => $page->getMorphClass(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_image_view_notice_shown_on_role_form()
|
public function test_image_view_notice_shown_on_role_form()
|
||||||
{
|
{
|
||||||
/** @var Role $role */
|
/** @var Role $role */
|
||||||
|
@ -253,11 +253,7 @@ class AttachmentTest extends TestCase
|
|||||||
$this->uploadFile($fileName, $page->id);
|
$this->uploadFile($fileName, $page->id);
|
||||||
$attachment = Attachment::orderBy('id', 'desc')->take(1)->first();
|
$attachment = Attachment::orderBy('id', 'desc')->take(1)->first();
|
||||||
|
|
||||||
$page->restricted = true;
|
$this->entities->setPermissions($page, [], []);
|
||||||
$page->permissions()->delete();
|
|
||||||
$page->save();
|
|
||||||
$page->rebuildPermissions();
|
|
||||||
$page->load('jointPermissions');
|
|
||||||
|
|
||||||
$this->actingAs($viewer);
|
$this->actingAs($viewer);
|
||||||
$attachmentGet = $this->get($attachment->getUrl());
|
$attachmentGet = $this->get($attachment->getUrl());
|
||||||
|
Loading…
Reference in New Issue
Block a user