From fbc6cbd92689f431e229f2d1d500f3baf615b47a Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Wed, 13 May 2026 07:16:21 +0100 Subject: [PATCH 1/3] Implement permissions cache on eloquent users --- src/Auth/Eloquent/User.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Auth/Eloquent/User.php b/src/Auth/Eloquent/User.php index 3de4a068e2..f704b00e7b 100644 --- a/src/Auth/Eloquent/User.php +++ b/src/Auth/Eloquent/User.php @@ -6,6 +6,7 @@ use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Hash; +use Statamic\Auth\PermissionCache; use Statamic\Auth\User as BaseUser; use Statamic\Contracts\Auth\Passkey; use Statamic\Contracts\Auth\Role as RoleContract; @@ -219,6 +220,12 @@ public function isInGroup($group) public function permissions() { + $cache = app(PermissionCache::class); + + if ($cached = $cache->get($this->id)) { + return $cached; + } + $permissions = $this->groups()->flatMap->roles() ->merge($this->roles()) ->flatMap->permissions(); From 03f01c336811669e3cb22210af4b0574fc9d3ab4 Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Wed, 13 May 2026 07:16:52 +0100 Subject: [PATCH 2/3] :beer: --- src/Auth/Eloquent/User.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Auth/Eloquent/User.php b/src/Auth/Eloquent/User.php index f704b00e7b..8779b9dac9 100644 --- a/src/Auth/Eloquent/User.php +++ b/src/Auth/Eloquent/User.php @@ -225,7 +225,7 @@ public function permissions() if ($cached = $cache->get($this->id)) { return $cached; } - + $permissions = $this->groups()->flatMap->roles() ->merge($this->roles()) ->flatMap->permissions(); From 2f7ec9c24f846c64c1eb4e09164aa2de4f99729c Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Wed, 13 May 2026 18:47:37 +0100 Subject: [PATCH 3/3] fixes and test coverage --- src/Auth/Eloquent/User.php | 6 ++- tests/Auth/Eloquent/EloquentUserTest.php | 60 ++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/Auth/Eloquent/User.php b/src/Auth/Eloquent/User.php index 8779b9dac9..4eae32e6d9 100644 --- a/src/Auth/Eloquent/User.php +++ b/src/Auth/Eloquent/User.php @@ -222,7 +222,7 @@ public function permissions() { $cache = app(PermissionCache::class); - if ($cached = $cache->get($this->id)) { + if ($cached = $cache->get($this->id())) { return $cached; } @@ -234,6 +234,10 @@ public function permissions() $permissions[] = 'super'; } + $permissions = $permissions->unique()->values(); + + $cache->put($this->id(), $permissions); + return $permissions; } diff --git a/tests/Auth/Eloquent/EloquentUserTest.php b/tests/Auth/Eloquent/EloquentUserTest.php index b57aa6fe0d..e00dcf3c57 100644 --- a/tests/Auth/Eloquent/EloquentUserTest.php +++ b/tests/Auth/Eloquent/EloquentUserTest.php @@ -14,6 +14,7 @@ use Statamic\Auth\Eloquent\WebAuthnModel; use Statamic\Auth\File\Role; use Statamic\Auth\File\UserGroup; +use Statamic\Auth\PermissionCache; use Statamic\Auth\WebAuthn\Serializer; use Statamic\Contracts\Auth\Role as RoleContract; use Statamic\Contracts\Auth\UserGroup as UserGroupContract; @@ -411,4 +412,63 @@ public function deserialize($data) ->all() ); } + + #[Test] + public function permissions_are_cached_after_first_call() + { + $role = Facades\Role::make('editor')->addPermission('access cp'); + Facades\Role::shouldReceive('find')->with('editor')->andReturn($role); + + $user = $this->createPermissible()->assignRole($role); + $user->save(); + + $cache = app(PermissionCache::class); + + $this->assertNull($cache->get($user->id())); + + $user->permissions(); + + $this->assertNotNull($cache->get($user->id())); + $this->assertTrue($cache->get($user->id())->contains('access cp')); + } + + #[Test] + public function permissions_are_read_from_cache_on_subsequent_calls() + { + $role = Facades\Role::make('editor')->addPermission('access cp'); + Facades\Role::shouldReceive('find')->with('editor')->andReturn($role); + + $user = $this->createPermissible()->assignRole($role); + $user->save(); + + $cache = app(PermissionCache::class); + + // Seed the cache with different data to prove subsequent calls use it + $cache->put($user->id(), collect(['cached-permission'])); + + $this->assertEquals(['cached-permission'], $user->permissions()->all()); + $this->assertTrue($user->hasPermission('cached-permission')); + $this->assertFalse($user->hasPermission('access cp')); + } + + #[Test] + public function permissions_cache_is_invalidated_when_cleared() + { + $role = Facades\Role::make('editor')->addPermission('access cp'); + Facades\Role::shouldReceive('find')->with('editor')->andReturn($role); + + $user = $this->createPermissible()->assignRole($role); + $user->save(); + + $cache = app(PermissionCache::class); + + $user->permissions(); + $this->assertNotNull($cache->get($user->id())); + + $cache->clear(); + $this->assertNull($cache->get($user->id())); + + // Recomputes correctly after cache is cleared + $this->assertTrue($user->permissions()->contains('access cp')); + } }