feat(auth): add hub_admin global access support across modules
- Add is_hub_admin() and resolve_tenant_id_required() to core/security - hub_admin resolves tenant from company or uses None as global sentinel - Update routes and services to skip tenant filter when tenant_id is None - UserService accepts is_hub_admin flag for cross-tenant user management - get_my_companies returns all companies for hub_admin without tenant restriction
This commit is contained in:
@@ -3,7 +3,7 @@ import logging
|
|||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
from core.database import get_core_db
|
from core.database import get_core_db
|
||||||
from core.security import get_current_user, validate_access_to_resource
|
from core.security import get_current_user, is_hub_admin, resolve_tenant_id_required, validate_access_to_resource
|
||||||
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query, Request
|
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query, Request
|
||||||
|
|
||||||
from api.v1.common.catalog_validation_errors import CatalogValidationError
|
from api.v1.common.catalog_validation_errors import CatalogValidationError
|
||||||
@@ -155,16 +155,9 @@ class TenantCRUDRoutes(
|
|||||||
db: Session = Depends(self.db_dependency),
|
db: Session = Depends(self.db_dependency),
|
||||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||||
):
|
):
|
||||||
from core.security import get_tenant_from_token
|
|
||||||
|
|
||||||
if all_companies:
|
if all_companies:
|
||||||
tenant_id = get_tenant_from_token(current_user)
|
# Hub admin: tenant_id=None → el servicio devuelve todas las empresas
|
||||||
if not tenant_id:
|
tenant_id = resolve_tenant_id_required(current_user, db=db)
|
||||||
tenant_id = current_user.get("tenant_id")
|
|
||||||
if not tenant_id:
|
|
||||||
raise HTTPException(status_code=400, detail="Tenant ID not found in token")
|
|
||||||
# In all_companies mode, we don't filter by company_id,
|
|
||||||
# but we still need the tenant_id from the session/token.
|
|
||||||
target_company_id = None
|
target_company_id = None
|
||||||
else:
|
else:
|
||||||
tenant_id = validate_access_to_resource(
|
tenant_id = validate_access_to_resource(
|
||||||
@@ -177,7 +170,7 @@ class TenantCRUDRoutes(
|
|||||||
target_company_id = company_id
|
target_company_id = company_id
|
||||||
|
|
||||||
skip = (page - 1) * page_size
|
skip = (page - 1) * page_size
|
||||||
|
|
||||||
# Extraer todos los parámetros de búsqueda dinámicamente
|
# Extraer todos los parámetros de búsqueda dinámicamente
|
||||||
# Excluimos los parámetros estándar de paginación y control
|
# Excluimos los parámetros estándar de paginación y control
|
||||||
standard_params = {"company_id", "all_companies", "page", "page_size", "sort_by", "sort_order"}
|
standard_params = {"company_id", "all_companies", "page", "page_size", "sort_by", "sort_order"}
|
||||||
@@ -245,14 +238,9 @@ class TenantCRUDRoutes(
|
|||||||
db: Session = Depends(self.db_dependency),
|
db: Session = Depends(self.db_dependency),
|
||||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||||
):
|
):
|
||||||
from core.security import get_tenant_from_token
|
|
||||||
|
|
||||||
if all_companies:
|
if all_companies:
|
||||||
tenant_id = get_tenant_from_token(current_user)
|
# Hub admin: tenant_id=None → el servicio devuelve todas las empresas
|
||||||
if not tenant_id:
|
tenant_id = resolve_tenant_id_required(current_user, db=db)
|
||||||
tenant_id = current_user.get("tenant_id")
|
|
||||||
if not tenant_id:
|
|
||||||
raise HTTPException(status_code=400, detail="Tenant ID not found in token")
|
|
||||||
target_company_id = None
|
target_company_id = None
|
||||||
else:
|
else:
|
||||||
tenant_id = validate_access_to_resource(
|
tenant_id = validate_access_to_resource(
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from core.s3_keys import (
|
|||||||
customs_broker_vu_doda_private_key_key,
|
customs_broker_vu_doda_private_key_key,
|
||||||
customs_broker_vu_private_key_key,
|
customs_broker_vu_private_key_key,
|
||||||
)
|
)
|
||||||
from core.security import get_current_user, get_tenant_from_token, validate_access_to_resource
|
from core.security import get_current_user, get_tenant_from_token, resolve_tenant_id_required, validate_access_to_resource
|
||||||
from core.storage_s3 import delete_object_if_exists, put_object_bytes
|
from core.storage_s3 import delete_object_if_exists, put_object_bytes
|
||||||
|
|
||||||
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
|
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
|
||||||
@@ -32,25 +32,13 @@ MAX_VU_CER_KEY_BYTES = 5 * 1024 * 1024 # 5 MB
|
|||||||
MAX_COVE_BYTES = 15 * 1024 * 1024 # 15 MB (xml/zip)
|
MAX_COVE_BYTES = 15 * 1024 * 1024 # 15 MB (xml/zip)
|
||||||
|
|
||||||
|
|
||||||
def _resolve_tenant_id_int(current_user: dict) -> int:
|
def _resolve_tenant_id_int(
|
||||||
tid = get_tenant_from_token(current_user)
|
current_user: dict,
|
||||||
if tid is not None:
|
db=None,
|
||||||
return int(tid)
|
company_id: int = None,
|
||||||
raw = current_user.get("tenant_id")
|
):
|
||||||
if isinstance(raw, list) and raw:
|
"""Delega a resolve_tenant_id_required. Hub admin resuelve tenant desde la empresa."""
|
||||||
raw = raw[0]
|
return resolve_tenant_id_required(current_user, db=db, company_id=company_id)
|
||||||
if raw is None:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="Tenant ID not found in user data",
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
return int(raw)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="Invalid tenant ID in token",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _remove_stored_vu_path(ref: Optional[str]) -> None:
|
def _remove_stored_vu_path(ref: Optional[str]) -> None:
|
||||||
@@ -198,7 +186,7 @@ async def upload_customs_broker_vu_file(
|
|||||||
- DODA: doda_certificate_path, doda_key_path, doda_xml_files_path
|
- DODA: doda_certificate_path, doda_key_path, doda_xml_files_path
|
||||||
"""
|
"""
|
||||||
validate_access_to_resource(db, company_id, current_user, ["customs_brokers.create"])
|
validate_access_to_resource(db, company_id, current_user, ["customs_brokers.create"])
|
||||||
tenant_id = _resolve_tenant_id_int(current_user)
|
tenant_id = _resolve_tenant_id_int(current_user, db=db, company_id=company_id)
|
||||||
|
|
||||||
broker = services.CustomsBrokerService.get_by_id(db, broker_key, tenant_id, company_id)
|
broker = services.CustomsBrokerService.get_by_id(db, broker_key, tenant_id, company_id)
|
||||||
if not broker:
|
if not broker:
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from core.config import settings
|
|||||||
from core.database import get_core_db
|
from core.database import get_core_db
|
||||||
from core.exceptions import ValidationException
|
from core.exceptions import ValidationException
|
||||||
from core.s3_keys import cove_acuse_pdf_key
|
from core.s3_keys import cove_acuse_pdf_key
|
||||||
from core.security import get_current_user, get_tenant_from_token, validate_access_to_resource
|
from core.security import get_current_user, get_tenant_from_token, resolve_tenant_id_required, validate_access_to_resource
|
||||||
|
|
||||||
from api.v1.modules.core.tasks_tracking import track_and_dispatch
|
from api.v1.modules.core.tasks_tracking import track_and_dispatch
|
||||||
|
|
||||||
@@ -147,14 +147,10 @@ def check_cove_eligibility(
|
|||||||
Evalúa si la factura tiene todos los datos necesarios (VU, factura, partidas)
|
Evalúa si la factura tiene todos los datos necesarios (VU, factura, partidas)
|
||||||
para poder generar un COVE. No dispara la tarea Celery.
|
para poder generar un COVE. No dispara la tarea Celery.
|
||||||
"""
|
"""
|
||||||
tenant_id = get_tenant_from_token(current_user)
|
tenant_id = resolve_tenant_id_required(current_user, db=db, company_id=company_id)
|
||||||
if not tenant_id:
|
|
||||||
raise HTTPException(status_code=400, detail="Tenant ID not found in token")
|
|
||||||
|
|
||||||
tenant_id_int = int(tenant_id)
|
|
||||||
|
|
||||||
service = FacturaCoveDomainService(db)
|
service = FacturaCoveDomainService(db)
|
||||||
eligibility = service.check_eligibility(invoice_id=invoice_id, tenant_id=tenant_id_int, company_id=company_id)
|
eligibility = service.check_eligibility(invoice_id=invoice_id, tenant_id=tenant_id, company_id=company_id)
|
||||||
return eligibility
|
return eligibility
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ from core.security import (
|
|||||||
collect_user_role_names,
|
collect_user_role_names,
|
||||||
get_current_user,
|
get_current_user,
|
||||||
get_tenant_from_token,
|
get_tenant_from_token,
|
||||||
|
is_hub_admin,
|
||||||
resolve_effective_tenant_id_from_user,
|
resolve_effective_tenant_id_from_user,
|
||||||
|
resolve_tenant_id_required,
|
||||||
validate_access_to_resource,
|
validate_access_to_resource,
|
||||||
)
|
)
|
||||||
from .....common.tenant_crud_routes import TenantCRUDRoutes
|
from .....common.tenant_crud_routes import TenantCRUDRoutes
|
||||||
@@ -40,7 +42,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def _user_is_admin(current_user: dict) -> bool:
|
def _user_is_admin(current_user: dict) -> bool:
|
||||||
return "admin" in collect_user_role_names(current_user)
|
return "admin" in collect_user_role_names(current_user) or is_hub_admin(current_user)
|
||||||
|
|
||||||
|
|
||||||
def _assert_permission_any_company(
|
def _assert_permission_any_company(
|
||||||
@@ -81,26 +83,13 @@ def _assert_permission_for_company(
|
|||||||
return validate_access_to_resource(db, company_id, current_user, [permission_code])
|
return validate_access_to_resource(db, company_id, current_user, [permission_code])
|
||||||
|
|
||||||
|
|
||||||
def _resolve_tenant_id_int(current_user: dict) -> int:
|
def _resolve_tenant_id_int(
|
||||||
"""Misma lógica que validate_access_to_resource: entero estable para BD y claves S3."""
|
current_user: dict,
|
||||||
tid = get_tenant_from_token(current_user)
|
db: Session = None,
|
||||||
if tid is not None:
|
company_id: int = None,
|
||||||
return int(tid)
|
) -> Optional[int]:
|
||||||
raw = current_user.get("tenant_id")
|
"""Delega a resolve_tenant_id_required. Hub admin resuelve tenant desde la empresa."""
|
||||||
if isinstance(raw, list) and raw:
|
return resolve_tenant_id_required(current_user, db=db, company_id=company_id)
|
||||||
raw = raw[0]
|
|
||||||
if raw is None:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="Tenant ID not found in user data",
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
return int(raw)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="Invalid tenant ID in token",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _is_s3_object_key(ref: Optional[str]) -> bool:
|
def _is_s3_object_key(ref: Optional[str]) -> bool:
|
||||||
@@ -133,11 +122,11 @@ async def create_company(
|
|||||||
):
|
):
|
||||||
_assert_permission_any_company(db, current_user, "cat_company.create")
|
_assert_permission_any_company(db, current_user, "cat_company.create")
|
||||||
|
|
||||||
tenant_id = current_user.get("tenant_id")
|
tenant_id = resolve_tenant_id_required(current_user, db=db)
|
||||||
if not tenant_id:
|
if tenant_id is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Tenant ID not found in user data",
|
detail="Hub admin: debe especificar tenant_id para crear una empresa",
|
||||||
)
|
)
|
||||||
|
|
||||||
service = CompanyService(db)
|
service = CompanyService(db)
|
||||||
@@ -161,12 +150,7 @@ async def list_companies(
|
|||||||
"""Get paginated list of companies for current tenant with optional filters"""
|
"""Get paginated list of companies for current tenant with optional filters"""
|
||||||
_assert_permission_any_company(db, current_user, "cat_company.view")
|
_assert_permission_any_company(db, current_user, "cat_company.view")
|
||||||
|
|
||||||
tenant_id = current_user.get("tenant_id")
|
tenant_id = resolve_tenant_id_required(current_user, db=db)
|
||||||
if not tenant_id:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="Tenant ID not found in user data",
|
|
||||||
)
|
|
||||||
|
|
||||||
skip = (page - 1) * page_size
|
skip = (page - 1) * page_size
|
||||||
filters = {}
|
filters = {}
|
||||||
@@ -214,6 +198,24 @@ async def get_my_companies(
|
|||||||
Un usuario solo con roles de app y sin ``tenant_id`` en /auth/me sigue pudiendo
|
Un usuario solo con roles de app y sin ``tenant_id`` en /auth/me sigue pudiendo
|
||||||
listar sus compañías asignadas.
|
listar sus compañías asignadas.
|
||||||
"""
|
"""
|
||||||
|
from core.security import collect_user_role_names
|
||||||
|
user_roles = collect_user_role_names(current_user)
|
||||||
|
|
||||||
|
# Hub admin: visibilidad global sobre todas las compañías sin restricciones
|
||||||
|
# de tenant ni licencia. El Hub ya validó el rol en /auth/me.
|
||||||
|
if "hub_admin" in user_roles:
|
||||||
|
service = CompanyService(db)
|
||||||
|
all_companies = (
|
||||||
|
db.query(Company)
|
||||||
|
.filter(Company.deleted_at.is_(None))
|
||||||
|
.order_by(Company.name)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return [
|
||||||
|
CompanyResponseDTO.model_validate(service.flatten_company_dto(c))
|
||||||
|
for c in all_companies
|
||||||
|
]
|
||||||
|
|
||||||
_assert_permission_any_company(db, current_user, "cat_company.view")
|
_assert_permission_any_company(db, current_user, "cat_company.view")
|
||||||
|
|
||||||
user_id = current_user.get("sub") or current_user.get("id")
|
user_id = current_user.get("sub") or current_user.get("id")
|
||||||
@@ -245,12 +247,7 @@ async def get_company(
|
|||||||
"""Get a specific company by ID"""
|
"""Get a specific company by ID"""
|
||||||
_assert_permission_for_company(db, company_id, current_user, "cat_company.view")
|
_assert_permission_for_company(db, company_id, current_user, "cat_company.view")
|
||||||
|
|
||||||
tenant_id = current_user.get("tenant_id")
|
tenant_id = resolve_tenant_id_required(current_user, db=db, company_id=company_id)
|
||||||
if not tenant_id:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="Tenant ID not found in user data",
|
|
||||||
)
|
|
||||||
|
|
||||||
service = CompanyService(db)
|
service = CompanyService(db)
|
||||||
company = CompanyService.get_by_id(db, company_id, tenant_id, 0)
|
company = CompanyService.get_by_id(db, company_id, tenant_id, 0)
|
||||||
@@ -277,12 +274,7 @@ async def update_company(
|
|||||||
"""Update a company"""
|
"""Update a company"""
|
||||||
_assert_permission_for_company(db, company_id, current_user, "cat_company.edit")
|
_assert_permission_for_company(db, company_id, current_user, "cat_company.edit")
|
||||||
|
|
||||||
tenant_id = current_user.get("tenant_id")
|
tenant_id = resolve_tenant_id_required(current_user, db=db, company_id=company_id)
|
||||||
if not tenant_id:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="Tenant ID not found in user data",
|
|
||||||
)
|
|
||||||
|
|
||||||
service = CompanyService(db)
|
service = CompanyService(db)
|
||||||
updated_company = service.update(db, company_id, tenant_id, 0, data)
|
updated_company = service.update(db, company_id, tenant_id, 0, data)
|
||||||
@@ -354,12 +346,7 @@ async def delete_company(
|
|||||||
"""Delete a company"""
|
"""Delete a company"""
|
||||||
_assert_permission_for_company(db, company_id, current_user, "cat_company.delete")
|
_assert_permission_for_company(db, company_id, current_user, "cat_company.delete")
|
||||||
|
|
||||||
tenant_id = current_user.get("tenant_id")
|
tenant_id = resolve_tenant_id_required(current_user, db=db, company_id=company_id)
|
||||||
if not tenant_id:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="Tenant ID not found in user data",
|
|
||||||
)
|
|
||||||
|
|
||||||
success = CompanyService.delete(db, company_id, tenant_id, 0)
|
success = CompanyService.delete(db, company_id, tenant_id, 0)
|
||||||
if not success:
|
if not success:
|
||||||
@@ -385,7 +372,7 @@ async def upload_company_logo(
|
|||||||
"""Upload a logo for a company"""
|
"""Upload a logo for a company"""
|
||||||
_assert_permission_for_company(db, company_id, current_user, "cat_company.edit")
|
_assert_permission_for_company(db, company_id, current_user, "cat_company.edit")
|
||||||
|
|
||||||
tenant_id = _resolve_tenant_id_int(current_user)
|
tenant_id = _resolve_tenant_id_int(current_user, db=db, company_id=company_id)
|
||||||
|
|
||||||
# Validar que la empresa existe
|
# Validar que la empresa existe
|
||||||
company = CompanyService.get_by_id(db, company_id, tenant_id, 0)
|
company = CompanyService.get_by_id(db, company_id, tenant_id, 0)
|
||||||
@@ -468,7 +455,7 @@ async def upload_company_certificate(
|
|||||||
"""
|
"""
|
||||||
_assert_permission_for_company(db, company_id, current_user, "cat_company.edit")
|
_assert_permission_for_company(db, company_id, current_user, "cat_company.edit")
|
||||||
|
|
||||||
tenant_id = _resolve_tenant_id_int(current_user)
|
tenant_id = _resolve_tenant_id_int(current_user, db=db, company_id=company_id)
|
||||||
|
|
||||||
# Validar que la empresa existe
|
# Validar que la empresa existe
|
||||||
service = CompanyService(db)
|
service = CompanyService(db)
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ class CompanyService:
|
|||||||
filters: Optional[Dict[str, Any]] = None,
|
filters: Optional[Dict[str, Any]] = None,
|
||||||
) -> Tuple[List[Company], int]:
|
) -> Tuple[List[Company], int]:
|
||||||
"""Get all companies for a tenant with pagination"""
|
"""Get all companies for a tenant with pagination"""
|
||||||
query = db.query(Company).filter(Company.tenant_id == tenant_id, Company.deleted_at.is_(None))
|
query = db.query(Company).filter(Company.deleted_at.is_(None))
|
||||||
|
if tenant_id is not None:
|
||||||
|
query = query.filter(Company.tenant_id == tenant_id)
|
||||||
|
|
||||||
# Apply filters if provided
|
# Apply filters if provided
|
||||||
if filters:
|
if filters:
|
||||||
@@ -67,15 +69,10 @@ class CompanyService:
|
|||||||
db: Session, company_id: int, tenant_id: int, company_id_unused: int
|
db: Session, company_id: int, tenant_id: int, company_id_unused: int
|
||||||
) -> Optional[Company]:
|
) -> Optional[Company]:
|
||||||
"""Get company by ID"""
|
"""Get company by ID"""
|
||||||
return (
|
query = db.query(Company).filter(Company.id == company_id, Company.deleted_at.is_(None))
|
||||||
db.query(Company)
|
if tenant_id is not None:
|
||||||
.filter(
|
query = query.filter(Company.tenant_id == tenant_id)
|
||||||
Company.id == company_id,
|
return query.first()
|
||||||
Company.tenant_id == tenant_id,
|
|
||||||
Company.deleted_at.is_(None)
|
|
||||||
)
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
|
|
||||||
# ESTE ES EL MÉTODO VIEJO QUE CAUSABA PROBLEMAS (Lo dejamos por si acaso)
|
# ESTE ES EL MÉTODO VIEJO QUE CAUSABA PROBLEMAS (Lo dejamos por si acaso)
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -764,20 +764,12 @@ def generate_invoice_report_async(
|
|||||||
# Serialize filters to dict for Celery
|
# Serialize filters to dict for Celery
|
||||||
filter_data = filters.model_dump()
|
filter_data = filters.model_dump()
|
||||||
user_email = current_user.get('email')
|
user_email = current_user.get('email')
|
||||||
|
|
||||||
# Trigger task
|
# tenant_id ya validado por validate_access_to_resource
|
||||||
tenant_id = get_tenant_from_token(current_user)
|
|
||||||
if not tenant_id:
|
|
||||||
tenant_id = current_user.get("tenant_id")
|
|
||||||
if not tenant_id:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="Tenant ID not found in token",
|
|
||||||
)
|
|
||||||
task = track_and_dispatch(
|
task = track_and_dispatch(
|
||||||
db=db,
|
db=db,
|
||||||
task=generate_invoice_movements_async,
|
task=generate_invoice_movements_async,
|
||||||
tenant_id=int(tenant_id),
|
tenant_id=tenant_id,
|
||||||
company_id=company_id,
|
company_id=company_id,
|
||||||
requested_by_user=current_user.get("preferred_username") or current_user.get("email") or current_user.get("sub"),
|
requested_by_user=current_user.get("preferred_username") or current_user.get("email") or current_user.get("sub"),
|
||||||
task_name="generate_invoice_movements_async",
|
task_name="generate_invoice_movements_async",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from core.database import get_core_db
|
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, resolve_tenant_id_required
|
||||||
|
|
||||||
from .models import TaskRun, TaskStatus
|
from .models import TaskRun, TaskStatus
|
||||||
from .schemas import TaskCatalogsResponse, TaskRunDetail, TaskRunListItem, TaskRunsResponse, TaskSyncRequest
|
from .schemas import TaskCatalogsResponse, TaskRunDetail, TaskRunListItem, TaskRunsResponse, TaskSyncRequest
|
||||||
@@ -58,9 +58,7 @@ def list_tasks(
|
|||||||
current_user: dict[str, Any] = Depends(get_current_user),
|
current_user: dict[str, Any] = Depends(get_current_user),
|
||||||
db: Session = Depends(get_core_db),
|
db: Session = Depends(get_core_db),
|
||||||
):
|
):
|
||||||
tenant_id = get_tenant_from_token(current_user)
|
tenant_id = resolve_tenant_id_required(current_user)
|
||||||
if not tenant_id:
|
|
||||||
raise HTTPException(status_code=400, detail="Tenant ID not found in token")
|
|
||||||
|
|
||||||
tracker = TaskTrackerService(db)
|
tracker = TaskTrackerService(db)
|
||||||
if sync_active:
|
if sync_active:
|
||||||
@@ -93,11 +91,12 @@ def get_task_detail(
|
|||||||
current_user: dict[str, Any] = Depends(get_current_user),
|
current_user: dict[str, Any] = Depends(get_current_user),
|
||||||
db: Session = Depends(get_core_db),
|
db: Session = Depends(get_core_db),
|
||||||
):
|
):
|
||||||
tenant_id = get_tenant_from_token(current_user)
|
tenant_id = resolve_tenant_id_required(current_user)
|
||||||
if not tenant_id:
|
|
||||||
raise HTTPException(status_code=400, detail="Tenant ID not found in token")
|
|
||||||
|
|
||||||
row = db.query(TaskRun).filter(TaskRun.task_id == task_id, TaskRun.tenant_id == tenant_id).first()
|
query = db.query(TaskRun).filter(TaskRun.task_id == task_id)
|
||||||
|
if tenant_id is not None:
|
||||||
|
query = query.filter(TaskRun.tenant_id == tenant_id)
|
||||||
|
row = query.first()
|
||||||
if not row:
|
if not row:
|
||||||
raise HTTPException(status_code=404, detail="Task not found")
|
raise HTTPException(status_code=404, detail="Task not found")
|
||||||
|
|
||||||
@@ -120,9 +119,7 @@ def sync_tasks(
|
|||||||
current_user: dict[str, Any] = Depends(get_current_user),
|
current_user: dict[str, Any] = Depends(get_current_user),
|
||||||
db: Session = Depends(get_core_db),
|
db: Session = Depends(get_core_db),
|
||||||
):
|
):
|
||||||
tenant_id = get_tenant_from_token(current_user)
|
tenant_id = resolve_tenant_id_required(current_user)
|
||||||
if not tenant_id:
|
|
||||||
raise HTTPException(status_code=400, detail="Tenant ID not found in token")
|
|
||||||
tracker = TaskTrackerService(db)
|
tracker = TaskTrackerService(db)
|
||||||
updated = tracker.sync_active_tasks(tenant_id=tenant_id, task_ids=body.task_ids)
|
updated = tracker.sync_active_tasks(tenant_id=tenant_id, task_ids=body.task_ids)
|
||||||
return {"updated": updated}
|
return {"updated": updated}
|
||||||
@@ -133,15 +130,18 @@ def get_catalogs(
|
|||||||
current_user: dict[str, Any] = Depends(get_current_user),
|
current_user: dict[str, Any] = Depends(get_current_user),
|
||||||
db: Session = Depends(get_core_db),
|
db: Session = Depends(get_core_db),
|
||||||
):
|
):
|
||||||
tenant_id = get_tenant_from_token(current_user)
|
tenant_id = resolve_tenant_id_required(current_user)
|
||||||
if not tenant_id:
|
|
||||||
raise HTTPException(status_code=400, detail="Tenant ID not found in token")
|
|
||||||
|
|
||||||
groups = (
|
groups_q = db.query(TaskRun.task_group)
|
||||||
db.query(TaskRun.task_group).filter(TaskRun.tenant_id == tenant_id).distinct().order_by(TaskRun.task_group).all()
|
names_q = db.query(TaskRun.task_name)
|
||||||
)
|
statuses_q = db.query(TaskRun.status)
|
||||||
names = db.query(TaskRun.task_name).filter(TaskRun.tenant_id == tenant_id).distinct().order_by(TaskRun.task_name).all()
|
if tenant_id is not None:
|
||||||
statuses = db.query(TaskRun.status).filter(TaskRun.tenant_id == tenant_id).distinct().order_by(TaskRun.status).all()
|
groups_q = groups_q.filter(TaskRun.tenant_id == tenant_id)
|
||||||
|
names_q = names_q.filter(TaskRun.tenant_id == tenant_id)
|
||||||
|
statuses_q = statuses_q.filter(TaskRun.tenant_id == tenant_id)
|
||||||
|
groups = groups_q.distinct().order_by(TaskRun.task_group).all()
|
||||||
|
names = names_q.distinct().order_by(TaskRun.task_name).all()
|
||||||
|
statuses = statuses_q.distinct().order_by(TaskRun.status).all()
|
||||||
return TaskCatalogsResponse(
|
return TaskCatalogsResponse(
|
||||||
task_groups=[g[0] for g in groups if g[0]],
|
task_groups=[g[0] for g in groups if g[0]],
|
||||||
task_names=[n[0] for n in names if n[0]],
|
task_names=[n[0] for n in names if n[0]],
|
||||||
|
|||||||
@@ -193,10 +193,12 @@ class TaskTrackerService:
|
|||||||
self.db.refresh(task_run)
|
self.db.refresh(task_run)
|
||||||
return task_run
|
return task_run
|
||||||
|
|
||||||
def sync_active_tasks(self, tenant_id: int, task_ids: list[str] | None = None) -> int:
|
def sync_active_tasks(self, tenant_id: int | None, task_ids: list[str] | None = None) -> int:
|
||||||
query = self.db.query(TaskRun).filter(
|
query = self.db.query(TaskRun).filter(
|
||||||
TaskRun.tenant_id == tenant_id, TaskRun.status.in_([TaskStatus.PENDING.value, TaskStatus.ACTIVE.value])
|
TaskRun.status.in_([TaskStatus.PENDING.value, TaskStatus.ACTIVE.value])
|
||||||
)
|
)
|
||||||
|
if tenant_id is not None:
|
||||||
|
query = query.filter(TaskRun.tenant_id == tenant_id)
|
||||||
if task_ids:
|
if task_ids:
|
||||||
query = query.filter(TaskRun.task_id.in_(task_ids))
|
query = query.filter(TaskRun.task_id.in_(task_ids))
|
||||||
rows = query.limit(200).all()
|
rows = query.limit(200).all()
|
||||||
@@ -207,7 +209,7 @@ class TaskTrackerService:
|
|||||||
def list_tasks(
|
def list_tasks(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
tenant_id: int,
|
tenant_id: int | None,
|
||||||
page: int,
|
page: int,
|
||||||
page_size: int,
|
page_size: int,
|
||||||
status: list[str] | None = None,
|
status: list[str] | None = None,
|
||||||
@@ -217,7 +219,9 @@ class TaskTrackerService:
|
|||||||
search: str | None = None,
|
search: str | None = None,
|
||||||
order: str = "desc",
|
order: str = "desc",
|
||||||
) -> tuple[list[TaskRun], int]:
|
) -> tuple[list[TaskRun], int]:
|
||||||
query = self.db.query(TaskRun).filter(TaskRun.tenant_id == tenant_id)
|
query = self.db.query(TaskRun)
|
||||||
|
if tenant_id is not None:
|
||||||
|
query = query.filter(TaskRun.tenant_id == tenant_id)
|
||||||
if status:
|
if status:
|
||||||
query = query.filter(TaskRun.status.in_(status))
|
query = query.filter(TaskRun.status.in_(status))
|
||||||
if task_group:
|
if task_group:
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from core.s3_keys import public_user_avatar_api_path, user_avatar_key
|
|||||||
from core.storage_s3 import delete_object_if_exists, get_object_bytes, put_object_bytes
|
from core.storage_s3 import delete_object_if_exists, get_object_bytes, put_object_bytes
|
||||||
from core.security import (
|
from core.security import (
|
||||||
get_current_user,
|
get_current_user,
|
||||||
|
is_hub_admin,
|
||||||
resolve_hub_tenant_id_for_api,
|
resolve_hub_tenant_id_for_api,
|
||||||
validate_access_to_resource,
|
validate_access_to_resource,
|
||||||
)
|
)
|
||||||
@@ -51,7 +52,7 @@ async def get_user_statistics(
|
|||||||
Obtiene estadísticas de usuarios del tenant actual
|
Obtiene estadísticas de usuarios del tenant actual
|
||||||
"""
|
"""
|
||||||
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["user.view"])
|
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["user.view"])
|
||||||
service = UserService(db, tenant_id, company_id)
|
service = UserService(db, tenant_id, company_id, is_hub_admin=is_hub_admin(current_user))
|
||||||
auth_header = request.headers.get("Authorization") or ""
|
auth_header = request.headers.get("Authorization") or ""
|
||||||
token = (
|
token = (
|
||||||
auth_header[7:].strip()
|
auth_header[7:].strip()
|
||||||
@@ -82,7 +83,7 @@ async def list_users(
|
|||||||
Lista todos los usuarios del tenant con paginación
|
Lista todos los usuarios del tenant con paginación
|
||||||
"""
|
"""
|
||||||
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["user.view"])
|
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["user.view"])
|
||||||
service = UserService(db, tenant_id, company_id)
|
service = UserService(db, tenant_id, company_id, is_hub_admin=is_hub_admin(current_user))
|
||||||
auth_header = request.headers.get("Authorization") or ""
|
auth_header = request.headers.get("Authorization") or ""
|
||||||
token = (
|
token = (
|
||||||
auth_header[7:].strip()
|
auth_header[7:].strip()
|
||||||
@@ -342,7 +343,7 @@ async def get_user_detail(
|
|||||||
Obtiene información detallada de un usuario específico
|
Obtiene información detallada de un usuario específico
|
||||||
"""
|
"""
|
||||||
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["user.view"])
|
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["user.view"])
|
||||||
service = UserService(db, tenant_id, company_id)
|
service = UserService(db, tenant_id, company_id, is_hub_admin=is_hub_admin(current_user))
|
||||||
return await service.get_user(user_id)
|
return await service.get_user(user_id)
|
||||||
|
|
||||||
|
|
||||||
@@ -357,7 +358,7 @@ async def create_new_user(
|
|||||||
Crea un nuevo usuario a través del Hub y lo asocia al tenant
|
Crea un nuevo usuario a través del Hub y lo asocia al tenant
|
||||||
"""
|
"""
|
||||||
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["user.create"])
|
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["user.create"])
|
||||||
service = UserService(db, tenant_id, company_id)
|
service = UserService(db, tenant_id, company_id, is_hub_admin=is_hub_admin(current_user))
|
||||||
user = await service.create_user(
|
user = await service.create_user(
|
||||||
email=data.email,
|
email=data.email,
|
||||||
username=data.username,
|
username=data.username,
|
||||||
@@ -383,7 +384,7 @@ async def update_user_detail(
|
|||||||
Actualiza información de un usuario
|
Actualiza información de un usuario
|
||||||
"""
|
"""
|
||||||
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["user.update"])
|
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["user.update"])
|
||||||
service = UserService(db, tenant_id, company_id)
|
service = UserService(db, tenant_id, company_id, is_hub_admin=is_hub_admin(current_user))
|
||||||
user = await service.update_user(
|
user = await service.update_user(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
first_name=data.first_name,
|
first_name=data.first_name,
|
||||||
@@ -413,7 +414,7 @@ async def get_user_tenant_count(
|
|||||||
tenant_id = validate_access_to_resource(
|
tenant_id = validate_access_to_resource(
|
||||||
db, company_id, current_user, required_permissions=["user.view"]
|
db, company_id, current_user, required_permissions=["user.view"]
|
||||||
)
|
)
|
||||||
service = UserService(db, tenant_id, company_id)
|
service = UserService(db, tenant_id, company_id, is_hub_admin=is_hub_admin(current_user))
|
||||||
count = service.get_user_tenant_count(user_id)
|
count = service.get_user_tenant_count(user_id)
|
||||||
return {"tenant_count": count}
|
return {"tenant_count": count}
|
||||||
|
|
||||||
@@ -449,7 +450,7 @@ async def delete_user_route(
|
|||||||
hub_tid = resolve_hub_tenant_id_for_api(
|
hub_tid = resolve_hub_tenant_id_for_api(
|
||||||
tenant_id, request.headers.get("X-Tenant-Override")
|
tenant_id, request.headers.get("X-Tenant-Override")
|
||||||
)
|
)
|
||||||
service = UserService(db, tenant_id, company_id)
|
service = UserService(db, tenant_id, company_id, is_hub_admin=is_hub_admin(current_user))
|
||||||
await service.delete_user(
|
await service.delete_user(
|
||||||
user_id,
|
user_id,
|
||||||
soft_delete=soft_delete,
|
soft_delete=soft_delete,
|
||||||
@@ -472,6 +473,6 @@ async def change_user_password(
|
|||||||
Cambia la contraseña de un usuario a través del Hub
|
Cambia la contraseña de un usuario a través del Hub
|
||||||
"""
|
"""
|
||||||
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["user.update"])
|
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["user.update"])
|
||||||
service = UserService(db, tenant_id, company_id)
|
service = UserService(db, tenant_id, company_id, is_hub_admin=is_hub_admin(current_user))
|
||||||
await service.change_password(user_id, data.password, data.temporary)
|
await service.change_password(user_id, data.password, data.temporary)
|
||||||
return {"message": "Password changed successfully"}
|
return {"message": "Password changed successfully"}
|
||||||
|
|||||||
@@ -94,10 +94,11 @@ def _normalize_user(
|
|||||||
class UserService:
|
class UserService:
|
||||||
"""Servicio para gestionar usuarios vía Hub"""
|
"""Servicio para gestionar usuarios vía Hub"""
|
||||||
|
|
||||||
def __init__(self, db: Session, tenant_id: int = None, company_id: int = None):
|
def __init__(self, db: Session, tenant_id: int = None, company_id: int = None, *, is_hub_admin: bool = False):
|
||||||
self.db = db
|
self.db = db
|
||||||
self.tenant_id = tenant_id
|
self.tenant_id = tenant_id
|
||||||
self.company_id = company_id
|
self.company_id = company_id
|
||||||
|
self.is_hub_admin = is_hub_admin
|
||||||
|
|
||||||
def _get_license(self) -> License:
|
def _get_license(self) -> License:
|
||||||
"""Obtiene la licencia del tenant actual"""
|
"""Obtiene la licencia del tenant actual"""
|
||||||
@@ -125,6 +126,8 @@ class UserService:
|
|||||||
|
|
||||||
def _check_user_limit(self) -> None:
|
def _check_user_limit(self) -> None:
|
||||||
"""Verifica si se puede crear un nuevo usuario según la licencia"""
|
"""Verifica si se puede crear un nuevo usuario según la licencia"""
|
||||||
|
if self.is_hub_admin:
|
||||||
|
return
|
||||||
license = self._get_license()
|
license = self._get_license()
|
||||||
|
|
||||||
# Contar usuarios activos del tenant
|
# Contar usuarios activos del tenant
|
||||||
@@ -595,8 +598,10 @@ class UserService:
|
|||||||
)
|
)
|
||||||
if lic_resp.status_code == 200:
|
if lic_resp.status_code == 200:
|
||||||
lic_body = lic_resp.json()
|
lic_body = lic_resp.json()
|
||||||
if lic_body.get("valid") and lic_body.get("max_users") is not None:
|
if lic_body.get("valid"):
|
||||||
max_users_allowed = int(lic_body["max_users"])
|
raw_max = lic_body.get("max_users")
|
||||||
|
# max_users=null → hub_admin sin cuota; usamos 0 como sentinel
|
||||||
|
max_users_allowed = int(raw_max) if raw_max is not None else 0
|
||||||
hub_max_ok = True
|
hub_max_ok = True
|
||||||
|
|
||||||
users_resp = client.get(
|
users_resp = client.get(
|
||||||
|
|||||||
@@ -464,6 +464,51 @@ def resolve_effective_tenant_id_from_user(current_user: Dict[str, Any]) -> Optio
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def is_hub_admin(current_user: Dict[str, Any]) -> bool:
|
||||||
|
"""True si el usuario tiene el rol hub_admin (super-admin del Hub con acceso global)."""
|
||||||
|
roles = current_user.get("roles")
|
||||||
|
if isinstance(roles, list) and "hub_admin" in roles:
|
||||||
|
return True
|
||||||
|
return bool(current_user.get("is_hub_admin"))
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_tenant_id_required(
|
||||||
|
current_user: Dict[str, Any],
|
||||||
|
db: Optional["Session"] = None,
|
||||||
|
company_id: Optional[int] = None,
|
||||||
|
) -> Optional[int]:
|
||||||
|
"""
|
||||||
|
Retorna el tenant_id efectivo o lanza 400.
|
||||||
|
Hub admin sin tenant_id en token: resuelve desde la empresa si company_id está disponible,
|
||||||
|
o retorna None como sentinel de acceso global (sin filtro de tenant).
|
||||||
|
"""
|
||||||
|
tid = get_tenant_from_token(current_user)
|
||||||
|
if tid is not None:
|
||||||
|
return int(tid)
|
||||||
|
raw = current_user.get("tenant_id")
|
||||||
|
if isinstance(raw, list) and raw:
|
||||||
|
raw = raw[0]
|
||||||
|
if raw is not None:
|
||||||
|
try:
|
||||||
|
return int(raw)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid tenant ID in token")
|
||||||
|
|
||||||
|
if is_hub_admin(current_user):
|
||||||
|
if db is not None and company_id is not None:
|
||||||
|
try:
|
||||||
|
from api.v1.modules.a76.general_catalogs.company.models import Company
|
||||||
|
company = db.query(Company).filter(Company.id == company_id).first()
|
||||||
|
if company and company.tenant_id:
|
||||||
|
return int(company.tenant_id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# Sin company_id disponible: sentinel None → el servicio no filtra por tenant
|
||||||
|
return None
|
||||||
|
|
||||||
|
raise HTTPException(status_code=400, detail="Tenant ID not found in user data")
|
||||||
|
|
||||||
|
|
||||||
def user_has_app_company_membership(
|
def user_has_app_company_membership(
|
||||||
db: Session, user_id: str, company_id: int
|
db: Session, user_id: str, company_id: int
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@@ -652,9 +697,9 @@ def validate_access_to_resource(
|
|||||||
|
|
||||||
tenant_id = resolve_effective_tenant_id_from_user(current_user)
|
tenant_id = resolve_effective_tenant_id_from_user(current_user)
|
||||||
|
|
||||||
# Admin global Keycloak / master: lista ``roles`` del Hub (/auth/me), con fallback JWT.
|
# Admin global Keycloak / master, o hub_admin del Hub.
|
||||||
all_user_roles = collect_user_role_names(current_user)
|
all_user_roles = collect_user_role_names(current_user)
|
||||||
is_keycloak_admin = "admin" in all_user_roles
|
is_keycloak_admin = "admin" in all_user_roles or is_hub_admin(current_user)
|
||||||
|
|
||||||
# 🚪 EXCEPCIÓN ESPECIAL: Si es el endpoint /me, permitimos el paso para el Bootstrap
|
# 🚪 EXCEPCIÓN ESPECIAL: Si es el endpoint /me, permitimos el paso para el Bootstrap
|
||||||
# Detectamos si no se requieren permisos (típico de /me)
|
# Detectamos si no se requieren permisos (típico de /me)
|
||||||
|
|||||||
Reference in New Issue
Block a user