systeme:documenso:minio
Différences
Ci-dessous, les différences entre deux révisions de la page.
| Les deux révisions précédentesRévision précédenteProchaine révision | Révision précédente | ||
| systeme:documenso:minio [2026/04/11 14:50] – [Webhook Python] techer.charles_educ-valadon-limoges.fr | systeme:documenso:minio [2026/04/11 17:55] (Version actuelle) – [Webhook Python] techer.charles_educ-valadon-limoges.fr | ||
|---|---|---|---|
| Ligne 152: | Ligne 152: | ||
| <WRAP center round info> | <WRAP center round info> | ||
| - | Pour sécuriser le webhook de MinIO vers le Proxy, utilisation d'une signature HMAC (RECOMMANDÉE) et vérification dans **main.py** | + | Pour sécuriser le webhook de MinIO vers le Proxy, utilisation d'un secret partagé |
| + | </ | ||
| + | |||
| + | <WRAP center round info> | ||
| + | Gérer les fichiers volumineux : | ||
| + | |||
| + | Microsoft Graph refuse : | ||
| + | * les PUT /content simples | ||
| + | * dès que le fichier dépasse ~4–10 Mo (limite variable) | ||
| + | |||
| + | Pour des fichiers > | ||
| + | * upload en session + chunks obligatoire | ||
| + | |||
| + | Principe Graph (officiel) | ||
| + | * Créer une upload session | ||
| + | * Envoyer le fichier par blocs (chunks) | ||
| + | * Graph reconstruit le fichier côté SharePoint | ||
| + | </ | ||
| + | |||
| + | <WRAP center round info> | ||
| + | Gestion de plusieurs dossiers dans une seule équipe SharePoint : | ||
| + | * dossiers automatiquement créés dans Sharepoint | ||
| + | * aucun droit d’écriture manuel côté SharePoint | ||
| + | * traçabilité MinIO → SharePoint | ||
| + | |||
| + | Structure : | ||
| + | < | ||
| + | Documents | ||
| + | ├── administration | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | ├── bts-sio | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | ├── bts-mco | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | </ | ||
| + | |||
| + | Mapping pédagogique MinIO → SharePoint (préfixe MinIO = dossier SharePoint) | ||
| + | ^ MinIO (bucket pedagogie) | ||
| + | |administration/ | ||
| + | |bts-sio/ | ||
| + | |bts-sio/ | ||
| </ | </ | ||
| Ligne 158: | Ligne 204: | ||
| <code python main.py> | <code python main.py> | ||
| from flask import Flask, request, abort | from flask import Flask, request, abort | ||
| - | import | + | import |
| from graph import graph_request | from graph import graph_request | ||
| - | |||
| - | SHARED_SECRET = os.getenv(" | ||
| - | |||
| - | app = Flask(__name__) | ||
| TENANT_ID = os.getenv(" | TENANT_ID = os.getenv(" | ||
| Ligne 170: | Ligne 212: | ||
| GRAPH_BASE = " | GRAPH_BASE = " | ||
| + | WEBHOOK_SECRET = os.getenv(" | ||
| + | if not WEBHOOK_SECRET: | ||
| + | raise RuntimeError(" | ||
| - | def verify_hmac(request): | + | # Détection de la taille du fichier / Upload thresholds |
| - | | + | MAX_SIMPLE_UPLOAD |
| - | if not signature: | + | CHUNK_SIZE = 10 * 1024 * 1024 # 10 MB |
| - | | + | |
| - | body = request.data | + | app = Flask(__name__) |
| - | expected = hmac.new( | + | |
| - | | + | |
| - | ).hexdigest() | + | # ───────────────────────────── |
| + | # Sécurité webhook MinIO | ||
| + | # ───────────────────────────── | ||
| + | def verify_minio_webhook(req): | ||
| + | auth = req.headers.get(" | ||
| - | if not hmac.compare_digest(signature, expected): | + | if not auth or not auth.startswith(" |
| - | abort(403) | + | abort(401, " |
| + | token = auth[len(" | ||
| + | if token != WEBHOOK_SECRET: | ||
| + | abort(403, " | ||
| + | | ||
| + | # ───────────────────────────── | ||
| + | # SharePoint drive | ||
| + | # ───────────────────────────── | ||
| def get_drive_id(): | def get_drive_id(): | ||
| site = graph_request( | site = graph_request( | ||
| Ligne 201: | Ligne 256: | ||
| print(f" | print(f" | ||
| + | # ───────────────────────────── | ||
| + | # Upload helpers | ||
| + | # ───────────────────────────── | ||
| + | def create_upload_session(sp_path): | ||
| + | res = graph_request( | ||
| + | " | ||
| + | f" | ||
| + | json={ | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | } | ||
| + | ) | ||
| + | return res[" | ||
| + | |||
| + | |||
| + | def upload_simple(sp_path, | ||
| + | graph_request( | ||
| + | " | ||
| + | f" | ||
| + | data=data | ||
| + | ) | ||
| + | |||
| + | |||
| + | def upload_chunked(sp_path, | ||
| + | upload_url = create_upload_session(sp_path) | ||
| + | size = os.path.getsize(file_path) | ||
| + | |||
| + | with open(file_path, | ||
| + | start = 0 | ||
| + | while start < size: | ||
| + | chunk = f.read(CHUNK_SIZE) | ||
| + | end = start + len(chunk) - 1 | ||
| + | |||
| + | headers = { | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | |||
| + | r = requests.put(upload_url, | ||
| + | r.raise_for_status() | ||
| + | start += len(chunk) | ||
| + | |||
| + | |||
| + | # ───────────────────────────── | ||
| + | # Event handlers | ||
| + | # ───────────────────────────── | ||
| + | def upload_object(key): | ||
| + | sp_path = map_mapping_path(key) | ||
| + | |||
| + | if not sp_path: | ||
| + | print(f" | ||
| + | return | ||
| + | |||
| + | print(f" | ||
| + | |||
| + | # Télécharger depuis MinIO → /tmp (à implémenter) | ||
| + | file_path = f"/ | ||
| + | |||
| + | size = os.path.getsize(file_path) | ||
| + | |||
| + | if size <= MAX_SIMPLE_UPLOAD: | ||
| + | with open(file_path, | ||
| + | upload_simple(key, | ||
| + | else: | ||
| + | upload_chunked(key, | ||
| + | |||
| + | os.remove(file_path) | ||
| + | |||
| + | |||
| + | def delete_object(key): | ||
| + | sp_path = map_mapping_path(key) | ||
| + | |||
| + | if not sp_path: | ||
| + | return | ||
| + | |||
| + | print(f" | ||
| + | |||
| + | graph_request( | ||
| + | " | ||
| + | f" | ||
| + | ) | ||
| + | |||
| + | # ───────────────────────────── | ||
| + | # Mappping | ||
| + | # ───────────────────────────── | ||
| + | def map_mapping_path(key: | ||
| + | """ | ||
| + | Transforme une clé MinIO en chemin SharePoint autorisé. | ||
| + | Retourne None si le chemin n'est pas declare. | ||
| + | """ | ||
| + | |||
| + | key = key.lstrip("/" | ||
| + | |||
| + | ALLOWED_PREFIXES = [ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | ] | ||
| + | |||
| + | for prefix in ALLOWED_PREFIXES: | ||
| + | if key.startswith(prefix): | ||
| + | return key | ||
| + | |||
| + | # Tout le reste est ignoré | ||
| + | return None | ||
| + | |||
| + | |||
| + | |||
| + | # ───────────────────────────── | ||
| + | # Webhook endpoint | ||
| + | # ───────────────────────────── | ||
| @app.route("/ | @app.route("/ | ||
| def s3_event(): | def s3_event(): | ||
| - | event = request.json | + | |
| + | | ||
| record = event[" | record = event[" | ||
| event_name = record[" | event_name = record[" | ||
| Ligne 214: | Ligne 382: | ||
| return "", | return "", | ||
| - | |||
| - | def upload_object(key): | ||
| - | print(f" | ||
| - | url = f" | ||
| - | graph_request(" | ||
| - | |||
| - | def delete_object(key): | ||
| - | print(f" | ||
| - | url = f" | ||
| - | graph_request(" | ||
| if __name__ == " | if __name__ == " | ||
| Ligne 283: | Ligne 441: | ||
| AUTHORITY=https:// | AUTHORITY=https:// | ||
| GRAPH_SCOPE=https:// | GRAPH_SCOPE=https:// | ||
| + | |||
| + | WEBHOOK_SECRET=" | ||
| DOCUMENT_LIBRARY=" | DOCUMENT_LIBRARY=" | ||
| Ligne 419: | Ligne 579: | ||
| </ | </ | ||
| - | Pour sécuriser le webhook MinIO vers le Proxy, utilisation d'une signature HMAC (RECOMMANDÉE) | + | Pour sécuriser le webhook MinIO vers le Proxy, utilisation d'un header statique contenant un token pour empêcher toute requête non légitime d’appeler /s3event. |
| - | * signature du webhook par MinIO, | + | |
| - | * vérification de la signature par le proxy. | + | |
| - | + | ||
| - | * Configurer le webhook avec HMAC : | + | * Configurer le webhook avec le token : |
| < | < | ||
| Ligne 495: | Ligne 653: | ||
| Please restart your server with `mc admin service restart minio`. | Please restart your server with `mc admin service restart minio`. | ||
| </ | </ | ||
| + | |||
| + | * créer le bucket | ||
| + | |||
| + | < | ||
| + | mc mb minio/lycee | ||
| + | |||
| + | => résultat attendu | ||
| + | Bucket created successfully `minio/ | ||
| + | </ | ||
| + | |||
| + | * vérification | ||
| + | |||
| + | < | ||
| + | mc ls minio | ||
| + | </ | ||
| + | |||
| * Lier le bucket aux événements afin que MinIO envoie les données | * Lier le bucket aux événements afin que MinIO envoie les données | ||
| + | |||
| + | <WRAP center round info> | ||
| + | Filtrage côté MinIO (RECOMMANDÉ) : le proxy ne reçoit QUE ce qui est attendu | ||
| + | </ | ||
| + | |||
| < | < | ||
| - | mc event add minio/test arn: | + | mc event add minio/lycee arn: |
| + | | ||
| + | --prefix administration/ | ||
| + | --prefix bts-sio/ \ | ||
| + | --prefix bts-mco/ | ||
| + | |||
| => doit renvoyer | => doit renvoyer | ||
| Ligne 571: | Ligne 756: | ||
| * Fichier supprimé dans SharePoint | * Fichier supprimé dans SharePoint | ||
| - | ===== Sécuriser le webhook MinIO → Proxy ===== | + | ===== Gérer les fichiers volumineux (Upload Microsoft Graph en chunks > |
| + | Microsoft Graph refuse : | ||
| + | * les PUT /content simples | ||
| + | * dès que le fichier dépasse ~4–10 Mo (limite variable) | ||
| + | * Pour des fichiers > | ||
| - | Utilisation d'une signature HMAC (RECOMMANDÉE) pour empêcher toute requête non légitime d’appeler /s3event : | + | Principe Graph (officiel) |
| - | * signature du webhook par MinIO, | + | * Créer une upload session |
| - | * vérification de la signature | + | * Envoyer le fichier |
| + | * Graph reconstruit | ||
| - | ==== Configuration MinIO ==== | + | ==== Détection de la taille |
| - | * Créer une clé secrète partagée : | + | <code python> |
| + | import os | ||
| - | < | + | MAX_SIMPLE_UPLOAD |
| - | export WEBHOOK_SECRET=" | + | |
| - | </ | + | |
| - | * Configurer le webhook avec HMAC : | + | size = os.path.getsize(file_path) |
| - | <code> | + | if size <= MAX_SIMPLE_UPLOAD: |
| - | mc admin config set minio notify_webhook: | + | |
| - | endpoint="http:// | + | else: |
| - | | + | |
| - | + | ||
| - | mc admin service restart minio | + | |
| </ | </ | ||
| - | * vérification côté proxy (Flask) | + | * Création de la session d’upload |
| - | Dans main.py | ||
| <code python> | <code python> | ||
| - | import hmac, hashlib, os | + | def create_upload_session(drive_id, sp_path): |
| - | from flask import abort | + | return graph_request( |
| + | " | ||
| + | | ||
| + | json={ | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | } | ||
| + | )[" | ||
| + | </ | ||
| - | SHARED_SECRET = os.getenv(" | + | * Upload par chunks |
| - | def verify_hmac(request): | + | < |
| - | | + | import requests |
| - | if not signature: | + | |
| - | abort(401) | + | |
| - | body = request.data | + | def upload_chunked(sp_path, |
| - | | + | |
| - | SHARED_SECRET, body, hashlib.sha256 | + | |
| - | ).hexdigest() | + | |
| - | | + | |
| - | abort(403) | + | size = os.path.getsize(local_file) |
| - | </ | + | |
| - | * dans la route | + | with open(local_file, |
| + | start = 0 | ||
| + | while start < size: | ||
| + | data = f.read(chunk_size) | ||
| + | end = start + len(data) - 1 | ||
| + | |||
| + | headers = { | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | |||
| + | r = requests.put(upload_url, | ||
| + | r.raise_for_status() | ||
| + | |||
| + | start += chunk_size | ||
| - | < | ||
| - | @app.route("/ | ||
| - | def s3_event(): | ||
| - | verify_hmac(request) | ||
| - | ... | ||
| </ | </ | ||
systeme/documenso/minio.1775911825.txt.gz · Dernière modification : 2026/04/11 14:50 de techer.charles_educ-valadon-limoges.fr
