Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6afc4f1fa | ||
|
|
fd4c085ee4 |
@@ -44,11 +44,23 @@ Sans variables d'environnement, l'API se connecte en authentification Windows
|
|||||||
|
|
||||||
### Comptes de démo
|
### Comptes de démo
|
||||||
|
|
||||||
| Identifiant | Mot de passe | Rôle |
|
| Identifiant | Mot de passe | Rôle | Usage |
|
||||||
|-------------|--------------|------|
|
|-------------|--------------|------|-------|
|
||||||
| `admin` | `Admin2026!` | Admin |
|
| `Juré` (`JURE@NEXA.com`) | `123456` | Admin | **Compte de visite / évaluation** — à communiquer aux personnes qui consultent le site |
|
||||||
| `superviseur` | `Super2026!` | Superviseur |
|
| `admin` | `Admin2026!` | Admin | Compte d'administration technique |
|
||||||
| `consultant` | `Conseil2026!` | Consultant |
|
| `superviseur` | `Super2026!` | Superviseur | Illustration du RBAC (pas d'accès `/admin/*`) |
|
||||||
|
| `consultant` | `Conseil2026!` | Consultant | Illustration du RBAC (lecture seule) |
|
||||||
|
|
||||||
|
Ces comptes sont créés par `sql/data_sentinel_auth.sql`. Pour (re)créer le seul
|
||||||
|
compte `Juré` sur une base déjà déployée, sans rejouer tout le script d'auth :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sqlcmd -S <serveur> -d DataSentinel -U <user> -P <mdp> -i sql/create_admin_jure.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
Les mots de passe ci-dessus sont des identifiants de démonstration : ils sont
|
||||||
|
stockés hachés (bcrypt, coût 12) et doivent être régénérés avant toute mise en
|
||||||
|
production réelle (`POST /admin/users/{id}/reset-password`).
|
||||||
|
|
||||||
## Endpoints (résumé)
|
## Endpoints (résumé)
|
||||||
|
|
||||||
@@ -65,7 +77,8 @@ Spécification complète : `GET /openapi.json` (export dans `docs/openapi.json`)
|
|||||||
- 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
|
||||||
n'est pas `sa`. Tables d'auth : `[USER]` + `JOURNAL_AUDIT` (`sql/data_sentinel_auth.sql`).
|
n'est pas `sa`. Tables d'auth : `[USER]` + `JOURNAL_AUDIT` (`sql/data_sentinel_auth.sql`,
|
||||||
|
identique au dump livré dans `RENDU/02_Dump_SQL/`).
|
||||||
- Journal d'audit alimenté à chaque login + action admin.
|
- Journal d'audit alimenté à chaque login + action admin.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -331,6 +331,33 @@ def get_dashboard(
|
|||||||
return rows_to_list(cursor, cursor.fetchall())
|
return rows_to_list(cursor, cursor.fetchall())
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/dashboard/filtres", tags=["Dashboard"])
|
||||||
|
def get_dashboard_filtres():
|
||||||
|
"""
|
||||||
|
Valeurs de filtre réellement présentes dans VUE_CONSO.
|
||||||
|
|
||||||
|
Les référentiels SERVICE / CATEGORIE contiennent des entrées auxquelles
|
||||||
|
aucun monitoring actif n'est rattaché (ex. « Business Intelligence »).
|
||||||
|
Les proposer dans les menus déroulants du dashboard mène à un écran vide :
|
||||||
|
cet endpoint ne renvoie que les valeurs qui ramènent au moins un monitoring.
|
||||||
|
|
||||||
|
`combinaisons` permet au frontend de restreindre les catégories proposées
|
||||||
|
au service sélectionné.
|
||||||
|
"""
|
||||||
|
with get_cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT DISTINCT service, categorie FROM VUE_CONSO "
|
||||||
|
"ORDER BY service, categorie"
|
||||||
|
)
|
||||||
|
combinaisons = [{"service": r[0], "categorie": r[1]} for r in cursor.fetchall()]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"services" : sorted({c["service"] for c in combinaisons}),
|
||||||
|
"categories" : sorted({c["categorie"] for c in combinaisons}),
|
||||||
|
"combinaisons" : combinaisons,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/dashboard/summary", tags=["Dashboard"])
|
@router.get("/dashboard/summary", tags=["Dashboard"])
|
||||||
def get_dashboard_summary():
|
def get_dashboard_summary():
|
||||||
"""KPI globaux pour les 4 cartes du dashboard."""
|
"""KPI globaux pour les 4 cartes du dashboard."""
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- DATA SENTINEL — Création / réinitialisation du compte « Juré »
|
||||||
|
-- Auteur : COYAUD Anthony
|
||||||
|
-- Version : 1.1 — Août 2026
|
||||||
|
--
|
||||||
|
-- Compte Admin destiné aux personnes qui consultent l'application
|
||||||
|
-- (évaluateurs, jury, démonstration).
|
||||||
|
--
|
||||||
|
-- identifiant : Juré
|
||||||
|
-- mail : JURE@NEXA.com
|
||||||
|
-- mot de passe : 123456
|
||||||
|
-- rôle : Admin
|
||||||
|
--
|
||||||
|
-- Script idempotent : il peut être rejoué sur une base déjà déployée
|
||||||
|
-- sans dupliquer le compte ni toucher aux autres utilisateurs.
|
||||||
|
-- Prérequis : data_sentinel_auth.sql déjà exécuté ([USER] + JOURNAL_AUDIT).
|
||||||
|
--
|
||||||
|
-- Pas d'instruction USE : la base cible vient du paramètre -d de la
|
||||||
|
-- connexion, et le garde-fou ci-dessous interrompt le script si ce
|
||||||
|
-- n'est pas la bonne. Tout tient dans un seul lot (aucun GO), sans
|
||||||
|
-- quoi le RETURN du garde-fou n'empêcherait pas la suite de tourner.
|
||||||
|
--
|
||||||
|
-- Exécution :
|
||||||
|
-- sqlcmd -S <serveur> -d <base> -U <user> -P <mdp> -C -b -f 65001 \
|
||||||
|
-- -i sql/create_admin_jure.sql
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
SET XACT_ABORT ON;
|
||||||
|
|
||||||
|
DECLARE @base NVARCHAR(128) = DB_NAME();
|
||||||
|
|
||||||
|
IF OBJECT_ID('[USER]', 'U') IS NULL OR OBJECT_ID('JOURNAL_AUDIT', 'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
RAISERROR(
|
||||||
|
'ARRET : base incorrecte (%s). [USER] ou JOURNAL_AUDIT est introuvable. Reconnectez-vous sur la base Data Sentinel (option -d).',
|
||||||
|
16, 1, @base);
|
||||||
|
RETURN;
|
||||||
|
END
|
||||||
|
|
||||||
|
-- Hachage bcrypt (coût 12) du mot de passe de démonstration.
|
||||||
|
-- Généré avec passlib.CryptContext(schemes=["bcrypt"]) — même librairie que auth.py.
|
||||||
|
DECLARE @username NVARCHAR(100) = N'Juré';
|
||||||
|
DECLARE @email NVARCHAR(200) = N'JURE@NEXA.com';
|
||||||
|
DECLARE @password_hash NVARCHAR(255) = N'$2b$12$nnWMOCjSiUq4rAyPGbC3W.HGdsvWKu9kEfLst6zBIEIcsgP97W94m';
|
||||||
|
|
||||||
|
BEGIN TRY
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM [USER] WHERE username = @username)
|
||||||
|
BEGIN
|
||||||
|
UPDATE [USER]
|
||||||
|
SET email = @email,
|
||||||
|
password_hash = @password_hash,
|
||||||
|
role = N'Admin',
|
||||||
|
actif = 1
|
||||||
|
WHERE username = @username;
|
||||||
|
|
||||||
|
INSERT INTO JOURNAL_AUDIT (id_user, username, action, detail)
|
||||||
|
SELECT id_user, username, 'RESET_PASSWORD', 'Compte de visite reinitialise (script SQL)'
|
||||||
|
FROM [USER] WHERE username = @username;
|
||||||
|
|
||||||
|
PRINT 'Compte "Jure" deja present -> mis a jour.';
|
||||||
|
END
|
||||||
|
ELSE
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO [USER] (username, email, password_hash, role, actif)
|
||||||
|
VALUES (@username, @email, @password_hash, N'Admin', 1);
|
||||||
|
|
||||||
|
INSERT INTO JOURNAL_AUDIT (id_user, username, action, detail)
|
||||||
|
SELECT id_user, username, 'CREATE_USER', 'Compte de visite cree (script SQL)'
|
||||||
|
FROM [USER] WHERE username = @username;
|
||||||
|
|
||||||
|
PRINT 'Compte "Jure" cree.';
|
||||||
|
END
|
||||||
|
|
||||||
|
COMMIT TRANSACTION;
|
||||||
|
END TRY
|
||||||
|
BEGIN CATCH
|
||||||
|
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
|
||||||
|
PRINT 'ECHEC : ' + ERROR_MESSAGE();
|
||||||
|
THROW;
|
||||||
|
END CATCH
|
||||||
|
|
||||||
|
-- Vérification
|
||||||
|
SELECT id_user, username, email, role, actif, created_at, last_login
|
||||||
|
FROM [USER]
|
||||||
|
WHERE username = N'Juré';
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- DATA SENTINEL — Script d'authentification SQL Server
|
||||||
|
-- Tables [USER] + JOURNAL_AUDIT + comptes de démonstration
|
||||||
|
-- Auteur : COYAUD Anthony
|
||||||
|
-- Version : 1.1 — Juin 2026
|
||||||
|
-- À exécuter APRÈS data_sentinel_init.sql (même base DataSentinel)
|
||||||
|
-- ============================================================
|
||||||
|
--
|
||||||
|
-- Ce script crée la couche de sécurité référencée par l'API
|
||||||
|
-- (main.py / auth.py) :
|
||||||
|
-- - [USER] : comptes applicatifs (RBAC : Admin / Superviseur / Consultant)
|
||||||
|
-- - JOURNAL_AUDIT : journal d'audit (connexions + actions d'administration)
|
||||||
|
--
|
||||||
|
-- Les mots de passe sont stockés hachés en bcrypt (passlib[bcrypt]).
|
||||||
|
-- Les hachages ci-dessous correspondent aux comptes de démonstration
|
||||||
|
-- documentés dans le README (à régénérer en production).
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
USE DataSentinel;
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- SUPPRESSION (ordre inverse des dépendances)
|
||||||
|
-- ============================================================
|
||||||
|
IF OBJECT_ID('JOURNAL_AUDIT', 'U') IS NOT NULL DROP TABLE JOURNAL_AUDIT;
|
||||||
|
IF OBJECT_ID('[USER]', 'U') IS NOT NULL DROP TABLE [USER];
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. TABLE [USER] — comptes applicatifs
|
||||||
|
-- ([USER] entre crochets car USER est un mot réservé SQL Server)
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE [USER] (
|
||||||
|
id_user INT NOT NULL IDENTITY(1,1),
|
||||||
|
username NVARCHAR(100) NOT NULL,
|
||||||
|
email NVARCHAR(200) NULL,
|
||||||
|
password_hash NVARCHAR(255) NOT NULL, -- bcrypt
|
||||||
|
role NVARCHAR(20) NOT NULL, -- Admin | Superviseur | Consultant
|
||||||
|
actif BIT NOT NULL DEFAULT 1,
|
||||||
|
created_at DATETIME2 NOT NULL DEFAULT SYSDATETIME(),
|
||||||
|
last_login DATETIME2 NULL,
|
||||||
|
CONSTRAINT PK_USER PRIMARY KEY (id_user),
|
||||||
|
CONSTRAINT UQ_USER_NAME UNIQUE (username),
|
||||||
|
CONSTRAINT CK_USER_ROLE CHECK (role IN ('Admin', 'Superviseur', 'Consultant'))
|
||||||
|
);
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE INDEX IX_USER_USERNAME ON [USER] (username);
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. TABLE JOURNAL_AUDIT — journal d'audit applicatif
|
||||||
|
-- Alimenté à chaque login + action d'administration.
|
||||||
|
-- username/id_user nullables (anonymisation RGPD : droit à l'oubli).
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE JOURNAL_AUDIT (
|
||||||
|
id_audit INT NOT NULL IDENTITY(1,1),
|
||||||
|
id_user INT NULL,
|
||||||
|
username NVARCHAR(100) NULL,
|
||||||
|
action NVARCHAR(50) NOT NULL, -- LOGIN | CREATE_USER | UPDATE_USER | DELETE_USER | RESET_PASSWORD
|
||||||
|
detail NVARCHAR(500) NULL,
|
||||||
|
ip NVARCHAR(50) NULL,
|
||||||
|
date_action DATETIME2 NOT NULL DEFAULT SYSDATETIME(),
|
||||||
|
CONSTRAINT PK_JOURNAL_AUDIT PRIMARY KEY (id_audit)
|
||||||
|
);
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE INDEX IX_AUDIT_DATE ON JOURNAL_AUDIT (date_action DESC);
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. COMPTES DE DÉMONSTRATION
|
||||||
|
-- ⚠️ Mots de passe en clair (à usage de recette / soutenance) :
|
||||||
|
-- Juré / 123456 → rôle Admin (compte de visite / évaluation)
|
||||||
|
-- admin / Admin2026! → rôle Admin
|
||||||
|
-- superviseur / Super2026! → rôle Superviseur
|
||||||
|
-- consultant / Conseil2026! → rôle Consultant
|
||||||
|
-- Les hachages bcrypt ci-dessous sont fonctionnels tels quels.
|
||||||
|
-- ============================================================
|
||||||
|
INSERT INTO [USER] (username, email, password_hash, role, actif) VALUES
|
||||||
|
(N'Juré', 'JURE@NEXA.com',
|
||||||
|
'$2b$12$nnWMOCjSiUq4rAyPGbC3W.HGdsvWKu9kEfLst6zBIEIcsgP97W94m', 'Admin', 1),
|
||||||
|
('admin', 'admin@xefi-fictif.fr',
|
||||||
|
'$2b$12$XJobnqM0cHwekv7UYWXUfuxVjMKLZI6VLzpDvRzDdLJgnYHLRNRCy', 'Admin', 1),
|
||||||
|
('superviseur', 'superviseur@xefi-fictif.fr',
|
||||||
|
'$2b$12$gMVg2j6wngZZ2KXH2jF6HuvlcAkGIJsbZ05JEQdDYNnIu7fDxOXri', 'Superviseur', 1),
|
||||||
|
('consultant', 'consultant@xefi-fictif.fr',
|
||||||
|
'$2b$12$s4Gl3UDq56HOw/GdjeHCrOFjXN/QrV1OEj3wmHhf3T8mFVfijJkty', 'Consultant', 1);
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- Entrée d'audit initiale (création du jeu de comptes)
|
||||||
|
INSERT INTO JOURNAL_AUDIT (id_user, username, action, detail)
|
||||||
|
SELECT id_user, username, 'CREATE_USER', 'Compte de démonstration (seed)'
|
||||||
|
FROM [USER];
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 4. VÉRIFICATIONS
|
||||||
|
-- ============================================================
|
||||||
|
SELECT 'USER' AS [Table], COUNT(*) AS [Lignes] FROM [USER]
|
||||||
|
UNION ALL SELECT 'JOURNAL_AUDIT', COUNT(*) FROM JOURNAL_AUDIT;
|
||||||
|
GO
|
||||||
|
|
||||||
|
SELECT id_user, username, email, role, actif, created_at FROM [USER] ORDER BY id_user;
|
||||||
|
GO
|
||||||
@@ -22,6 +22,24 @@ def test_services_returns_list(client, cur, auth_headers):
|
|||||||
assert {"id_service": 1, "nom_service": "Contrat"} in r.json()
|
assert {"id_service": 1, "nom_service": "Contrat"} in r.json()
|
||||||
|
|
||||||
|
|
||||||
|
def test_dashboard_filtres_ne_renvoie_que_les_valeurs_utilisees(client, cur, auth_headers):
|
||||||
|
# VUE_CONSO ne contient que Contrat / Fournisseur : les services du
|
||||||
|
# référentiel sans monitoring rattaché ne doivent pas remonter.
|
||||||
|
cur.description = [("service",), ("categorie",)]
|
||||||
|
cur._rows = [
|
||||||
|
("Contrat", "DOM-TOM"),
|
||||||
|
("Contrat", "Tiers-payeurs"),
|
||||||
|
("Fournisseur", "Contreparties"),
|
||||||
|
]
|
||||||
|
r = client.get("/dashboard/filtres", headers=auth_headers)
|
||||||
|
assert r.status_code == 200
|
||||||
|
body = r.json()
|
||||||
|
assert body["services"] == ["Contrat", "Fournisseur"]
|
||||||
|
assert "Business Intelligence" not in body["services"]
|
||||||
|
assert body["categories"] == ["Contreparties", "DOM-TOM", "Tiers-payeurs"]
|
||||||
|
assert {"service": "Contrat", "categorie": "DOM-TOM"} in body["combinaisons"]
|
||||||
|
|
||||||
|
|
||||||
def test_auth_login_success(client, cur):
|
def test_auth_login_success(client, cur):
|
||||||
cur._one = (1, "admin", hash_password("Admin2026!"), "Admin", 1)
|
cur._one = (1, "admin", hash_password("Admin2026!"), "Admin", 1)
|
||||||
r = client.post("/auth/login", data={"username": "admin", "password": "Admin2026!"})
|
r = client.post("/auth/login", data={"username": "admin", "password": "Admin2026!"})
|
||||||
|
|||||||
Reference in New Issue
Block a user