Also updated non-joint permission handling to support user permissions.
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
{
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.
+ * Get the role assigned to this entity permission.
*/
- public function restrictable(): MorphTo
+ public function role(): BelongsTo
{
- return $this->morphTo('restrictable');
+ return $this->belongsTo(Role::class);
}
/**
- * Get the role assigned to this entity permission.
+ * Get the user assigned to this entity permission.
*/
- public function role(): BelongsTo
+ public function user(): BelongsTo
{
- return $this->belongsTo(Role::class);
+ return $this->belongsTo(User::class);
}
}
return $hasRolePermission;
}
- $hasApplicableEntityPermissions = $this->hasEntityPermission($ownable, $userRoleIds, $action);
+ $hasApplicableEntityPermissions = $this->hasEntityPermission($ownable, $userRoleIds, $user->id, $action);
return is_null($hasApplicableEntityPermissions) ? $hasRolePermission : $hasApplicableEntityPermissions;
}
* 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);
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']);
});
->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.
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) {
$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 */
->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.
use BookStack\Entities\Models\Entity;
use BookStack\Facades\Activity;
use Illuminate\Http\Request;
-use Illuminate\Support\Collection;
class PermissionsUpdater
{
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;
'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,
+ ]);
+ }
}
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();
}
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) {
}
// 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;
if (modelType === 'role') {
this.roleSelect.append(option);
}
- // TODO - User role!
+
row.remove();
}
'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
<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>
<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', [
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']);