# Story: Add Redis Authentication Configuration

**Epic:** Redis Authentication
**Story ID:** redis-auth-1
**Author:** TextIQ
**Date:** 2025-12-04
**Status:** Ready for Implementation
**Estimated Effort:** 1-2 hours

---

## User Story

As a **DevOps engineer**,
I want **Redis to require password authentication**,
So that **unauthorized access is prevented and the system meets security best practices**.

---

## Acceptance Criteria

### AC1: Environment Configuration

**Given** the project configuration files
**When** I review `.env.example`
**Then** I see the following Redis configuration variables:
- `REDIS_HOST` (optional, for future flexibility)
- `REDIS_PORT` (optional, for future flexibility)
- `REDIS_PASSWORD` (with placeholder value)
- `REDIS_DB` (with default: 0)

**And** the variables are documented with comments explaining their purpose
**And** `.env.example` shows example value: `REDIS_PASSWORD=your_secure_password_here`

### AC2: Settings Class Configuration

**Given** the Settings class in `src/core/config.py`
**When** the application loads configuration
**Then** the Settings class has the following fields:
- `redis_host: str = "localhost"`
- `redis_port: int = 6379`
- `redis_password: str = ""` (empty string default)
- `redis_db: int = 0`

**And** `redis_url` is a `@property` that builds the URL dynamically:
```python
@property
def redis_url(self) -> str:
    """Build Redis URL with optional authentication and database selection."""
    if self.redis_password:
        return f"redis://:{self.redis_password}@{self.redis_host}:{self.redis_port}/{self.redis_db}"
    else:
        return f"redis://{self.redis_host}:{self.redis_port}/{self.redis_db}"
```

**And** the property includes docstring explaining the URL format

### AC3: Docker Compose Configuration

**Given** the `docker-compose.yml` file
**When** Redis service is configured
**Then** the Redis service includes:
- `command: sh -c 'redis-server $${REDIS_PASSWORD:+--requirepass "$$REDIS_PASSWORD"}'`
- Environment variable: `REDIS_PASSWORD=${REDIS_PASSWORD:-}`
- Updated healthcheck: `test: ["CMD", "sh", "-c", "redis-cli $${REDIS_PASSWORD:+-a \"$$REDIS_PASSWORD\"} ping | grep PONG"]`

**And** Redis container starts successfully with password
**And** healthcheck passes with authentication
**Note:** The `${VAR:+value}` syntax conditionally adds flags only when VAR is set, avoiding "wrong number of arguments" error

### AC4: Backward Compatibility

**Given** a developer has no `REDIS_PASSWORD` set in their `.env` file
**When** the application starts
**Then** Redis URL is built without authentication: `redis://localhost:6379/0`
**And** Redis connects successfully (assuming Redis has no password)
**And** all functionality works as before

**Given** a developer sets `REDIS_PASSWORD=testpass` in their `.env` file
**When** Docker Compose starts Redis with `--requirepass testpass`
**Then** Redis URL is built with authentication: `redis://:testpass@localhost:6379/0`
**And** all services connect successfully with password

### AC5: All Services Connect Successfully

**Given** Redis is configured with password authentication
**When** the following services start:
- FastAPI application
- Celery workers
- Health check endpoint

**Then** all services connect to Redis successfully
**And** no authentication errors appear in logs
**And** Celery tasks can be dispatched and processed
**And** Rate limiting functions correctly
**And** Health check reports Redis as "healthy"

### AC6: Documentation Updated

**Given** the project documentation
**When** I review `README.md`
**Then** I see a section explaining Redis configuration:
- How to set `REDIS_PASSWORD` in `.env`
- Why password authentication is important
- How to verify Redis authentication is working
- Production deployment considerations

**And** setup instructions include:
```bash
# Set Redis password in .env
REDIS_PASSWORD=your_secure_password_here

# Restart Docker services
docker-compose down
docker-compose up -d

# Verify authentication
docker exec -it dsol-redis redis-cli -a your_secure_password_here ping
```

### AC7: Manual Verification Tests Pass

**Given** Redis is running with password authentication
**When** I run manual verification tests
**Then** the following commands succeed:

```bash
# Test 1: Redis requires password
docker exec -it dsol-redis redis-cli ping
# Expected: (error) NOAUTH Authentication required.

# Test 2: Redis accepts correct password
docker exec -it dsol-redis redis-cli -a $REDIS_PASSWORD ping
# Expected: PONG

# Test 3: Health endpoint reports healthy
curl http://localhost:8000/health
# Expected: {"status": "healthy", "dependencies": {"redis": {"status": "healthy", ...}}}

# Test 4: Celery worker connects
# Check logs for: "Celery worker started" with masked Redis URL
```

---

## Technical Implementation

### Files to Modify

**1. `.env.example` (ADD after line 14)**

```bash
# Redis Configuration
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=your_secure_password_here
REDIS_DB=0
```

**2. `src/core/config.py` (MODIFY lines 12-13, add property)**

```python
class Settings(BaseSettings):
    """Application settings loaded from environment variables."""

    # Database
    database_url: str = "postgresql+asyncpg://dsol:dsol_dev_password@localhost:5432/dsol"

    # Redis
    redis_host: str = "localhost"
    redis_port: int = 6379
    redis_password: str = ""  # Empty = no auth
    redis_db: int = 0

    # ... rest of settings ...

    @property
    def redis_url(self) -> str:
        """Build Redis URL with optional authentication and database selection.

        Returns:
            Redis connection URL in format:
            - With auth: redis://:password@host:port/db
            - Without auth: redis://host:port/db
        """
        if self.redis_password:
            return f"redis://:{self.redis_password}@{self.redis_host}:{self.redis_port}/{self.redis_db}"
        else:
            return f"redis://{self.redis_host}:{self.redis_port}/{self.redis_db}"
```

**3. `docker-compose.yml` (MODIFY redis service, lines 21-33)**

```yaml
  redis:
    image: redis:7
    container_name: dsol-redis
    command: sh -c 'redis-server $${REDIS_PASSWORD:+--requirepass "$$REDIS_PASSWORD"}'
    environment:
      - REDIS_PASSWORD=${REDIS_PASSWORD:-}
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "sh", "-c", "redis-cli $${REDIS_PASSWORD:+-a \"$$REDIS_PASSWORD\"} ping | grep PONG"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 5s
```

**Note:** Uses `${VAR:+value}` parameter expansion to conditionally add `--requirepass` only when password is set.

**4. `README.md` (ADD Redis Configuration section)**

Add to "Environment Configuration" section:

```markdown
### Redis Configuration

Redis is used for Celery task queue, rate limiting, and progress tracking.

**Environment Variables:**
- `REDIS_HOST`: Redis server hostname (default: localhost)
- `REDIS_PORT`: Redis server port (default: 6379)
- `REDIS_PASSWORD`: Redis authentication password (leave empty for no auth)
- `REDIS_DB`: Redis database number 0-15 (default: 0)

**Security:**
For production deployments, **always set a strong `REDIS_PASSWORD`**. This prevents unauthorized access to your task queue and cached data.

**Local Development:**
If you're running Redis without Docker, you can leave `REDIS_PASSWORD` empty. The application will connect without authentication.

**Verification:**
```bash
# Test Redis authentication
docker exec -it dsol-redis redis-cli -a $REDIS_PASSWORD ping
# Should return: PONG
```
```

---

## Testing Requirements

### Unit Tests (Create: tests/core/test_config.py)

```python
import pytest
from src.core.config import Settings


def test_redis_url_with_password():
    """Test redis_url property builds authenticated URL."""
    settings = Settings(
        redis_host="localhost",
        redis_port=6379,
        redis_password="testpass",
        redis_db=0
    )
    assert settings.redis_url == "redis://:testpass@localhost:6379/0"


def test_redis_url_without_password():
    """Test redis_url property builds unauthenticated URL."""
    settings = Settings(
        redis_host="localhost",
        redis_port=6379,
        redis_password="",
        redis_db=0
    )
    assert settings.redis_url == "redis://localhost:6379/0"


def test_redis_url_custom_database():
    """Test redis_url property with custom database."""
    settings = Settings(
        redis_host="localhost",
        redis_port=6379,
        redis_password="mypass",
        redis_db=2
    )
    assert settings.redis_url == "redis://:mypass@localhost:6379/2"


def test_redis_url_custom_host_port():
    """Test redis_url property with custom host and port."""
    settings = Settings(
        redis_host="redis.example.com",
        redis_port=6380,
        redis_password="secure123",
        redis_db=0
    )
    assert settings.redis_url == "redis://:secure123@redis.example.com:6380/0"
```

### Integration Tests (Update: tests/services/test_health.py)

```python
import pytest
from src.services.health import check_redis
from src.api.dependencies import get_redis_client


@pytest.mark.asyncio
async def test_redis_health_check_with_auth():
    """Test Redis health check with password authentication."""
    # Use real Redis client from dependency
    redis_client = await anext(get_redis_client())

    try:
        result = await check_redis(redis_client)

        assert result.status == "healthy"
        assert result.latency_ms is not None
        assert result.latency_ms < 300  # Under timeout threshold
        assert result.error is None
    finally:
        await redis_client.aclose()
```

### Manual Tests

Run these commands after implementation:

```bash
# 1. Update .env with password
echo "REDIS_PASSWORD=test_secure_pass_123" >> .env

# 2. Restart Docker services
docker-compose down
docker-compose up -d

# 3. Verify Redis requires auth
docker exec -it dsol-redis redis-cli ping
# Expected: (error) NOAUTH Authentication required.

# 4. Verify Redis accepts password
docker exec -it dsol-redis redis-cli -a test_secure_pass_123 ping
# Expected: PONG

# 5. Start application (pick up new config)
uv run uvicorn src.api.main:app --reload

# 6. Check health endpoint
curl http://localhost:8000/health | jq
# Expected: {"status": "healthy", "dependencies": {"redis": {"status": "healthy", ...}}}

# 7. Start Celery worker
uv run celery -A src.workers.celery_app worker --loglevel=info
# Expected: Worker starts, logs show masked Redis URL

# 8. Run unit tests
uv run pytest tests/core/test_config.py -v
# Expected: All tests pass
```

---

## Implementation Steps (Sequential)

### Step 1: Update .env.example
1. Open `.env.example`
2. Add Redis authentication variables after line 14
3. Save file

### Step 2: Modify Settings class
1. Open `src/core/config.py`
2. Change `redis_url` from field to property
3. Add `redis_host`, `redis_port`, `redis_password`, `redis_db` fields
4. Implement property with conditional auth logic
5. Add docstring
6. Save file

### Step 3: Update Docker Compose
1. Open `docker-compose.yml`
2. Modify `redis` service:
   - Add `command` with `--requirepass`
   - Add `environment` with `REDIS_PASSWORD`
   - Update `healthcheck` with auth flag
3. Save file

### Step 4: Update Documentation
1. Open `README.md`
2. Find or create "Environment Configuration" section
3. Add Redis configuration subsection
4. Document all Redis environment variables
5. Add verification commands
6. Save file

### Step 5: Create Unit Tests
1. Create `tests/core/test_config.py`
2. Implement 4 unit test functions
3. Run tests: `uv run pytest tests/core/test_config.py -v`
4. Verify all pass

### Step 6: Update Integration Tests
1. Open `tests/services/test_health.py`
2. Add test for Redis health check with auth
3. Run tests: `uv run pytest tests/services/test_health.py -v`
4. Verify all pass

### Step 7: Manual Verification
1. Update local `.env` with test password
2. Restart Docker Compose
3. Run all manual verification commands
4. Verify all succeed

### Step 8: Documentation Review
1. Review README for clarity
2. Ensure all env vars documented
3. Verify code examples are correct

---

## Definition of Done

- [ ] `.env.example` updated with Redis auth variables
- [ ] `Settings` class modified with redis_url property
- [ ] `docker-compose.yml` updated with Redis authentication
- [ ] `README.md` updated with Redis configuration docs
- [ ] Unit tests created and passing (4 tests)
- [ ] Integration tests updated and passing
- [ ] Manual verification tests all succeed
- [ ] Code follows existing patterns (type hints, docstrings)
- [ ] No authentication errors in application logs
- [ ] Health check reports Redis as healthy
- [ ] Celery workers connect and process tasks
- [ ] Rate limiting still functional
- [ ] Git commit created with descriptive message
- [ ] Tech-spec reviewed and accurate

---

## Related Documentation

**Tech-Spec:** `docs/tech-spec-redis-auth.md` - Complete technical specification
**Epic:** `docs/sprint-artifacts/epic-redis-auth.md` - Epic overview
**Architecture:** `docs/architecture.md` - System architecture reference
**PRD:** `docs/prd.md` - NFR11, NFR12 security requirements

---

## Notes

**Backward Compatibility:**
Empty `REDIS_PASSWORD` means no authentication. This allows developers to run Redis locally without Docker if needed.

**Production Deployment:**
Always set strong password in production. Use secret management service (AWS Secrets Manager, K8s secrets, etc.).

**Redis URL Format:**
Standard format: `redis://[username][:password]@host:port/db`
DSOL uses: `redis://:password@host:port/db` (no username, password only)

**Security:**
- Password never logged
- Masked in Celery worker logs
- Stored in .env (not committed)
- .env.example has placeholder only

---

_Story ready for implementation. Estimated time: 1-2 hours. Low risk, high security value._
