feat: Implement multi-tenancy support in middleware and security layers
- Enhanced TenantMiddleware to validate tenant information from JWT tokens. - Added LicenseValidationMiddleware to check tenant licenses before processing requests. - Updated security utilities to extract tenant information from tokens and validate company access. - Introduced CompanyStore to manage active company state and handle company switching in the frontend. - Modified API routes to include company_id in requests for better resource management. - Improved logging and error handling throughout the middleware and API layers. - Updated frontend components to reflect changes in company management and selection. - Added new API route for fetching user's companies with proper authentication handling.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Core module - Configuración y utilidades centrales de la aplicación
|
||||
"""
|
||||
|
||||
from .config import settings
|
||||
from .database import (
|
||||
Base,
|
||||
@@ -8,14 +9,14 @@ from .database import (
|
||||
get_async_core_db,
|
||||
get_tenant_db,
|
||||
init_db,
|
||||
init_async_db
|
||||
init_async_db,
|
||||
)
|
||||
from .security import (
|
||||
verify_token,
|
||||
get_current_user,
|
||||
get_current_active_user,
|
||||
has_role,
|
||||
get_tenant_from_token
|
||||
get_tenant_from_token,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
"""
|
||||
Configuración centralizada de la aplicación usando Pydantic Settings
|
||||
"""
|
||||
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from typing import List
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Configuración de la aplicación"""
|
||||
|
||||
|
||||
# Application
|
||||
APP_NAME: str = "Anexo76"
|
||||
APP_VERSION: str = "1.0.0"
|
||||
DEBUG: bool = True
|
||||
ENVIRONMENT: str = "development"
|
||||
|
||||
|
||||
# Database - Core (Shared)
|
||||
CORE_DB_HOST: str = "postgres-a76"
|
||||
CORE_DB_PORT: int = 5432
|
||||
CORE_DB_NAME: str = "anexo76_core"
|
||||
CORE_DB_USER: str = "postgres"
|
||||
CORE_DB_PASSWORD: str = "postgres"
|
||||
|
||||
|
||||
# Keycloak
|
||||
KEYCLOAK_SERVER_URL: str = "http://localhost:8080"
|
||||
KEYCLOAK_REALM: str = "master"
|
||||
@@ -28,34 +29,32 @@ class Settings(BaseSettings):
|
||||
KEYCLOAK_CLIENT_SECRET: str = ""
|
||||
KEYCLOAK_ADMIN_USERNAME: str = "admin"
|
||||
KEYCLOAK_ADMIN_PASSWORD: str = "admin"
|
||||
|
||||
|
||||
# Security
|
||||
SECRET_KEY: str = "change-this-secret-key-in-production"
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
||||
|
||||
|
||||
# CORS
|
||||
CORS_ORIGINS: str = "http://localhost:5173,http://localhost:3000"
|
||||
|
||||
|
||||
# License
|
||||
LICENSE_CHECK_ENABLED: bool = True
|
||||
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
case_sensitive=True,
|
||||
extra="ignore"
|
||||
env_file=".env", case_sensitive=True, extra="ignore"
|
||||
)
|
||||
|
||||
|
||||
@property
|
||||
def core_database_url(self) -> str:
|
||||
"""URL de conexión a la base de datos core"""
|
||||
return f"postgresql://{self.CORE_DB_USER}:{self.CORE_DB_PASSWORD}@{self.CORE_DB_HOST}:{self.CORE_DB_PORT}/{self.CORE_DB_NAME}"
|
||||
|
||||
|
||||
@property
|
||||
def async_core_database_url(self) -> str:
|
||||
"""URL de conexión asíncrona a la base de datos core"""
|
||||
return f"postgresql+asyncpg://{self.CORE_DB_USER}:{self.CORE_DB_PASSWORD}@{self.CORE_DB_HOST}:{self.CORE_DB_PORT}/{self.CORE_DB_NAME}"
|
||||
|
||||
|
||||
@property
|
||||
def cors_origins_list(self) -> List[str]:
|
||||
"""Lista de orígenes CORS permitidos"""
|
||||
|
||||
@@ -3,6 +3,7 @@ Configuración de base de datos con soporte multi-tenant
|
||||
- Base de datos compartida (core_db) para tenants pequeños/medianos
|
||||
- Bases de datos dedicadas para clientes enterprise
|
||||
"""
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker, Session
|
||||
@@ -20,14 +21,10 @@ core_engine = create_engine(
|
||||
pool_pre_ping=True,
|
||||
pool_size=10,
|
||||
max_overflow=20,
|
||||
echo=settings.DEBUG
|
||||
echo=settings.DEBUG,
|
||||
)
|
||||
|
||||
CoreSessionLocal = sessionmaker(
|
||||
autocommit=False,
|
||||
autoflush=False,
|
||||
bind=core_engine
|
||||
)
|
||||
CoreSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=core_engine)
|
||||
|
||||
# Engine asíncrono para operaciones async
|
||||
async_core_engine = create_async_engine(
|
||||
@@ -35,13 +32,11 @@ async_core_engine = create_async_engine(
|
||||
pool_pre_ping=True,
|
||||
pool_size=10,
|
||||
max_overflow=20,
|
||||
echo=settings.DEBUG
|
||||
echo=settings.DEBUG,
|
||||
)
|
||||
|
||||
AsyncCoreSessionLocal = async_sessionmaker(
|
||||
async_core_engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False
|
||||
async_core_engine, class_=AsyncSession, expire_on_commit=False
|
||||
)
|
||||
|
||||
# Cache de engines para tenants con BD dedicada
|
||||
@@ -74,33 +69,32 @@ async def get_async_core_db() -> AsyncGenerator[AsyncSession, None]:
|
||||
def get_tenant_engine(tenant_id: int, db_config: dict):
|
||||
"""
|
||||
Obtiene o crea un engine para un tenant con BD dedicada
|
||||
|
||||
|
||||
Args:
|
||||
tenant_id: ID del tenant
|
||||
db_config: Configuración de BD {host, port, name, user, password}
|
||||
|
||||
|
||||
Returns:
|
||||
Engine de SQLAlchemy para el tenant
|
||||
"""
|
||||
if tenant_id not in _tenant_engines:
|
||||
db_url = f"postgresql://{db_config['user']}:{db_config['password']}@{db_config['host']}:{db_config['port']}/{db_config['name']}"
|
||||
_tenant_engines[tenant_id] = create_engine(
|
||||
db_url,
|
||||
pool_pre_ping=True,
|
||||
pool_size=5,
|
||||
max_overflow=10
|
||||
db_url, pool_pre_ping=True, pool_size=5, max_overflow=10
|
||||
)
|
||||
return _tenant_engines[tenant_id]
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_tenant_db(tenant_id: int, db_config: Optional[dict] = None) -> Generator[Session, None, None]:
|
||||
def get_tenant_db(
|
||||
tenant_id: int, db_config: Optional[dict] = None
|
||||
) -> Generator[Session, None, None]:
|
||||
"""
|
||||
Context manager para obtener sesión de BD de un tenant específico
|
||||
|
||||
|
||||
Si db_config es None, usa la BD core (compartida)
|
||||
Si db_config está presente, usa la BD dedicada del tenant
|
||||
|
||||
|
||||
Uso:
|
||||
with get_tenant_db(tenant_id, config) as db:
|
||||
# operaciones con db
|
||||
@@ -113,7 +107,7 @@ def get_tenant_db(tenant_id: int, db_config: Optional[dict] = None) -> Generator
|
||||
engine = get_tenant_engine(tenant_id, db_config)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
db = SessionLocal()
|
||||
|
||||
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
|
||||
@@ -4,6 +4,7 @@ Middleware personalizado para Anexo76
|
||||
- Gestión de multi-tenancy
|
||||
- Logging de requests
|
||||
"""
|
||||
|
||||
from fastapi import Request, HTTPException
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from typing import Callable
|
||||
@@ -22,58 +23,59 @@ class TenantMiddleware(BaseHTTPMiddleware):
|
||||
"""
|
||||
Middleware para identificar y validar el tenant en cada request
|
||||
"""
|
||||
|
||||
|
||||
async def dispatch(self, request: Request, call_next: Callable):
|
||||
# Rutas públicas que no requieren tenant
|
||||
# Permitir acceso sin autenticación a rutas de documentación y salud
|
||||
doc_prefixes = [
|
||||
"/api/redoc",
|
||||
"/api/openapi.json"
|
||||
]
|
||||
public_prefixes = [
|
||||
"/api/v1/auth",
|
||||
"/api/v1/status",
|
||||
"/api/health",
|
||||
"/api/"
|
||||
]
|
||||
doc_prefixes = ["/api/redoc", "/api/openapi.json"]
|
||||
public_prefixes = ["/api/v1/auth", "/api/v1/status", "/api/health", "/api/"]
|
||||
|
||||
path = request.url.path
|
||||
# Permitir cualquier subruta de docs/redoc/openapi.json (por ejemplo, /api/docs, /api/docs/, /api/docs/oauth2-redirect)
|
||||
if any(path == prefix or path.startswith(prefix + "/") for prefix in doc_prefixes):
|
||||
if any(
|
||||
path == prefix or path.startswith(prefix + "/") for prefix in doc_prefixes
|
||||
):
|
||||
return await call_next(request)
|
||||
# Permitir rutas públicas exactas o con prefijo
|
||||
if any(path == prefix or (prefix != "/" and path.startswith(prefix)) for prefix in public_prefixes):
|
||||
if any(
|
||||
path == prefix or (prefix != "/" and path.startswith(prefix))
|
||||
for prefix in public_prefixes
|
||||
):
|
||||
return await call_next(request)
|
||||
|
||||
|
||||
# Extraer token y obtener tenant
|
||||
auth_header = request.headers.get("Authorization")
|
||||
|
||||
|
||||
if not auth_header or not auth_header.startswith("Bearer "):
|
||||
raise HTTPException(status_code=401, detail="Missing or invalid authorization header")
|
||||
|
||||
raise HTTPException(
|
||||
status_code=401, detail="Missing or invalid authorization header"
|
||||
)
|
||||
|
||||
token = auth_header.split(" ")[1]
|
||||
|
||||
|
||||
try:
|
||||
user_info = verify_token(token)
|
||||
tenant_id = get_tenant_from_token(user_info)
|
||||
|
||||
|
||||
# ⚠️ NOTA: tenant_id puede ser None para usuarios SSO que aún no tienen tenant asignado
|
||||
# En ese caso, el endpoint específico deberá manejarlo
|
||||
if not tenant_id:
|
||||
logger.warning(f"⚠️ Token sin tenant_id para usuario: {user_info.get('sub', 'unknown')}")
|
||||
logger.warning(
|
||||
f"⚠️ Token sin tenant_id para usuario: {user_info.get('sub', 'unknown')}"
|
||||
)
|
||||
# No lanzamos error aquí, dejamos que el endpoint decida qué hacer
|
||||
|
||||
|
||||
# Agregar tenant_id al state del request (puede ser None)
|
||||
request.state.tenant_id = tenant_id
|
||||
request.state.user_info = user_info
|
||||
|
||||
|
||||
except HTTPException:
|
||||
# Re-lanzar HTTPException directamente
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Tenant validation error: {str(e)}")
|
||||
raise HTTPException(status_code=401, detail="Invalid authentication")
|
||||
|
||||
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
@@ -82,58 +84,60 @@ class LicenseValidationMiddleware(BaseHTTPMiddleware):
|
||||
"""
|
||||
Middleware para validar la licencia del tenant antes de procesar requests
|
||||
"""
|
||||
|
||||
|
||||
async def dispatch(self, request: Request, call_next: Callable):
|
||||
if not settings.LICENSE_CHECK_ENABLED:
|
||||
return await call_next(request)
|
||||
|
||||
|
||||
# Rutas que no requieren validación de licencia
|
||||
exempt_paths = [
|
||||
"/api/docs",
|
||||
"/api/docs",
|
||||
"/api/redoc",
|
||||
"/openapi.json",
|
||||
"/api/v1/auth",
|
||||
"/api/v1/auth",
|
||||
"/openapi.json",
|
||||
"/api/v1/auth",
|
||||
"/api/v1/auth",
|
||||
"/api/v1/status",
|
||||
"/api/v1/status",
|
||||
"/api/health",
|
||||
"/api/"
|
||||
"/api/",
|
||||
]
|
||||
|
||||
|
||||
# Verificar si la ruta está exenta (comparación exacta o prefijo)
|
||||
is_exempt = False
|
||||
for path in exempt_paths:
|
||||
if request.url.path == path or (path != "/" and request.url.path.startswith(path)):
|
||||
if request.url.path == path or (
|
||||
path != "/" and request.url.path.startswith(path)
|
||||
):
|
||||
is_exempt = True
|
||||
break
|
||||
|
||||
|
||||
if is_exempt:
|
||||
return await call_next(request)
|
||||
|
||||
|
||||
# Obtener tenant_id del request state (debe ser seteado por TenantMiddleware)
|
||||
tenant_id = getattr(request.state, "tenant_id", None)
|
||||
|
||||
|
||||
if not tenant_id:
|
||||
return await call_next(request) # Dejamos que TenantMiddleware maneje esto
|
||||
|
||||
|
||||
# Validar licencia
|
||||
db = CoreSessionLocal()
|
||||
try:
|
||||
# Importar aquí para evitar imports circulares
|
||||
from api.v1.modules.a76.licenses.service import LicenseService
|
||||
|
||||
|
||||
license_service = LicenseService(db)
|
||||
license_info = license_service.validate_license(tenant_id)
|
||||
|
||||
|
||||
if not license_info["is_valid"]:
|
||||
raise HTTPException(
|
||||
status_code=402,
|
||||
detail=f"License validation failed: {license_info['reason']}"
|
||||
detail=f"License validation failed: {license_info['reason']}",
|
||||
)
|
||||
|
||||
|
||||
# Agregar info de licencia al request state
|
||||
request.state.license_info = license_info
|
||||
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -141,7 +145,7 @@ class LicenseValidationMiddleware(BaseHTTPMiddleware):
|
||||
raise HTTPException(status_code=500, detail="License validation error")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
@@ -150,15 +154,15 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
||||
"""
|
||||
Middleware para logging de requests
|
||||
"""
|
||||
|
||||
|
||||
async def dispatch(self, request: Request, call_next: Callable):
|
||||
start_time = time.time()
|
||||
|
||||
|
||||
# Log request
|
||||
logger.info(f"Request: {request.method} {request.url.path}")
|
||||
|
||||
|
||||
response = await call_next(request)
|
||||
|
||||
|
||||
# Log response
|
||||
process_time = time.time() - start_time
|
||||
logger.info(
|
||||
@@ -166,8 +170,8 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
||||
f"Status: {response.status_code} "
|
||||
f"Duration: {process_time:.3f}s"
|
||||
)
|
||||
|
||||
|
||||
# Agregar header con tiempo de procesamiento
|
||||
response.headers["X-Process-Time"] = str(process_time)
|
||||
|
||||
|
||||
return response
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"""
|
||||
Utilidades de seguridad y autenticación con Keycloak
|
||||
"""
|
||||
|
||||
from fastapi import HTTPException, Security, Depends
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from keycloak import KeycloakOpenID
|
||||
from jose import jwt, JWTError
|
||||
from typing import Optional, Dict, Any
|
||||
from sqlalchemy.orm import Session
|
||||
from .config import settings
|
||||
import logging
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -19,7 +21,7 @@ keycloak_openid = KeycloakOpenID(
|
||||
server_url=settings.KEYCLOAK_SERVER_URL,
|
||||
client_id=settings.KEYCLOAK_CLIENT_ID,
|
||||
realm_name=settings.KEYCLOAK_REALM,
|
||||
client_secret_key=settings.KEYCLOAK_CLIENT_SECRET
|
||||
client_secret_key=settings.KEYCLOAK_CLIENT_SECRET,
|
||||
)
|
||||
|
||||
# Security scheme
|
||||
@@ -29,13 +31,13 @@ security = HTTPBearer()
|
||||
def verify_token(token: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Verifica y decodifica un token JWT de Keycloak
|
||||
|
||||
|
||||
Args:
|
||||
token: Token JWT
|
||||
|
||||
|
||||
Returns:
|
||||
Payload del token decodificado
|
||||
|
||||
|
||||
Raises:
|
||||
HTTPException: Si el token es inválido
|
||||
"""
|
||||
@@ -46,45 +48,30 @@ def verify_token(token: str) -> Dict[str, Any]:
|
||||
+ keycloak_openid.public_key()
|
||||
+ "\n-----END PUBLIC KEY-----"
|
||||
)
|
||||
|
||||
|
||||
# Decodificar y verificar token
|
||||
options = {
|
||||
"verify_signature": True,
|
||||
"verify_aud": False,
|
||||
"verify_exp": True
|
||||
}
|
||||
|
||||
options = {"verify_signature": True, "verify_aud": False, "verify_exp": True}
|
||||
|
||||
decoded_token = jwt.decode(
|
||||
token,
|
||||
KEYCLOAK_PUBLIC_KEY,
|
||||
algorithms=["RS256"],
|
||||
options=options
|
||||
token, KEYCLOAK_PUBLIC_KEY, algorithms=["RS256"], options=options
|
||||
)
|
||||
|
||||
|
||||
return decoded_token
|
||||
|
||||
|
||||
except JWTError as e:
|
||||
logger.error(f"Token verification failed: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Could not validate credentials"
|
||||
)
|
||||
raise HTTPException(status_code=401, detail="Could not validate credentials")
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error during token verification: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Authentication error"
|
||||
)
|
||||
raise HTTPException(status_code=401, detail="Authentication error")
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials = Security(security),
|
||||
db: Session = Depends(get_core_db)
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Dependency para obtener el usuario actual desde el token JWT
|
||||
Enriquecido con tenant_id y company_id desde la tabla user_tenant
|
||||
|
||||
|
||||
Uso en FastAPI:
|
||||
current_user: dict = Depends(get_current_user)
|
||||
"""
|
||||
@@ -109,7 +96,7 @@ async def get_current_user(
|
||||
|
||||
|
||||
async def get_current_active_user(
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Dependency para obtener usuario activo (puede incluir validaciones adicionales)
|
||||
@@ -122,121 +109,127 @@ async def get_current_active_user(
|
||||
def has_role(required_role: str):
|
||||
"""
|
||||
Decorator/Dependency para verificar roles de usuario
|
||||
|
||||
|
||||
Uso:
|
||||
@router.get("/admin")
|
||||
async def admin_endpoint(user = Depends(has_role("admin"))):
|
||||
...
|
||||
"""
|
||||
|
||||
async def role_checker(
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
) -> Dict[str, Any]:
|
||||
user_roles = current_user.get("realm_access", {}).get("roles", [])
|
||||
|
||||
|
||||
if required_role not in user_roles:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"User does not have required role: {required_role}"
|
||||
detail=f"User does not have required role: {required_role}",
|
||||
)
|
||||
|
||||
|
||||
return current_user
|
||||
|
||||
|
||||
return role_checker
|
||||
|
||||
|
||||
def get_tenant_from_token(user_info: Dict[str, Any]) -> Optional[int]:
|
||||
"""
|
||||
Extrae el tenant_id del token JWT
|
||||
|
||||
|
||||
El tenant_id puede estar en diferentes lugares según configuración de Keycloak:
|
||||
- En claims personalizados
|
||||
- En el realm
|
||||
- En atributos del usuario
|
||||
"""
|
||||
# Intentar obtener de claims personalizados
|
||||
tenant_id = user_info.get("tenant_id")
|
||||
|
||||
tenant_id = user_info.get("tenant_id")
|
||||
if not tenant_id:
|
||||
# Intentar obtener de atributos
|
||||
tenant_id = user_info.get("attributes", {}).get("tenant_id")
|
||||
|
||||
|
||||
if tenant_id:
|
||||
return int(tenant_id)
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def validate_company_access(
|
||||
company_id: int,
|
||||
current_user: Dict[str, Any]
|
||||
) -> bool:
|
||||
def validate_company_access(db: Session, company_id: int, current_user: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Valida que el usuario tenga acceso a la compañía solicitada
|
||||
|
||||
|
||||
Args:
|
||||
company_id: ID de la compañía a la que se quiere acceder
|
||||
current_user: Información del usuario actual desde el token
|
||||
|
||||
|
||||
Returns:
|
||||
True si el usuario tiene acceso, False en caso contrario
|
||||
|
||||
|
||||
Nota:
|
||||
Por ahora solo verifica que el tenant_id del usuario coincida con el company_id.
|
||||
Se puede extender para validar permisos específicos por compañía.
|
||||
Verifica que la compañía pertenezca al tenant del usuario consultando la BD.
|
||||
"""
|
||||
tenant_id = get_tenant_from_token(current_user)
|
||||
|
||||
tenant_id = get_tenant_from_token(current_user)
|
||||
|
||||
# Si no hay tenant_id en el token, denegar acceso
|
||||
if not tenant_id:
|
||||
return False
|
||||
|
||||
# Validar que el company_id pertenezca al tenant del usuario
|
||||
# Por ahora asumimos que company_id == tenant_id
|
||||
# Esto se puede modificar si hay una tabla de relación tenant-company
|
||||
return tenant_id == company_id
|
||||
|
||||
def validate_access_to_resource(
|
||||
company_id: int,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
) -> bool:
|
||||
# Consultar si la compañía pertenece al tenant
|
||||
try:
|
||||
from api.v1.modules.a76.company.models import Company
|
||||
|
||||
company = db.query(Company).filter(
|
||||
Company.id == company_id,
|
||||
Company.tenant_id == tenant_id
|
||||
).first()
|
||||
|
||||
return company is not None
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def validate_access_to_resource(db: Session, company_id: int, current_user: Dict[str, Any]) -> int:
|
||||
"""
|
||||
Valida que el usuario tenga acceso a un recurso específico basado en company_id
|
||||
y regresa el tenant_id
|
||||
|
||||
|
||||
Args:
|
||||
company_id: company_id asociado al recurso
|
||||
current_user: Información del usuario actual desde el token
|
||||
|
||||
|
||||
Returns:
|
||||
True si el usuario tiene acceso, False en caso contrario
|
||||
tenant_id si el usuario tiene acceso
|
||||
|
||||
Raises:
|
||||
HTTPException: Si no hay tenant_id o no tiene acceso
|
||||
"""
|
||||
|
||||
|
||||
tenant_id = get_tenant_from_token(current_user)
|
||||
if not tenant_id:
|
||||
raise HTTPException(status_code=400, detail="Tenant ID not found in token")
|
||||
|
||||
if not validate_company_access(company_id, current_user):
|
||||
|
||||
if not validate_company_access(db, company_id, current_user):
|
||||
raise HTTPException(status_code=403, detail="Access denied to this company")
|
||||
|
||||
|
||||
# Validar que el tenant_id del usuario coincida con el del recurso
|
||||
return tenant_id
|
||||
|
||||
|
||||
class KeycloakClient:
|
||||
"""Cliente para interactuar con Keycloak Admin API"""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.openid = keycloak_openid
|
||||
|
||||
|
||||
def create_user(self, email: str, password: str, tenant_id: int, **kwargs):
|
||||
"""Crea un usuario en Keycloak"""
|
||||
# Implementar lógica para crear usuario usando keycloak admin
|
||||
pass
|
||||
|
||||
|
||||
def assign_role(self, user_id: str, role: str):
|
||||
"""Asigna un rol a un usuario"""
|
||||
pass
|
||||
|
||||
|
||||
def create_tenant_realm(self, tenant_name: str):
|
||||
"""Crea un realm para un nuevo tenant"""
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user