Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bc4770e2d | ||
|
|
cb84e20e9a |
@@ -16,7 +16,10 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build image
|
||||
run: docker build -t "${IMAGE}:latest" -t "${IMAGE}:${GITHUB_SHA::12}" .
|
||||
run: |
|
||||
docker build \
|
||||
--build-arg APP_BUILD="${GITHUB_SHA::12}" \
|
||||
-t "${IMAGE}:latest" -t "${IMAGE}:${GITHUB_SHA::12}" .
|
||||
|
||||
- name: Tests (pytest dans l'image, BDD simulée)
|
||||
run: |
|
||||
|
||||
@@ -17,5 +17,10 @@ COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY . .
|
||||
|
||||
# SHA du commit construit, injecté par la CI et exposé par GET /health :
|
||||
# permet de vérifier quelle version tourne réellement après un déploiement.
|
||||
ARG APP_BUILD=local
|
||||
ENV APP_BUILD=$APP_BUILD
|
||||
|
||||
EXPOSE 8000
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
|
||||
@@ -34,7 +34,8 @@ sur `localhost`. Pour une instance nommée, définir `DB_SERVER` (ex.
|
||||
| `DB_TRUSTED_CONNECTION` | force l'authentification Windows même si `DB_USER` est défini | — |
|
||||
| `DB_DRIVER` | pilote ODBC | `ODBC Driver 18 for SQL Server` |
|
||||
| `CORS_ORIGINS` | origines autorisées (séparées par `,`) | `localhost:5173,localhost:3000` |
|
||||
| `JWT_SECRET` | clé de signature JWT | placeholder (à définir en prod) |
|
||||
| `JWT_SECRET` | clé de signature JWT — **obligatoire en déploiement** | clé aléatoire régénérée à chaque démarrage |
|
||||
| `APP_BUILD` | SHA du commit construit, injecté par la CI et exposé par `/health` | `local` |
|
||||
| `JWT_ALGORITHM` / `JWT_EXPIRE_MINUTES` | algo / durée du token | `HS256` / `60` |
|
||||
|
||||
## Authentification & rôles
|
||||
@@ -96,8 +97,26 @@ routeurs). Chaque domaine fonctionnel vit dans `routers/` :
|
||||
|
||||
Spécification complète : `GET /openapi.json` (export dans `docs/openapi.json`).
|
||||
|
||||
## Vérifier qu'un déploiement a pris
|
||||
|
||||
`GET /health` expose le SHA du commit dont l'image a été construite :
|
||||
|
||||
```bash
|
||||
curl -s https://datasentinel-api.nfteam.ovh/health
|
||||
# {"api":"ok","database":"ok","version":"1.0.0","build":"cb84e20e1f2a",...}
|
||||
```
|
||||
|
||||
Comparer `build` au dernier commit poussé sur `main`. S'ils diffèrent, le
|
||||
conteneur tourne encore une ancienne image : `docker compose pull` puis
|
||||
`docker compose up -d` (un `up -d` seul ne retélécharge rien). Le front expose
|
||||
la même information sur `/version.json`.
|
||||
|
||||
## Sécurité
|
||||
|
||||
- `JWT_SECRET` doit être défini en déploiement. À défaut, l'API démarre quand
|
||||
même mais tire une clé aléatoire à chaque lancement (sessions perdues au
|
||||
redémarrage) : aucun secret de repli n'est écrit dans le dépôt, un secret
|
||||
public permettrait de forger un jeton d'administrateur.
|
||||
- En-têtes : `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Strict-Transport-Security`.
|
||||
- CORS restreint aux origines `CORS_ORIGINS`, tous verbes + credentials.
|
||||
- Requêtes SQL **paramétrées** (noms de tables/colonnes whitelistés) ; le compte applicatif
|
||||
|
||||
@@ -3,10 +3,13 @@
|
||||
# Data Sentinel | COYAUD Anthony | 2026
|
||||
# ============================================================
|
||||
|
||||
import logging
|
||||
import os
|
||||
import pyodbc
|
||||
import secrets
|
||||
from contextlib import contextmanager
|
||||
|
||||
import pyodbc
|
||||
|
||||
|
||||
def _build_connection_string() -> str:
|
||||
"""
|
||||
@@ -45,6 +48,33 @@ def _build_connection_string() -> str:
|
||||
)
|
||||
|
||||
|
||||
def _resolve_jwt_secret() -> str:
|
||||
"""
|
||||
Clé de signature des jetons.
|
||||
|
||||
Elle doit venir de JWT_SECRET. À défaut, on tire une clé aléatoire au
|
||||
démarrage plutôt que de retomber sur une valeur écrite dans le dépôt :
|
||||
un secret public permettrait à quiconque lit le code de forger un jeton
|
||||
d'administrateur.
|
||||
|
||||
Conséquence assumée du repli : la clé change à chaque redémarrage, donc
|
||||
les sessions en cours sont invalidées. C'est visible et sans gravité en
|
||||
développement, et le message ci-dessous dit quoi faire en déploiement.
|
||||
On ne bloque volontairement pas le démarrage, pour ne pas transformer un
|
||||
oubli de configuration en indisponibilité totale du service.
|
||||
"""
|
||||
secret = os.getenv("JWT_SECRET")
|
||||
if secret:
|
||||
return secret
|
||||
|
||||
logging.getLogger("uvicorn.error").warning(
|
||||
"JWT_SECRET n'est pas defini : une cle aleatoire est generee pour cette "
|
||||
"execution. Les sessions seront perdues a chaque redemarrage. "
|
||||
"Definir JWT_SECRET dans l'environnement (api.env) pour un deploiement."
|
||||
)
|
||||
return secrets.token_urlsafe(64)
|
||||
|
||||
|
||||
class Config:
|
||||
# Chaîne de connexion SQL Server (env en prod, Windows en local)
|
||||
DB_CONNECTION_STRING = _build_connection_string()
|
||||
@@ -54,8 +84,12 @@ class Config:
|
||||
API_VERSION = "1.0.0"
|
||||
API_DESCRIPTION = "API de monitoring de la qualité des données — XEFI"
|
||||
|
||||
# Identifiant de build injecté par la CI : permet de vérifier quelle
|
||||
# version tourne réellement après un déploiement (voir GET /health).
|
||||
BUILD = os.getenv("APP_BUILD", "local")
|
||||
|
||||
# Sécurité JWT
|
||||
SECRET_KEY = os.getenv("JWT_SECRET", "data-sentinel-secret-change-in-prod")
|
||||
SECRET_KEY = _resolve_jwt_secret()
|
||||
ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256")
|
||||
TOKEN_EXPIRE_MINUTES = int(os.getenv("JWT_EXPIRE_MINUTES", "60"))
|
||||
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -81,7 +81,13 @@ async def security_headers(request: Request, call_next):
|
||||
|
||||
@app.get("/health", tags=["Système"])
|
||||
def health_check():
|
||||
"""Ping API + test connexion SQL Server."""
|
||||
"""
|
||||
Ping API + test connexion SQL Server.
|
||||
|
||||
`build` porte le SHA du commit dont l'image a été construite (injecté par
|
||||
la CI). C'est le seul moyen fiable de vérifier qu'un déploiement a bien
|
||||
pris : comparer ce champ au dernier commit poussé.
|
||||
"""
|
||||
try:
|
||||
with get_cursor() as cursor:
|
||||
cursor.execute("SELECT 1")
|
||||
@@ -94,6 +100,7 @@ def health_check():
|
||||
"api" : "ok",
|
||||
"database" : db_status,
|
||||
"version" : Config.API_VERSION,
|
||||
"build" : Config.BUILD,
|
||||
"nb_monitorings" : len(MONITO_TABLES),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user