From 8f276b5c472e2df282a880826561ae9be5f04d02 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 14 Aug 2026 21:46:00 +0000 Subject: [PATCH] =?UTF-8?q?S=C3=A9curise=20et=20supervise=20les=20int?= =?UTF-8?q?=C3=A9grations=20externes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 1 + app_new/ai_config/routes.py | 9 ++++--- .../ai_config/templates/ai_config/index.html | 3 ++- app_new/core/models/settings.py | 27 ++++++++++++++++--- app_new/gmao_config/routes.py | 8 +++--- app_new/logs/routes.py | 17 +++++++++++- app_new/logs/templates/logs/index.html | 1 + .../integration/test_integration_security.py | 26 ++++++++++++++++++ 8 files changed, 80 insertions(+), 12 deletions(-) create mode 100644 tests/integration/test_integration_security.py diff --git a/Dockerfile b/Dockerfile index cd16467..fbffbc6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -67,6 +67,7 @@ COPY --chown=1000:1000 migrations /app/migrations COPY --chown=1000:1000 VERSION /app/VERSION COPY --chown=1000:1000 wsgi.py run_app_new.py /app/ COPY --chown=1000:1000 gmao_watchdog.py ent_watchdog.py pronote_watchdog.py watchdog_dnd.py /app/ +COPY --chown=1000:1000 watchdog_gate.py /app/watchdog_gate.py COPY --chown=1000:1000 docker/backup.py docker/backup_loop.py /app/ # Tracabilite du build (surchargeable avec --build-arg GIT_VERSION=...) diff --git a/app_new/ai_config/routes.py b/app_new/ai_config/routes.py index daf72f4..6a9f472 100644 --- a/app_new/ai_config/routes.py +++ b/app_new/ai_config/routes.py @@ -59,7 +59,8 @@ def index(): has_key=bool(api_key), model=model, model_status=model_status, - model_error=model_error) + model_error=model_error, + human_approval=True) @ai_config_bp.route('/save', methods=['POST']) @@ -68,7 +69,7 @@ def save(): """Sauvegarde la configuration IA.""" # Cle API api_key = request.form.get('api_key', '').strip() - if api_key and api_key != '***' and not api_key.startswith('***'): + if api_key and '...' not in api_key and api_key != '***' and not api_key.startswith('***'): AppSettings.set('openrouter_api_key', api_key, description='Cle API OpenRouter pour l\'interpretation des emails', is_encrypted=True) @@ -78,6 +79,8 @@ def save(): if model: AppSettings.set('openrouter_model', model, description='Modele OpenRouter pour l\'interpretation IA') + AppSettings.set('ai_requires_human_approval', 'true', + description='Validation humaine obligatoire avant action métier') flash('Configuration IA sauvegardee.', 'success') return redirect(url_for('ai_config.index')) @@ -155,4 +158,4 @@ def api_status(): else: return jsonify({'status': 'error', 'model': model, 'message': f'HTTP {resp.status_code}'}) except Exception as e: - return jsonify({'status': 'error', 'model': model, 'message': str(e)[:100]}) \ No newline at end of file + return jsonify({'status': 'error', 'model': model, 'message': str(e)[:100]}) diff --git a/app_new/ai_config/templates/ai_config/index.html b/app_new/ai_config/templates/ai_config/index.html index 168c482..d1b9e02 100644 --- a/app_new/ai_config/templates/ai_config/index.html +++ b/app_new/ai_config/templates/ai_config/index.html @@ -11,6 +11,7 @@

Configuration IA (OpenRouter)

+
Validation humaine obligatoire : l’IA produit uniquement des propositions. Elle ne crée, ne modifie et ne clôture jamais automatiquement une intervention.
@@ -129,4 +130,4 @@ document.getElementById('btn-test-model').addEventListener('click', async functi btn.innerHTML = original; }); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app_new/core/models/settings.py b/app_new/core/models/settings.py index 63935e7..b03805a 100644 --- a/app_new/core/models/settings.py +++ b/app_new/core/models/settings.py @@ -3,6 +3,12 @@ Modèles pour les paramètres de l'application """ from datetime import datetime, timezone from app_new.extensions import db +import os + +def _cipher(): + from cryptography.fernet import Fernet + key = os.environ.get('OUTLOOK_ENCRYPTION_KEY') + return Fernet(key.encode()) if key else None class AppSettings(db.Model): @@ -22,22 +28,37 @@ class AppSettings(db.Model): """Récupère un paramètre par sa clé.""" setting = AppSettings.query.filter_by(key=key).first() if setting: + if setting.is_encrypted and setting.value and setting.value.startswith('enc:'): + cipher = _cipher() + if not cipher: + return default + try: + return cipher.decrypt(setting.value[4:].encode()).decode() + except Exception: + return default return setting.value return default @staticmethod def set(key, value, description=None, is_encrypted=False): """Définit un paramètre.""" + stored_value = value + if is_encrypted and value: + cipher = _cipher() + if not cipher: + raise RuntimeError("OUTLOOK_ENCRYPTION_KEY est requise pour stocker ce secret.") + stored_value = 'enc:' + cipher.encrypt(value.encode()).decode() setting = AppSettings.query.filter_by(key=key).first() if setting: - setting.value = value + setting.value = stored_value + setting.is_encrypted = is_encrypted setting.updated_at = datetime.now(timezone.utc) if description: setting.description = description else: setting = AppSettings( key=key, - value=value, + value=stored_value, description=description, is_encrypted=is_encrypted ) @@ -61,4 +82,4 @@ class AppSettings(db.Model): ) def __repr__(self): - return f'' \ No newline at end of file + return f'' diff --git a/app_new/gmao_config/routes.py b/app_new/gmao_config/routes.py index f0b7d9b..e286210 100644 --- a/app_new/gmao_config/routes.py +++ b/app_new/gmao_config/routes.py @@ -26,7 +26,7 @@ def index(): outlook_accounts = OutlookAccount.query.filter_by(user_id=current_user.id).all() # Recuperer la cle API et le modele OpenRouter - openrouter_key = AppSettings.get('openrouter_api_key') or '' + openrouter_key = '••••••••' if AppSettings.get('openrouter_api_key') else '' openrouter_model = AppSettings.get('openrouter_model', 'poolside/laguna-s-2.1:free') ollama_model = AppSettings.get('ollama_model', 'gemma3:4b-it') @@ -80,8 +80,8 @@ def save(): # Configuration IA from app_new.core.models.settings import AppSettings api_key = request.form.get('openrouter_api_key', '').strip() - if api_key: - AppSettings.set('openrouter_api_key', api_key) + if api_key and api_key != '••••••••': + AppSettings.set('openrouter_api_key', api_key, is_encrypted=True) model = request.form.get('openrouter_model', '').strip() if model: AppSettings.set('openrouter_model', model) @@ -150,4 +150,4 @@ def test(): 'success': True, 'analysis': result, 'context_summary': context.get_full_context()[:200] + '...' if context.get_full_context() else 'Vide' - }) \ No newline at end of file + }) diff --git a/app_new/logs/routes.py b/app_new/logs/routes.py index 86513bd..29c7dfe 100644 --- a/app_new/logs/routes.py +++ b/app_new/logs/routes.py @@ -1,6 +1,8 @@ """Logs centralises des watchdogs.""" from flask import Blueprint, render_template, request, jsonify, flash, redirect, url_for from flask_login import login_required +from datetime import datetime, timezone, timedelta +from app_new.core.models.settings import AppSettings from sqlalchemy import text from app_new.extensions import db from app_new.core.models.maintenance import WatchdogLog @@ -34,7 +36,8 @@ def index(): return render_template('logs/index.html', pagination=pagination, watchdogs=watchdogs, - filters={'level': level, 'watchdog': watchdog}) + filters={'level': level, 'watchdog': watchdog}, + retention_days=int(AppSettings.get('watchdog_log_retention_days', '90'))) @logs_bp.route('/api/recent') @@ -69,3 +72,15 @@ def clear_watchdog_logs(): db.session.commit() flash('Les journaux des watchdogs ont été vidés.', 'success') return redirect(url_for('logs.index')) + + +@logs_bp.route('/retention', methods=['POST']) +@system_admin_required +def apply_retention(): + days = max(1, min(request.form.get('days', type=int) or 90, 3650)) + cutoff = datetime.now(timezone.utc) - timedelta(days=days) + deleted = WatchdogLog.query.filter(WatchdogLog.created_at < cutoff).delete(synchronize_session=False) + db.session.commit() + AppSettings.set('watchdog_log_retention_days', str(days), 'Durée de conservation des logs watchdog') + flash(f'Rétention fixée à {days} jours ; {deleted} ancien(s) journal(aux) supprimé(s).', 'success') + return redirect(url_for('logs.index')) diff --git a/app_new/logs/templates/logs/index.html b/app_new/logs/templates/logs/index.html index e9fcbea..f439165 100644 --- a/app_new/logs/templates/logs/index.html +++ b/app_new/logs/templates/logs/index.html @@ -7,6 +7,7 @@
API JSON {% if current_user.is_admin() %} +