pywatt_sdk 0.5.3

Standardized SDK for building PyWatt modules in Rust
Documentation
# Secret Provider

The secret provider module implements server-side secret management with pluggable backends, metrics collection, and event-driven rotation support. It provides a trait-based architecture for different secret storage implementations.

## Architecture

The module is built around the `SecretProvider` trait, which defines a common interface for secret storage backends. Multiple implementations can be chained together for fallback scenarios, and all providers support async operations with optional watching for changes.

## Core Components

### [`mod.rs`]./mod.rs
Main module defining the `SecretProvider` trait and initialization logic:

#### SecretProvider Trait
```rust
#[async_trait]
pub trait SecretProvider: Send + Sync + std::fmt::Debug {
    async fn get(&self, key: &str) -> Result<SecretString, SecretError>;
    async fn set(&self, key: &str, value: SecretString) -> Result<(), SecretError>;
    async fn keys(&self) -> Result<Vec<String>, SecretError>;
    async fn watch(&self, tx: broadcast::Sender<SecretEvent>) -> Result<(), SecretError>;
}
```

#### Initialization
- `init()` - Creates provider chain based on `SECRET_PROVIDER_CHAIN` environment variable
- Supports `env` and `file` providers with configuration via environment variables
- Automatic fallback chaining when multiple providers are specified

#### Utility Functions
- `redact_line()` - Redacts secret values from log lines using provider keys
- Re-exports all provider implementations and supporting types

### [`errors.rs`]./errors.rs
Defines `SecretError` enum for comprehensive error handling:

```rust
pub enum SecretError {
    NotFound(String),                    // Secret key not found
    UnsupportedOperation(String),        // Operation not supported by provider
    Backend(anyhow::Error),             // Underlying backend error
    Configuration(String),               // Provider configuration error
}
```

### [`events.rs`]./events.rs
Defines `SecretEvent` enum for change notifications:

```rust
pub enum SecretEvent {
    Rotated(Vec<String>),    // Secrets have been updated/rotated
    Added(String),           // New secret key added
    Removed(String),         // Existing secret key removed
}
```

### [`metrics.rs`]./metrics.rs *(feature-gated)*
Comprehensive metrics collection for secret operations:

#### Metrics Functions
- `record_operation()` - Track operation counts by provider/operation/outcome
- `record_rotation()` - Track rotation events and counts
- `record_cache_access()` - Track cache hit/miss ratios
- `record_operation_duration()` - Track operation latency
- `record_cache_size()` - Track current cache size

#### OpTimer Utility
```rust
let timer = OpTimer::new("env", "get");
// ... perform operation ...
timer.finish_with_outcome("success");
```

Automatic timing and outcome recording with RAII cleanup.

### [`tracing.rs`]./tracing.rs
Structured tracing support with security considerations:

#### Key Hashing
- `hash_key()` - Hash secret key names to avoid exposing sensitive key names in logs
- `add_hashed_key()` - Add hashed key to current tracing span

#### Span Management
- `record_success()` - Mark operation as successful in current span
- `record_error()` - Record error details with categorization
- `instrument_secret_op!()` - Macro for instrumenting secret operations

#### Security Features
- Prevents secret key names from appearing in logs
- Categorizes errors without exposing sensitive details
- Structured fields for metrics correlation

## Provider Implementations

### [`providers/env_provider.rs`]./providers/env_provider.rs
Environment variable-based secret provider:

#### Features
- **Read-only**: `set()` operations return `UnsupportedOperation`
- **No watching**: `watch()` is a no-op (environment variables don't change dynamically)
- **Empty keys**: Returns empty list for `keys()` for security/performance
- **Error handling**: Distinguishes between missing variables and access errors

#### Usage
```rust
let provider = EnvProvider::new();
let secret = provider.get("DATABASE_URL").await?;
```

#### Instrumentation
- Full tracing support with operation timing
- Metrics collection when feature enabled
- Hashed key logging for security

### [`providers/file_provider.rs`]./providers/file_provider.rs
File-based secret provider with watching support:

#### Features
- **TOML format**: Currently supports TOML file format
- **File watching**: Uses `notify` crate for real-time change detection
- **Read-only**: `set()` operations are not supported
- **Atomic reloading**: Complete file reload on changes
- **Change detection**: Tracks which keys changed during reload

#### File Format
```toml
DATABASE_URL = "postgresql://user:pass@localhost/db"
API_KEY = "secret-api-key-value"
DEBUG_MODE = "true"
```

#### Usage
```rust
let provider = FileProvider::new("/path/to/secrets.toml", FileFormat::Toml).await?;
let secret = provider.get("API_KEY").await?;

// Enable watching
let (tx, rx) = broadcast::channel(10);
provider.watch(tx).await?;
```

#### Watching Implementation
- Background task monitors file system events
- Debounced reloading to handle multiple rapid changes
- Broadcasts `SecretEvent::Rotated` with changed key list
- Handles file creation, modification, and deletion events

### [`providers/memory_provider.rs`]./providers/memory_provider.rs
In-memory secret provider for testing and development:

#### Features
- **Full CRUD**: Supports all operations including `set()`
- **Concurrent access**: Thread-safe using `DashMap`
- **Event notifications**: Broadcasts `Added` and `Rotated` events
- **Testing utilities**: Easy setup with initial secret maps

#### Usage
```rust
let provider = MemoryProvider::new();
provider.set("TEST_KEY", SecretString::new("test_value".into())).await?;

// With initial secrets
let initial = DashMap::new();
initial.insert("KEY1".to_string(), SecretString::new("value1".into()));
let provider = MemoryProvider::with_secrets(initial);
```

#### Event Handling
- `set()` operations trigger appropriate events
- Multiple subscribers supported via `broadcast::Sender`
- Automatic event generation for additions vs. rotations

### [`providers/chained_provider.rs`]./providers/chained_provider.rs
Chains multiple providers for fallback scenarios:

#### Features
- **Fallback logic**: Tries providers in order until one succeeds
- **Combined keys**: Returns union of all provider keys
- **Set operations**: Attempts `set()` on first supporting provider
- **Event aggregation**: Forwards events from all chained providers

#### Usage
```rust
let providers = vec![
    Arc::new(EnvProvider::new()),
    Arc::new(FileProvider::new("secrets.toml", FileFormat::Toml).await?),
    Arc::new(MemoryProvider::new()),
];
let chained = ChainedProvider::new(providers);
```

#### Behavior
- `get()`: Returns first successful result, continues on `NotFound`
- `set()`: Tries each provider until one supports the operation
- `keys()`: Returns deduplicated union of all provider keys
- `watch()`: Attaches same event sender to all providers

## Configuration

### Environment Variables
- `SECRET_PROVIDER_CHAIN` - Comma-separated list of providers (`env,file`)
- `SECRET_FILE_PATH` - Path to secrets file (required for `file` provider)
- `SECRET_FILE_FORMAT` - File format (`toml`, default: `toml`)

### Example Configuration
```bash
export SECRET_PROVIDER_CHAIN="env,file"
export SECRET_FILE_PATH="/etc/secrets/app.toml"
export SECRET_FILE_FORMAT="toml"
```

## Usage Patterns

### Basic Provider Setup
```rust
use pywatt_sdk::security::secret_provider::{init, SecretProvider};

// Initialize from environment
let provider = init().await?;
let secret = provider.get("DATABASE_URL").await?;
```

### Custom Provider Chain
```rust
use pywatt_sdk::security::secret_provider::providers::*;

let providers: Vec<Arc<dyn SecretProvider>> = vec![
    Arc::new(EnvProvider::new()),
    Arc::new(FileProvider::new("secrets.toml", FileFormat::Toml).await?),
    Arc::new(MemoryProvider::new()),
];
let provider = ChainedProvider::new(providers);
```

### Event Watching
```rust
let (tx, mut rx) = broadcast::channel(10);
provider.watch(tx).await?;

tokio::spawn(async move {
    while let Ok(event) = rx.recv().await {
        match event {
            SecretEvent::Rotated(keys) => {
                println!("Secrets rotated: {:?}", keys);
                // Refresh application state
            }
            SecretEvent::Added(key) => {
                println!("Secret added: {}", key);
            }
            SecretEvent::Removed(key) => {
                println!("Secret removed: {}", key);
            }
        }
    }
});
```

### Metrics Integration
```rust
#[cfg(feature = "metrics")]
{
    use pywatt_sdk::security::secret_provider::metrics::OpTimer;
    
    let timer = OpTimer::new("file", "get");
    let result = provider.get("API_KEY").await;
    match result {
        Ok(_) => timer.finish_with_outcome("success"),
        Err(_) => timer.finish_with_outcome("error"),
    }
}
```

## Security Considerations

### Key Name Protection
- Secret key names are hashed before appearing in logs
- Tracing spans use hashed keys to prevent exposure
- Error messages avoid including sensitive key names

### Memory Safety
- All secret values use `secrecy::SecretString`
- Automatic zeroization on drop
- No accidental exposure in debug output

### File Security
- File provider validates file permissions
- Atomic reloading prevents partial reads
- Secure handling of file system events

### Access Control
- Providers can implement custom access controls
- Error categorization helps with security auditing
- Comprehensive logging for security monitoring

## Testing

### Unit Tests
Each provider includes comprehensive unit tests:
- Basic CRUD operations
- Error conditions
- Event generation
- Concurrent access patterns

### Integration Tests
- Provider chaining scenarios
- File watching functionality
- Metrics collection verification
- Event propagation testing

### Test Utilities
```rust
// Memory provider for testing
let provider = MemoryProvider::new();
provider.set("TEST_KEY", SecretString::new("test_value".into())).await?;

// Verify events
let (tx, mut rx) = broadcast::channel(1);
provider.watch(tx).await?;
provider.set("KEY", SecretString::new("value".into())).await?;
assert_eq!(rx.recv().await?, SecretEvent::Added("KEY".to_string()));
```

## Performance Characteristics

### Memory Provider
- O(1) get/set operations using DashMap
- Lock-free concurrent reads
- Minimal memory overhead

### File Provider
- O(n) reload time proportional to file size
- Debounced reloading reduces I/O overhead
- Efficient change detection

### Environment Provider
- O(1) access via system calls
- No caching (relies on OS caching)
- Minimal memory footprint

### Chained Provider
- O(n) worst-case where n is number of providers
- Short-circuits on first success
- Minimal overhead for successful operations