diff --git a/src/Auth/Eloquent/User.php b/src/Auth/Eloquent/User.php index 3de4a068e2..4eae32e6d9 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(); @@ -227,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')); + } }