Files
pvemanager/backend/app/api/logs.py
Dilshod Rasulov de967c35e0 refactor(rbac): migrate API permission checks to resource:action format
Replace all legacy dotted permission codes (vms.view, proxmox.manage, ...)
in PermissionChecker/has_permission across app/api with the new
resource:action format (vm:view, server:manage, ...), per LEGACY_PERMISSION_MAP.
All 33 codes used by the API are now registered in the permission registry.

Also switch the /api/auth/me admin branch to get_all_permissions_v2 so the
endpoint emits permissions in the new format only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 02:07:48 +05:00

208 lines
6.7 KiB
Python

"""
Logs API endpoints
View and manage audit logs
"""
from datetime import datetime, timedelta
from typing import Optional, List
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from sqlalchemy.orm import Session
from pydantic import BaseModel
from ..db import get_db
from ..auth import get_current_user, PermissionChecker
from ..models import User, AuditLog
from ..logging_service import LoggingService
from ..i18n import I18nService
router = APIRouter()
# ==================== Schemas ====================
class LogResponse(BaseModel):
id: int
level: str
category: str
action: str
message: str
username: Optional[str] = None
ip_address: Optional[str] = None
user_agent: Optional[str] = None
resource_type: Optional[str] = None
resource_id: Optional[str] = None
resource_name: Optional[str] = None
details: Optional[dict] = None
request_method: Optional[str] = None
request_path: Optional[str] = None
response_status: Optional[int] = None
duration_ms: Optional[int] = None
success: bool
error_message: Optional[str] = None
created_at: datetime
# Enhanced logging fields
request_id: Optional[str] = None
session_id: Optional[str] = None
geo_location: Optional[str] = None
server_id: Optional[int] = None
server_name: Optional[str] = None
node_name: Optional[str] = None
request_body: Optional[dict] = None
response_body: Optional[dict] = None
query_params: Optional[str] = None
error_traceback: Optional[str] = None
class Config:
from_attributes = True
class LogsListResponse(BaseModel):
logs: List[LogResponse]
total: int
page: int
limit: int
pages: int
class LogStatsResponse(BaseModel):
period_hours: int
total: int
by_level: dict
by_category: dict
errors_count: int
failed_logins: int
recent_errors: List[dict]
# ==================== API Endpoints ====================
@router.get("/api/logs", response_model=LogsListResponse)
async def get_logs(
level: Optional[str] = Query(None, description="Filter by log level"),
category: Optional[str] = Query(None, description="Filter by category"),
username: Optional[str] = Query(None, description="Filter by username"),
ip: Optional[str] = Query(None, description="Filter by IP address"),
resource_type: Optional[str] = Query(None, description="Filter by resource type"),
resource_id: Optional[str] = Query(None, description="Filter by resource ID"),
date_from: Optional[str] = Query(None, description="Filter from date (YYYY-MM-DD)"),
date_to: Optional[str] = Query(None, description="Filter to date (YYYY-MM-DD)"),
search: Optional[str] = Query(None, description="Search in message"),
page: int = Query(1, ge=1, description="Page number"),
limit: int = Query(50, ge=1, le=500, description="Items per page"),
db: Session = Depends(get_db),
current_user: User = Depends(PermissionChecker("log:view"))
):
"""Get logs with filtering and pagination"""
# Parse dates
parsed_date_from = None
parsed_date_to = None
if date_from:
try:
parsed_date_from = datetime.strptime(date_from, "%Y-%m-%d")
except ValueError:
raise HTTPException(status_code=400, detail="Invalid date_from format. Use YYYY-MM-DD")
if date_to:
try:
parsed_date_to = datetime.strptime(date_to, "%Y-%m-%d") + timedelta(days=1) # Include the whole day
except ValueError:
raise HTTPException(status_code=400, detail="Invalid date_to format. Use YYYY-MM-DD")
# Calculate offset
offset = (page - 1) * limit
# Get logs
logs, total = LoggingService.get_logs(
db=db,
level=level,
category=category,
username=username,
ip_address=ip,
resource_type=resource_type,
resource_id=resource_id,
date_from=parsed_date_from,
date_to=parsed_date_to,
search=search,
limit=limit,
offset=offset
)
# Calculate total pages
pages = (total + limit - 1) // limit if total > 0 else 1
return LogsListResponse(
logs=[LogResponse.model_validate(log) for log in logs],
total=total,
page=page,
limit=limit,
pages=pages
)
@router.get("/api/stats", response_model=LogStatsResponse)
async def get_log_stats(
hours: int = Query(24, ge=1, le=720, description="Period in hours"),
db: Session = Depends(get_db),
current_user: User = Depends(PermissionChecker("log:view"))
):
"""Get log statistics for the specified period"""
stats = LoggingService.get_stats(db, hours=hours)
return LogStatsResponse(**stats)
@router.delete("/api/logs")
async def cleanup_logs(
days: int = Query(30, ge=1, le=365, description="Delete logs older than this many days"),
db: Session = Depends(get_db),
current_user: User = Depends(PermissionChecker("log:delete"))
):
"""Delete old logs (admin only)"""
if not current_user.is_admin:
raise HTTPException(status_code=403, detail="Only admins can delete logs")
deleted = LoggingService.cleanup_old_logs(db, days=days)
# Log this action
LoggingService.log(
db=db,
level=LoggingService.WARNING,
category=LoggingService.SYSTEM,
action="logs_cleanup",
message=f"Удалено {deleted} логов старше {days} дней",
username=current_user.username
)
return {"deleted": deleted, "message": f"Удалено {deleted} логов старше {days} дней"}
@router.get("/api/levels")
async def get_log_levels(lang: str = Query("ru")):
"""Get available log levels"""
return {
"levels": [
{"value": "debug", "label": I18nService.get("log_level_debug", lang), "color": "#6c757d"},
{"value": "info", "label": I18nService.get("log_level_info", lang), "color": "#0dcaf0"},
{"value": "warning", "label": I18nService.get("log_level_warning", lang), "color": "#ffc107"},
{"value": "error", "label": I18nService.get("log_level_error", lang), "color": "#dc3545"},
{"value": "critical", "label": I18nService.get("log_level_error", lang), "color": "#dc3545"}
]
}
@router.get("/api/categories")
async def get_log_categories():
"""Get available log categories"""
return {
"categories": [
{"value": "auth", "label": "Авторизация"},
{"value": "proxmox", "label": "Proxmox"},
{"value": "ipam", "label": "IPAM"},
{"value": "system", "label": "Система"},
{"value": "api", "label": "API"},
{"value": "docker", "label": "Docker"}
]
}