149 lines
5.1 KiB
Python
149 lines
5.1 KiB
Python
"""
|
|
# Summary
|
|
|
|
Redis-backed credential persistence for Mail Intake.
|
|
|
|
This module provides a Redis-based implementation of the
|
|
`CredentialStore` abstraction, enabling credential persistence
|
|
across distributed and horizontally scaled deployments.
|
|
|
|
The Redis credential store is designed for environments where
|
|
authentication credentials must be shared safely across multiple
|
|
processes, containers, or nodes, such as container orchestration
|
|
platforms and microservice architectures.
|
|
|
|
Key characteristics:
|
|
|
|
- Distributed-safe, shared storage using Redis.
|
|
- Explicit, caller-defined serialization and deserialization.
|
|
- No reliance on unsafe mechanisms such as `pickle`.
|
|
- Optional time-to-live (TTL) support for automatic credential expiry.
|
|
|
|
This module is responsible solely for persistence concerns.
|
|
Credential validation, refresh, rotation, and acquisition remain the
|
|
responsibility of authentication provider implementations.
|
|
"""
|
|
|
|
|
|
from typing import Optional, TypeVar, Callable
|
|
|
|
from mail_intake.credentials.store import CredentialStore
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
class RedisCredentialStore(CredentialStore[T]):
|
|
"""
|
|
Redis-backed implementation of `CredentialStore`.
|
|
|
|
This store persists credentials in Redis and is suitable for
|
|
distributed and horizontally scaled deployments where credentials
|
|
must be shared across multiple processes or nodes.
|
|
|
|
Notes:
|
|
**Responsibilities:**
|
|
|
|
- This class is responsible only for persistence and retrieval.
|
|
- It does not interpret, validate, refresh, or otherwise manage the
|
|
lifecycle of the credentials being stored.
|
|
|
|
**Guarantees:**
|
|
|
|
- The store is intentionally generic and delegates all serialization
|
|
concerns to caller-provided functions.
|
|
- This avoids unsafe mechanisms such as `pickle` and allows
|
|
credential formats to be explicitly controlled and audited.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
redis_client,
|
|
key: str,
|
|
serialize: Callable[[T], bytes],
|
|
deserialize: Callable[[bytes], T],
|
|
ttl_seconds: Optional[int] = None,
|
|
):
|
|
"""
|
|
Initialize a Redis-backed credential store.
|
|
|
|
Args:
|
|
redis_client (Any):
|
|
An initialized Redis client instance (for example, ``redis.Redis`` or a compatible interface) used to communicate with the Redis server.
|
|
|
|
key (str):
|
|
The Redis key under which credentials are stored. Callers are responsible for applying appropriate namespacing to avoid collisions.
|
|
|
|
serialize (Callable[[T], bytes]):
|
|
A callable that converts a credential object of type ``T`` into a ``bytes`` representation suitable for storage in Redis.
|
|
|
|
deserialize (Callable[[bytes], T]):
|
|
A callable that converts a ``bytes`` payload retrieved from Redis back into a credential object of type ``T``.
|
|
|
|
ttl_seconds (Optional[int]):
|
|
Optional time-to-live (TTL) for the stored credentials, expressed in seconds. When provided, Redis will automatically expire the stored credentials after the specified duration. If ``None``, credentials are stored without an expiration.
|
|
"""
|
|
self.redis = redis_client
|
|
self.key = key
|
|
self.serialize = serialize
|
|
self.deserialize = deserialize
|
|
self.ttl_seconds = ttl_seconds
|
|
|
|
def load(self) -> Optional[T]:
|
|
"""
|
|
Load credentials from Redis.
|
|
|
|
Returns:
|
|
Optional[T]:
|
|
An instance of type `T` if credentials are present and
|
|
successfully deserialized; otherwise `None`.
|
|
|
|
Notes:
|
|
**Guarantees:**
|
|
|
|
- If no value exists for the configured key, or if the stored
|
|
payload cannot be successfully deserialized, this method
|
|
returns `None`.
|
|
- The store does not attempt to validate the returned
|
|
credentials or determine whether they are expired or
|
|
otherwise usable.
|
|
"""
|
|
raw = self.redis.get(self.key)
|
|
if not raw:
|
|
return None
|
|
try:
|
|
return self.deserialize(raw)
|
|
except Exception:
|
|
return None
|
|
|
|
def save(self, credentials: T) -> None:
|
|
"""
|
|
Persist credentials to Redis.
|
|
|
|
Args:
|
|
credentials (T):
|
|
The credential object to persist.
|
|
|
|
Notes:
|
|
**Responsibilities:**
|
|
|
|
- Any previously stored credentials under the same key are overwritten
|
|
- If a TTL is configured, the credentials will expire automatically after the specified duration
|
|
"""
|
|
payload = self.serialize(credentials)
|
|
if self.ttl_seconds:
|
|
self.redis.setex(self.key, self.ttl_seconds, payload)
|
|
else:
|
|
self.redis.set(self.key, payload)
|
|
|
|
def clear(self) -> None:
|
|
"""
|
|
Remove stored credentials from Redis.
|
|
|
|
Notes:
|
|
**Lifecycle:**
|
|
|
|
- This operation deletes the configured Redis key if it exists
|
|
- Implementations should treat this method as idempotent
|
|
"""
|
|
self.redis.delete(self.key)
|