From df142b7c88c6f8f42372fee76c10e250ebe7b2ce Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Fri, 14 Aug 2026 23:00:07 +0600 Subject: [PATCH] updated fail lock --- docs/cache.rst | 32 +++++++- docs/metrics-and-locking.rst | 8 ++ docs/security.rst | 17 ++++ .../AuthenticationStateCacheInterface.php | 21 +++++ src/Cache/Cache.php | 77 +++++++++++++++++-- tests/Cache/ArchitectureHardeningTest.php | 31 ++++++++ 6 files changed, 176 insertions(+), 10 deletions(-) create mode 100644 src/Cache/AuthenticationStateCacheInterface.php diff --git a/docs/cache.rst b/docs/cache.rst index db0af23..6174993 100644 --- a/docs/cache.rst +++ b/docs/cache.rst @@ -3,9 +3,10 @@ Cache facade ============ -``Infocyph\CacheLayer\Cache\Cache`` implements PSR-6, PSR-16, and -``ArrayAccess``. It adds generation-tagged records, native bulk operations, bounded -stampede protection, tiering, metrics, and per-instance payload policy. +``Infocyph\CacheLayer\Cache\Cache`` implements PSR-6, PSR-16, ``ArrayAccess``, +and ``AuthenticationStateCacheInterface``. It adds generation-tagged records, +native bulk operations, bounded stampede protection, tiering, metrics, and +per-instance payload policy. Factories --------- @@ -91,6 +92,31 @@ Adapters can be used directly as PSR-6 pools, but CacheLayer's tagging, stampede protection, metrics, and fail-open policy live in the ``Cache`` facade. Prefer the facade unless the narrower adapter behavior is intentional. +Authentication-state capability +-------------------------------- + +Security-sensitive consumers can inspect only the effective policy they need: + +.. code-block:: php + + $cache->isFailOpen(); + $cache->hasPayloadIntegrity(); + $cache->isAuthoritative(); + $cache->authenticationStateLock(); // ?LockProviderInterface + +``isAuthoritative()`` is false for the null and tiered facades. A tiered cache +cannot safely provide a current monotonic authentication value because a stale +L1 read may hide newer lower-tier state. CacheLayer cannot detect whether an +injected Redis/Valkey/SQL client reads from a replica; applications must provide +a primary/authoritative connection. + +``authenticationStateLock()`` returns a provider only when one was explicitly +configured by the facade factory, constructor, or ``setLockProvider()`` and the +cache is authoritative. Direct construction without a lock, ``nullStore()``, +and ``tiered()`` return null. This lets a consumer reject unsupported state +configuration instead of silently coordinating a distributed cache with a +process-local lock. + Tiering ------- diff --git a/docs/metrics-and-locking.rst b/docs/metrics-and-locking.rst index 10e78af..3fb7d9a 100644 --- a/docs/metrics-and-locking.rst +++ b/docs/metrics-and-locking.rst @@ -60,6 +60,7 @@ Facade helpers: * ``useRedisLock(?Redis $client = null, string $prefix = 'cachelayer:lock:'): self`` * ``useValkeyLock(?Redis $client = null, string $prefix = 'cachelayer:lock:'): self`` * ``useMemcachedLock(?Memcached $client = null, string $prefix = 'cachelayer:lock:'): self`` +* ``authenticationStateLock(): ?LockProviderInterface`` Custom lock providers can implement ``LockProviderInterface``: @@ -128,6 +129,13 @@ Adapter defaults: * Redis adapter factory sets ``RedisLockProvider`` * Valkey adapter factory sets ``RedisLockProvider`` * Memcached adapter factory sets ``MemcachedLockProvider`` +* PDO/SQLite adapter factories set ``PdoLockProvider`` or its file fallback +* local memory/file/APCu/shared-memory factories set ``FileLockProvider`` + +MongoDB, ScyllaDB, Redis Cluster, null-store, and directly constructed caches do +not claim an authentication-state lock until the caller explicitly configures +one. Tiered caches never expose an authentication-state lock because their read +path is not authoritative for monotonic state. * PDO/SQLite adapter factories set ``PdoLockProvider``; SQLite uses its file-lock fallback * all other adapters use ``FileLockProvider`` by default diff --git a/docs/security.rst b/docs/security.rst index 64df73b..de40690 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -104,6 +104,23 @@ Recommended Production Profile 4. Prefer non-executable file storage adapters over ``phpFiles`` where possible. +Authentication State +-------------------- + +For replay counters, one-time consumption, authorization state, or another +security decision, require all of the following: + +* ``isFailOpen() === false``; +* ``hasPayloadIntegrity() === true``; +* ``isAuthoritative() === true``; and +* ``authenticationStateLock()`` returns a provider in the same coordination + domain as the state backend. + +Use one direct primary backend. Do not use tiered/local-L1 reads, replica reads, +or an eventually consistent cache for monotonic authentication state. The +capability API exposes CacheLayer's effective local policy; deployment topology +such as replica routing remains the application's responsibility. + Backend-Specific Notes ---------------------- diff --git a/src/Cache/AuthenticationStateCacheInterface.php b/src/Cache/AuthenticationStateCacheInterface.php new file mode 100644 index 0000000..1cfa0e9 --- /dev/null +++ b/src/Cache/AuthenticationStateCacheInterface.php @@ -0,0 +1,21 @@ +authoritative = !in_array( + $adapter::class, + [Adapter\TieredCacheAdapter::class, Adapter\NullCacheAdapter::class], + true, + ); + $this->lockProvider = $lockProvider ?? new FileLockProvider(); + $this->authenticationStateLockCapable = $lockProvider !== null; $this->options = $options ?? new CacheOptions(); if ($adapter instanceof AbstractCacheAdapter) { $adapter->configureOptions($this->options); @@ -51,7 +64,12 @@ public function __construct( public static function apcu(string $namespace = 'default', ?CacheOptions $options = null): self { - return new self(new Adapter\ApcuCacheAdapter($namespace), options: $options, namespace: $namespace); + return new self( + new Adapter\ApcuCacheAdapter($namespace), + new FileLockProvider(), + options: $options, + namespace: $namespace, + ); } public static function file( @@ -59,7 +77,12 @@ public static function file( ?string $dir = null, ?CacheOptions $options = null, ): self { - return new self(new Adapter\FileCacheAdapter($namespace, $dir), options: $options, namespace: $namespace); + return new self( + new Adapter\FileCacheAdapter($namespace, $dir), + new FileLockProvider(), + options: $options, + namespace: $namespace, + ); } /** @param list $servers */ @@ -81,7 +104,12 @@ public static function memcached( public static function memory(string $namespace = 'default', ?CacheOptions $options = null): self { - return new self(new Adapter\ArrayCacheAdapter($namespace), options: $options, namespace: $namespace); + return new self( + new Adapter\ArrayCacheAdapter($namespace), + new FileLockProvider(), + options: $options, + namespace: $namespace, + ); } public static function mongodb( @@ -145,7 +173,12 @@ public static function phpFiles( ?string $dir = null, ?CacheOptions $options = null, ): self { - return new self(new Adapter\PhpFilesCacheAdapter($namespace, $dir), options: $options, namespace: $namespace); + return new self( + new Adapter\PhpFilesCacheAdapter($namespace, $dir), + new FileLockProvider(), + options: $options, + namespace: $namespace, + ); } public static function redis( @@ -219,6 +252,7 @@ public static function sharedMemory( ): self { return new self( new Adapter\SharedMemoryCacheAdapter($namespace, $segmentSize), + new FileLockProvider(), options: $options, namespace: $namespace, ); @@ -269,7 +303,20 @@ public static function valkey( public static function weakMap(string $namespace = 'default', ?CacheOptions $options = null): self { - return new self(new Adapter\WeakMapCacheAdapter($namespace), options: $options, namespace: $namespace); + return new self( + new Adapter\WeakMapCacheAdapter($namespace), + new FileLockProvider(), + options: $options, + namespace: $namespace, + ); + } + + public function authenticationStateLock(): ?LockProviderInterface + { + return match ([$this->authenticationStateLockCapable, $this->authoritative]) { + [true, true] => $this->lockProvider, + default => null, + }; } public function clear(): bool @@ -397,6 +444,11 @@ public function hasItem(string $key): bool return $this->getItem($key)->isHit(); } + public function hasPayloadIntegrity(): bool + { + return $this->options->integrityKey !== null; + } + public function invalidateTag(string $tag): bool { return $this->invalidateTags([$tag]); @@ -411,6 +463,16 @@ public function invalidateTags(array $tags): bool return $invalidated; } + public function isAuthoritative(): bool + { + return $this->authoritative; + } + + public function isFailOpen(): bool + { + return $this->options->failOpen; + } + public function offsetExists(mixed $offset): bool { return $this->has($this->requireStringOffset($offset)); @@ -546,6 +608,7 @@ public function set(string $key, mixed $value, mixed $ttl = null): bool public function setLockProvider(LockProviderInterface $lockProvider): self { $this->lockProvider = $lockProvider; + $this->authenticationStateLockCapable = true; return $this; } diff --git a/tests/Cache/ArchitectureHardeningTest.php b/tests/Cache/ArchitectureHardeningTest.php index eb3619d..121094a 100644 --- a/tests/Cache/ArchitectureHardeningTest.php +++ b/tests/Cache/ArchitectureHardeningTest.php @@ -9,6 +9,7 @@ use Infocyph\CacheLayer\Cache\Cache; use Infocyph\CacheLayer\Cache\CacheOptions; use Infocyph\CacheLayer\Cache\Item\CacheItem; +use Infocyph\CacheLayer\Cache\Lock\FileLockProvider; use Infocyph\CacheLayer\Exceptions\CacheBackendException; use Infocyph\CacheLayer\Exceptions\CacheInvalidArgumentException; use Psr\Cache\CacheItemInterface; @@ -261,6 +262,36 @@ public function resetOperationCounts(): void expect(fn() => $closed->get('key'))->toThrow(CacheBackendException::class); }); +test('authentication state capabilities expose effective cache policy', function () { + $safe = Cache::memory( + 'authentication-state', + new CacheOptions(integrityKey: 'state-integrity-key', failOpen: false), + ); + + expect($safe->isFailOpen())->toBeFalse() + ->and($safe->hasPayloadIntegrity())->toBeTrue() + ->and($safe->isAuthoritative())->toBeTrue() + ->and($safe->authenticationStateLock())->toBeInstanceOf(FileLockProvider::class); + + $default = new Cache(new ArchitectureHardeningTest()); + expect($default->isFailOpen())->toBeTrue() + ->and($default->hasPayloadIntegrity())->toBeFalse() + ->and($default->authenticationStateLock())->toBeNull(); + + $default->setLockProvider(new FileLockProvider()); + expect($default->authenticationStateLock())->toBeInstanceOf(FileLockProvider::class); +}); + +test('non-authoritative caches cannot expose an authentication state lock', function () { + $tiered = Cache::tiered([new ArchitectureHardeningTest()]); + $tiered->setLockProvider(new FileLockProvider()); + + expect($tiered->isAuthoritative())->toBeFalse() + ->and($tiered->authenticationStateLock())->toBeNull() + ->and(Cache::nullStore()->isAuthoritative())->toBeFalse() + ->and(Cache::nullStore()->authenticationStateLock())->toBeNull(); +}); + test('fail-closed backend failures satisfy both PSR cache exception contracts', function () { $exception = new CacheBackendException('failed');