feat: enhance user deletion process with access token and hub tenant ID propagation
This commit is contained in:
@@ -238,6 +238,7 @@ async def get_user_tenant_count(
|
||||
|
||||
@router.delete("/{user_id}")
|
||||
async def delete_user_route(
|
||||
request: Request,
|
||||
user_id: str,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
soft_delete: bool = Query(
|
||||
@@ -257,8 +258,23 @@ async def delete_user_route(
|
||||
scope='all': de todos los tenants en los que aparece el usuario.
|
||||
"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["user.delete"])
|
||||
auth_header = request.headers.get("Authorization") or ""
|
||||
token = (
|
||||
auth_header[7:].strip()
|
||||
if auth_header.lower().startswith("bearer ")
|
||||
else auth_header.strip()
|
||||
)
|
||||
hub_tid = resolve_hub_tenant_id_for_api(
|
||||
tenant_id, request.headers.get("X-Tenant-Override")
|
||||
)
|
||||
service = UserService(db, tenant_id, company_id)
|
||||
await service.delete_user(user_id, soft_delete=soft_delete, scope=scope)
|
||||
await service.delete_user(
|
||||
user_id,
|
||||
soft_delete=soft_delete,
|
||||
scope=scope,
|
||||
access_token=token or None,
|
||||
hub_tenant_id=hub_tid,
|
||||
)
|
||||
return {"message": "User deleted successfully"}
|
||||
|
||||
|
||||
|
||||
@@ -286,6 +286,10 @@ class UserService:
|
||||
kc = u.get("keycloak_user_id")
|
||||
if not kc:
|
||||
continue
|
||||
# Filtrar usuarios soft-deleted localmente (is_active=False en user_tenants local)
|
||||
local_ut_check = local_by_kc.get(kc)
|
||||
if local_ut_check is not None and not local_ut_check.is_active:
|
||||
continue
|
||||
if needle:
|
||||
blob = " ".join(
|
||||
[
|
||||
@@ -395,16 +399,24 @@ class UserService:
|
||||
return _normalize_user({"id": user_id}, user_tenant.role, user_tenant)
|
||||
|
||||
def get_user_tenant_count(self, user_id: str) -> int:
|
||||
"""Cuenta en cuántos tenants está registrado el usuario."""
|
||||
"""Cuenta en cuántos tenants activos está registrado el usuario."""
|
||||
return (
|
||||
self.db.query(func.count(UserTenant.id))
|
||||
.filter(UserTenant.keycloak_user_id == user_id)
|
||||
.filter(
|
||||
UserTenant.keycloak_user_id == user_id,
|
||||
UserTenant.is_active == True,
|
||||
)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
async def delete_user(
|
||||
self, user_id: str, soft_delete: bool = True, scope: str = "current"
|
||||
self,
|
||||
user_id: str,
|
||||
soft_delete: bool = True,
|
||||
scope: str = "current",
|
||||
access_token: Optional[str] = None,
|
||||
hub_tenant_id: Optional[int] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Elimina/Desactiva usuario.
|
||||
@@ -419,33 +431,81 @@ class UserService:
|
||||
)
|
||||
if not rows:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
if soft_delete:
|
||||
for row in rows:
|
||||
row.is_active = False
|
||||
self.db.commit()
|
||||
else:
|
||||
# TODO: Call Hub to delete from Keycloak
|
||||
for row in rows:
|
||||
self.db.delete(row)
|
||||
self.db.commit()
|
||||
now = datetime.utcnow()
|
||||
# Collect unique hub_tenant_ids to notify Hub for each tenant
|
||||
hub_tenant_ids = {row.tenant_id for row in rows}
|
||||
for row in rows:
|
||||
row.is_active = False
|
||||
if not soft_delete:
|
||||
row.deleted_at = now
|
||||
self.db.commit()
|
||||
# Propagate to Hub for every tenant the user belonged to
|
||||
if access_token:
|
||||
for tid in hub_tenant_ids:
|
||||
await self._hub_remove_user(user_id, tid, soft_delete, access_token)
|
||||
return
|
||||
|
||||
# scope == "current" (default)
|
||||
user_tenant = self.db.query(UserTenant).filter(
|
||||
and_(UserTenant.keycloak_user_id == user_id, UserTenant.tenant_id == self.tenant_id)
|
||||
and_(
|
||||
UserTenant.keycloak_user_id == user_id,
|
||||
UserTenant.tenant_id == self.tenant_id,
|
||||
UserTenant.company_id == self.company_id,
|
||||
)
|
||||
).first()
|
||||
|
||||
if not user_tenant:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
if soft_delete:
|
||||
user_tenant.is_active = False
|
||||
# No local record — user exists in Hub but not synced locally yet.
|
||||
# Create tombstone so user is filtered from future listings.
|
||||
user_tenant = UserTenant(
|
||||
keycloak_user_id=user_id,
|
||||
tenant_id=self.tenant_id,
|
||||
company_id=self.company_id,
|
||||
is_active=False,
|
||||
deleted_at=None if soft_delete else datetime.utcnow(),
|
||||
)
|
||||
self.db.add(user_tenant)
|
||||
self.db.commit()
|
||||
else:
|
||||
# TODO: Call Hub to delete from Keycloak
|
||||
self.db.delete(user_tenant)
|
||||
user_tenant.is_active = False
|
||||
if not soft_delete:
|
||||
user_tenant.deleted_at = datetime.utcnow()
|
||||
self.db.commit()
|
||||
|
||||
# Propagate to Hub
|
||||
if access_token and hub_tenant_id:
|
||||
await self._hub_remove_user(user_id, hub_tenant_id, soft_delete, access_token)
|
||||
|
||||
async def _hub_remove_user(
|
||||
self,
|
||||
user_id: str,
|
||||
hub_tenant_id: int,
|
||||
soft_delete: bool,
|
||||
access_token: str,
|
||||
) -> None:
|
||||
"""Calls Hub POST /api/v1/hub/user-tenants/remove to sync the deletion."""
|
||||
base = (settings.HUB_URL or "").rstrip("/")
|
||||
url = f"{base}/api/v1/hub/user-tenants/remove"
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
resp = await client.post(
|
||||
url,
|
||||
json={
|
||||
"keycloak_user_id": user_id,
|
||||
"tenant_id": hub_tenant_id,
|
||||
"soft_delete": soft_delete,
|
||||
},
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
)
|
||||
if resp.status_code >= 400:
|
||||
logger.warning(
|
||||
"Hub remove user-tenant returned %s for user %s tenant %s: %s",
|
||||
resp.status_code, user_id, hub_tenant_id, resp.text[:200],
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.error("Error calling Hub remove user-tenant: %s", exc)
|
||||
# Do not raise — local deletion already committed; Hub sync is best-effort.
|
||||
|
||||
async def change_password(self, user_id: str, password: str, temporary: bool = True) -> None:
|
||||
"""Cambia contraseña vía Hub"""
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user