Started build of content-permissions API endpoints
This commit is contained in:
		
							parent
							
								
									7b5111571c
								
							
						
					
					
						commit
						c42956bcaf
					
				| 
						 | 
				
			
			@ -5,7 +5,6 @@ namespace BookStack\Auth\Permissions;
 | 
			
		|||
use BookStack\Auth\Role;
 | 
			
		||||
use BookStack\Model;
 | 
			
		||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 | 
			
		||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @property int $id
 | 
			
		||||
| 
						 | 
				
			
			@ -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.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,30 +18,11 @@ use BookStack\Entities\Models\PageRevision;
 | 
			
		|||
 */
 | 
			
		||||
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()
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -69,13 +50,18 @@ class EntityProvider
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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;
 | 
			
		||||
 | 
			
		||||
        return $this->all()[$type];
 | 
			
		||||
        if (is_null($instance)) {
 | 
			
		||||
            throw new \InvalidArgumentException("Provided type \"{$type}\" is not a valid entity type");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $instance;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,20 +4,20 @@ namespace BookStack\Entities\Tools;
 | 
			
		|||
 | 
			
		||||
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);
 | 
			
		||||
| 
						 | 
				
			
			@ -39,12 +39,44 @@ class PermissionsUpdater
 | 
			
		|||
        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)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -67,7 +99,41 @@ class PermissionsUpdater
 | 
			
		|||
            $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']);
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,87 @@
 | 
			
		|||
<?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),
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -13,6 +13,7 @@ use BookStack\Http\Controllers\Api\BookExportApiController;
 | 
			
		|||
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;
 | 
			
		||||
| 
						 | 
				
			
			@ -85,3 +86,6 @@ Route::delete('roles/{id}', [RoleApiController::class, 'delete']);
 | 
			
		|||
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']);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue