-<?php namespace BookStack\Auth\Permissions;
+<?php
+
+namespace BookStack\Auth\Permissions;
use BookStack\Auth\Role;
use BookStack\Auth\User;
}
/**
- * Set the database connection
+ * Set the database connection.
*/
public function setConnection(Connection $connection)
{
}
/**
- * Prepare the local entity cache and ensure it's empty
+ * Prepare the local entity cache and ensure it's empty.
+ *
* @param Entity[] $entities
*/
protected function readyEntityCache(array $entities = [])
}
/**
- * Get a book via ID, Checks local cache
+ * Get a book via ID, Checks local cache.
*/
protected function getBook(int $bookId): ?Book
{
}
/**
- * Get a chapter via ID, Checks local cache
+ * Get a chapter via ID, Checks local cache.
*/
protected function getChapter(int $chapterId): ?Chapter
{
},
'pages' => function ($query) {
$query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id', 'chapter_id']);
- }
+ },
]);
}
/**
* Build joint permissions for the given shelf and role combinations.
+ *
* @throws Throwable
*/
protected function buildJointPermissionsForShelves(EloquentCollection $shelves, array $roles, bool $deleteOld = false)
/**
* Build joint permissions for the given book and role combinations.
+ *
* @throws Throwable
*/
protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false)
/**
* Rebuild the entity jointPermissions for a particular entity.
+ *
* @throws Throwable
*/
public function buildJointPermissionsForEntity(Entity $entity)
if ($entity instanceof Book) {
$books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get();
$this->buildJointPermissionsForBooks($books, Role::query()->get()->all(), true);
+
return;
}
/**
* Rebuild the entity jointPermissions for a collection of entities.
+ *
* @throws Throwable
*/
public function buildJointPermissionsForEntities(array $entities)
/**
* Delete all of the entity jointPermissions for a list of entities.
+ *
* @param Role[] $roles
*/
protected function deleteManyJointPermissionsForRoles($roles)
/**
* Delete the entity jointPermissions for a particular entity.
+ *
* @param Entity $entity
+ *
* @throws Throwable
*/
public function deleteJointPermissionsForEntity(Entity $entity)
/**
* Delete all of the entity jointPermissions for a list of entities.
+ *
* @param Entity[] $entities
+ *
* @throws Throwable
*/
protected function deleteManyJointPermissionsForEntities(array $entities)
}
$this->db->transaction(function () use ($entities) {
-
foreach (array_chunk($entities, 1000) as $entityChunk) {
$query = $this->db->table('joint_permissions');
foreach ($entityChunk as $entity) {
/**
* Create & Save entity jointPermissions for many entities and roles.
+ *
* @param Entity[] $entities
- * @param Role[] $roles
+ * @param Role[] $roles
+ *
* @throws Throwable
*/
protected function createManyJointPermissions(array $entities, array $roles)
});
}
-
/**
* Get the actions related to an entity.
*/
if ($entity instanceof Book) {
$baseActions[] = 'chapter-create';
}
+
return $baseActions;
}
if ($entity->restricted) {
$hasAccess = $this->mapHasActiveRestriction($permissionMap, $entity, $role, $restrictionAction);
+
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
}
protected function mapHasActiveRestriction(array $entityMap, Entity $entity, Role $role, string $action): bool
{
$key = $entity->getMorphClass() . ':' . $entity->getRawAttribute('id') . ':' . $role->getRawAttribute('id') . ':' . $action;
+
return $entityMap[$key] ?? false;
}
protected function createJointPermissionDataArray(Entity $entity, Role $role, string $action, bool $permissionAll, bool $permissionOwn): array
{
return [
- 'role_id' => $role->getRawAttribute('id'),
- 'entity_id' => $entity->getRawAttribute('id'),
- 'entity_type' => $entity->getMorphClass(),
- 'action' => $action,
- 'has_permission' => $permissionAll,
+ 'role_id' => $role->getRawAttribute('id'),
+ 'entity_id' => $entity->getRawAttribute('id'),
+ 'entity_type' => $entity->getMorphClass(),
+ 'action' => $action,
+ 'has_permission' => $permissionAll,
'has_permission_own' => $permissionOwn,
- 'owned_by' => $entity->getRawAttribute('owned_by'),
+ 'owned_by' => $entity->getRawAttribute('owned_by'),
];
}
/**
* Checks if an entity has a restriction set upon it.
+ *
* @param HasCreatorAndUpdater|HasOwner $ownable
*/
public function checkOwnableUserAccess(Model $ownable, string $permission): bool
$ownPermission = $user && $user->can($permission . '-own');
$ownerField = ($ownable instanceof Entity) ? 'owned_by' : 'created_by';
$isOwner = $user && $user->id === $ownable->$ownerField;
- return ($allPermission || ($isOwner && $ownPermission));
+
+ return $allPermission || ($isOwner && $ownPermission);
}
// Handle abnormal create jointPermissions
$hasAccess = $this->entityRestrictionQuery($baseQuery, $action)->count() > 0;
$this->clean();
+
return $hasAccess;
}
$userRoleIds = $this->currentUser()->roles()->select('id')->pluck('id')->toArray();
$userId = $this->currentUser()->id;
- $permissionQuery = $this->db->table('joint_permissions')
+ $permissionQuery = JointPermission::query()
->where('action', '=', $permission)
->whereIn('role_id', $userRoleIds)
- ->where(function ($query) use ($userId) {
- $query->where('has_permission', '=', 1)
- ->orWhere(function ($query2) use ($userId) {
- $query2->where('has_permission_own', '=', 1)
- ->where('owned_by', '=', $userId);
- });
+ ->where(function (Builder $query) use ($userId) {
+ $this->addJointHasPermissionCheck($query, $userId);
});
if (!is_null($entityClass)) {
- $entityInstance = app()->make($entityClass);
+ $entityInstance = app($entityClass);
$permissionQuery = $permissionQuery->where('entity_type', '=', $entityInstance->getMorphClass());
}
$hasPermission = $permissionQuery->count() > 0;
$this->clean();
+
return $hasPermission;
}
$parentQuery->whereHas('jointPermissions', function ($permissionQuery) use ($action) {
$permissionQuery->whereIn('role_id', $this->getCurrentUserRoles())
->where('action', '=', $action)
- ->where(function ($query) {
- $query->where('has_permission', '=', true)
- ->orWhere(function ($query) {
- $query->where('has_permission_own', '=', true)
- ->where('owned_by', '=', $this->currentUser()->id);
- });
+ ->where(function (Builder $query) {
+ $this->addJointHasPermissionCheck($query, $this->currentUser()->id);
});
});
});
$this->clean();
+
return $q;
}
public function restrictEntityQuery(Builder $query, string $ability = 'view'): Builder
{
$this->clean();
+
return $query->where(function (Builder $parentQuery) use ($ability) {
$parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) use ($ability) {
$permissionQuery->whereIn('role_id', $this->getCurrentUserRoles())
->where('action', '=', $ability)
->where(function (Builder $query) {
- $query->where('has_permission', '=', true)
- ->orWhere(function (Builder $query) {
- $query->where('has_permission_own', '=', true)
- ->where('owned_by', '=', $this->currentUser()->id);
- });
+ $this->addJointHasPermissionCheck($query, $this->currentUser()->id);
});
});
});
/**
* Add restrictions for a generic entity.
*/
- public function enforceEntityRestrictions(string $entityType, Builder $query, string $action = 'view'): Builder
+ public function enforceEntityRestrictions(Entity $entity, Builder $query, string $action = 'view'): Builder
{
- if (strtolower($entityType) === 'page') {
+ if ($entity instanceof Page) {
// Prevent drafts being visible to others.
- $query->where(function ($query) {
- $query->where('draft', '=', false)
- ->orWhere(function ($query) {
- $query->where('draft', '=', true)
- ->where('owned_by', '=', $this->currentUser()->id);
- });
- });
+ $this->enforceDraftVisibilityOnQuery($query);
}
return $this->entityRestrictionQuery($query, $action);
/**
* Filter items that have entities set as a polymorphic relation.
+ * For simplicity, this will not return results attached to draft pages.
+ * Draft pages should never really have related items though.
+ *
+ * @param Builder|QueryBuilder $query
*/
- public function filterRestrictedEntityRelations(Builder $query, string $tableName, string $entityIdColumn, string $entityTypeColumn, string $action = 'view'): Builder
+ public function filterRestrictedEntityRelations($query, string $tableName, string $entityIdColumn, string $entityTypeColumn, string $action = 'view')
{
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
-
- $q = $query->where(function ($query) use ($tableDetails, $action) {
- $query->whereExists(function ($permissionQuery) use (&$tableDetails, $action) {
- $permissionQuery->select('id')->from('joint_permissions')
- ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
- ->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
- ->where('action', '=', $action)
- ->whereIn('role_id', $this->getCurrentUserRoles())
- ->where(function ($query) {
- $query->where('has_permission', '=', true)->orWhere(function ($query) {
- $query->where('has_permission_own', '=', true)
- ->where('owned_by', '=', $this->currentUser()->id);
- });
- });
- });
+ $pageMorphClass = (new Page())->getMorphClass();
+
+ $q = $query->whereExists(function ($permissionQuery) use (&$tableDetails, $action) {
+ /** @var Builder $permissionQuery */
+ $permissionQuery->select(['role_id'])->from('joint_permissions')
+ ->whereColumn('joint_permissions.entity_id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
+ ->whereColumn('joint_permissions.entity_type', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
+ ->where('joint_permissions.action', '=', $action)
+ ->whereIn('joint_permissions.role_id', $this->getCurrentUserRoles())
+ ->where(function (QueryBuilder $query) {
+ $this->addJointHasPermissionCheck($query, $this->currentUser()->id);
+ });
+ })->where(function ($query) use ($tableDetails, $pageMorphClass) {
+ /** @var Builder $query */
+ $query->where($tableDetails['entityTypeColumn'], '!=', $pageMorphClass)
+ ->orWhereExists(function (QueryBuilder $query) use ($tableDetails, $pageMorphClass) {
+ $query->select('id')->from('pages')
+ ->whereColumn('pages.id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
+ ->where($tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'], '=', $pageMorphClass)
+ ->where('pages.draft', '=', false);
+ });
});
$this->clean();
+
return $q;
}
*/
public function filterRelatedEntity(string $entityClass, Builder $query, string $tableName, string $entityIdColumn): Builder
{
- $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
- $morphClass = app($entityClass)->getMorphClass();
-
- $q = $query->where(function ($query) use ($tableDetails, $morphClass) {
- $query->where(function ($query) use (&$tableDetails, $morphClass) {
- $query->whereExists(function ($permissionQuery) use (&$tableDetails, $morphClass) {
- $permissionQuery->select('id')->from('joint_permissions')
- ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
- ->where('entity_type', '=', $morphClass)
- ->where('action', '=', 'view')
- ->whereIn('role_id', $this->getCurrentUserRoles())
- ->where(function ($query) {
- $query->where('has_permission', '=', true)->orWhere(function ($query) {
- $query->where('has_permission_own', '=', true)
- ->where('owned_by', '=', $this->currentUser()->id);
- });
- });
+ $fullEntityIdColumn = $tableName . '.' . $entityIdColumn;
+ $instance = new $entityClass();
+ $morphClass = $instance->getMorphClass();
+
+ $existsQuery = function ($permissionQuery) use ($fullEntityIdColumn, $morphClass) {
+ /** @var Builder $permissionQuery */
+ $permissionQuery->select('joint_permissions.role_id')->from('joint_permissions')
+ ->whereColumn('joint_permissions.entity_id', '=', $fullEntityIdColumn)
+ ->where('joint_permissions.entity_type', '=', $morphClass)
+ ->where('joint_permissions.action', '=', 'view')
+ ->whereIn('joint_permissions.role_id', $this->getCurrentUserRoles())
+ ->where(function (QueryBuilder $query) {
+ $this->addJointHasPermissionCheck($query, $this->currentUser()->id);
});
- })->orWhere($tableDetails['entityIdColumn'], '=', 0);
+ };
+
+ $q = $query->where(function ($query) use ($existsQuery, $fullEntityIdColumn) {
+ $query->whereExists($existsQuery)
+ ->orWhere($fullEntityIdColumn, '=', 0);
});
+ if ($instance instanceof Page) {
+ // Prevent visibility of non-owned draft pages
+ $q->whereExists(function (QueryBuilder $query) use ($fullEntityIdColumn) {
+ $query->select('id')->from('pages')
+ ->whereColumn('pages.id', '=', $fullEntityIdColumn)
+ ->where(function (QueryBuilder $query) {
+ $query->where('pages.draft', '=', false)
+ ->orWhere('pages.owned_by', '=', $this->currentUser()->id);
+ });
+ });
+ }
+
$this->clean();
+
return $q;
}
/**
- * Get the current user
+ * Add the query for checking the given user id has permission
+ * within the join_permissions table.
+ *
+ * @param QueryBuilder|Builder $query
+ */
+ protected function addJointHasPermissionCheck($query, int $userIdToCheck)
+ {
+ $query->where('joint_permissions.has_permission', '=', true)->orWhere(function ($query) use ($userIdToCheck) {
+ $query->where('joint_permissions.has_permission_own', '=', true)
+ ->where('joint_permissions.owned_by', '=', $userIdToCheck);
+ });
+ }
+
+ /**
+ * Get the current user.
*/
private function currentUser(): User
{