Aligned logic to entity_permission role_id usage change

Now idenitifies fallback using role_id and user_id = null.
Lays some foundations for handling user_id.
This commit is contained in:
Dan Brown 2022-12-07 22:07:03 +00:00
parent 1c53ffc4d1
commit f8c4725166
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
11 changed files with 129 additions and 59 deletions

View File

@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
/** /**
* @property int $id * @property int $id
* @property int $role_id * @property int $role_id
* @property int $user_id
* @property int $entity_id * @property int $entity_id
* @property string $entity_type * @property string $entity_type
* @property boolean $view * @property boolean $view

View File

@ -245,7 +245,9 @@ class JointPermissionBuilder
// Create a mapping of explicit entity permissions // Create a mapping of explicit entity permissions
$permissionMap = []; $permissionMap = [];
foreach ($permissions as $permission) { foreach ($permissions as $permission) {
$key = $permission->entity_type . ':' . $permission->entity_id . ':' . $permission->role_id; $type = $permission->role_id ? 'role' : ($permission->user_id ? 'user' : 'fallback');
$id = $permission->role_id ?? $permission->user_id ?? '0';
$key = $permission->entity_type . ':' . $permission->entity_id . ':' . $type . ':' . $id;
$permissionMap[$key] = $permission->view; $permissionMap[$key] = $permission->view;
} }
@ -376,18 +378,18 @@ class JointPermissionBuilder
protected function entityPermissionsActiveForRole(array $permissionMap, SimpleEntityData $entity, int $roleId): bool protected function entityPermissionsActiveForRole(array $permissionMap, SimpleEntityData $entity, int $roleId): bool
{ {
$keyPrefix = $entity->type . ':' . $entity->id . ':'; $keyPrefix = $entity->type . ':' . $entity->id . ':';
return isset($permissionMap[$keyPrefix . $roleId]) || isset($permissionMap[$keyPrefix . '0']); return isset($permissionMap[$keyPrefix . 'role:' . $roleId]) || isset($permissionMap[$keyPrefix . 'fallback:0']);
} }
/** /**
* Check for an active restriction in an entity map. * Check for an active restriction in an entity map.
*/ */
protected function mapHasActiveRestriction(array $entityMap, SimpleEntityData $entity, int $roleId): bool protected function mapHasActiveRestriction(array $permissionMap, SimpleEntityData $entity, int $roleId): bool
{ {
$roleKey = $entity->type . ':' . $entity->id . ':' . $roleId; $roleKey = $entity->type . ':' . $entity->id . ':role:' . $roleId;
$defaultKey = $entity->type . ':' . $entity->id . ':0'; $defaultKey = $entity->type . ':' . $entity->id . ':fallback:0';
return $entityMap[$roleKey] ?? $entityMap[$defaultKey] ?? false; return $permissionMap[$roleKey] ?? $permissionMap[$defaultKey] ?? false;
} }
/** /**

View File

@ -78,26 +78,53 @@ class PermissionApplicator
} }
foreach ($chain as $currentEntity) { foreach ($chain as $currentEntity) {
$allowedByRoleId = $currentEntity->permissions() $relevantPermissions = $currentEntity->permissions()
->whereIn('role_id', [0, ...$userRoleIds]) ->where(function (Builder $query) use ($userRoleIds) {
->pluck($action, 'role_id'); $query->whereIn('role_id', $userRoleIds)
->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
// 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;
}
// Continue up the chain if no applicable entity permission overrides. // Continue up the chain if no applicable entity permission overrides.
if ($allowedByRoleId->isEmpty()) { if (empty($allowedById)) {
continue; continue;
} }
// If we have user-role-specific permissions set, allow if any of those // If we have user-role-specific permissions set, allow if any of those
// role permissions allow access. // role permissions allow access.
$hasDefault = $allowedByRoleId->has(0); $hasDefault = isset($allowedById[':']);
if (!$hasDefault || $allowedByRoleId->count() > 1) { if (!$hasDefault || count($allowedById) > 1) {
return $allowedByRoleId->search(function (bool $allowed, int $roleId) { foreach ($allowedById as $key => $allowed) {
return $roleId !== 0 && $allowed; if ($key !== ':' && $allowed) {
}) !== false; return true;
}
}
return false;
} }
// Otherwise, return the default "Other roles" fallback value. // Otherwise, return the default "Other roles" fallback value.
return $allowedByRoleId->get(0); return $allowedById[':'];
} }
return null; return null;
@ -114,6 +141,7 @@ class PermissionApplicator
$permissionQuery = EntityPermission::query() $permissionQuery = EntityPermission::query()
->where($action, '=', true) ->where($action, '=', true)
->whereIn('role_id', $this->getCurrentUserRoleIds()); ->whereIn('role_id', $this->getCurrentUserRoleIds());
// TODO - Update for user permission
if (!empty($entityClass)) { if (!empty($entityClass)) {
/** @var Entity $entityInstance */ /** @var Entity $entityInstance */
@ -134,6 +162,7 @@ class PermissionApplicator
{ {
return $query->where(function (Builder $parentQuery) { return $query->where(function (Builder $parentQuery) {
$parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) { $parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) {
// TODO - Update for user permission
$permissionQuery->whereIn('role_id', $this->getCurrentUserRoleIds()) $permissionQuery->whereIn('role_id', $this->getCurrentUserRoleIds())
->where(function (Builder $query) { ->where(function (Builder $query) {
$this->addJointHasPermissionCheck($query, $this->currentUser()->id); $this->addJointHasPermissionCheck($query, $this->currentUser()->id);
@ -170,6 +199,7 @@ class PermissionApplicator
$pageMorphClass = (new Page())->getMorphClass(); $pageMorphClass = (new Page())->getMorphClass();
$q = $query->whereExists(function ($permissionQuery) use (&$tableDetails) { $q = $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
// TODO - Update for user permission
/** @var Builder $permissionQuery */ /** @var Builder $permissionQuery */
$permissionQuery->select(['role_id'])->from('joint_permissions') $permissionQuery->select(['role_id'])->from('joint_permissions')
->whereColumn('joint_permissions.entity_id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) ->whereColumn('joint_permissions.entity_id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
@ -203,6 +233,7 @@ class PermissionApplicator
$fullPageIdColumn = $tableName . '.' . $pageIdColumn; $fullPageIdColumn = $tableName . '.' . $pageIdColumn;
$morphClass = (new Page())->getMorphClass(); $morphClass = (new Page())->getMorphClass();
// TODO - Update for user permission
$existsQuery = function ($permissionQuery) use ($fullPageIdColumn, $morphClass) { $existsQuery = function ($permissionQuery) use ($fullPageIdColumn, $morphClass) {
/** @var Builder $permissionQuery */ /** @var Builder $permissionQuery */
$permissionQuery->select('joint_permissions.role_id')->from('joint_permissions') $permissionQuery->select('joint_permissions.role_id')->from('joint_permissions')

View File

@ -21,7 +21,7 @@ class PermissionFormData
{ {
return $this->entity->permissions() return $this->entity->permissions()
->with('role') ->with('role')
->where('role_id', '!=', 0) ->whereNotNull('role_id')
->get() ->get()
->sortBy('role.display_name') ->sortBy('role.display_name')
->all(); ->all();
@ -33,7 +33,7 @@ class PermissionFormData
*/ */
public function rolesNotAssigned(): array public function rolesNotAssigned(): array
{ {
$assigned = $this->entity->permissions()->pluck('role_id'); $assigned = $this->entity->permissions()->whereNotNull('role_id')->pluck('role_id');
return Role::query() return Role::query()
->where('system_name', '!=', 'admin') ->where('system_name', '!=', 'admin')
->whereNotIn('id', $assigned) ->whereNotIn('id', $assigned)
@ -49,20 +49,19 @@ class PermissionFormData
{ {
/** @var ?EntityPermission $permission */ /** @var ?EntityPermission $permission */
$permission = $this->entity->permissions() $permission = $this->entity->permissions()
->where('role_id', '=', 0) ->whereNull(['role_id', 'user_id'])
->first(); ->first();
return $permission ?? (new EntityPermission()); return $permission ?? (new EntityPermission());
} }
/** /**
* Get the "Everyone Else" role entry. * Check if the "Everyone else" option is inheriting default role system permissions.
* Is determined by any system entity_permission existing for the current entity.
*/ */
public function everyoneElseRole(): Role public function everyoneElseInheriting(): bool
{ {
return (new Role())->forceFill([ return !$this->entity->permissions()
'id' => 0, ->whereNull(['role_id', 'user_id'])
'display_name' => trans('entities.permissions_role_everyone_else'), ->exists();
'description' => trans('entities.permissions_role_everyone_else_desc'),
]);
} }
} }

View File

@ -58,13 +58,30 @@ class PermissionsUpdater
protected function formatPermissionsFromRequestToEntityPermissions(array $permissions): array protected function formatPermissionsFromRequestToEntityPermissions(array $permissions): array
{ {
$formatted = []; $formatted = [];
$columnsByType = [
'role' => 'role_id',
'user' => 'user_id',
'fallback' => '',
];
foreach ($permissions as $roleId => $info) { foreach ($permissions as $type => $byId) {
$entityPermissionData = ['role_id' => $roleId]; $column = $columnsByType[$type] ?? null;
foreach (EntityPermission::PERMISSIONS as $permission) { if (is_null($column)) {
$entityPermissionData[$permission] = (($info[$permission] ?? false) === "true"); continue;
}
foreach ($byId as $id => $info) {
$entityPermissionData = [];
if (!empty($column)) {
$entityPermissionData[$column] = $id;
}
foreach (EntityPermission::PERMISSIONS as $permission) {
$entityPermissionData[$permission] = (($info[$permission] ?? false) === "true");
}
$formatted[] = $entityPermissionData;
} }
$formatted[] = $entityPermissionData;
} }
return $formatted; return $formatted;

View File

@ -162,10 +162,14 @@ class PermissionsController extends Controller
{ {
$this->checkPermissionOr('restrictions-manage-all', fn() => userCan('restrictions-manage-own')); $this->checkPermissionOr('restrictions-manage-all', fn() => userCan('restrictions-manage-own'));
/** @var Role $role */
$role = Role::query()->findOrFail($roleId); $role = Role::query()->findOrFail($roleId);
return view('form.entity-permissions-row', [ return view('form.entity-permissions-row', [
'role' => $role, 'modelType' => 'role',
'modelId' => $role->id,
'modelName' => $role->display_name,
'modelDescription' => $role->description,
'permission' => new EntityPermission(), 'permission' => new EntityPermission(),
'entityType' => $entityType, 'entityType' => $entityType,
'inheriting' => false, 'inheriting' => false,

View File

@ -28,7 +28,7 @@ export class EntityPermissions extends Component {
// Remove role row button click // Remove role row button click
this.container.addEventListener('click', event => { this.container.addEventListener('click', event => {
const button = event.target.closest('button'); const button = event.target.closest('button');
if (button && button.dataset.roleId) { if (button && button.dataset.modelType) {
this.removeRowOnButtonClick(button) this.removeRowOnButtonClick(button)
} }
}); });
@ -61,14 +61,18 @@ export class EntityPermissions extends Component {
removeRowOnButtonClick(button) { removeRowOnButtonClick(button) {
const row = button.closest('.item-list-row'); const row = button.closest('.item-list-row');
const roleId = button.dataset.roleId; const modelId = button.dataset.modelId;
const roleName = button.dataset.roleName; const modelName = button.dataset.modelName;
const modelType = button.dataset.modelType;
const option = document.createElement('option'); const option = document.createElement('option');
option.value = roleId; option.value = modelId;
option.textContent = roleName; option.textContent = modelName;
this.roleSelect.append(option); if (modelType === 'role') {
this.roleSelect.append(option);
}
// TODO - User role!
row.remove(); row.remove();
} }

View File

@ -1,5 +1,8 @@
{{-- {{--
$role - The Role to display this row for. $modelType - The type of permission model; String matching one of: user, role, fallback
$modelId - The ID of the permission model.
$modelName - The name of the permission model.
$modelDescription - The description of the permission model.
$entityType - String identifier for type of entity having permissions applied. $entityType - String identifier for type of entity having permissions applied.
$permission - The entity permission containing the permissions. $permission - The entity permission containing the permissions.
$inheriting - Boolean if the current row should be marked as inheriting default permissions. Used for "Everyone Else" role. $inheriting - Boolean if the current row should be marked as inheriting default permissions. Used for "Everyone Else" role.
@ -7,21 +10,21 @@ $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 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="gap-x-m flex-container-row items-center px-l py-m flex">
<div class="text-large" title="{{ $role->id === 0 ? trans('entities.permissions_role_everyone_else') : trans('common.role') }}"> <div class="text-large" title="{{ $modelType === 'fallback' ? trans('entities.permissions_role_everyone_else') : trans('common.role') }}">
@icon($role->id === 0 ? 'groups' : 'role') @icon($modelType === 'fallback' ? 'groups' : 'role')
</div> </div>
<span> <span>
<strong>{{ $role->display_name }}</strong> <br> <strong>{{ $modelName }}</strong> <br>
<small class="text-muted">{{ $role->description }}</small> <small class="text-muted">{{ $modelDescription }}</small>
</span> </span>
@if($role->id !== 0) @if($modelType !== 'fallback')
<button type="button" <button type="button"
class="ml-auto flex-none text-small text-primary text-button hover-underline item-list-row-toggle-all hide-under-s" class="ml-auto flex-none text-small text-primary text-button hover-underline item-list-row-toggle-all hide-under-s"
refs="permissions-table@toggle-all" refs="permissions-table@toggle-all"
><strong>{{ trans('common.toggle_all') }}</strong></button> ><strong>{{ trans('common.toggle_all') }}</strong></button>
@endif @endif
</div> </div>
@if($role->id === 0) @if($modelType === 'fallback')
<div class="px-l flex-container-row items-center" refs="entity-permissions@everyone-inherit"> <div class="px-l flex-container-row items-center" refs="entity-permissions@everyone-inherit">
@include('form.custom-checkbox', [ @include('form.custom-checkbox', [
'name' => 'entity-permissions-inherit', 'name' => 'entity-permissions-inherit',
@ -32,12 +35,12 @@ $inheriting - Boolean if the current row should be marked as inheriting default
</div> </div>
@endif @endif
<div class="flex-container-row justify-space-between gap-x-xl wrap items-center"> <div class="flex-container-row justify-space-between gap-x-xl wrap items-center">
<input type="hidden" name="permissions[{{ $role->id }}][active]" <input type="hidden" name="permissions[{{ $modelType }}][{{ $modelId }}][active]"
@if($inheriting) disabled="disabled" @endif @if($inheriting) disabled="disabled" @endif
value="true"> value="true">
<div class="px-l"> <div class="px-l">
@include('form.custom-checkbox', [ @include('form.custom-checkbox', [
'name' => 'permissions[' . $role->id . '][view]', 'name' => 'permissions[' . $modelType . '][' . $modelId . '][view]',
'label' => trans('common.view'), 'label' => trans('common.view'),
'value' => 'true', 'value' => 'true',
'checked' => $permission->view, 'checked' => $permission->view,
@ -47,7 +50,7 @@ $inheriting - Boolean if the current row should be marked as inheriting default
@if($entityType !== 'page') @if($entityType !== 'page')
<div class="px-l"> <div class="px-l">
@include('form.custom-checkbox', [ @include('form.custom-checkbox', [
'name' => 'permissions[' . $role->id . '][create]', 'name' => 'permissions[' . $modelType . '][' . $modelId . '][create]',
'label' => trans('common.create'), 'label' => trans('common.create'),
'value' => 'true', 'value' => 'true',
'checked' => $permission->create, 'checked' => $permission->create,
@ -57,7 +60,7 @@ $inheriting - Boolean if the current row should be marked as inheriting default
@endif @endif
<div class="px-l"> <div class="px-l">
@include('form.custom-checkbox', [ @include('form.custom-checkbox', [
'name' => 'permissions[' . $role->id . '][update]', 'name' => 'permissions[' . $modelType . '][' . $modelId . '][update]',
'label' => trans('common.update'), 'label' => trans('common.update'),
'value' => 'true', 'value' => 'true',
'checked' => $permission->update, 'checked' => $permission->update,
@ -66,7 +69,7 @@ $inheriting - Boolean if the current row should be marked as inheriting default
</div> </div>
<div class="px-l"> <div class="px-l">
@include('form.custom-checkbox', [ @include('form.custom-checkbox', [
'name' => 'permissions[' . $role->id . '][delete]', 'name' => 'permissions[' . $modelType . '][' . $modelId . '][delete]',
'label' => trans('common.delete'), 'label' => trans('common.delete'),
'value' => 'true', 'value' => 'true',
'checked' => $permission->delete, 'checked' => $permission->delete,
@ -74,12 +77,13 @@ $inheriting - Boolean if the current row should be marked as inheriting default
]) ])
</div> </div>
</div> </div>
@if($role->id !== 0) @if($modelType !== 'fallback')
<div class="flex-container-row items-center px-m py-s"> <div class="flex-container-row items-center px-m py-s">
<button type="button" <button type="button"
class="text-neg p-m icon-button" class="text-neg p-m icon-button"
data-role-id="{{ $role->id }}" data-model-type="{{ $modelType }}"
data-role-name="{{ $role->display_name }}" data-model-id="{{ $modelId }}"
data-model-name="{{ $modelName }}"
title="{{ trans('common.remove') }}"> title="{{ trans('common.remove') }}">
@icon('close') <span class="hide-over-m ml-xs">{{ trans('common.remove') }}</span> @icon('close') <span class="hide-over-m ml-xs">{{ trans('common.remove') }}</span>
</button> </button>

View File

@ -39,7 +39,10 @@
@foreach($data->permissionsWithRoles() as $permission) @foreach($data->permissionsWithRoles() as $permission)
@include('form.entity-permissions-row', [ @include('form.entity-permissions-row', [
'permission' => $permission, 'permission' => $permission,
'role' => $permission->role, 'modelType' => 'role',
'modelId' => $permission->role->id,
'modelName' => $permission->role->display_name,
'modelDescription' => $permission->role->description,
'entityType' => $model->getType(), 'entityType' => $model->getType(),
'inheriting' => false, 'inheriting' => false,
]) ])
@ -60,10 +63,13 @@
<div class="item-list mt-m mb-xl"> <div class="item-list mt-m mb-xl">
@include('form.entity-permissions-row', [ @include('form.entity-permissions-row', [
'role' => $data->everyoneElseRole(), 'modelType' => 'fallback',
'modelId' => 0,
'modelName' => trans('entities.permissions_role_everyone_else'),
'modelDescription' => trans('entities.permissions_role_everyone_else_desc'),
'permission' => $data->everyoneElseEntityPermission(), 'permission' => $data->everyoneElseEntityPermission(),
'entityType' => $model->getType(), 'entityType' => $model->getType(),
'inheriting' => !$model->permissions()->where('role_id', '=', 0)->exists(), 'inheriting' => $data->everyoneElseInheriting(),
]) ])
</div> </div>

View File

@ -208,7 +208,7 @@ class EntityProvider
$permissions = [ $permissions = [
// Set default permissions to not allow actions so that only the provided role permissions are at play. // Set default permissions to not allow actions so that only the provided role permissions are at play.
['role_id' => 0, 'view' => false, 'create' => false, 'update' => false, 'delete' => false], ['role_id' => null, 'view' => false, 'create' => false, 'update' => false, 'delete' => false],
]; ];
foreach ($roles as $role) { foreach ($roles as $role) {

View File

@ -378,8 +378,10 @@ class EntityPermissionsTest extends TestCase
$this->put($modelInstance->getUrl('/permissions'), [ $this->put($modelInstance->getUrl('/permissions'), [
'permissions' => [ 'permissions' => [
$roleId => [ 'role' => [
$permission => 'true', $roleId => [
$permission => 'true',
],
], ],
], ],
]); ]);