use BookStack\Auth\Role;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
-use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
* @property int $id
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.
*/
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()
{
}
/**
- * 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;
}
/**
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);
Activity::add(ActivityType::PERMISSIONS_UPDATE, $entity);
}
+ /**
+ * Update permissions from API request data.
+ */
+ public function updateFromApiRequestData(Entity $entity, array $data): void
+ {
+ if (isset($data['override_role_permissions'])) {
+ $entity->permissions()->where('role_id', '!=', 0)->delete();
+ $rolePermissionData = $this->formatPermissionsFromApiRequestToEntityPermissions($data['override_role_permissions'] ?? [], false);
+ $entity->permissions()->createMany($rolePermissionData);
+ }
+
+ if (array_key_exists('override_fallback_permissions', $data)) {
+ $entity->permissions()->where('role_id', '=', 0)->delete();
+ }
+
+ if (isset($data['override_fallback_permissions'])) {
+ $data = $data['override_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)) {
$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']);
+ }));
}
/**
--- /dev/null
+<?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'],
+
+ 'override_role_permissions' => ['array'],
+ 'override_role_permissions.*.role_id' => ['required', 'int'],
+ 'override_role_permissions.*.view' => ['required', 'boolean'],
+ 'override_role_permissions.*.create' => ['required', 'boolean'],
+ 'override_role_permissions.*.update' => ['required', 'boolean'],
+ 'override_role_permissions.*.delete' => ['required', 'boolean'],
+
+ 'override_fallback_permissions' => ['nullable'],
+ 'override_fallback_permissions.view' => ['required', 'boolean'],
+ 'override_fallback_permissions.create' => ['required', 'boolean'],
+ 'override_fallback_permissions.update' => ['required', 'boolean'],
+ 'override_fallback_permissions.delete' => ['required', '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.
+ */
+ 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 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.
+ */
+ 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();
+ $fallback?->makeHidden('role_id');
+
+ return [
+ 'owner' => $entity->ownedBy()->first(),
+ 'override_role_permissions' => $rolePermissions,
+ 'override_fallback_permissions' => $fallback,
+ 'inheriting' => is_null($fallback),
+ ];
+ }
+}
use BookStack\Http\Controllers\Api\BookshelfApiController;
use BookStack\Http\Controllers\Api\ChapterApiController;
use BookStack\Http\Controllers\Api\ChapterExportApiController;
+use BookStack\Http\Controllers\Api\ContentPermissionsController;
use BookStack\Http\Controllers\Api\PageApiController;
use BookStack\Http\Controllers\Api\PageExportApiController;
use BookStack\Http\Controllers\Api\RecycleBinApiController;
Route::get('recycle-bin', [RecycleBinApiController::class, 'list']);
Route::put('recycle-bin/{deletionId}', [RecycleBinApiController::class, 'restore']);
Route::delete('recycle-bin/{deletionId}', [RecycleBinApiController::class, 'destroy']);
+
+Route::get('content-permissions/{contentType}/{contentId}', [ContentPermissionsController::class, 'read']);
+Route::put('content-permissions/{contentType}/{contentId}', [ContentPermissionsController::class, 'update']);