Files
mail-intake/mail_intake/credentials/redis.py

146 lines
4.8 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 collections.abc import Callable
from typing import Any, TypeVar
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: Any,
key: str,
serialize: Callable[[T], bytes],
deserialize: Callable[[bytes], T],
ttl_seconds: int | None = None,
):
"""
Initialize a Redis-backed credential store.
Args:
redis_client (Any):
Initialized Redis client instance used for persistence.
key (str):
Storage key under which credentials are persisted.
serialize (Callable[[T], bytes]):
Callable that encodes credentials to bytes for storage.
deserialize (Callable[[bytes], T]):
Callable that decodes stored bytes back into credentials.
ttl_seconds (int | None):
Optional time-to-live in seconds after which stored
credentials expire automatically. ``None`` disables expiry.
"""
self.redis = redis_client
self.key = key
self.serialize = serialize
self.deserialize = deserialize
self.ttl_seconds = ttl_seconds
def load(self) -> T | None:
"""
Load credentials from Redis.
Returns:
T | None:
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)