feature/permisos-keycloak-correccion

This commit is contained in:
2026-05-04 13:22:18 -06:00
parent 376b99d1f9
commit 97bb5c23b6
15 changed files with 563 additions and 150 deletions

View File

@@ -7,10 +7,10 @@ from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from sqlalchemy import or_, desc, distinct
from sqlalchemy import or_, desc
from core.database import get_core_db
from core.security import get_current_user, get_tenant_from_token
from core.security import get_current_user, validate_access_to_resource
from core.storage_s3 import get_object_bytes, list_objects_tree, should_ensure_s3_bucket
from .models import AuditLog
from .schemas import (
@@ -42,13 +42,6 @@ _SEGMENT_LABELS = {
}
def _tenant_id_from_user(current_user: Dict[str, Any]) -> int:
tenant_id = get_tenant_from_token(current_user) or current_user.get("tenant_id")
if not tenant_id:
raise HTTPException(status_code=401, detail="User context is invalid")
return int(tenant_id)
def _normalize_relative_path(raw: Optional[str]) -> str:
if not raw:
return ""
@@ -181,6 +174,7 @@ def _build_breadcrumbs(
@router.get("/bitacora", response_model=AuditLogListResponse)
async def get_bitacora(
company_id: int = Query(..., description="Company ID"),
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=100),
search: Optional[str] = None,
@@ -189,13 +183,24 @@ async def get_bitacora(
reference: Optional[str] = None,
date_from: Optional[date] = None,
date_to: Optional[date] = None,
db: Session = Depends(get_core_db)
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""
Get legacy audit log (Bitácora)
Bitácora por compañía. Requiere permiso ``audit_logs.view``.
"""
query = db.query(AuditLog)
tenant_id = validate_access_to_resource(
db,
company_id,
current_user,
required_permissions=["audit_logs.view"],
)
query = db.query(AuditLog).filter(
AuditLog.company_id == company_id,
AuditLog.tenant_id == tenant_id,
)
# Filters
if date_from:
query = query.filter(AuditLog.date >= date_from)
@@ -237,22 +242,59 @@ async def get_bitacora(
}
@router.get("/bitacora/procedimientos", response_model=List[str])
async def get_procedures(db: Session = Depends(get_core_db)):
async def get_procedures(
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""
Get distinct list of procedures for filters
Lista de procedimientos para filtros (alcance compañía). Requiere ``audit_logs.view``.
"""
results = db.query(distinct(AuditLog.procedure))\
.order_by(AuditLog.procedure)\
.all()
# verify if result is tuple
tenant_id = validate_access_to_resource(
db,
company_id,
current_user,
required_permissions=["audit_logs.view"],
)
results = (
db.query(AuditLog.procedure)
.filter(
AuditLog.company_id == company_id,
AuditLog.tenant_id == tenant_id,
)
.distinct()
.order_by(AuditLog.procedure)
.all()
)
return [r[0] for r in results if r[0]]
@router.get("/bitacora/{spec_id}/detalle", response_model=AuditLogDetailResponse)
async def get_audit_detail(spec_id: int, db: Session = Depends(get_core_db)):
async def get_audit_detail(
spec_id: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""
Get full detail of a log entry
Detalle de un registro de bitácora. Requiere ``audit_logs.view``.
"""
log = db.query(AuditLog).filter(AuditLog.spec_id == spec_id).first()
tenant_id = validate_access_to_resource(
db,
company_id,
current_user,
required_permissions=["audit_logs.view"],
)
log = (
db.query(AuditLog)
.filter(
AuditLog.spec_id == spec_id,
AuditLog.company_id == company_id,
AuditLog.tenant_id == tenant_id,
)
.first()
)
if not log:
raise HTTPException(status_code=404, detail="Log entry not found")
return log
@@ -260,6 +302,7 @@ async def get_audit_detail(spec_id: int, db: Session = Depends(get_core_db)):
@router.get("/files", response_model=AuditFileBrowserResponse)
async def list_tenant_files(
company_id: int = Query(..., description="Company ID"),
path: Optional[str] = Query(default="", description="Ruta relativa de navegación."),
continuation_token: Optional[str] = Query(default=None),
max_keys: int = Query(default=100, ge=1, le=500),
@@ -268,11 +311,17 @@ async def list_tenant_files(
):
"""
Explorador de archivos de solo lectura para Auditoría.
Requiere permiso ``audit_logs.view``; el prefijo S3 sigue al tenant de la compañía.
"""
if not should_ensure_s3_bucket():
raise HTTPException(status_code=400, detail="S3 storage is disabled")
tenant_id = _tenant_id_from_user(current_user)
tenant_id = validate_access_to_resource(
db,
company_id,
current_user,
required_permissions=["audit_logs.view"],
)
tenant_prefix = _tenant_prefix(tenant_id)
rel_path = _normalize_relative_path(path)
list_prefix = f"{tenant_prefix}{rel_path}/" if rel_path else tenant_prefix
@@ -349,16 +398,24 @@ async def list_tenant_files(
@router.get("/files/download")
async def download_tenant_file(
company_id: int = Query(..., description="Company ID"),
path: str = Query(..., description="Ruta relativa del archivo a descargar."),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""
Descarga segura (backend streaming) de archivos autorizados.
Requiere ``audit_logs.view``.
"""
if not should_ensure_s3_bucket():
raise HTTPException(status_code=400, detail="S3 storage is disabled")
tenant_id = _tenant_id_from_user(current_user)
tenant_id = validate_access_to_resource(
db,
company_id,
current_user,
required_permissions=["audit_logs.view"],
)
tenant_prefix = _tenant_prefix(tenant_id)
rel_path = _normalize_relative_path(path)
if not rel_path or rel_path.endswith("/"):