Compare commits

...

24 Commits

Author SHA1 Message Date
Dan Brown
3083979855 Added method for using enity ownership in relation queries
It has a large linear-entity-scaling performance impact though.
2023-01-15 17:38:08 +00:00
Dan Brown
55642a33ee Attempted fix of issues, realised new query system is a failure
As part of the permission checking we need to check owner user status.
Upon this, we'd also want to check page draft status (and its
creator/owner).
These, for cross-entity/relation queries would need up to another 4 joins.
The performance/index usage is already questionable here.
2023-01-14 13:50:41 +00:00
Dan Brown
93ba572369 Aligned admin permission check restriction ignore 2023-01-13 22:19:29 +00:00
Dan Brown
a825f27930 Updated additional relation queries to apply permissions correctly 2023-01-13 22:13:31 +00:00
Dan Brown
932e1d7c61 Got entity relation query permission application working
May be issues at points of use though, Added todo for this in code.
Also added extra indexes to collapsed table for better query
performance.
2023-01-13 17:10:20 +00:00
Dan Brown
2f1491c5a4 Split out 'restrictEntityQuery' function components
Also fixed search query issue with abiguous column
2023-01-13 16:07:36 +00:00
Dan Brown
026e9030b9 Reworked userCan permission check to follow defined logic.
Got all current scenario tests passing.
Also fixes own permission which was using the wrong field.
2022-12-23 21:07:49 +00:00
Dan Brown
451e4ac452 Fixed collapsed perm. gen for book sub-items.
Also converted the existing "JointPermission" usage to the new
collapsed permission system.
2022-12-23 14:05:43 +00:00
Dan Brown
7330139555 Created big scary query to apply permissions via new format 2022-12-22 20:32:06 +00:00
Dan Brown
39acbeac68 Started new permission-caching/querying model 2022-12-22 15:09:17 +00:00
Dan Brown
2d9d2bba80 Added additional case thats known to currently fail
Also removed so no-longer-relevant todo/comments.
2022-12-21 17:14:54 +00:00
Dan Brown
adabf06dbe Added more inter-method permissions test cases 2022-12-20 19:10:09 +00:00
Dan Brown
5ffc10e688 Added entity user permission scenarios
Also added definitions for general expected behaviour to readme doc, and
added some entity role inherit scenarios to check they meet expectations.
Currently failing role test but not an issue with test, needs fixing to
app logic.
2022-12-20 15:50:41 +00:00
Dan Brown
6a6f5e4d19 Added a bunch of role content permissions 2022-12-17 19:46:48 +00:00
Dan Brown
491beee93e Added additional entity_role_permission scenario tests 2022-12-17 15:27:09 +00:00
Dan Brown
f844ae0902 Create additional test helper classes
Following recent similar actions done for entities.
Required at this stage to provider better & cleaner helpers
for common user and permission actions to built out permission testing.
2022-12-15 12:29:10 +00:00
Dan Brown
d54ea1b3ed Started more formal permission test case definitions 2022-12-15 11:22:53 +00:00
Dan Brown
e8a8fedfd6 Started aligning permission behaviour across application methods 2022-12-14 18:14:01 +00:00
Dan Brown
60bf838a4a Added joint_user_permissions handling to query system
Some issues exist to resolve though, not in final state.
2022-12-11 22:53:46 +00:00
Dan Brown
0411185fbb Added, and built perm. gen for, joint_user_permissions table 2022-12-11 14:51:53 +00:00
Dan Brown
93cbd3b8aa Improved user-permissions adding ux
- Reset input after user selection.
- Corrected permission row title text for user rows.
2022-12-10 14:48:19 +00:00
Dan Brown
7a269e7689 Added users to permission form interface
Also updated non-joint permission handling to support user permissions.
2022-12-10 14:37:18 +00:00
Dan Brown
f8c4725166 Aligned logic to entity_permission role_id usage change
Now idenitifies fallback using role_id and user_id = null.
Lays some foundations for handling user_id.
2022-12-07 22:07:03 +00:00
Dan Brown
1c53ffc4d1 Updated entity_permissions table for user perms.
As start of user permissions work
2022-12-07 14:57:23 +00:00
105 changed files with 2469 additions and 1131 deletions

View File

@@ -15,6 +15,8 @@ class ActivityQueries
{ {
protected PermissionApplicator $permissions; protected PermissionApplicator $permissions;
protected array $fieldsForLists = ['id', 'type', 'detail', 'activities.entity_type', 'activities.entity_id', 'user_id', 'created_at'];
public function __construct(PermissionApplicator $permissions) public function __construct(PermissionApplicator $permissions)
{ {
$this->permissions = $permissions; $this->permissions = $permissions;
@@ -25,9 +27,11 @@ class ActivityQueries
*/ */
public function latest(int $count = 20, int $page = 0): array public function latest(int $count = 20, int $page = 0): array
{ {
$query = Activity::query()->select($this->fieldsForLists);
$activityList = $this->permissions $activityList = $this->permissions
->restrictEntityRelationQuery(Activity::query(), 'activities', 'entity_id', 'entity_type') ->restrictEntityRelationQuery($query, 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')
->whereNotNull('activities.entity_id')
->with(['user', 'entity']) ->with(['user', 'entity'])
->skip($count * $page) ->skip($count * $page)
->take($count) ->take($count)
@@ -78,10 +82,12 @@ class ActivityQueries
*/ */
public function userActivity(User $user, int $count = 20, int $page = 0): array public function userActivity(User $user, int $count = 20, int $page = 0): array
{ {
$query = Activity::query()->select($this->fieldsForLists);
$activityList = $this->permissions $activityList = $this->permissions
->restrictEntityRelationQuery(Activity::query(), 'activities', 'entity_id', 'entity_type') ->restrictEntityRelationQuery($query, 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')
->where('user_id', '=', $user->id) ->where('user_id', '=', $user->id)
->whereNotNull('activities.entity_id')
->skip($count * $page) ->skip($count * $page)
->take($count) ->take($count)
->get(); ->get();

View File

@@ -29,15 +29,16 @@ class TagRepo
$sort = 'value'; $sort = 'value';
} }
$entityTypeCol = DB::getTablePrefix() . 'tags.entity_type';
$query = Tag::query() $query = Tag::query()
->select([ ->select([
'name', 'name',
($searchTerm || $nameFilter) ? 'value' : DB::raw('COUNT(distinct value) as `values`'), ($searchTerm || $nameFilter) ? 'value' : DB::raw('COUNT(distinct value) as `values`'),
DB::raw('COUNT(id) as usages'), DB::raw('COUNT(id) as usages'),
DB::raw('SUM(IF(entity_type = \'page\', 1, 0)) as page_count'), DB::raw("SUM(IF({$entityTypeCol} = 'page', 1, 0)) as page_count"),
DB::raw('SUM(IF(entity_type = \'chapter\', 1, 0)) as chapter_count'), DB::raw("SUM(IF({$entityTypeCol} = 'chapter', 1, 0)) as chapter_count"),
DB::raw('SUM(IF(entity_type = \'book\', 1, 0)) as book_count'), DB::raw("SUM(IF({$entityTypeCol} = 'book', 1, 0)) as book_count"),
DB::raw('SUM(IF(entity_type = \'bookshelf\', 1, 0)) as shelf_count'), DB::raw("SUM(IF({$entityTypeCol} = 'bookshelf', 1, 0)) as shelf_count"),
]) ])
->orderBy($sort, $listOptions->getOrder()); ->orderBy($sort, $listOptions->getOrder());

View File

@@ -0,0 +1,18 @@
<?php
namespace BookStack\Auth\Permissions;
use BookStack\Model;
/**
* @property int $id
* @property ?int $role_id
* @property ?int $user_id
* @property string $entity_type
* @property int $entity_id
* @property bool $view
*/
class CollapsedPermission extends Model
{
protected $table = 'entity_permissions_collapsed';
}

View File

@@ -0,0 +1,278 @@
<?php
namespace BookStack\Auth\Permissions;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Facades\DB;
/**
* Collapsed permissions act as a "flattened" view of entity-level permissions in the system
* so inheritance does not have to managed as part of permission querying.
*/
class CollapsedPermissionBuilder
{
/**
* Re-generate all collapsed permissions from scratch.
*/
public function rebuildForAll()
{
DB::table('entity_permissions_collapsed')->truncate();
// Chunk through all books
$this->bookFetchQuery()->chunk(5, function (EloquentCollection $books) {
$this->buildForBooks($books, false);
});
// Chunk through all bookshelves
Bookshelf::query()->withTrashed()
->select(['id'])
->chunk(50, function (EloquentCollection $shelves) {
$this->generateCollapsedPermissions($shelves->all());
});
}
/**
* Rebuild the collapsed permissions for a particular entity.
*/
public function rebuildForEntity(Entity $entity)
{
$entities = [$entity];
if ($entity instanceof Book) {
$books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get();
$this->buildForBooks($books, true);
return;
}
/** @var BookChild $entity */
if ($entity->book) {
$entities[] = $entity->book;
}
if ($entity instanceof Page && $entity->chapter_id) {
$entities[] = $entity->chapter;
}
if ($entity instanceof Chapter) {
foreach ($entity->pages as $page) {
$entities[] = $page;
}
}
$this->buildForEntities($entities);
}
/**
* Get a query for fetching a book with its children.
*/
protected function bookFetchQuery(): Builder
{
return Book::query()->withTrashed()
->select(['id'])->with([
'chapters' => function ($query) {
$query->withTrashed()->select(['id', 'book_id']);
},
'pages' => function ($query) {
$query->withTrashed()->select(['id', 'book_id', 'chapter_id']);
},
]);
}
/**
* Build collapsed permissions for the given books.
*/
protected function buildForBooks(EloquentCollection $books, bool $deleteOld)
{
$entities = clone $books;
/** @var Book $book */
foreach ($books->all() as $book) {
foreach ($book->getRelation('chapters') as $chapter) {
$entities->push($chapter);
}
foreach ($book->getRelation('pages') as $page) {
$entities->push($page);
}
}
if ($deleteOld) {
$this->deleteForEntities($entities->all());
}
$this->generateCollapsedPermissions($entities->all());
}
/**
* Rebuild the collapsed permissions for a collection of entities.
*/
protected function buildForEntities(array $entities)
{
$this->deleteForEntities($entities);
$this->generateCollapsedPermissions($entities);
}
/**
* Delete the stored collapsed permissions for a list of entities.
*
* @param Entity[] $entities
*/
protected function deleteForEntities(array $entities)
{
$simpleEntities = $this->entitiesToSimpleEntities($entities);
$idsByType = $this->entitiesToTypeIdMap($simpleEntities);
DB::transaction(function () use ($idsByType) {
foreach ($idsByType as $type => $ids) {
foreach (array_chunk($ids, 1000) as $idChunk) {
DB::table('entity_permissions_collapsed')
->where('entity_type', '=', $type)
->whereIn('entity_id', $idChunk)
->delete();
}
}
});
}
/**
* Convert the given list of entities into "SimpleEntityData" representations
* for faster usage and property access.
*
* @param Entity[] $entities
*
* @return SimpleEntityData[]
*/
protected function entitiesToSimpleEntities(array $entities): array
{
$simpleEntities = [];
foreach ($entities as $entity) {
$attrs = $entity->getAttributes();
$simple = new SimpleEntityData();
$simple->id = $attrs['id'];
$simple->type = $entity->getMorphClass();
$simple->book_id = $attrs['book_id'] ?? null;
$simple->chapter_id = $attrs['chapter_id'] ?? null;
$simpleEntities[] = $simple;
}
return $simpleEntities;
}
/**
* Create & Save collapsed entity permissions.
*
* @param Entity[] $originalEntities
*/
protected function generateCollapsedPermissions(array $originalEntities)
{
$entities = $this->entitiesToSimpleEntities($originalEntities);
$collapsedPermData = [];
// Fetch related entity permissions
$permissions = $this->getEntityPermissionsForEntities($entities);
// Create a mapping of explicit entity permissions
$permissionMap = new EntityPermissionMap($permissions);
// Create Joint Permission Data
foreach ($entities as $entity) {
array_push($collapsedPermData, ...$this->createCollapsedPermissionData($entity, $permissionMap));
}
DB::transaction(function () use ($collapsedPermData) {
foreach (array_chunk($collapsedPermData, 1000) as $dataChunk) {
DB::table('entity_permissions_collapsed')->insert($dataChunk);
}
});
}
/**
* Create collapsed permission data for the given entity using the given permission map.
*/
protected function createCollapsedPermissionData(SimpleEntityData $entity, EntityPermissionMap $permissionMap): array
{
$chain = [
$entity->type . ':' . $entity->id,
$entity->chapter_id ? ('chapter:' . $entity->chapter_id) : null,
$entity->book_id ? ('book:' . $entity->book_id) : null,
];
$permissionData = [];
$overridesApplied = [];
foreach ($chain as $entityTypeId) {
if ($entityTypeId === null) {
continue;
}
$permissions = $permissionMap->getForEntity($entityTypeId);
foreach ($permissions as $permission) {
$related = $permission->getAssignedType() . ':' . $permission->getAssignedTypeId();
if (!isset($overridesApplied[$related])) {
$permissionData[] = [
'role_id' => $permission->role_id,
'user_id' => $permission->user_id,
'view' => $permission->view,
'entity_type' => $entity->type,
'entity_id' => $entity->id,
];
$overridesApplied[$related] = true;
}
}
}
return $permissionData;
}
/**
* From the given entity list, provide back a mapping of entity types to
* the ids of that given type. The type used is the DB morph class.
*
* @param SimpleEntityData[] $entities
*
* @return array<string, int[]>
*/
protected function entitiesToTypeIdMap(array $entities): array
{
$idsByType = [];
foreach ($entities as $entity) {
if (!isset($idsByType[$entity->type])) {
$idsByType[$entity->type] = [];
}
$idsByType[$entity->type][] = $entity->id;
}
return $idsByType;
}
/**
* Get the entity permissions for all the given entities.
*
* @param SimpleEntityData[] $entities
*
* @return EntityPermission[]
*/
protected function getEntityPermissionsForEntities(array $entities): array
{
$idsByType = $this->entitiesToTypeIdMap($entities);
$permissionFetch = EntityPermission::query()
->where(function (Builder $query) use ($idsByType) {
foreach ($idsByType as $type => $ids) {
$query->orWhere(function (Builder $query) use ($type, $ids) {
$query->where('entity_type', '=', $type)->whereIn('entity_id', $ids);
});
}
});
return $permissionFetch->get()->all();
}
}

View File

@@ -3,13 +3,14 @@
namespace BookStack\Auth\Permissions; namespace BookStack\Auth\Permissions;
use BookStack\Auth\Role; use BookStack\Auth\Role;
use BookStack\Auth\User;
use BookStack\Model; use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/** /**
* @property int $id * @property int $id
* @property int $role_id * @property int $role_id
* @property int $user_id
* @property int $entity_id * @property int $entity_id
* @property string $entity_type * @property string $entity_type
* @property boolean $view * @property boolean $view
@@ -21,17 +22,9 @@ class EntityPermission extends Model
{ {
public const PERMISSIONS = ['view', 'create', 'update', 'delete']; public const PERMISSIONS = ['view', 'create', 'update', 'delete'];
protected $fillable = ['role_id', 'view', 'create', 'update', 'delete']; protected $fillable = ['role_id', 'user_id', 'view', 'create', 'update', 'delete'];
public $timestamps = false; public $timestamps = false;
/**
* Get this restriction's attached entity.
*/
public function restrictable(): MorphTo
{
return $this->morphTo('restrictable');
}
/** /**
* Get the role assigned to this entity permission. * Get the role assigned to this entity permission.
*/ */
@@ -39,4 +32,38 @@ class EntityPermission extends Model
{ {
return $this->belongsTo(Role::class); return $this->belongsTo(Role::class);
} }
/**
* Get the user assigned to this entity permission.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Get the type of entity permission this is.
* Will be one of: user, role, fallback
*/
public function getAssignedType(): string
{
if ($this->user_id) {
return 'user';
}
if ($this->role_id) {
return 'role';
}
return 'fallback';
}
/**
* Get the ID for the assigned type of permission.
* (Role/User ID). Defaults to 0 for fallback.
*/
public function getAssignedTypeId(): int
{
return $this->user_id ?? $this->role_id ?? 0;
}
} }

View File

@@ -0,0 +1,37 @@
<?php
namespace BookStack\Auth\Permissions;
class EntityPermissionMap
{
protected array $map = [];
/**
* @param EntityPermission[] $permissions
*/
public function __construct(array $permissions = [])
{
foreach ($permissions as $entityPermission) {
$this->addPermission($entityPermission);
}
}
protected function addPermission(EntityPermission $permission)
{
$entityCombinedId = $permission->entity_type . ':' . $permission->entity_id;
if (!isset($this->map[$entityCombinedId])) {
$this->map[$entityCombinedId] = [];
}
$this->map[$entityCombinedId][] = $permission;
}
/**
* @return EntityPermission[]
*/
public function getForEntity(string $typeIdString): array
{
return $this->map[$typeIdString] ?? [];
}
}

View File

@@ -1,31 +0,0 @@
<?php
namespace BookStack\Auth\Permissions;
use BookStack\Auth\Role;
use BookStack\Entities\Models\Entity;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphOne;
class JointPermission extends Model
{
protected $primaryKey = null;
public $timestamps = false;
/**
* Get the role that this points to.
*/
public function role(): BelongsTo
{
return $this->belongsTo(Role::class);
}
/**
* Get the entity this points to.
*/
public function entity(): MorphOne
{
return $this->morphOne(Entity::class, 'entity');
}
}

View File

@@ -1,408 +0,0 @@
<?php
namespace BookStack\Auth\Permissions;
use BookStack\Auth\Role;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Facades\DB;
/**
* Joint permissions provide a pre-query "cached" table of view permissions for all core entity
* types for all roles in the system. This class generates out that table for different scenarios.
*/
class JointPermissionBuilder
{
/**
* @var array<string, array<int, SimpleEntityData>>
*/
protected array $entityCache;
/**
* Re-generate all entity permission from scratch.
*/
public function rebuildForAll()
{
JointPermission::query()->truncate();
// Get all roles (Should be the most limited dimension)
$roles = Role::query()->with('permissions')->get()->all();
// Chunk through all books
$this->bookFetchQuery()->chunk(5, function (EloquentCollection $books) use ($roles) {
$this->buildJointPermissionsForBooks($books, $roles);
});
// Chunk through all bookshelves
Bookshelf::query()->withTrashed()->select(['id', 'owned_by'])
->chunk(50, function (EloquentCollection $shelves) use ($roles) {
$this->createManyJointPermissions($shelves->all(), $roles);
});
}
/**
* Rebuild the entity jointPermissions for a particular entity.
*/
public function rebuildForEntity(Entity $entity)
{
$entities = [$entity];
if ($entity instanceof Book) {
$books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get();
$this->buildJointPermissionsForBooks($books, Role::query()->with('permissions')->get()->all(), true);
return;
}
/** @var BookChild $entity */
if ($entity->book) {
$entities[] = $entity->book;
}
if ($entity instanceof Page && $entity->chapter_id) {
$entities[] = $entity->chapter;
}
if ($entity instanceof Chapter) {
foreach ($entity->pages as $page) {
$entities[] = $page;
}
}
$this->buildJointPermissionsForEntities($entities);
}
/**
* Build the entity jointPermissions for a particular role.
*/
public function rebuildForRole(Role $role)
{
$roles = [$role];
$role->jointPermissions()->delete();
$role->load('permissions');
// Chunk through all books
$this->bookFetchQuery()->chunk(20, function ($books) use ($roles) {
$this->buildJointPermissionsForBooks($books, $roles);
});
// Chunk through all bookshelves
Bookshelf::query()->select(['id', 'owned_by'])
->chunk(50, function ($shelves) use ($roles) {
$this->createManyJointPermissions($shelves->all(), $roles);
});
}
/**
* Prepare the local entity cache and ensure it's empty.
*
* @param SimpleEntityData[] $entities
*/
protected function readyEntityCache(array $entities)
{
$this->entityCache = [];
foreach ($entities as $entity) {
if (!isset($this->entityCache[$entity->type])) {
$this->entityCache[$entity->type] = [];
}
$this->entityCache[$entity->type][$entity->id] = $entity;
}
}
/**
* Get a book via ID, Checks local cache.
*/
protected function getBook(int $bookId): SimpleEntityData
{
return $this->entityCache['book'][$bookId];
}
/**
* Get a chapter via ID, Checks local cache.
*/
protected function getChapter(int $chapterId): SimpleEntityData
{
return $this->entityCache['chapter'][$chapterId];
}
/**
* Get a query for fetching a book with its children.
*/
protected function bookFetchQuery(): Builder
{
return Book::query()->withTrashed()
->select(['id', 'owned_by'])->with([
'chapters' => function ($query) {
$query->withTrashed()->select(['id', 'owned_by', 'book_id']);
},
'pages' => function ($query) {
$query->withTrashed()->select(['id', 'owned_by', 'book_id', 'chapter_id']);
},
]);
}
/**
* Build joint permissions for the given book and role combinations.
*/
protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false)
{
$entities = clone $books;
/** @var Book $book */
foreach ($books->all() as $book) {
foreach ($book->getRelation('chapters') as $chapter) {
$entities->push($chapter);
}
foreach ($book->getRelation('pages') as $page) {
$entities->push($page);
}
}
if ($deleteOld) {
$this->deleteManyJointPermissionsForEntities($entities->all());
}
$this->createManyJointPermissions($entities->all(), $roles);
}
/**
* Rebuild the entity jointPermissions for a collection of entities.
*/
protected function buildJointPermissionsForEntities(array $entities)
{
$roles = Role::query()->get()->values()->all();
$this->deleteManyJointPermissionsForEntities($entities);
$this->createManyJointPermissions($entities, $roles);
}
/**
* Delete all the entity jointPermissions for a list of entities.
*
* @param Entity[] $entities
*/
protected function deleteManyJointPermissionsForEntities(array $entities)
{
$simpleEntities = $this->entitiesToSimpleEntities($entities);
$idsByType = $this->entitiesToTypeIdMap($simpleEntities);
DB::transaction(function () use ($idsByType) {
foreach ($idsByType as $type => $ids) {
foreach (array_chunk($ids, 1000) as $idChunk) {
DB::table('joint_permissions')
->where('entity_type', '=', $type)
->whereIn('entity_id', $idChunk)
->delete();
}
}
});
}
/**
* @param Entity[] $entities
*
* @return SimpleEntityData[]
*/
protected function entitiesToSimpleEntities(array $entities): array
{
$simpleEntities = [];
foreach ($entities as $entity) {
$attrs = $entity->getAttributes();
$simple = new SimpleEntityData();
$simple->id = $attrs['id'];
$simple->type = $entity->getMorphClass();
$simple->owned_by = $attrs['owned_by'] ?? 0;
$simple->book_id = $attrs['book_id'] ?? null;
$simple->chapter_id = $attrs['chapter_id'] ?? null;
$simpleEntities[] = $simple;
}
return $simpleEntities;
}
/**
* Create & Save entity jointPermissions for many entities and roles.
*
* @param Entity[] $originalEntities
* @param Role[] $roles
*/
protected function createManyJointPermissions(array $originalEntities, array $roles)
{
$entities = $this->entitiesToSimpleEntities($originalEntities);
$this->readyEntityCache($entities);
$jointPermissions = [];
// Fetch related entity permissions
$permissions = $this->getEntityPermissionsForEntities($entities);
// Create a mapping of explicit entity permissions
$permissionMap = [];
foreach ($permissions as $permission) {
$key = $permission->entity_type . ':' . $permission->entity_id . ':' . $permission->role_id;
$permissionMap[$key] = $permission->view;
}
// Create a mapping of role permissions
$rolePermissionMap = [];
foreach ($roles as $role) {
foreach ($role->permissions as $permission) {
$rolePermissionMap[$role->getRawAttribute('id') . ':' . $permission->getRawAttribute('name')] = true;
}
}
// Create Joint Permission Data
foreach ($entities as $entity) {
foreach ($roles as $role) {
$jointPermissions[] = $this->createJointPermissionData(
$entity,
$role->getRawAttribute('id'),
$permissionMap,
$rolePermissionMap,
$role->system_name === 'admin'
);
}
}
DB::transaction(function () use ($jointPermissions) {
foreach (array_chunk($jointPermissions, 1000) as $jointPermissionChunk) {
DB::table('joint_permissions')->insert($jointPermissionChunk);
}
});
}
/**
* From the given entity list, provide back a mapping of entity types to
* the ids of that given type. The type used is the DB morph class.
*
* @param SimpleEntityData[] $entities
*
* @return array<string, int[]>
*/
protected function entitiesToTypeIdMap(array $entities): array
{
$idsByType = [];
foreach ($entities as $entity) {
if (!isset($idsByType[$entity->type])) {
$idsByType[$entity->type] = [];
}
$idsByType[$entity->type][] = $entity->id;
}
return $idsByType;
}
/**
* Get the entity permissions for all the given entities.
*
* @param SimpleEntityData[] $entities
*
* @return EntityPermission[]
*/
protected function getEntityPermissionsForEntities(array $entities): array
{
$idsByType = $this->entitiesToTypeIdMap($entities);
$permissionFetch = EntityPermission::query()
->where(function (Builder $query) use ($idsByType) {
foreach ($idsByType as $type => $ids) {
$query->orWhere(function (Builder $query) use ($type, $ids) {
$query->where('entity_type', '=', $type)->whereIn('entity_id', $ids);
});
}
});
return $permissionFetch->get()->all();
}
/**
* Create entity permission data for an entity and role
* for a particular action.
*/
protected function createJointPermissionData(SimpleEntityData $entity, int $roleId, array $permissionMap, array $rolePermissionMap, bool $isAdminRole): array
{
$permissionPrefix = $entity->type . '-view';
$roleHasPermission = isset($rolePermissionMap[$roleId . ':' . $permissionPrefix . '-all']);
$roleHasPermissionOwn = isset($rolePermissionMap[$roleId . ':' . $permissionPrefix . '-own']);
if ($isAdminRole) {
return $this->createJointPermissionDataArray($entity, $roleId, true, true);
}
if ($this->entityPermissionsActiveForRole($permissionMap, $entity, $roleId)) {
$hasAccess = $this->mapHasActiveRestriction($permissionMap, $entity, $roleId);
return $this->createJointPermissionDataArray($entity, $roleId, $hasAccess, $hasAccess);
}
if ($entity->type === 'book' || $entity->type === 'bookshelf') {
return $this->createJointPermissionDataArray($entity, $roleId, $roleHasPermission, $roleHasPermissionOwn);
}
// For chapters and pages, Check if explicit permissions are set on the Book.
$book = $this->getBook($entity->book_id);
$hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $book, $roleId);
$hasPermissiveAccessToParents = !$this->entityPermissionsActiveForRole($permissionMap, $book, $roleId);
// For pages with a chapter, Check if explicit permissions are set on the Chapter
if ($entity->type === 'page' && $entity->chapter_id !== 0) {
$chapter = $this->getChapter($entity->chapter_id);
$chapterRestricted = $this->entityPermissionsActiveForRole($permissionMap, $chapter, $roleId);
$hasPermissiveAccessToParents = $hasPermissiveAccessToParents && !$chapterRestricted;
if ($chapterRestricted) {
$hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $chapter, $roleId);
}
}
return $this->createJointPermissionDataArray(
$entity,
$roleId,
($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
);
}
/**
* 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.
*/
protected function mapHasActiveRestriction(array $entityMap, SimpleEntityData $entity, int $roleId): bool
{
$roleKey = $entity->type . ':' . $entity->id . ':' . $roleId;
$defaultKey = $entity->type . ':' . $entity->id . ':0';
return $entityMap[$roleKey] ?? $entityMap[$defaultKey] ?? false;
}
/**
* Create an array of data with the information of an entity jointPermissions.
* Used to build data for bulk insertion.
*/
protected function createJointPermissionDataArray(SimpleEntityData $entity, int $roleId, bool $permissionAll, bool $permissionOwn): array
{
return [
'entity_id' => $entity->id,
'entity_type' => $entity->type,
'has_permission' => $permissionAll,
'has_permission_own' => $permissionOwn,
'owned_by' => $entity->owned_by,
'role_id' => $roleId,
];
}
}

View File

@@ -12,6 +12,8 @@ use BookStack\Traits\HasCreatorAndUpdater;
use BookStack\Traits\HasOwner; use BookStack\Traits\HasOwner;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException; use InvalidArgumentException;
class PermissionApplicator class PermissionApplicator
@@ -48,7 +50,7 @@ class PermissionApplicator
return $hasRolePermission; return $hasRolePermission;
} }
$hasApplicableEntityPermissions = $this->hasEntityPermission($ownable, $userRoleIds, $action); $hasApplicableEntityPermissions = $this->hasEntityPermission($ownable, $userRoleIds, $user->id, $action);
return is_null($hasApplicableEntityPermissions) ? $hasRolePermission : $hasApplicableEntityPermissions; return is_null($hasApplicableEntityPermissions) ? $hasRolePermission : $hasApplicableEntityPermissions;
} }
@@ -57,7 +59,7 @@ class PermissionApplicator
* Check if there are permissions that are applicable for the given entity item, action and roles. * Check if there are permissions that are applicable for the given entity item, action and roles.
* Returns null when no entity permissions are in force. * Returns null when no entity permissions are in force.
*/ */
protected function hasEntityPermission(Entity $entity, array $userRoleIds, string $action): ?bool protected function hasEntityPermission(Entity $entity, array $userRoleIds, int $userId, string $action): ?bool
{ {
$this->ensureValidEntityAction($action); $this->ensureValidEntityAction($action);
@@ -66,38 +68,63 @@ class PermissionApplicator
return true; return true;
} }
// The chain order here is very important due to the fact we walk up the chain // The array 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. // in the flattening loop below. Earlier items in the chain have higher priority.
$chain = [$entity]; $typeIdList = [$entity->getMorphClass() . ':' . $entity->id];
if ($entity instanceof Page && $entity->chapter_id) { if ($entity instanceof Page && $entity->chapter_id) {
$chain[] = $entity->chapter; $typeIdList[] = 'chapter:' . $entity->chapter_id;
} }
if ($entity instanceof Page || $entity instanceof Chapter) { if ($entity instanceof Page || $entity instanceof Chapter) {
$chain[] = $entity->book; $typeIdList[] = 'book:' . $entity->book_id;
} }
foreach ($chain as $currentEntity) { $relevantPermissions = EntityPermission::query()
$allowedByRoleId = $currentEntity->permissions() ->where(function (Builder $query) use ($typeIdList) {
->whereIn('role_id', [0, ...$userRoleIds]) foreach ($typeIdList as $typeId) {
->pluck($action, 'role_id'); $query->orWhere(function (Builder $query) use ($typeId) {
[$type, $id] = explode(':', $typeId);
$query->where('entity_type', '=', $type)
->where('entity_id', '=', $id);
});
}
})->where(function (Builder $query) use ($userRoleIds, $userId) {
$query->whereIn('role_id', $userRoleIds)
->orWhere('user_id', '=', $userId)
->orWhere(function (Builder $query) {
$query->whereNull(['role_id', 'user_id']);
});
})->get(['entity_id', 'entity_type', 'role_id', 'user_id', $action])
->all();
// Continue up the chain if no applicable entity permission overrides. $permissionMap = new EntityPermissionMap($relevantPermissions);
if ($allowedByRoleId->isEmpty()) { $permitsByType = ['user' => [], 'fallback' => [], 'role' => []];
continue;
// Collapse and simplify permission structure
foreach ($typeIdList as $typeId) {
$permissions = $permissionMap->getForEntity($typeId);
foreach ($permissions as $permission) {
$related = $permission->getAssignedType();
$relatedId = $permission->getAssignedTypeId();
if (!isset($permitsByType[$related][$relatedId])) {
$permitsByType[$related][$relatedId] = $permission->$action;
}
} }
}
// If we have user-role-specific permissions set, allow if any of those // Return user-level permission if exists
// role permissions allow access. if (count($permitsByType['user']) > 0) {
$hasDefault = $allowedByRoleId->has(0); return boolval(array_values($permitsByType['user'])[0]);
if (!$hasDefault || $allowedByRoleId->count() > 1) { }
return $allowedByRoleId->search(function (bool $allowed, int $roleId) {
return $roleId !== 0 && $allowed;
}) !== false;
}
// Otherwise, return the default "Other roles" fallback value. // Return grant or reject from role-level if exists
return $allowedByRoleId->get(0); if (count($permitsByType['role']) > 0) {
return boolval(max($permitsByType['role']));
}
// Return fallback permission if exists
if (count($permitsByType['fallback']) > 0) {
return boolval($permitsByType['fallback'][0]);
} }
return null; return null;
@@ -113,7 +140,10 @@ class PermissionApplicator
$permissionQuery = EntityPermission::query() $permissionQuery = EntityPermission::query()
->where($action, '=', true) ->where($action, '=', true)
->whereIn('role_id', $this->getCurrentUserRoleIds()); ->where(function (Builder $query) {
$query->whereIn('role_id', $this->getCurrentUserRoleIds())
->orWhere('user_id', '=', $this->currentUser()->id);
});
if (!empty($entityClass)) { if (!empty($entityClass)) {
/** @var Entity $entityInstance */ /** @var Entity $entityInstance */
@@ -130,18 +160,140 @@ class PermissionApplicator
* Limit the given entity query so that the query will only * Limit the given entity query so that the query will only
* return items that the user has view permission for. * return items that the user has view permission for.
*/ */
public function restrictEntityQuery(Builder $query): Builder public function restrictEntityQuery(Builder $query, string $morphClass): Builder
{ {
return $query->where(function (Builder $parentQuery) { $this->applyPermissionsToQuery($query, $query->getModel()->getTable(), $morphClass, 'id', '');
$parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) {
$permissionQuery->whereIn('role_id', $this->getCurrentUserRoleIds()) return $query;
->where(function (Builder $query) { }
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
}); /**
}); * @param Builder|QueryBuilder $query
*/
protected function applyPermissionsToQuery($query, string $queryTable, string $entityTypeLimiter, string $entityIdColumn, string $entityTypeColumn): void
{
if ($this->currentUser()->hasSystemRole('admin')) {
return;
}
$this->applyFallbackJoin($query, $queryTable, $entityTypeLimiter, $entityIdColumn, $entityTypeColumn);
$this->applyRoleJoin($query, $queryTable, $entityTypeLimiter, $entityIdColumn, $entityTypeColumn);
$this->applyUserJoin($query, $queryTable, $entityTypeLimiter, $entityIdColumn, $entityTypeColumn);
$this->applyPermissionWhereFilter($query, $queryTable, $entityTypeLimiter, $entityTypeColumn);
}
/**
* Apply the where condition to a permission restricting query, to limit based upon the values of the joined
* permission data. Query must have joins pre-applied.
* Either entityTypeLimiter or entityTypeColumn should be supplied, with the other empty.
* Both should not be applied since that would conflict upon intent.
* @param Builder|QueryBuilder $query
*/
protected function applyPermissionWhereFilter($query, string $queryTable, string $entityTypeLimiter, string $entityTypeColumn)
{
$abilities = ['all' => [], 'own' => []];
$types = $entityTypeLimiter ? [$entityTypeLimiter] : ['page', 'chapter', 'bookshelf', 'book'];
$fullEntityTypeColumn = $queryTable . '.' . $entityTypeColumn;
foreach ($types as $type) {
$abilities['all'][$type] = userCan($type . '-view-all');
$abilities['own'][$type] = userCan($type . '-view-own');
}
$abilities['all'] = array_filter($abilities['all']);
$abilities['own'] = array_filter($abilities['own']);
$query->where(function (Builder $query) use ($abilities, $fullEntityTypeColumn, $entityTypeColumn) {
$query->where('perms_user', '=', 1)
->orWhere(function (Builder $query) {
$query->whereNull('perms_user')->where('perms_role', '=', 1);
})->orWhere(function (Builder $query) {
$query->whereNull(['perms_user', 'perms_role'])
->where('perms_fallback', '=', 1);
});
if (count($abilities['all']) > 0) {
$query->orWhere(function (Builder $query) use ($abilities, $fullEntityTypeColumn, $entityTypeColumn) {
$query->whereNull(['perms_user', 'perms_role', 'perms_fallback']);
if ($entityTypeColumn) {
$query->whereIn($fullEntityTypeColumn, array_keys($abilities['all']));
}
});
}
if (count($abilities['own']) > 0) {
$query->orWhere(function (Builder $query) use ($abilities, $fullEntityTypeColumn, $entityTypeColumn) {
$query->whereNull(['perms_user', 'perms_role', 'perms_fallback'])
->where('owned_by', '=', $this->currentUser()->id);
if ($entityTypeColumn) {
$query->whereIn($fullEntityTypeColumn, array_keys($abilities['all']));
}
});
}
}); });
} }
/**
* @param Builder|QueryBuilder $query
*/
protected function applyPermissionJoin(callable $joinCallable, string $subAlias, $query, string $queryTable, string $entityTypeLimiter, string $entityIdColumn, string $entityTypeColumn)
{
$joinCondition = $this->getJoinCondition($queryTable, $subAlias, $entityIdColumn, $entityTypeColumn);
$query->joinSub(function (QueryBuilder $joinQuery) use ($joinCallable, $entityTypeLimiter) {
$joinQuery->select(['entity_id', 'entity_type'])->from('entity_permissions_collapsed')
->groupBy('entity_id', 'entity_type');
$joinCallable($joinQuery);
if ($entityTypeLimiter) {
$joinQuery->where('entity_type', '=', $entityTypeLimiter);
}
}, $subAlias, $joinCondition, null, null, 'left');
}
/**
* @param Builder|QueryBuilder $query
*/
protected function applyUserJoin($query, string $queryTable, string $entityTypeLimiter, string $entityIdColumn, string $entityTypeColumn)
{
$this->applyPermissionJoin(function (QueryBuilder $joinQuery) {
$joinQuery->selectRaw('max(view) as perms_user')
->where('user_id', '=', $this->currentUser()->id);
}, 'p_u', $query, $queryTable, $entityTypeLimiter, $entityIdColumn, $entityTypeColumn);
}
/**
* @param Builder|QueryBuilder $query
*/
protected function applyRoleJoin($query, string $queryTable, string $entityTypeLimiter, string $entityIdColumn, string $entityTypeColumn)
{
$this->applyPermissionJoin(function (QueryBuilder $joinQuery) {
$joinQuery->selectRaw('max(view) as perms_role')
->whereIn('role_id', $this->getCurrentUserRoleIds());
}, 'p_r', $query, $queryTable, $entityTypeLimiter, $entityIdColumn, $entityTypeColumn);
}
/**
* @param Builder|QueryBuilder $query
*/
protected function applyFallbackJoin($query, string $queryTable, string $entityTypeLimiter, string $entityIdColumn, string $entityTypeColumn)
{
$this->applyPermissionJoin(function (QueryBuilder $joinQuery) {
$joinQuery->selectRaw('max(view) as perms_fallback')
->whereNull(['role_id', 'user_id']);
}, 'p_f', $query, $queryTable, $entityTypeLimiter, $entityIdColumn, $entityTypeColumn);
}
protected function getJoinCondition(string $queryTable, string $joinTableName, string $entityIdColumn, string $entityTypeColumn): callable
{
return function (JoinClause $join) use ($queryTable, $joinTableName, $entityIdColumn, $entityTypeColumn) {
$join->on($queryTable . '.' . $entityIdColumn, '=', $joinTableName . '.entity_id');
if ($entityTypeColumn) {
$join->on($queryTable . '.' . $entityTypeColumn, '=', $joinTableName . '.entity_type');
}
};
}
/** /**
* Extend the given page query to ensure draft items are not visible * Extend the given page query to ensure draft items are not visible
* unless created by the given user. * unless created by the given user.
@@ -166,30 +318,23 @@ class PermissionApplicator
*/ */
public function restrictEntityRelationQuery($query, string $tableName, string $entityIdColumn, string $entityTypeColumn) public function restrictEntityRelationQuery($query, string $tableName, string $entityIdColumn, string $entityTypeColumn)
{ {
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn]; $query->leftJoinSub(function (QueryBuilder $query) {
$pageMorphClass = (new Page())->getMorphClass(); $query->select(['id as entity_id', DB::raw("'page' as entity_type"), 'owned_by', 'deleted_at', 'draft'])->from('pages');
$tablesByType = ['page' => 'pages', 'book' => 'books', 'chapter' => 'chapters', 'bookshelf' => 'bookshelves'];
$q = $query->whereExists(function ($permissionQuery) use (&$tableDetails) { foreach ($tablesByType as $type => $table) {
/** @var Builder $permissionQuery */ $query->unionAll(function (QueryBuilder $query) use ($type, $table) {
$permissionQuery->select(['role_id'])->from('joint_permissions') $query->select(['id as entity_id', DB::raw("'{$type}' as entity_type"), 'owned_by', 'deleted_at', DB::raw('0 as draft')])->from($table);
->whereColumn('joint_permissions.entity_id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
->whereColumn('joint_permissions.entity_type', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
->whereIn('joint_permissions.role_id', $this->getCurrentUserRoleIds())
->where(function (QueryBuilder $query) {
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
});
})->where(function ($query) use ($tableDetails, $pageMorphClass) {
/** @var Builder $query */
$query->where($tableDetails['entityTypeColumn'], '!=', $pageMorphClass)
->orWhereExists(function (QueryBuilder $query) use ($tableDetails, $pageMorphClass) {
$query->select('id')->from('pages')
->whereColumn('pages.id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
->where($tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'], '=', $pageMorphClass)
->where('pages.draft', '=', false);
}); });
}
}, 'entities', function (JoinClause $join) use ($tableName, $entityIdColumn, $entityTypeColumn) {
$join->on($tableName . '.' . $entityIdColumn, '=', 'entities.entity_id')
->on($tableName . '.' . $entityTypeColumn, '=', 'entities.entity_type');
}); });
return $q; $this->applyPermissionsToQuery($query, $tableName, '', $entityIdColumn, $entityTypeColumn);
// TODO - Test page draft access (Might allow drafts which should not be seen)
return $query;
} }
/** /**
@@ -200,50 +345,12 @@ class PermissionApplicator
*/ */
public function restrictPageRelationQuery(Builder $query, string $tableName, string $pageIdColumn): Builder public function restrictPageRelationQuery(Builder $query, string $tableName, string $pageIdColumn): Builder
{ {
$fullPageIdColumn = $tableName . '.' . $pageIdColumn;
$morphClass = (new Page())->getMorphClass(); $morphClass = (new Page())->getMorphClass();
$existsQuery = function ($permissionQuery) use ($fullPageIdColumn, $morphClass) { $this->applyPermissionsToQuery($query, $tableName, $morphClass, $pageIdColumn, '');
/** @var Builder $permissionQuery */ // TODO - Draft display
$permissionQuery->select('joint_permissions.role_id')->from('joint_permissions') // TODO - Likely need owned_by entity join workaround as used above
->whereColumn('joint_permissions.entity_id', '=', $fullPageIdColumn) return $query;
->where('joint_permissions.entity_type', '=', $morphClass)
->whereIn('joint_permissions.role_id', $this->getCurrentUserRoleIds())
->where(function (QueryBuilder $query) {
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
});
};
$q = $query->where(function ($query) use ($existsQuery, $fullPageIdColumn) {
$query->whereExists($existsQuery)
->orWhere($fullPageIdColumn, '=', 0);
});
// Prevent visibility of non-owned draft pages
$q->whereExists(function (QueryBuilder $query) use ($fullPageIdColumn) {
$query->select('id')->from('pages')
->whereColumn('pages.id', '=', $fullPageIdColumn)
->where(function (QueryBuilder $query) {
$query->where('pages.draft', '=', false)
->orWhere('pages.owned_by', '=', $this->currentUser()->id);
});
});
return $q;
}
/**
* Add the query for checking the given user id has permission
* within the join_permissions table.
*
* @param QueryBuilder|Builder $query
*/
protected function addJointHasPermissionCheck($query, int $userIdToCheck)
{
$query->where('joint_permissions.has_permission', '=', true)->orWhere(function ($query) use ($userIdToCheck) {
$query->where('joint_permissions.has_permission_own', '=', true)
->where('joint_permissions.owned_by', '=', $userIdToCheck);
});
} }
/** /**

View File

@@ -21,19 +21,32 @@ class PermissionFormData
{ {
return $this->entity->permissions() return $this->entity->permissions()
->with('role') ->with('role')
->where('role_id', '!=', 0) ->whereNotNull('role_id')
->get() ->get()
->sortBy('role.display_name') ->sortBy('role.display_name')
->all(); ->all();
} }
/**
* Get the permissions with assigned users.
*/
public function permissionsWithUsers(): array
{
return $this->entity->permissions()
->with('user')
->whereNotNull('user_id')
->get()
->sortBy('user.name')
->all();
}
/** /**
* Get the roles that don't yet have specific permissions for the * Get the roles that don't yet have specific permissions for the
* entity we're managing permissions for. * entity we're managing permissions for.
*/ */
public function rolesNotAssigned(): array public function rolesNotAssigned(): array
{ {
$assigned = $this->entity->permissions()->pluck('role_id'); $assigned = $this->entity->permissions()->whereNotNull('role_id')->pluck('role_id');
return Role::query() return Role::query()
->where('system_name', '!=', 'admin') ->where('system_name', '!=', 'admin')
->whereNotIn('id', $assigned) ->whereNotIn('id', $assigned)
@@ -49,20 +62,19 @@ class PermissionFormData
{ {
/** @var ?EntityPermission $permission */ /** @var ?EntityPermission $permission */
$permission = $this->entity->permissions() $permission = $this->entity->permissions()
->where('role_id', '=', 0) ->whereNull(['role_id', 'user_id'])
->first(); ->first();
return $permission ?? (new EntityPermission()); return $permission ?? (new EntityPermission());
} }
/** /**
* Get the "Everyone Else" role entry. * Check if the "Everyone else" option is inheriting default role system permissions.
* Is determined by any system entity_permission existing for the current entity.
*/ */
public function everyoneElseRole(): Role public function everyoneElseInheriting(): bool
{ {
return (new Role())->forceFill([ return !$this->entity->permissions()
'id' => 0, ->whereNull(['role_id', 'user_id'])
'display_name' => trans('entities.permissions_role_everyone_else'), ->exists();
'description' => trans('entities.permissions_role_everyone_else_desc'),
]);
} }
} }

View File

@@ -11,13 +11,13 @@ use Illuminate\Database\Eloquent\Collection;
class PermissionsRepo class PermissionsRepo
{ {
protected JointPermissionBuilder $permissionBuilder; protected CollapsedPermissionBuilder $permissionBuilder;
protected $systemRoles = ['admin', 'public']; protected array $systemRoles = ['admin', 'public'];
/** /**
* PermissionsRepo constructor. * PermissionsRepo constructor.
*/ */
public function __construct(JointPermissionBuilder $permissionBuilder) public function __construct(CollapsedPermissionBuilder $permissionBuilder)
{ {
$this->permissionBuilder = $permissionBuilder; $this->permissionBuilder = $permissionBuilder;
} }
@@ -57,7 +57,6 @@ class PermissionsRepo
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : []; $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
$this->assignRolePermissions($role, $permissions); $this->assignRolePermissions($role, $permissions);
$this->permissionBuilder->rebuildForRole($role);
Activity::add(ActivityType::ROLE_CREATE, $role); Activity::add(ActivityType::ROLE_CREATE, $role);
@@ -88,7 +87,6 @@ class PermissionsRepo
$role->fill($roleData); $role->fill($roleData);
$role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true'; $role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true';
$role->save(); $role->save();
$this->permissionBuilder->rebuildForRole($role);
Activity::add(ActivityType::ROLE_UPDATE, $role); Activity::add(ActivityType::ROLE_UPDATE, $role);
} }
@@ -140,7 +138,7 @@ class PermissionsRepo
} }
$role->entityPermissions()->delete(); $role->entityPermissions()->delete();
$role->jointPermissions()->delete(); $role->collapsedPermissions()->delete();
Activity::add(ActivityType::ROLE_DELETE, $role); Activity::add(ActivityType::ROLE_DELETE, $role);
$role->delete(); $role->delete();
} }

View File

@@ -6,7 +6,6 @@ class SimpleEntityData
{ {
public int $id; public int $id;
public string $type; public string $type;
public int $owned_by;
public ?int $book_id; public ?int $book_id;
public ?int $chapter_id; public ?int $chapter_id;
} }

View File

@@ -2,8 +2,8 @@
namespace BookStack\Auth; namespace BookStack\Auth;
use BookStack\Auth\Permissions\CollapsedPermission;
use BookStack\Auth\Permissions\EntityPermission; use BookStack\Auth\Permissions\EntityPermission;
use BookStack\Auth\Permissions\JointPermission;
use BookStack\Auth\Permissions\RolePermission; use BookStack\Auth\Permissions\RolePermission;
use BookStack\Interfaces\Loggable; use BookStack\Interfaces\Loggable;
use BookStack\Model; use BookStack\Model;
@@ -39,14 +39,6 @@ class Role extends Model implements Loggable
return $this->belongsToMany(User::class)->orderBy('name', 'asc'); return $this->belongsToMany(User::class)->orderBy('name', 'asc');
} }
/**
* Get all related JointPermissions.
*/
public function jointPermissions(): HasMany
{
return $this->hasMany(JointPermission::class);
}
/** /**
* The RolePermissions that belong to the role. * The RolePermissions that belong to the role.
*/ */
@@ -63,6 +55,14 @@ class Role extends Model implements Loggable
return $this->hasMany(EntityPermission::class); return $this->hasMany(EntityPermission::class);
} }
/**
* Get all related entity collapsed permissions.
*/
public function collapsedPermissions(): HasMany
{
return $this->hasMany(CollapsedPermission::class);
}
/** /**
* Check if this role has a permission. * Check if this role has a permission.
*/ */

View File

@@ -5,6 +5,8 @@ namespace BookStack\Auth;
use BookStack\Actions\Favourite; use BookStack\Actions\Favourite;
use BookStack\Api\ApiToken; use BookStack\Api\ApiToken;
use BookStack\Auth\Access\Mfa\MfaValue; use BookStack\Auth\Access\Mfa\MfaValue;
use BookStack\Auth\Permissions\CollapsedPermission;
use BookStack\Auth\Permissions\EntityPermission;
use BookStack\Entities\Tools\SlugGenerator; use BookStack\Entities\Tools\SlugGenerator;
use BookStack\Interfaces\Loggable; use BookStack\Interfaces\Loggable;
use BookStack\Interfaces\Sluggable; use BookStack\Interfaces\Sluggable;
@@ -298,6 +300,22 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
}, 'activities', 'users.id', '=', 'activities.user_id'); }, 'activities', 'users.id', '=', 'activities.user_id');
} }
/**
* Get the entity permissions assigned to this specific user.
*/
public function entityPermissions(): HasMany
{
return $this->hasMany(EntityPermission::class);
}
/**
* Get all related entity collapsed permissions.
*/
public function collapsedPermissions(): HasMany
{
return $this->hasMany(CollapsedPermission::class);
}
/** /**
* Get the url for editing this user. * Get the url for editing this user.
*/ */

View File

@@ -153,6 +153,8 @@ class UserRepo
$user->apiTokens()->delete(); $user->apiTokens()->delete();
$user->favourites()->delete(); $user->favourites()->delete();
$user->mfaValues()->delete(); $user->mfaValues()->delete();
$user->collapsedPermissions()->delete();
$user->entityPermissions()->delete();
$user->delete(); $user->delete();
// Delete user profile images // Delete user profile images

View File

@@ -2,7 +2,7 @@
namespace BookStack\Console\Commands; namespace BookStack\Console\Commands;
use BookStack\Auth\Permissions\JointPermissionBuilder; use BookStack\Auth\Permissions\CollapsedPermissionBuilder;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@@ -22,12 +22,12 @@ class RegeneratePermissions extends Command
*/ */
protected $description = 'Regenerate all system permissions'; protected $description = 'Regenerate all system permissions';
protected JointPermissionBuilder $permissionBuilder; protected CollapsedPermissionBuilder $permissionBuilder;
/** /**
* Create a new command instance. * Create a new command instance.
*/ */
public function __construct(JointPermissionBuilder $permissionBuilder) public function __construct(CollapsedPermissionBuilder $permissionBuilder)
{ {
$this->permissionBuilder = $permissionBuilder; $this->permissionBuilder = $permissionBuilder;
parent::__construct(); parent::__construct();

View File

@@ -7,9 +7,9 @@ use BookStack\Actions\Comment;
use BookStack\Actions\Favourite; use BookStack\Actions\Favourite;
use BookStack\Actions\Tag; use BookStack\Actions\Tag;
use BookStack\Actions\View; use BookStack\Actions\View;
use BookStack\Auth\Permissions\CollapsedPermission;
use BookStack\Auth\Permissions\EntityPermission; use BookStack\Auth\Permissions\EntityPermission;
use BookStack\Auth\Permissions\JointPermission; use BookStack\Auth\Permissions\CollapsedPermissionBuilder;
use BookStack\Auth\Permissions\JointPermissionBuilder;
use BookStack\Auth\Permissions\PermissionApplicator; use BookStack\Auth\Permissions\PermissionApplicator;
use BookStack\Entities\Tools\SlugGenerator; use BookStack\Entities\Tools\SlugGenerator;
use BookStack\Interfaces\Deletable; use BookStack\Interfaces\Deletable;
@@ -69,7 +69,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
*/ */
public function scopeVisible(Builder $query): Builder public function scopeVisible(Builder $query): Builder
{ {
return app()->make(PermissionApplicator::class)->restrictEntityQuery($query); return app()->make(PermissionApplicator::class)->restrictEntityQuery($query, $this->getMorphClass());
} }
/** /**
@@ -187,11 +187,11 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
} }
/** /**
* Get the entity jointPermissions this is connected to. * Get the entity collapsed permissions this is connected to.
*/ */
public function jointPermissions(): MorphMany public function collapsedPermissions(): MorphMany
{ {
return $this->morphMany(JointPermission::class, 'entity'); return $this->morphMany(CollapsedPermission::class, 'entity');
} }
/** /**
@@ -292,7 +292,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
*/ */
public function rebuildPermissions() public function rebuildPermissions()
{ {
app()->make(JointPermissionBuilder::class)->rebuildForEntity(clone $this); app()->make(CollapsedPermissionBuilder::class)->rebuildForEntity(clone $this);
} }
/** /**

View File

@@ -10,7 +10,6 @@ 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;
use Illuminate\Support\Collection;
class PermissionsUpdater class PermissionsUpdater
{ {
@@ -58,13 +57,30 @@ class PermissionsUpdater
protected function formatPermissionsFromRequestToEntityPermissions(array $permissions): array protected function formatPermissionsFromRequestToEntityPermissions(array $permissions): array
{ {
$formatted = []; $formatted = [];
$columnsByType = [
'role' => 'role_id',
'user' => 'user_id',
'fallback' => '',
];
foreach ($permissions as $roleId => $info) { foreach ($permissions as $type => $byId) {
$entityPermissionData = ['role_id' => $roleId]; $column = $columnsByType[$type] ?? null;
foreach (EntityPermission::PERMISSIONS as $permission) { if (is_null($column)) {
$entityPermissionData[$permission] = (($info[$permission] ?? false) === "true"); continue;
}
foreach ($byId as $id => $info) {
$entityPermissionData = [];
if (!empty($column)) {
$entityPermissionData[$column] = $id;
}
foreach (EntityPermission::PERMISSIONS as $permission) {
$entityPermissionData[$permission] = (($info[$permission] ?? false) === "true");
}
$formatted[] = $entityPermissionData;
} }
$formatted[] = $entityPermissionData;
} }
return $formatted; return $formatted;

View File

@@ -372,7 +372,7 @@ class TrashCan
$entity->permissions()->delete(); $entity->permissions()->delete();
$entity->tags()->delete(); $entity->tags()->delete();
$entity->comments()->delete(); $entity->comments()->delete();
$entity->jointPermissions()->delete(); $entity->collapsedPermissions()->delete();
$entity->searchTerms()->delete(); $entity->searchTerms()->delete();
$entity->deletions()->delete(); $entity->deletions()->delete();
$entity->favourites()->delete(); $entity->favourites()->delete();

View File

@@ -5,6 +5,7 @@ namespace BookStack\Http\Controllers;
use BookStack\Auth\Permissions\EntityPermission; use BookStack\Auth\Permissions\EntityPermission;
use BookStack\Auth\Permissions\PermissionFormData; use BookStack\Auth\Permissions\PermissionFormData;
use BookStack\Auth\Role; use BookStack\Auth\Role;
use BookStack\Auth\User;
use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Chapter;
@@ -162,10 +163,35 @@ class PermissionsController extends Controller
{ {
$this->checkPermissionOr('restrictions-manage-all', fn() => userCan('restrictions-manage-own')); $this->checkPermissionOr('restrictions-manage-all', fn() => userCan('restrictions-manage-own'));
/** @var Role $role */
$role = Role::query()->findOrFail($roleId); $role = Role::query()->findOrFail($roleId);
return view('form.entity-permissions-row', [ return view('form.entity-permissions-row', [
'role' => $role, 'modelType' => 'role',
'modelId' => $role->id,
'modelName' => $role->display_name,
'modelDescription' => $role->description,
'permission' => new EntityPermission(),
'entityType' => $entityType,
'inheriting' => false,
]);
}
/**
* Get an empty entity permissions form row for the given user.
*/
public function formRowForUser(string $entityType, string $userId)
{
$this->checkPermissionOr('restrictions-manage-all', fn() => userCan('restrictions-manage-own'));
/** @var User $user */
$user = User::query()->findOrFail($userId);
return view('form.entity-permissions-row', [
'modelType' => 'user',
'modelId' => $user->id,
'modelName' => $user->name,
'modelDescription' => '',
'permission' => new EntityPermission(), 'permission' => new EntityPermission(),
'entityType' => $entityType, 'entityType' => $entityType,
'inheriting' => false, 'inheriting' => false,

View File

@@ -223,7 +223,7 @@ class SearchRunner
}); });
$subQuery->groupBy('entity_type', 'entity_id'); $subQuery->groupBy('entity_type', 'entity_id');
$entityQuery->joinSub($subQuery, 's', 'id', '=', 'entity_id'); $entityQuery->joinSub($subQuery, 's', 'id', '=', 's.entity_id');
$entityQuery->addSelect('s.score'); $entityQuery->addSelect('s.score');
$entityQuery->orderBy('score', 'desc'); $entityQuery->orderBy('score', 'desc');
} }

View File

@@ -55,8 +55,9 @@ function hasAppAccess(): bool
} }
/** /**
* Check if the current user has a permission. If an ownable element * Check if the current user has a permission.
* is passed in the jointPermissions are checked against that particular item. * Checks a generic role permission or, if an ownable model is passed in, it will
* check against the given entity model, taking into account entity-level permissions.
*/ */
function userCan(string $permission, Model $ownable = null): bool function userCan(string $permission, Model $ownable = null): bool
{ {

View File

@@ -0,0 +1,47 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class AddUserIdToEntityPermissions extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('entity_permissions', function (Blueprint $table) {
$table->unsignedInteger('role_id')->nullable()->default(null)->change();
$table->unsignedInteger('user_id')->nullable()->default(null)->index();
});
DB::table('entity_permissions')
->where('role_id', '=', 0)
->update(['role_id' => null]);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
DB::table('entity_permissions')
->whereNull('role_id')
->update(['role_id' => 0]);
DB::table('entity_permissions')
->whereNotNull('user_id')
->delete();
Schema::table('entity_permissions', function (Blueprint $table) {
$table->unsignedInteger('role_id')->nullable(false)->change();
$table->dropColumn('user_id');
});
}
}

View File

@@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateCollapsedRolePermissionsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// TODO - Drop joint permissions
// TODO - Run collapsed table rebuild.
Schema::create('entity_permissions_collapsed', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('role_id')->nullable()->index();
$table->unsignedInteger('user_id')->nullable()->index();
$table->string('entity_type');
$table->unsignedInteger('entity_id');
$table->boolean('view')->index();
$table->index(['entity_type', 'entity_id']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('entity_permissions_collapsed');
}
}

View File

@@ -3,7 +3,7 @@
namespace Database\Seeders; namespace Database\Seeders;
use BookStack\Api\ApiToken; use BookStack\Api\ApiToken;
use BookStack\Auth\Permissions\JointPermissionBuilder; use BookStack\Auth\Permissions\CollapsedPermissionBuilder;
use BookStack\Auth\Permissions\RolePermission; use BookStack\Auth\Permissions\RolePermission;
use BookStack\Auth\Role; use BookStack\Auth\Role;
use BookStack\Auth\User; use BookStack\Auth\User;
@@ -69,7 +69,7 @@ class DummyContentSeeder extends Seeder
]); ]);
$token->save(); $token->save();
app(JointPermissionBuilder::class)->rebuildForAll(); app(CollapsedPermissionBuilder::class)->rebuildForAll();
app(SearchIndex::class)->indexAllEntities(); app(SearchIndex::class)->indexAllEntities();
} }
} }

View File

@@ -2,7 +2,7 @@
namespace Database\Seeders; namespace Database\Seeders;
use BookStack\Auth\Permissions\JointPermissionBuilder; use BookStack\Auth\Permissions\CollapsedPermissionBuilder;
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;
@@ -35,7 +35,7 @@ class LargeContentSeeder extends Seeder
$largeBook->chapters()->saveMany($chapters); $largeBook->chapters()->saveMany($chapters);
$all = array_merge([$largeBook], array_values($pages->all()), array_values($chapters->all())); $all = array_merge([$largeBook], array_values($pages->all()), array_values($chapters->all()));
app()->make(JointPermissionBuilder::class)->rebuildForEntity($largeBook); app()->make(CollapsedPermissionBuilder::class)->rebuildForEntity($largeBook);
app()->make(SearchIndex::class)->indexEntities($all); app()->make(SearchIndex::class)->indexEntities($all);
} }
} }

View File

@@ -0,0 +1,421 @@
# Permission Scenario Testing
Due to complexity that can arise in the various combinations of permissions, this document details scenarios and their expected results.
Test cases are written ability abstract, since all abilities should act the same in theory. Functional test cases may test abilities separate due to implementation differences.
Tests are categorised by the most specific element involved in the scenario, where the below list is most specific to least:
- User entity permissions.
- Role entity permissions.
- Fallback entity permissions.
- Role permissions.
- TODO - Test fallback in the context of the above.
## General Permission Logical Rules
The below are some general rules we follow to standardise the behaviour of permissions in the platform:
- Most specific permission application (as above) take priority and can deny less specific permissions.
- Parent user/role entity permissions that may be inherited, are considered to essentially be applied on the item they are inherited to unless a lower level has its own permission rule for an already specific role/user.
- Where both grant and deny exist at the same specificity, we side towards grant.
## Cases
### Content Role Permissions
These are tests related to item/entity permissions that are set only at a role level.
#### test_01_allow
- Role A has role all-page permission.
- User has Role A.
User granted page permission.
#### test_02_deny
- Role A has no page permission.
- User has Role A.
User denied page permission.
#### test_10_allow_on_own_with_own
- Role A has role own-page permission.
- User has Role A.
- User is owner of page.
User granted page permission.
#### test_11_deny_on_other_with_own
- Role A has role own-page permission.
- User has Role A.
- User is not owner of page.
User denied page permission.
#### test_20_multiple_role_conflicting_all
- Role A has role all-page permission.
- Role B has no page permission.
- User has Role A & B.
User granted page permission.
#### test_21_multiple_role_conflicting_own
- Role A has role own-page permission.
- Role B has no page permission.
- User has Role A & B.
- User is owner of page.
User granted page permission.
---
### Entity Role Permissions
These are tests related to entity-level role-specific permission overrides.
#### test_01_explicit_allow
- Page permissions have inherit disabled.
- Role A has entity allow page permission.
- User has Role A.
User granted page permission.
#### test_02_explicit_deny
- Page permissions have inherit disabled.
- Role A has entity deny page permission.
- User has Role A.
User denied page permission.
#### test_03_same_level_conflicting
- Page permissions have inherit disabled.
- Role A has entity allow page permission.
- Role B has entity deny page permission.
- User has both Role A & B.
User granted page permission.
Explicit grant overrides entity deny at same level.
#### test_20_inherit_allow
- Page permissions have inherit enabled.
- Chapter permissions has inherit disabled.
- Role A has entity allow chapter permission.
- User has Role A.
User granted page permission.
#### test_21_inherit_deny
- Page permissions have inherit enabled.
- Chapter permissions has inherit disabled.
- Role A has entity deny chapter permission.
- User has Role A.
User denied page permission.
#### test_22_same_level_conflict_inherit
- Page permissions have inherit enabled.
- Chapter permissions has inherit disabled.
- Role A has entity deny chapter permission.
- Role B has entity allow chapter permission.
- User has both Role A & B.
User granted page permission.
#### test_30_child_inherit_override_allow
- Page permissions have inherit enabled.
- Chapter permissions has inherit disabled.
- Role A has entity deny chapter permission.
- Role A has entity allow page permission.
- User has Role A.
User granted page permission.
#### test_31_child_inherit_override_deny
- Page permissions have inherit enabled.
- Chapter permissions has inherit disabled.
- Role A has entity allow chapter permission.
- Role A has entity deny page permission.
- User has Role A.
User denied page permission.
#### test_40_multi_role_inherit_conflict_override_deny
- Page permissions have inherit enabled.
- Chapter permissions has inherit disabled.
- Role A has entity deny page permission.
- Role B has entity allow chapter permission.
- User has Role A & B.
User granted page permission.
#### test_41_multi_role_inherit_conflict_retain_allow
- Page permissions have inherit enabled.
- Chapter permissions has inherit disabled.
- Role A has entity allow page permission.
- Role B has entity deny chapter permission.
- User has Role A & B.
User granted page permission.
#### test_50_role_override_allow
- Page permissions have inherit enabled.
- Role A has no page role permission.
- Role A has entity allow page permission.
- User has Role A.
User granted page permission.
#### test_51_role_override_deny
- Page permissions have inherit enabled.
- Role A has no page-view-all role permission.
- Role A has entity deny page permission.
- User has Role A.
User denied page permission.
#### test_60_inherited_role_override_allow
- Page permissions have inherit enabled.
- Chapter permissions have inherit enabled.
- Role A has no page role permission.
- Role A has entity allow chapter permission.
- User has Role A.
User granted page permission.
#### test_61_inherited_role_override_deny
- Page permissions have inherit enabled.
- Chapter permissions have inherit enabled.
- Role A has page role permission.
- Role A has entity denied chapter permission.
- User has Role A.
User denied page permission.
#### test_62_inherited_role_override_deny_on_own
- Page permissions have inherit enabled.
- Chapter permissions have inherit enabled.
- Role A has own-page role permission.
- Role A has entity denied chapter permission.
- User has Role A.
- User owns Page.
User denied page permission.
#### test_70_multi_role_inheriting_deny
- Page permissions have inherit enabled.
- Role A has all page role permission.
- Role B has entity denied page permission.
- User has Role A and B.
User denied page permission.
#### test_80_multi_role_inherited_deny_via_parent
- Page permissions have inherit enabled.
- Chapter permissions have inherit enabled.
- Role A has all-pages role permission.
- Role B has entity denied chapter permission.
- User has Role A & B.
User denied page permission.
---
### Entity User Permissions
These are tests related to entity-level user-specific permission overrides.
#### test_01_explicit_allow
- Page permissions have inherit disabled.
- User has entity allow page permission.
User granted page permission.
#### test_02_explicit_deny
- Page permissions have inherit disabled.
- User has entity deny page permission.
User denied page permission.
#### test_10_allow_inherit
- Page permissions have inherit enabled.
- Chapter permissions have inherit disabled.
- User has entity allow chapter permission.
User granted page permission.
#### test_11_deny_inherit
- Page permissions have inherit enabled.
- Chapter permissions have inherit disabled.
- User has entity deny chapter permission.
User denied page permission.
#### test_12_allow_inherit_override
- Page permissions have inherit enabled.
- Chapter permissions have inherit disabled.
- User has entity deny chapter permission.
- User has entity allow page permission.
User granted page permission.
#### test_13_deny_inherit_override
- Page permissions have inherit enabled.
- Chapter permissions have inherit disabled.
- User has entity allow chapter permission.
- User has entity deny page permission.
User denied page permission.
#### test_40_entity_role_override_allow
- Page permissions have inherit disabled.
- User has entity allow page permission.
- Role A has entity deny page permission.
- User has role A.
User granted page permission.
#### test_41_entity_role_override_deny
- Page permissions have inherit disabled.
- User has entity deny page permission.
- Role A has entity allow page permission.
- User has role A.
User denied page permission.
#### test_42_entity_role_override_allow_via_inherit
- Page permissions have inherit enabled.
- Chapter permissions have inherit disabled.
- User has entity allow chapter permission.
- Role A has entity deny page permission.
- User has role A.
User granted page permission.
#### test_43_entity_role_override_deny_via_inherit
- Page permissions have inherit enabled.
- Chapter permissions have inherit disabled.
- User has entity deny chapter permission.
- Role A has entity allow page permission.
- User has role A.
User denied page permission.
#### test_50_role_override_allow
- Page permissions have inherit enabled.
- Role A has no page role permission.
- User has entity allow page permission.
- User has Role A.
User granted page permission.
#### test_51_role_override_deny
- Page permissions have inherit enabled.
- Role A has all-page role permission.
- User has entity deny page permission.
- User has Role A.
User denied page permission.
#### test_60_inherited_role_override_allow
- Page permissions have inherit enabled.
- Role A has no page role permission.
- User has entity allow chapter permission.
- User has Role A.
User granted page permission.
#### test_61_inherited_role_override_deny
- Page permissions have inherit enabled.
- Role A has view-all page role permission.
- User has entity deny chapter permission.
- User has Role A.
User denied page permission.
#### test_61_inherited_role_override_deny_on_own
- Page permissions have inherit enabled.
- Role A has view-own page role permission.
- User has entity deny chapter permission.
- User has Role A.
- User owns Page.
User denied page permission.
#### test_70_all_override_allow
- Page permissions have inherit enabled.
- Role A has no page role permission.
- Role A has entity deny page permission.
- User has entity allow page permission.
- User has Role A.
User granted page permission.
#### test_71_all_override_deny
- Page permissions have inherit enabled.
- Role A has page-all role permission.
- Role A has entity allow page permission.
- User has entity deny page permission.
- User has Role A.
User denied page permission.
#### test_80_inherited_all_override_allow
- Page permissions have inherit enabled.
- Role A has no page role permission.
- Role A has entity deny chapter permission.
- User has entity allow chapter permission.
- User has Role A.
User granted page permission.
#### test_81_inherited_all_override_deny
- Page permissions have inherit enabled.
- Role A has view-all page role permission.
- Role A has entity allow chapter permission.
- User has entity deny chapter permission.
- User has Role A.
User denied page permission.

View File

@@ -10,6 +10,8 @@ export class EntityPermissions extends Component {
this.everyoneInheritToggle = this.$refs.everyoneInherit; this.everyoneInheritToggle = this.$refs.everyoneInherit;
this.roleSelect = this.$refs.roleSelect; this.roleSelect = this.$refs.roleSelect;
this.roleContainer = this.$refs.roleContainer; this.roleContainer = this.$refs.roleContainer;
this.userContainer = this.$refs.userContainer;
this.userSelectContainer = this.$refs.userSelectContainer;
this.setupListeners(); this.setupListeners();
} }
@@ -18,7 +20,7 @@ export class EntityPermissions extends Component {
// "Everyone Else" inherit toggle // "Everyone Else" inherit toggle
this.everyoneInheritToggle.addEventListener('change', event => { this.everyoneInheritToggle.addEventListener('change', event => {
const inherit = event.target.checked; const inherit = event.target.checked;
const permissions = document.querySelectorAll('input[name^="permissions[0]["]'); const permissions = document.querySelectorAll('input[name^="permissions[fallback]"]');
for (const permission of permissions) { for (const permission of permissions) {
permission.disabled = inherit; permission.disabled = inherit;
permission.checked = false; permission.checked = false;
@@ -28,7 +30,7 @@ export class EntityPermissions extends Component {
// Remove role row button click // Remove role row button click
this.container.addEventListener('click', event => { this.container.addEventListener('click', event => {
const button = event.target.closest('button'); const button = event.target.closest('button');
if (button && button.dataset.roleId) { if (button && button.dataset.modelType) {
this.removeRowOnButtonClick(button) this.removeRowOnButtonClick(button)
} }
}); });
@@ -40,6 +42,14 @@ export class EntityPermissions extends Component {
this.addRoleRow(roleId); this.addRoleRow(roleId);
} }
}); });
// User select change
this.userSelectContainer.querySelector('input[name="user_select"]').addEventListener('change', event => {
const userId = event.target.value;
if (userId) {
this.addUserRow(userId);
}
});
} }
async addRoleRow(roleId) { async addRoleRow(roleId) {
@@ -52,23 +62,50 @@ export class EntityPermissions extends Component {
} }
// Get and insert new row // Get and insert new row
const resp = await window.$http.get(`/permissions/form-row/${this.entityType}/${roleId}`); const resp = await window.$http.get(`/permissions/role-form-row/${this.entityType}/${roleId}`);
const row = htmlToDom(resp.data); const row = htmlToDom(resp.data);
this.roleContainer.append(row); this.roleContainer.append(row);
this.roleSelect.disabled = false; this.roleSelect.disabled = false;
} }
async addUserRow(userId) {
const exists = this.userContainer.querySelector(`[name^="permissions[user][${userId}]"]`) !== null;
if (exists) {
return;
}
const toggle = this.userSelectContainer.querySelector('.dropdown-search-toggle-select');
toggle.classList.add('disabled');
this.userContainer.style.pointerEvents = 'none';
// Get and insert new row
const resp = await window.$http.get(`/permissions/user-form-row/${this.entityType}/${userId}`);
const row = htmlToDom(resp.data);
this.userContainer.append(row);
toggle.classList.remove('disabled');
this.userContainer.style.pointerEvents = null;
/** @var {UserSelect} **/
const userSelect = window.$components.firstOnElement(this.userSelectContainer.querySelector('.dropdown-search'), 'user-select');
userSelect.reset();
}
removeRowOnButtonClick(button) { removeRowOnButtonClick(button) {
const row = button.closest('.item-list-row'); const row = button.closest('.item-list-row');
const roleId = button.dataset.roleId; const modelId = button.dataset.modelId;
const roleName = button.dataset.roleName; const modelName = button.dataset.modelName;
const modelType = button.dataset.modelType;
const option = document.createElement('option'); const option = document.createElement('option');
option.value = roleId; option.value = modelId;
option.textContent = roleName; option.textContent = modelName;
if (modelType === 'role') {
this.roleSelect.append(option);
}
this.roleSelect.append(option);
row.remove(); row.remove();
} }

View File

@@ -8,6 +8,9 @@ export class UserSelect extends Component {
this.input = this.$refs.input; this.input = this.$refs.input;
this.userInfoContainer = this.$refs.userInfo; this.userInfoContainer = this.$refs.userInfo;
this.initialValue = this.input.value;
this.initialContent = this.userInfoContainer.innerHTML;
onChildEvent(this.container, 'a.dropdown-search-item', 'click', this.selectUser.bind(this)); onChildEvent(this.container, 'a.dropdown-search-item', 'click', this.selectUser.bind(this));
} }
@@ -19,6 +22,13 @@ export class UserSelect extends Component {
this.hide(); this.hide();
} }
reset() {
this.input.value = this.initialValue;
this.userInfoContainer.innerHTML = this.initialContent;
this.input.dispatchEvent(new Event('change', {bubbles: true}));
this.hide();
}
hide() { hide() {
/** @var {Dropdown} **/ /** @var {Dropdown} **/
const dropdown = window.$components.firstOnElement(this.container, 'dropdown'); const dropdown = window.$components.firstOnElement(this.container, 'dropdown');

View File

@@ -18,6 +18,7 @@ return [
'name' => 'Name', 'name' => 'Name',
'description' => 'Description', 'description' => 'Description',
'role' => 'Role', 'role' => 'Role',
'user' => 'User',
'cover_image' => 'Cover image', 'cover_image' => 'Cover image',
'cover_image_description' => 'This image should be approx 440x250px.', 'cover_image_description' => 'This image should be approx 440x250px.',

View File

@@ -50,6 +50,7 @@ return [
'permissions_role_everyone_else' => 'Everyone Else', 'permissions_role_everyone_else' => 'Everyone Else',
'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.', 'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
'permissions_role_override' => 'Override permissions for role', 'permissions_role_override' => 'Override permissions for role',
'permissions_user_override' => 'Override permissions for user',
'permissions_inherit_defaults' => 'Inherit defaults', 'permissions_inherit_defaults' => 'Inherit defaults',
// Search // Search

View File

@@ -1,5 +1,8 @@
{{-- {{--
$role - The Role to display this row for. $modelType - The type of permission model; String matching one of: user, role, fallback
$modelId - The ID of the permission model.
$modelName - The name of the permission model.
$modelDescription - The description of the permission model.
$entityType - String identifier for type of entity having permissions applied. $entityType - String identifier for type of entity having permissions applied.
$permission - The entity permission containing the permissions. $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. $inheriting - Boolean if the current row should be marked as inheriting default permissions. Used for "Everyone Else" role.
@@ -7,21 +10,21 @@ $inheriting - Boolean if the current row should be marked as inheriting default
<div component="permissions-table" class="item-list-row flex-container-row justify-space-between wrap"> <div component="permissions-table" class="item-list-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="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') }}"> <div class="text-large" title="{{ $modelType === 'fallback' ? trans('entities.permissions_role_everyone_else') : ($modelType === 'role' ? trans('common.role') : trans('common.user')) }}">
@icon($role->id === 0 ? 'groups' : 'role') @icon($modelType === 'fallback' ? 'groups' : ($modelType === 'role' ? 'role' : 'user'))
</div> </div>
<span> <span>
<strong>{{ $role->display_name }}</strong> <br> <strong>{{ $modelName }}</strong> <br>
<small class="text-muted">{{ $role->description }}</small> <small class="text-muted">{{ $modelDescription }}</small>
</span> </span>
@if($role->id !== 0) @if($modelType !== 'fallback')
<button type="button" <button type="button"
class="ml-auto flex-none text-small text-primary text-button hover-underline item-list-row-toggle-all hide-under-s" class="ml-auto flex-none text-small text-primary text-button hover-underline item-list-row-toggle-all hide-under-s"
refs="permissions-table@toggle-all" refs="permissions-table@toggle-all"
><strong>{{ trans('common.toggle_all') }}</strong></button> ><strong>{{ trans('common.toggle_all') }}</strong></button>
@endif @endif
</div> </div>
@if($role->id === 0) @if($modelType === 'fallback')
<div class="px-l flex-container-row items-center" refs="entity-permissions@everyone-inherit"> <div class="px-l flex-container-row items-center" refs="entity-permissions@everyone-inherit">
@include('form.custom-checkbox', [ @include('form.custom-checkbox', [
'name' => 'entity-permissions-inherit', 'name' => 'entity-permissions-inherit',
@@ -32,12 +35,12 @@ $inheriting - Boolean if the current row should be marked as inheriting default
</div> </div>
@endif @endif
<div class="flex-container-row justify-space-between gap-x-xl wrap items-center"> <div class="flex-container-row justify-space-between gap-x-xl wrap items-center">
<input type="hidden" name="permissions[{{ $role->id }}][active]" <input type="hidden" name="permissions[{{ $modelType }}][{{ $modelId }}][active]"
@if($inheriting) disabled="disabled" @endif @if($inheriting) disabled="disabled" @endif
value="true"> value="true">
<div class="px-l"> <div class="px-l">
@include('form.custom-checkbox', [ @include('form.custom-checkbox', [
'name' => 'permissions[' . $role->id . '][view]', 'name' => 'permissions[' . $modelType . '][' . $modelId . '][view]',
'label' => trans('common.view'), 'label' => trans('common.view'),
'value' => 'true', 'value' => 'true',
'checked' => $permission->view, 'checked' => $permission->view,
@@ -47,7 +50,7 @@ $inheriting - Boolean if the current row should be marked as inheriting default
@if($entityType !== 'page') @if($entityType !== 'page')
<div class="px-l"> <div class="px-l">
@include('form.custom-checkbox', [ @include('form.custom-checkbox', [
'name' => 'permissions[' . $role->id . '][create]', 'name' => 'permissions[' . $modelType . '][' . $modelId . '][create]',
'label' => trans('common.create'), 'label' => trans('common.create'),
'value' => 'true', 'value' => 'true',
'checked' => $permission->create, 'checked' => $permission->create,
@@ -57,7 +60,7 @@ $inheriting - Boolean if the current row should be marked as inheriting default
@endif @endif
<div class="px-l"> <div class="px-l">
@include('form.custom-checkbox', [ @include('form.custom-checkbox', [
'name' => 'permissions[' . $role->id . '][update]', 'name' => 'permissions[' . $modelType . '][' . $modelId . '][update]',
'label' => trans('common.update'), 'label' => trans('common.update'),
'value' => 'true', 'value' => 'true',
'checked' => $permission->update, 'checked' => $permission->update,
@@ -66,7 +69,7 @@ $inheriting - Boolean if the current row should be marked as inheriting default
</div> </div>
<div class="px-l"> <div class="px-l">
@include('form.custom-checkbox', [ @include('form.custom-checkbox', [
'name' => 'permissions[' . $role->id . '][delete]', 'name' => 'permissions[' . $modelType . '][' . $modelId . '][delete]',
'label' => trans('common.delete'), 'label' => trans('common.delete'),
'value' => 'true', 'value' => 'true',
'checked' => $permission->delete, 'checked' => $permission->delete,
@@ -74,12 +77,13 @@ $inheriting - Boolean if the current row should be marked as inheriting default
]) ])
</div> </div>
</div> </div>
@if($role->id !== 0) @if($modelType !== 'fallback')
<div class="flex-container-row items-center px-m py-s"> <div class="flex-container-row items-center px-m py-s">
<button type="button" <button type="button"
class="text-neg p-m icon-button" class="text-neg p-m icon-button"
data-role-id="{{ $role->id }}" data-model-type="{{ $modelType }}"
data-role-name="{{ $role->display_name }}" data-model-id="{{ $modelId }}"
data-model-name="{{ $modelName }}"
title="{{ trans('common.remove') }}"> title="{{ trans('common.remove') }}">
@icon('close') <span class="hide-over-m ml-xs">{{ trans('common.remove') }}</span> @icon('close') <span class="hide-over-m ml-xs">{{ trans('common.remove') }}</span>
</button> </button>

View File

@@ -35,11 +35,35 @@
<hr> <hr>
<div refs="entity-permissions@user-container" class="item-list mt-m mb-m">
@foreach($data->permissionsWithUsers() as $permission)
@include('form.entity-permissions-row', [
'permission' => $permission,
'modelType' => 'user',
'modelId' => $permission->user->id,
'modelName' => $permission->user->name,
'modelDescription' => '',
'entityType' => $model->getType(),
'inheriting' => false,
])
@endforeach
</div>
<div class="flex-container-row justify-flex-end mb-xl">
<div refs="entity-permissions@user-select-container" class="flex-container-row items-center gap-m">
<label for="user_select" class="m-none p-none"><span class="bold">{{ trans('entities.permissions_user_override') }}</span></label>
@include('form.user-select', ['name' => 'user_select', 'user' => null])
</div>
</div>
<div refs="entity-permissions@role-container" class="item-list mt-m mb-m"> <div refs="entity-permissions@role-container" class="item-list mt-m mb-m">
@foreach($data->permissionsWithRoles() as $permission) @foreach($data->permissionsWithRoles() as $permission)
@include('form.entity-permissions-row', [ @include('form.entity-permissions-row', [
'permission' => $permission, 'permission' => $permission,
'role' => $permission->role, 'modelType' => 'role',
'modelId' => $permission->role->id,
'modelName' => $permission->role->display_name,
'modelDescription' => $permission->role->description,
'entityType' => $model->getType(), 'entityType' => $model->getType(),
'inheriting' => false, 'inheriting' => false,
]) ])
@@ -60,10 +84,13 @@
<div class="item-list mt-m mb-xl"> <div class="item-list mt-m mb-xl">
@include('form.entity-permissions-row', [ @include('form.entity-permissions-row', [
'role' => $data->everyoneElseRole(), 'modelType' => 'fallback',
'modelId' => 0,
'modelName' => trans('entities.permissions_role_everyone_else'),
'modelDescription' => trans('entities.permissions_role_everyone_else_desc'),
'permission' => $data->everyoneElseEntityPermission(), 'permission' => $data->everyoneElseEntityPermission(),
'entityType' => $model->getType(), 'entityType' => $model->getType(),
'inheriting' => !$model->permissions()->where('role_id', '=', 0)->exists(), 'inheriting' => $data->everyoneElseInheriting(),
]) ])
</div> </div>

View File

@@ -217,7 +217,8 @@ Route::middleware('auth')->group(function () {
Route::get('/home', [HomeController::class, 'index']); Route::get('/home', [HomeController::class, 'index']);
// Permissions // Permissions
Route::get('/permissions/form-row/{entityType}/{roleId}', [PermissionsController::class, 'formRowForRole']); Route::get('/permissions/role-form-row/{entityType}/{roleId}', [PermissionsController::class, 'formRowForRole']);
Route::get('/permissions/user-form-row/{entityType}/{userId}', [PermissionsController::class, 'formRowForUser']);
// Maintenance // Maintenance
Route::get('/settings/maintenance', [MaintenanceController::class, 'index']); Route::get('/settings/maintenance', [MaintenanceController::class, 'index']);

View File

@@ -23,17 +23,17 @@ class AuditLogTest extends TestCase
public function test_only_accessible_with_right_permissions() public function test_only_accessible_with_right_permissions()
{ {
$viewer = $this->getViewer(); $viewer = $this->users->viewer();
$this->actingAs($viewer); $this->actingAs($viewer);
$resp = $this->get('/settings/audit'); $resp = $this->get('/settings/audit');
$this->assertPermissionError($resp); $this->assertPermissionError($resp);
$this->giveUserPermissions($viewer, ['settings-manage']); $this->permissions->grantUserRolePermissions($viewer, ['settings-manage']);
$resp = $this->get('/settings/audit'); $resp = $this->get('/settings/audit');
$this->assertPermissionError($resp); $this->assertPermissionError($resp);
$this->giveUserPermissions($viewer, ['users-manage']); $this->permissions->grantUserRolePermissions($viewer, ['users-manage']);
$resp = $this->get('/settings/audit'); $resp = $this->get('/settings/audit');
$resp->assertStatus(200); $resp->assertStatus(200);
$resp->assertSeeText('Audit Log'); $resp->assertSeeText('Audit Log');
@@ -41,7 +41,7 @@ class AuditLogTest extends TestCase
public function test_shows_activity() public function test_shows_activity()
{ {
$admin = $this->getAdmin(); $admin = $this->users->admin();
$this->actingAs($admin); $this->actingAs($admin);
$page = $this->entities->page(); $page = $this->entities->page();
$this->activityService->add(ActivityType::PAGE_CREATE, $page); $this->activityService->add(ActivityType::PAGE_CREATE, $page);
@@ -56,7 +56,7 @@ class AuditLogTest extends TestCase
public function test_shows_name_for_deleted_items() public function test_shows_name_for_deleted_items()
{ {
$this->actingAs($this->getAdmin()); $this->actingAs($this->users->admin());
$page = $this->entities->page(); $page = $this->entities->page();
$pageName = $page->name; $pageName = $page->name;
$this->activityService->add(ActivityType::PAGE_CREATE, $page); $this->activityService->add(ActivityType::PAGE_CREATE, $page);
@@ -71,12 +71,12 @@ class AuditLogTest extends TestCase
public function test_shows_activity_for_deleted_users() public function test_shows_activity_for_deleted_users()
{ {
$viewer = $this->getViewer(); $viewer = $this->users->viewer();
$this->actingAs($viewer); $this->actingAs($viewer);
$page = $this->entities->page(); $page = $this->entities->page();
$this->activityService->add(ActivityType::PAGE_CREATE, $page); $this->activityService->add(ActivityType::PAGE_CREATE, $page);
$this->actingAs($this->getAdmin()); $this->actingAs($this->users->admin());
app(UserRepo::class)->destroy($viewer); app(UserRepo::class)->destroy($viewer);
$resp = $this->get('settings/audit'); $resp = $this->get('settings/audit');
@@ -85,7 +85,7 @@ class AuditLogTest extends TestCase
public function test_filters_by_key() public function test_filters_by_key()
{ {
$this->actingAs($this->getAdmin()); $this->actingAs($this->users->admin());
$page = $this->entities->page(); $page = $this->entities->page();
$this->activityService->add(ActivityType::PAGE_CREATE, $page); $this->activityService->add(ActivityType::PAGE_CREATE, $page);
@@ -98,7 +98,7 @@ class AuditLogTest extends TestCase
public function test_date_filters() public function test_date_filters()
{ {
$this->actingAs($this->getAdmin()); $this->actingAs($this->users->admin());
$page = $this->entities->page(); $page = $this->entities->page();
$this->activityService->add(ActivityType::PAGE_CREATE, $page); $this->activityService->add(ActivityType::PAGE_CREATE, $page);
@@ -120,8 +120,8 @@ class AuditLogTest extends TestCase
public function test_user_filter() public function test_user_filter()
{ {
$admin = $this->getAdmin(); $admin = $this->users->admin();
$editor = $this->getEditor(); $editor = $this->users->editor();
$this->actingAs($admin); $this->actingAs($admin);
$page = $this->entities->page(); $page = $this->entities->page();
$this->activityService->add(ActivityType::PAGE_CREATE, $page); $this->activityService->add(ActivityType::PAGE_CREATE, $page);
@@ -142,7 +142,7 @@ class AuditLogTest extends TestCase
public function test_ip_address_logged_and_visible() public function test_ip_address_logged_and_visible()
{ {
config()->set('app.proxies', '*'); config()->set('app.proxies', '*');
$editor = $this->getEditor(); $editor = $this->users->editor();
$page = $this->entities->page(); $page = $this->entities->page();
$this->actingAs($editor)->put($page->getUrl(), [ $this->actingAs($editor)->put($page->getUrl(), [
@@ -166,7 +166,7 @@ class AuditLogTest extends TestCase
public function test_ip_address_is_searchable() public function test_ip_address_is_searchable()
{ {
config()->set('app.proxies', '*'); config()->set('app.proxies', '*');
$editor = $this->getEditor(); $editor = $this->users->editor();
$page = $this->entities->page(); $page = $this->entities->page();
$this->actingAs($editor)->put($page->getUrl(), [ $this->actingAs($editor)->put($page->getUrl(), [
@@ -192,7 +192,7 @@ class AuditLogTest extends TestCase
{ {
config()->set('app.proxies', '*'); config()->set('app.proxies', '*');
config()->set('app.env', 'demo'); config()->set('app.env', 'demo');
$editor = $this->getEditor(); $editor = $this->users->editor();
$page = $this->entities->page(); $page = $this->entities->page();
$this->actingAs($editor)->put($page->getUrl(), [ $this->actingAs($editor)->put($page->getUrl(), [
@@ -215,7 +215,7 @@ class AuditLogTest extends TestCase
{ {
config()->set('app.proxies', '*'); config()->set('app.proxies', '*');
config()->set('app.ip_address_precision', 2); config()->set('app.ip_address_precision', 2);
$editor = $this->getEditor(); $editor = $this->users->editor();
$page = $this->entities->page(); $page = $this->entities->page();
$this->actingAs($editor)->put($page->getUrl(), [ $this->actingAs($editor)->put($page->getUrl(), [

View File

@@ -88,7 +88,7 @@ class WebhookCallTest extends TestCase
]); ]);
$webhook = $this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.com'], ['all']); $webhook = $this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.com'], ['all']);
$page = $this->entities->page(); $page = $this->entities->page();
$editor = $this->getEditor(); $editor = $this->users->editor();
$this->runEvent(ActivityType::PAGE_UPDATE, $page, $editor); $this->runEvent(ActivityType::PAGE_UPDATE, $page, $editor);
@@ -111,7 +111,7 @@ class WebhookCallTest extends TestCase
protected function runEvent(string $event, $detail = '', ?User $user = null) protected function runEvent(string $event, $detail = '', ?User $user = null)
{ {
if (is_null($user)) { if (is_null($user)) {
$user = $this->getEditor(); $user = $this->users->editor();
} }
$this->actingAs($user); $this->actingAs($user);

View File

@@ -41,7 +41,7 @@ class WebhookFormatTesting extends TestCase
protected function getWebhookData(string $event, $detail): array protected function getWebhookData(string $event, $detail): array
{ {
$webhook = Webhook::factory()->make(); $webhook = Webhook::factory()->make();
$user = $this->getEditor(); $user = $this->users->editor();
$formatter = WebhookFormatter::getDefault($event, $webhook, $detail, $user, time()); $formatter = WebhookFormatter::getDefault($event, $webhook, $detail, $user, time());
return $formatter->format(); return $formatter->format();

View File

@@ -135,7 +135,7 @@ class WebhookManagementTest extends TestCase
public function test_settings_manage_permission_required_for_webhook_routes() public function test_settings_manage_permission_required_for_webhook_routes()
{ {
$editor = $this->getEditor(); $editor = $this->users->editor();
$this->actingAs($editor); $this->actingAs($editor);
$routes = [ $routes = [
@@ -153,7 +153,7 @@ class WebhookManagementTest extends TestCase
$this->assertPermissionError($resp); $this->assertPermissionError($resp);
} }
$this->giveUserPermissions($editor, ['settings-manage']); $this->permissions->grantUserRolePermissions($editor, ['settings-manage']);
foreach ($routes as [$method, $endpoint]) { foreach ($routes as [$method, $endpoint]) {
$resp = $this->call($method, $endpoint); $resp = $this->call($method, $endpoint);

View File

@@ -16,8 +16,8 @@ class ApiAuthTest extends TestCase
public function test_requests_succeed_with_default_auth() public function test_requests_succeed_with_default_auth()
{ {
$viewer = $this->getViewer(); $viewer = $this->users->viewer();
$this->giveUserPermissions($viewer, ['access-api']); $this->permissions->grantUserRolePermissions($viewer, ['access-api']);
$resp = $this->get($this->endpoint); $resp = $this->get($this->endpoint);
$resp->assertStatus(401); $resp->assertStatus(401);
@@ -63,7 +63,7 @@ class ApiAuthTest extends TestCase
auth()->logout(); auth()->logout();
$accessApiPermission = RolePermission::getByName('access-api'); $accessApiPermission = RolePermission::getByName('access-api');
$editorRole = $this->getEditor()->roles()->first(); $editorRole = $this->users->editor()->roles()->first();
$editorRole->detachPermission($accessApiPermission); $editorRole->detachPermission($accessApiPermission);
$resp = $this->get($this->endpoint, $this->apiAuthHeader()); $resp = $this->get($this->endpoint, $this->apiAuthHeader());
@@ -73,7 +73,7 @@ class ApiAuthTest extends TestCase
public function test_api_access_permission_required_to_access_api_with_session_auth() public function test_api_access_permission_required_to_access_api_with_session_auth()
{ {
$editor = $this->getEditor(); $editor = $this->users->editor();
$this->actingAs($editor, 'standard'); $this->actingAs($editor, 'standard');
$resp = $this->get($this->endpoint); $resp = $this->get($this->endpoint);
@@ -81,7 +81,7 @@ class ApiAuthTest extends TestCase
auth('standard')->logout(); auth('standard')->logout();
$accessApiPermission = RolePermission::getByName('access-api'); $accessApiPermission = RolePermission::getByName('access-api');
$editorRole = $this->getEditor()->roles()->first(); $editorRole = $this->users->editor()->roles()->first();
$editorRole->detachPermission($accessApiPermission); $editorRole->detachPermission($accessApiPermission);
$editor = User::query()->where('id', '=', $editor->id)->first(); $editor = User::query()->where('id', '=', $editor->id)->first();
@@ -114,7 +114,7 @@ class ApiAuthTest extends TestCase
public function test_token_expiry_checked() public function test_token_expiry_checked()
{ {
$editor = $this->getEditor(); $editor = $this->users->editor();
$token = $editor->apiTokens()->first(); $token = $editor->apiTokens()->first();
$resp = $this->get($this->endpoint, $this->apiAuthHeader()); $resp = $this->get($this->endpoint, $this->apiAuthHeader());
@@ -130,7 +130,7 @@ class ApiAuthTest extends TestCase
public function test_email_confirmation_checked_using_api_auth() public function test_email_confirmation_checked_using_api_auth()
{ {
$editor = $this->getEditor(); $editor = $this->users->editor();
$editor->email_confirmed = false; $editor->email_confirmed = false;
$editor->save(); $editor->save();

View File

@@ -50,7 +50,7 @@ class AttachmentsApiTest extends TestCase
], ],
]]); ]]);
$this->entities->setPermissions($page, [], []); $this->permissions->setEntityPermissions($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' => [
@@ -246,13 +246,13 @@ class AttachmentsApiTest extends TestCase
public function test_attachment_not_visible_on_other_users_draft() public function test_attachment_not_visible_on_other_users_draft()
{ {
$this->actingAsApiAdmin(); $this->actingAsApiAdmin();
$editor = $this->getEditor(); $editor = $this->users->editor();
$page = $this->entities->page(); $page = $this->entities->page();
$page->draft = true; $page->draft = true;
$page->owned_by = $editor->id; $page->owned_by = $editor->id;
$page->save(); $page->save();
$this->entities->regenPermissions($page); $this->permissions->regenerateForEntity($page);
$attachment = $this->createAttachmentForPage($page, [ $attachment = $this->createAttachmentForPage($page, [
'name' => 'my attachment', 'name' => 'my attachment',
@@ -342,7 +342,7 @@ class AttachmentsApiTest extends TestCase
protected function createAttachmentForPage(Page $page, $attributes = []): Attachment protected function createAttachmentForPage(Page $page, $attributes = []): Attachment
{ {
$admin = $this->getAdmin(); $admin = $this->users->admin();
/** @var Attachment $attachment */ /** @var Attachment $attachment */
$attachment = $page->attachments()->forceCreate(array_merge([ $attachment = $page->attachments()->forceCreate(array_merge([
'uploaded_to' => $page->id, 'uploaded_to' => $page->id,

View File

@@ -246,7 +246,7 @@ class BooksApiTest extends TestCase
{ {
$types = ['html', 'plaintext', 'pdf', 'markdown']; $types = ['html', 'plaintext', 'pdf', 'markdown'];
$this->actingAsApiEditor(); $this->actingAsApiEditor();
$this->removePermissionFromUser($this->getEditor(), 'content-export'); $this->permissions->removeUserRolePermissions($this->users->editor(), ['content-export']);
$book = $this->entities->book(); $book = $this->entities->book();
foreach ($types as $type) { foreach ($types as $type) {

View File

@@ -221,7 +221,7 @@ class ChaptersApiTest extends TestCase
{ {
$types = ['html', 'plaintext', 'pdf', 'markdown']; $types = ['html', 'plaintext', 'pdf', 'markdown'];
$this->actingAsApiEditor(); $this->actingAsApiEditor();
$this->removePermissionFromUser($this->getEditor(), 'content-export'); $this->permissions->removeUserRolePermissions($this->users->editor(), ['content-export']);
$chapter = Chapter::visible()->has('pages')->first(); $chapter = Chapter::visible()->has('pages')->first();
foreach ($types as $type) { foreach ($types as $type) {

View File

@@ -209,7 +209,7 @@ class PagesApiTest extends TestCase
$this->actingAsApiEditor(); $this->actingAsApiEditor();
$page = $this->entities->page(); $page = $this->entities->page();
$chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first(); $chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first();
$this->entities->setPermissions($chapter, ['view'], [$this->getEditor()->roles()->first()]); $this->permissions->setEntityPermissions($chapter, ['view'], [$this->users->editor()->roles()->first()]);
$details = [ $details = [
'name' => 'My updated API page', 'name' => 'My updated API page',
'chapter_id' => $chapter->id, 'chapter_id' => $chapter->id,
@@ -315,7 +315,7 @@ class PagesApiTest extends TestCase
{ {
$types = ['html', 'plaintext', 'pdf', 'markdown']; $types = ['html', 'plaintext', 'pdf', 'markdown'];
$this->actingAsApiEditor(); $this->actingAsApiEditor();
$this->removePermissionFromUser($this->getEditor(), 'content-export'); $this->permissions->removeUserRolePermissions($this->users->editor(), ['content-export']);
$page = $this->entities->page(); $page = $this->entities->page();
foreach ($types as $type) { foreach ($types as $type) {

View File

@@ -21,8 +21,8 @@ class RecycleBinApiTest extends TestCase
public function test_settings_manage_permission_needed_for_all_endpoints() public function test_settings_manage_permission_needed_for_all_endpoints()
{ {
$editor = $this->getEditor(); $editor = $this->users->editor();
$this->giveUserPermissions($editor, ['settings-manage']); $this->permissions->grantUserRolePermissions($editor, ['settings-manage']);
$this->actingAs($editor); $this->actingAs($editor);
foreach ($this->endpointMap as [$method, $uri]) { foreach ($this->endpointMap as [$method, $uri]) {
@@ -34,8 +34,8 @@ class RecycleBinApiTest extends TestCase
public function test_restrictions_manage_all_permission_needed_for_all_endpoints() public function test_restrictions_manage_all_permission_needed_for_all_endpoints()
{ {
$editor = $this->getEditor(); $editor = $this->users->editor();
$this->giveUserPermissions($editor, ['restrictions-manage-all']); $this->permissions->grantUserRolePermissions($editor, ['restrictions-manage-all']);
$this->actingAs($editor); $this->actingAs($editor);
foreach ($this->endpointMap as [$method, $uri]) { foreach ($this->endpointMap as [$method, $uri]) {
@@ -47,7 +47,7 @@ class RecycleBinApiTest extends TestCase
public function test_index_endpoint_returns_expected_page() public function test_index_endpoint_returns_expected_page()
{ {
$admin = $this->getAdmin(); $admin = $this->users->admin();
$page = $this->entities->page(); $page = $this->entities->page();
$book = $this->entities->book(); $book = $this->entities->book();
@@ -82,7 +82,7 @@ class RecycleBinApiTest extends TestCase
public function test_index_endpoint_returns_children_count() public function test_index_endpoint_returns_children_count()
{ {
$admin = $this->getAdmin(); $admin = $this->users->admin();
$book = Book::query()->whereHas('pages')->whereHas('chapters')->withCount(['pages', 'chapters'])->first(); $book = Book::query()->whereHas('pages')->whereHas('chapters')->withCount(['pages', 'chapters'])->first();
$this->actingAs($admin)->delete($book->getUrl()); $this->actingAs($admin)->delete($book->getUrl());
@@ -109,7 +109,7 @@ class RecycleBinApiTest extends TestCase
public function test_index_endpoint_returns_parent() public function test_index_endpoint_returns_parent()
{ {
$admin = $this->getAdmin(); $admin = $this->users->admin();
$page = $this->entities->pageWithinChapter(); $page = $this->entities->pageWithinChapter();
$this->actingAs($admin)->delete($page->getUrl()); $this->actingAs($admin)->delete($page->getUrl());

View File

@@ -12,7 +12,7 @@ trait TestsApi
*/ */
protected function actingAsApiEditor() protected function actingAsApiEditor()
{ {
$this->actingAs($this->getEditor(), 'api'); $this->actingAs($this->users->editor(), 'api');
return $this; return $this;
} }
@@ -22,7 +22,7 @@ trait TestsApi
*/ */
protected function actingAsApiAdmin() protected function actingAsApiAdmin()
{ {
$this->actingAs($this->getAdmin(), 'api'); $this->actingAs($this->users->admin(), 'api');
return $this; return $this;
} }

View File

@@ -175,7 +175,7 @@ class UsersApiTest extends TestCase
{ {
$this->actingAsApiAdmin(); $this->actingAsApiAdmin();
/** @var User $user */ /** @var User $user */
$user = $this->getAdmin(); $user = $this->users->admin();
$roles = Role::query()->pluck('id'); $roles = Role::query()->pluck('id');
$resp = $this->putJson($this->baseEndpoint . "/{$user->id}", [ $resp = $this->putJson($this->baseEndpoint . "/{$user->id}", [
'name' => 'My updated user', 'name' => 'My updated user',
@@ -204,7 +204,7 @@ class UsersApiTest extends TestCase
{ {
$this->actingAsApiAdmin(); $this->actingAsApiAdmin();
/** @var User $user */ /** @var User $user */
$user = $this->getAdmin(); $user = $this->users->admin();
$roleCount = $user->roles()->count(); $roleCount = $user->roles()->count();
$resp = $this->putJson($this->baseEndpoint . "/{$user->id}", []); $resp = $this->putJson($this->baseEndpoint . "/{$user->id}", []);
@@ -222,7 +222,7 @@ class UsersApiTest extends TestCase
{ {
$this->actingAsApiAdmin(); $this->actingAsApiAdmin();
/** @var User $user */ /** @var User $user */
$user = User::query()->where('id', '!=', $this->getAdmin()->id) $user = User::query()->where('id', '!=', $this->users->admin()->id)
->whereNull('system_name') ->whereNull('system_name')
->first(); ->first();
@@ -236,7 +236,7 @@ class UsersApiTest extends TestCase
{ {
$this->actingAsApiAdmin(); $this->actingAsApiAdmin();
/** @var User $user */ /** @var User $user */
$user = User::query()->where('id', '!=', $this->getAdmin()->id) $user = User::query()->where('id', '!=', $this->users->admin()->id)
->whereNull('system_name') ->whereNull('system_name')
->first(); ->first();
$entityChain = $this->entities->createChainBelongingToUser($user); $entityChain = $this->entities->createChainBelongingToUser($user);

View File

@@ -44,7 +44,7 @@ class AuthTest extends TestCase
public function test_mfa_session_cleared_on_logout() public function test_mfa_session_cleared_on_logout()
{ {
$user = $this->getEditor(); $user = $this->users->editor();
$mfaSession = $this->app->make(MfaSession::class); $mfaSession = $this->app->make(MfaSession::class);
$mfaSession->markVerifiedForUser($user); $mfaSession->markVerifiedForUser($user);
@@ -94,7 +94,7 @@ class AuthTest extends TestCase
public function test_login_authenticates_nonadmins_on_default_guard_only() public function test_login_authenticates_nonadmins_on_default_guard_only()
{ {
$editor = $this->getEditor(); $editor = $this->users->editor();
$editor->password = bcrypt('password'); $editor->password = bcrypt('password');
$editor->save(); $editor->save();
@@ -120,7 +120,7 @@ class AuthTest extends TestCase
public function test_logged_in_user_with_unconfirmed_email_is_logged_out() public function test_logged_in_user_with_unconfirmed_email_is_logged_out()
{ {
$this->setSettings(['registration-confirmation' => 'true']); $this->setSettings(['registration-confirmation' => 'true']);
$user = $this->getEditor(); $user = $this->users->editor();
$user->email_confirmed = false; $user->email_confirmed = false;
$user->save(); $user->save();

View File

@@ -11,7 +11,7 @@ class GroupSyncServiceTest extends TestCase
{ {
public function test_user_is_assigned_to_matching_roles() public function test_user_is_assigned_to_matching_roles()
{ {
$user = $this->getViewer(); $user = $this->users->viewer();
$roleA = Role::factory()->create(['display_name' => 'Wizards']); $roleA = Role::factory()->create(['display_name' => 'Wizards']);
$roleB = Role::factory()->create(['display_name' => 'Gremlins']); $roleB = Role::factory()->create(['display_name' => 'Gremlins']);
@@ -33,7 +33,7 @@ class GroupSyncServiceTest extends TestCase
public function test_multiple_values_in_role_external_auth_id_handled() public function test_multiple_values_in_role_external_auth_id_handled()
{ {
$user = $this->getViewer(); $user = $this->users->viewer();
$role = Role::factory()->create(['display_name' => 'ABC123', 'external_auth_id' => 'sales, engineering, developers, marketers']); $role = Role::factory()->create(['display_name' => 'ABC123', 'external_auth_id' => 'sales, engineering, developers, marketers']);
$this->assertFalse($user->hasRole($role->id)); $this->assertFalse($user->hasRole($role->id));
@@ -45,7 +45,7 @@ class GroupSyncServiceTest extends TestCase
public function test_commas_can_be_used_in_external_auth_id_if_escaped() public function test_commas_can_be_used_in_external_auth_id_if_escaped()
{ {
$user = $this->getViewer(); $user = $this->users->viewer();
$role = Role::factory()->create(['display_name' => 'ABC123', 'external_auth_id' => 'sales\,-developers, marketers']); $role = Role::factory()->create(['display_name' => 'ABC123', 'external_auth_id' => 'sales\,-developers, marketers']);
$this->assertFalse($user->hasRole($role->id)); $this->assertFalse($user->hasRole($role->id));
@@ -57,7 +57,7 @@ class GroupSyncServiceTest extends TestCase
public function test_external_auth_id_matches_ignoring_case() public function test_external_auth_id_matches_ignoring_case()
{ {
$user = $this->getViewer(); $user = $this->users->viewer();
$role = Role::factory()->create(['display_name' => 'ABC123', 'external_auth_id' => 'WaRRioRs']); $role = Role::factory()->create(['display_name' => 'ABC123', 'external_auth_id' => 'WaRRioRs']);
$this->assertFalse($user->hasRole($role->id)); $this->assertFalse($user->hasRole($role->id));

View File

@@ -235,7 +235,7 @@ class LdapTest extends TestCase
public function test_user_edit_form() public function test_user_edit_form()
{ {
$editUser = $this->getNormalUser(); $editUser = $this->users->viewer();
$editPage = $this->asAdmin()->get("/settings/users/{$editUser->id}"); $editPage = $this->asAdmin()->get("/settings/users/{$editUser->id}");
$editPage->assertSee('Edit User'); $editPage->assertSee('Edit User');
$editPage->assertDontSee('Password'); $editPage->assertDontSee('Password');
@@ -257,7 +257,7 @@ class LdapTest extends TestCase
public function test_non_admins_cannot_change_auth_id() public function test_non_admins_cannot_change_auth_id()
{ {
$testUser = $this->getNormalUser(); $testUser = $this->users->viewer();
$this->actingAs($testUser) $this->actingAs($testUser)
->get('/settings/users/' . $testUser->id) ->get('/settings/users/' . $testUser->id)
->assertDontSee('External Authentication'); ->assertDontSee('External Authentication');

View File

@@ -70,7 +70,7 @@ class LoginAutoInitiateTest extends TestCase
config()->set([ config()->set([
'auth.method' => 'oidc', 'auth.method' => 'oidc',
]); ]);
$this->actingAs($this->getEditor()); $this->actingAs($this->users->editor());
$req = $this->post('/logout'); $req = $this->post('/logout');
$req->assertRedirect('/login?prevent_auto_init=true'); $req->assertRedirect('/login?prevent_auto_init=true');

View File

@@ -13,7 +13,7 @@ class MfaConfigurationTest extends TestCase
{ {
public function test_totp_setup() public function test_totp_setup()
{ {
$editor = $this->getEditor(); $editor = $this->users->editor();
$this->assertDatabaseMissing('mfa_values', ['user_id' => $editor->id]); $this->assertDatabaseMissing('mfa_values', ['user_id' => $editor->id]);
// Setup page state // Setup page state
@@ -66,7 +66,7 @@ class MfaConfigurationTest extends TestCase
public function test_backup_codes_setup() public function test_backup_codes_setup()
{ {
$editor = $this->getEditor(); $editor = $this->users->editor();
$this->assertDatabaseMissing('mfa_values', ['user_id' => $editor->id]); $this->assertDatabaseMissing('mfa_values', ['user_id' => $editor->id]);
// Setup page state // Setup page state
@@ -112,8 +112,8 @@ class MfaConfigurationTest extends TestCase
public function test_mfa_method_count_is_visible_on_user_edit_page() public function test_mfa_method_count_is_visible_on_user_edit_page()
{ {
$user = $this->getEditor(); $user = $this->users->editor();
$resp = $this->actingAs($this->getAdmin())->get($user->getEditUrl()); $resp = $this->actingAs($this->users->admin())->get($user->getEditUrl());
$resp->assertSee('0 methods configured'); $resp->assertSee('0 methods configured');
MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, 'test'); MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, 'test');
@@ -127,17 +127,17 @@ class MfaConfigurationTest extends TestCase
public function test_mfa_setup_link_only_shown_when_viewing_own_user_edit_page() public function test_mfa_setup_link_only_shown_when_viewing_own_user_edit_page()
{ {
$admin = $this->getAdmin(); $admin = $this->users->admin();
$resp = $this->actingAs($admin)->get($admin->getEditUrl()); $resp = $this->actingAs($admin)->get($admin->getEditUrl());
$this->withHtml($resp)->assertElementExists('a[href$="/mfa/setup"]'); $this->withHtml($resp)->assertElementExists('a[href$="/mfa/setup"]');
$resp = $this->actingAs($admin)->get($this->getEditor()->getEditUrl()); $resp = $this->actingAs($admin)->get($this->users->editor()->getEditUrl());
$this->withHtml($resp)->assertElementNotExists('a[href$="/mfa/setup"]'); $this->withHtml($resp)->assertElementNotExists('a[href$="/mfa/setup"]');
} }
public function test_mfa_indicator_shows_in_user_list() public function test_mfa_indicator_shows_in_user_list()
{ {
$admin = $this->getAdmin(); $admin = $this->users->admin();
User::query()->where('id', '!=', $admin->id)->delete(); User::query()->where('id', '!=', $admin->id)->delete();
$resp = $this->actingAs($admin)->get('/settings/users'); $resp = $this->actingAs($admin)->get('/settings/users');
@@ -150,7 +150,7 @@ class MfaConfigurationTest extends TestCase
public function test_remove_mfa_method() public function test_remove_mfa_method()
{ {
$admin = $this->getAdmin(); $admin = $this->users->admin();
MfaValue::upsertWithValue($admin, MfaValue::METHOD_TOTP, 'test'); MfaValue::upsertWithValue($admin, MfaValue::METHOD_TOTP, 'test');
$this->assertEquals(1, $admin->mfaValues()->count()); $this->assertEquals(1, $admin->mfaValues()->count());
@@ -168,7 +168,7 @@ class MfaConfigurationTest extends TestCase
public function test_totp_setup_url_shows_correct_user_when_setup_forced_upon_login() public function test_totp_setup_url_shows_correct_user_when_setup_forced_upon_login()
{ {
$admin = $this->getAdmin(); $admin = $this->users->admin();
/** @var Role $role */ /** @var Role $role */
$role = $admin->roles()->first(); $role = $admin->roles()->first();
$role->mfa_enforced = true; $role->mfa_enforced = true;

View File

@@ -140,7 +140,7 @@ class MfaVerificationTest extends TestCase
public function test_both_mfa_options_available_if_set_on_profile() public function test_both_mfa_options_available_if_set_on_profile()
{ {
$user = $this->getEditor(); $user = $this->users->editor();
$user->password = Hash::make('password'); $user->password = Hash::make('password');
$user->save(); $user->save();
@@ -165,7 +165,7 @@ class MfaVerificationTest extends TestCase
public function test_mfa_required_with_no_methods_leads_to_setup() public function test_mfa_required_with_no_methods_leads_to_setup()
{ {
$user = $this->getEditor(); $user = $this->users->editor();
$user->password = Hash::make('password'); $user->password = Hash::make('password');
$user->save(); $user->save();
/** @var Role $role */ /** @var Role $role */
@@ -222,7 +222,7 @@ class MfaVerificationTest extends TestCase
// Attempted login user, who has configured mfa, access // Attempted login user, who has configured mfa, access
// Sets up user that has MFA required after attempted login. // Sets up user that has MFA required after attempted login.
$loginService = $this->app->make(LoginService::class); $loginService = $this->app->make(LoginService::class);
$user = $this->getEditor(); $user = $this->users->editor();
/** @var Role $role */ /** @var Role $role */
$role = $user->roles->first(); $role = $user->roles->first();
$role->mfa_enforced = true; $role->mfa_enforced = true;
@@ -257,7 +257,7 @@ class MfaVerificationTest extends TestCase
protected function startTotpLogin(): array protected function startTotpLogin(): array
{ {
$secret = $this->app->make(TotpService::class)->generateSecret(); $secret = $this->app->make(TotpService::class)->generateSecret();
$user = $this->getEditor(); $user = $this->users->editor();
$user->password = Hash::make('password'); $user->password = Hash::make('password');
$user->save(); $user->save();
MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, $secret); MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, $secret);
@@ -274,7 +274,7 @@ class MfaVerificationTest extends TestCase
*/ */
protected function startBackupCodeLogin($codes = ['kzzu6-1pgll', 'bzxnf-plygd', 'bwdsp-ysl51', '1vo93-ioy7n', 'lf7nw-wdyka', 'xmtrd-oplac']): array protected function startBackupCodeLogin($codes = ['kzzu6-1pgll', 'bzxnf-plygd', 'bwdsp-ysl51', '1vo93-ioy7n', 'lf7nw-wdyka', 'xmtrd-oplac']): array
{ {
$user = $this->getEditor(); $user = $this->users->editor();
$user->password = Hash::make('password'); $user->password = Hash::make('password');
$user->save(); $user->save();
MfaValue::upsertWithValue($user, MfaValue::METHOD_BACKUP_CODES, json_encode($codes)); MfaValue::upsertWithValue($user, MfaValue::METHOD_BACKUP_CODES, json_encode($codes));

View File

@@ -93,7 +93,7 @@ class OidcTest extends TestCase
public function test_logout_route_functions() public function test_logout_route_functions()
{ {
$this->actingAs($this->getEditor()); $this->actingAs($this->users->editor());
$this->post('/logout'); $this->post('/logout');
$this->assertFalse(auth()->check()); $this->assertFalse(auth()->check());
} }
@@ -228,7 +228,7 @@ class OidcTest extends TestCase
public function test_auth_login_as_existing_user() public function test_auth_login_as_existing_user()
{ {
$editor = $this->getEditor(); $editor = $this->users->editor();
$editor->external_auth_id = 'benny505'; $editor->external_auth_id = 'benny505';
$editor->save(); $editor->save();
@@ -245,7 +245,7 @@ class OidcTest extends TestCase
public function test_auth_login_as_existing_user_email_with_different_auth_id_fails() public function test_auth_login_as_existing_user_email_with_different_auth_id_fails()
{ {
$editor = $this->getEditor(); $editor = $this->users->editor();
$editor->external_auth_id = 'editor101'; $editor->external_auth_id = 'editor101';
$editor->save(); $editor->save();

View File

@@ -85,7 +85,7 @@ class ResetPasswordTest extends TestCase
public function test_reset_request_is_throttled() public function test_reset_request_is_throttled()
{ {
$editor = $this->getEditor(); $editor = $this->users->editor();
Notification::fake(); Notification::fake();
$this->get('/password/email'); $this->get('/password/email');
$this->followingRedirects()->post('/password/email', [ $this->followingRedirects()->post('/password/email', [

View File

@@ -170,7 +170,7 @@ class Saml2Test extends TestCase
'saml2.onelogin.strict' => false, 'saml2.onelogin.strict' => false,
]); ]);
$resp = $this->actingAs($this->getEditor())->get('/'); $resp = $this->actingAs($this->users->editor())->get('/');
$this->withHtml($resp)->assertElementContains('form[action$="/saml2/logout"] button', 'Logout'); $this->withHtml($resp)->assertElementContains('form[action$="/saml2/logout"] button', 'Logout');
} }

View File

@@ -77,18 +77,18 @@ class SocialAuthTest extends TestCase
// Test social callback with matching social account // Test social callback with matching social account
DB::table('social_accounts')->insert([ DB::table('social_accounts')->insert([
'user_id' => $this->getAdmin()->id, 'user_id' => $this->users->admin()->id,
'driver' => 'github', 'driver' => 'github',
'driver_id' => 'logintest123', 'driver_id' => 'logintest123',
]); ]);
$resp = $this->followingRedirects()->get('/login/service/github/callback'); $resp = $this->followingRedirects()->get('/login/service/github/callback');
$resp->assertDontSee('login-form'); $resp->assertDontSee('login-form');
$this->assertActivityExists(ActivityType::AUTH_LOGIN, null, 'github; (' . $this->getAdmin()->id . ') ' . $this->getAdmin()->name); $this->assertActivityExists(ActivityType::AUTH_LOGIN, null, 'github; (' . $this->users->admin()->id . ') ' . $this->users->admin()->name);
} }
public function test_social_account_detach() public function test_social_account_detach()
{ {
$editor = $this->getEditor(); $editor = $this->users->editor();
config([ config([
'GITHUB_APP_ID' => 'abc123', 'GITHUB_APP_SECRET' => '123abc', 'GITHUB_APP_ID' => 'abc123', 'GITHUB_APP_SECRET' => '123abc',
'APP_URL' => 'http://localhost', 'APP_URL' => 'http://localhost',

View File

@@ -17,7 +17,7 @@ class UserInviteTest extends TestCase
public function test_user_creation_creates_invite() public function test_user_creation_creates_invite()
{ {
Notification::fake(); Notification::fake();
$admin = $this->getAdmin(); $admin = $this->users->admin();
$email = Str::random(16) . '@example.com'; $email = Str::random(16) . '@example.com';
$resp = $this->actingAs($admin)->post('/settings/users/create', [ $resp = $this->actingAs($admin)->post('/settings/users/create', [
@@ -38,7 +38,7 @@ class UserInviteTest extends TestCase
public function test_user_invite_sent_in_selected_language() public function test_user_invite_sent_in_selected_language()
{ {
Notification::fake(); Notification::fake();
$admin = $this->getAdmin(); $admin = $this->users->admin();
$email = Str::random(16) . '@example.com'; $email = Str::random(16) . '@example.com';
$resp = $this->actingAs($admin)->post('/settings/users/create', [ $resp = $this->actingAs($admin)->post('/settings/users/create', [
@@ -62,7 +62,7 @@ class UserInviteTest extends TestCase
public function test_invite_set_password() public function test_invite_set_password()
{ {
Notification::fake(); Notification::fake();
$user = $this->getViewer(); $user = $this->users->viewer();
$inviteService = app(UserInviteService::class); $inviteService = app(UserInviteService::class);
$inviteService->sendInvitation($user); $inviteService->sendInvitation($user);
@@ -91,7 +91,7 @@ class UserInviteTest extends TestCase
public function test_invite_set_has_password_validation() public function test_invite_set_has_password_validation()
{ {
Notification::fake(); Notification::fake();
$user = $this->getViewer(); $user = $this->users->viewer();
$inviteService = app(UserInviteService::class); $inviteService = app(UserInviteService::class);
$inviteService->sendInvitation($user); $inviteService->sendInvitation($user);
@@ -126,7 +126,7 @@ class UserInviteTest extends TestCase
public function test_token_expires_after_two_weeks() public function test_token_expires_after_two_weeks()
{ {
Notification::fake(); Notification::fake();
$user = $this->getViewer(); $user = $this->users->viewer();
$inviteService = app(UserInviteService::class); $inviteService = app(UserInviteService::class);
$inviteService->sendInvitation($user); $inviteService->sendInvitation($user);

View File

@@ -19,7 +19,7 @@ class ClearActivityCommandTest extends TestCase
$this->assertDatabaseHas('activities', [ $this->assertDatabaseHas('activities', [
'type' => 'page_update', 'type' => 'page_update',
'entity_id' => $page->id, 'entity_id' => $page->id,
'user_id' => $this->getEditor()->id, 'user_id' => $this->users->editor()->id,
]); ]);
DB::rollBack(); DB::rollBack();

View File

@@ -16,7 +16,7 @@ class ClearViewsCommandTest extends TestCase
$this->get($page->getUrl()); $this->get($page->getUrl());
$this->assertDatabaseHas('views', [ $this->assertDatabaseHas('views', [
'user_id' => $this->getEditor()->id, 'user_id' => $this->users->editor()->id,
'viewable_id' => $page->id, 'viewable_id' => $page->id,
'views' => 1, 'views' => 1,
]); ]);
@@ -27,7 +27,7 @@ class ClearViewsCommandTest extends TestCase
$this->assertTrue($exitCode === 0, 'Command executed successfully'); $this->assertTrue($exitCode === 0, 'Command executed successfully');
$this->assertDatabaseMissing('views', [ $this->assertDatabaseMissing('views', [
'user_id' => $this->getEditor()->id, 'user_id' => $this->users->editor()->id,
]); ]);
} }
} }

View File

@@ -18,11 +18,11 @@ 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->users->editor()->roles()->first();
$this->assertFalse($child->hasPermissions(), '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->permissions->setEntityPermissions($shelf, ['view', 'update'], [$editorRole]);
$this->artisan('bookstack:copy-shelf-permissions', [ $this->artisan('bookstack:copy-shelf-permissions', [
'--slug' => $shelf->slug, '--slug' => $shelf->slug,
]); ]);
@@ -43,11 +43,11 @@ class CopyShelfPermissionsCommandTest extends TestCase
$shelf = $this->entities->shelf(); $shelf = $this->entities->shelf();
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->users->editor()->roles()->first();
$this->assertFalse($child->hasPermissions(), '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->permissions->setEntityPermissions($shelf, ['view', 'update'], [$editorRole]);
$this->artisan('bookstack:copy-shelf-permissions --all') $this->artisan('bookstack:copy-shelf-permissions --all')
->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();

View File

@@ -2,8 +2,7 @@
namespace Tests\Commands; namespace Tests\Commands;
use BookStack\Auth\Permissions\JointPermission; use BookStack\Auth\Permissions\CollapsedPermission;
use BookStack\Entities\Models\Page;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Tests\TestCase; use Tests\TestCase;
@@ -13,15 +12,23 @@ class RegeneratePermissionsCommandTest extends TestCase
public function test_regen_permissions_command() public function test_regen_permissions_command()
{ {
DB::rollBack(); DB::rollBack();
JointPermission::query()->truncate(); $page = $this->entities->page();
$page = Page::first(); $editor = $this->users->editor();
$this->permissions->addEntityPermission($page, ['view'], null, $editor);
CollapsedPermission::query()->truncate();
$this->assertDatabaseMissing('joint_permissions', ['entity_id' => $page->id]); $this->assertDatabaseMissing('entity_permissions_collapsed', ['entity_id' => $page->id]);
$exitCode = Artisan::call('bookstack:regenerate-permissions'); $exitCode = Artisan::call('bookstack:regenerate-permissions');
$this->assertTrue($exitCode === 0, 'Command executed successfully'); $this->assertTrue($exitCode === 0, 'Command executed successfully');
DB::beginTransaction();
$this->assertDatabaseHas('joint_permissions', ['entity_id' => $page->id]); $this->assertDatabaseHas('entity_permissions_collapsed', [
'entity_id' => $page->id,
'user_id' => $editor->id,
'view' => 1,
]);
CollapsedPermission::query()->truncate();
DB::beginTransaction();
} }
} }

View File

@@ -16,21 +16,20 @@ class BookShelfTest extends TestCase
public function test_shelves_shows_in_header_if_have_view_permissions() public function test_shelves_shows_in_header_if_have_view_permissions()
{ {
$viewer = $this->getViewer(); $viewer = $this->users->viewer();
$resp = $this->actingAs($viewer)->get('/'); $resp = $this->actingAs($viewer)->get('/');
$this->withHtml($resp)->assertElementContains('header', 'Shelves'); $this->withHtml($resp)->assertElementContains('header', 'Shelves');
$viewer->roles()->delete(); $viewer->roles()->delete();
$this->giveUserPermissions($viewer);
$resp = $this->actingAs($viewer)->get('/'); $resp = $this->actingAs($viewer)->get('/');
$this->withHtml($resp)->assertElementNotContains('header', 'Shelves'); $this->withHtml($resp)->assertElementNotContains('header', 'Shelves');
$this->giveUserPermissions($viewer, ['bookshelf-view-all']); $this->permissions->grantUserRolePermissions($viewer, ['bookshelf-view-all']);
$resp = $this->actingAs($viewer)->get('/'); $resp = $this->actingAs($viewer)->get('/');
$this->withHtml($resp)->assertElementContains('header', 'Shelves'); $this->withHtml($resp)->assertElementContains('header', 'Shelves');
$viewer->roles()->delete(); $viewer->roles()->delete();
$this->giveUserPermissions($viewer, ['bookshelf-view-own']); $this->permissions->grantUserRolePermissions($viewer, ['bookshelf-view-own']);
$resp = $this->actingAs($viewer)->get('/'); $resp = $this->actingAs($viewer)->get('/');
$this->withHtml($resp)->assertElementContains('header', 'Shelves'); $this->withHtml($resp)->assertElementContains('header', 'Shelves');
} }
@@ -38,14 +37,14 @@ class BookShelfTest extends TestCase
public function test_shelves_shows_in_header_if_have_any_shelve_view_permission() public function test_shelves_shows_in_header_if_have_any_shelve_view_permission()
{ {
$user = User::factory()->create(); $user = User::factory()->create();
$this->giveUserPermissions($user, ['image-create-all']); $this->permissions->grantUserRolePermissions($user, ['image-create-all']);
$shelf = $this->entities->shelf(); $shelf = $this->entities->shelf();
$userRole = $user->roles()->first(); $userRole = $user->roles()->first();
$resp = $this->actingAs($user)->get('/'); $resp = $this->actingAs($user)->get('/');
$this->withHtml($resp)->assertElementNotContains('header', 'Shelves'); $this->withHtml($resp)->assertElementNotContains('header', 'Shelves');
$this->entities->setPermissions($shelf, ['view'], [$userRole]); $this->permissions->setEntityPermissions($shelf, ['view'], [$userRole]);
$resp = $this->get('/'); $resp = $this->get('/');
$this->withHtml($resp)->assertElementContains('header', 'Shelves'); $this->withHtml($resp)->assertElementContains('header', 'Shelves');
@@ -69,7 +68,7 @@ class BookShelfTest extends TestCase
$resp->assertSee($book->name); $resp->assertSee($book->name);
$resp->assertSee($book->getUrl()); $resp->assertSee($book->getUrl());
$this->entities->setPermissions($book, []); $this->permissions->setEntityPermissions($book, []);
$resp = $this->asEditor()->get('/shelves'); $resp = $this->asEditor()->get('/shelves');
$resp->assertDontSee($book->name); $resp->assertDontSee($book->name);
@@ -93,7 +92,7 @@ class BookShelfTest extends TestCase
], ],
])); ]));
$resp->assertRedirect(); $resp->assertRedirect();
$editorId = $this->getEditor()->id; $editorId = $this->users->editor()->id;
$this->assertDatabaseHas('bookshelves', array_merge($shelfInfo, ['created_by' => $editorId, 'updated_by' => $editorId])); $this->assertDatabaseHas('bookshelves', array_merge($shelfInfo, ['created_by' => $editorId, 'updated_by' => $editorId]));
$shelf = Bookshelf::where('name', '=', $shelfInfo['name'])->first(); $shelf = Bookshelf::where('name', '=', $shelfInfo['name'])->first();
@@ -186,13 +185,13 @@ class BookShelfTest extends TestCase
$this->withHtml($resp)->assertElementContains('.book-content a.grid-card:nth-child(1)', $books[0]->name); $this->withHtml($resp)->assertElementContains('.book-content a.grid-card:nth-child(1)', $books[0]->name);
$this->withHtml($resp)->assertElementNotContains('.book-content a.grid-card:nth-child(3)', $books[0]->name); $this->withHtml($resp)->assertElementNotContains('.book-content a.grid-card:nth-child(3)', $books[0]->name);
setting()->putUser($this->getEditor(), 'shelf_books_sort_order', 'desc'); setting()->putUser($this->users->editor(), 'shelf_books_sort_order', 'desc');
$resp = $this->asEditor()->get($shelf->getUrl()); $resp = $this->asEditor()->get($shelf->getUrl());
$this->withHtml($resp)->assertElementNotContains('.book-content a.grid-card:nth-child(1)', $books[0]->name); $this->withHtml($resp)->assertElementNotContains('.book-content a.grid-card:nth-child(1)', $books[0]->name);
$this->withHtml($resp)->assertElementContains('.book-content a.grid-card:nth-child(3)', $books[0]->name); $this->withHtml($resp)->assertElementContains('.book-content a.grid-card:nth-child(3)', $books[0]->name);
setting()->putUser($this->getEditor(), 'shelf_books_sort_order', 'desc'); setting()->putUser($this->users->editor(), 'shelf_books_sort_order', 'desc');
setting()->putUser($this->getEditor(), 'shelf_books_sort', 'name'); setting()->putUser($this->users->editor(), 'shelf_books_sort', 'name');
$resp = $this->asEditor()->get($shelf->getUrl()); $resp = $this->asEditor()->get($shelf->getUrl());
$this->withHtml($resp)->assertElementContains('.book-content a.grid-card:nth-child(1)', 'hdgfgdfg'); $this->withHtml($resp)->assertElementContains('.book-content a.grid-card:nth-child(1)', 'hdgfgdfg');
$this->withHtml($resp)->assertElementContains('.book-content a.grid-card:nth-child(2)', 'bsfsdfsdfsd'); $this->withHtml($resp)->assertElementContains('.book-content a.grid-card:nth-child(2)', 'bsfsdfsdfsd');
@@ -224,7 +223,7 @@ class BookShelfTest extends TestCase
$resp->assertRedirect($shelf->getUrl()); $resp->assertRedirect($shelf->getUrl());
$this->assertSessionHas('success'); $this->assertSessionHas('success');
$editorId = $this->getEditor()->id; $editorId = $this->users->editor()->id;
$this->assertDatabaseHas('bookshelves', array_merge($shelfInfo, ['id' => $shelf->id, 'created_by' => $editorId, 'updated_by' => $editorId])); $this->assertDatabaseHas('bookshelves', array_merge($shelfInfo, ['id' => $shelf->id, 'created_by' => $editorId, 'updated_by' => $editorId]));
$shelfPage = $this->get($shelf->getUrl()); $shelfPage = $this->get($shelf->getUrl());
@@ -294,11 +293,11 @@ class BookShelfTest extends TestCase
$resp->assertSee("action=\"{$shelf->getUrl('/copy-permissions')}\"", false); $resp->assertSee("action=\"{$shelf->getUrl('/copy-permissions')}\"", false);
$child = $shelf->books()->first(); $child = $shelf->books()->first();
$editorRole = $this->getEditor()->roles()->first(); $editorRole = $this->users->editor()->roles()->first();
$this->assertFalse($child->hasPermissions(), '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->permissions->setEntityPermissions($shelf, ['view', 'update'], [$editorRole]);
$resp = $this->post($shelf->getUrl('/copy-permissions')); $resp = $this->post($shelf->getUrl('/copy-permissions'));
$child = $shelf->books()->first(); $child = $shelf->books()->first();

View File

@@ -221,7 +221,7 @@ class BookTest extends TestCase
public function test_books_view_shows_view_toggle_option() public function test_books_view_shows_view_toggle_option()
{ {
/** @var Book $book */ /** @var Book $book */
$editor = $this->getEditor(); $editor = $this->users->editor();
setting()->putUser($editor, 'books_view_type', 'list'); setting()->putUser($editor, 'books_view_type', 'list');
$resp = $this->actingAs($editor)->get('/books'); $resp = $this->actingAs($editor)->get('/books');
@@ -304,7 +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) {
$this->entities->setPermissions($child, [], []); $this->permissions->setEntityPermissions($child, [], []);
} }
$this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']); $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);
@@ -318,8 +318,8 @@ class BookTest extends TestCase
{ {
/** @var Book $book */ /** @var Book $book */
$book = Book::query()->whereHas('chapters')->whereHas('directPages')->whereHas('chapters')->first(); $book = Book::query()->whereHas('chapters')->whereHas('directPages')->whereHas('chapters')->first();
$viewer = $this->getViewer(); $viewer = $this->users->viewer();
$this->giveUserPermissions($viewer, ['book-create-all']); $this->permissions->grantUserRolePermissions($viewer, ['book-create-all']);
$this->actingAs($viewer)->post($book->getUrl('/copy'), ['name' => 'My copy book']); $this->actingAs($viewer)->post($book->getUrl('/copy'), ['name' => 'My copy book']);
/** @var Book $copy */ /** @var Book $copy */
@@ -354,9 +354,9 @@ class BookTest extends TestCase
$shelfA->appendBook($book); $shelfA->appendBook($book);
$shelfB->appendBook($book); $shelfB->appendBook($book);
$viewer = $this->getViewer(); $viewer = $this->users->viewer();
$this->giveUserPermissions($viewer, ['book-update-all', 'book-create-all', 'bookshelf-update-all']); $this->permissions->grantUserRolePermissions($viewer, ['book-update-all', 'book-create-all', 'bookshelf-update-all']);
$this->entities->setPermissions($shelfB); $this->permissions->setEntityPermissions($shelfB);
$this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']); $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);

View File

@@ -101,7 +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) {
$this->entities->setPermissions($page, [], []); $this->permissions->setEntityPermissions($page, [], []);
} }
$this->asEditor()->post($chapter->getUrl('/copy'), [ $this->asEditor()->post($chapter->getUrl('/copy'), [
@@ -116,8 +116,8 @@ class ChapterTest extends TestCase
public function test_copy_does_not_copy_pages_if_user_cant_page_create() public function test_copy_does_not_copy_pages_if_user_cant_page_create()
{ {
$chapter = $this->entities->chapterHasPages(); $chapter = $this->entities->chapterHasPages();
$viewer = $this->getViewer(); $viewer = $this->users->viewer();
$this->giveUserPermissions($viewer, ['chapter-create-all']); $this->permissions->grantUserRolePermissions($viewer, ['chapter-create-all']);
// Lacking permission results in no copied pages // Lacking permission results in no copied pages
$this->actingAs($viewer)->post($chapter->getUrl('/copy'), [ $this->actingAs($viewer)->post($chapter->getUrl('/copy'), [
@@ -128,7 +128,7 @@ class ChapterTest extends TestCase
$newChapter = Chapter::query()->where('name', '=', 'My copied chapter')->first(); $newChapter = Chapter::query()->where('name', '=', 'My copied chapter')->first();
$this->assertEquals(0, $newChapter->pages()->count()); $this->assertEquals(0, $newChapter->pages()->count());
$this->giveUserPermissions($viewer, ['page-create-all']); $this->permissions->grantUserRolePermissions($viewer, ['page-create-all']);
// Having permission rules in copied pages // Having permission rules in copied pages
$this->actingAs($viewer)->post($chapter->getUrl('/copy'), [ $this->actingAs($viewer)->post($chapter->getUrl('/copy'), [
@@ -144,7 +144,7 @@ class ChapterTest extends TestCase
{ {
$chapter = $this->entities->chapter(); $chapter = $this->entities->chapter();
$resp = $this->actingAs($this->getViewer())->get($chapter->getUrl()); $resp = $this->actingAs($this->users->viewer())->get($chapter->getUrl());
$this->withHtml($resp)->assertLinkNotExists($chapter->book->getUrl('sort')); $this->withHtml($resp)->assertLinkNotExists($chapter->book->getUrl('sort'));
$resp = $this->asEditor()->get($chapter->getUrl()); $resp = $this->asEditor()->get($chapter->getUrl());

View File

@@ -49,16 +49,16 @@ class ConvertTest extends TestCase
public function test_convert_chapter_to_book_requires_permissions() public function test_convert_chapter_to_book_requires_permissions()
{ {
$chapter = $this->entities->chapter(); $chapter = $this->entities->chapter();
$user = $this->getViewer(); $user = $this->users->viewer();
$permissions = ['chapter-delete-all', 'book-create-all', 'chapter-update-all']; $permissions = ['chapter-delete-all', 'book-create-all', 'chapter-update-all'];
$this->giveUserPermissions($user, $permissions); $this->permissions->grantUserRolePermissions($user, $permissions);
foreach ($permissions as $permission) { foreach ($permissions as $permission) {
$this->removePermissionFromUser($user, $permission); $this->permissions->removeUserRolePermissions($user, [$permission]);
$resp = $this->actingAs($user)->post($chapter->getUrl('/convert-to-book')); $resp = $this->actingAs($user)->post($chapter->getUrl('/convert-to-book'));
$this->assertPermissionError($resp); $this->assertPermissionError($resp);
$this->giveUserPermissions($user, [$permission]); $this->permissions->grantUserRolePermissions($user, [$permission]);
} }
$resp = $this->actingAs($user)->post($chapter->getUrl('/convert-to-book')); $resp = $this->actingAs($user)->post($chapter->getUrl('/convert-to-book'));
@@ -122,16 +122,16 @@ class ConvertTest extends TestCase
public function test_book_convert_to_shelf_requires_permissions() public function test_book_convert_to_shelf_requires_permissions()
{ {
$book = $this->entities->book(); $book = $this->entities->book();
$user = $this->getViewer(); $user = $this->users->viewer();
$permissions = ['book-delete-all', 'bookshelf-create-all', 'book-update-all', 'book-create-all']; $permissions = ['book-delete-all', 'bookshelf-create-all', 'book-update-all', 'book-create-all'];
$this->giveUserPermissions($user, $permissions); $this->permissions->grantUserRolePermissions($user, $permissions);
foreach ($permissions as $permission) { foreach ($permissions as $permission) {
$this->removePermissionFromUser($user, $permission); $this->permissions->removeUserRolePermissions($user, [$permission]);
$resp = $this->actingAs($user)->post($book->getUrl('/convert-to-shelf')); $resp = $this->actingAs($user)->post($book->getUrl('/convert-to-shelf'));
$this->assertPermissionError($resp); $this->assertPermissionError($resp);
$this->giveUserPermissions($user, [$permission]); $this->permissions->grantUserRolePermissions($user, [$permission]);
} }
$resp = $this->actingAs($user)->post($book->getUrl('/convert-to-shelf')); $resp = $this->actingAs($user)->post($book->getUrl('/convert-to-shelf'));

View File

@@ -11,8 +11,8 @@ class EntityAccessTest extends TestCase
public function test_entities_viewable_after_creator_deletion() public function test_entities_viewable_after_creator_deletion()
{ {
// Create required assets and revisions // Create required assets and revisions
$creator = $this->getEditor(); $creator = $this->users->editor();
$updater = $this->getViewer(); $updater = $this->users->viewer();
$entities = $this->entities->createChainBelongingToUser($creator, $updater); $entities = $this->entities->createChainBelongingToUser($creator, $updater);
app()->make(UserRepo::class)->destroy($creator); app()->make(UserRepo::class)->destroy($creator);
$this->entities->updatePage($entities['page'], ['html' => '<p>hello!</p>>']); $this->entities->updatePage($entities['page'], ['html' => '<p>hello!</p>>']);
@@ -23,8 +23,8 @@ class EntityAccessTest extends TestCase
public function test_entities_viewable_after_updater_deletion() public function test_entities_viewable_after_updater_deletion()
{ {
// Create required assets and revisions // Create required assets and revisions
$creator = $this->getViewer(); $creator = $this->users->viewer();
$updater = $this->getEditor(); $updater = $this->users->editor();
$entities = $this->entities->createChainBelongingToUser($creator, $updater); $entities = $this->entities->createChainBelongingToUser($creator, $updater);
app()->make(UserRepo::class)->destroy($updater); app()->make(UserRepo::class)->destroy($updater);
$this->entities->updatePage($entities['page'], ['html' => '<p>Hello there!</p>']); $this->entities->updatePage($entities['page'], ['html' => '<p>Hello there!</p>']);

View File

@@ -132,7 +132,7 @@ 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']);
$editor = $this->getEditor(); $editor = $this->users->editor();
$this->actingAs($editor); $this->actingAs($editor);
// Viewed filter searches // Viewed filter searches
@@ -171,7 +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);
$this->entities->setPermissions($page, ['view'], [$editor->roles->first()]); $this->permissions->setEntityPermissions($page, ['view'], [$editor->roles->first()]);
$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
@@ -235,7 +235,7 @@ class EntitySearchTest extends TestCase
$this->withHtml($resp)->assertElementContains($baseSelector, $page->name); $this->withHtml($resp)->assertElementContains($baseSelector, $page->name);
$this->withHtml($resp)->assertElementNotContains($baseSelector, "You don't have the required permissions to select this item"); $this->withHtml($resp)->assertElementNotContains($baseSelector, "You don't have the required permissions to select this item");
$resp = $this->actingAs($this->getViewer())->get($searchUrl); $resp = $this->actingAs($this->users->viewer())->get($searchUrl);
$this->withHtml($resp)->assertElementContains($baseSelector, $page->name); $this->withHtml($resp)->assertElementContains($baseSelector, $page->name);
$this->withHtml($resp)->assertElementContains($baseSelector, "You don't have the required permissions to select this item"); $this->withHtml($resp)->assertElementContains($baseSelector, "You don't have the required permissions to select this item");
} }
@@ -246,7 +246,7 @@ class EntitySearchTest extends TestCase
$this->assertGreaterThan(2, count($chapter->pages), 'Ensure we\'re testing with at least 1 sibling'); $this->assertGreaterThan(2, count($chapter->pages), 'Ensure we\'re testing with at least 1 sibling');
$page = $chapter->pages->first(); $page = $chapter->pages->first();
$search = $this->actingAs($this->getViewer())->get("/search/entity/siblings?entity_id={$page->id}&entity_type=page"); $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$page->id}&entity_type=page");
$search->assertSuccessful(); $search->assertSuccessful();
foreach ($chapter->pages as $page) { foreach ($chapter->pages as $page) {
$search->assertSee($page->name); $search->assertSee($page->name);
@@ -261,7 +261,7 @@ class EntitySearchTest extends TestCase
$bookChildren = $page->book->getDirectChildren(); $bookChildren = $page->book->getDirectChildren();
$this->assertGreaterThan(2, count($bookChildren), 'Ensure we\'re testing with at least 1 sibling'); $this->assertGreaterThan(2, count($bookChildren), 'Ensure we\'re testing with at least 1 sibling');
$search = $this->actingAs($this->getViewer())->get("/search/entity/siblings?entity_id={$page->id}&entity_type=page"); $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$page->id}&entity_type=page");
$search->assertSuccessful(); $search->assertSuccessful();
foreach ($bookChildren as $child) { foreach ($bookChildren as $child) {
$search->assertSee($child->name); $search->assertSee($child->name);
@@ -276,7 +276,7 @@ class EntitySearchTest extends TestCase
$bookChildren = $chapter->book->getDirectChildren(); $bookChildren = $chapter->book->getDirectChildren();
$this->assertGreaterThan(2, count($bookChildren), 'Ensure we\'re testing with at least 1 sibling'); $this->assertGreaterThan(2, count($bookChildren), 'Ensure we\'re testing with at least 1 sibling');
$search = $this->actingAs($this->getViewer())->get("/search/entity/siblings?entity_id={$chapter->id}&entity_type=chapter"); $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$chapter->id}&entity_type=chapter");
$search->assertSuccessful(); $search->assertSuccessful();
foreach ($bookChildren as $child) { foreach ($bookChildren as $child) {
$search->assertSee($child->name); $search->assertSee($child->name);
@@ -291,7 +291,7 @@ class EntitySearchTest extends TestCase
$book = $books->first(); $book = $books->first();
$this->assertGreaterThan(2, count($books), 'Ensure we\'re testing with at least 1 sibling'); $this->assertGreaterThan(2, count($books), 'Ensure we\'re testing with at least 1 sibling');
$search = $this->actingAs($this->getViewer())->get("/search/entity/siblings?entity_id={$book->id}&entity_type=book"); $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$book->id}&entity_type=book");
$search->assertSuccessful(); $search->assertSuccessful();
foreach ($books as $expectedBook) { foreach ($books as $expectedBook) {
$search->assertSee($expectedBook->name); $search->assertSee($expectedBook->name);
@@ -304,7 +304,7 @@ class EntitySearchTest extends TestCase
$shelf = $shelves->first(); $shelf = $shelves->first();
$this->assertGreaterThan(2, count($shelves), 'Ensure we\'re testing with at least 1 sibling'); $this->assertGreaterThan(2, count($shelves), 'Ensure we\'re testing with at least 1 sibling');
$search = $this->actingAs($this->getViewer())->get("/search/entity/siblings?entity_id={$shelf->id}&entity_type=bookshelf"); $search = $this->actingAs($this->users->viewer())->get("/search/entity/siblings?entity_id={$shelf->id}&entity_type=bookshelf");
$search->assertSuccessful(); $search->assertSuccessful();
foreach ($shelves as $expectedShelf) { foreach ($shelves as $expectedShelf) {
$search->assertSee($expectedShelf->name); $search->assertSee($expectedShelf->name);

View File

@@ -275,7 +275,7 @@ class ExportTest extends TestCase
public function test_page_export_with_deleted_creator_and_updater() public function test_page_export_with_deleted_creator_and_updater()
{ {
$user = $this->getViewer(['name' => 'ExportWizardTheFifth']); $user = $this->users->viewer(['name' => 'ExportWizardTheFifth']);
$page = $this->entities->page(); $page = $this->entities->page();
$page->created_by = $user->id; $page->created_by = $user->id;
$page->updated_by = $user->id; $page->updated_by = $user->id;
@@ -409,7 +409,7 @@ class ExportTest extends TestCase
$chapter = $book->chapters()->first(); $chapter = $book->chapters()->first();
$page = $chapter->pages()->first(); $page = $chapter->pages()->first();
$entities = [$book, $chapter, $page]; $entities = [$book, $chapter, $page];
$user = $this->getViewer(); $user = $this->users->viewer();
$this->actingAs($user); $this->actingAs($user);
foreach ($entities as $entity) { foreach ($entities as $entity) {
@@ -417,8 +417,7 @@ class ExportTest extends TestCase
$resp->assertSee('/export/pdf'); $resp->assertSee('/export/pdf');
} }
/** @var Role $role */ $this->permissions->removeUserRolePermissions($user, ['content-export']);
$this->removePermissionFromUser($user, 'content-export');
foreach ($entities as $entity) { foreach ($entities as $entity) {
$resp = $this->get($entity->getUrl()); $resp = $this->get($entity->getUrl());

View File

@@ -483,7 +483,7 @@ class PageContentTest extends TestCase
{ {
$page = $this->entities->page(); $page = $this->entities->page();
$this->actingAs($this->getAdmin()) $this->actingAs($this->users->admin())
->put($page->getUrl(''), [ ->put($page->getUrl(''), [
'name' => 'Testing', 'name' => 'Testing',
'html' => '<p>&quot;Hello &amp; welcome&quot;</p>', 'html' => '<p>&quot;Hello &amp; welcome&quot;</p>',

View File

@@ -39,7 +39,7 @@ class PageDraftTest extends TestCase
$this->withHtml($resp)->assertElementNotContains('[name="html"]', $addedContent); $this->withHtml($resp)->assertElementNotContains('[name="html"]', $addedContent);
$newContent = $this->page->html . $addedContent; $newContent = $this->page->html . $addedContent;
$newUser = $this->getEditor(); $newUser = $this->users->editor();
$this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]); $this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]);
$resp = $this->actingAs($newUser)->get($this->page->getUrl('/edit')); $resp = $this->actingAs($newUser)->get($this->page->getUrl('/edit'));
@@ -62,7 +62,7 @@ class PageDraftTest extends TestCase
$this->withHtml($resp)->assertElementNotContains('[name="html"]', $addedContent); $this->withHtml($resp)->assertElementNotContains('[name="html"]', $addedContent);
$newContent = $this->page->html . $addedContent; $newContent = $this->page->html . $addedContent;
$newUser = $this->getEditor(); $newUser = $this->users->editor();
$this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]); $this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]);
$this->actingAs($newUser) $this->actingAs($newUser)
@@ -75,8 +75,8 @@ class PageDraftTest extends TestCase
public function test_draft_save_shows_alert_if_draft_older_than_last_page_update() public function test_draft_save_shows_alert_if_draft_older_than_last_page_update()
{ {
$admin = $this->getAdmin(); $admin = $this->users->admin();
$editor = $this->getEditor(); $editor = $this->users->editor();
$page = $this->entities->page(); $page = $this->entities->page();
$this->actingAs($editor)->put('/ajax/page/' . $page->id . '/save-draft', [ $this->actingAs($editor)->put('/ajax/page/' . $page->id . '/save-draft', [
@@ -109,8 +109,8 @@ class PageDraftTest extends TestCase
public function test_draft_save_shows_alert_if_draft_edit_started_by_someone_else() public function test_draft_save_shows_alert_if_draft_edit_started_by_someone_else()
{ {
$admin = $this->getAdmin(); $admin = $this->users->admin();
$editor = $this->getEditor(); $editor = $this->users->editor();
$page = $this->entities->page(); $page = $this->entities->page();
$this->actingAs($admin)->put('/ajax/page/' . $page->id . '/save-draft', [ $this->actingAs($admin)->put('/ajax/page/' . $page->id . '/save-draft', [
@@ -143,7 +143,7 @@ class PageDraftTest extends TestCase
{ {
$book = $this->entities->book(); $book = $this->entities->book();
$chapter = $book->chapters->first(); $chapter = $book->chapters->first();
$newUser = $this->getEditor(); $newUser = $this->users->editor();
$this->actingAs($newUser)->get($book->getUrl('/create-page')); $this->actingAs($newUser)->get($book->getUrl('/create-page'));
$this->get($chapter->getUrl('/create-page')); $this->get($chapter->getUrl('/create-page'));

View File

@@ -208,13 +208,13 @@ class PageRevisionTest extends TestCase
$page = $this->entities->page(); $page = $this->entities->page();
$this->createRevisions($page, 2); $this->createRevisions($page, 2);
$viewer = $this->getViewer(); $viewer = $this->users->viewer();
$this->actingAs($viewer); $this->actingAs($viewer);
$respHtml = $this->withHtml($this->get($page->getUrl('/revisions'))); $respHtml = $this->withHtml($this->get($page->getUrl('/revisions')));
$respHtml->assertElementNotContains('.actions a', 'Restore'); $respHtml->assertElementNotContains('.actions a', 'Restore');
$respHtml->assertElementNotExists('form[action$="/restore"]'); $respHtml->assertElementNotExists('form[action$="/restore"]');
$this->giveUserPermissions($viewer, ['page-update-all']); $this->permissions->grantUserRolePermissions($viewer, ['page-update-all']);
$respHtml = $this->withHtml($this->get($page->getUrl('/revisions'))); $respHtml = $this->withHtml($this->get($page->getUrl('/revisions')));
$respHtml->assertElementContains('.actions a', 'Restore'); $respHtml->assertElementContains('.actions a', 'Restore');
@@ -226,13 +226,13 @@ class PageRevisionTest extends TestCase
$page = $this->entities->page(); $page = $this->entities->page();
$this->createRevisions($page, 2); $this->createRevisions($page, 2);
$viewer = $this->getViewer(); $viewer = $this->users->viewer();
$this->actingAs($viewer); $this->actingAs($viewer);
$respHtml = $this->withHtml($this->get($page->getUrl('/revisions'))); $respHtml = $this->withHtml($this->get($page->getUrl('/revisions')));
$respHtml->assertElementNotContains('.actions a', 'Delete'); $respHtml->assertElementNotContains('.actions a', 'Delete');
$respHtml->assertElementNotExists('form[action$="/delete"]'); $respHtml->assertElementNotExists('form[action$="/delete"]');
$this->giveUserPermissions($viewer, ['page-delete-all']); $this->permissions->grantUserRolePermissions($viewer, ['page-delete-all']);
$respHtml = $this->withHtml($this->get($page->getUrl('/revisions'))); $respHtml = $this->withHtml($this->get($page->getUrl('/revisions')));
$respHtml->assertElementContains('.actions a', 'Delete'); $respHtml->assertElementContains('.actions a', 'Delete');

View File

@@ -25,7 +25,7 @@ class PageTemplateTest extends TestCase
public function test_manage_templates_permission_required_to_change_page_template_status() public function test_manage_templates_permission_required_to_change_page_template_status()
{ {
$page = $this->entities->page(); $page = $this->entities->page();
$editor = $this->getEditor(); $editor = $this->users->editor();
$this->actingAs($editor); $this->actingAs($editor);
$pageUpdateData = [ $pageUpdateData = [
@@ -40,7 +40,7 @@ class PageTemplateTest extends TestCase
'template' => false, 'template' => false,
]); ]);
$this->giveUserPermissions($editor, ['templates-manage']); $this->permissions->grantUserRolePermissions($editor, ['templates-manage']);
$this->put($page->getUrl(), $pageUpdateData); $this->put($page->getUrl(), $pageUpdateData);
$this->assertDatabaseHas('pages', [ $this->assertDatabaseHas('pages', [
@@ -53,7 +53,7 @@ class PageTemplateTest extends TestCase
{ {
$content = '<div>my_custom_template_content</div>'; $content = '<div>my_custom_template_content</div>';
$page = $this->entities->page(); $page = $this->entities->page();
$editor = $this->getEditor(); $editor = $this->users->editor();
$this->actingAs($editor); $this->actingAs($editor);
$templateFetch = $this->get('/templates/' . $page->id); $templateFetch = $this->get('/templates/' . $page->id);
@@ -73,7 +73,7 @@ class PageTemplateTest extends TestCase
public function test_template_endpoint_returns_paginated_list_of_templates() public function test_template_endpoint_returns_paginated_list_of_templates()
{ {
$editor = $this->getEditor(); $editor = $this->users->editor();
$this->actingAs($editor); $this->actingAs($editor);
$toBeTemplates = Page::query()->orderBy('name', 'asc')->take(12)->get(); $toBeTemplates = Page::query()->orderBy('name', 'asc')->take(12)->get();

View File

@@ -38,8 +38,8 @@ class PageTest extends TestCase
public function test_page_view_when_creator_is_deleted_but_owner_exists() public function test_page_view_when_creator_is_deleted_but_owner_exists()
{ {
$page = $this->entities->page(); $page = $this->entities->page();
$user = $this->getViewer(); $user = $this->users->viewer();
$owner = $this->getEditor(); $owner = $this->users->editor();
$page->created_by = $user->id; $page->created_by = $user->id;
$page->owned_by = $owner->id; $page->owned_by = $owner->id;
$page->save(); $page->save();
@@ -190,15 +190,15 @@ class PageTest extends TestCase
$page = $this->entities->page(); $page = $this->entities->page();
$currentBook = $page->book; $currentBook = $page->book;
$newBook = Book::where('id', '!=', $currentBook->id)->first(); $newBook = Book::where('id', '!=', $currentBook->id)->first();
$viewer = $this->getViewer(); $viewer = $this->users->viewer();
$resp = $this->actingAs($viewer)->get($page->getUrl()); $resp = $this->actingAs($viewer)->get($page->getUrl());
$resp->assertDontSee($page->getUrl('/copy')); $resp->assertDontSee($page->getUrl('/copy'));
$newBook->owned_by = $viewer->id; $newBook->owned_by = $viewer->id;
$newBook->save(); $newBook->save();
$this->giveUserPermissions($viewer, ['page-create-own']); $this->permissions->grantUserRolePermissions($viewer, ['page-create-own']);
$this->entities->regenPermissions($newBook); $this->permissions->regenerateForEntity($newBook);
$resp = $this->actingAs($viewer)->get($page->getUrl()); $resp = $this->actingAs($viewer)->get($page->getUrl());
$resp->assertSee($page->getUrl('/copy')); $resp->assertSee($page->getUrl('/copy'));
@@ -249,7 +249,7 @@ class PageTest extends TestCase
public function test_recently_updated_pages_view() public function test_recently_updated_pages_view()
{ {
$user = $this->getEditor(); $user = $this->users->editor();
$content = $this->entities->createChainBelongingToUser($user); $content = $this->entities->createChainBelongingToUser($user);
$resp = $this->asAdmin()->get('/pages/recently-updated'); $resp = $this->asAdmin()->get('/pages/recently-updated');
@@ -258,7 +258,7 @@ class PageTest extends TestCase
public function test_recently_updated_pages_view_shows_updated_by_details() public function test_recently_updated_pages_view_shows_updated_by_details()
{ {
$user = $this->getEditor(); $user = $this->users->editor();
$page = $this->entities->page(); $page = $this->entities->page();
$this->actingAs($user)->put($page->getUrl(), [ $this->actingAs($user)->put($page->getUrl(), [
@@ -272,7 +272,7 @@ class PageTest extends TestCase
public function test_recently_updated_pages_view_shows_parent_chain() public function test_recently_updated_pages_view_shows_parent_chain()
{ {
$user = $this->getEditor(); $user = $this->users->editor();
$page = $this->entities->pageWithinChapter(); $page = $this->entities->pageWithinChapter();
$this->actingAs($user)->put($page->getUrl(), [ $this->actingAs($user)->put($page->getUrl(), [
@@ -287,7 +287,7 @@ class PageTest extends TestCase
public function test_recently_updated_pages_view_does_not_show_parent_if_not_visible() public function test_recently_updated_pages_view_does_not_show_parent_if_not_visible()
{ {
$user = $this->getEditor(); $user = $this->users->editor();
$page = $this->entities->pageWithinChapter(); $page = $this->entities->pageWithinChapter();
$this->actingAs($user)->put($page->getUrl(), [ $this->actingAs($user)->put($page->getUrl(), [
@@ -295,8 +295,8 @@ class PageTest extends TestCase
'html' => '<p>Updated content</p>', 'html' => '<p>Updated content</p>',
]); ]);
$this->entities->setPermissions($page->book); $this->permissions->setEntityPermissions($page->book);
$this->entities->setPermissions($page, ['view'], [$user->roles->first()]); $this->permissions->setEntityPermissions($page, ['view'], [$user->roles->first()]);
$resp = $this->get('/pages/recently-updated'); $resp = $this->get('/pages/recently-updated');
$resp->assertDontSee($page->book->getShortName(42)); $resp->assertDontSee($page->book->getShortName(42));

View File

@@ -53,7 +53,7 @@ class SortTest extends TestCase
$newBook = Book::query()->where('id', '!=', $currentBook->id)->first(); $newBook = Book::query()->where('id', '!=', $currentBook->id)->first();
$newChapter = $newBook->chapters()->first(); $newChapter = $newBook->chapters()->first();
$movePageResp = $this->actingAs($this->getEditor())->put($page->getUrl('/move'), [ $movePageResp = $this->actingAs($this->users->editor())->put($page->getUrl('/move'), [
'entity_selection' => 'chapter:' . $newChapter->id, 'entity_selection' => 'chapter:' . $newChapter->id,
]); ]);
$page->refresh(); $page->refresh();
@@ -71,7 +71,7 @@ class SortTest extends TestCase
$page = $oldChapter->pages()->first(); $page = $oldChapter->pages()->first();
$newBook = Book::query()->where('id', '!=', $oldChapter->book_id)->first(); $newBook = Book::query()->where('id', '!=', $oldChapter->book_id)->first();
$movePageResp = $this->actingAs($this->getEditor())->put($page->getUrl('/move'), [ $movePageResp = $this->actingAs($this->users->editor())->put($page->getUrl('/move'), [
'entity_selection' => 'book:' . $newBook->id, 'entity_selection' => 'book:' . $newBook->id,
]); ]);
$page->refresh(); $page->refresh();
@@ -89,16 +89,16 @@ class SortTest extends TestCase
$page = $this->entities->page(); $page = $this->entities->page();
$currentBook = $page->book; $currentBook = $page->book;
$newBook = Book::query()->where('id', '!=', $currentBook->id)->first(); $newBook = Book::query()->where('id', '!=', $currentBook->id)->first();
$editor = $this->getEditor(); $editor = $this->users->editor();
$this->entities->setPermissions($newBook, ['view', 'update', 'delete'], $editor->roles->all()); $this->permissions->setEntityPermissions($newBook, ['view', 'update', 'delete'], $editor->roles->all());
$movePageResp = $this->actingAs($editor)->put($page->getUrl('/move'), [ $movePageResp = $this->actingAs($editor)->put($page->getUrl('/move'), [
'entity_selection' => 'book:' . $newBook->id, 'entity_selection' => 'book:' . $newBook->id,
]); ]);
$this->assertPermissionError($movePageResp); $this->assertPermissionError($movePageResp);
$this->entities->setPermissions($newBook, ['view', 'update', 'delete', 'create'], $editor->roles->all()); $this->permissions->setEntityPermissions($newBook, ['view', 'update', 'delete', 'create'], $editor->roles->all());
$movePageResp = $this->put($page->getUrl('/move'), [ $movePageResp = $this->put($page->getUrl('/move'), [
'entity_selection' => 'book:' . $newBook->id, 'entity_selection' => 'book:' . $newBook->id,
]); ]);
@@ -114,10 +114,10 @@ class SortTest extends TestCase
$page = $this->entities->page(); $page = $this->entities->page();
$currentBook = $page->book; $currentBook = $page->book;
$newBook = Book::query()->where('id', '!=', $currentBook->id)->first(); $newBook = Book::query()->where('id', '!=', $currentBook->id)->first();
$editor = $this->getEditor(); $editor = $this->users->editor();
$this->entities->setPermissions($newBook, ['view', 'update', 'create', 'delete'], $editor->roles->all()); $this->permissions->setEntityPermissions($newBook, ['view', 'update', 'create', 'delete'], $editor->roles->all());
$this->entities->setPermissions($page, ['view', 'update', 'create'], $editor->roles->all()); $this->permissions->setEntityPermissions($page, ['view', 'update', 'create'], $editor->roles->all());
$movePageResp = $this->actingAs($editor)->put($page->getUrl('/move'), [ $movePageResp = $this->actingAs($editor)->put($page->getUrl('/move'), [
'entity_selection' => 'book:' . $newBook->id, 'entity_selection' => 'book:' . $newBook->id,
@@ -126,7 +126,7 @@ class SortTest extends TestCase
$pageView = $this->get($page->getUrl()); $pageView = $this->get($page->getUrl());
$pageView->assertDontSee($page->getUrl('/move')); $pageView->assertDontSee($page->getUrl('/move'));
$this->entities->setPermissions($page, ['view', 'update', 'create', 'delete'], $editor->roles->all()); $this->permissions->setEntityPermissions($page, ['view', 'update', 'create', 'delete'], $editor->roles->all());
$movePageResp = $this->put($page->getUrl('/move'), [ $movePageResp = $this->put($page->getUrl('/move'), [
'entity_selection' => 'book:' . $newBook->id, 'entity_selection' => 'book:' . $newBook->id,
]); ]);
@@ -169,10 +169,10 @@ class SortTest extends TestCase
$chapter = $this->entities->chapter(); $chapter = $this->entities->chapter();
$currentBook = $chapter->book; $currentBook = $chapter->book;
$newBook = Book::query()->where('id', '!=', $currentBook->id)->first(); $newBook = Book::query()->where('id', '!=', $currentBook->id)->first();
$editor = $this->getEditor(); $editor = $this->users->editor();
$this->entities->setPermissions($newBook, ['view', 'update', 'create', 'delete'], $editor->roles->all()); $this->permissions->setEntityPermissions($newBook, ['view', 'update', 'create', 'delete'], $editor->roles->all());
$this->entities->setPermissions($chapter, ['view', 'update', 'create'], $editor->roles->all()); $this->permissions->setEntityPermissions($chapter, ['view', 'update', 'create'], $editor->roles->all());
$moveChapterResp = $this->actingAs($editor)->put($chapter->getUrl('/move'), [ $moveChapterResp = $this->actingAs($editor)->put($chapter->getUrl('/move'), [
'entity_selection' => 'book:' . $newBook->id, 'entity_selection' => 'book:' . $newBook->id,
@@ -181,7 +181,7 @@ class SortTest extends TestCase
$pageView = $this->get($chapter->getUrl()); $pageView = $this->get($chapter->getUrl());
$pageView->assertDontSee($chapter->getUrl('/move')); $pageView->assertDontSee($chapter->getUrl('/move'));
$this->entities->setPermissions($chapter, ['view', 'update', 'create', 'delete'], $editor->roles->all()); $this->permissions->setEntityPermissions($chapter, ['view', 'update', 'create', 'delete'], $editor->roles->all());
$moveChapterResp = $this->put($chapter->getUrl('/move'), [ $moveChapterResp = $this->put($chapter->getUrl('/move'), [
'entity_selection' => 'book:' . $newBook->id, 'entity_selection' => 'book:' . $newBook->id,
]); ]);
@@ -196,17 +196,17 @@ class SortTest extends TestCase
$chapter = $this->entities->chapter(); $chapter = $this->entities->chapter();
$currentBook = $chapter->book; $currentBook = $chapter->book;
$newBook = Book::query()->where('id', '!=', $currentBook->id)->first(); $newBook = Book::query()->where('id', '!=', $currentBook->id)->first();
$editor = $this->getEditor(); $editor = $this->users->editor();
$this->entities->setPermissions($newBook, ['view', 'update', 'delete'], [$editor->roles->first()]); $this->permissions->setEntityPermissions($newBook, ['view', 'update', 'delete'], [$editor->roles->first()]);
$this->entities->setPermissions($chapter, ['view', 'update', 'create', 'delete'], [$editor->roles->first()]); $this->permissions->setEntityPermissions($chapter, ['view', 'update', 'create', 'delete'], [$editor->roles->first()]);
$moveChapterResp = $this->actingAs($editor)->put($chapter->getUrl('/move'), [ $moveChapterResp = $this->actingAs($editor)->put($chapter->getUrl('/move'), [
'entity_selection' => 'book:' . $newBook->id, 'entity_selection' => 'book:' . $newBook->id,
]); ]);
$this->assertPermissionError($moveChapterResp); $this->assertPermissionError($moveChapterResp);
$this->entities->setPermissions($newBook, ['view', 'update', 'create', 'delete'], [$editor->roles->first()]); $this->permissions->setEntityPermissions($newBook, ['view', 'update', 'create', 'delete'], [$editor->roles->first()]);
$moveChapterResp = $this->put($chapter->getUrl('/move'), [ $moveChapterResp = $this->put($chapter->getUrl('/move'), [
'entity_selection' => 'book:' . $newBook->id, 'entity_selection' => 'book:' . $newBook->id,
]); ]);
@@ -313,7 +313,7 @@ class SortTest extends TestCase
$page = $this->entities->pageWithinChapter(); $page = $this->entities->pageWithinChapter();
/** @var Chapter $otherChapter */ /** @var Chapter $otherChapter */
$otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first(); $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first();
$this->entities->setPermissions($otherChapter); $this->permissions->setEntityPermissions($otherChapter);
$sortData = [ $sortData = [
'id' => $page->id, 'id' => $page->id,
@@ -334,8 +334,8 @@ class SortTest extends TestCase
$page = $this->entities->pageWithinChapter(); $page = $this->entities->pageWithinChapter();
/** @var Chapter $otherChapter */ /** @var Chapter $otherChapter */
$otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first(); $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first();
$editor = $this->getEditor(); $editor = $this->users->editor();
$this->entities->setPermissions($otherChapter->book, ['update', 'delete'], [$editor->roles()->first()]); $this->permissions->setEntityPermissions($otherChapter->book, ['update', 'delete'], [$editor->roles()->first()]);
$sortData = [ $sortData = [
'id' => $page->id, 'id' => $page->id,
@@ -356,8 +356,8 @@ class SortTest extends TestCase
$page = $this->entities->pageWithinChapter(); $page = $this->entities->pageWithinChapter();
/** @var Chapter $otherChapter */ /** @var Chapter $otherChapter */
$otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first(); $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first();
$editor = $this->getEditor(); $editor = $this->users->editor();
$this->entities->setPermissions($otherChapter, ['view', 'delete'], [$editor->roles()->first()]); $this->permissions->setEntityPermissions($otherChapter, ['view', 'delete'], [$editor->roles()->first()]);
$sortData = [ $sortData = [
'id' => $page->id, 'id' => $page->id,
@@ -378,8 +378,8 @@ class SortTest extends TestCase
$page = $this->entities->pageWithinChapter(); $page = $this->entities->pageWithinChapter();
/** @var Chapter $otherChapter */ /** @var Chapter $otherChapter */
$otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first(); $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first();
$editor = $this->getEditor(); $editor = $this->users->editor();
$this->entities->setPermissions($page, ['view', 'delete'], [$editor->roles()->first()]); $this->permissions->setEntityPermissions($page, ['view', 'delete'], [$editor->roles()->first()]);
$sortData = [ $sortData = [
'id' => $page->id, 'id' => $page->id,
@@ -400,8 +400,8 @@ class SortTest extends TestCase
$page = $this->entities->pageWithinChapter(); $page = $this->entities->pageWithinChapter();
/** @var Chapter $otherChapter */ /** @var Chapter $otherChapter */
$otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first(); $otherChapter = Chapter::query()->where('book_id', '!=', $page->book_id)->first();
$editor = $this->getEditor(); $editor = $this->users->editor();
$this->entities->setPermissions($page, ['view', 'update'], [$editor->roles()->first()]); $this->permissions->setEntityPermissions($page, ['view', 'update'], [$editor->roles()->first()]);
$sortData = [ $sortData = [
'id' => $page->id, 'id' => $page->id,

View File

@@ -75,7 +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
$this->entities->setPermissions($page, [], []); $this->permissions->setEntityPermissions($page, [], []);
$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([]);
@@ -178,7 +178,7 @@ class TagTest extends TestCase
$resp = $this->get('/tags?name=SuperCategory'); $resp = $this->get('/tags?name=SuperCategory');
$resp->assertSee('GreatTestContent'); $resp->assertSee('GreatTestContent');
$this->entities->setPermissions($page, [], []); $this->permissions->setEntityPermissions($page, [], []);
$resp = $this->asEditor()->get('/tags'); $resp = $this->asEditor()->get('/tags');
$resp->assertDontSee('SuperCategory'); $resp->assertDontSee('SuperCategory');

View File

@@ -11,7 +11,7 @@ class ErrorTest extends TestCase
// Due to middleware being handled differently this will not fail // Due to middleware being handled differently this will not fail
// if our custom, middleware-loaded handler fails but this is here // if our custom, middleware-loaded handler fails but this is here
// as a reminder and as a general check in the event of other issues. // as a reminder and as a general check in the event of other issues.
$editor = $this->getEditor(); $editor = $this->users->editor();
$editor->name = 'tester'; $editor->name = 'tester';
$editor->save(); $editor->save();
@@ -24,7 +24,7 @@ class ErrorTest extends TestCase
public function test_item_not_found_does_not_get_logged_to_file() public function test_item_not_found_does_not_get_logged_to_file()
{ {
$this->actingAs($this->getViewer()); $this->actingAs($this->users->viewer());
$handler = $this->withTestLogger(); $handler = $this->withTestLogger();
$book = $this->entities->book(); $book = $this->entities->book();
@@ -41,7 +41,7 @@ class ErrorTest extends TestCase
public function test_access_to_non_existing_image_location_provides_404_response() public function test_access_to_non_existing_image_location_provides_404_response()
{ {
$resp = $this->actingAs($this->getViewer())->get('/uploads/images/gallery/2021-05/anonexistingimage.png'); $resp = $this->actingAs($this->users->viewer())->get('/uploads/images/gallery/2021-05/anonexistingimage.png');
$resp->assertStatus(404); $resp->assertStatus(404);
$resp->assertSeeText('Image Not Found'); $resp->assertSeeText('Image Not Found');
} }

View File

@@ -10,7 +10,7 @@ class FavouriteTest extends TestCase
public function test_page_add_favourite_flow() public function test_page_add_favourite_flow()
{ {
$page = $this->entities->page(); $page = $this->entities->page();
$editor = $this->getEditor(); $editor = $this->users->editor();
$resp = $this->actingAs($editor)->get($page->getUrl()); $resp = $this->actingAs($editor)->get($page->getUrl());
$this->withHtml($resp)->assertElementContains('button', 'Favourite'); $this->withHtml($resp)->assertElementContains('button', 'Favourite');
@@ -33,7 +33,7 @@ class FavouriteTest extends TestCase
public function test_page_remove_favourite_flow() public function test_page_remove_favourite_flow()
{ {
$page = $this->entities->page(); $page = $this->entities->page();
$editor = $this->getEditor(); $editor = $this->users->editor();
Favourite::query()->forceCreate([ Favourite::query()->forceCreate([
'user_id' => $editor->id, 'user_id' => $editor->id,
'favouritable_id' => $page->id, 'favouritable_id' => $page->id,
@@ -63,7 +63,7 @@ class FavouriteTest extends TestCase
$book->owned_by = $user->id; $book->owned_by = $user->id;
$book->save(); $book->save();
$this->giveUserPermissions($user, ['book-view-own']); $this->permissions->grantUserRolePermissions($user, ['book-view-own']);
$this->actingAs($user)->get($book->getUrl()); $this->actingAs($user)->get($book->getUrl());
$resp = $this->post('/favourites/add', [ $resp = $this->post('/favourites/add', [
@@ -81,7 +81,7 @@ class FavouriteTest extends TestCase
public function test_each_entity_type_shows_favourite_button() public function test_each_entity_type_shows_favourite_button()
{ {
$this->actingAs($this->getEditor()); $this->actingAs($this->users->editor());
foreach ($this->entities->all() as $entity) { foreach ($this->entities->all() as $entity) {
$resp = $this->get($entity->getUrl()); $resp = $this->get($entity->getUrl());
@@ -94,13 +94,13 @@ class FavouriteTest extends TestCase
$this->setSettings(['app-public' => 'true']); $this->setSettings(['app-public' => 'true']);
$resp = $this->get('/'); $resp = $this->get('/');
$this->withHtml($resp)->assertElementNotContains('header', 'My Favourites'); $this->withHtml($resp)->assertElementNotContains('header', 'My Favourites');
$resp = $this->actingAs($this->getViewer())->get('/'); $resp = $this->actingAs($this->users->viewer())->get('/');
$this->withHtml($resp)->assertElementContains('header a', 'My Favourites'); $this->withHtml($resp)->assertElementContains('header a', 'My Favourites');
} }
public function test_favourites_shown_on_homepage() public function test_favourites_shown_on_homepage()
{ {
$editor = $this->getEditor(); $editor = $this->users->editor();
$resp = $this->actingAs($editor)->get('/'); $resp = $this->actingAs($editor)->get('/');
$this->withHtml($resp)->assertElementNotExists('#top-favourites'); $this->withHtml($resp)->assertElementNotExists('#top-favourites');
@@ -116,7 +116,7 @@ class FavouriteTest extends TestCase
public function test_favourites_list_page_shows_favourites_and_has_working_pagination() public function test_favourites_list_page_shows_favourites_and_has_working_pagination()
{ {
$page = $this->entities->page(); $page = $this->entities->page();
$editor = $this->getEditor(); $editor = $this->users->editor();
$resp = $this->actingAs($editor)->get('/favourites'); $resp = $this->actingAs($editor)->get('/favourites');
$resp->assertDontSee($page->name); $resp->assertDontSee($page->name);

View File

@@ -2,8 +2,6 @@
namespace Tests\Helpers; namespace Tests\Helpers;
use BookStack\Auth\Permissions\EntityPermission;
use BookStack\Auth\Role;
use BookStack\Auth\User; use BookStack\Auth\User;
use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Bookshelf;
@@ -186,44 +184,6 @@ class EntityProvider
return $pageRepo->publishDraft($draftPage, $input); return $pageRepo->publishDraft($draftPage, $input);
} }
/**
* Regenerate the permission for an entity.
* Centralised to manage clearing of cached elements between requests.
*/
public function regenPermissions(Entity $entity): void
{
$entity->rebuildPermissions();
$entity->load('jointPermissions');
}
/**
* Set the given entity as having restricted permissions, and apply the given
* permissions for the given roles.
* @param string[] $actions
* @param Role[] $roles
*/
public function setPermissions(Entity $entity, array $actions = [], array $roles = []): void
{
$entity->permissions()->delete();
$permissions = [
// Set default permissions to not allow actions so that only the provided role permissions are at play.
['role_id' => 0, 'view' => false, 'create' => false, 'update' => false, 'delete' => false],
];
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->load('permissions');
$this->regenPermissions($entity);
}
/** /**
* @param Entity|Entity[] $entities * @param Entity|Entity[] $entities
*/ */

View File

@@ -0,0 +1,136 @@
<?php
namespace Tests\Helpers;
use BookStack\Auth\Permissions\EntityPermission;
use BookStack\Auth\Permissions\RolePermission;
use BookStack\Auth\Role;
use BookStack\Auth\User;
use BookStack\Entities\Models\Entity;
class PermissionsProvider
{
protected UserRoleProvider $userRoleProvider;
public function __construct(UserRoleProvider $userRoleProvider)
{
$this->userRoleProvider = $userRoleProvider;
}
/**
* Grant role permissions to the provided user.
*/
public function grantUserRolePermissions(User $user, array $permissions): void
{
$newRole = $this->userRoleProvider->createRole($permissions);
$user->attachRole($newRole);
$user->load('roles');
$user->clearPermissionCache();
}
/**
* Completely remove specific role permissions from the provided user.
*/
public function removeUserRolePermissions(User $user, array $permissions): void
{
foreach ($permissions as $permissionName) {
/** @var RolePermission $permission */
$permission = RolePermission::query()
->where('name', '=', $permissionName)
->firstOrFail();
$roles = $user->roles()->whereHas('permissions', function ($query) use ($permission) {
$query->where('id', '=', $permission->id);
})->get();
/** @var Role $role */
foreach ($roles as $role) {
$role->detachPermission($permission);
}
$user->clearPermissionCache();
}
}
/**
* Change the owner of the given entity to the given user.
*/
public function changeEntityOwner(Entity $entity, User $newOwner): void
{
$entity->owned_by = $newOwner->id;
$entity->save();
$entity->rebuildPermissions();
}
/**
* Regenerate the permission for an entity.
* Centralised to manage clearing of cached elements between requests.
*/
public function regenerateForEntity(Entity $entity): void
{
$entity->rebuildPermissions();
}
/**
* Set the given entity as having restricted permissions, and apply the given
* permissions for the given roles.
* @param string[] $actions
* @param Role[] $roles
*/
public function setEntityPermissions(Entity $entity, array $actions = [], array $roles = [], $inherit = false): void
{
$entity->permissions()->delete();
$permissions = [];
if (!$inherit) {
// Set default permissions to not allow actions so that only the provided role permissions are at play.
$permissions[] = ['role_id' => null, 'user_id' => null, 'view' => false, 'create' => false, 'update' => false, 'delete' => false];
}
foreach ($roles as $role) {
$permissions[] = $this->actionListToEntityPermissionData($actions, $role->id);
}
$this->addEntityPermissionEntries($entity, $permissions);
}
public function addEntityPermission(Entity $entity, array $actionList, ?Role $role = null, ?User $user = null)
{
$permissionData = $this->actionListToEntityPermissionData($actionList, $role->id ?? null, $user->id ?? null);
$this->addEntityPermissionEntries($entity, [$permissionData]);
}
/**
* Disable inherited permissions on the given entity.
* Effectively sets the "Other Users" UI permission option to not inherit, with no permissions.
*/
public function disableEntityInheritedPermissions(Entity $entity): void
{
$entity->permissions()->whereNull(['user_id', 'role_id'])->delete();
$fallback = $this->actionListToEntityPermissionData([]);
$this->addEntityPermissionEntries($entity, [$fallback]);
}
protected function addEntityPermissionEntries(Entity $entity, array $entityPermissionData): void
{
$entity->permissions()->createMany($entityPermissionData);
$entity->load('permissions');
$this->regenerateForEntity($entity);
}
/**
* For the given simple array of string actions (view, create, update, delete), convert
* the format to entity permission data, where permission is granted if the action is in the
* given actionList array.
*/
protected function actionListToEntityPermissionData(array $actionList, int $roleId = null, int $userId = null): array
{
$permissionData = ['role_id' => $roleId, 'user_id' => $userId];
foreach (EntityPermission::PERMISSIONS as $possibleAction) {
$permissionData[$possibleAction] = in_array($possibleAction, $actionList);
}
return $permissionData;
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace Tests\Helpers;
use BookStack\Auth\Permissions\PermissionsRepo;
use BookStack\Auth\Role;
use BookStack\Auth\User;
class UserRoleProvider
{
protected ?User $admin = null;
protected ?User $editor = null;
/**
* Get a typical "Admin" user.
*/
public function admin(): User
{
if (is_null($this->admin)) {
$adminRole = Role::getSystemRole('admin');
$this->admin = $adminRole->users->first();
}
return $this->admin;
}
/**
* Get a typical "Editor" user.
*/
public function editor(): User
{
if ($this->editor === null) {
$editorRole = Role::getRole('editor');
$this->editor = $editorRole->users->first();
}
return $this->editor;
}
/**
* Get a typical "Viewer" user.
*/
public function viewer(array $attributes = []): User
{
$user = Role::getRole('viewer')->users()->first();
if (!empty($attributes)) {
$user->forceFill($attributes)->save();
}
return $user;
}
/**
* Create a new fresh user without any relations.
*/
public function newUser(array $attrs = []): User
{
return User::factory()->create($attrs);
}
/**
* Create a new fresh user, with the given attrs, that has assigned a fresh role
* that has the given role permissions.
* Intended as a helper to create a blank slate baseline user and role.
* @return array{0: User, 1: Role}
*/
public function newUserWithRole(array $userAttrs = [], array $rolePermissions = []): array
{
$user = $this->newUser($userAttrs);
$role = $this->attachNewRole($user, $rolePermissions);
return [$user, $role];
}
/**
* Attach a new role, with the given role permissions, to the given user
* and return that role.
*/
public function attachNewRole(User $user, array $rolePermissions = []): Role
{
$role = $this->createRole($rolePermissions);
$user->attachRole($role);
return $role;
}
/**
* Create a new basic role with the given role permissions.
*/
public function createRole(array $rolePermissions = []): Role
{
$permissionRepo = app(PermissionsRepo::class);
$roleData = Role::factory()->make()->toArray();
$roleData['permissions'] = array_flip($rolePermissions);
return $permissionRepo->saveNewRole($roleData);
}
}

View File

@@ -114,7 +114,7 @@ class HomepageTest extends TestCase
public function test_set_book_homepage() public function test_set_book_homepage()
{ {
$editor = $this->getEditor(); $editor = $this->users->editor();
setting()->putUser($editor, 'books_view_type', 'grid'); setting()->putUser($editor, 'books_view_type', 'grid');
$this->setSettings(['app-homepage-type' => 'books']); $this->setSettings(['app-homepage-type' => 'books']);
@@ -133,7 +133,7 @@ class HomepageTest extends TestCase
public function test_set_bookshelves_homepage() public function test_set_bookshelves_homepage()
{ {
$editor = $this->getEditor(); $editor = $this->users->editor();
setting()->putUser($editor, 'bookshelves_view_type', 'grid'); setting()->putUser($editor, 'bookshelves_view_type', 'grid');
$shelf = $this->entities->shelf(); $shelf = $this->entities->shelf();
@@ -152,7 +152,7 @@ class HomepageTest extends TestCase
public function test_shelves_list_homepage_adheres_to_book_visibility_permissions() public function test_shelves_list_homepage_adheres_to_book_visibility_permissions()
{ {
$editor = $this->getEditor(); $editor = $this->users->editor();
setting()->putUser($editor, 'bookshelves_view_type', 'list'); setting()->putUser($editor, 'bookshelves_view_type', 'list');
$this->setSettings(['app-homepage-type' => 'bookshelves']); $this->setSettings(['app-homepage-type' => 'bookshelves']);
$this->asEditor(); $this->asEditor();
@@ -167,13 +167,13 @@ class HomepageTest extends TestCase
// Ensure book no longer visible without view permission // Ensure book no longer visible without view permission
$editor->roles()->detach(); $editor->roles()->detach();
$this->giveUserPermissions($editor, ['bookshelf-view-all']); $this->permissions->grantUserRolePermissions($editor, ['bookshelf-view-all']);
$homeVisit = $this->get('/'); $homeVisit = $this->get('/');
$this->withHtml($homeVisit)->assertElementContains('.content-wrap', $shelf->name); $this->withHtml($homeVisit)->assertElementContains('.content-wrap', $shelf->name);
$this->withHtml($homeVisit)->assertElementNotContains('.content-wrap', $book->name); $this->withHtml($homeVisit)->assertElementNotContains('.content-wrap', $book->name);
// Ensure is visible again with entity-level view permission // Ensure is visible again with entity-level view permission
$this->entities->setPermissions($book, ['view'], [$editor->roles()->first()]); $this->permissions->setEntityPermissions($book, ['view'], [$editor->roles()->first()]);
$homeVisit = $this->get('/'); $homeVisit = $this->get('/');
$this->withHtml($homeVisit)->assertElementContains('.content-wrap', $shelf->name); $this->withHtml($homeVisit)->assertElementContains('.content-wrap', $shelf->name);
$this->withHtml($homeVisit)->assertElementContains('.content-wrap', $book->name); $this->withHtml($homeVisit)->assertElementContains('.content-wrap', $book->name);

View File

@@ -77,7 +77,7 @@ class LanguageTest extends TestCase
{ {
$this->asEditor(); $this->asEditor();
$this->assertFalse(config('app.rtl'), 'App RTL config should be false by default'); $this->assertFalse(config('app.rtl'), 'App RTL config should be false by default');
setting()->putUser($this->getEditor(), 'language', 'ar'); setting()->putUser($this->users->editor(), 'language', 'ar');
$this->get('/'); $this->get('/');
$this->assertTrue(config('app.rtl'), 'App RTL config should have been set to true by middleware'); $this->assertTrue(config('app.rtl'), 'App RTL config should have been set to true by middleware');
} }

View File

@@ -2,6 +2,7 @@
namespace Tests\Permissions; namespace Tests\Permissions;
use BookStack\Auth\Role;
use BookStack\Auth\User; use BookStack\Auth\User;
use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Bookshelf;
@@ -20,8 +21,8 @@ class EntityPermissionsTest extends TestCase
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
$this->user = $this->getEditor(); $this->user = $this->users->editor();
$this->viewer = $this->getViewer(); $this->viewer = $this->users->viewer();
} }
protected function setRestrictionsForTestRoles(Entity $entity, array $actions = []) protected function setRestrictionsForTestRoles(Entity $entity, array $actions = [])
@@ -30,7 +31,7 @@ class EntityPermissionsTest extends TestCase
$this->user->roles->first(), $this->user->roles->first(),
$this->viewer->roles->first(), $this->viewer->roles->first(),
]; ];
$this->entities->setPermissions($entity, $actions, $roles); $this->permissions->setEntityPermissions($entity, $actions, $roles);
} }
public function test_bookshelf_view_restriction() public function test_bookshelf_view_restriction()
@@ -378,8 +379,10 @@ class EntityPermissionsTest extends TestCase
$this->put($modelInstance->getUrl('/permissions'), [ $this->put($modelInstance->getUrl('/permissions'), [
'permissions' => [ 'permissions' => [
$roleId => [ 'role' => [
$permission => 'true', $roleId => [
$permission => 'true',
],
], ],
], ],
]); ]);
@@ -655,6 +658,34 @@ class EntityPermissionsTest extends TestCase
$resp->assertRedirect($book->getUrl('/page/test-page')); $resp->assertRedirect($book->getUrl('/page/test-page'));
} }
public function test_access_to_item_prevented_if_inheritance_active_but_permission_prevented_via_role()
{
$user = $this->users->viewer();
$viewerRole = $user->roles->first();
$chapter = $this->entities->chapter();
$book = $chapter->book;
$this->permissions->setEntityPermissions($book, ['edit'], [$viewerRole], false);
$this->permissions->setEntityPermissions($chapter, [], [$viewerRole], true);
$this->assertFalse(userCan('chapter-update', $chapter));
}
public function test_access_to_item_allowed_if_inheritance_active_and_permission_prevented_via_role_but_allowed_via_parent()
{
$user = $this->users->viewer();
$viewerRole = $user->roles->first();
$editorRole = Role::getRole('Editor');
$user->attachRole($editorRole);
$chapter = $this->entities->chapter();
$book = $chapter->book;
$this->permissions->setEntityPermissions($book, ['edit'], [$editorRole], false);
$this->permissions->setEntityPermissions($chapter, [], [$viewerRole], true);
$this->assertTrue(userCan('chapter-update', $chapter));
}
public function test_book_permissions_can_be_generated_without_error_if_child_chapter_is_in_recycle_bin() public function test_book_permissions_can_be_generated_without_error_if_child_chapter_is_in_recycle_bin()
{ {
$book = $this->entities->bookHasChaptersAndPages(); $book = $this->entities->bookHasChaptersAndPages();
@@ -665,7 +696,7 @@ class EntityPermissionsTest extends TestCase
$error = null; $error = null;
try { try {
$this->entities->setPermissions($book, ['view'], []); $this->permissions->setEntityPermissions($book, ['view'], []);
} catch (Exception $e) { } catch (Exception $e) {
$error = $e; $error = $e;
} }

View File

@@ -14,7 +14,7 @@ class ExportPermissionsTest extends TestCase
$pageContent = Str::random(48); $pageContent = Str::random(48);
$page->html = '<p>' . $pageContent . '</p>'; $page->html = '<p>' . $pageContent . '</p>';
$page->save(); $page->save();
$viewer = $this->getViewer(); $viewer = $this->users->viewer();
$this->actingAs($viewer); $this->actingAs($viewer);
$formats = ['html', 'plaintext']; $formats = ['html', 'plaintext'];
@@ -25,7 +25,7 @@ class ExportPermissionsTest extends TestCase
$resp->assertSee($pageContent); $resp->assertSee($pageContent);
} }
$this->entities->setPermissions($page, []); $this->permissions->setEntityPermissions($page, []);
foreach ($formats as $format) { foreach ($formats as $format) {
$resp = $this->get($chapter->getUrl("export/{$format}")); $resp = $this->get($chapter->getUrl("export/{$format}"));
@@ -42,7 +42,7 @@ class ExportPermissionsTest extends TestCase
$pageContent = Str::random(48); $pageContent = Str::random(48);
$page->html = '<p>' . $pageContent . '</p>'; $page->html = '<p>' . $pageContent . '</p>';
$page->save(); $page->save();
$viewer = $this->getViewer(); $viewer = $this->users->viewer();
$this->actingAs($viewer); $this->actingAs($viewer);
$formats = ['html', 'plaintext']; $formats = ['html', 'plaintext'];
@@ -53,7 +53,7 @@ class ExportPermissionsTest extends TestCase
$resp->assertSee($pageContent); $resp->assertSee($pageContent);
} }
$this->entities->setPermissions($page, []); $this->permissions->setEntityPermissions($page, []);
foreach ($formats as $format) { foreach ($formats as $format) {
$resp = $this->get($book->getUrl("export/{$format}")); $resp = $this->get($book->getUrl("export/{$format}"));

View File

@@ -22,7 +22,7 @@ class RolesTest extends TestCase
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
$this->user = $this->getViewer(); $this->user = $this->users->viewer();
} }
public function test_admin_can_see_settings() public function test_admin_can_see_settings()
@@ -42,7 +42,7 @@ class RolesTest extends TestCase
public function test_role_cannot_be_deleted_if_default() public function test_role_cannot_be_deleted_if_default()
{ {
$newRole = $this->createNewRole(); $newRole = $this->users->createRole();
$this->setSettings(['registration-role' => $newRole->id]); $this->setSettings(['registration-role' => $newRole->id]);
$deletePageUrl = '/settings/roles/delete/' . $newRole->id; $deletePageUrl = '/settings/roles/delete/' . $newRole->id;
@@ -121,11 +121,11 @@ class RolesTest extends TestCase
{ {
/** @var Role $adminRole */ /** @var Role $adminRole */
$adminRole = Role::query()->where('system_name', '=', 'admin')->first(); $adminRole = Role::query()->where('system_name', '=', 'admin')->first();
$adminUser = $this->getAdmin(); $adminUser = $this->users->admin();
$adminRole->users()->where('id', '!=', $adminUser->id)->delete(); $adminRole->users()->where('id', '!=', $adminUser->id)->delete();
$this->assertEquals(1, $adminRole->users()->count()); $this->assertEquals(1, $adminRole->users()->count());
$viewerRole = $this->getViewer()->roles()->first(); $viewerRole = $this->users->viewer()->roles()->first();
$editUrl = '/settings/users/' . $adminUser->id; $editUrl = '/settings/users/' . $adminUser->id;
$resp = $this->actingAs($adminUser)->put($editUrl, [ $resp = $this->actingAs($adminUser)->put($editUrl, [
@@ -169,7 +169,7 @@ class RolesTest extends TestCase
$roleA = Role::query()->create(['display_name' => 'Entity Permissions Delete Test']); $roleA = Role::query()->create(['display_name' => 'Entity Permissions Delete Test']);
$page = $this->entities->page(); $page = $this->entities->page();
$this->entities->setPermissions($page, ['view'], [$roleA]); $this->permissions->setEntityPermissions($page, ['view'], [$roleA]);
$this->assertDatabaseHas('entity_permissions', [ $this->assertDatabaseHas('entity_permissions', [
'role_id' => $roleA->id, 'role_id' => $roleA->id,
@@ -214,7 +214,7 @@ class RolesTest extends TestCase
public function test_manage_user_permission() public function test_manage_user_permission()
{ {
$this->actingAs($this->user)->get('/settings/users')->assertRedirect('/'); $this->actingAs($this->user)->get('/settings/users')->assertRedirect('/');
$this->giveUserPermissions($this->user, ['users-manage']); $this->permissions->grantUserRolePermissions($this->user, ['users-manage']);
$this->actingAs($this->user)->get('/settings/users')->assertOk(); $this->actingAs($this->user)->get('/settings/users')->assertOk();
} }
@@ -222,9 +222,9 @@ class RolesTest extends TestCase
{ {
$usersLink = 'href="' . url('/settings/users') . '"'; $usersLink = 'href="' . url('/settings/users') . '"';
$this->actingAs($this->user)->get('/')->assertDontSee($usersLink, false); $this->actingAs($this->user)->get('/')->assertDontSee($usersLink, false);
$this->giveUserPermissions($this->user, ['users-manage']); $this->permissions->grantUserRolePermissions($this->user, ['users-manage']);
$this->actingAs($this->user)->get('/')->assertSee($usersLink, false); $this->actingAs($this->user)->get('/')->assertSee($usersLink, false);
$this->giveUserPermissions($this->user, ['settings-manage', 'users-manage']); $this->permissions->grantUserRolePermissions($this->user, ['settings-manage', 'users-manage']);
$this->actingAs($this->user)->get('/')->assertDontSee($usersLink, false); $this->actingAs($this->user)->get('/')->assertDontSee($usersLink, false);
} }
@@ -247,7 +247,7 @@ class RolesTest extends TestCase
'name' => 'my_new_name', 'name' => 'my_new_name',
]); ]);
$this->giveUserPermissions($this->user, ['users-manage']); $this->permissions->grantUserRolePermissions($this->user, ['users-manage']);
$resp = $this->get($userProfileUrl) $resp = $this->get($userProfileUrl)
->assertOk(); ->assertOk();
@@ -269,7 +269,7 @@ class RolesTest extends TestCase
{ {
$this->actingAs($this->user)->get('/settings/roles')->assertRedirect('/'); $this->actingAs($this->user)->get('/settings/roles')->assertRedirect('/');
$this->get('/settings/roles/1')->assertRedirect('/'); $this->get('/settings/roles/1')->assertRedirect('/');
$this->giveUserPermissions($this->user, ['user-roles-manage']); $this->permissions->grantUserRolePermissions($this->user, ['user-roles-manage']);
$this->actingAs($this->user)->get('/settings/roles')->assertOk(); $this->actingAs($this->user)->get('/settings/roles')->assertOk();
$this->get('/settings/roles/1') $this->get('/settings/roles/1')
->assertOk() ->assertOk()
@@ -279,7 +279,7 @@ class RolesTest extends TestCase
public function test_settings_manage_permission() public function test_settings_manage_permission()
{ {
$this->actingAs($this->user)->get('/settings/features')->assertRedirect('/'); $this->actingAs($this->user)->get('/settings/features')->assertRedirect('/');
$this->giveUserPermissions($this->user, ['settings-manage']); $this->permissions->grantUserRolePermissions($this->user, ['settings-manage']);
$this->get('/settings/features')->assertOk(); $this->get('/settings/features')->assertOk();
$resp = $this->post('/settings/features', []); $resp = $this->post('/settings/features', []);
@@ -295,7 +295,7 @@ class RolesTest extends TestCase
$this->actingAs($this->user)->get($page->getUrl())->assertDontSee('Permissions'); $this->actingAs($this->user)->get($page->getUrl())->assertDontSee('Permissions');
$this->get($page->getUrl('/permissions'))->assertRedirect('/'); $this->get($page->getUrl('/permissions'))->assertRedirect('/');
$this->giveUserPermissions($this->user, ['restrictions-manage-all']); $this->permissions->grantUserRolePermissions($this->user, ['restrictions-manage-all']);
$this->actingAs($this->user)->get($page->getUrl())->assertSee('Permissions'); $this->actingAs($this->user)->get($page->getUrl())->assertSee('Permissions');
@@ -325,7 +325,7 @@ class RolesTest extends TestCase
$this->actingAs($this->user)->get($page->getUrl())->assertDontSee('Permissions'); $this->actingAs($this->user)->get($page->getUrl())->assertDontSee('Permissions');
$this->get($page->getUrl('/permissions'))->assertRedirect('/'); $this->get($page->getUrl('/permissions'))->assertRedirect('/');
$this->giveUserPermissions($this->user, ['restrictions-manage-own']); $this->permissions->grantUserRolePermissions($this->user, ['restrictions-manage-own']);
// Check can't restrict other's content // Check can't restrict other's content
$this->actingAs($this->user)->get($otherUsersPage->getUrl())->assertDontSee('Permissions'); $this->actingAs($this->user)->get($otherUsersPage->getUrl())->assertDontSee('Permissions');
@@ -350,7 +350,7 @@ class RolesTest extends TestCase
$this->withHtml($resp)->assertElementNotContains('.action-buttons', $text); $this->withHtml($resp)->assertElementNotContains('.action-buttons', $text);
} }
$this->giveUserPermissions($this->user, [$permission]); $this->permissions->grantUserRolePermissions($this->user, [$permission]);
foreach ($accessUrls as $url) { foreach ($accessUrls as $url) {
$this->actingAs($this->user)->get($url)->assertOk(); $this->actingAs($this->user)->get($url)->assertOk();
@@ -380,7 +380,7 @@ class RolesTest extends TestCase
$otherShelf = Bookshelf::query()->first(); $otherShelf = Bookshelf::query()->first();
$ownShelf = $this->entities->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']); $ownShelf = $this->entities->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']);
$ownShelf->forceFill(['owned_by' => $this->user->id, 'updated_by' => $this->user->id])->save(); $ownShelf->forceFill(['owned_by' => $this->user->id, 'updated_by' => $this->user->id])->save();
$this->entities->regenPermissions($ownShelf); $this->permissions->regenerateForEntity($ownShelf);
$this->checkAccessPermission('bookshelf-update-own', [ $this->checkAccessPermission('bookshelf-update-own', [
$ownShelf->getUrl('/edit'), $ownShelf->getUrl('/edit'),
@@ -406,12 +406,12 @@ class RolesTest extends TestCase
public function test_bookshelves_delete_own_permission() public function test_bookshelves_delete_own_permission()
{ {
$this->giveUserPermissions($this->user, ['bookshelf-update-all']); $this->permissions->grantUserRolePermissions($this->user, ['bookshelf-update-all']);
/** @var Bookshelf $otherShelf */ /** @var Bookshelf $otherShelf */
$otherShelf = Bookshelf::query()->first(); $otherShelf = Bookshelf::query()->first();
$ownShelf = $this->entities->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']); $ownShelf = $this->entities->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']);
$ownShelf->forceFill(['owned_by' => $this->user->id, 'updated_by' => $this->user->id])->save(); $ownShelf->forceFill(['owned_by' => $this->user->id, 'updated_by' => $this->user->id])->save();
$this->entities->regenPermissions($ownShelf); $this->permissions->regenerateForEntity($ownShelf);
$this->checkAccessPermission('bookshelf-delete-own', [ $this->checkAccessPermission('bookshelf-delete-own', [
$ownShelf->getUrl('/delete'), $ownShelf->getUrl('/delete'),
@@ -430,7 +430,7 @@ class RolesTest extends TestCase
public function test_bookshelves_delete_all_permission() public function test_bookshelves_delete_all_permission()
{ {
$this->giveUserPermissions($this->user, ['bookshelf-update-all']); $this->permissions->grantUserRolePermissions($this->user, ['bookshelf-update-all']);
/** @var Bookshelf $otherShelf */ /** @var Bookshelf $otherShelf */
$otherShelf = Bookshelf::query()->first(); $otherShelf = Bookshelf::query()->first();
$this->checkAccessPermission('bookshelf-delete-all', [ $this->checkAccessPermission('bookshelf-delete-all', [
@@ -486,7 +486,7 @@ class RolesTest extends TestCase
public function test_books_delete_own_permission() public function test_books_delete_own_permission()
{ {
$this->giveUserPermissions($this->user, ['book-update-all']); $this->permissions->grantUserRolePermissions($this->user, ['book-update-all']);
/** @var Book $otherBook */ /** @var Book $otherBook */
$otherBook = Book::query()->take(1)->get()->first(); $otherBook = Book::query()->take(1)->get()->first();
$ownBook = $this->entities->createChainBelongingToUser($this->user)['book']; $ownBook = $this->entities->createChainBelongingToUser($this->user)['book'];
@@ -506,7 +506,7 @@ class RolesTest extends TestCase
public function test_books_delete_all_permission() public function test_books_delete_all_permission()
{ {
$this->giveUserPermissions($this->user, ['book-update-all']); $this->permissions->grantUserRolePermissions($this->user, ['book-update-all']);
/** @var Book $otherBook */ /** @var Book $otherBook */
$otherBook = Book::query()->take(1)->get()->first(); $otherBook = Book::query()->take(1)->get()->first();
$this->checkAccessPermission('book-delete-all', [ $this->checkAccessPermission('book-delete-all', [
@@ -585,7 +585,7 @@ class RolesTest extends TestCase
public function test_chapter_delete_own_permission() public function test_chapter_delete_own_permission()
{ {
$this->giveUserPermissions($this->user, ['chapter-update-all']); $this->permissions->grantUserRolePermissions($this->user, ['chapter-update-all']);
/** @var Chapter $otherChapter */ /** @var Chapter $otherChapter */
$otherChapter = Chapter::query()->first(); $otherChapter = Chapter::query()->first();
$ownChapter = $this->entities->createChainBelongingToUser($this->user)['chapter']; $ownChapter = $this->entities->createChainBelongingToUser($this->user)['chapter'];
@@ -607,7 +607,7 @@ class RolesTest extends TestCase
public function test_chapter_delete_all_permission() public function test_chapter_delete_all_permission()
{ {
$this->giveUserPermissions($this->user, ['chapter-update-all']); $this->permissions->grantUserRolePermissions($this->user, ['chapter-update-all']);
/** @var Chapter $otherChapter */ /** @var Chapter $otherChapter */
$otherChapter = Chapter::query()->first(); $otherChapter = Chapter::query()->first();
$this->checkAccessPermission('chapter-delete-all', [ $this->checkAccessPermission('chapter-delete-all', [
@@ -645,7 +645,7 @@ class RolesTest extends TestCase
$ownChapter->getUrl() => 'New Page', $ownChapter->getUrl() => 'New Page',
]); ]);
$this->giveUserPermissions($this->user, ['page-create-own']); $this->permissions->grantUserRolePermissions($this->user, ['page-create-own']);
foreach ($accessUrls as $index => $url) { foreach ($accessUrls as $index => $url) {
$resp = $this->actingAs($this->user)->get($url); $resp = $this->actingAs($this->user)->get($url);
@@ -688,7 +688,7 @@ class RolesTest extends TestCase
$chapter->getUrl() => 'New Page', $chapter->getUrl() => 'New Page',
]); ]);
$this->giveUserPermissions($this->user, ['page-create-all']); $this->permissions->grantUserRolePermissions($this->user, ['page-create-all']);
foreach ($accessUrls as $index => $url) { foreach ($accessUrls as $index => $url) {
$resp = $this->actingAs($this->user)->get($url); $resp = $this->actingAs($this->user)->get($url);
@@ -742,7 +742,7 @@ class RolesTest extends TestCase
public function test_page_delete_own_permission() public function test_page_delete_own_permission()
{ {
$this->giveUserPermissions($this->user, ['page-update-all']); $this->permissions->grantUserRolePermissions($this->user, ['page-update-all']);
/** @var Page $otherPage */ /** @var Page $otherPage */
$otherPage = Page::query()->first(); $otherPage = Page::query()->first();
$ownPage = $this->entities->createChainBelongingToUser($this->user)['page']; $ownPage = $this->entities->createChainBelongingToUser($this->user)['page'];
@@ -764,7 +764,7 @@ class RolesTest extends TestCase
public function test_page_delete_all_permission() public function test_page_delete_all_permission()
{ {
$this->giveUserPermissions($this->user, ['page-update-all']); $this->permissions->grantUserRolePermissions($this->user, ['page-update-all']);
/** @var Page $otherPage */ /** @var Page $otherPage */
$otherPage = Page::query()->first(); $otherPage = Page::query()->first();
@@ -823,7 +823,7 @@ class RolesTest extends TestCase
public function test_image_delete_own_permission() public function test_image_delete_own_permission()
{ {
$this->giveUserPermissions($this->user, ['image-update-all']); $this->permissions->grantUserRolePermissions($this->user, ['image-update-all']);
$page = $this->entities->page(); $page = $this->entities->page();
$image = Image::factory()->create([ $image = Image::factory()->create([
'uploaded_to' => $page->id, 'uploaded_to' => $page->id,
@@ -833,7 +833,7 @@ class RolesTest extends TestCase
$this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertStatus(403); $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertStatus(403);
$this->giveUserPermissions($this->user, ['image-delete-own']); $this->permissions->grantUserRolePermissions($this->user, ['image-delete-own']);
$this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertOk(); $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertOk();
$this->assertDatabaseMissing('images', ['id' => $image->id]); $this->assertDatabaseMissing('images', ['id' => $image->id]);
@@ -841,18 +841,18 @@ class RolesTest extends TestCase
public function test_image_delete_all_permission() public function test_image_delete_all_permission()
{ {
$this->giveUserPermissions($this->user, ['image-update-all']); $this->permissions->grantUserRolePermissions($this->user, ['image-update-all']);
$admin = $this->getAdmin(); $admin = $this->users->admin();
$page = $this->entities->page(); $page = $this->entities->page();
$image = Image::factory()->create(['uploaded_to' => $page->id, 'created_by' => $admin->id, 'updated_by' => $admin->id]); $image = Image::factory()->create(['uploaded_to' => $page->id, 'created_by' => $admin->id, 'updated_by' => $admin->id]);
$this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertStatus(403); $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertStatus(403);
$this->giveUserPermissions($this->user, ['image-delete-own']); $this->permissions->grantUserRolePermissions($this->user, ['image-delete-own']);
$this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertStatus(403); $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertStatus(403);
$this->giveUserPermissions($this->user, ['image-delete-all']); $this->permissions->grantUserRolePermissions($this->user, ['image-delete-all']);
$this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertOk(); $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertOk();
$this->assertDatabaseMissing('images', ['id' => $image->id]); $this->assertDatabaseMissing('images', ['id' => $image->id]);
@@ -863,7 +863,7 @@ class RolesTest extends TestCase
// To cover issue fixed in f99c8ff99aee9beb8c692f36d4b84dc6e651e50a. // To cover issue fixed in f99c8ff99aee9beb8c692f36d4b84dc6e651e50a.
$page = $this->entities->page(); $page = $this->entities->page();
$viewerRole = Role::getRole('viewer'); $viewerRole = Role::getRole('viewer');
$viewer = $this->getViewer(); $viewer = $this->users->viewer();
$this->actingAs($viewer)->get($page->getUrl())->assertOk(); $this->actingAs($viewer)->get($page->getUrl())->assertOk();
$this->asAdmin()->put('/settings/roles/' . $viewerRole->id, [ $this->asAdmin()->put('/settings/roles/' . $viewerRole->id, [
@@ -877,18 +877,18 @@ class RolesTest extends TestCase
public function test_empty_state_actions_not_visible_without_permission() public function test_empty_state_actions_not_visible_without_permission()
{ {
$admin = $this->getAdmin(); $admin = $this->users->admin();
// Book links // Book links
$book = Book::factory()->create(['created_by' => $admin->id, 'updated_by' => $admin->id]); $book = Book::factory()->create(['created_by' => $admin->id, 'updated_by' => $admin->id]);
$this->entities->regenPermissions($book); $this->permissions->regenerateForEntity($book);
$this->actingAs($this->getViewer())->get($book->getUrl()) $this->actingAs($this->users->viewer())->get($book->getUrl())
->assertDontSee('Create a new page') ->assertDontSee('Create a new page')
->assertDontSee('Add a chapter'); ->assertDontSee('Add a chapter');
// Chapter links // Chapter links
$chapter = Chapter::factory()->create(['created_by' => $admin->id, 'updated_by' => $admin->id, 'book_id' => $book->id]); $chapter = Chapter::factory()->create(['created_by' => $admin->id, 'updated_by' => $admin->id, 'book_id' => $book->id]);
$this->entities->regenPermissions($chapter); $this->permissions->regenerateForEntity($chapter);
$this->actingAs($this->getViewer())->get($chapter->getUrl()) $this->actingAs($this->users->viewer())->get($chapter->getUrl())
->assertDontSee('Create a new page') ->assertDontSee('Create a new page')
->assertDontSee('Sort the current book'); ->assertDontSee('Sort the current book');
} }
@@ -901,7 +901,7 @@ class RolesTest extends TestCase
->addComment($ownPage) ->addComment($ownPage)
->assertStatus(403); ->assertStatus(403);
$this->giveUserPermissions($this->user, ['comment-create-all']); $this->permissions->grantUserRolePermissions($this->user, ['comment-create-all']);
$this->actingAs($this->user) $this->actingAs($this->user)
->addComment($ownPage) ->addComment($ownPage)
@@ -911,7 +911,7 @@ class RolesTest extends TestCase
public function test_comment_update_own_permission() public function test_comment_update_own_permission()
{ {
$ownPage = $this->entities->createChainBelongingToUser($this->user)['page']; $ownPage = $this->entities->createChainBelongingToUser($this->user)['page'];
$this->giveUserPermissions($this->user, ['comment-create-all']); $this->permissions->grantUserRolePermissions($this->user, ['comment-create-all']);
$this->actingAs($this->user)->addComment($ownPage); $this->actingAs($this->user)->addComment($ownPage);
/** @var Comment $comment */ /** @var Comment $comment */
$comment = $ownPage->comments()->latest()->first(); $comment = $ownPage->comments()->latest()->first();
@@ -919,7 +919,7 @@ class RolesTest extends TestCase
// no comment-update-own // no comment-update-own
$this->actingAs($this->user)->updateComment($comment)->assertStatus(403); $this->actingAs($this->user)->updateComment($comment)->assertStatus(403);
$this->giveUserPermissions($this->user, ['comment-update-own']); $this->permissions->grantUserRolePermissions($this->user, ['comment-update-own']);
// now has comment-update-own // now has comment-update-own
$this->actingAs($this->user)->updateComment($comment)->assertOk(); $this->actingAs($this->user)->updateComment($comment)->assertOk();
@@ -936,7 +936,7 @@ class RolesTest extends TestCase
// no comment-update-all // no comment-update-all
$this->actingAs($this->user)->updateComment($comment)->assertStatus(403); $this->actingAs($this->user)->updateComment($comment)->assertStatus(403);
$this->giveUserPermissions($this->user, ['comment-update-all']); $this->permissions->grantUserRolePermissions($this->user, ['comment-update-all']);
// now has comment-update-all // now has comment-update-all
$this->actingAs($this->user)->updateComment($comment)->assertOk(); $this->actingAs($this->user)->updateComment($comment)->assertOk();
@@ -946,7 +946,7 @@ class RolesTest extends TestCase
{ {
/** @var Page $ownPage */ /** @var Page $ownPage */
$ownPage = $this->entities->createChainBelongingToUser($this->user)['page']; $ownPage = $this->entities->createChainBelongingToUser($this->user)['page'];
$this->giveUserPermissions($this->user, ['comment-create-all']); $this->permissions->grantUserRolePermissions($this->user, ['comment-create-all']);
$this->actingAs($this->user)->addComment($ownPage); $this->actingAs($this->user)->addComment($ownPage);
/** @var Comment $comment */ /** @var Comment $comment */
@@ -955,7 +955,7 @@ class RolesTest extends TestCase
// no comment-delete-own // no comment-delete-own
$this->actingAs($this->user)->deleteComment($comment)->assertStatus(403); $this->actingAs($this->user)->deleteComment($comment)->assertStatus(403);
$this->giveUserPermissions($this->user, ['comment-delete-own']); $this->permissions->grantUserRolePermissions($this->user, ['comment-delete-own']);
// now has comment-update-own // now has comment-update-own
$this->actingAs($this->user)->deleteComment($comment)->assertOk(); $this->actingAs($this->user)->deleteComment($comment)->assertOk();
@@ -972,7 +972,7 @@ class RolesTest extends TestCase
// no comment-delete-all // no comment-delete-all
$this->actingAs($this->user)->deleteComment($comment)->assertStatus(403); $this->actingAs($this->user)->deleteComment($comment)->assertStatus(403);
$this->giveUserPermissions($this->user, ['comment-delete-all']); $this->permissions->grantUserRolePermissions($this->user, ['comment-delete-all']);
// now has comment-delete-all // now has comment-delete-all
$this->actingAs($this->user)->deleteComment($comment)->assertOk(); $this->actingAs($this->user)->deleteComment($comment)->assertOk();

View File

@@ -0,0 +1,201 @@
<?php
namespace Tests\Permissions\Scenarios;
class EntityRolePermissionsTest extends PermissionScenarioTestCase
{
public function test_01_explicit_allow()
{
[$user, $role] = $this->users->newUserWithRole();
$page = $this->entities->page();
$this->permissions->setEntityPermissions($page, ['view'], [$role], false);
$this->assertVisibleToUser($page, $user);
}
public function test_02_explicit_deny()
{
[$user, $role] = $this->users->newUserWithRole();
$page = $this->entities->page();
$this->permissions->setEntityPermissions($page, [], [$role], false);
$this->assertNotVisibleToUser($page, $user);
}
public function test_03_same_level_conflicting()
{
[$user, $roleA] = $this->users->newUserWithRole();
$roleB = $this->users->attachNewRole($user);
$page = $this->entities->page();
$this->permissions->disableEntityInheritedPermissions($page);
$this->permissions->addEntityPermission($page, [], $roleA);
$this->permissions->addEntityPermission($page, ['view'], $roleB);
$this->assertVisibleToUser($page, $user);
}
public function test_20_inherit_allow()
{
[$user, $roleA] = $this->users->newUserWithRole();
$page = $this->entities->pageWithinChapter();
$chapter = $page->chapter;
$this->permissions->disableEntityInheritedPermissions($chapter);
$this->permissions->addEntityPermission($chapter, ['view'], $roleA);
$this->assertVisibleToUser($page, $user);
}
public function test_21_inherit_deny()
{
[$user, $roleA] = $this->users->newUserWithRole();
$page = $this->entities->pageWithinChapter();
$chapter = $page->chapter;
$this->permissions->disableEntityInheritedPermissions($chapter);
$this->permissions->addEntityPermission($chapter, [], $roleA);
$this->assertNotVisibleToUser($page, $user);
}
public function test_22_same_level_conflict_inherit()
{
[$user, $roleA] = $this->users->newUserWithRole();
$roleB = $this->users->attachNewRole($user);
$page = $this->entities->pageWithinChapter();
$chapter = $page->chapter;
$this->permissions->disableEntityInheritedPermissions($chapter);
$this->permissions->addEntityPermission($chapter, [], $roleA);
$this->permissions->addEntityPermission($chapter, ['view'], $roleB);
$this->assertVisibleToUser($page, $user);
}
public function test_30_child_inherit_override_allow()
{
[$user, $roleA] = $this->users->newUserWithRole();
$page = $this->entities->pageWithinChapter();
$chapter = $page->chapter;
$this->permissions->disableEntityInheritedPermissions($chapter);
$this->permissions->addEntityPermission($chapter, [], $roleA);
$this->permissions->addEntityPermission($page, ['view'], $roleA);
$this->assertVisibleToUser($page, $user);
}
public function test_31_child_inherit_override_deny()
{
[$user, $roleA] = $this->users->newUserWithRole();
$page = $this->entities->pageWithinChapter();
$chapter = $page->chapter;
$this->permissions->disableEntityInheritedPermissions($chapter);
$this->permissions->addEntityPermission($chapter, ['view'], $roleA);
$this->permissions->addEntityPermission($page, [], $roleA);
$this->assertNotVisibleToUser($page, $user);
}
public function test_40_multi_role_inherit_conflict_override_deny()
{
[$user, $roleA] = $this->users->newUserWithRole();
$roleB = $this->users->attachNewRole($user);
$page = $this->entities->pageWithinChapter();
$chapter = $page->chapter;
$this->permissions->disableEntityInheritedPermissions($chapter);
$this->permissions->addEntityPermission($page, [], $roleA);
$this->permissions->addEntityPermission($chapter, ['view'], $roleB);
$this->assertVisibleToUser($page, $user);
}
public function test_41_multi_role_inherit_conflict_retain_allow()
{
[$user, $roleA] = $this->users->newUserWithRole();
$roleB = $this->users->attachNewRole($user);
$page = $this->entities->pageWithinChapter();
$chapter = $page->chapter;
$this->permissions->disableEntityInheritedPermissions($chapter);
$this->permissions->addEntityPermission($page, ['view'], $roleA);
$this->permissions->addEntityPermission($chapter, [], $roleB);
$this->assertVisibleToUser($page, $user);
}
public function test_50_role_override_allow()
{
[$user, $roleA] = $this->users->newUserWithRole();
$page = $this->entities->page();
$this->permissions->addEntityPermission($page, ['view'], $roleA);
$this->assertVisibleToUser($page, $user);
}
public function test_51_role_override_deny()
{
[$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']);
$page = $this->entities->page();
$this->permissions->addEntityPermission($page, [], $roleA);
$this->assertNotVisibleToUser($page, $user);
}
public function test_60_inherited_role_override_allow()
{
[$user, $roleA] = $this->users->newUserWithRole([], []);
$page = $this->entities->pageWithinChapter();
$chapter = $page->chapter;
$this->permissions->addEntityPermission($chapter, ['view'], $roleA);
$this->assertVisibleToUser($page, $user);
}
public function test_61_inherited_role_override_deny()
{
[$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']);
$page = $this->entities->pageWithinChapter();
$chapter = $page->chapter;
$this->permissions->addEntityPermission($chapter, [], $roleA);
$this->assertNotVisibleToUser($page, $user);
}
public function test_62_inherited_role_override_deny_on_own()
{
[$user, $roleA] = $this->users->newUserWithRole([], ['page-view-own']);
$page = $this->entities->pageWithinChapter();
$chapter = $page->chapter;
$this->permissions->addEntityPermission($chapter, [], $roleA);
$this->permissions->changeEntityOwner($page, $user);
$this->assertNotVisibleToUser($page, $user);
}
public function test_70_multi_role_inheriting_deny()
{
[$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']);
$roleB = $this->users->attachNewRole($user);
$page = $this->entities->page();
$this->permissions->addEntityPermission($page, [], $roleB);
$this->assertNotVisibleToUser($page, $user);
}
public function test_80_multi_role_inherited_deny_via_parent()
{
[$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']);
$roleB = $this->users->attachNewRole($user);
$page = $this->entities->pageWithinChapter();
$chapter = $page->chapter;
$this->permissions->addEntityPermission($chapter, [], $roleB);
$this->assertNotVisibleToUser($page, $user);
}
}

View File

@@ -0,0 +1,209 @@
<?php
namespace Tests\Permissions\Scenarios;
class EntityUserPermissionsTest extends PermissionScenarioTestCase
{
public function test_01_explicit_allow()
{
$user = $this->users->newUser();
$page = $this->entities->page();
$this->permissions->disableEntityInheritedPermissions($page);
$this->permissions->addEntityPermission($page, ['view'], null, $user);
$this->assertVisibleToUser($page, $user);
}
public function test_02_explicit_deny()
{
$user = $this->users->newUser();
$page = $this->entities->page();
$this->permissions->disableEntityInheritedPermissions($page);
$this->permissions->addEntityPermission($page, [], null, $user);
$this->assertNotVisibleToUser($page, $user);
}
public function test_10_allow_inherit()
{
$user = $this->users->newUser();
$page = $this->entities->pageWithinChapter();
$chapter = $page->chapter;
$this->permissions->disableEntityInheritedPermissions($chapter);
$this->permissions->addEntityPermission($chapter, ['view'], null, $user);
$this->assertVisibleToUser($page, $user);
}
public function test_11_deny_inherit()
{
$user = $this->users->newUser();
$page = $this->entities->pageWithinChapter();
$chapter = $page->chapter;
$this->permissions->disableEntityInheritedPermissions($chapter);
$this->permissions->addEntityPermission($chapter, [], null, $user);
$this->assertNotVisibleToUser($page, $user);
}
public function test_12_allow_inherit_override()
{
$user = $this->users->newUser();
$page = $this->entities->pageWithinChapter();
$chapter = $page->chapter;
$this->permissions->disableEntityInheritedPermissions($chapter);
$this->permissions->addEntityPermission($chapter, [], null, $user);
$this->permissions->addEntityPermission($page, ['view'], null, $user);
$this->assertVisibleToUser($page, $user);
}
public function test_13_deny_inherit_override()
{
$user = $this->users->newUser();
$page = $this->entities->pageWithinChapter();
$chapter = $page->chapter;
$this->permissions->disableEntityInheritedPermissions($chapter);
$this->permissions->addEntityPermission($chapter, ['view'], null, $user);
$this->permissions->addEntityPermission($page, ['deny'], null, $user);
$this->assertNotVisibleToUser($page, $user);
}
public function test_40_entity_role_override_allow()
{
[$user, $role] = $this->users->newUserWithRole();
$page = $this->entities->page();
$this->permissions->disableEntityInheritedPermissions($page);
$this->permissions->addEntityPermission($page, ['view'], null, $user);
$this->permissions->addEntityPermission($page, [], $role);
$this->assertVisibleToUser($page, $user);
}
public function test_41_entity_role_override_deny()
{
[$user, $role] = $this->users->newUserWithRole();
$page = $this->entities->page();
$this->permissions->disableEntityInheritedPermissions($page);
$this->permissions->addEntityPermission($page, [], null, $user);
$this->permissions->addEntityPermission($page, ['view'], $role);
$this->assertNotVisibleToUser($page, $user);
}
public function test_42_entity_role_override_allow_via_inherit()
{
[$user, $role] = $this->users->newUserWithRole();
$page = $this->entities->pageWithinChapter();
$chapter = $page->chapter;
$this->permissions->disableEntityInheritedPermissions($chapter);
$this->permissions->addEntityPermission($chapter, ['view'], null, $user);
$this->permissions->addEntityPermission($page, [], $role);
$this->assertVisibleToUser($page, $user);
}
public function test_43_entity_role_override_deny_via_inherit()
{
[$user, $role] = $this->users->newUserWithRole();
$page = $this->entities->pageWithinChapter();
$chapter = $page->chapter;
$this->permissions->disableEntityInheritedPermissions($chapter);
$this->permissions->addEntityPermission($chapter, [], null, $user);
$this->permissions->addEntityPermission($page, ['view'], $role);
$this->assertNotVisibleToUser($page, $user);
}
public function test_50_role_override_allow()
{
[$user, $roleA] = $this->users->newUserWithRole();
$page = $this->entities->page();
$this->permissions->addEntityPermission($page, ['view'], null, $user);
$this->assertVisibleToUser($page, $user);
}
public function test_51_role_override_deny()
{
[$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']);
$page = $this->entities->page();
$this->permissions->addEntityPermission($page, [], null, $user);
$this->assertNotVisibleToUser($page, $user);
}
public function test_60_inherited_role_override_allow()
{
[$user, $roleA] = $this->users->newUserWithRole([], []);
$page = $this->entities->pageWithinChapter();
$chapter = $page->chapter;
$this->permissions->addEntityPermission($chapter, ['view'], null, $user);
$this->assertVisibleToUser($page, $user);
}
public function test_61_inherited_role_override_deny()
{
[$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']);
$page = $this->entities->pageWithinChapter();
$chapter = $page->chapter;
$this->permissions->addEntityPermission($chapter, [], null, $user);
$this->assertNotVisibleToUser($page, $user);
}
public function test_61_inherited_role_override_deny_on_own()
{
[$user, $roleA] = $this->users->newUserWithRole([], ['page-view-own']);
$page = $this->entities->pageWithinChapter();
$chapter = $page->chapter;
$this->permissions->addEntityPermission($chapter, [], null, $user);
$this->permissions->changeEntityOwner($page, $user);
$this->assertNotVisibleToUser($page, $user);
}
public function test_70_all_override_allow()
{
[$user, $roleA] = $this->users->newUserWithRole([], []);
$page = $this->entities->page();
$this->permissions->addEntityPermission($page, [], $roleA, null);
$this->permissions->addEntityPermission($page, ['view'], null, $user);
$this->assertVisibleToUser($page, $user);
}
public function test_71_all_override_deny()
{
[$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']);
$page = $this->entities->page();
$this->permissions->addEntityPermission($page, ['view'], $roleA, null);
$this->permissions->addEntityPermission($page, [], null, $user);
$this->assertNotVisibleToUser($page, $user);
}
public function test_80_inherited_all_override_allow()
{
[$user, $roleA] = $this->users->newUserWithRole([], []);
$page = $this->entities->pageWithinChapter();
$chapter = $page->chapter;
$this->permissions->addEntityPermission($chapter, [], $roleA, null);
$this->permissions->addEntityPermission($chapter, ['view'], null, $user);
$this->assertVisibleToUser($page, $user);
}
public function test_81_inherited_all_override_deny()
{
[$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']);
$page = $this->entities->pageWithinChapter();
$chapter = $page->chapter;
$this->permissions->addEntityPermission($chapter, ['view'], $roleA, null);
$this->permissions->addEntityPermission($chapter, [], null, $user);
$this->assertNotVisibleToUser($page, $user);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Tests\Permissions\Scenarios;
use BookStack\Auth\User;
use BookStack\Entities\Models\Entity;
use Tests\TestCase;
// Cases defined in dev/docs/permission-scenario-testing.md
class PermissionScenarioTestCase extends TestCase
{
protected function assertVisibleToUser(Entity $entity, User $user)
{
$this->actingAs($user);
$funcView = userCan($entity->getMorphClass() . '-view', $entity);
$queryView = $entity->newQuery()->scopes(['visible'])->find($entity->id) !== null;
$id = $entity->getMorphClass() . ':' . $entity->id;
$msg = "Item [{$id}] should be visible but was not found via ";
$msg .= implode(' and ', array_filter([!$funcView ? 'userCan' : '', !$queryView ? 'query' : '']));
static::assertTrue($funcView && $queryView, $msg);
}
protected function assertNotVisibleToUser(Entity $entity, User $user)
{
$this->actingAs($user);
$funcView = userCan($entity->getMorphClass() . '-view', $entity);
$queryView = $entity->newQuery()->scopes(['visible'])->find($entity->id) !== null;
$id = $entity->getMorphClass() . ':' . $entity->id;
$msg = "Item [{$id}] should not be visible but was found via ";
$msg .= implode(' and ', array_filter([$funcView ? 'userCan' : '', $queryView ? 'query' : '']));
static::assertTrue(!$funcView && !$queryView, $msg);
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Tests\Permissions\Scenarios;
class RoleContentPermissionsTest extends PermissionScenarioTestCase
{
public function test_01_allow()
{
[$user] = $this->users->newUserWithRole([], ['page-view-all']);
$page = $this->entities->page();
$this->assertVisibleToUser($page, $user);
}
public function test_02_deny()
{
[$user] = $this->users->newUserWithRole([], []);
$page = $this->entities->page();
$this->assertNotVisibleToUser($page, $user);
}
public function test_10_allow_on_own_with_own()
{
[$user] = $this->users->newUserWithRole([], ['page-view-own']);
$page = $this->entities->page();
$this->permissions->changeEntityOwner($page, $user);
$this->assertVisibleToUser($page, $user);
}
public function test_11_deny_on_other_with_own()
{
[$user] = $this->users->newUserWithRole([], ['page-view-own']);
$page = $this->entities->page();
$this->permissions->changeEntityOwner($page, $this->users->editor());
$this->assertNotVisibleToUser($page, $user);
}
public function test_20_multiple_role_conflicting_all()
{
[$user] = $this->users->newUserWithRole([], ['page-view-all']);
$this->users->attachNewRole($user, []);
$page = $this->entities->page();
$this->assertVisibleToUser($page, $user);
}
public function test_21_multiple_role_conflicting_own()
{
[$user] = $this->users->newUserWithRole([], ['page-view-own']);
$this->users->attachNewRole($user, []);
$page = $this->entities->page();
$this->permissions->changeEntityOwner($page, $user);
$this->assertVisibleToUser($page, $user);
}
}

View File

@@ -2,7 +2,6 @@
namespace Tests; namespace Tests;
use BookStack\Auth\Permissions\JointPermissionBuilder;
use BookStack\Auth\Permissions\RolePermission; use BookStack\Auth\Permissions\RolePermission;
use BookStack\Auth\Role; use BookStack\Auth\Role;
use BookStack\Auth\User; use BookStack\Auth\User;
@@ -89,7 +88,6 @@ class PublicActionTest extends TestCase
foreach (RolePermission::all() as $perm) { foreach (RolePermission::all() as $perm) {
$publicRole->attachPermission($perm); $publicRole->attachPermission($perm);
} }
$this->app->make(JointPermissionBuilder::class)->rebuildForRole($publicRole);
user()->clearPermissionCache(); user()->clearPermissionCache();
$chapter = $this->entities->chapter(); $chapter = $this->entities->chapter();
@@ -173,7 +171,7 @@ class PublicActionTest extends TestCase
{ {
$this->setSettings(['app-public' => 'true']); $this->setSettings(['app-public' => 'true']);
$book = $this->entities->book(); $book = $this->entities->book();
$this->entities->setPermissions($book); $this->permissions->setEntityPermissions($book);
$resp = $this->get($book->getUrl()); $resp = $this->get($book->getUrl());
$resp->assertSee('Book not found'); $resp->assertSee('Book not found');

View File

@@ -91,7 +91,7 @@ class ReferencesTest extends TestCase
$pageB = $this->entities->page(); $pageB = $this->entities->page();
$this->createReference($pageB, $page); $this->createReference($pageB, $page);
$this->entities->setPermissions($pageB); $this->permissions->setEntityPermissions($pageB);
$this->asEditor()->get($page->getUrl('/references'))->assertDontSee($pageB->name); $this->asEditor()->get($page->getUrl('/references'))->assertDontSee($pageB->name);
$this->asAdmin()->get($page->getUrl('/references'))->assertSee($pageB->name); $this->asAdmin()->get($page->getUrl('/references'))->assertSee($pageB->name);

View File

@@ -14,7 +14,7 @@ class RecycleBinTest extends TestCase
public function test_recycle_bin_routes_permissions() public function test_recycle_bin_routes_permissions()
{ {
$page = $this->entities->page(); $page = $this->entities->page();
$editor = $this->getEditor(); $editor = $this->users->editor();
$this->actingAs($editor)->delete($page->getUrl()); $this->actingAs($editor)->delete($page->getUrl());
$deletion = Deletion::query()->firstOrFail(); $deletion = Deletion::query()->firstOrFail();
@@ -33,7 +33,7 @@ class RecycleBinTest extends TestCase
$this->assertPermissionError($resp); $this->assertPermissionError($resp);
} }
$this->giveUserPermissions($editor, ['restrictions-manage-all']); $this->permissions->grantUserRolePermissions($editor, ['restrictions-manage-all']);
foreach ($routes as $route) { foreach ($routes as $route) {
[$method, $url] = explode(':', $route); [$method, $url] = explode(':', $route);
@@ -41,7 +41,7 @@ class RecycleBinTest extends TestCase
$this->assertPermissionError($resp); $this->assertPermissionError($resp);
} }
$this->giveUserPermissions($editor, ['settings-manage']); $this->permissions->grantUserRolePermissions($editor, ['settings-manage']);
foreach ($routes as $route) { foreach ($routes as $route) {
DB::beginTransaction(); DB::beginTransaction();
@@ -56,7 +56,7 @@ class RecycleBinTest extends TestCase
{ {
$page = $this->entities->page(); $page = $this->entities->page();
$book = Book::query()->whereHas('pages')->whereHas('chapters')->withCount(['pages', 'chapters'])->first(); $book = Book::query()->whereHas('pages')->whereHas('chapters')->withCount(['pages', 'chapters'])->first();
$editor = $this->getEditor(); $editor = $this->users->editor();
$this->actingAs($editor)->delete($page->getUrl()); $this->actingAs($editor)->delete($page->getUrl());
$this->actingAs($editor)->delete($book->getUrl()); $this->actingAs($editor)->delete($book->getUrl());
@@ -73,7 +73,7 @@ class RecycleBinTest extends TestCase
{ {
$page = $this->entities->page(); $page = $this->entities->page();
$book = Book::query()->where('id', '!=', $page->book_id)->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail(); $book = Book::query()->where('id', '!=', $page->book_id)->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
$editor = $this->getEditor(); $editor = $this->users->editor();
$this->actingAs($editor)->delete($page->getUrl()); $this->actingAs($editor)->delete($page->getUrl());
$this->actingAs($editor)->delete($book->getUrl()); $this->actingAs($editor)->delete($book->getUrl());

View File

@@ -32,11 +32,11 @@ class RegenerateReferencesTest extends TestCase
public function test_settings_manage_permission_required() public function test_settings_manage_permission_required()
{ {
$editor = $this->getEditor(); $editor = $this->users->editor();
$resp = $this->actingAs($editor)->post('/settings/maintenance/regenerate-references'); $resp = $this->actingAs($editor)->post('/settings/maintenance/regenerate-references');
$this->assertPermissionError($resp); $this->assertPermissionError($resp);
$this->giveUserPermissions($editor, ['settings-manage']); $this->permissions->grantUserRolePermissions($editor, ['settings-manage']);
$resp = $this->actingAs($editor)->post('/settings/maintenance/regenerate-references'); $resp = $this->actingAs($editor)->post('/settings/maintenance/regenerate-references');
$this->assertNotPermissionError($resp); $this->assertNotPermissionError($resp);

View File

@@ -20,7 +20,7 @@ class TestEmailTest extends TestCase
public function test_send_test_email_endpoint_sends_email_and_redirects_user_and_shows_notification() public function test_send_test_email_endpoint_sends_email_and_redirects_user_and_shows_notification()
{ {
Notification::fake(); Notification::fake();
$admin = $this->getAdmin(); $admin = $this->users->admin();
$sendReq = $this->actingAs($admin)->post('/settings/maintenance/send-test-email'); $sendReq = $this->actingAs($admin)->post('/settings/maintenance/send-test-email');
$sendReq->assertRedirect('/settings/maintenance#image-cleanup'); $sendReq->assertRedirect('/settings/maintenance#image-cleanup');
@@ -37,7 +37,7 @@ class TestEmailTest extends TestCase
$exception = new \Exception('A random error occurred when testing an email'); $exception = new \Exception('A random error occurred when testing an email');
$mockDispatcher->shouldReceive('sendNow')->andThrow($exception); $mockDispatcher->shouldReceive('sendNow')->andThrow($exception);
$admin = $this->getAdmin(); $admin = $this->users->admin();
$sendReq = $this->actingAs($admin)->post('/settings/maintenance/send-test-email'); $sendReq = $this->actingAs($admin)->post('/settings/maintenance/send-test-email');
$sendReq->assertRedirect('/settings/maintenance#image-cleanup'); $sendReq->assertRedirect('/settings/maintenance#image-cleanup');
$this->assertSessionHas('error'); $this->assertSessionHas('error');
@@ -50,12 +50,12 @@ class TestEmailTest extends TestCase
public function test_send_test_email_requires_settings_manage_permission() public function test_send_test_email_requires_settings_manage_permission()
{ {
Notification::fake(); Notification::fake();
$user = $this->getViewer(); $user = $this->users->viewer();
$sendReq = $this->actingAs($user)->post('/settings/maintenance/send-test-email'); $sendReq = $this->actingAs($user)->post('/settings/maintenance/send-test-email');
Notification::assertNothingSent(); Notification::assertNothingSent();
$this->giveUserPermissions($user, ['settings-manage']); $this->permissions->grantUserRolePermissions($user, ['settings-manage']);
$sendReq = $this->actingAs($user)->post('/settings/maintenance/send-test-email'); $sendReq = $this->actingAs($user)->post('/settings/maintenance/send-test-email');
Notification::assertSentTo($user, TestEmail::class); Notification::assertSentTo($user, TestEmail::class);
} }

View File

@@ -2,11 +2,6 @@
namespace Tests; namespace Tests;
use BookStack\Auth\Permissions\JointPermissionBuilder;
use BookStack\Auth\Permissions\PermissionsRepo;
use BookStack\Auth\Permissions\RolePermission;
use BookStack\Auth\Role;
use BookStack\Auth\User;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Settings\SettingService; use BookStack\Settings\SettingService;
use BookStack\Uploads\HttpFetcher; use BookStack\Uploads\HttpFetcher;
@@ -22,12 +17,15 @@ use Illuminate\Support\Env;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Testing\Assert as PHPUnit; use Illuminate\Testing\Assert as PHPUnit;
use Mockery;
use Monolog\Handler\TestHandler; use Monolog\Handler\TestHandler;
use Monolog\Logger; use Monolog\Logger;
use Psr\Http\Client\ClientInterface; use Psr\Http\Client\ClientInterface;
use Ssddanbrown\AssertHtml\TestsHtml; use Ssddanbrown\AssertHtml\TestsHtml;
use Tests\Helpers\EntityProvider; use Tests\Helpers\EntityProvider;
use Tests\Helpers\PermissionsProvider;
use Tests\Helpers\TestServiceProvider; use Tests\Helpers\TestServiceProvider;
use Tests\Helpers\UserRoleProvider;
abstract class TestCase extends BaseTestCase abstract class TestCase extends BaseTestCase
{ {
@@ -35,13 +33,16 @@ abstract class TestCase extends BaseTestCase
use DatabaseTransactions; use DatabaseTransactions;
use TestsHtml; use TestsHtml;
protected ?User $admin = null;
protected ?User $editor = null;
protected EntityProvider $entities; protected EntityProvider $entities;
protected UserRoleProvider $users;
protected PermissionsProvider $permissions;
protected function setUp(): void protected function setUp(): void
{ {
$this->entities = new EntityProvider(); $this->entities = new EntityProvider();
$this->users = new UserRoleProvider();
$this->permissions = new PermissionsProvider($this->users);
parent::setUp(); parent::setUp();
} }
@@ -70,20 +71,7 @@ abstract class TestCase extends BaseTestCase
*/ */
public function asAdmin() public function asAdmin()
{ {
return $this->actingAs($this->getAdmin()); return $this->actingAs($this->users->admin());
}
/**
* Get the current admin user.
*/
public function getAdmin(): User
{
if (is_null($this->admin)) {
$adminRole = Role::getSystemRole('admin');
$this->admin = $adminRole->users->first();
}
return $this->admin;
} }
/** /**
@@ -91,20 +79,7 @@ abstract class TestCase extends BaseTestCase
*/ */
public function asEditor() public function asEditor()
{ {
return $this->actingAs($this->getEditor()); return $this->actingAs($this->users->editor());
}
/**
* Get a editor user.
*/
protected function getEditor(): User
{
if ($this->editor === null) {
$editorRole = Role::getRole('editor');
$this->editor = $editorRole->users->first();
}
return $this->editor;
} }
/** /**
@@ -112,28 +87,7 @@ abstract class TestCase extends BaseTestCase
*/ */
public function asViewer() public function asViewer()
{ {
return $this->actingAs($this->getViewer()); return $this->actingAs($this->users->viewer());
}
/**
* Get an instance of a user with 'viewer' permissions.
*/
protected function getViewer(array $attributes = []): User
{
$user = Role::getRole('viewer')->users()->first();
if (!empty($attributes)) {
$user->forceFill($attributes)->save();
}
return $user;
}
/**
* Get a user that's not a system user such as the guest user.
*/
public function getNormalUser(): User
{
return User::query()->where('system_name', '=', null)->get()->last();
} }
/** /**
@@ -147,52 +101,6 @@ abstract class TestCase extends BaseTestCase
} }
} }
/**
* Give the given user some permissions.
*/
protected function giveUserPermissions(User $user, array $permissions = []): void
{
$newRole = $this->createNewRole($permissions);
$user->attachRole($newRole);
$user->load('roles');
$user->clearPermissionCache();
}
/**
* Completely remove the given permission name from the given user.
*/
protected function removePermissionFromUser(User $user, string $permissionName)
{
$permissionBuilder = app()->make(JointPermissionBuilder::class);
/** @var RolePermission $permission */
$permission = RolePermission::query()->where('name', '=', $permissionName)->firstOrFail();
$roles = $user->roles()->whereHas('permissions', function ($query) use ($permission) {
$query->where('id', '=', $permission->id);
})->get();
/** @var Role $role */
foreach ($roles as $role) {
$role->detachPermission($permission);
$permissionBuilder->rebuildForRole($role);
}
$user->clearPermissionCache();
}
/**
* Create a new basic role for testing purposes.
*/
protected function createNewRole(array $permissions = []): Role
{
$permissionRepo = app(PermissionsRepo::class);
$roleData = Role::factory()->make()->toArray();
$roleData['permissions'] = array_flip($permissions);
return $permissionRepo->saveNewRole($roleData);
}
/** /**
* Mock the HttpFetcher service and return the given data on fetch. * Mock the HttpFetcher service and return the given data on fetch.
*/ */

View File

@@ -36,7 +36,7 @@ class ThemeTest extends TestCase
'; ';
file_put_contents($translationPath . '/entities.php', $customTranslations); file_put_contents($translationPath . '/entities.php', $customTranslations);
$homeRequest = $this->actingAs($this->getViewer())->get('/'); $homeRequest = $this->actingAs($this->users->viewer())->get('/');
$this->withHtml($homeRequest)->assertElementContains('header nav', 'Sandwiches'); $this->withHtml($homeRequest)->assertElementContains('header nav', 'Sandwiches');
}); });
} }

View File

@@ -25,7 +25,7 @@ class FrameworkAssumptionTest extends TestCase
// Page has SoftDeletes trait by default, so we apply our custom scope and ensure // Page has SoftDeletes trait by default, so we apply our custom scope and ensure
// it stacks on the global scope to filter out deleted items. // it stacks on the global scope to filter out deleted items.
$query = Page::query()->scopes('visible')->toSql(); $query = Page::query()->scopes('visible')->toSql();
$this->assertStringContainsString('joint_permissions', $query); $this->assertStringContainsString('entity_permissions_collapsed', $query);
$this->assertStringContainsString('`deleted_at` is null', $query); $this->assertStringContainsString('`deleted_at` is null', $query);
} }
} }

View File

@@ -75,7 +75,7 @@ class AttachmentTest extends TestCase
{ {
$page = $this->entities->page(); $page = $this->entities->page();
$this->asAdmin(); $this->asAdmin();
$admin = $this->getAdmin(); $admin = $this->users->admin();
$fileName = 'upload_test_file.txt'; $fileName = 'upload_test_file.txt';
$expectedResp = [ $expectedResp = [
@@ -137,7 +137,7 @@ class AttachmentTest extends TestCase
public function test_attaching_link_to_page() public function test_attaching_link_to_page()
{ {
$page = $this->entities->page(); $page = $this->entities->page();
$admin = $this->getAdmin(); $admin = $this->users->admin();
$this->asAdmin(); $this->asAdmin();
$linkReq = $this->call('POST', 'attachments/link', [ $linkReq = $this->call('POST', 'attachments/link', [
@@ -245,15 +245,15 @@ class AttachmentTest extends TestCase
public function test_attachment_access_without_permission_shows_404() public function test_attachment_access_without_permission_shows_404()
{ {
$admin = $this->getAdmin(); $admin = $this->users->admin();
$viewer = $this->getViewer(); $viewer = $this->users->viewer();
$page = $this->entities->page(); /** @var Page $page */ $page = $this->entities->page(); /** @var Page $page */
$this->actingAs($admin); $this->actingAs($admin);
$fileName = 'permission_test.txt'; $fileName = 'permission_test.txt';
$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();
$this->entities->setPermissions($page, [], []); $this->permissions->setEntityPermissions($page, [], []);
$this->actingAs($viewer); $this->actingAs($viewer);
$attachmentGet = $this->get($attachment->getUrl()); $attachmentGet = $this->get($attachment->getUrl());

View File

@@ -30,7 +30,7 @@ class DrawioTest extends TestCase
public function test_drawing_base64_upload() public function test_drawing_base64_upload()
{ {
$page = Page::first(); $page = Page::first();
$editor = $this->getEditor(); $editor = $this->users->editor();
$this->actingAs($editor); $this->actingAs($editor);
$upload = $this->postJson('images/drawio', [ $upload = $this->postJson('images/drawio', [
@@ -58,7 +58,7 @@ class DrawioTest extends TestCase
{ {
config()->set('services.drawio', 'http://cats.com?dog=tree'); config()->set('services.drawio', 'http://cats.com?dog=tree');
$page = Page::first(); $page = Page::first();
$editor = $this->getEditor(); $editor = $this->users->editor();
$resp = $this->actingAs($editor)->get($page->getUrl('/edit')); $resp = $this->actingAs($editor)->get($page->getUrl('/edit'));
$resp->assertSee('drawio-url="http://cats.com?dog=tree"', false); $resp->assertSee('drawio-url="http://cats.com?dog=tree"', false);
@@ -68,7 +68,7 @@ class DrawioTest extends TestCase
{ {
config()->set('services.drawio', true); config()->set('services.drawio', true);
$page = Page::first(); $page = Page::first();
$editor = $this->getEditor(); $editor = $this->users->editor();
$resp = $this->actingAs($editor)->get($page->getUrl('/edit')); $resp = $this->actingAs($editor)->get($page->getUrl('/edit'));
$resp->assertSee('drawio-url="https://embed.diagrams.net/?embed=1&amp;proto=json&amp;spin=1&amp;configure=1"', false); $resp->assertSee('drawio-url="https://embed.diagrams.net/?embed=1&amp;proto=json&amp;spin=1&amp;configure=1"', false);

View File

@@ -16,7 +16,7 @@ class ImageTest extends TestCase
public function test_image_upload() public function test_image_upload()
{ {
$page = $this->entities->page(); $page = $this->entities->page();
$admin = $this->getAdmin(); $admin = $this->users->admin();
$this->actingAs($admin); $this->actingAs($admin);
$imgDetails = $this->uploadGalleryImage($page); $imgDetails = $this->uploadGalleryImage($page);
@@ -40,7 +40,7 @@ class ImageTest extends TestCase
public function test_image_display_thumbnail_generation_does_not_increase_image_size() public function test_image_display_thumbnail_generation_does_not_increase_image_size()
{ {
$page = $this->entities->page(); $page = $this->entities->page();
$admin = $this->getAdmin(); $admin = $this->users->admin();
$this->actingAs($admin); $this->actingAs($admin);
$originalFile = $this->getTestImageFilePath('compressed.png'); $originalFile = $this->getTestImageFilePath('compressed.png');
@@ -64,7 +64,7 @@ class ImageTest extends TestCase
public function test_image_display_thumbnail_generation_for_apng_images_uses_original_file() public function test_image_display_thumbnail_generation_for_apng_images_uses_original_file()
{ {
$page = $this->entities->page(); $page = $this->entities->page();
$admin = $this->getAdmin(); $admin = $this->users->admin();
$this->actingAs($admin); $this->actingAs($admin);
$imgDetails = $this->uploadGalleryImage($page, 'animated.png'); $imgDetails = $this->uploadGalleryImage($page, 'animated.png');
@@ -76,7 +76,7 @@ class ImageTest extends TestCase
public function test_image_edit() public function test_image_edit()
{ {
$editor = $this->getEditor(); $editor = $this->users->editor();
$this->actingAs($editor); $this->actingAs($editor);
$imgDetails = $this->uploadGalleryImage(); $imgDetails = $this->uploadGalleryImage();
@@ -126,7 +126,7 @@ class ImageTest extends TestCase
public function test_image_usage() public function test_image_usage()
{ {
$page = $this->entities->page(); $page = $this->entities->page();
$editor = $this->getEditor(); $editor = $this->users->editor();
$this->actingAs($editor); $this->actingAs($editor);
$imgDetails = $this->uploadGalleryImage($page); $imgDetails = $this->uploadGalleryImage($page);
@@ -146,7 +146,7 @@ class ImageTest extends TestCase
public function test_php_files_cannot_be_uploaded() public function test_php_files_cannot_be_uploaded()
{ {
$page = $this->entities->page(); $page = $this->entities->page();
$admin = $this->getAdmin(); $admin = $this->users->admin();
$this->actingAs($admin); $this->actingAs($admin);
$fileName = 'bad.php'; $fileName = 'bad.php';
@@ -168,7 +168,7 @@ class ImageTest extends TestCase
public function test_php_like_files_cannot_be_uploaded() public function test_php_like_files_cannot_be_uploaded()
{ {
$page = $this->entities->page(); $page = $this->entities->page();
$admin = $this->getAdmin(); $admin = $this->users->admin();
$this->actingAs($admin); $this->actingAs($admin);
$fileName = 'bad.phtml'; $fileName = 'bad.phtml';
@@ -185,7 +185,7 @@ class ImageTest extends TestCase
public function test_files_with_double_extensions_will_get_sanitized() public function test_files_with_double_extensions_will_get_sanitized()
{ {
$page = $this->entities->page(); $page = $this->entities->page();
$admin = $this->getAdmin(); $admin = $this->users->admin();
$this->actingAs($admin); $this->actingAs($admin);
$fileName = 'bad.phtml.png'; $fileName = 'bad.phtml.png';
@@ -358,7 +358,7 @@ class ImageTest extends TestCase
$this->get($expectedUrl)->assertOk(); $this->get($expectedUrl)->assertOk();
$this->entities->setPermissions($page, [], []); $this->permissions->setEntityPermissions($page, [], []);
$resp = $this->get($expectedUrl); $resp = $this->get($expectedUrl);
$resp->assertNotFound(); $resp->assertNotFound();
@@ -382,7 +382,7 @@ class ImageTest extends TestCase
$this->get($expectedUrl)->assertOk(); $this->get($expectedUrl)->assertOk();
$this->entities->setPermissions($page, [], []); $this->permissions->setEntityPermissions($page, [], []);
$resp = $this->get($expectedUrl); $resp = $this->get($expectedUrl);
$resp->assertNotFound(); $resp->assertNotFound();
@@ -415,7 +415,7 @@ class ImageTest extends TestCase
$export = $this->get($pageB->getUrl('/export/html')); $export = $this->get($pageB->getUrl('/export/html'));
$this->assertStringContainsString($encodedImageContent, $export->getContent()); $this->assertStringContainsString($encodedImageContent, $export->getContent());
$this->entities->setPermissions($pageA, [], []); $this->permissions->setEntityPermissions($pageA, [], []);
$export = $this->get($pageB->getUrl('/export/html')); $export = $this->get($pageB->getUrl('/export/html'));
$this->assertStringNotContainsString($encodedImageContent, $export->getContent()); $this->assertStringNotContainsString($encodedImageContent, $export->getContent());
@@ -479,7 +479,7 @@ class ImageTest extends TestCase
$imageName = 'first-image.png'; $imageName = 'first-image.png';
$relPath = $this->getTestImagePath('gallery', $imageName); $relPath = $this->getTestImagePath('gallery', $imageName);
$this->deleteImage($relPath); $this->deleteImage($relPath);
$viewer = $this->getViewer(); $viewer = $this->users->viewer();
$this->uploadImage($imageName, $page->id); $this->uploadImage($imageName, $page->id);
$image = Image::first(); $image = Image::first();
@@ -490,7 +490,7 @@ class ImageTest extends TestCase
$resp = $this->actingAs($viewer)->get("/images/edit/{$image->id}"); $resp = $this->actingAs($viewer)->get("/images/edit/{$image->id}");
$this->withHtml($resp)->assertElementNotExists('button#image-manager-delete[title="Delete"]'); $this->withHtml($resp)->assertElementNotExists('button#image-manager-delete[title="Delete"]');
$this->giveUserPermissions($viewer, ['image-delete-all']); $this->permissions->grantUserRolePermissions($viewer, ['image-delete-all']);
$resp = $this->actingAs($viewer)->get("/images/edit/{$image->id}"); $resp = $this->actingAs($viewer)->get("/images/edit/{$image->id}");
$this->withHtml($resp)->assertElementExists('button#image-manager-delete[title="Delete"]'); $this->withHtml($resp)->assertElementExists('button#image-manager-delete[title="Delete"]');
@@ -509,8 +509,8 @@ class ImageTest extends TestCase
public function test_user_image_upload() public function test_user_image_upload()
{ {
$editor = $this->getEditor(); $editor = $this->users->editor();
$admin = $this->getAdmin(); $admin = $this->users->admin();
$this->actingAs($admin); $this->actingAs($admin);
$file = $this->getTestProfileImage(); $file = $this->getTestProfileImage();
@@ -525,7 +525,7 @@ class ImageTest extends TestCase
public function test_user_images_deleted_on_user_deletion() public function test_user_images_deleted_on_user_deletion()
{ {
$editor = $this->getEditor(); $editor = $this->users->editor();
$this->actingAs($editor); $this->actingAs($editor);
$file = $this->getTestProfileImage(); $file = $this->getTestProfileImage();
@@ -555,7 +555,7 @@ class ImageTest extends TestCase
public function test_deleted_unused_images() public function test_deleted_unused_images()
{ {
$page = $this->entities->page(); $page = $this->entities->page();
$admin = $this->getAdmin(); $admin = $this->users->admin();
$this->actingAs($admin); $this->actingAs($admin);
$imageName = 'unused-image.png'; $imageName = 'unused-image.png';

Some files were not shown because too many files have changed in this diff Show More