Major changes: - Add AuditProvider protocol for cross-module audit logging - Move customer order operations to orders module (dependency inversion) - Add customer order metrics via MetricsProvider pattern - Fix missing db parameter in get_admin_context() calls - Move ProductMedia relationship to catalog module (proper ownership) - Add marketplace breakdown stats to marketplace_widgets New files: - contracts/audit.py - AuditProviderProtocol - core/services/audit_aggregator.py - Aggregates audit providers - monitoring/services/audit_provider.py - Monitoring audit implementation - orders/services/customer_order_service.py - Customer order operations - orders/routes/api/vendor_customer_orders.py - Customer order endpoints - catalog/services/product_media_service.py - Product media service - Architecture documentation for patterns Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
266 lines
9.7 KiB
Markdown
266 lines
9.7 KiB
Markdown
# Audit Provider Pattern
|
|
|
|
The audit provider pattern enables modules to provide audit logging backends without creating cross-module dependencies. This allows the monitoring module to be truly optional while still providing audit logging when enabled.
|
|
|
|
## Overview
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────┐
|
|
│ Admin Action Request │
|
|
│ (Settings update, User management, etc.) │
|
|
└─────────────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────────┐
|
|
│ AuditAggregatorService │
|
|
│ (app/modules/core/services/audit_aggregator.py) │
|
|
│ │
|
|
│ • Discovers AuditProviders from all registered modules │
|
|
│ • Calls log_action() on all available providers │
|
|
│ • Handles graceful degradation if no providers available │
|
|
└─────────────────────────────────────────────────────────────────────┘
|
|
│
|
|
┌───────────────┼───────────────┐
|
|
▼ ▼ ▼
|
|
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
|
│ DatabaseAudit │ │ (Future) │ │ (Future) │
|
|
│ Provider │ │ FileProvider │ │ CloudWatch │
|
|
│ (monitoring) │ │ │ │ │
|
|
└───────────────┘ └───────────────┘ └───────────────┘
|
|
│
|
|
▼
|
|
┌───────────────┐
|
|
│AdminAuditLog │
|
|
│ (table) │
|
|
└───────────────┘
|
|
```
|
|
|
|
## Problem Solved
|
|
|
|
Before this pattern, core modules had **hard imports** from the monitoring module:
|
|
|
|
```python
|
|
# BAD: Core module with hard dependency on optional/internal module
|
|
from app.modules.monitoring.services.admin_audit_service import admin_audit_service
|
|
|
|
def create_setting(...):
|
|
result = settings_service.create_setting(...)
|
|
admin_audit_service.log_action(...) # Crashes if monitoring disabled!
|
|
```
|
|
|
|
This violated the architecture rule: **Core modules cannot depend on optional modules.**
|
|
|
|
## Solution: Protocol-Based Audit Logging
|
|
|
|
The monitoring module implements `AuditProviderProtocol` and registers it in its `definition.py`. The `AuditAggregatorService` in core discovers and uses audit providers from all modules.
|
|
|
|
## Key Components
|
|
|
|
### 1. AuditEvent Dataclass
|
|
|
|
Standard structure for audit events:
|
|
|
|
```python
|
|
# app/modules/contracts/audit.py
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
@dataclass
|
|
class AuditEvent:
|
|
admin_user_id: int # ID of admin performing action
|
|
action: str # Action name (e.g., "create_setting")
|
|
target_type: str # Target type (e.g., "setting", "user")
|
|
target_id: str # Target identifier
|
|
details: dict[str, Any] | None = None # Additional context
|
|
ip_address: str | None = None # Admin's IP address
|
|
user_agent: str | None = None # Browser user agent
|
|
request_id: str | None = None # Request correlation ID
|
|
```
|
|
|
|
### 2. AuditProviderProtocol
|
|
|
|
Protocol that modules implement:
|
|
|
|
```python
|
|
# app/modules/contracts/audit.py
|
|
from typing import Protocol, runtime_checkable
|
|
|
|
@runtime_checkable
|
|
class AuditProviderProtocol(Protocol):
|
|
@property
|
|
def audit_backend(self) -> str:
|
|
"""Backend name (e.g., 'database', 'file', 'cloudwatch')."""
|
|
...
|
|
|
|
def log_action(self, db: Session, event: AuditEvent) -> bool:
|
|
"""Log an audit event. Returns True on success."""
|
|
...
|
|
```
|
|
|
|
### 3. AuditAggregatorService
|
|
|
|
The aggregator in core that discovers and uses providers:
|
|
|
|
```python
|
|
# app/modules/core/services/audit_aggregator.py
|
|
|
|
class AuditAggregatorService:
|
|
def _get_enabled_providers(self, db: Session):
|
|
"""Discover audit providers from registered modules."""
|
|
from app.modules.registry import MODULES
|
|
|
|
for module in MODULES.values():
|
|
if module.has_audit_provider():
|
|
provider = module.get_audit_provider_instance()
|
|
if provider:
|
|
yield (module, provider)
|
|
|
|
def log_action(self, db: Session, event: AuditEvent) -> bool:
|
|
"""Log to all available providers."""
|
|
providers = list(self._get_enabled_providers(db))
|
|
|
|
if not providers:
|
|
# No providers - acceptable, audit is optional
|
|
return False
|
|
|
|
any_success = False
|
|
for module, provider in providers:
|
|
try:
|
|
if provider.log_action(db, event):
|
|
any_success = True
|
|
except Exception as e:
|
|
logger.warning(f"Audit provider {module.code} failed: {e}")
|
|
|
|
return any_success
|
|
|
|
def log(self, db, admin_user_id, action, target_type, target_id, **kwargs):
|
|
"""Convenience method with individual parameters."""
|
|
event = AuditEvent(
|
|
admin_user_id=admin_user_id,
|
|
action=action,
|
|
target_type=target_type,
|
|
target_id=str(target_id),
|
|
**kwargs
|
|
)
|
|
return self.log_action(db, event)
|
|
```
|
|
|
|
## Implementing a Provider
|
|
|
|
### Step 1: Create the Provider
|
|
|
|
```python
|
|
# app/modules/monitoring/services/audit_provider.py
|
|
|
|
from app.modules.contracts.audit import AuditEvent, AuditProviderProtocol
|
|
from app.modules.tenancy.models import AdminAuditLog
|
|
|
|
class DatabaseAuditProvider:
|
|
@property
|
|
def audit_backend(self) -> str:
|
|
return "database"
|
|
|
|
def log_action(self, db: Session, event: AuditEvent) -> bool:
|
|
try:
|
|
audit_log = AdminAuditLog(
|
|
admin_user_id=event.admin_user_id,
|
|
action=event.action,
|
|
target_type=event.target_type,
|
|
target_id=event.target_id,
|
|
details=event.details or {},
|
|
ip_address=event.ip_address,
|
|
user_agent=event.user_agent,
|
|
request_id=event.request_id,
|
|
)
|
|
db.add(audit_log)
|
|
db.flush()
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Failed to log audit: {e}")
|
|
return False
|
|
|
|
audit_provider = DatabaseAuditProvider()
|
|
```
|
|
|
|
### Step 2: Register in Module Definition
|
|
|
|
```python
|
|
# app/modules/monitoring/definition.py
|
|
|
|
def _get_audit_provider():
|
|
"""Lazy import to avoid circular imports."""
|
|
from app.modules.monitoring.services.audit_provider import audit_provider
|
|
return audit_provider
|
|
|
|
monitoring_module = ModuleDefinition(
|
|
code="monitoring",
|
|
name="Platform Monitoring",
|
|
audit_provider=_get_audit_provider, # Register the provider
|
|
...
|
|
)
|
|
```
|
|
|
|
### Step 3: Use via Aggregator
|
|
|
|
```python
|
|
# app/modules/core/routes/api/admin_settings.py
|
|
|
|
from app.modules.core.services.audit_aggregator import audit_aggregator
|
|
|
|
@router.post("/settings")
|
|
def create_setting(setting_data: SettingCreate, ...):
|
|
result = settings_service.create_setting(db, setting_data)
|
|
|
|
# Log via aggregator - works even if monitoring is disabled
|
|
audit_aggregator.log(
|
|
db=db,
|
|
admin_user_id=current_admin.id,
|
|
action="create_setting",
|
|
target_type="setting",
|
|
target_id=setting_data.key,
|
|
details={"category": setting_data.category},
|
|
)
|
|
|
|
db.commit()
|
|
return result
|
|
```
|
|
|
|
## Graceful Degradation
|
|
|
|
When no audit providers are registered (e.g., monitoring module disabled):
|
|
|
|
- `audit_aggregator.log()` returns `False`
|
|
- No exceptions are raised
|
|
- The operation continues normally
|
|
- A debug log message notes the absence of providers
|
|
|
|
This ensures core functionality works regardless of whether auditing is available.
|
|
|
|
## Multiple Backends
|
|
|
|
The pattern supports multiple audit backends simultaneously:
|
|
|
|
```python
|
|
# All registered providers receive the event
|
|
audit_aggregator.log(...)
|
|
|
|
# Provider 1: Database (AdminAuditLog table)
|
|
# Provider 2: CloudWatch Logs (if aws-monitoring module enabled)
|
|
# Provider 3: File-based audit log (if file-audit module enabled)
|
|
```
|
|
|
|
## Benefits
|
|
|
|
1. **Decoupling**: Core modules don't depend on monitoring
|
|
2. **Optional Auditing**: Monitoring can be disabled without breaking the app
|
|
3. **Extensibility**: Easy to add new audit backends (file, cloud, SIEM)
|
|
4. **Testability**: Can mock audit providers in tests
|
|
5. **Fault Tolerance**: One provider failure doesn't affect others
|
|
|
|
## Related Documentation
|
|
|
|
- [Cross-Module Import Rules](cross-module-import-rules.md) - Import restrictions
|
|
- [Metrics Provider Pattern](metrics-provider-pattern.md) - Similar pattern for statistics
|
|
- [Widget Provider Pattern](widget-provider-pattern.md) - Similar pattern for dashboard widgets
|
|
- [Module System Architecture](module-system.md) - Module structure and auto-discovery
|