Skip to content

Commit

Permalink
DRY permission caching
Browse files Browse the repository at this point in the history
  • Loading branch information
lukasbestle committed Jan 18, 2025
1 parent 5181783 commit 1800a69
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 24 deletions.
14 changes: 2 additions & 12 deletions src/Cms/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -318,12 +318,7 @@ public function isAccessible(): bool
return false;
}

static $accessible = [];
$role = $this->kirby()->role()?->id() ?? '__none__';
$template = $this->template() ?? '__none__';
$accessible[$role] ??= [];

return $accessible[$role][$template] ??= $this->permissions()->can('access');
return FilePermissions::canFromCache($this, 'access');
}

/**
Expand All @@ -342,12 +337,7 @@ public function isListable(): bool
return false;
}

static $listable = [];
$role = $this->kirby()->role()?->id() ?? '__none__';
$template = $this->template() ?? '__none__';
$listable[$role] ??= [];

return $listable[$role][$template] ??= $this->permissions()->can('list');
return FilePermissions::canFromCache($this, 'list');
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/Cms/FilePermissions.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ class FilePermissions extends ModelPermissions
{
protected const CATEGORY = 'files';

/**
* Used to cache once determined permissions in memory
*/
protected static function cacheKey(ModelWithContent|Language $model): string
{
return $model->template() ?? '__none__';
}

protected function canChangeTemplate(): bool
{
if (count($this->model->blueprints()) <= 1) {
Expand Down
38 changes: 38 additions & 0 deletions src/Cms/ModelPermissions.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Kirby\Cms;

use Kirby\Exception\LogicException;
use Kirby\Toolkit\A;

/**
Expand All @@ -18,6 +19,8 @@ abstract class ModelPermissions
protected const CATEGORY = 'model';
protected array $options;

protected static array $cache = [];

public function __construct(protected ModelWithContent|Language $model)
{
$this->options = match (true) {
Expand All @@ -40,6 +43,17 @@ public function __debugInfo(): array
return $this->toArray();
}

/**
* Can be overridden by specific child classes
* to return a model-specific value used to
* cache a once determined permission in memory
* @codeCoverageIgnore
*/
protected static function cacheKey(ModelWithContent|Language $model): string
{
return '';
}

/**
* Returns whether the current user is allowed to do
* a certain action on the model
Expand Down Expand Up @@ -105,6 +119,30 @@ public function can(
return $permissions->for(static::category($this->model), $action, $default);
}

/**
* Quickly determines a permission for the current user role
* and model blueprint unless dynamic checking is required
*/
public static function canFromCache(
ModelWithContent|Language $model,
string $action,
bool $default = false
): bool {
$role = $model->kirby()->role()?->id() ?? '__none__';
$category = static::category($model);
$cacheKey = $category . '.' . $action . '/' . static::cacheKey($model) . '/' . $role;

if (isset(static::$cache[$cacheKey]) === true) {
return static::$cache[$cacheKey];
}

if (method_exists(static::class, 'can' . $action) === true) {
throw new LogicException('Cannot use permission cache for dynamically-determined permission');
}

return static::$cache[$cacheKey] = $model->permissions()->can($action, $role, $default);
}

/**
* Returns whether the current user is not allowed to do
* a certain action on the model
Expand Down
14 changes: 2 additions & 12 deletions src/Cms/Page.php
Original file line number Diff line number Diff line change
Expand Up @@ -522,12 +522,7 @@ public function isAccessible(): bool
return false;
}

static $accessible = [];
$role = $this->kirby()->role()?->id() ?? '__none__';
$template = $this->intendedTemplate()->name();
$accessible[$role] ??= [];

return $accessible[$role][$template] ??= $this->permissions()->can('access');
return PagePermissions::canFromCache($this, 'access');
}

/**
Expand Down Expand Up @@ -694,12 +689,7 @@ public function isListable(): bool
return false;
}

static $listable = [];
$role = $this->kirby()->role()?->id() ?? '__none__';
$template = $this->intendedTemplate()->name();
$listable[$role] ??= [];

return $listable[$role][$template] ??= $this->permissions()->can('list');
return PagePermissions::canFromCache($this, 'list');
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/Cms/PagePermissions.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ class PagePermissions extends ModelPermissions
{
protected const CATEGORY = 'pages';

/**
* Used to cache once determined permissions in memory
*/
protected static function cacheKey(ModelWithContent|Language $model): string
{
return $model->intendedTemplate()->name();
}

protected function canChangeSlug(): bool
{
return $this->model->isHomeOrErrorPage() !== true;
Expand Down
8 changes: 8 additions & 0 deletions src/Cms/UserPermissions.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@
*/
class UserPermissions extends ModelPermissions
{
/**
* Used to cache once determined permissions in memory
*/
protected static function cacheKey(ModelWithContent|Language $model): string
{
return $model->role()->id();
}

protected function canChangeRole(): bool
{
// protect admin from role changes by non-admin
Expand Down
49 changes: 49 additions & 0 deletions tests/Cms/Files/FilePermissionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Kirby\Cms;

use Kirby\Exception\LogicException;
use Kirby\TestCase;

/**
Expand All @@ -14,6 +15,9 @@ public function setUp(): void
$this->app = new App([
'roots' => [
'index' => '/dev/null'
],
'users' => [
['id' => 'bastian', 'role' => 'admin']
]
]);
}
Expand Down Expand Up @@ -60,6 +64,51 @@ public function testWithNobody($action)
$this->assertFalse($perms->can($action));
}

/**
* @covers \Kirby\Cms\ModelPermissions::canFromCache
*/
public function testCanFromCache()
{
$this->app->impersonate('bastian');

$page = new Page(['slug' => 'test']);
$file = new File([
'filename' => 'test.jpg',
'parent' => $page,
'template' => 'some-template',
'blueprint' => [
'name' => 'files/some-template',
'options' => [
'access' => false,
'list' => false
]
]
]);

$this->assertFalse(FilePermissions::canFromCache($file, 'access'));
$this->assertFalse(FilePermissions::canFromCache($file, 'access'));
$this->assertFalse(FilePermissions::canFromCache($file, 'list'));
$this->assertFalse(FilePermissions::canFromCache($file, 'list'));
}

/**
* @covers \Kirby\Cms\ModelPermissions::canFromCache
*/
public function testCanFromCacheDynamic()
{
$this->expectException(LogicException::class);
$this->expectExceptionMessage('Cannot use permission cache for dynamically-determined permission');

$page = new Page(['slug' => 'test']);
$file = new File([
'filename' => 'test.jpg',
'parent' => $page,
'template' => 'some-template',
]);

FilePermissions::canFromCache($file, 'changeTemplate');
}

/**
* @covers ::canChangeTemplate
*/
Expand Down
44 changes: 44 additions & 0 deletions tests/Cms/Pages/PagePermissionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Kirby\Cms;

use Kirby\Exception\LogicException;
use Kirby\TestCase;

/**
Expand Down Expand Up @@ -270,6 +271,49 @@ public function testWithNobody($action)
$this->assertFalse($perms->can($action));
}

/**
* @covers \Kirby\Cms\ModelPermissions::canFromCache
*/
public function testCanFromCache()
{
$this->app->impersonate('bastian');

$page = new Page([
'slug' => 'test',
'num' => 1,
'template' => 'some-template',
'blueprint' => [
'name' => 'some-template',
'options' => [
'access' => false,
'list' => false
]
]
]);

$this->assertFalse(PagePermissions::canFromCache($page, 'access'));
$this->assertFalse(PagePermissions::canFromCache($page, 'access'));
$this->assertFalse(PagePermissions::canFromCache($page, 'list'));
$this->assertFalse(PagePermissions::canFromCache($page, 'list'));
}

/**
* @covers \Kirby\Cms\ModelPermissions::canFromCache
*/
public function testCanFromCacheDynamic()
{
$this->expectException(LogicException::class);
$this->expectExceptionMessage('Cannot use permission cache for dynamically-determined permission');

$page = new Page([
'slug' => 'test',
'num' => 1,
'template' => 'some-template',
]);

PagePermissions::canFromCache($page, 'changeTemplate');
}

/**
* @covers ::canChangeTemplate
*/
Expand Down

0 comments on commit 1800a69

Please sign in to comment.