Improve teh securty of teh DB
This commit is contained in:
@@ -46,26 +46,17 @@ class BaseAlertService:
|
||||
"""Initialize all detection mechanisms"""
|
||||
try:
|
||||
# Connect to Redis for leader election and deduplication
|
||||
import os
|
||||
redis_password = os.getenv('REDIS_PASSWORD', '')
|
||||
redis_host = os.getenv('REDIS_HOST', 'redis-service')
|
||||
redis_port = int(os.getenv('REDIS_PORT', '6379'))
|
||||
# Use the shared Redis URL which includes TLS configuration
|
||||
from redis.asyncio import from_url
|
||||
redis_url = self.config.REDIS_URL
|
||||
|
||||
# Create Redis client with explicit password parameter
|
||||
if redis_password:
|
||||
self.redis = await Redis(
|
||||
host=redis_host,
|
||||
port=redis_port,
|
||||
password=redis_password,
|
||||
decode_responses=True
|
||||
)
|
||||
else:
|
||||
self.redis = await Redis(
|
||||
host=redis_host,
|
||||
port=redis_port,
|
||||
decode_responses=True
|
||||
)
|
||||
logger.info("Connected to Redis", service=self.config.SERVICE_NAME)
|
||||
# Create Redis client from URL (supports TLS via rediss:// protocol)
|
||||
self.redis = await from_url(
|
||||
redis_url,
|
||||
decode_responses=True,
|
||||
max_connections=20
|
||||
)
|
||||
logger.info("Connected to Redis", service=self.config.SERVICE_NAME, redis_url=redis_url.split("@")[-1])
|
||||
|
||||
# Connect to RabbitMQ
|
||||
await self.rabbitmq_client.connect()
|
||||
|
||||
@@ -58,26 +58,40 @@ class BaseServiceSettings(BaseSettings):
|
||||
|
||||
@property
|
||||
def REDIS_URL(self) -> str:
|
||||
"""Build Redis URL from secure components"""
|
||||
"""Build Redis URL from secure components with TLS support"""
|
||||
# Try complete URL first (for backward compatibility)
|
||||
complete_url = os.getenv("REDIS_URL")
|
||||
if complete_url:
|
||||
# Upgrade to TLS if not already
|
||||
if complete_url.startswith("redis://") and "tls" not in complete_url.lower():
|
||||
complete_url = complete_url.replace("redis://", "rediss://", 1)
|
||||
return complete_url
|
||||
|
||||
# Build from components (secure approach)
|
||||
# Build from components (secure approach with TLS)
|
||||
password = os.getenv("REDIS_PASSWORD", "")
|
||||
host = os.getenv("REDIS_HOST", "redis-service")
|
||||
port = os.getenv("REDIS_PORT", "6379")
|
||||
use_tls = os.getenv("REDIS_TLS_ENABLED", "true").lower() == "true"
|
||||
|
||||
# Use rediss:// for TLS, redis:// for non-TLS
|
||||
protocol = "rediss" if use_tls else "redis"
|
||||
|
||||
# DEBUG: print what we're using
|
||||
import sys
|
||||
print(f"[DEBUG REDIS_URL] password={repr(password)}, host={host}, port={port}", file=sys.stderr)
|
||||
print(f"[DEBUG REDIS_URL] password={repr(password)}, host={host}, port={port}, tls={use_tls}", file=sys.stderr)
|
||||
|
||||
if password:
|
||||
url = f"redis://:{password}@{host}:{port}"
|
||||
print(f"[DEBUG REDIS_URL] Returning URL with auth: {url}", file=sys.stderr)
|
||||
url = f"{protocol}://:{password}@{host}:{port}"
|
||||
if use_tls:
|
||||
# Use ssl_cert_reqs=none for self-signed certs in internal cluster
|
||||
# Still encrypted, just skips cert validation
|
||||
url += "?ssl_cert_reqs=none"
|
||||
print(f"[DEBUG REDIS_URL] Returning URL with auth and TLS: {url}", file=sys.stderr)
|
||||
return url
|
||||
url = f"redis://{host}:{port}"
|
||||
url = f"{protocol}://{host}:{port}"
|
||||
if use_tls:
|
||||
# Use ssl_cert_reqs=none for self-signed certs in internal cluster
|
||||
url += "?ssl_cert_reqs=none"
|
||||
print(f"[DEBUG REDIS_URL] Returning URL without auth: {url}", file=sys.stderr)
|
||||
return url
|
||||
|
||||
|
||||
@@ -32,8 +32,8 @@ class DatabaseManager:
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
database_url: str,
|
||||
self,
|
||||
database_url: str,
|
||||
service_name: str = "unknown",
|
||||
pool_size: int = 20,
|
||||
max_overflow: int = 30,
|
||||
@@ -43,11 +43,18 @@ class DatabaseManager:
|
||||
connect_timeout: int = 30,
|
||||
**engine_kwargs
|
||||
):
|
||||
# Add SSL parameters to database URL if PostgreSQL
|
||||
if "postgresql" in database_url.lower() and "ssl" not in database_url.lower():
|
||||
separator = "&" if "?" in database_url else "?"
|
||||
# asyncpg uses 'ssl=require' or 'ssl=verify-full', not 'sslmode'
|
||||
database_url = f"{database_url}{separator}ssl=require"
|
||||
logger.info(f"SSL enforcement added to database URL for {service_name}")
|
||||
|
||||
self.database_url = database_url
|
||||
self.service_name = service_name
|
||||
self.pool_size = pool_size
|
||||
self.max_overflow = max_overflow
|
||||
|
||||
|
||||
# Configure pool for async engines
|
||||
# Note: SQLAlchemy 2.0 async engines automatically use AsyncAdaptedQueuePool
|
||||
# We should NOT specify poolclass for async engines unless using StaticPool for SQLite
|
||||
@@ -66,7 +73,7 @@ class DatabaseManager:
|
||||
engine_config["poolclass"] = StaticPool
|
||||
engine_config["pool_size"] = 1
|
||||
engine_config["max_overflow"] = 0
|
||||
|
||||
|
||||
self.async_engine = create_async_engine(database_url, **engine_config)
|
||||
|
||||
# Create session factory
|
||||
@@ -325,7 +332,14 @@ AsyncSessionLocal = None
|
||||
def init_legacy_compatibility(database_url: str):
|
||||
"""Initialize legacy global variables for backward compatibility"""
|
||||
global engine, AsyncSessionLocal
|
||||
|
||||
|
||||
# Add SSL parameters to database URL if PostgreSQL
|
||||
if "postgresql" in database_url.lower() and "ssl" not in database_url.lower():
|
||||
separator = "&" if "?" in database_url else "?"
|
||||
# asyncpg uses 'ssl=require' or 'ssl=verify-full', not 'sslmode'
|
||||
database_url = f"{database_url}{separator}ssl=require"
|
||||
logger.info("SSL enforcement added to legacy database URL")
|
||||
|
||||
engine = create_async_engine(
|
||||
database_url,
|
||||
echo=False,
|
||||
|
||||
@@ -9,6 +9,9 @@ from datetime import datetime, timezone
|
||||
from typing import List, Optional, Dict, Any
|
||||
import uuid
|
||||
from decimal import Decimal
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class AlertSeverity:
|
||||
@@ -35,11 +38,12 @@ async def create_demo_alert(
|
||||
title: str,
|
||||
message: str,
|
||||
service: str,
|
||||
rabbitmq_client,
|
||||
metadata: Dict[str, Any] = None,
|
||||
created_at: Optional[datetime] = None
|
||||
):
|
||||
"""
|
||||
Create and persist a demo alert
|
||||
Create and persist a demo alert, then publish to RabbitMQ
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
@@ -49,18 +53,24 @@ async def create_demo_alert(
|
||||
title: Alert title (in Spanish)
|
||||
message: Alert message (in Spanish)
|
||||
service: Service name that generated the alert
|
||||
rabbitmq_client: RabbitMQ client for publishing alerts
|
||||
metadata: Additional alert-specific data
|
||||
created_at: When the alert was created (defaults to now)
|
||||
|
||||
Returns:
|
||||
Created Alert instance (dict for cross-service compatibility)
|
||||
"""
|
||||
from shared.config.rabbitmq_config import get_routing_key
|
||||
|
||||
alert_id = uuid.uuid4()
|
||||
alert_created_at = created_at or datetime.now(timezone.utc)
|
||||
|
||||
# Import here to avoid circular dependencies
|
||||
try:
|
||||
from app.models.alerts import Alert
|
||||
|
||||
alert = Alert(
|
||||
id=uuid.uuid4(),
|
||||
id=alert_id,
|
||||
tenant_id=tenant_id,
|
||||
item_type="alert",
|
||||
alert_type=alert_type,
|
||||
@@ -70,33 +80,84 @@ async def create_demo_alert(
|
||||
title=title,
|
||||
message=message,
|
||||
alert_metadata=metadata or {},
|
||||
created_at=created_at or datetime.now(timezone.utc)
|
||||
created_at=alert_created_at
|
||||
)
|
||||
db.add(alert)
|
||||
return alert
|
||||
await db.flush()
|
||||
except ImportError:
|
||||
# If Alert model not available, return dict representation
|
||||
# This allows the function to work across services
|
||||
alert_dict = {
|
||||
"id": uuid.uuid4(),
|
||||
"tenant_id": tenant_id,
|
||||
"item_type": "alert",
|
||||
"alert_type": alert_type,
|
||||
"severity": severity,
|
||||
"status": AlertStatus.ACTIVE,
|
||||
"service": service,
|
||||
"title": title,
|
||||
"message": message,
|
||||
"alert_metadata": metadata or {},
|
||||
"created_at": created_at or datetime.now(timezone.utc)
|
||||
}
|
||||
return alert_dict
|
||||
# If Alert model not available, skip DB insert
|
||||
logger.warning("Alert model not available, skipping DB insert", service=service)
|
||||
|
||||
# Publish alert to RabbitMQ for processing by Alert Processor
|
||||
if rabbitmq_client:
|
||||
try:
|
||||
alert_message = {
|
||||
'id': str(alert_id),
|
||||
'tenant_id': str(tenant_id),
|
||||
'item_type': 'alert',
|
||||
'type': alert_type,
|
||||
'severity': severity,
|
||||
'service': service,
|
||||
'title': title,
|
||||
'message': message,
|
||||
'metadata': metadata or {},
|
||||
'timestamp': alert_created_at.isoformat()
|
||||
}
|
||||
|
||||
routing_key = get_routing_key('alert', severity, service)
|
||||
|
||||
published = await rabbitmq_client.publish_event(
|
||||
exchange_name='alerts.exchange',
|
||||
routing_key=routing_key,
|
||||
event_data=alert_message
|
||||
)
|
||||
|
||||
if published:
|
||||
logger.info(
|
||||
"Demo alert published to RabbitMQ",
|
||||
alert_id=str(alert_id),
|
||||
alert_type=alert_type,
|
||||
severity=severity,
|
||||
service=service,
|
||||
routing_key=routing_key
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"Failed to publish demo alert to RabbitMQ",
|
||||
alert_id=str(alert_id),
|
||||
alert_type=alert_type
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error publishing demo alert to RabbitMQ",
|
||||
alert_id=str(alert_id),
|
||||
error=str(e),
|
||||
exc_info=True
|
||||
)
|
||||
else:
|
||||
logger.warning("No RabbitMQ client provided, alert will not be streamed", alert_id=str(alert_id))
|
||||
|
||||
# Return alert dict for compatibility
|
||||
return {
|
||||
"id": str(alert_id),
|
||||
"tenant_id": str(tenant_id),
|
||||
"item_type": "alert",
|
||||
"alert_type": alert_type,
|
||||
"severity": severity,
|
||||
"status": AlertStatus.ACTIVE,
|
||||
"service": service,
|
||||
"title": title,
|
||||
"message": message,
|
||||
"alert_metadata": metadata or {},
|
||||
"created_at": alert_created_at
|
||||
}
|
||||
|
||||
|
||||
async def generate_inventory_alerts(
|
||||
db,
|
||||
tenant_id: uuid.UUID,
|
||||
session_created_at: datetime
|
||||
session_created_at: datetime,
|
||||
rabbitmq_client=None
|
||||
) -> int:
|
||||
"""
|
||||
Generate inventory-related alerts for demo session
|
||||
@@ -111,6 +172,7 @@ async def generate_inventory_alerts(
|
||||
db: Database session
|
||||
tenant_id: Virtual tenant UUID
|
||||
session_created_at: When the demo session was created
|
||||
rabbitmq_client: RabbitMQ client for publishing alerts
|
||||
|
||||
Returns:
|
||||
Number of alerts created
|
||||
@@ -156,6 +218,7 @@ async def generate_inventory_alerts(
|
||||
f"Cantidad: {stock.current_quantity:.2f} {ingredient.unit_of_measure.value}. "
|
||||
f"Acción requerida: Retirar inmediatamente del inventario y registrar como pérdida.",
|
||||
service="inventory",
|
||||
rabbitmq_client=rabbitmq_client,
|
||||
metadata={
|
||||
"stock_id": str(stock.id),
|
||||
"ingredient_id": str(ingredient.id),
|
||||
@@ -181,6 +244,7 @@ async def generate_inventory_alerts(
|
||||
f"Cantidad: {stock.current_quantity:.2f} {ingredient.unit_of_measure.value}. "
|
||||
f"Recomendación: Planificar uso prioritario en producción inmediata.",
|
||||
service="inventory",
|
||||
rabbitmq_client=rabbitmq_client,
|
||||
metadata={
|
||||
"stock_id": str(stock.id),
|
||||
"ingredient_id": str(ingredient.id),
|
||||
@@ -207,6 +271,7 @@ async def generate_inventory_alerts(
|
||||
f"Faltante: {shortage:.2f} {ingredient.unit_of_measure.value}. "
|
||||
f"Se recomienda realizar pedido de {ingredient.reorder_quantity:.2f} {ingredient.unit_of_measure.value}.",
|
||||
service="inventory",
|
||||
rabbitmq_client=rabbitmq_client,
|
||||
metadata={
|
||||
"stock_id": str(stock.id),
|
||||
"ingredient_id": str(ingredient.id),
|
||||
@@ -233,6 +298,7 @@ async def generate_inventory_alerts(
|
||||
f"Exceso: {excess:.2f} {ingredient.unit_of_measure.value}. "
|
||||
f"Considerar reducir cantidad en próximos pedidos o buscar uso alternativo.",
|
||||
service="inventory",
|
||||
rabbitmq_client=rabbitmq_client,
|
||||
metadata={
|
||||
"stock_id": str(stock.id),
|
||||
"ingredient_id": str(ingredient.id),
|
||||
@@ -250,7 +316,8 @@ async def generate_inventory_alerts(
|
||||
async def generate_equipment_alerts(
|
||||
db,
|
||||
tenant_id: uuid.UUID,
|
||||
session_created_at: datetime
|
||||
session_created_at: datetime,
|
||||
rabbitmq_client=None
|
||||
) -> int:
|
||||
"""
|
||||
Generate equipment-related alerts for demo session
|
||||
@@ -264,6 +331,7 @@ async def generate_equipment_alerts(
|
||||
db: Database session
|
||||
tenant_id: Virtual tenant UUID
|
||||
session_created_at: When the demo session was created
|
||||
rabbitmq_client: RabbitMQ client for publishing alerts
|
||||
|
||||
Returns:
|
||||
Number of alerts created
|
||||
@@ -295,6 +363,7 @@ async def generate_equipment_alerts(
|
||||
f"Último mantenimiento: {equipment.last_maintenance_date.strftime('%d/%m/%Y') if equipment.last_maintenance_date else 'No registrado'}. "
|
||||
f"Programar mantenimiento preventivo lo antes posible.",
|
||||
service="production",
|
||||
rabbitmq_client=rabbitmq_client,
|
||||
metadata={
|
||||
"equipment_id": str(equipment.id),
|
||||
"equipment_name": equipment.name,
|
||||
@@ -316,6 +385,7 @@ async def generate_equipment_alerts(
|
||||
message=f"El equipo {equipment.name} está actualmente en mantenimiento y no disponible para producción. "
|
||||
f"Ajustar planificación de producción según capacidad reducida.",
|
||||
service="production",
|
||||
rabbitmq_client=rabbitmq_client,
|
||||
metadata={
|
||||
"equipment_id": str(equipment.id),
|
||||
"equipment_name": equipment.name,
|
||||
@@ -335,6 +405,7 @@ async def generate_equipment_alerts(
|
||||
f"Contactar con servicio técnico inmediatamente. "
|
||||
f"Revisar planificación de producción y reasignar lotes a otros equipos.",
|
||||
service="production",
|
||||
rabbitmq_client=rabbitmq_client,
|
||||
metadata={
|
||||
"equipment_id": str(equipment.id),
|
||||
"equipment_name": equipment.name,
|
||||
@@ -354,6 +425,7 @@ async def generate_equipment_alerts(
|
||||
f"Eficiencia actual: {equipment.efficiency_percentage:.1f}%. "
|
||||
f"Monitorear de cerca y considerar inspección preventiva.",
|
||||
service="production",
|
||||
rabbitmq_client=rabbitmq_client,
|
||||
metadata={
|
||||
"equipment_id": str(equipment.id),
|
||||
"equipment_name": equipment.name,
|
||||
@@ -375,6 +447,7 @@ async def generate_equipment_alerts(
|
||||
f"Eficiencia objetivo: e 85%. "
|
||||
f"Revisar causas: limpieza, calibración, desgaste de componentes.",
|
||||
service="production",
|
||||
rabbitmq_client=rabbitmq_client,
|
||||
metadata={
|
||||
"equipment_id": str(equipment.id),
|
||||
"equipment_name": equipment.name,
|
||||
@@ -390,7 +463,8 @@ async def generate_equipment_alerts(
|
||||
async def generate_order_alerts(
|
||||
db,
|
||||
tenant_id: uuid.UUID,
|
||||
session_created_at: datetime
|
||||
session_created_at: datetime,
|
||||
rabbitmq_client=None
|
||||
) -> int:
|
||||
"""
|
||||
Generate order-related alerts for demo session
|
||||
@@ -404,6 +478,7 @@ async def generate_order_alerts(
|
||||
db: Database session
|
||||
tenant_id: Virtual tenant UUID
|
||||
session_created_at: When the demo session was created
|
||||
rabbitmq_client: RabbitMQ client for publishing alerts
|
||||
|
||||
Returns:
|
||||
Number of alerts created
|
||||
@@ -443,6 +518,7 @@ async def generate_order_alerts(
|
||||
f"Estado actual: {order.status}. "
|
||||
f"Verificar que esté en producción.",
|
||||
service="orders",
|
||||
rabbitmq_client=rabbitmq_client,
|
||||
metadata={
|
||||
"order_id": str(order.id),
|
||||
"order_number": order.order_number,
|
||||
@@ -465,6 +541,7 @@ async def generate_order_alerts(
|
||||
f"Fecha de entrega prevista: {order.requested_delivery_date.strftime('%d/%m/%Y')}. "
|
||||
f"Contactar al cliente y renegociar fecha de entrega.",
|
||||
service="orders",
|
||||
rabbitmq_client=rabbitmq_client,
|
||||
metadata={
|
||||
"order_id": str(order.id),
|
||||
"order_number": order.order_number,
|
||||
@@ -487,6 +564,7 @@ async def generate_order_alerts(
|
||||
f"Monto: ¬{float(order.total_amount):.2f}. "
|
||||
f"Revisar disponibilidad de ingredientes y confirmar producción.",
|
||||
service="orders",
|
||||
rabbitmq_client=rabbitmq_client,
|
||||
metadata={
|
||||
"order_id": str(order.id),
|
||||
"order_number": order.order_number,
|
||||
|
||||
Reference in New Issue
Block a user