133 lines
4.8 KiB
Python
133 lines
4.8 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Autenticación `api_key` para las rutas públicas `/skeen/api/v1/*`.
|
|
|
|
Odoo no trae un método de autenticación `api_key`; las rutas de
|
|
`skeen_whatsapp/controllers/main.py` lo declaraban pero no existía, así que
|
|
TODAS las peticiones respondían AccessDenied. Aquí se implementa el método que
|
|
falta, sin cambiar ni el formato de la clave ni el de los encabezados:
|
|
|
|
1) CLave legacy de Hermes: `ir.config_parameter 'skeen.api.legacy_key'`
|
|
(sembrada con `skeen_api_key_2026`).
|
|
2) Claves del modelo `skeen.api.key` (hash, alcances, vencimiento, revocación).
|
|
|
|
La clave se lee de `Authorization: Bearer <clave>`, de `X-API-Key` o del
|
|
parámetro `api_key` (query/form), que es lo que ya usaba Hermes.
|
|
"""
|
|
|
|
import hmac
|
|
import json
|
|
|
|
from werkzeug.exceptions import Forbidden
|
|
|
|
from odoo import SUPERUSER_ID, models, http
|
|
from odoo.exceptions import AccessDenied
|
|
from odoo.http import request, Response
|
|
|
|
from odoo.addons.skeen_citas.clinic_settings import licencia_de, modulo_bloqueado
|
|
|
|
LEGACY_KEY_PARAM = 'skeen.api.legacy_key'
|
|
LEGACY_KEY_DEFAULT = 'skeen_api_key_2026'
|
|
|
|
# Rutas del frontend cuyo módulo se controla por licencia. Los módulos con
|
|
# bloque `activo` (compras, membresías, comisiones, recursos, notificaciones,
|
|
# reservas) ya responden 403 solos al apagarse, así que aquí solo hacen falta
|
|
# los que no tienen ese bloque.
|
|
RUTAS_LICENCIA = (
|
|
('/skeen/frontend/v1/formularios', 'formularios'),
|
|
('/skeen/frontend/v1/wacrm', 'whatsapp'),
|
|
)
|
|
|
|
|
|
def _respuesta_licencia():
|
|
"""Respuesta 403 JSON si la ruta pertenece a un módulo fuera de la licencia.
|
|
|
|
Devuelve None si la petición puede continuar. No aplica a OPTIONS (preflight
|
|
CORS) ni cuando no hay licencia en modo bloqueo."""
|
|
try:
|
|
if request.httprequest.method == 'OPTIONS':
|
|
return None
|
|
path = request.httprequest.path or ''
|
|
modulo = next((m for prefijo, m in RUTAS_LICENCIA if path.startswith(prefijo)), None)
|
|
if not modulo or not request.env:
|
|
return None
|
|
if not modulo_bloqueado(licencia_de(request.env), modulo):
|
|
return None
|
|
except Exception:
|
|
return None
|
|
cuerpo = json.dumps({'status': 'error',
|
|
'message': 'Módulo no incluido en tu plan'},
|
|
ensure_ascii=False)
|
|
return Response(cuerpo, status=403, mimetype='application/json',
|
|
headers={'Access-Control-Allow-Origin': '*'})
|
|
|
|
|
|
def extraer_clave(req):
|
|
"""Devuelve la clave de API de la petición (o '')."""
|
|
try:
|
|
headers = req.httprequest.headers
|
|
auth = headers.get('Authorization', '') or ''
|
|
if auth.lower().startswith('bearer '):
|
|
return auth[7:].strip()
|
|
for header in ('X-API-Key', 'X-Api-Key', 'Api-Key'):
|
|
valor = headers.get(header)
|
|
if valor:
|
|
return valor.strip()
|
|
args = req.httprequest.args
|
|
for parametro in ('api_key', 'api-key', 'apikey'):
|
|
if args.get(parametro):
|
|
return args.get(parametro).strip()
|
|
except Exception:
|
|
return ''
|
|
return ''
|
|
|
|
|
|
def clave_permite_scope(req, scope):
|
|
"""True si la clave de la petición concede el alcance `scope`.
|
|
|
|
La clave legacy de Hermes tiene todos los alcances. Una petición sin
|
|
`request.api_key` (p. ej. dentro de pruebas) se considera legacy.
|
|
"""
|
|
key = getattr(req, 'api_key', None)
|
|
if key is None:
|
|
return True
|
|
return key.permite(scope)
|
|
|
|
|
|
def exigir_scope(req, scope):
|
|
"""Lanza 403 (HTTP 403) si la clave no tiene el alcance indicado."""
|
|
if not clave_permite_scope(req, scope):
|
|
raise Forbidden(description='La clave de API no tiene el alcance "%s".' % scope)
|
|
|
|
|
|
class IrHttp(models.AbstractModel):
|
|
_inherit = 'ir.http'
|
|
|
|
@classmethod
|
|
def _dispatch(cls, endpoint):
|
|
respuesta = _respuesta_licencia()
|
|
if respuesta is not None:
|
|
return respuesta
|
|
return super()._dispatch(endpoint)
|
|
|
|
@classmethod
|
|
def _auth_method_api_key(cls):
|
|
raw = extraer_clave(request)
|
|
if not raw:
|
|
raise AccessDenied('Falta la clave de API (Authorization: Bearer ...).')
|
|
ICP = request.env['ir.config_parameter'].sudo()
|
|
legacy = ICP.get_param(LEGACY_KEY_PARAM) or LEGACY_KEY_DEFAULT
|
|
if hmac.compare_digest(raw, legacy):
|
|
request.api_key = None
|
|
request.api_key_legacy = True
|
|
request.update_env(user=SUPERUSER_ID)
|
|
return
|
|
key = None
|
|
if 'skeen.api.key' in request.env:
|
|
key = request.env['skeen.api.key'].sudo()._autenticar(raw)
|
|
if not key:
|
|
raise AccessDenied('Clave de API inválida o revocada.')
|
|
request.api_key = key
|
|
request.api_key_legacy = False
|
|
key._registrar_uso()
|
|
request.update_env(user=SUPERUSER_ID)
|