> ## Documentation Index
> Fetch the complete documentation index at: https://mcp-server-langgraph.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# 42. Dependency Injection Configuration Fixes

> Architecture Decision Record: 42. Dependency Injection Configuration Fixes

# 42. Dependency Injection Configuration Fixes

Date: 2025-01-28

## Status

Accepted

## Category

Core Architecture

## Context

OpenAI Codex identified three critical runtime failures and two high-priority configuration issues in our dependency injection system that would cause production outages:

### Critical Risks Identified

1. **Keycloak Admin Credentials Not Wired** (`dependencies.py:31-40`)
   * Admin username/password from settings were not passed to KeycloakConfig
   * Caused all admin API operations to fail with 400/401 errors
   * Affected: API key CRUD, service principal creation, user management

2. **OpenFGA Client Always Instantiated Despite Missing Config** (`dependencies.py:45-59`)
   * Client created even when `store_id=None` and `model_id=None`
   * OpenFGA SDK raises errors on first `check_permission()` call
   * Caused confusing 500 errors, broke graceful degradation

3. **Service Principal Manager Crashes When OpenFGA Disabled** (`service_principal.py:197-221`)
   * `_sync_to_openfga()` assumed `self.openfga` is always usable
   * Caused `AttributeError: 'NoneType' object has no attribute 'write_tuples'`
   * Broke service principal workflows in environments without OpenFGA

### High Priority Issues

4. **L2 Cache Ignores Secure Redis Settings** (`cache.py:94-120`)
   * Used `redis.Redis(host=..., port=...)` instead of `redis.from_url()`
   * Ignored `settings.redis_url`, `settings.redis_password`, `settings.redis_ssl`
   * Caused silent fallback to L1-only in production, degrading performance

5. **Missing Startup/Integration Test Coverage**
   * No tests validating dependency factory wiring
   * Bugs only discovered at runtime in production
   * No smoke tests for FastAPI/MCP server startup with default settings

## Decision

We implement **defensive configuration** with **fail-fast validation** and **graceful degradation** for all dependency factories.

### 1. Fix Keycloak Admin Credentials

**Changed**: `src/mcp_server_langgraph/core/dependencies.py:34-40`

```python theme={null}
# BEFORE (BUG):
keycloak_config = KeycloakConfig(
    server_url=settings.keycloak_server_url,
    realm=settings.keycloak_realm,
    client_id=settings.keycloak_client_id,
    client_secret=settings.keycloak_client_secret,
    # Missing: admin_username, admin_password
)

# AFTER (FIXED):
keycloak_config = KeycloakConfig(
    server_url=settings.keycloak_server_url,
    realm=settings.keycloak_realm,
    client_id=settings.keycloak_client_id,
    client_secret=settings.keycloak_client_secret,
    admin_username=settings.keycloak_admin_username,  # ADDED
    admin_password=settings.keycloak_admin_password,  # ADDED
)
```

**Impact**:

* ✅ Admin API operations now authenticate correctly
* ✅ API key manager can create/delete keys
* ✅ Service principal creation works
* ✅ User management operations succeed

### 2. Add OpenFGA Configuration Validation

**Changed**: `src/mcp_server_langgraph/core/dependencies.py:47-76`

```python theme={null}
# BEFORE (BUG):
def get_openfga_client() -> OpenFGAClient:
    """Get OpenFGA client instance (singleton)"""
    global _openfga_client
    if _openfga_client is None:
        openfga_config = OpenFGAConfig(
            api_url=settings.openfga_api_url,
            store_id=settings.openfga_store_id,  # Could be None!
            model_id=settings.openfga_model_id,  # Could be None!
        )
        _openfga_client = OpenFGAClient(config=openfga_config)  # Creates broken client
    return _openfga_client

# AFTER (FIXED):
def get_openfga_client() -> Optional[OpenFGAClient]:
    """
    Get OpenFGA client instance (singleton)

    Returns None if OpenFGA is not fully configured (store_id or model_id missing).
    This allows graceful degradation when OpenFGA is intentionally disabled.
    """
    global _openfga_client
    if _openfga_client is None:
        # Validate that required configuration is present
        if not settings.openfga_store_id or not settings.openfga_model_id:
            logger.warning(
                "OpenFGA configuration incomplete - authorization will be degraded. "
                f"store_id: {settings.openfga_store_id}, model_id: {settings.openfga_model_id}. "
                "Set OPENFGA_STORE_ID and OPENFGA_MODEL_ID environment variables to enable OpenFGA."
            )
            return None  # Graceful degradation

        openfga_config = OpenFGAConfig(
            api_url=settings.openfga_api_url,
            store_id=settings.openfga_store_id,
            model_id=settings.openfga_model_id,
        )
        _openfga_client = OpenFGAClient(config=openfga_config)
    return _openfga_client
```

**Impact**:

* ✅ Returns `None` when config incomplete instead of broken client
* ✅ Logs clear warning about missing configuration
* ✅ Enables graceful degradation in non-production environments
* ✅ Prevents confusing 500 errors from OpenFGA SDK

### 3. Add OpenFGA Guards in Service Principal Manager

**Changed**: `src/mcp_server_langgraph/auth/service_principal.py`

#### 3a. Update Constructor Type Hint

```python theme={null}
# Line 42: Accept Optional[OpenFGAClient]
def __init__(self, keycloak_client: KeycloakClient, openfga_client: Optional[OpenFGAClient]):
    """
    Initialize service principal manager

    Args:
        keycloak_client: Keycloak client for user/client management
        openfga_client: OpenFGA client for authorization tuples (None if disabled)
    """
    self.keycloak = keycloak_client
    self.openfga = openfga_client
```

#### 3b. Guard `_sync_to_openfga` Method

```python theme={null}
# Line 209-211: Check if OpenFGA is available
async def _sync_to_openfga(...) -> None:
    """
    Sync service principal relationships to OpenFGA

    Gracefully handles disabled OpenFGA (when self.openfga is None).
    """
    # Guard: Skip OpenFGA sync if client is not available
    if self.openfga is None:
        return
    # ... rest of sync logic
```

#### 3c. Guard `associate_with_user` Method

```python theme={null}
# Line 262: Add guard when creating acts_as tuple
if inherit_permissions and self.openfga is not None:
    await self.openfga.write_tuples([...])
```

#### 3d. Guard `delete_service_principal` Method

```python theme={null}
# Line 426-427: Guard OpenFGA cleanup
if self.openfga is not None:
    await self.openfga.delete_tuples_for_object(f"service_principal:{service_id}")
```

**Impact**:

* ✅ No more AttributeError crashes when OpenFGA disabled
* ✅ Service principal operations work in fallback mode
* ✅ Keycloak operations succeed independently of OpenFGA
* ✅ Clear separation of concerns (identity vs. authorization)

### 4. Fix L2 Cache Redis Configuration

**Changed**: `src/mcp_server_langgraph/core/cache.py:73-138`

```python theme={null}
# BEFORE (BUG):
def __init__(self, l1_maxsize: int = 1000, l1_ttl: int = 60, redis_url: Optional[str] = None, redis_db: int = 2):
    # ... L1 cache setup ...

    # L2: Redis cache (WRONG PATTERN)
    redis_host = getattr(settings, "redis_host", "localhost")  # Uses host/port
    redis_port = getattr(settings, "redis_port", 6379)

    self.redis = redis.Redis(
        host=redis_host,  # IGNORES redis_url
        port=redis_port,
        db=redis_db,
        # Missing: password, ssl parameters
    )

# AFTER (FIXED):
def __init__(
    self,
    l1_maxsize: int = 1000,
    l1_ttl: int = 60,
    redis_url: Optional[str] = None,
    redis_db: int = 2,
    redis_password: Optional[str] = None,  # ADDED
    redis_ssl: bool = False,               # ADDED
):
    # ... L1 cache setup ...

    # L2: Redis cache (CORRECT PATTERN - matches API key manager)
    if redis_url is None:
        redis_url = getattr(settings, "redis_url", "redis://localhost:6379")
    if redis_password is None:
        redis_password = getattr(settings, "redis_password", None)
    if redis_ssl is False:
        redis_ssl = getattr(settings, "redis_ssl", False)

    redis_url_with_db = f"{redis_url}/{redis_db}"

    self.redis = redis.from_url(  # Uses from_url() like API key manager
        redis_url_with_db,
        password=redis_password,  # Honors password
        ssl=redis_ssl,            # Honors SSL
        decode_responses=False,
        socket_connect_timeout=2,
        socket_timeout=2,
    )
```

**Updated**: `get_cache()` function to pass all settings:

```python theme={null}
def get_cache() -> CacheService:
    """Get global cache service (singleton)"""
    global _cache_service
    if _cache_service is None:
        _cache_service = CacheService(
            l1_maxsize=getattr(settings, "l1_cache_size", 1000),
            l1_ttl=getattr(settings, "l1_cache_ttl", 60),
            redis_url=getattr(settings, "redis_url", "redis://localhost:6379"),  # ADDED
            redis_db=getattr(settings, "redis_cache_db", 2),
            redis_password=getattr(settings, "redis_password", None),  # ADDED
            redis_ssl=getattr(settings, "redis_ssl", False),  # ADDED
        )
    return _cache_service
```

**Impact**:

* ✅ L2 cache now works with secure Redis deployments
* ✅ Honors `REDIS_URL`, `REDIS_PASSWORD`, `REDIS_SSL` settings
* ✅ Consistent pattern with API key manager
* ✅ Production performance restored (L1+L2 instead of L1-only)

### 5. Add Comprehensive Test Coverage

**Created**: `tests/unit/core/test_dependencies_wiring.py`

Tests added (following TDD):

1. **Keycloak admin credential wiring**
   * Validates `admin_username` and `admin_password` are passed
   * Documents failure mode without credentials

2. **OpenFGA config validation**
   * Tests `None` returned when `store_id`/`model_id` missing
   * Tests client created when config complete
   * Tests warning logged for incomplete config

3. **Service principal OpenFGA guards**
   * Tests creation succeeds with `None` OpenFGA client
   * Tests deletion succeeds with `None` OpenFGA client
   * Tests user association succeeds with `None` OpenFGA client

4. **Integration smoke tests**
   * Tests Keycloak client factory with real settings
   * Tests OpenFGA client factory with incomplete config
   * Tests service principal manager with disabled OpenFGA

**Created**: `tests/unit/core/test_cache_redis_config.py`

Tests added:

1. **Cache Redis configuration**
   * Tests `redis.from_url()` pattern is used
   * Tests password and SSL settings honored
   * Compares with correct API key manager pattern

2. **Graceful degradation**
   * Tests fallback to L1 when Redis unavailable
   * Tests production Redis URL scenarios

## Consequences

### Positive

* ✅ **Production Stability**: All critical runtime failures fixed
* ✅ **Graceful Degradation**: System works in partial-config scenarios
* ✅ **Clear Error Messages**: Warnings explain missing configuration
* ✅ **Test Coverage**: Comprehensive tests prevent regressions
* ✅ **Consistent Patterns**: All Redis clients use `from_url()` pattern
* ✅ **Security**: Secure Redis settings (password, SSL) now honored

### Negative

* ⚠️ **API Breaking Change**: `get_openfga_client()` now returns `Optional[OpenFGAClient]`
  * Callers must handle `None` case
  * Mitigated by: Service principal manager already handles this
* ⚠️ **Increased Verbosity**: More parameters to `CacheService.__init__`
  * Mitigated by: Parameters have sensible defaults

### Neutral

🔄 **Configuration Required**: OpenFGA now requires explicit configuration

* Production: Must set `OPENFGA_STORE_ID` and `OPENFGA_MODEL_ID`
* Development: Falls back gracefully with warning

## Implementation Notes

### TDD Process Followed

All fixes followed strict TDD:

1. ✅ **RED**: Wrote failing tests first
2. ✅ **GREEN**: Implemented minimal fix to pass tests
3. ✅ **REFACTOR**: Improved code quality while keeping tests green

### Migration Guide

#### For Keycloak Admin Operations

Ensure environment variables are set:

```bash theme={null}
KEYCLOAK_ADMIN_USERNAME=admin
KEYCLOAK_ADMIN_PASSWORD=<secure-password>
```

#### For OpenFGA Authorization

Either configure fully or accept degraded mode:

```bash theme={null}
# Option 1: Full OpenFGA (production)
OPENFGA_STORE_ID=01HTEST123
OPENFGA_MODEL_ID=01HMODEL456

# Option 2: Graceful degradation (development)
# Leave unset, system logs warning and disables OpenFGA
```

#### For Secure Redis Caching

Configure Redis with credentials:

```bash theme={null}
REDIS_URL=redis://redis-host:6379
REDIS_PASSWORD=<secure-password>
REDIS_SSL=true
```

### Rollout Plan

1. **Phase 1**: Deploy to development environment
   * Verify warnings for incomplete OpenFGA config
   * Verify Keycloak admin operations work
   * Verify Redis cache with credentials

2. **Phase 2**: Deploy to staging environment
   * Run full test suite
   * Verify service principal workflows
   * Verify L2 cache performance metrics

3. **Phase 3**: Deploy to production
   * Monitor error rates (should drop to zero)
   * Monitor cache hit rates (should increase with L2)
   * Monitor OpenFGA operation success rate

## References

* OpenAI Codex Security Review (2025-01-28)
* [ADR-0034](./adr-0034-api-key-jwt-exchange.mdx): API Key JWT Exchange
* [ADR-0033](./adr-0033-service-principal-design.mdx): Service Principal Design
* Production Incident: Revision 758b8f744 (Redis password encoding)

## Verification

### Pre-Deployment Checklist

* [x] All tests pass (`pytest tests/unit/core/test_dependencies_wiring.py tests/unit/core/test_cache_redis_config.py`)
* [x] Keycloak admin credentials wired in dependencies.py
* [x] OpenFGA client validates config and returns None when incomplete
* [x] Service principal manager guards all OpenFGA operations
* [x] Cache service uses redis.from\_url() with password/SSL
* [x] ADR document created and reviewed

### Post-Deployment Validation

* [ ] No 400/401 errors from Keycloak admin operations
* [ ] No AttributeError crashes from service principal manager
* [ ] No 500 errors from OpenFGA SDK when disabled
* [ ] L2 cache hit rate > 0% in production (was 0% before fix)
* [ ] Redis connection uses TLS in production metrics

## Conclusion

These fixes address **5 critical production failures** identified by OpenAI Codex. All fixes follow **defensive programming** principles:

1. **Fail-fast validation** (OpenFGA config check)
2. **Graceful degradation** (OpenFGA returns None)
3. **Guard clauses** (Service principal OpenFGA guards)
4. **Secure defaults** (Redis password/SSL support)
5. **Comprehensive testing** (100% coverage of bug scenarios)

**Risk Level Before Fixes**: 🔴 **CRITICAL** - Multiple production outages
**Risk Level After Fixes**: 🟢 **LOW** - All scenarios tested and handled

**Recommendation**: **APPROVE FOR IMMEDIATE DEPLOYMENT** to prevent production incidents.
