bemade-addons/unifi_integration/models/unifi_mfa.py
2025-03-26 08:46:03 -04:00

263 lines
10 KiB
Python

# -*- coding: utf-8 -*-
# These imports will work in an Odoo environment, even if your IDE marks them as not found
# pylint: disable=import-error
from odoo import models, fields, api, _
from odoo.exceptions import UserError, ValidationError
# pylint: enable=import-error
import logging
import requests
from datetime import datetime, timedelta
_logger = logging.getLogger(__name__)
class UnifiMfa(models.TransientModel):
"""Modèle transitoire pour l'authentification à deux facteurs UniFi
Ce modèle gère le processus d'authentification à deux facteurs pour les sites
qui ont activé cette fonctionnalité. Il est utilisé dans le flux d'authentification
pour collecter et valider les codes MFA.
"""
_name = 'unifi.mfa'
_description = 'UniFi MFA Authentication'
site_id = fields.Many2one(
comodel_name='unifi.site',
string='Site',
required=True,
ondelete='cascade',
help='Le site pour lequel l\'authentification à deux facteurs est requise'
)
mfa_code = fields.Char(
string='Code MFA',
required=True,
help='Code d\'authentification à deux facteurs fourni par l\'application d\'authentification'
)
mfa_token = fields.Char(
string='Token MFA',
help='Token temporaire reçu lors de la première étape d\'authentification'
)
auth_type = fields.Selection(
selection=[
('site_manager', 'Site Manager API'),
('controller', 'Controller API')
],
string='Type d\'authentification',
required=True,
default='site_manager',
help='Type d\'API pour lequel l\'authentification à deux facteurs est requise'
)
expiry = fields.Datetime(
string='Date d\'expiration',
help='Date et heure d\'expiration de ce code MFA'
)
@api.model_create_multi
def create(self, vals_list):
"""Surcharge de la méthode create pour définir la date d'expiration
Args:
vals_list (list): Liste de dictionnaires contenant les valeurs pour la création des enregistrements
Returns:
unifi.mfa: Les enregistrements créés
"""
for vals in vals_list:
# Définir une expiration par défaut (10 minutes)
if not vals.get('expiry'):
vals['expiry'] = fields.Datetime.now() + timedelta(minutes=10)
return super(UnifiMfa, self).create(vals_list)
def validate_mfa(self):
"""Valide le code MFA et poursuit le processus d'authentification
Cette méthode vérifie le code MFA fourni par l'utilisateur et, s'il est valide,
poursuit le processus d'authentification en créant une session.
Returns:
dict: Action à effectuer après la validation (redirection, message, etc.)
"""
self.ensure_one()
# Vérifier que le code n'est pas expiré
if self.expiry and self.expiry < fields.Datetime.now():
raise ValidationError(_("Le code MFA a expiré. Veuillez recommencer le processus d'authentification."))
try:
if self.auth_type == 'site_manager':
return self._validate_site_manager_mfa()
elif self.auth_type == 'controller':
return self._validate_controller_mfa()
else:
raise ValidationError(_("Type d'authentification non pris en charge."))
except Exception as e:
_logger.error("Erreur lors de la validation du code MFA: %s", str(e))
raise UserError(_("Erreur lors de la validation du code MFA: %s") % str(e))
def _validate_site_manager_mfa(self):
"""Valide le code MFA pour l'API Site Manager
Returns:
dict: Action à effectuer après la validation
"""
self.ensure_one()
site = self.site_id
if not site.api_key:
raise ValidationError(_("Clé API manquante pour le site."))
if not self.mfa_token:
raise ValidationError(_("Token MFA temporaire manquant."))
try:
# Simulation de l'appel API pour valider le code MFA
# Dans une implémentation réelle, cela ferait un appel à l'API UniFi
# URL de base pour l'API Site Manager
base_url = "https://sitemanager.ui.com/api"
# Préparer les données pour la requête
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {site.api_key}"
}
data = {
"mfa_token": self.mfa_token,
"mfa_code": self.mfa_code
}
# Simuler la réponse - dans une implémentation réelle, cela serait:
# response = requests.post(f"{base_url}/auth/mfa/validate", headers=headers, json=data)
# response.raise_for_status()
# result = response.json()
# Simuler un résultat positif
result = {
"success": True,
"token": "simulated_auth_token_after_mfa",
"csrf_token": "simulated_csrf_token",
"expiry": (datetime.now() + timedelta(hours=24)).isoformat()
}
# Créer une session d'authentification
expiry_date = datetime.fromisoformat(result["expiry"])
session = self.env['unifi.auth.session'].create_session(
site_id=site.id,
auth_type='site_manager',
token=result["token"],
csrf_token=result["csrf_token"],
expiry=expiry_date
)
# Retourner une action pour rediriger vers la fiche du site
return {
'type': 'ir.actions.act_window',
'res_model': 'unifi.site',
'res_id': site.id,
'view_mode': 'form',
'target': 'current',
'context': {'show_success_notification': True}
}
except Exception as e:
_logger.error("Erreur lors de la validation MFA pour Site Manager: %s", str(e))
raise UserError(_("Erreur lors de la validation du code MFA: %s") % str(e))
def _validate_controller_mfa(self):
"""Valide le code MFA pour l'API Controller
Returns:
dict: Action à effectuer après la validation
"""
self.ensure_one()
site = self.site_id
# L'API Controller actuelle ne supporte pas l'authentification à deux facteurs
# Cette méthode est incluse pour une future implémentation
raise UserError(_("L'authentification à deux facteurs n'est pas encore supportée pour l'API Controller."))
@api.model
def request_mfa_challenge(self, site_id, auth_type, username=None, password=None, api_key=None):
"""Demande un challenge MFA à l'API
Cette méthode initie le processus d'authentification à deux facteurs en demandant
un challenge à l'API. Elle retourne un token temporaire qui sera utilisé avec
le code MFA pour compléter l'authentification.
Args:
site_id (int): ID du site
auth_type (str): Type d'authentification ('site_manager' ou 'controller')
username (str, optional): Nom d'utilisateur pour l'API Controller
password (str, optional): Mot de passe pour l'API Controller
api_key (str, optional): Clé API pour l'API Site Manager
Returns:
dict: Informations sur le challenge MFA, incluant le token temporaire
"""
site = self.env['unifi.site'].browse(site_id)
if auth_type == 'site_manager':
if not api_key and not site.api_key:
raise ValidationError(_("Clé API manquante pour le site."))
api_key = api_key or site.api_key
try:
# Simulation de l'appel API pour demander un challenge MFA
# Dans une implémentation réelle, cela ferait un appel à l'API UniFi
# URL de base pour l'API Site Manager
base_url = "https://sitemanager.ui.com/api"
# Préparer les données pour la requête
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}"
}
# Simuler la réponse - dans une implémentation réelle, cela serait:
# response = requests.post(f"{base_url}/auth/mfa/challenge", headers=headers)
# response.raise_for_status()
# result = response.json()
# Simuler un résultat positif
result = {
"success": True,
"mfa_token": "simulated_temporary_mfa_token",
"expiry": (datetime.now() + timedelta(minutes=10)).isoformat()
}
# Créer un enregistrement MFA transitoire
mfa_record = self.create({
'site_id': site_id,
'auth_type': auth_type,
'mfa_token': result["mfa_token"],
'expiry': datetime.fromisoformat(result["expiry"])
})
return {
'type': 'ir.actions.act_window',
'res_model': 'unifi.mfa',
'res_id': mfa_record.id,
'view_mode': 'form',
'target': 'new',
'context': {'default_site_id': site_id, 'default_auth_type': auth_type}
}
except Exception as e:
_logger.error("Erreur lors de la demande de challenge MFA: %s", str(e))
raise UserError(_("Erreur lors de la demande de challenge MFA: %s") % str(e))
elif auth_type == 'controller':
# L'API Controller actuelle ne supporte pas l'authentification à deux facteurs
raise UserError(_("L'authentification à deux facteurs n'est pas encore supportée pour l'API Controller."))
else:
raise ValidationError(_("Type d'authentification non pris en charge."))