Merge pull request 'feature/hub-admin-global-access' (#410) from feature/hub-admin-global-access into development
Reviewed-on: ADUANASOFT/anexo76#410
This commit is contained in:
@@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
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 .schemas import TaskCatalogsResponse, TaskRunDetail, TaskRunListItem, TaskRunsResponse, TaskSyncRequest
|
||||
@@ -58,9 +58,7 @@ def list_tasks(
|
||||
current_user: dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_core_db),
|
||||
):
|
||||
tenant_id = get_tenant_from_token(current_user)
|
||||
if not tenant_id:
|
||||
raise HTTPException(status_code=400, detail="Tenant ID not found in token")
|
||||
tenant_id = resolve_tenant_id_required(current_user)
|
||||
|
||||
tracker = TaskTrackerService(db)
|
||||
if sync_active:
|
||||
@@ -93,11 +91,12 @@ def get_task_detail(
|
||||
current_user: dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_core_db),
|
||||
):
|
||||
tenant_id = get_tenant_from_token(current_user)
|
||||
if not tenant_id:
|
||||
raise HTTPException(status_code=400, detail="Tenant ID not found in token")
|
||||
tenant_id = resolve_tenant_id_required(current_user)
|
||||
|
||||
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:
|
||||
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),
|
||||
db: Session = Depends(get_core_db),
|
||||
):
|
||||
tenant_id = get_tenant_from_token(current_user)
|
||||
if not tenant_id:
|
||||
raise HTTPException(status_code=400, detail="Tenant ID not found in token")
|
||||
tenant_id = resolve_tenant_id_required(current_user)
|
||||
tracker = TaskTrackerService(db)
|
||||
updated = tracker.sync_active_tasks(tenant_id=tenant_id, task_ids=body.task_ids)
|
||||
return {"updated": updated}
|
||||
@@ -133,15 +130,18 @@ def get_catalogs(
|
||||
current_user: dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_core_db),
|
||||
):
|
||||
tenant_id = get_tenant_from_token(current_user)
|
||||
if not tenant_id:
|
||||
raise HTTPException(status_code=400, detail="Tenant ID not found in token")
|
||||
tenant_id = resolve_tenant_id_required(current_user)
|
||||
|
||||
groups = (
|
||||
db.query(TaskRun.task_group).filter(TaskRun.tenant_id == tenant_id).distinct().order_by(TaskRun.task_group).all()
|
||||
)
|
||||
names = db.query(TaskRun.task_name).filter(TaskRun.tenant_id == tenant_id).distinct().order_by(TaskRun.task_name).all()
|
||||
statuses = db.query(TaskRun.status).filter(TaskRun.tenant_id == tenant_id).distinct().order_by(TaskRun.status).all()
|
||||
groups_q = db.query(TaskRun.task_group)
|
||||
names_q = db.query(TaskRun.task_name)
|
||||
statuses_q = db.query(TaskRun.status)
|
||||
if tenant_id is not None:
|
||||
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(
|
||||
task_groups=[g[0] for g in groups if g[0]],
|
||||
task_names=[n[0] for n in names if n[0]],
|
||||
|
||||
@@ -193,10 +193,12 @@ class TaskTrackerService:
|
||||
self.db.refresh(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(
|
||||
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:
|
||||
query = query.filter(TaskRun.task_id.in_(task_ids))
|
||||
rows = query.limit(200).all()
|
||||
@@ -207,7 +209,7 @@ class TaskTrackerService:
|
||||
def list_tasks(
|
||||
self,
|
||||
*,
|
||||
tenant_id: int,
|
||||
tenant_id: int | None,
|
||||
page: int,
|
||||
page_size: int,
|
||||
status: list[str] | None = None,
|
||||
@@ -217,7 +219,9 @@ class TaskTrackerService:
|
||||
search: str | None = None,
|
||||
order: str = "desc",
|
||||
) -> 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:
|
||||
query = query.filter(TaskRun.status.in_(status))
|
||||
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.security import (
|
||||
get_current_user,
|
||||
is_hub_admin,
|
||||
resolve_hub_tenant_id_for_api,
|
||||
validate_access_to_resource,
|
||||
)
|
||||
@@ -51,7 +52,7 @@ async def get_user_statistics(
|
||||
Obtiene estadísticas de usuarios del tenant actual
|
||||
"""
|
||||
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 ""
|
||||
token = (
|
||||
auth_header[7:].strip()
|
||||
@@ -82,7 +83,7 @@ async def list_users(
|
||||
Lista todos los usuarios del tenant con paginación
|
||||
"""
|
||||
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 ""
|
||||
token = (
|
||||
auth_header[7:].strip()
|
||||
@@ -342,7 +343,7 @@ async def get_user_detail(
|
||||
Obtiene información detallada de un usuario específico
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
@@ -357,7 +358,7 @@ async def create_new_user(
|
||||
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"])
|
||||
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(
|
||||
email=data.email,
|
||||
username=data.username,
|
||||
@@ -383,7 +384,7 @@ async def update_user_detail(
|
||||
Actualiza información de un usuario
|
||||
"""
|
||||
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_id=user_id,
|
||||
first_name=data.first_name,
|
||||
@@ -413,7 +414,7 @@ async def get_user_tenant_count(
|
||||
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))
|
||||
count = service.get_user_tenant_count(user_id)
|
||||
return {"tenant_count": count}
|
||||
|
||||
@@ -449,7 +450,7 @@ async def delete_user_route(
|
||||
hub_tid = resolve_hub_tenant_id_for_api(
|
||||
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(
|
||||
user_id,
|
||||
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
|
||||
"""
|
||||
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)
|
||||
return {"message": "Password changed successfully"}
|
||||
|
||||
@@ -94,10 +94,11 @@ def _normalize_user(
|
||||
class UserService:
|
||||
"""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.tenant_id = tenant_id
|
||||
self.company_id = company_id
|
||||
self.is_hub_admin = is_hub_admin
|
||||
|
||||
def _get_license(self) -> License:
|
||||
"""Obtiene la licencia del tenant actual"""
|
||||
@@ -125,6 +126,8 @@ class UserService:
|
||||
|
||||
def _check_user_limit(self) -> None:
|
||||
"""Verifica si se puede crear un nuevo usuario según la licencia"""
|
||||
if self.is_hub_admin:
|
||||
return
|
||||
license = self._get_license()
|
||||
|
||||
# Contar usuarios activos del tenant
|
||||
@@ -139,6 +142,11 @@ class UserService:
|
||||
.scalar()
|
||||
)
|
||||
|
||||
# max_users=NULL en BD indica licencia sin cuota (ilimitada).
|
||||
# Comparar con None lanzaría TypeError — salida temprana explícita.
|
||||
if license.max_users is None:
|
||||
return
|
||||
|
||||
if active_users >= license.max_users:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
@@ -577,7 +585,7 @@ class UserService:
|
||||
con ``X-Tenant-Override``; activos desde users-with-info del Hub si hay token;
|
||||
inactivos y fallback de conteos en BD local.
|
||||
"""
|
||||
max_users_allowed = 0
|
||||
max_users_allowed: Optional[int] = None # None = sin cuota (hub_admin ilimitado)
|
||||
hub_max_ok = False
|
||||
active_users = 0
|
||||
active_from_hub = False
|
||||
@@ -595,8 +603,10 @@ class UserService:
|
||||
)
|
||||
if lic_resp.status_code == 200:
|
||||
lic_body = lic_resp.json()
|
||||
if lic_body.get("valid") and lic_body.get("max_users") is not None:
|
||||
max_users_allowed = int(lic_body["max_users"])
|
||||
if lic_body.get("valid"):
|
||||
raw_max = lic_body.get("max_users")
|
||||
# max_users=null → hub_admin sin cuota; None indica ilimitado
|
||||
max_users_allowed = int(raw_max) if raw_max is not None else None
|
||||
hub_max_ok = True
|
||||
|
||||
users_resp = client.get(
|
||||
@@ -642,9 +652,14 @@ class UserService:
|
||||
)
|
||||
|
||||
total_users = active_users + inactive_users
|
||||
users_available = max(0, max_users_allowed - active_users)
|
||||
# Cuando max_users_allowed es None la cuota es ilimitada (hub_admin)
|
||||
users_available = (
|
||||
max(0, max_users_allowed - active_users)
|
||||
if max_users_allowed is not None
|
||||
else None
|
||||
)
|
||||
usage_percentage = (
|
||||
(active_users / max_users_allowed * 100) if max_users_allowed > 0 else 0
|
||||
(active_users / max_users_allowed * 100) if max_users_allowed else 0.0
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user