Skip to content

Commit

Permalink
Merge pull request BookStackApp#4099 from BookStackApp/permissions_api
Browse files Browse the repository at this point in the history
Content-Permissions API Endpoints
  • Loading branch information
ssddanbrown committed Mar 13, 2023
2 parents 7b51115 + 1903924 commit a369971
Show file tree
Hide file tree
Showing 9 changed files with 558 additions and 39 deletions.
17 changes: 8 additions & 9 deletions app/Auth/Permissions/EntityPermission.php
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
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
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
@@ -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
@@ -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
@@ -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
}
}

0 comments on commit a369971

Please sign in to comment.