Merge pull request 'feature/pedimentos' (#11) from feature/pedimentos into development
Reviewed-on: ADUANASOFT/anexo76#11
This commit is contained in:
@@ -49,7 +49,10 @@ class AuthService:
|
||||
try:
|
||||
# Verificar que el tenant existe
|
||||
from api.v1.modules.a76.tenants.service import TenantService
|
||||
from api.v1.modules.a76.user_tenant.service import UserTenantService
|
||||
|
||||
tenant_service = TenantService(self.db)
|
||||
user_tenant_service = UserTenantService(self.db)
|
||||
tenant = tenant_service.get_tenant_by_slug(login_data.tenant_slug)
|
||||
|
||||
if not tenant:
|
||||
@@ -73,7 +76,59 @@ class AuthService:
|
||||
grant_type=["password"]
|
||||
)
|
||||
|
||||
logger.info(f"User logged in: {login_data.username} (tenant: {tenant.slug})")
|
||||
# Obtener información del usuario y verificar acceso al tenant
|
||||
user_info = keycloak_client.userinfo(token_response["access_token"])
|
||||
user_id = user_info.get("sub")
|
||||
|
||||
if user_id:
|
||||
# Verificar si el usuario tiene acceso a este tenant
|
||||
has_access = user_tenant_service.user_has_access_to_tenant(user_id, tenant.id)
|
||||
|
||||
if not has_access:
|
||||
logger.warning(f"User {user_id} tried to access tenant {tenant.id} without permission")
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You don't have access to this tenant"
|
||||
)
|
||||
|
||||
# Actualizar el tenant_id del usuario en Keycloak basado en el slug usado
|
||||
try:
|
||||
# Crear instancia de KeycloakAdmin para actualizar atributos
|
||||
keycloak_admin = KeycloakAdmin(
|
||||
server_url=settings.KEYCLOAK_SERVER_URL,
|
||||
username=settings.KEYCLOAK_ADMIN_USERNAME,
|
||||
password=settings.KEYCLOAK_ADMIN_PASSWORD,
|
||||
realm_name=tenant.keycloak_realm,
|
||||
user_realm_name="master",
|
||||
verify=True
|
||||
)
|
||||
|
||||
# Obtener los datos actuales del usuario para no sobrescribirlos
|
||||
current_user = keycloak_admin.get_user(user_id)
|
||||
|
||||
# Obtener los atributos actuales o crear un dict vacío
|
||||
current_attributes = current_user.get("attributes", {})
|
||||
|
||||
# Actualizar solo los atributos de tenant
|
||||
current_attributes["tenant_id"] = [str(tenant.id)]
|
||||
current_attributes["tenant_slug"] = [tenant.slug]
|
||||
|
||||
# Actualizar el usuario enviando TODOS los campos para evitar que se borren
|
||||
update_payload = {
|
||||
"email": current_user.get("email"),
|
||||
"firstName": current_user.get("firstName"),
|
||||
"lastName": current_user.get("lastName"),
|
||||
"enabled": current_user.get("enabled", True),
|
||||
"emailVerified": current_user.get("emailVerified", False),
|
||||
"attributes": current_attributes
|
||||
}
|
||||
|
||||
keycloak_admin.update_user(user_id=user_id, payload=update_payload)
|
||||
logger.info(f"Updated tenant_id={tenant.id} for user {login_data.username}")
|
||||
|
||||
except Exception as e:
|
||||
# No queremos que falle el login si no se puede actualizar el atributo
|
||||
logger.warning(f"Could not update tenant_id attribute: {str(e)}")
|
||||
|
||||
return TokenResponseDTO(
|
||||
access_token=token_response["access_token"],
|
||||
@@ -249,6 +304,29 @@ class AuthService:
|
||||
# El rol 'user' no existe, no es un error crítico
|
||||
logger.warning(f"Could not assign 'user' role: {str(e)}")
|
||||
|
||||
# Agregar el usuario al tenant en la base de datos
|
||||
try:
|
||||
from api.v1.modules.a76.user_tenant.service import UserTenantService
|
||||
user_tenant_service = UserTenantService(self.db)
|
||||
user_tenant_service.add_user_to_tenant(
|
||||
keycloak_user_id=user_id,
|
||||
tenant_id=tenant.id,
|
||||
role="user" # Rol por defecto
|
||||
)
|
||||
logger.info(f"Added user {user_id} to tenant {tenant.id} in database")
|
||||
except Exception as e:
|
||||
# Si falla, hacer rollback del usuario en Keycloak
|
||||
logger.error(f"Failed to add user to tenant in database: {str(e)}")
|
||||
try:
|
||||
keycloak_admin.delete_user(user_id)
|
||||
logger.info(f"Rolled back user creation in Keycloak")
|
||||
except:
|
||||
pass
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Failed to register user in database"
|
||||
)
|
||||
|
||||
logger.info(f"User registered: {register_data.username} (tenant: {tenant.slug}, user_id: {user_id})")
|
||||
|
||||
return RegisterResponseDTO(
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
from enum import IntEnum
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
from typing import Optional
|
||||
from decimal import Decimal
|
||||
from datetime import datetime
|
||||
|
||||
class OperationType(IntEnum):
|
||||
EXPORTACION = 1
|
||||
IMPORTACION = 2
|
||||
|
||||
class PedimentosBase(BaseModel):
|
||||
"""Base schema for Pedimentos"""
|
||||
@@ -29,16 +33,16 @@ class PedimentosCreate(PedimentosBase):
|
||||
|
||||
class PedimentosUpdate(BaseModel):
|
||||
"""Schema for updating a Pedimento"""
|
||||
year: Optional[str] = Field(None, max_length=2)
|
||||
customs_office: Optional[str] = Field(None, max_length=2)
|
||||
license: Optional[str] = Field(None, max_length=4)
|
||||
pedimento_number: Optional[str] = Field(None, max_length=7)
|
||||
client_id: Optional[int] = None
|
||||
operation_type: Optional[int] = None
|
||||
pedimento_type: Optional[int] = None
|
||||
pedimento_code: Optional[str] = Field(None, max_length=2)
|
||||
regime: Optional[str] = Field(None, max_length=3)
|
||||
status: Optional[str] = Field(None, max_length=30)
|
||||
year: Optional[str] = Field(..., max_length=2)
|
||||
customs_office: Optional[str] = Field(..., max_length=2)
|
||||
license: Optional[str] = Field(..., max_length=4)
|
||||
pedimento_number: Optional[str] = Field(..., max_length=7)
|
||||
client_id: Optional[int]
|
||||
operation_type: Optional[OperationType]
|
||||
pedimento_type: Optional[int]
|
||||
pedimento_code: Optional[str] = Field(..., max_length=2)
|
||||
regime: Optional[str] = Field(..., max_length=3)
|
||||
status: Optional[str] = Field(..., max_length=30)
|
||||
usd_value: Optional[Decimal] = None
|
||||
paid_price: Optional[Decimal] = None
|
||||
gross_weight: Optional[Decimal] = None
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from typing import TYPE_CHECKING
|
||||
from decimal import Decimal
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from sqlalchemy import DateTime, ForeignKeyConstraint, Index, Integer, Numeric, PrimaryKeyConstraint, String, func, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.orm.base import Mapped
|
||||
from datetime import datetime
|
||||
from enum import IntEnum
|
||||
from core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -37,22 +39,22 @@ class Pedimentos(Base):
|
||||
{'schema': 'a76'}
|
||||
)
|
||||
|
||||
id = mapped_column(Integer)
|
||||
tenant_id = mapped_column(Integer, nullable=False, index=True)
|
||||
year = mapped_column(String(2))
|
||||
customs_office = mapped_column(String(2))
|
||||
license = mapped_column(String(4))
|
||||
pedimento_number = mapped_column(String(7))
|
||||
client_id = mapped_column(Integer)
|
||||
operation_type = mapped_column(Integer)
|
||||
pedimento_type = mapped_column(Integer)
|
||||
id: Mapped[int] = mapped_column(Integer)
|
||||
tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
|
||||
year: Mapped[str] = mapped_column(String(2))
|
||||
customs_office: Mapped[str] = mapped_column(String(2))
|
||||
license: Mapped[str] = mapped_column(String(4))
|
||||
pedimento_number: Mapped[str] = mapped_column(String(7))
|
||||
client_id: Mapped[int] = mapped_column(Integer)
|
||||
operation_type: Mapped[int] = mapped_column(Integer)
|
||||
pedimento_type: Mapped[int] = mapped_column(Integer)
|
||||
pedimento_code = mapped_column(String(2))
|
||||
regime = mapped_column(String(3))
|
||||
status = mapped_column(String(30))
|
||||
usd_value = mapped_column(Numeric(17, 6))
|
||||
paid_price = mapped_column(Numeric(17, 6))
|
||||
gross_weight = mapped_column(Numeric(19, 3))
|
||||
exchange_rate = mapped_column(Numeric(9, 5))
|
||||
regime: Mapped[str] = mapped_column(String(3))
|
||||
status: Mapped[str] = mapped_column(String(30))
|
||||
usd_value: Mapped[Optional[Decimal]] = mapped_column(Numeric(17, 6))
|
||||
paid_price: Mapped[Optional[Decimal]] = mapped_column(Numeric(17, 6))
|
||||
gross_weight: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 3))
|
||||
exchange_rate: Mapped[Optional[Decimal]] = mapped_column(Numeric(9, 5))
|
||||
|
||||
# Timestamps
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now())
|
||||
|
||||
@@ -7,6 +7,7 @@ from fastapi import APIRouter
|
||||
# Importar routers de módulos
|
||||
from .auth import router as auth_router
|
||||
from .tenants import router as tenants_router
|
||||
from .user_tenant.routes import router as user_tenant_router
|
||||
from .licenses import router as licenses_router
|
||||
from .pedmientos.router import router as pedimentos_router
|
||||
from .client_and_provider import router as client_and_provider_router
|
||||
@@ -26,6 +27,7 @@ router = APIRouter()
|
||||
# Registrar módulos
|
||||
router.include_router(auth_router)
|
||||
router.include_router(tenants_router, prefix="/a76", tags=["a76 / tenants"])
|
||||
router.include_router(user_tenant_router, prefix="/a76", tags=["a76 / user-tenants"])
|
||||
router.include_router(licenses_router, prefix="/a76", tags=["a76 / licenses"])
|
||||
router.include_router(pedimentos_router, prefix="/a76")
|
||||
router.include_router(client_and_provider_router, prefix="/a76", tags=["a76 / clients and providers"])
|
||||
|
||||
@@ -3,11 +3,15 @@ Modelos ORM para gestión de tenants
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, Enum as SQLEnum
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from datetime import datetime
|
||||
from typing import List, TYPE_CHECKING
|
||||
from core.database import Base
|
||||
import enum
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from api.v1.modules.a76.user_tenant.models import UserTenant
|
||||
|
||||
|
||||
class TenantType(enum.Enum):
|
||||
"""Tipo de tenant según tamaño y necesidades"""
|
||||
@@ -49,5 +53,8 @@ class Tenant(Base):
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now())
|
||||
deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
|
||||
# Relación con UserTenant
|
||||
user_relations: Mapped[List["UserTenant"]] = relationship("UserTenant", back_populates="tenant")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Tenant(id={self.id}, name={self.name}, type={self.type.value})>"
|
||||
|
||||
59
backend/api/v1/modules/a76/user_tenant/dto.py
Normal file
59
backend/api/v1/modules/a76/user_tenant/dto.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
DTOs para gestión de relaciones usuario-tenant
|
||||
"""
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class AddUserToTenantRequestDTO(BaseModel):
|
||||
"""Request para agregar un usuario a un tenant"""
|
||||
keycloak_user_id: str = Field(..., description="ID del usuario en Keycloak")
|
||||
tenant_id: int = Field(..., description="ID del tenant")
|
||||
role: Optional[str] = Field(None, description="Rol del usuario en el tenant")
|
||||
|
||||
|
||||
class RemoveUserFromTenantRequestDTO(BaseModel):
|
||||
"""Request para eliminar un usuario de un tenant"""
|
||||
keycloak_user_id: str = Field(..., description="ID del usuario en Keycloak")
|
||||
tenant_id: int = Field(..., description="ID del tenant")
|
||||
soft_delete: bool = Field(True, description="Si True, desactiva. Si False, elimina")
|
||||
|
||||
|
||||
class UpdateUserRoleRequestDTO(BaseModel):
|
||||
"""Request para actualizar el rol de un usuario en un tenant"""
|
||||
keycloak_user_id: str = Field(..., description="ID del usuario en Keycloak")
|
||||
tenant_id: int = Field(..., description="ID del tenant")
|
||||
role: str = Field(..., description="Nuevo rol del usuario")
|
||||
|
||||
|
||||
class UserTenantResponseDTO(BaseModel):
|
||||
"""Response con información de relación usuario-tenant"""
|
||||
id: int
|
||||
keycloak_user_id: str
|
||||
tenant_id: int
|
||||
is_active: bool
|
||||
role: Optional[str]
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class TenantBasicInfoDTO(BaseModel):
|
||||
"""Información básica de un tenant"""
|
||||
id: int
|
||||
name: str
|
||||
slug: str
|
||||
is_active: bool
|
||||
keycloak_realm: str
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class UserTenantsResponseDTO(BaseModel):
|
||||
"""Response con los tenants de un usuario"""
|
||||
keycloak_user_id: str
|
||||
tenants: list[TenantBasicInfoDTO]
|
||||
49
backend/api/v1/modules/a76/user_tenant/models.py
Normal file
49
backend/api/v1/modules/a76/user_tenant/models.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
Modelo de relación entre usuarios (Keycloak) y tenants
|
||||
"""
|
||||
from sqlalchemy import Integer, String, DateTime, Boolean, UniqueConstraint, ForeignKeyConstraint
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from datetime import datetime
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from api.v1.modules.a76.tenants.models import Tenant
|
||||
|
||||
class UserTenant(Base):
|
||||
"""
|
||||
Relación muchos-a-muchos entre usuarios de Keycloak y tenants
|
||||
|
||||
Un usuario puede pertenecer a múltiples tenants
|
||||
Un tenant puede tener múltiples usuarios
|
||||
"""
|
||||
__tablename__ = "user_tenants"
|
||||
__table_args__ = (
|
||||
ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']),
|
||||
UniqueConstraint('keycloak_user_id', 'tenant_id', name='uq_user_tenant'),
|
||||
{"schema": "a76"}
|
||||
)
|
||||
|
||||
# Primary Key
|
||||
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
||||
|
||||
# ID del usuario en Keycloak (UUID string)
|
||||
keycloak_user_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
|
||||
|
||||
# ID del tenant
|
||||
tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
|
||||
|
||||
# Estado de la relación
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
|
||||
# Información adicional - Rol del usuario en este tenant (opcional)
|
||||
role: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
|
||||
|
||||
# Timestamps
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.now(), onupdate=func.now())
|
||||
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
|
||||
# Relación con Tenant
|
||||
tenant: Mapped["Tenant"] = relationship("Tenant", back_populates="user_relations")
|
||||
147
backend/api/v1/modules/a76/user_tenant/routes.py
Normal file
147
backend/api/v1/modules/a76/user_tenant/routes.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""
|
||||
Rutas para gestión de relaciones usuario-tenant
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user
|
||||
from .service import UserTenantService
|
||||
from .dto import (
|
||||
AddUserToTenantRequestDTO,
|
||||
RemoveUserFromTenantRequestDTO,
|
||||
UpdateUserRoleRequestDTO,
|
||||
UserTenantResponseDTO,
|
||||
UserTenantsResponseDTO,
|
||||
TenantBasicInfoDTO
|
||||
)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/user-tenants",
|
||||
tags=["User-Tenant Relations"]
|
||||
)
|
||||
|
||||
|
||||
@router.post("/add", response_model=UserTenantResponseDTO)
|
||||
def add_user_to_tenant(
|
||||
data: AddUserToTenantRequestDTO,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Agrega un usuario a un tenant
|
||||
|
||||
Requiere permisos de administrador
|
||||
"""
|
||||
service = UserTenantService(db)
|
||||
result = service.add_user_to_tenant(
|
||||
keycloak_user_id=data.keycloak_user_id,
|
||||
tenant_id=data.tenant_id,
|
||||
role=data.role
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/remove")
|
||||
def remove_user_from_tenant(
|
||||
data: RemoveUserFromTenantRequestDTO,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Elimina un usuario de un tenant
|
||||
|
||||
Requiere permisos de administrador
|
||||
"""
|
||||
service = UserTenantService(db)
|
||||
service.remove_user_from_tenant(
|
||||
keycloak_user_id=data.keycloak_user_id,
|
||||
tenant_id=data.tenant_id,
|
||||
soft_delete=data.soft_delete
|
||||
)
|
||||
return {"message": "User removed from tenant successfully"}
|
||||
|
||||
|
||||
@router.put("/update-role", response_model=UserTenantResponseDTO)
|
||||
def update_user_role(
|
||||
data: UpdateUserRoleRequestDTO,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Actualiza el rol de un usuario en un tenant
|
||||
|
||||
Requiere permisos de administrador
|
||||
"""
|
||||
service = UserTenantService(db)
|
||||
result = service.update_user_role_in_tenant(
|
||||
keycloak_user_id=data.keycloak_user_id,
|
||||
tenant_id=data.tenant_id,
|
||||
role=data.role
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/user/{keycloak_user_id}", response_model=UserTenantsResponseDTO)
|
||||
def get_user_tenants(
|
||||
keycloak_user_id: str,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Obtiene todos los tenants a los que tiene acceso un usuario
|
||||
|
||||
Los usuarios solo pueden ver sus propios tenants, a menos que sean admin
|
||||
"""
|
||||
# Verificar que el usuario solo pueda ver sus propios tenants (excepto admin)
|
||||
if current_user.get("sub") != keycloak_user_id:
|
||||
# TODO: Verificar si es admin
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You can only view your own tenants"
|
||||
)
|
||||
|
||||
service = UserTenantService(db)
|
||||
tenants = service.get_user_tenants(keycloak_user_id)
|
||||
|
||||
return UserTenantsResponseDTO(
|
||||
keycloak_user_id=keycloak_user_id,
|
||||
tenants=[TenantBasicInfoDTO.model_validate(t) for t in tenants]
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tenant/{tenant_id}", response_model=List[UserTenantResponseDTO])
|
||||
def get_tenant_users(
|
||||
tenant_id: int,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Obtiene todos los usuarios que tienen acceso a un tenant
|
||||
|
||||
Requiere permisos de administrador del tenant
|
||||
"""
|
||||
service = UserTenantService(db)
|
||||
user_tenants = service.get_tenant_users(tenant_id)
|
||||
return user_tenants
|
||||
|
||||
|
||||
@router.get("/check-access/{keycloak_user_id}/{tenant_id}")
|
||||
def check_user_access(
|
||||
keycloak_user_id: str,
|
||||
tenant_id: int,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Verifica si un usuario tiene acceso a un tenant
|
||||
"""
|
||||
service = UserTenantService(db)
|
||||
has_access = service.user_has_access_to_tenant(keycloak_user_id, tenant_id)
|
||||
|
||||
return {
|
||||
"keycloak_user_id": keycloak_user_id,
|
||||
"tenant_id": tenant_id,
|
||||
"has_access": has_access
|
||||
}
|
||||
228
backend/api/v1/modules/a76/user_tenant/service.py
Normal file
228
backend/api/v1/modules/a76/user_tenant/service.py
Normal file
@@ -0,0 +1,228 @@
|
||||
"""
|
||||
Servicio para gestionar relaciones entre usuarios y tenants
|
||||
"""
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_
|
||||
from typing import List, Optional
|
||||
from fastapi import HTTPException
|
||||
import logging
|
||||
|
||||
from .models import UserTenant
|
||||
from ..tenants.models import Tenant
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UserTenantService:
|
||||
"""Servicio para gestionar acceso de usuarios a tenants"""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def add_user_to_tenant(
|
||||
self,
|
||||
keycloak_user_id: str,
|
||||
tenant_id: int,
|
||||
role: Optional[str] = None
|
||||
) -> UserTenant:
|
||||
"""
|
||||
Agrega un usuario a un tenant
|
||||
|
||||
Args:
|
||||
keycloak_user_id: ID del usuario en Keycloak
|
||||
tenant_id: ID del tenant
|
||||
role: Rol opcional del usuario en este tenant
|
||||
|
||||
Returns:
|
||||
UserTenant creado
|
||||
"""
|
||||
# Verificar que el tenant existe
|
||||
tenant = self.db.query(Tenant).filter(Tenant.id == tenant_id).first()
|
||||
if not tenant:
|
||||
raise HTTPException(status_code=404, detail="Tenant not found")
|
||||
|
||||
# Verificar si la relación ya existe
|
||||
existing = self.db.query(UserTenant).filter(
|
||||
and_(
|
||||
UserTenant.keycloak_user_id == keycloak_user_id,
|
||||
UserTenant.tenant_id == tenant_id
|
||||
)
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
# Si existe pero está inactiva, reactivarla
|
||||
if not existing.is_active:
|
||||
existing.is_active = True
|
||||
existing.role = role
|
||||
self.db.commit()
|
||||
self.db.refresh(existing)
|
||||
logger.info(f"Reactivated user {keycloak_user_id} in tenant {tenant_id}")
|
||||
return existing
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="User already has access to this tenant"
|
||||
)
|
||||
|
||||
# Crear nueva relación
|
||||
user_tenant = UserTenant(
|
||||
keycloak_user_id=keycloak_user_id,
|
||||
tenant_id=tenant_id,
|
||||
role=role,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
self.db.add(user_tenant)
|
||||
self.db.commit()
|
||||
self.db.refresh(user_tenant)
|
||||
|
||||
logger.info(f"Added user {keycloak_user_id} to tenant {tenant_id}")
|
||||
return user_tenant
|
||||
|
||||
def remove_user_from_tenant(
|
||||
self,
|
||||
keycloak_user_id: str,
|
||||
tenant_id: int,
|
||||
soft_delete: bool = True
|
||||
) -> bool:
|
||||
"""
|
||||
Elimina un usuario de un tenant
|
||||
|
||||
Args:
|
||||
keycloak_user_id: ID del usuario en Keycloak
|
||||
tenant_id: ID del tenant
|
||||
soft_delete: Si True, solo marca como inactivo. Si False, elimina físicamente
|
||||
|
||||
Returns:
|
||||
True si se eliminó correctamente
|
||||
"""
|
||||
user_tenant = self.db.query(UserTenant).filter(
|
||||
and_(
|
||||
UserTenant.keycloak_user_id == keycloak_user_id,
|
||||
UserTenant.tenant_id == tenant_id
|
||||
)
|
||||
).first()
|
||||
|
||||
if not user_tenant:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="User-tenant relationship not found"
|
||||
)
|
||||
|
||||
if soft_delete:
|
||||
user_tenant.is_active = False
|
||||
self.db.commit()
|
||||
logger.info(f"Deactivated user {keycloak_user_id} from tenant {tenant_id}")
|
||||
else:
|
||||
self.db.delete(user_tenant)
|
||||
self.db.commit()
|
||||
logger.info(f"Deleted user {keycloak_user_id} from tenant {tenant_id}")
|
||||
|
||||
return True
|
||||
|
||||
def get_user_tenants(self, keycloak_user_id: str) -> List[Tenant]:
|
||||
"""
|
||||
Obtiene todos los tenants a los que tiene acceso un usuario
|
||||
|
||||
Args:
|
||||
keycloak_user_id: ID del usuario en Keycloak
|
||||
|
||||
Returns:
|
||||
Lista de tenants
|
||||
"""
|
||||
user_tenants = self.db.query(UserTenant).filter(
|
||||
and_(
|
||||
UserTenant.keycloak_user_id == keycloak_user_id,
|
||||
UserTenant.is_active == True
|
||||
)
|
||||
).all()
|
||||
|
||||
tenant_ids = [ut.tenant_id for ut in user_tenants]
|
||||
|
||||
tenants = self.db.query(Tenant).filter(
|
||||
and_(
|
||||
Tenant.id.in_(tenant_ids),
|
||||
Tenant.is_active == True
|
||||
)
|
||||
).all()
|
||||
|
||||
return tenants
|
||||
|
||||
def get_tenant_users(self, tenant_id: int) -> List[UserTenant]:
|
||||
"""
|
||||
Obtiene todos los usuarios que tienen acceso a un tenant
|
||||
|
||||
Args:
|
||||
tenant_id: ID del tenant
|
||||
|
||||
Returns:
|
||||
Lista de relaciones UserTenant
|
||||
"""
|
||||
return self.db.query(UserTenant).filter(
|
||||
and_(
|
||||
UserTenant.tenant_id == tenant_id,
|
||||
UserTenant.is_active == True
|
||||
)
|
||||
).all()
|
||||
|
||||
def user_has_access_to_tenant(
|
||||
self,
|
||||
keycloak_user_id: str,
|
||||
tenant_id: int
|
||||
) -> bool:
|
||||
"""
|
||||
Verifica si un usuario tiene acceso a un tenant
|
||||
|
||||
Args:
|
||||
keycloak_user_id: ID del usuario en Keycloak
|
||||
tenant_id: ID del tenant
|
||||
|
||||
Returns:
|
||||
True si tiene acceso, False en caso contrario
|
||||
"""
|
||||
user_tenant = self.db.query(UserTenant).filter(
|
||||
and_(
|
||||
UserTenant.keycloak_user_id == keycloak_user_id,
|
||||
UserTenant.tenant_id == tenant_id,
|
||||
UserTenant.is_active == True
|
||||
)
|
||||
).first()
|
||||
|
||||
return user_tenant is not None
|
||||
|
||||
def update_user_role_in_tenant(
|
||||
self,
|
||||
keycloak_user_id: str,
|
||||
tenant_id: int,
|
||||
role: str
|
||||
) -> UserTenant:
|
||||
"""
|
||||
Actualiza el rol de un usuario en un tenant
|
||||
|
||||
Args:
|
||||
keycloak_user_id: ID del usuario en Keycloak
|
||||
tenant_id: ID del tenant
|
||||
role: Nuevo rol
|
||||
|
||||
Returns:
|
||||
UserTenant actualizado
|
||||
"""
|
||||
user_tenant = self.db.query(UserTenant).filter(
|
||||
and_(
|
||||
UserTenant.keycloak_user_id == keycloak_user_id,
|
||||
UserTenant.tenant_id == tenant_id
|
||||
)
|
||||
).first()
|
||||
|
||||
if not user_tenant:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="User-tenant relationship not found"
|
||||
)
|
||||
|
||||
user_tenant.role = role
|
||||
self.db.commit()
|
||||
self.db.refresh(user_tenant)
|
||||
|
||||
logger.info(f"Updated role for user {keycloak_user_id} in tenant {tenant_id} to {role}")
|
||||
return user_tenant
|
||||
Reference in New Issue
Block a user