# -*- 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 `, 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)