Add role-based filtering and imporve code
This commit is contained in:
@@ -16,9 +16,13 @@ RUN apt-get update && apt-get install -y \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements
|
||||
COPY shared/requirements-tracing.txt /tmp/
|
||||
|
||||
COPY services/orders/requirements.txt .
|
||||
|
||||
# Install Python dependencies
|
||||
RUN pip install --no-cache-dir -r /tmp/requirements-tracing.txt
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy shared libraries from the shared stage
|
||||
|
||||
@@ -13,6 +13,7 @@ import structlog
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from shared.auth.access_control import require_user_role
|
||||
from shared.routing import RouteBuilder
|
||||
from shared.security import create_audit_logger, AuditSeverity, AuditAction
|
||||
from app.core.database import get_db
|
||||
from app.services.orders_service import OrdersService
|
||||
from app.schemas.order_schemas import (
|
||||
@@ -22,6 +23,7 @@ from app.schemas.order_schemas import (
|
||||
)
|
||||
|
||||
logger = structlog.get_logger()
|
||||
audit_logger = create_audit_logger("orders-service")
|
||||
|
||||
# Create route builder for consistent URL structure
|
||||
route_builder = RouteBuilder('orders')
|
||||
@@ -236,7 +238,10 @@ async def delete_customer(
|
||||
orders_service: OrdersService = Depends(get_orders_service),
|
||||
db = Depends(get_db)
|
||||
):
|
||||
"""Delete a customer (soft delete)"""
|
||||
"""
|
||||
Delete a customer (Admin+ only, GDPR-compliant soft delete)
|
||||
Removes PII while maintaining referential integrity
|
||||
"""
|
||||
try:
|
||||
customer = await orders_service.customer_repo.get(db, customer_id, tenant_id)
|
||||
if not customer:
|
||||
@@ -245,10 +250,39 @@ async def delete_customer(
|
||||
detail="Customer not found"
|
||||
)
|
||||
|
||||
# Capture customer data before deletion (for audit trail)
|
||||
# Note: This is anonymized after retention period in compliance with GDPR
|
||||
customer_data = {
|
||||
"customer_code": customer.customer_code,
|
||||
"customer_name": customer.customer_name,
|
||||
"email": customer.email,
|
||||
"phone": customer.phone,
|
||||
"business_type": customer.business_type if hasattr(customer, 'business_type') else None
|
||||
}
|
||||
|
||||
await orders_service.customer_repo.delete(db, customer_id, tenant_id)
|
||||
|
||||
logger.info("Customer deleted successfully",
|
||||
customer_id=str(customer_id))
|
||||
# Log HIGH severity audit event for customer deletion (GDPR compliance)
|
||||
try:
|
||||
await audit_logger.log_deletion(
|
||||
db_session=db,
|
||||
tenant_id=str(tenant_id),
|
||||
user_id=current_user["user_id"],
|
||||
resource_type="customer",
|
||||
resource_id=str(customer_id),
|
||||
resource_data=customer_data,
|
||||
description=f"Admin {current_user.get('email', 'unknown')} deleted customer {customer_data['customer_code']} (GDPR-compliant soft delete)",
|
||||
endpoint=f"/customers/{customer_id}",
|
||||
method="DELETE",
|
||||
severity=AuditSeverity.HIGH.value
|
||||
)
|
||||
except Exception as audit_error:
|
||||
logger.warning("Failed to log audit event", error=str(audit_error))
|
||||
|
||||
logger.info("Customer deleted successfully (GDPR-compliant)",
|
||||
customer_id=str(customer_id),
|
||||
tenant_id=str(tenant_id),
|
||||
user_id=current_user["user_id"])
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
@@ -14,6 +14,7 @@ import structlog
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from shared.auth.access_control import require_user_role
|
||||
from shared.routing import RouteBuilder
|
||||
from shared.security import create_audit_logger, AuditSeverity, AuditAction
|
||||
from app.core.database import get_db
|
||||
from app.services.orders_service import OrdersService
|
||||
from app.schemas.order_schemas import (
|
||||
@@ -23,6 +24,7 @@ from app.schemas.order_schemas import (
|
||||
)
|
||||
|
||||
logger = structlog.get_logger()
|
||||
audit_logger = create_audit_logger("orders-service")
|
||||
|
||||
# Create route builder for consistent URL structure
|
||||
route_builder = RouteBuilder('orders')
|
||||
@@ -238,7 +240,7 @@ async def delete_order(
|
||||
orders_service: OrdersService = Depends(get_orders_service),
|
||||
db = Depends(get_db)
|
||||
):
|
||||
"""Delete an order (soft delete)"""
|
||||
"""Delete an order (Admin+ only, soft delete)"""
|
||||
try:
|
||||
order = await orders_service.order_repo.get(db, order_id, tenant_id)
|
||||
if not order:
|
||||
@@ -247,10 +249,37 @@ async def delete_order(
|
||||
detail="Order not found"
|
||||
)
|
||||
|
||||
# Capture order data before deletion
|
||||
order_data = {
|
||||
"order_number": order.order_number,
|
||||
"customer_id": str(order.customer_id) if order.customer_id else None,
|
||||
"order_status": order.order_status,
|
||||
"total_amount": float(order.total_amount) if order.total_amount else 0.0,
|
||||
"order_date": order.order_date.isoformat() if order.order_date else None
|
||||
}
|
||||
|
||||
await orders_service.order_repo.delete(db, order_id, tenant_id)
|
||||
|
||||
# Log audit event for order deletion
|
||||
try:
|
||||
await audit_logger.log_deletion(
|
||||
db_session=db,
|
||||
tenant_id=str(tenant_id),
|
||||
user_id=current_user["user_id"],
|
||||
resource_type="order",
|
||||
resource_id=str(order_id),
|
||||
resource_data=order_data,
|
||||
description=f"Admin {current_user.get('email', 'unknown')} deleted order {order_data['order_number']}",
|
||||
endpoint=f"/orders/{order_id}",
|
||||
method="DELETE"
|
||||
)
|
||||
except Exception as audit_error:
|
||||
logger.warning("Failed to log audit event", error=str(audit_error))
|
||||
|
||||
logger.info("Order deleted successfully",
|
||||
order_id=str(order_id))
|
||||
order_id=str(order_id),
|
||||
tenant_id=str(tenant_id),
|
||||
user_id=current_user["user_id"])
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
@@ -4,6 +4,13 @@ Orders Service Models Package
|
||||
Import all models to ensure they are registered with SQLAlchemy Base.
|
||||
"""
|
||||
|
||||
# Import AuditLog model for this service
|
||||
from shared.security import create_audit_log_model
|
||||
from shared.database.base import Base
|
||||
|
||||
# Create audit log model for this service
|
||||
AuditLog = create_audit_log_model(Base)
|
||||
|
||||
# Import all models to register them with the Base metadata
|
||||
from .customer import Customer, CustomerContact
|
||||
from .order import CustomerOrder, OrderItem, OrderStatusHistory
|
||||
@@ -60,4 +67,5 @@ __all__ = [
|
||||
"PriorityLevel",
|
||||
"RequirementStatus",
|
||||
"RiskLevel",
|
||||
"AuditLog",
|
||||
]
|
||||
|
||||
@@ -9,9 +9,9 @@ import json
|
||||
import uuid
|
||||
from datetime import datetime, date, timedelta
|
||||
from typing import Optional, Dict, Any, List
|
||||
import redis
|
||||
import structlog
|
||||
from pydantic import BaseModel
|
||||
from shared.redis_utils import get_redis_client
|
||||
|
||||
from app.core.config import settings
|
||||
from app.models.procurement import ProcurementPlan
|
||||
@@ -22,31 +22,17 @@ logger = structlog.get_logger()
|
||||
|
||||
class CacheService:
|
||||
"""Service for managing Redis cache operations"""
|
||||
|
||||
def __init__(self, redis_url: Optional[str] = None):
|
||||
"""Initialize Redis connection"""
|
||||
self.redis_url = redis_url or settings.REDIS_URL
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize cache service"""
|
||||
self._redis_client = None
|
||||
self._connect()
|
||||
|
||||
def _connect(self):
|
||||
"""Connect to Redis"""
|
||||
try:
|
||||
self._redis_client = redis.from_url(
|
||||
self.redis_url,
|
||||
decode_responses=True,
|
||||
socket_keepalive=True,
|
||||
socket_keepalive_options={1: 1, 3: 3, 5: 5}, # Use integer keys
|
||||
retry_on_timeout=True,
|
||||
max_connections=50
|
||||
)
|
||||
# Test connection
|
||||
self._redis_client.ping()
|
||||
logger.info("Redis connection established")
|
||||
except Exception as e:
|
||||
logger.error("Failed to connect to Redis", error=str(e))
|
||||
self._redis_client = None
|
||||
|
||||
|
||||
async def _get_redis(self):
|
||||
"""Get shared Redis client"""
|
||||
if self._redis_client is None:
|
||||
self._redis_client = await get_redis_client()
|
||||
return self._redis_client
|
||||
|
||||
@property
|
||||
def redis(self):
|
||||
"""Get Redis client with connection check"""
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""initial_schema_20251009_2038
|
||||
"""initial_schema_20251015_1229
|
||||
|
||||
Revision ID: 2f48673b672c
|
||||
Revision ID: 7f882c2ca25c
|
||||
Revises:
|
||||
Create Date: 2025-10-09 20:38:51.897501+02:00
|
||||
Create Date: 2025-10-15 12:29:27.201743+02:00
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
@@ -12,7 +12,7 @@ import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '2f48673b672c'
|
||||
revision: str = '7f882c2ca25c'
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
@@ -20,6 +20,38 @@ depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('audit_logs',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('tenant_id', sa.UUID(), nullable=False),
|
||||
sa.Column('user_id', sa.UUID(), nullable=False),
|
||||
sa.Column('action', sa.String(length=100), nullable=False),
|
||||
sa.Column('resource_type', sa.String(length=100), nullable=False),
|
||||
sa.Column('resource_id', sa.String(length=255), nullable=True),
|
||||
sa.Column('severity', sa.String(length=20), nullable=False),
|
||||
sa.Column('service_name', sa.String(length=100), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('changes', postgresql.JSON(astext_type=sa.Text()), nullable=True),
|
||||
sa.Column('audit_metadata', postgresql.JSON(astext_type=sa.Text()), nullable=True),
|
||||
sa.Column('ip_address', sa.String(length=45), nullable=True),
|
||||
sa.Column('user_agent', sa.Text(), nullable=True),
|
||||
sa.Column('endpoint', sa.String(length=255), nullable=True),
|
||||
sa.Column('method', sa.String(length=10), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('idx_audit_resource_type_action', 'audit_logs', ['resource_type', 'action'], unique=False)
|
||||
op.create_index('idx_audit_service_created', 'audit_logs', ['service_name', 'created_at'], unique=False)
|
||||
op.create_index('idx_audit_severity_created', 'audit_logs', ['severity', 'created_at'], unique=False)
|
||||
op.create_index('idx_audit_tenant_created', 'audit_logs', ['tenant_id', 'created_at'], unique=False)
|
||||
op.create_index('idx_audit_user_created', 'audit_logs', ['user_id', 'created_at'], unique=False)
|
||||
op.create_index(op.f('ix_audit_logs_action'), 'audit_logs', ['action'], unique=False)
|
||||
op.create_index(op.f('ix_audit_logs_created_at'), 'audit_logs', ['created_at'], unique=False)
|
||||
op.create_index(op.f('ix_audit_logs_resource_id'), 'audit_logs', ['resource_id'], unique=False)
|
||||
op.create_index(op.f('ix_audit_logs_resource_type'), 'audit_logs', ['resource_type'], unique=False)
|
||||
op.create_index(op.f('ix_audit_logs_service_name'), 'audit_logs', ['service_name'], unique=False)
|
||||
op.create_index(op.f('ix_audit_logs_severity'), 'audit_logs', ['severity'], unique=False)
|
||||
op.create_index(op.f('ix_audit_logs_tenant_id'), 'audit_logs', ['tenant_id'], unique=False)
|
||||
op.create_index(op.f('ix_audit_logs_user_id'), 'audit_logs', ['user_id'], unique=False)
|
||||
op.create_table('customers',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('tenant_id', sa.UUID(), nullable=False),
|
||||
@@ -352,4 +384,18 @@ def downgrade() -> None:
|
||||
op.drop_index(op.f('ix_customers_tenant_id'), table_name='customers')
|
||||
op.drop_index(op.f('ix_customers_customer_code'), table_name='customers')
|
||||
op.drop_table('customers')
|
||||
op.drop_index(op.f('ix_audit_logs_user_id'), table_name='audit_logs')
|
||||
op.drop_index(op.f('ix_audit_logs_tenant_id'), table_name='audit_logs')
|
||||
op.drop_index(op.f('ix_audit_logs_severity'), table_name='audit_logs')
|
||||
op.drop_index(op.f('ix_audit_logs_service_name'), table_name='audit_logs')
|
||||
op.drop_index(op.f('ix_audit_logs_resource_type'), table_name='audit_logs')
|
||||
op.drop_index(op.f('ix_audit_logs_resource_id'), table_name='audit_logs')
|
||||
op.drop_index(op.f('ix_audit_logs_created_at'), table_name='audit_logs')
|
||||
op.drop_index(op.f('ix_audit_logs_action'), table_name='audit_logs')
|
||||
op.drop_index('idx_audit_user_created', table_name='audit_logs')
|
||||
op.drop_index('idx_audit_tenant_created', table_name='audit_logs')
|
||||
op.drop_index('idx_audit_severity_created', table_name='audit_logs')
|
||||
op.drop_index('idx_audit_service_created', table_name='audit_logs')
|
||||
op.drop_index('idx_audit_resource_type_action', table_name='audit_logs')
|
||||
op.drop_table('audit_logs')
|
||||
# ### end Alembic commands ###
|
||||
Reference in New Issue
Block a user