Added users to permission form interface

Also updated non-joint permission handling to support user permissions.
This commit is contained in:
Dan Brown 2022-12-10 14:37:18 +00:00
parent f8c4725166
commit 7a269e7689
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
10 changed files with 124 additions and 33 deletions

View File

@ -3,9 +3,9 @@
namespace BookStack\Auth\Permissions;
use BookStack\Auth\Role;
use BookStack\Auth\User;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
* @property int $id
@ -22,17 +22,9 @@ class EntityPermission extends Model
{
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;
/**
* Get this restriction's attached entity.
*/
public function restrictable(): MorphTo
{
return $this->morphTo('restrictable');
}
/**
* Get the role assigned to this entity permission.
*/
@ -40,4 +32,12 @@ class EntityPermission extends Model
{
return $this->belongsTo(Role::class);
}
/**
* Get the user assigned to this entity permission.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@ -48,7 +48,7 @@ class PermissionApplicator
return $hasRolePermission;
}
$hasApplicableEntityPermissions = $this->hasEntityPermission($ownable, $userRoleIds, $action);
$hasApplicableEntityPermissions = $this->hasEntityPermission($ownable, $userRoleIds, $user->id, $action);
return is_null($hasApplicableEntityPermissions) ? $hasRolePermission : $hasApplicableEntityPermissions;
}
@ -57,7 +57,7 @@ class PermissionApplicator
* 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.
*/
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);
@ -79,8 +79,9 @@ class PermissionApplicator
foreach ($chain as $currentEntity) {
$relevantPermissions = $currentEntity->permissions()
->where(function (Builder $query) use ($userRoleIds) {
->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']);
});
@ -88,22 +89,17 @@ class PermissionApplicator
->get(['role_id', 'user_id', $action])
->all();
// TODO - Update below for user permissions
// 1. Default fallback set and allows, no role permissions -> True
// 2. Default fallback set and prevents, no role permissions -> False
// 3. Role permission allows, fallback set and allows -> True
// 3. Role permission allows, fallback set and prevents -> True
// 3. Role permission allows, fallback not set -> True
// 3. Role permission prevents, fallback set and allows -> False
// 3. Role permission prevents, fallback set and prevents -> False
// 3. Role permission prevents, fallback not set -> False
// 4. Nothing exists -> Continue
// Permissions work on specificity, in order of:
// 1. User-specific permissions
// 2. Role-specific permissions
// 3. Fallback-specific permissions
// For role permissions, the system tries to be fairly permissive, in that if the user has two roles,
// one lacking and one permitting an action, they will be permitted.
// If the default is set, we have to return something here.
$allowedById = [];
foreach ($relevantPermissions as $permission) {
$allowedById[$permission->role_id . ':' . $permission->user_id] = $permission->$action;
$allowedById[($permission->role_id ?? '') . ':' . ($permission->user_id ?? '')] = $permission->$action;
}
// Continue up the chain if no applicable entity permission overrides.
@ -111,7 +107,14 @@ class PermissionApplicator
continue;
}
// If we have user-role-specific permissions set, allow if any of those
// If we have user-specific permissions set, return the status of that
// since it's the most specific possible.
$userKey = ':' . $userId;
if (isset($allowedById[$userKey])) {
return $allowedById[$userKey];
}
// If we have role-specific permissions set, allow if any of those
// role permissions allow access.
$hasDefault = isset($allowedById[':']);
if (!$hasDefault || count($allowedById) > 1) {
@ -140,8 +143,10 @@ class PermissionApplicator
$permissionQuery = EntityPermission::query()
->where($action, '=', true)
->whereIn('role_id', $this->getCurrentUserRoleIds());
// TODO - Update for user permission
->where(function (Builder $query) {
$query->whereIn('role_id', $this->getCurrentUserRoleIds())
->orWhere('user_id', '=', $this->currentUser()->id);
});
if (!empty($entityClass)) {
/** @var Entity $entityInstance */

View File

@ -27,6 +27,19 @@ class PermissionFormData
->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
* entity we're managing permissions for.

View File

@ -10,7 +10,6 @@ use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Entity;
use BookStack\Facades\Activity;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
class PermissionsUpdater
{

View File

@ -5,6 +5,7 @@ namespace BookStack\Http\Controllers;
use BookStack\Auth\Permissions\EntityPermission;
use BookStack\Auth\Permissions\PermissionFormData;
use BookStack\Auth\Role;
use BookStack\Auth\User;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
@ -175,4 +176,25 @@ class PermissionsController extends Controller
'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(),
'entityType' => $entityType,
'inheriting' => false,
]);
}
}

View File

@ -10,6 +10,8 @@ export class EntityPermissions extends Component {
this.everyoneInheritToggle = this.$refs.everyoneInherit;
this.roleSelect = this.$refs.roleSelect;
this.roleContainer = this.$refs.roleContainer;
this.userContainer = this.$refs.userContainer;
this.userSelectContainer = this.$refs.userSelectContainer;
this.setupListeners();
}
@ -40,6 +42,14 @@ export class EntityPermissions extends Component {
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) {
@ -52,13 +62,32 @@ export class EntityPermissions extends Component {
}
// 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);
this.roleContainer.append(row);
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;
}
removeRowOnButtonClick(button) {
const row = button.closest('.item-list-row');
const modelId = button.dataset.modelId;
@ -72,7 +101,7 @@ export class EntityPermissions extends Component {
if (modelType === 'role') {
this.roleSelect.append(option);
}
// TODO - User role!
row.remove();
}

View File

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

View File

@ -11,7 +11,7 @@ $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 class="gap-x-m flex-container-row items-center px-l py-m flex">
<div class="text-large" title="{{ $modelType === 'fallback' ? trans('entities.permissions_role_everyone_else') : trans('common.role') }}">
@icon($modelType === 'fallback' ? 'groups' : 'role')
@icon($modelType === 'fallback' ? 'groups' : ($modelType === 'role' ? 'role' : 'user'))
</div>
<span>
<strong>{{ $modelName }}</strong> <br>

View File

@ -35,6 +35,27 @@
<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">
@foreach($data->permissionsWithRoles() as $permission)
@include('form.entity-permissions-row', [

View File

@ -217,7 +217,8 @@ Route::middleware('auth')->group(function () {
Route::get('/home', [HomeController::class, 'index']);
// 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
Route::get('/settings/maintenance', [MaintenanceController::class, 'index']);