refactor(auth): type auth providers and decouple Google auth from disk storage
- Make MailIntakeAuthProvider generic over credential type to enforce typed auth contracts between providers and adapters - Refactor Google OAuth provider to use CredentialStore abstraction instead of filesystem-based pickle persistence - Remove node-local state assumptions from Google auth implementation - Clarify documentation to distinguish credential lifecycle from credential persistence responsibilities This change enables distributed-safe authentication providers and allows multiple credential persistence strategies without modifying auth logic.
This commit is contained in:
142
mail_intake/credentials/redis.py
Normal file
142
mail_intake/credentials/redis.py
Normal file
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
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:
|
||||
An initialized Redis client instance (for example,
|
||||
``redis.Redis`` or a compatible interface) used to
|
||||
communicate with the Redis server.
|
||||
|
||||
key:
|
||||
The Redis key under which credentials are stored.
|
||||
Callers are responsible for applying appropriate
|
||||
namespacing to avoid collisions.
|
||||
|
||||
serialize:
|
||||
A callable that converts a credential object of type
|
||||
``T`` into a ``bytes`` representation suitable for
|
||||
storage in Redis.
|
||||
|
||||
deserialize:
|
||||
A callable that converts a ``bytes`` payload retrieved
|
||||
from Redis back into a credential object of type ``T``.
|
||||
|
||||
ttl_seconds:
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
Returns:
|
||||
An instance of type ``T`` if credentials are present and
|
||||
successfully deserialized; otherwise ``None``.
|
||||
"""
|
||||
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.
|
||||
|
||||
Any previously stored credentials under the same key are
|
||||
overwritten. If a TTL is configured, the credentials will
|
||||
expire automatically after the specified duration.
|
||||
|
||||
Args:
|
||||
credentials:
|
||||
The credential object to persist.
|
||||
"""
|
||||
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.
|
||||
|
||||
This operation deletes the configured Redis key if it exists.
|
||||
Implementations should treat this method as idempotent.
|
||||
"""
|
||||
self.redis.delete(self.key)
|
||||
Reference in New Issue
Block a user