Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions app/Auth/Permissions/EntityPermission.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use BookStack\Auth\Role;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;

/**
* @property int $id
Expand All @@ -23,14 +22,14 @@ class EntityPermission extends Model

protected $fillable = ['role_id', 'view', 'create', 'update', 'delete'];
public $timestamps = false;

/**
* Get this restriction's attached entity.
*/
public function restrictable(): MorphTo
{
return $this->morphTo('restrictable');
}
protected $hidden = ['entity_id', 'entity_type', 'id'];
protected $casts = [
'view' => 'boolean',
'create' => 'boolean',
'read' => 'boolean',
'update' => 'boolean',
'delete' => 'boolean',
];

/**
* Get the role assigned to this entity permission.
Expand Down
38 changes: 12 additions & 26 deletions app/Entities/EntityProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,11 @@
*/
class EntityProvider
{
/**
* @var Bookshelf
*/
public $bookshelf;

/**
* @var Book
*/
public $book;

/**
* @var Chapter
*/
public $chapter;

/**
* @var Page
*/
public $page;

/**
* @var PageRevision
*/
public $pageRevision;
public Bookshelf $bookshelf;
public Book $book;
public Chapter $chapter;
public Page $page;
public PageRevision $pageRevision;

public function __construct()
{
Expand Down Expand Up @@ -69,13 +50,18 @@ public function all(): array
}

/**
* Get an entity instance by it's basic name.
* Get an entity instance by its basic name.
*/
public function get(string $type): Entity
{
$type = strtolower($type);
$instance = $this->all()[$type] ?? null;

if (is_null($instance)) {
throw new \InvalidArgumentException("Provided type \"{$type}\" is not a valid entity type");
}

return $this->all()[$type];
return $instance;
}

/**
Expand Down
74 changes: 70 additions & 4 deletions app/Entities/Tools/PermissionsUpdater.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@

use BookStack\Actions\ActivityType;
use BookStack\Auth\Permissions\EntityPermission;
use BookStack\Auth\Role;
use BookStack\Auth\User;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Entity;
use BookStack\Facades\Activity;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;

class PermissionsUpdater
{
/**
* Update an entities permissions from a permission form submit request.
*/
public function updateFromPermissionsForm(Entity $entity, Request $request)
public function updateFromPermissionsForm(Entity $entity, Request $request): void
{
$permissions = $request->get('permissions', null);
$ownerId = $request->get('owned_by', null);
Expand All @@ -39,12 +39,44 @@ public function updateFromPermissionsForm(Entity $entity, Request $request)
Activity::add(ActivityType::PERMISSIONS_UPDATE, $entity);
}

/**
* Update permissions from API request data.
*/
public function updateFromApiRequestData(Entity $entity, array $data): void
{
if (isset($data['role_permissions'])) {
$entity->permissions()->where('role_id', '!=', 0)->delete();
$rolePermissionData = $this->formatPermissionsFromApiRequestToEntityPermissions($data['role_permissions'] ?? [], false);
$entity->permissions()->createMany($rolePermissionData);
}

if (array_key_exists('fallback_permissions', $data)) {
$entity->permissions()->where('role_id', '=', 0)->delete();
}

if (isset($data['fallback_permissions']['inheriting']) && $data['fallback_permissions']['inheriting'] !== true) {
$data = $data['fallback_permissions'];
$data['role_id'] = 0;
$rolePermissionData = $this->formatPermissionsFromApiRequestToEntityPermissions([$data], true);
$entity->permissions()->createMany($rolePermissionData);
}

if (isset($data['owner_id'])) {
$this->updateOwnerFromId($entity, intval($data['owner_id']));
}

$entity->save();
$entity->rebuildPermissions();

Activity::add(ActivityType::PERMISSIONS_UPDATE, $entity);
}

/**
* Update the owner of the given entity.
* Checks the user exists in the system first.
* Does not save the model, just updates it.
*/
protected function updateOwnerFromId(Entity $entity, int $newOwnerId)
protected function updateOwnerFromId(Entity $entity, int $newOwnerId): void
{
$newOwner = User::query()->find($newOwnerId);
if (!is_null($newOwner)) {
Expand All @@ -67,7 +99,41 @@ protected function formatPermissionsFromRequestToEntityPermissions(array $permis
$formatted[] = $entityPermissionData;
}

return $formatted;
return $this->filterEntityPermissionDataUponRole($formatted, true);
}

protected function formatPermissionsFromApiRequestToEntityPermissions(array $permissions, bool $allowFallback): array
{
$formatted = [];

foreach ($permissions as $requestPermissionData) {
$entityPermissionData = ['role_id' => $requestPermissionData['role_id']];
foreach (EntityPermission::PERMISSIONS as $permission) {
$entityPermissionData[$permission] = boolval($requestPermissionData[$permission] ?? false);
}
$formatted[] = $entityPermissionData;
}

return $this->filterEntityPermissionDataUponRole($formatted, $allowFallback);
}

protected function filterEntityPermissionDataUponRole(array $entityPermissionData, bool $allowFallback): array
{
$roleIds = [];
foreach ($entityPermissionData as $permissionEntry) {
$roleIds[] = intval($permissionEntry['role_id']);
}

$actualRoleIds = array_unique(array_values(array_filter($roleIds)));
$rolesById = Role::query()->whereIn('id', $actualRoleIds)->get('id')->keyBy('id');

return array_values(array_filter($entityPermissionData, function ($data) use ($rolesById, $allowFallback) {
if (intval($data['role_id']) === 0) {
return $allowFallback;
}

return $rolesById->has($data['role_id']);
}));
}

/**
Expand Down
100 changes: 100 additions & 0 deletions app/Http/Controllers/Api/ContentPermissionsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

namespace BookStack\Http\Controllers\Api;

use BookStack\Entities\EntityProvider;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Tools\PermissionsUpdater;
use Illuminate\Http\Request;

class ContentPermissionsController extends ApiController
{
public function __construct(
protected PermissionsUpdater $permissionsUpdater,
protected EntityProvider $entities
) {
}

protected $rules = [
'update' => [
'owner_id' => ['int'],

'role_permissions' => ['array'],
'role_permissions.*.role_id' => ['required', 'int', 'exists:roles,id'],
'role_permissions.*.view' => ['required', 'boolean'],
'role_permissions.*.create' => ['required', 'boolean'],
'role_permissions.*.update' => ['required', 'boolean'],
'role_permissions.*.delete' => ['required', 'boolean'],

'fallback_permissions' => ['nullable'],
'fallback_permissions.inheriting' => ['required_with:fallback_permissions', 'boolean'],
'fallback_permissions.view' => ['required_if:fallback_permissions.inheriting,false', 'boolean'],
'fallback_permissions.create' => ['required_if:fallback_permissions.inheriting,false', 'boolean'],
'fallback_permissions.update' => ['required_if:fallback_permissions.inheriting,false', 'boolean'],
'fallback_permissions.delete' => ['required_if:fallback_permissions.inheriting,false', 'boolean'],
]
];

/**
* Read the configured content-level permissions for the item of the given type and ID.
* 'contentType' should be one of: page, book, chapter, bookshelf.
* 'contentId' should be the relevant ID of that item type you'd like to handle permissions for.
* The permissions shown are those that override the default for just the specified item, they do not show the
* full evaluated permission for a role, nor do they reflect permissions inherited from other items in the hierarchy.
* Fallback permission values may be `null` when inheriting is active.
*/
public function read(string $contentType, string $contentId)
{
$entity = $this->entities->get($contentType)
->newQuery()->scopes(['visible'])->findOrFail($contentId);

$this->checkOwnablePermission('restrictions-manage', $entity);

return response()->json($this->formattedPermissionDataForEntity($entity));
}

/**
* Update the configured content-level permission overrides for the item of the given type and ID.
* 'contentType' should be one of: page, book, chapter, bookshelf.
* 'contentId' should be the relevant ID of that item type you'd like to handle permissions for.
* Providing an empty `role_permissions` array will remove any existing configured role permissions,
* so you may want to fetch existing permissions beforehand if just adding/removing a single item.
* You should completely omit the `owner_id`, `role_permissions` and/or the `fallback_permissions` properties
* from your request data if you don't wish to update details within those categories.
*/
public function update(Request $request, string $contentType, string $contentId)
{
$entity = $this->entities->get($contentType)
->newQuery()->scopes(['visible'])->findOrFail($contentId);

$this->checkOwnablePermission('restrictions-manage', $entity);

$data = $this->validate($request, $this->rules()['update']);
$this->permissionsUpdater->updateFromApiRequestData($entity, $data);

return response()->json($this->formattedPermissionDataForEntity($entity));
}

protected function formattedPermissionDataForEntity(Entity $entity): array
{
$rolePermissions = $entity->permissions()
->where('role_id', '!=', 0)
->with(['role:id,display_name'])
->get();

$fallback = $entity->permissions()->where('role_id', '=', 0)->first();
$fallbackData = [
'inheriting' => is_null($fallback),
'view' => $fallback->view ?? null,
'create' => $fallback->create ?? null,
'update' => $fallback->update ?? null,
'delete' => $fallback->delete ?? null,
];

return [
'owner' => $entity->ownedBy()->first(),
'role_permissions' => $rolePermissions,
'fallback_permissions' => $fallbackData,
];
}
}
26 changes: 26 additions & 0 deletions dev/api/requests/content-permissions-update.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"owner_id": 1,
"role_permissions": [
{
"role_id": 2,
"view": true,
"create": true,
"update": true,
"delete": false
},
{
"role_id": 3,
"view": false,
"create": false,
"update": false,
"delete": false
}
],
"fallback_permissions": {
"inheriting": false,
"view": true,
"create": true,
"update": false,
"delete": false
}
}
38 changes: 38 additions & 0 deletions dev/api/responses/content-permissions-read.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"owner": {
"id": 1,
"name": "Admin",
"slug": "admin"
},
"role_permissions": [
{
"role_id": 2,
"view": true,
"create": false,
"update": true,
"delete": false,
"role": {
"id": 2,
"display_name": "Editor"
}
},
{
"role_id": 10,
"view": true,
"create": true,
"update": false,
"delete": false,
"role": {
"id": 10,
"display_name": "Wizards of the west"
}
}
],
"fallback_permissions": {
"inheriting": false,
"view": true,
"create": false,
"update": false,
"delete": false
}
}
Loading