feat(security): implement permission checks in tenant CRUD routes and enhance API error handling
This commit is contained in:
@@ -50,27 +50,14 @@ from api.v1.modules.public.reference_data.transport_modes.seed import (
|
||||
from api.v1.modules.public.reference_data.transport_types.seed import (
|
||||
seed as transport_types_seed,
|
||||
)
|
||||
from api.v1.modules.public.reference_data.trailer_types.seed import (
|
||||
seed as trailer_types_seed,
|
||||
)
|
||||
from api.v1.modules.public.reference_data.valuation_methods.seed import (
|
||||
seed as valuation_methods_seed,
|
||||
)
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.seed import (
|
||||
seed as units_of_measure_seed,
|
||||
)
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.seed_ace import (
|
||||
seed as ace_seed,
|
||||
)
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.seed_oma import (
|
||||
seed as oma_seed,
|
||||
)
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.seed_ame import (
|
||||
seed as ame_seed,
|
||||
)
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.seed_adua import (
|
||||
seed as adua_seed,
|
||||
)
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.seed import seed as units_of_measure_seed
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.seed_ace import seed as ace_seed
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.seed_oma import seed as oma_seed
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.seed_ame import seed as ame_seed
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.seed_adua import seed as adua_seed
|
||||
from api.v1.modules.core.permissions.seed import (
|
||||
seed_invoices,
|
||||
seed_user,
|
||||
@@ -86,12 +73,12 @@ depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
"""Upgrade schema."""
|
||||
|
||||
# --- UTILIDAD DE FORMATEO ---
|
||||
def format_value(val):
|
||||
if val is None or str(val).strip() == "" or str(val).upper() == "NONE":
|
||||
return "NULL"
|
||||
if val is None or str(val).strip() == '' or str(val).upper() == 'NONE':
|
||||
return 'NULL'
|
||||
return f"'{str(val).replace(chr(39), chr(39)*2)}'"
|
||||
|
||||
# --- SEEDS PUBLIC (Tablas base) ---
|
||||
@@ -306,20 +293,6 @@ def upgrade() -> None:
|
||||
"""
|
||||
)
|
||||
|
||||
values_trailert = ", ".join(
|
||||
[
|
||||
f"('{code}', '{desc.replace(chr(39), chr(39)*2)}')"
|
||||
for code, desc in trailer_types_seed
|
||||
]
|
||||
)
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO public.trailer_type (trailer_type_key, description) VALUES
|
||||
{values_trailert}
|
||||
ON CONFLICT (trailer_type_key) DO NOTHING;
|
||||
"""
|
||||
)
|
||||
|
||||
values_vm = ", ".join(
|
||||
[
|
||||
f"('{key}', '{desc.replace(chr(39), chr(39)*2)}')"
|
||||
@@ -337,131 +310,93 @@ def upgrade() -> None:
|
||||
# --- SEEDS A76 (Unidades de Medida) ---
|
||||
|
||||
# ACE
|
||||
val_ace = ", ".join(
|
||||
[f"({format_value(c)}, {format_value(d)})" for c, d in ace_seed]
|
||||
)
|
||||
op.execute(
|
||||
f"INSERT INTO a76.unit_of_measure_ace (code, description) VALUES {val_ace} ON CONFLICT ON CONSTRAINT uq_uom_ace_code DO NOTHING;"
|
||||
)
|
||||
val_ace = ", ".join([f"({format_value(c)}, {format_value(d)})" for c, d in ace_seed])
|
||||
op.execute(f"INSERT INTO a76.unit_of_measure_ace (code, description) VALUES {val_ace} ON CONFLICT ON CONSTRAINT uq_uom_ace_code DO NOTHING;")
|
||||
|
||||
# OMA
|
||||
val_oma = ", ".join(
|
||||
[f"({format_value(c)}, {format_value(d)})" for c, d in oma_seed]
|
||||
)
|
||||
op.execute(
|
||||
f"INSERT INTO a76.unit_of_measure_oma (code, description) VALUES {val_oma} ON CONFLICT ON CONSTRAINT uq_uom_oma_code DO NOTHING;"
|
||||
)
|
||||
val_oma = ", ".join([f"({format_value(c)}, {format_value(d)})" for c, d in oma_seed])
|
||||
op.execute(f"INSERT INTO a76.unit_of_measure_oma (code, description) VALUES {val_oma} ON CONFLICT ON CONSTRAINT uq_uom_oma_code DO NOTHING;")
|
||||
|
||||
# AME
|
||||
val_ame = ", ".join(
|
||||
[f"({format_value(c)}, {format_value(d)})" for c, d in ame_seed]
|
||||
)
|
||||
op.execute(
|
||||
f"INSERT INTO a76.unit_of_measure_american (code, description) VALUES {val_ame} ON CONFLICT ON CONSTRAINT uq_uom_american_code DO NOTHING;"
|
||||
)
|
||||
val_ame = ", ".join([f"({format_value(c)}, {format_value(d)})" for c, d in ame_seed])
|
||||
op.execute(f"INSERT INTO a76.unit_of_measure_american (code, description) VALUES {val_ame} ON CONFLICT ON CONSTRAINT uq_uom_american_code DO NOTHING;")
|
||||
|
||||
# ADUA (Customs)
|
||||
val_adua = ", ".join(
|
||||
[f"({format_value(c)}, {format_value(d)})" for c, d in adua_seed]
|
||||
)
|
||||
op.execute(
|
||||
f"INSERT INTO a76.unit_of_measure_customs (code, description) VALUES {val_adua} ON CONFLICT ON CONSTRAINT uq_uom_customs_code DO NOTHING;"
|
||||
)
|
||||
val_adua = ", ".join([f"({format_value(c)}, {format_value(d)})" for c, d in adua_seed])
|
||||
op.execute(f"INSERT INTO a76.unit_of_measure_customs (code, description) VALUES {val_adua} ON CONFLICT ON CONSTRAINT uq_uom_customs_code DO NOTHING;")
|
||||
|
||||
# Recolectar códigos adicionales que faltan en los catálogos
|
||||
additional_customs = set()
|
||||
additional_american = set()
|
||||
additional_ace = set()
|
||||
additional_oma = set()
|
||||
|
||||
|
||||
existing_customs = {c for c, d in adua_seed}
|
||||
existing_american = {c for c, d in ame_seed}
|
||||
existing_ace = {c for c, d in ace_seed}
|
||||
existing_oma = {c for c, d in oma_seed}
|
||||
|
||||
|
||||
for code, desc, desc_en, customs, american, ace, oma in units_of_measure_seed:
|
||||
if customs and customs.strip() and customs not in existing_customs:
|
||||
additional_customs.add((customs, f"Auto-generated from {code}"))
|
||||
additional_customs.add((customs, f'Auto-generated from {code}'))
|
||||
if american and american.strip() and american not in existing_american:
|
||||
additional_american.add((american, f"Auto-generated from {code}"))
|
||||
additional_american.add((american, f'Auto-generated from {code}'))
|
||||
if ace and ace.strip() and ace not in existing_ace:
|
||||
additional_ace.add((ace, f"Auto-generated from {code}"))
|
||||
additional_ace.add((ace, f'Auto-generated from {code}'))
|
||||
if oma and oma.strip() and oma not in existing_oma:
|
||||
additional_oma.add((oma, f"Auto-generated from {code}"))
|
||||
|
||||
additional_oma.add((oma, f'Auto-generated from {code}'))
|
||||
|
||||
# Insertar códigos adicionales
|
||||
if additional_customs:
|
||||
val_add_customs = ", ".join(
|
||||
[f"({format_value(c)}, {format_value(d)})" for c, d in additional_customs]
|
||||
)
|
||||
op.execute(
|
||||
f"INSERT INTO a76.unit_of_measure_customs (code, description, a76_unit_code) VALUES {val_add_customs} ON CONFLICT ON CONSTRAINT uq_uom_customs_code DO NOTHING;"
|
||||
)
|
||||
|
||||
val_add_customs = ", ".join([f"({format_value(c)}, {format_value(d)})" for c, d in additional_customs])
|
||||
op.execute(f"INSERT INTO a76.unit_of_measure_customs (code, description, a76_unit_code) VALUES {val_add_customs} ON CONFLICT ON CONSTRAINT uq_uom_customs_code DO NOTHING;")
|
||||
|
||||
if additional_american:
|
||||
val_add_american = ", ".join(
|
||||
[f"({format_value(c)}, {format_value(d)})" for c, d in additional_american]
|
||||
)
|
||||
op.execute(
|
||||
f"INSERT INTO a76.unit_of_measure_american (code, description) VALUES {val_add_american} ON CONFLICT ON CONSTRAINT uq_uom_american_code DO NOTHING;"
|
||||
)
|
||||
|
||||
val_add_american = ", ".join([f"({format_value(c)}, {format_value(d)})" for c, d in additional_american])
|
||||
op.execute(f"INSERT INTO a76.unit_of_measure_american (code, description) VALUES {val_add_american} ON CONFLICT ON CONSTRAINT uq_uom_american_code DO NOTHING;")
|
||||
|
||||
if additional_ace:
|
||||
val_add_ace = ", ".join(
|
||||
[f"({format_value(c)}, {format_value(d)})" for c, d in additional_ace]
|
||||
)
|
||||
op.execute(
|
||||
f"INSERT INTO a76.unit_of_measure_ace (code, description) VALUES {val_add_ace} ON CONFLICT ON CONSTRAINT uq_uom_ace_code DO NOTHING;"
|
||||
)
|
||||
|
||||
val_add_ace = ", ".join([f"({format_value(c)}, {format_value(d)})" for c, d in additional_ace])
|
||||
op.execute(f"INSERT INTO a76.unit_of_measure_ace (code, description) VALUES {val_add_ace} ON CONFLICT ON CONSTRAINT uq_uom_ace_code DO NOTHING;")
|
||||
|
||||
if additional_oma:
|
||||
val_add_oma = ", ".join(
|
||||
[f"({format_value(c)}, {format_value(d)})" for c, d in additional_oma]
|
||||
)
|
||||
op.execute(
|
||||
f"INSERT INTO a76.unit_of_measure_oma (code, description) VALUES {val_add_oma} ON CONFLICT ON CONSTRAINT uq_uom_oma_code DO NOTHING;"
|
||||
)
|
||||
val_add_oma = ", ".join([f"({format_value(c)}, {format_value(d)})" for c, d in additional_oma])
|
||||
op.execute(f"INSERT INTO a76.unit_of_measure_oma (code, description) VALUES {val_add_oma} ON CONFLICT ON CONSTRAINT uq_uom_oma_code DO NOTHING;")
|
||||
|
||||
# TABLA MAESTRA UOM
|
||||
# TODO: Generar tenant_id y company_id correctos
|
||||
val_uom = ", ".join(
|
||||
[
|
||||
f"({format_value(code)}, {format_value(desc)}, {format_value(desc_en)}, "
|
||||
f"{format_value(customs)}, {format_value(american)}, {format_value(ace)}, {format_value(oma)}, 1, 1)"
|
||||
for code, desc, desc_en, customs, american, ace, oma in units_of_measure_seed
|
||||
]
|
||||
)
|
||||
val_uom = ", ".join([
|
||||
f"({format_value(code)}, {format_value(desc)}, {format_value(desc_en)}, "
|
||||
f"{format_value(customs)}, {format_value(american)}, {format_value(ace)}, {format_value(oma)}, 1, 1)"
|
||||
for code, desc, desc_en, customs, american, ace, oma in units_of_measure_seed
|
||||
])
|
||||
op.execute("ALTER TABLE a76.units_of_measure DISABLE TRIGGER ALL;")
|
||||
op.execute(
|
||||
f"""
|
||||
op.execute(f"""
|
||||
INSERT INTO a76.units_of_measure
|
||||
(code, description, description_en, customs_code, american_code, ace_code, oma_code, tenant_id, company_id)
|
||||
VALUES {val_uom}
|
||||
ON CONFLICT (code, tenant_id, company_id) DO NOTHING;
|
||||
"""
|
||||
)
|
||||
""")
|
||||
op.execute("ALTER TABLE a76.units_of_measure ENABLE TRIGGER ALL;")
|
||||
|
||||
# --- SEEDS CORE (Permissions) ---
|
||||
|
||||
|
||||
# Combinar todas las seeds de permisos
|
||||
all_permissions = seed_invoices + seed_user + seed_report + seed_roles
|
||||
|
||||
values_permissions = ", ".join(
|
||||
[
|
||||
f"({format_value(code)}, {format_value(description)}, {format_value(module)}, {format_value(action)})"
|
||||
for code, description, module, action in all_permissions
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
values_permissions = ", ".join([
|
||||
f"({format_value(code)}, {format_value(desc)}, {format_value(resource)}, {format_value(action)})"
|
||||
for code, desc, resource, action in all_permissions
|
||||
])
|
||||
|
||||
if values_permissions:
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO core.permissions (code, description, module, action)
|
||||
op.execute(f"""
|
||||
INSERT INTO core.permissions (code, description, resource, action)
|
||||
VALUES {values_permissions}
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
"""
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
@@ -492,4 +427,4 @@ def downgrade() -> None:
|
||||
op.drop_table("unit_of_measure_customs", schema="a76")
|
||||
op.drop_table("unit_of_measure_american", schema="a76")
|
||||
op.drop_table("unit_of_measure_oma", schema="a76")
|
||||
op.drop_table("unit_of_measure_ace", schema="a76")
|
||||
op.drop_table("unit_of_measure_ace", schema="a76")
|
||||
@@ -15,8 +15,7 @@ ServiceType = TypeVar("ServiceType")
|
||||
|
||||
|
||||
class TenantCRUDRoutes(
|
||||
Generic[CreateSchemaType, UpdateSchemaType,
|
||||
ResponseSchemaType, ServiceType]
|
||||
Generic[CreateSchemaType, UpdateSchemaType, ResponseSchemaType, ServiceType]
|
||||
):
|
||||
"""
|
||||
Generic CRUD routes factory for tenant-scoped resources
|
||||
@@ -88,6 +87,13 @@ class TenantCRUDRoutes(
|
||||
enable_filters: bool = False, # Enable custom filters in list endpoint
|
||||
default_page_size: int = 50,
|
||||
max_page_size: int = 100,
|
||||
# Permissions for each operation
|
||||
list_permissions: Optional[list[str]] = None,
|
||||
get_permissions: Optional[list[str]] = None,
|
||||
create_permissions: Optional[list[str]] = None,
|
||||
update_permissions: Optional[list[str]] = None,
|
||||
delete_permissions: Optional[list[str]] = None,
|
||||
require_all: bool = True, # If True, requires ALL permissions; if False, requires ANY
|
||||
):
|
||||
self.service = service
|
||||
self.create_schema = create_schema
|
||||
@@ -104,6 +110,12 @@ class TenantCRUDRoutes(
|
||||
self.enable_filters = enable_filters
|
||||
self.default_page_size = default_page_size
|
||||
self.max_page_size = max_page_size
|
||||
self.list_permissions = list_permissions
|
||||
self.get_permissions = get_permissions
|
||||
self.create_permissions = create_permissions
|
||||
self.update_permissions = update_permissions
|
||||
self.delete_permissions = delete_permissions
|
||||
self.require_all = require_all
|
||||
|
||||
self.router = APIRouter(prefix=prefix, tags=tags)
|
||||
self._register_routes()
|
||||
@@ -130,18 +142,22 @@ class TenantCRUDRoutes(
|
||||
le=self.max_page_size,
|
||||
description="Page size",
|
||||
),
|
||||
status: Optional[str] = Query(
|
||||
None, description="Filter by status"),
|
||||
status: Optional[str] = Query(None, description="Filter by status"),
|
||||
operation_type: Optional[str] = Query(
|
||||
None, description="Filter by operation type"),
|
||||
None, description="Filter by operation type"
|
||||
),
|
||||
invoice_type: Optional[str] = Query(
|
||||
None, description="Filter by invoice type"),
|
||||
None, description="Filter by invoice type"
|
||||
),
|
||||
db: Session = Depends(self.db_dependency),
|
||||
current_user: Dict[str, Any] = Depends(
|
||||
self.auth_dependency),
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(
|
||||
db, company_id, current_user
|
||||
db,
|
||||
company_id,
|
||||
current_user,
|
||||
self.list_permissions,
|
||||
self.require_all,
|
||||
)
|
||||
|
||||
skip = (page - 1) * page_size
|
||||
@@ -184,11 +200,14 @@ class TenantCRUDRoutes(
|
||||
description="Page size",
|
||||
),
|
||||
db: Session = Depends(self.db_dependency),
|
||||
current_user: Dict[str, Any] = Depends(
|
||||
self.auth_dependency),
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(
|
||||
db, company_id, current_user
|
||||
db,
|
||||
company_id,
|
||||
current_user,
|
||||
self.list_permissions,
|
||||
self.require_all,
|
||||
)
|
||||
|
||||
skip = (page - 1) * page_size
|
||||
@@ -225,7 +244,8 @@ class TenantCRUDRoutes(
|
||||
):
|
||||
|
||||
tenant_id = validate_access_to_resource(
|
||||
db, company_id, current_user)
|
||||
db, company_id, current_user, self.get_permissions, self.require_all
|
||||
)
|
||||
parent_id = path_params.get(self.parent_id_name)
|
||||
|
||||
# Try method with 4 params (pedimento_id, tenant_id, company_id)
|
||||
@@ -239,8 +259,7 @@ class TenantCRUDRoutes(
|
||||
db, parent_id, tenant_id, company_id
|
||||
)
|
||||
else:
|
||||
resource = self.service.get(
|
||||
db, parent_id, tenant_id, company_id)
|
||||
resource = self.service.get(db, parent_id, tenant_id, company_id)
|
||||
|
||||
if not resource:
|
||||
raise HTTPException(
|
||||
@@ -265,7 +284,8 @@ class TenantCRUDRoutes(
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(
|
||||
db, company_id, current_user)
|
||||
db, company_id, current_user, self.get_permissions, self.require_all
|
||||
)
|
||||
|
||||
resource = self.service.get_by_id(
|
||||
db, resource_id, tenant_id, company_id
|
||||
@@ -298,7 +318,12 @@ class TenantCRUDRoutes(
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(
|
||||
db, company_id, current_user)
|
||||
db,
|
||||
company_id,
|
||||
current_user,
|
||||
self.create_permissions,
|
||||
self.require_all,
|
||||
)
|
||||
|
||||
# For child resources, parent_id validation would go here
|
||||
try:
|
||||
@@ -310,6 +335,7 @@ class TenantCRUDRoutes(
|
||||
except Exception as e:
|
||||
# Re-lanzar otros errores
|
||||
raise
|
||||
|
||||
else:
|
||||
# Parent resource - no parent_id needed
|
||||
|
||||
@@ -330,7 +356,12 @@ class TenantCRUDRoutes(
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(
|
||||
db, company_id, current_user)
|
||||
db,
|
||||
company_id,
|
||||
current_user,
|
||||
self.create_permissions,
|
||||
self.require_all,
|
||||
)
|
||||
try:
|
||||
resource = self.service.create(db, data, tenant_id, company_id)
|
||||
return resource
|
||||
@@ -364,7 +395,12 @@ class TenantCRUDRoutes(
|
||||
**path_params,
|
||||
):
|
||||
tenant_id = validate_access_to_resource(
|
||||
db, company_id, current_user)
|
||||
db,
|
||||
company_id,
|
||||
current_user,
|
||||
self.update_permissions,
|
||||
self.require_all,
|
||||
)
|
||||
parent_id = path_params.get(self.parent_id_name)
|
||||
|
||||
try:
|
||||
@@ -404,7 +440,12 @@ class TenantCRUDRoutes(
|
||||
):
|
||||
f"""Update {self.resource_name}"""
|
||||
tenant_id = validate_access_to_resource(
|
||||
db, company_id, current_user)
|
||||
db,
|
||||
company_id,
|
||||
current_user,
|
||||
self.update_permissions,
|
||||
self.require_all,
|
||||
)
|
||||
|
||||
try:
|
||||
resource = self.service.update(
|
||||
@@ -438,11 +479,15 @@ class TenantCRUDRoutes(
|
||||
**path_params,
|
||||
):
|
||||
tenant_id = validate_access_to_resource(
|
||||
db, company_id, current_user)
|
||||
db,
|
||||
company_id,
|
||||
current_user,
|
||||
self.delete_permissions,
|
||||
self.require_all,
|
||||
)
|
||||
parent_id = path_params.get(self.parent_id_name)
|
||||
|
||||
success = self.service.delete(
|
||||
db, parent_id, tenant_id, company_id)
|
||||
success = self.service.delete(db, parent_id, tenant_id, company_id)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
@@ -467,10 +512,14 @@ class TenantCRUDRoutes(
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(
|
||||
db, company_id, current_user)
|
||||
db,
|
||||
company_id,
|
||||
current_user,
|
||||
self.delete_permissions,
|
||||
self.require_all,
|
||||
)
|
||||
|
||||
success = self.service.delete(
|
||||
db, resource_id, tenant_id, company_id)
|
||||
success = self.service.delete(db, resource_id, tenant_id, company_id)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
|
||||
@@ -20,9 +20,14 @@ invoice_crud = TenantCRUDRoutes(
|
||||
tags=[],
|
||||
resource_name="Invoice",
|
||||
id_name="invoice_id",
|
||||
id_type=int,
|
||||
id_type=int,
|
||||
enable_list=True, # Enable list endpoint with pagination
|
||||
enable_filters=True, # Enable filters for status, operation_type, etc.
|
||||
list_permissions=[],
|
||||
get_permissions=[],
|
||||
create_permissions=[],
|
||||
update_permissions=[],
|
||||
delete_permissions=[],
|
||||
default_page_size=50,
|
||||
max_page_size=200,
|
||||
)
|
||||
|
||||
@@ -11,34 +11,6 @@ from core.database import get_core_db
|
||||
from core.security import get_current_user # Asumiendo que existe esta función
|
||||
from .service import PermissionService
|
||||
|
||||
|
||||
# Dependencia para obtener el ID del cliente del header o contexto
|
||||
async def get_client_id(
|
||||
x_client_id: Optional[str] = Header(None, alias="X-Client-ID")
|
||||
) -> int:
|
||||
"""
|
||||
Obtiene el ID del cliente desde el header de la petición.
|
||||
|
||||
En producción, esto podría obtenerse de:
|
||||
- Un header HTTP (X-Client-ID)
|
||||
- Un subdomain (cliente1.miapp.com)
|
||||
- El token JWT del usuario
|
||||
- La sesión del usuario
|
||||
"""
|
||||
if not x_client_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Client ID is required. Provide X-Client-ID header.",
|
||||
)
|
||||
|
||||
try:
|
||||
return int(x_client_id)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid client ID format"
|
||||
)
|
||||
|
||||
|
||||
# Dependencia para obtener el servicio de permisos
|
||||
def get_permission_service(db: Session = Depends(get_core_db)) -> PermissionService:
|
||||
"""
|
||||
@@ -72,9 +44,9 @@ class PermissionChecker:
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
client_id: int = Depends(get_client_id),
|
||||
permission_service: PermissionService = Depends(get_permission_service),
|
||||
client_id: int,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
permission_service: PermissionService = Depends(get_permission_service),
|
||||
):
|
||||
"""
|
||||
Verifica que el usuario tenga los permisos requeridos.
|
||||
@@ -130,8 +102,8 @@ class RequirePermission:
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
client_id: int = Depends(get_client_id),
|
||||
client_id: int,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
permission_service: PermissionService = Depends(get_permission_service),
|
||||
):
|
||||
user_id = current_user.get("sub") or current_user.get("id")
|
||||
@@ -212,8 +184,8 @@ def require_permissions(*permissions: str, require_all: bool = True):
|
||||
|
||||
# Función helper para obtener permisos del usuario actual
|
||||
async def get_current_user_permissions(
|
||||
current_user: dict = Depends(get_current_user),
|
||||
client_id: int = Depends(get_client_id),
|
||||
client_id: int,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
permission_service: PermissionService = Depends(get_permission_service),
|
||||
) -> set:
|
||||
"""
|
||||
|
||||
@@ -176,29 +176,64 @@ def validate_company_access(
|
||||
|
||||
|
||||
def validate_access_to_resource(
|
||||
db: Session, company_id: int, current_user: Dict[str, Any]
|
||||
db: Session,
|
||||
company_id: int,
|
||||
current_user: Dict[str, Any],
|
||||
required_permissions: Optional[list[str]] = None,
|
||||
require_all: bool = False,
|
||||
) -> int:
|
||||
"""
|
||||
Valida que el usuario tenga acceso a un recurso específico basado en company_id
|
||||
y regresa el tenant_id
|
||||
y regresa el tenant_id. Opcionalmente verifica permisos.
|
||||
|
||||
Args:
|
||||
db: Sesión de base de datos
|
||||
company_id: company_id asociado al recurso
|
||||
current_user: Información del usuario actual desde el token
|
||||
required_permissions: Lista opcional de permisos requeridos. Si es None, no verifica permisos.
|
||||
require_all: Si True, requiere TODOS los permisos. Si False, requiere AL MENOS UNO.
|
||||
|
||||
Returns:
|
||||
tenant_id si el usuario tiene acceso
|
||||
|
||||
Raises:
|
||||
HTTPException: Si no hay tenant_id o no tiene acceso
|
||||
HTTPException: Si no hay tenant_id, no tiene acceso o no tiene los permisos requeridos
|
||||
"""
|
||||
|
||||
tenant_id = get_tenant_from_token(current_user)
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
if not tenant_id:
|
||||
raise HTTPException(status_code=400, detail="Tenant ID not found in token")
|
||||
|
||||
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
|
||||
# Verificar permisos si se proporcionaron
|
||||
if required_permissions:
|
||||
from api.v1.modules.core.permissions.service import PermissionService
|
||||
|
||||
user_id = current_user.get("sub") or current_user.get("id")
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="User ID not found in token")
|
||||
|
||||
permission_service = PermissionService(db)
|
||||
|
||||
if require_all:
|
||||
has_access = permission_service.has_all_permissions(
|
||||
user_id=user_id,
|
||||
company_id=company_id,
|
||||
permission_codes=required_permissions,
|
||||
)
|
||||
else:
|
||||
has_access = permission_service.has_any_permission(
|
||||
user_id=user_id,
|
||||
company_id=company_id,
|
||||
permission_codes=required_permissions,
|
||||
)
|
||||
|
||||
if not has_access:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"Missing required permissions: {', '.join(required_permissions)}",
|
||||
)
|
||||
|
||||
return tenant_id
|
||||
|
||||
Reference in New Issue
Block a user