namespace BookStack\Actions;
+use BookStack\Auth\Permissions\JointPermission;
use BookStack\Auth\User;
use BookStack\Entities\Models\Entity;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Support\Str;
return $this->belongsTo(User::class);
}
+ public function jointPermissions(): HasMany
+ {
+ return $this->hasMany(JointPermission::class, 'entity_id', 'entity_id')
+ ->whereColumn('activities.entity_type', '=', 'joint_permissions.entity_type');
+ }
+
/**
* Returns text from the language files, Looks up by using the activity key.
*/
namespace BookStack\Actions;
+use BookStack\Auth\Permissions\JointPermission;
use BookStack\Model;
+use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Favourite extends Model
{
return $this->morphTo();
}
+
+ public function jointPermissions(): HasMany
+ {
+ return $this->hasMany(JointPermission::class, 'entity_id', 'favouritable_id')
+ ->whereColumn('favourites.favouritable_type', '=', 'joint_permissions.entity_type');
+ }
}
namespace BookStack\Actions;
+use BookStack\Auth\Permissions\JointPermission;
use BookStack\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
return $this->morphTo('entity');
}
+ public function jointPermissions(): HasMany
+ {
+ return $this->hasMany(JointPermission::class, 'entity_id', 'entity_id')
+ ->whereColumn('tags.entity_type', '=', 'joint_permissions.entity_type');
+ }
+
/**
* Get a full URL to start a tag name search for this tag name.
*/
namespace BookStack\Actions;
+use BookStack\Auth\Permissions\JointPermission;
use BookStack\Interfaces\Viewable;
use BookStack\Model;
+use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
return $this->morphTo();
}
+ public function jointPermissions(): HasMany
+ {
+ return $this->hasMany(JointPermission::class, 'entity_id', 'viewable_id')
+ ->whereColumn('views.viewable_type', '=', 'joint_permissions.entity_type');
+ }
+
/**
* Increment the current user's view count for the given viewable model.
*/
{
// Ensure system admin role retains permissions
if ($isAdminRole) {
- return $this->createJointPermissionDataArray($entity, $roleId, true, true);
+ return $this->createJointPermissionDataArray($entity, $roleId, PermissionStatus::EXPLICIT_ALLOW, true);
}
// Return evaluated entity permission status if it has an affect.
$entityPermissionStatus = $permissionMap->evaluateEntityForRole($entity, $roleId);
if ($entityPermissionStatus !== null) {
- return $this->createJointPermissionDataArray($entity, $roleId, $entityPermissionStatus, $entityPermissionStatus);
+ $status = $entityPermissionStatus ? PermissionStatus::EXPLICIT_ALLOW : PermissionStatus::EXPLICIT_DENY;
+ return $this->createJointPermissionDataArray($entity, $roleId, $status, $entityPermissionStatus);
}
// Otherwise default to the role-level permissions
$permissionPrefix = $entity->type . '-view';
$roleHasPermission = isset($rolePermissionMap[$roleId . ':' . $permissionPrefix . '-all']);
$roleHasPermissionOwn = isset($rolePermissionMap[$roleId . ':' . $permissionPrefix . '-own']);
- return $this->createJointPermissionDataArray($entity, $roleId, $roleHasPermission, $roleHasPermissionOwn);
+ $status = $roleHasPermission ? PermissionStatus::IMPLICIT_ALLOW : PermissionStatus::IMPLICIT_DENY;
+ return $this->createJointPermissionDataArray($entity, $roleId, $status, $roleHasPermissionOwn);
}
/**
* Create an array of data with the information of an entity jointPermissions.
* Used to build data for bulk insertion.
*/
- protected function createJointPermissionDataArray(SimpleEntityData $entity, int $roleId, bool $permissionAll, bool $permissionOwn): array
+ protected function createJointPermissionDataArray(SimpleEntityData $entity, int $roleId, int $permissionStatus, bool $hasPermissionOwn): array
{
+ $ownPermissionActive = ($hasPermissionOwn && $permissionStatus !== PermissionStatus::EXPLICIT_DENY && $entity->owned_by);
+
return [
- 'entity_id' => $entity->id,
- 'entity_type' => $entity->type,
- 'has_permission' => $permissionAll,
- 'has_permission_own' => $permissionOwn,
- 'owned_by' => $entity->owned_by,
- 'role_id' => $roleId,
+ 'entity_id' => $entity->id,
+ 'entity_type' => $entity->type,
+ 'role_id' => $roleId,
+ 'status' => $permissionStatus,
+ 'owner_id' => $ownPermissionActive ? $entity->owned_by : null,
];
}
}
return $query->where(function (Builder $parentQuery) {
$parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) {
$permissionQuery->select(['entity_id', 'entity_type'])
- ->selectRaw('max(owned_by) as owned_by')
- ->selectRaw('max(has_permission) as has_permission')
- ->selectRaw('max(has_permission_own) as has_permission_own')
+ ->selectRaw('max(owner_id) as owner_id')
+ ->selectRaw('max(status) as status')
->whereIn('role_id', $this->getCurrentUserRoleIds())
->groupBy(['entity_type', 'entity_id'])
- ->havingRaw('has_permission > 0')
- ->orHavingRaw('(has_permission_own > 0 and owned_by = ?)', [$this->currentUser()->id]);
+ ->havingRaw('(status IN (1, 3) or owner_id = ?)', [$this->currentUser()->id]);
});
});
}
* 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 restrictEntityRelationQuery($query, string $tableName, string $entityIdColumn, string $entityTypeColumn)
+ public function restrictEntityRelationQuery(Builder $query, string $tableName, string $entityIdColumn, string $entityTypeColumn): Builder
{
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
$pageMorphClass = (new Page())->getMorphClass();
- $q = $query->whereExists(function ($permissionQuery) use (&$tableDetails) {
- /** @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'])
- ->whereIn('joint_permissions.role_id', $this->getCurrentUserRoleIds())
- ->where(function (QueryBuilder $query) {
- $this->addJointHasPermissionCheck($query, $this->currentUser()->id);
- });
- })->where(function ($query) use ($tableDetails, $pageMorphClass) {
- /** @var Builder $query */
- $query->where($tableDetails['entityTypeColumn'], '!=', $pageMorphClass)
+ return $this->restrictEntityQuery($query)
+ ->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);
});
- });
-
- return $q;
+ });
}
/**
*/
public function restrictPageRelationQuery(Builder $query, string $tableName, string $pageIdColumn): Builder
{
+ // TODO - Refactor
$fullPageIdColumn = $tableName . '.' . $pageIdColumn;
$morphClass = (new Page())->getMorphClass();
--- /dev/null
+<?php
+
+namespace BookStack\Auth\Permissions;
+
+class PermissionStatus
+{
+ const IMPLICIT_DENY = 0;
+ const IMPLICIT_ALLOW = 1;
+ const EXPLICIT_DENY = 2;
+ const EXPLICIT_ALLOW = 3;
+}
namespace BookStack\References;
+use BookStack\Auth\Permissions\JointPermission;
use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
{
return $this->morphTo('to');
}
+
+ public function jointPermissions(): HasMany
+ {
+ return $this->hasMany(JointPermission::class, 'entity_id', 'from_id')
+ ->whereColumn('references.from_type', '=', 'joint_permissions.entity_type');
+ }
}
use BookStack\Auth\Permissions\PermissionApplicator;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
+use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\Relation;
*/
public function getPageReferencesToEntity(Entity $entity): Collection
{
- $baseQuery = $entity->referencesTo()
- ->where('from_type', '=', (new Page())->getMorphClass())
+ $baseQuery = $this->queryPageReferencesToEntity($entity)
->with([
'from' => fn (Relation $query) => $query->select(Page::$listAttributes),
'from.book' => fn (Relation $query) => $query->scopes('visible'),
*/
public function getPageReferenceCountToEntity(Entity $entity): int
{
- $baseQuery = $entity->referencesTo()
- ->where('from_type', '=', (new Page())->getMorphClass());
-
$count = $this->permissions->restrictEntityRelationQuery(
- $baseQuery,
+ $this->queryPageReferencesToEntity($entity),
'references',
'from_id',
'from_type'
return $count;
}
+
+ protected function queryPageReferencesToEntity(Entity $entity): Builder
+ {
+ return Reference::query()
+ ->where('to_type', '=', $entity->getMorphClass())
+ ->where('to_id', '=', $entity->id)
+ ->where('from_type', '=', (new Page())->getMorphClass());
+ }
}
--- /dev/null
+<?php
+
+use BookStack\Auth\Permissions\JointPermissionBuilder;
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Schema;
+
+class RefactorJointPermissionsStorage extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ // Truncate before schema changes to avoid performance issues
+ // since we'll need to rebuild anyway.
+ DB::table('joint_permissions')->truncate();
+
+ if (Schema::hasColumn('joint_permissions', 'owned_by')) {
+ Schema::table('joint_permissions', function (Blueprint $table) {
+ $table->dropColumn(['has_permission', 'has_permission_own', 'owned_by']);
+
+ $table->unsignedTinyInteger('status')->index();
+ $table->unsignedInteger('owner_id')->nullable()->index();
+ });
+ }
+
+ // Rebuild permissions
+ app(JointPermissionBuilder::class)->rebuildForAll();
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ DB::table('joint_permissions')->truncate();
+
+ Schema::table('joint_permissions', function (Blueprint $table) {
+ $table->dropColumn(['status', 'owner_id']);
+
+ $table->boolean('has_permission')->index();
+ $table->boolean('has_permission_own')->index();
+ $table->unsignedInteger('created_by')->index();
+ });
+ }
+}