Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bf83d0a7f | ||
|
|
4bc4770e2d |
@@ -16,7 +16,10 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Build image
|
- 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)
|
- name: Tests (pytest dans l'image, BDD simulée)
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -17,5 +17,10 @@ COPY requirements.txt .
|
|||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
COPY . .
|
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
|
EXPOSE 8000
|
||||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "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_TRUSTED_CONNECTION` | force l'authentification Windows même si `DB_USER` est défini | — |
|
||||||
| `DB_DRIVER` | pilote ODBC | `ODBC Driver 18 for SQL Server` |
|
| `DB_DRIVER` | pilote ODBC | `ODBC Driver 18 for SQL Server` |
|
||||||
| `CORS_ORIGINS` | origines autorisées (séparées par `,`) | `localhost:5173,localhost:3000` |
|
| `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` |
|
| `JWT_ALGORITHM` / `JWT_EXPIRE_MINUTES` | algo / durée du token | `HS256` / `60` |
|
||||||
|
|
||||||
## Authentification & rôles
|
## 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`).
|
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é
|
## 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`.
|
- En-têtes : `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Strict-Transport-Security`.
|
||||||
- CORS restreint aux origines `CORS_ORIGINS`, tous verbes + credentials.
|
- CORS restreint aux origines `CORS_ORIGINS`, tous verbes + credentials.
|
||||||
- Requêtes SQL **paramétrées** (noms de tables/colonnes whitelistés) ; le compte applicatif
|
- Requêtes SQL **paramétrées** (noms de tables/colonnes whitelistés) ; le compte applicatif
|
||||||
|
|||||||
@@ -3,10 +3,13 @@
|
|||||||
# Data Sentinel | COYAUD Anthony | 2026
|
# Data Sentinel | COYAUD Anthony | 2026
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import pyodbc
|
import secrets
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
import pyodbc
|
||||||
|
|
||||||
|
|
||||||
def _build_connection_string() -> str:
|
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:
|
class Config:
|
||||||
# Chaîne de connexion SQL Server (env en prod, Windows en local)
|
# Chaîne de connexion SQL Server (env en prod, Windows en local)
|
||||||
DB_CONNECTION_STRING = _build_connection_string()
|
DB_CONNECTION_STRING = _build_connection_string()
|
||||||
@@ -54,8 +84,12 @@ class Config:
|
|||||||
API_VERSION = "1.0.0"
|
API_VERSION = "1.0.0"
|
||||||
API_DESCRIPTION = "API de monitoring de la qualité des données — XEFI"
|
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
|
# 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")
|
ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256")
|
||||||
TOKEN_EXPIRE_MINUTES = int(os.getenv("JWT_EXPIRE_MINUTES", "60"))
|
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"])
|
@app.get("/health", tags=["Système"])
|
||||||
def health_check():
|
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:
|
try:
|
||||||
with get_cursor() as cursor:
|
with get_cursor() as cursor:
|
||||||
cursor.execute("SELECT 1")
|
cursor.execute("SELECT 1")
|
||||||
@@ -94,6 +100,7 @@ def health_check():
|
|||||||
"api" : "ok",
|
"api" : "ok",
|
||||||
"database" : db_status,
|
"database" : db_status,
|
||||||
"version" : Config.API_VERSION,
|
"version" : Config.API_VERSION,
|
||||||
|
"build" : Config.BUILD,
|
||||||
"nb_monitorings" : len(MONITO_TABLES),
|
"nb_monitorings" : len(MONITO_TABLES),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user