test: partage un jeu de démonstration et des comptes de test
db/seeds/ était vide, et chaque outil de test semait ses données à la main : un site et deux relevés dans dast.yml, rien pour le reste. - db/seeds/demo.sql : trois sites, 72 heures de relevés relatives à now(), une prévision par site, quatorze alertes de tous types et sévérités, un rapport de dérive par site. Rejouable. - scripts/comptes-test.sh : administrateur par la CLI, lecteur et opérateur activés, et un compte laissé sur son mot de passe temporaire ; identifiants écrits en JSON (mode 600). Fonctionne en natif ou contre la stack Compose (BASE_URL, APP_CLI). - scripts/dast-token.sh s'appuie désormais dessus ; dast.yml sème demo.sql.
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
name: DAST
|
name: DAST
|
||||||
|
|
||||||
# Scan dynamique OWASP ZAP de l'API (issue #41). Il attaque une API qui tourne : le job démarre
|
# Scan dynamique OWASP ZAP de l'API (issue #41). Il attaque une API qui tourne : le job démarre
|
||||||
# la base et le backend sur le runner, sème un site et quelques relevés (sans ça le scan ne
|
# la base et le backend sur le runner, sème le jeu de démonstration (sans ça le scan ne frappe que
|
||||||
# frappe que des gestionnaires d'erreur), crée un compte `lecteur` jetable
|
# des gestionnaires d'erreur), crée des comptes jetables (scripts/dast-token.sh), puis lance ZAP
|
||||||
# (scripts/dast-token.sh), puis lance ZAP sur le contrat OpenAPI avec le jeton de ce compte.
|
# sur le contrat OpenAPI avec le jeton du `lecteur`.
|
||||||
#
|
#
|
||||||
# Non bloquant pour l'instant sur les alertes (`continue-on-error` sur la seule étape du scan) :
|
# Non bloquant pour l'instant sur les alertes (`continue-on-error` sur la seule étape du scan) :
|
||||||
# le volume d'un premier passage trié est inconnu. Deux étapes suivantes, elles, bloquent si le
|
# le volume d'un premier passage trié est inconnu. Deux étapes suivantes, elles, bloquent si le
|
||||||
@@ -24,6 +24,8 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- ".github/workflows/dast.yml"
|
- ".github/workflows/dast.yml"
|
||||||
- "scripts/dast-token.sh"
|
- "scripts/dast-token.sh"
|
||||||
|
- "scripts/comptes-test.sh"
|
||||||
|
- "db/seeds/**"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -108,22 +110,9 @@ jobs:
|
|||||||
working-directory: apps/backend
|
working-directory: apps/backend
|
||||||
|
|
||||||
# Sans données, `GET /sites` rend `[]`, chaque `/{site_id}` rend 404 et le scan actif ne
|
# Sans données, `GET /sites` rend `[]`, chaque `/{site_id}` rend 404 et le scan actif ne
|
||||||
# frappe que des gestionnaires d'erreur plutôt que la logique métier. `db/seeds/` est vide
|
# frappe que des gestionnaires d'erreur plutôt que la logique métier.
|
||||||
# (pas encore d'outillage de jeu de données pour la CI) : un site et deux relevés à la main,
|
- name: Sème le jeu de démonstration
|
||||||
# juste assez pour que les routes de lecture aient quelque chose à rendre.
|
run: psql -h localhost -p 5433 -U enervision -d enervision_dast -v ON_ERROR_STOP=1 -f db/seeds/demo.sql
|
||||||
- name: Insère un site et des relevés minimaux pour le scan
|
|
||||||
run: |
|
|
||||||
psql -h localhost -p 5433 -U enervision -d enervision_dast <<'SQL'
|
|
||||||
INSERT INTO site (site_id, site_name, site_type, location, capacity_kw, status)
|
|
||||||
VALUES ('dast-site', 'Site du scan DAST', 'bureau', 'CI', 50, 'actif')
|
|
||||||
ON CONFLICT (site_id) DO NOTHING;
|
|
||||||
|
|
||||||
INSERT INTO reading (site_id, timestamp, source, consumption_kw, consumption_kwh, is_working_hours, data_quality, raw_data)
|
|
||||||
VALUES
|
|
||||||
('dast-site', now() - interval '2 hours', 'api_current', 12.5, 12.5, true, 'good', '{}'),
|
|
||||||
('dast-site', now() - interval '1 hour', 'api_current', 13.0, 13.0, true, 'good', '{}')
|
|
||||||
ON CONFLICT DO NOTHING;
|
|
||||||
SQL
|
|
||||||
|
|
||||||
- name: Démarre l'API
|
- name: Démarre l'API
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
-- Contrainte : jeu de démonstration pour une base JETABLE (CI, e2e, charge, DAST) - demo.sql.
|
||||||
|
-- Rejouable : identifiants fixes et `ON CONFLICT DO NOTHING`, ou `NOT EXISTS` là où aucune
|
||||||
|
-- contrainte d'unicité ne protège la table.
|
||||||
|
-- Pourquoi : les horodatages suivent `now()`. La fenêtre par défaut de `GET /readings` couvre les
|
||||||
|
-- 24 dernières heures, et un jeu figé dans le passé laisserait le tableau de bord vide.
|
||||||
|
-- Piège : `demo-ecole` finit sur une lecture `partial` sans humidité, pour que la supervision des
|
||||||
|
-- capteurs montre un site dégradé ; au-delà de dix alertes, le fil affiche « Afficher plus ».
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
INSERT INTO site (site_id, site_name, site_type, location, capacity_kw, status)
|
||||||
|
VALUES
|
||||||
|
('demo-siege', 'Siège Part-Dieu', 'office', 'Lyon', 450, 'actif'),
|
||||||
|
('demo-usine', 'Usine de Vénissieux', 'factory', 'Vénissieux', 900, 'actif'),
|
||||||
|
('demo-ecole', 'Groupe scolaire Gratte-Ciel', 'school', 'Villeurbanne', 250, 'maintenance')
|
||||||
|
ON CONFLICT (site_id) DO NOTHING;
|
||||||
|
|
||||||
|
WITH profil (site_id, base_kw, amplitude_kw) AS (
|
||||||
|
VALUES ('demo-siege', 180.0, 120.0), ('demo-usine', 520.0, 260.0), ('demo-ecole', 70.0, 60.0)
|
||||||
|
),
|
||||||
|
heures AS (
|
||||||
|
SELECT date_trunc('hour', now()) - make_interval(hours => n) AS horodatage, n
|
||||||
|
FROM generate_series(0, 71) AS n
|
||||||
|
),
|
||||||
|
lectures AS (
|
||||||
|
SELECT
|
||||||
|
p.site_id,
|
||||||
|
h.horodatage,
|
||||||
|
h.n,
|
||||||
|
round((p.base_kw + p.amplitude_kw * greatest(0, sin(pi() * (extract(hour FROM h.horodatage) - 6) / 14)))::numeric, 2)::double precision AS kw,
|
||||||
|
extract(isodow FROM h.horodatage) < 6 AND extract(hour FROM h.horodatage) BETWEEN 8 AND 18 AS ouvre
|
||||||
|
FROM profil AS p
|
||||||
|
CROSS JOIN heures AS h
|
||||||
|
)
|
||||||
|
INSERT INTO reading (
|
||||||
|
site_id, timestamp, source, consumption_kw, consumption_kwh, consumption_euros,
|
||||||
|
voltage_v, current_a, power_factor, temperature_celsius, humidity_percent,
|
||||||
|
is_working_hours, data_quality, null_reasons, raw_data
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
site_id,
|
||||||
|
horodatage,
|
||||||
|
'api_current',
|
||||||
|
kw,
|
||||||
|
kw,
|
||||||
|
round((kw * 0.19)::numeric, 2),
|
||||||
|
230.0,
|
||||||
|
round((kw * 1000 / (230.0 * 3 * 0.95))::numeric, 1)::double precision,
|
||||||
|
0.95,
|
||||||
|
19.5 + 3 * sin(pi() * extract(hour FROM horodatage) / 12),
|
||||||
|
CASE WHEN site_id = 'demo-ecole' AND n = 0 THEN NULL ELSE 45.0 END,
|
||||||
|
ouvre,
|
||||||
|
CASE WHEN site_id = 'demo-ecole' AND n = 0 THEN 'partial' ELSE 'good' END,
|
||||||
|
CASE WHEN site_id = 'demo-ecole' AND n = 0 THEN ARRAY['humidity_sensor_failure'] END,
|
||||||
|
'{}'::jsonb
|
||||||
|
FROM lectures
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO prediction (site_id, target_at, target_metric, period_minutes, predicted_value, model_reference, status)
|
||||||
|
SELECT s.site_id, date_trunc('hour', now()) + interval '1 hour', 'consumption_kwh', 60, s.valeur, 'demo-seed', 'available'
|
||||||
|
FROM (VALUES ('demo-siege', 214.0), ('demo-usine', 610.5), ('demo-ecole', 88.2)) AS s (site_id, valeur)
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM prediction AS p
|
||||||
|
WHERE p.site_id = s.site_id
|
||||||
|
AND p.model_reference = 'demo-seed'
|
||||||
|
AND p.target_at = date_trunc('hour', now()) + interval '1 hour'
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO alert (source_alert_id, site_id, source, timestamp, type, severity, message, value, threshold, metric, raw_data)
|
||||||
|
SELECT a.id, a.site_id, 'enervision', now() - make_interval(hours => a.age_h), a.type, a.severite, a.message, a.valeur, a.seuil, a.metrique, '{}'::jsonb
|
||||||
|
FROM (
|
||||||
|
VALUES
|
||||||
|
('demo-01', 'demo-siege', 1, 'spike', 'high', 'Pic de consommation à 312 kW', 312.0, 250.0, 'consumption_kw'),
|
||||||
|
('demo-02', 'demo-siege', 3, 'threshold', 'medium', 'Seuil de 80 % de la capacité franchi', 372.0, 360.0, 'consumption_kw'),
|
||||||
|
('demo-03', 'demo-siege', 6, 'anomaly', 'low', 'Consommation nocturne inhabituelle', 205.0, NULL, 'consumption_kw'),
|
||||||
|
('demo-04', 'demo-siege', 9, 'sensor', 'medium', 'Capteur de température muet', NULL, NULL, 'temperature_celsius'),
|
||||||
|
('demo-05', 'demo-siege', 20, 'outage', 'critical', 'Coupure de courant de 2 heures', 0.0, NULL, 'consumption_kw'),
|
||||||
|
('demo-06', 'demo-usine', 2, 'spike', 'critical', 'Pic de consommation à 1 020 kW', 1020.0, 850.0, 'consumption_kw'),
|
||||||
|
('demo-07', 'demo-usine', 4, 'threshold', 'high', 'Seuil de 90 % de la capacité franchi', 830.0, 810.0, 'consumption_kw'),
|
||||||
|
('demo-08', 'demo-usine', 8, 'anomaly', 'medium', 'Facteur de puissance dégradé', 0.71, 0.85, 'power_factor'),
|
||||||
|
('demo-09', 'demo-usine', 14, 'outage', 'high', 'Perte de mesure sur la ligne principale', NULL, NULL, 'consumption_kw'),
|
||||||
|
('demo-10', 'demo-usine', 30, 'sensor', 'low', 'Capteur électrique intermittent', NULL, NULL, 'voltage_v'),
|
||||||
|
('demo-11', 'demo-ecole', 1, 'sensor', 'high', 'Capteur d''humidité muet', NULL, NULL, 'humidity_percent'),
|
||||||
|
('demo-12', 'demo-ecole', 5, 'anomaly', 'low', 'Chauffage actif hors des heures d''ouverture', 96.0, NULL, 'consumption_kw'),
|
||||||
|
('demo-13', 'demo-ecole', 12, 'threshold', 'medium', 'Seuil de 60 % de la capacité franchi', 158.0, 150.0, 'consumption_kw'),
|
||||||
|
('demo-14', 'demo-ecole', 40, 'spike', 'medium', 'Pic de consommation à 190 kW', 190.0, 160.0, 'consumption_kw')
|
||||||
|
) AS a (id, site_id, age_h, type, severite, message, valeur, seuil, metrique)
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO drift_report (window_start, window_end, n_observations, mae, mape, bias, reference_mae, coverage_ratio, insufficient_data_ratio, model_references, status, reason, site_id)
|
||||||
|
SELECT date_trunc('day', now()) - interval '7 days', date_trunc('day', now()), d.n, d.mae, d.mape, d.biais, 11.0, 0.98, 0.0, ARRAY['demo-seed'], d.statut, d.raison, d.site_id
|
||||||
|
FROM (
|
||||||
|
VALUES
|
||||||
|
('demo-siege', 168, 9.4, 0.052, -1.2, 'stable', NULL),
|
||||||
|
('demo-usine', 168, 31.8, 0.061, 14.5, 'derive', 'MAE supérieure à 1,5 fois la référence'),
|
||||||
|
('demo-ecole', 120, 6.1, 0.083, 0.4, 'stable', NULL),
|
||||||
|
(NULL, 456, 16.9, 0.064, 5.1, 'stable', NULL)
|
||||||
|
) AS d (site_id, n, mae, mape, biais, statut, raison)
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
Executable
+103
@@ -0,0 +1,103 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Contrainte : réservé à une base JETABLE (CI, e2e, charge sur le poste) - comptes-test.sh. Crée
|
||||||
|
# un administrateur par la CLI, puis un lecteur, un opérateur et un compte laissé sur son mot de
|
||||||
|
# passe temporaire, et écrit leurs identifiants en JSON dans $COMPTES_FICHIER.
|
||||||
|
# Piège : un compte neuf est en `must_change_password`, que toute route gardée refuse. Seul
|
||||||
|
# `premiere_connexion` garde ce statut : c'est le parcours du premier login de l'e2e.
|
||||||
|
# Piège : `create-admin` refuse un second administrateur actif. Sur une base qui en a déjà un
|
||||||
|
# (recette, poste peuplé), créer les comptes depuis l'interface d'administration.
|
||||||
|
#
|
||||||
|
# BASE_URL vise l'API (http://localhost:8000 par défaut, ou le proxy en https). APP_CLI lance la
|
||||||
|
# CLI du backend : depuis apps/backend par défaut, ou `docker compose ... exec -T backend python
|
||||||
|
# -m app.cli` contre la stack conteneurisée.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
# Sans lui, une erreur dans `$(...)` ne s'arrête pas : la substitution rendrait un mot de passe vide.
|
||||||
|
shopt -s inherit_errexit
|
||||||
|
|
||||||
|
BASE_URL="${BASE_URL:-http://localhost:8000}"
|
||||||
|
API="$BASE_URL/api/v1"
|
||||||
|
COMPTES_FICHIER="${COMPTES_FICHIER:?COMPTES_FICHIER=chemin du fichier JSON requis}"
|
||||||
|
read -ra CLI <<<"${APP_CLI:-uv run --frozen --no-sync --no-build python -m app.cli}"
|
||||||
|
SUFFIXE="$(openssl rand -hex 4)"
|
||||||
|
|
||||||
|
CURL=(curl -fsS)
|
||||||
|
[[ "$BASE_URL" == https://* ]] && CURL+=(--insecure)
|
||||||
|
|
||||||
|
# Classes exigées par le validateur : majuscule, minuscule, chiffre, caractère spécial.
|
||||||
|
nouveau_mot_de_passe() { echo "Test-$(openssl rand -hex 12)-Aa1!"; }
|
||||||
|
|
||||||
|
journal() { echo "comptes-test: $*" >&2; }
|
||||||
|
|
||||||
|
connexion() {
|
||||||
|
local email="$1" mot_de_passe="$2"
|
||||||
|
"${CURL[@]}" -X POST "$API/auth/login" -H 'Content-Type: application/json' \
|
||||||
|
-d "$(jq -n --arg e "$email" --arg p "$mot_de_passe" '{email:$e, password:$p}')" \
|
||||||
|
| jq -r '.access_token'
|
||||||
|
}
|
||||||
|
|
||||||
|
# `/auth/password` rend un nouveau jeton d'accès : pas de reconnexion, donc pas de second hachage
|
||||||
|
# Argon2id ni de requête de plus dans la zone `auth` de nginx (30 par minute).
|
||||||
|
changer_mot_de_passe() {
|
||||||
|
local jeton="$1" ancien="$2" nouveau="$3"
|
||||||
|
"${CURL[@]}" -X POST "$API/auth/password" \
|
||||||
|
-H "Authorization: Bearer $jeton" -H 'Content-Type: application/json' \
|
||||||
|
-d "$(jq -n --arg a "$ancien" --arg n "$nouveau" '{current_password:$a, new_password:$n}')" \
|
||||||
|
| jq -r '.access_token'
|
||||||
|
}
|
||||||
|
|
||||||
|
creer_compte() {
|
||||||
|
local jeton_admin="$1" email="$2" role="$3" reponse temporaire
|
||||||
|
reponse="$("${CURL[@]}" -X POST "$API/users" -H "Authorization: Bearer $jeton_admin" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d "$(jq -n --arg e "$email" --arg r "$role" '{email:$e, role:$r}')")"
|
||||||
|
temporaire="$(jq -r '.temporary_password // empty' <<<"$reponse")"
|
||||||
|
[[ -n "$temporaire" ]] || { journal "mot de passe temporaire absent de la réponse : $reponse"; exit 1; }
|
||||||
|
echo "$temporaire"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Rend le mot de passe définitif d'un compte créé par `creer_compte`.
|
||||||
|
activer_compte() {
|
||||||
|
local email="$1" temporaire="$2" definitif jeton
|
||||||
|
definitif="$(nouveau_mot_de_passe)"
|
||||||
|
jeton="$(connexion "$email" "$temporaire")"
|
||||||
|
changer_mot_de_passe "$jeton" "$temporaire" "$definitif" >/dev/null
|
||||||
|
echo "$definitif"
|
||||||
|
}
|
||||||
|
|
||||||
|
EMAIL_ADMIN="test-admin-$SUFFIXE@enervision.fr"
|
||||||
|
journal "création de l'administrateur $EMAIL_ADMIN"
|
||||||
|
if ! SORTIE="$("${CLI[@]}" create-admin --email "$EMAIL_ADMIN" --generate)"; then
|
||||||
|
journal "la création de l'administrateur a échoué : $SORTIE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
MDP_TEMPORAIRE_ADMIN="$(sed -n 's/^Mot de passe généré, il ne sera plus affiché : //p' <<<"$SORTIE" | tr -d '\r')"
|
||||||
|
[[ -n "$MDP_TEMPORAIRE_ADMIN" ]] || { journal "mot de passe introuvable dans la sortie : $SORTIE"; exit 1; }
|
||||||
|
|
||||||
|
MDP_ADMIN="$(nouveau_mot_de_passe)"
|
||||||
|
JETON_ADMIN="$(connexion "$EMAIL_ADMIN" "$MDP_TEMPORAIRE_ADMIN")"
|
||||||
|
JETON_ADMIN="$(changer_mot_de_passe "$JETON_ADMIN" "$MDP_TEMPORAIRE_ADMIN" "$MDP_ADMIN")"
|
||||||
|
|
||||||
|
EMAIL_LECTEUR="test-lecteur-$SUFFIXE@enervision.fr"
|
||||||
|
EMAIL_OPERATEUR="test-operateur-$SUFFIXE@enervision.fr"
|
||||||
|
EMAIL_PREMIERE="test-premiere-connexion-$SUFFIXE@enervision.fr"
|
||||||
|
journal "création du lecteur, de l'opérateur et du compte de première connexion"
|
||||||
|
TEMPORAIRE_LECTEUR="$(creer_compte "$JETON_ADMIN" "$EMAIL_LECTEUR" lecteur)"
|
||||||
|
MDP_LECTEUR="$(activer_compte "$EMAIL_LECTEUR" "$TEMPORAIRE_LECTEUR")"
|
||||||
|
TEMPORAIRE_OPERATEUR="$(creer_compte "$JETON_ADMIN" "$EMAIL_OPERATEUR" operateur)"
|
||||||
|
MDP_OPERATEUR="$(activer_compte "$EMAIL_OPERATEUR" "$TEMPORAIRE_OPERATEUR")"
|
||||||
|
MDP_PREMIERE="$(creer_compte "$JETON_ADMIN" "$EMAIL_PREMIERE" lecteur)"
|
||||||
|
|
||||||
|
umask 077
|
||||||
|
jq -n \
|
||||||
|
--arg ae "$EMAIL_ADMIN" --arg ap "$MDP_ADMIN" \
|
||||||
|
--arg le "$EMAIL_LECTEUR" --arg lp "$MDP_LECTEUR" \
|
||||||
|
--arg oe "$EMAIL_OPERATEUR" --arg op "$MDP_OPERATEUR" \
|
||||||
|
--arg pe "$EMAIL_PREMIERE" --arg pp "$MDP_PREMIERE" \
|
||||||
|
'{
|
||||||
|
admin: {email: $ae, password: $ap},
|
||||||
|
lecteur: {email: $le, password: $lp},
|
||||||
|
operateur: {email: $oe, password: $op},
|
||||||
|
premiere_connexion: {email: $pe, password: $pp}
|
||||||
|
}' >"$COMPTES_FICHIER"
|
||||||
|
journal "identifiants écrits dans $COMPTES_FICHIER"
|
||||||
+12
-66
@@ -1,78 +1,24 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Prépare le scan DAST : crée un compte `lecteur` sur une API déjà démarrée, lui fait passer le
|
# Contrainte : prépare le scan DAST sur une base JETABLE - dast-token.sh. Crée les comptes de
|
||||||
# changement de mot de passe obligatoire, et écrit son jeton d'accès sur la sortie standard.
|
# test (scripts/comptes-test.sh, mêmes variables) et écrit sur la sortie standard le seul jeton
|
||||||
#
|
# d'accès du `lecteur`.
|
||||||
# Piège : un compte neuf est en `must_change_password`, et toute route gardée le refuse tant que
|
|
||||||
# le mot de passe n'a pas été changé. Sans cette étape, ZAP ne verrait que 403 sur les routes
|
|
||||||
# gardées et le scan ne testerait rien de l'API authentifiée.
|
|
||||||
#
|
|
||||||
# Contrainte : le compte du scan est `lecteur`, jamais `admin`. Un scan actif avec un jeton admin
|
# Contrainte : le compte du scan est `lecteur`, jamais `admin`. Un scan actif avec un jeton admin
|
||||||
# frapperait POST /users ou la réinitialisation de mots de passe pour de bon.
|
# frapperait POST /users ou la réinitialisation de mots de passe pour de bon.
|
||||||
#
|
|
||||||
# L'administrateur n'existe que pour créer ce compte (l'API n'a pas d'inscription publique).
|
|
||||||
# À lancer depuis apps/backend, dans un environnement où DATABASE_URL et APP_SECRET_KEY visent
|
|
||||||
# une base JETABLE : le script y crée deux comptes.
|
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
shopt -s inherit_errexit
|
||||||
|
|
||||||
|
ICI="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
BASE_URL="${BASE_URL:-http://localhost:8000}"
|
BASE_URL="${BASE_URL:-http://localhost:8000}"
|
||||||
API="$BASE_URL/api/v1"
|
COMPTES="$(mktemp)"
|
||||||
SUFFIXE="$(openssl rand -hex 4)"
|
trap 'rm -f "$COMPTES"' EXIT
|
||||||
EMAIL_ADMIN="dast-admin-$SUFFIXE@enervision.fr"
|
|
||||||
EMAIL_LECTEUR="dast-lecteur-$SUFFIXE@enervision.fr"
|
|
||||||
|
|
||||||
# Classes exigées par le validateur : majuscule, minuscule, chiffre, caractère spécial.
|
COMPTES_FICHIER="$COMPTES" BASE_URL="$BASE_URL" "$ICI/comptes-test.sh"
|
||||||
nouveau_mot_de_passe() { echo "Dast-$(openssl rand -hex 12)-Aa1!"; }
|
|
||||||
|
|
||||||
# Tout ce qui n'est pas la sortie finale part sur stderr : la sortie standard ne porte que le jeton.
|
JETON="$(curl -fsS -X POST "$BASE_URL/api/v1/auth/login" -H 'Content-Type: application/json' \
|
||||||
journal() { echo "dast-token: $*" >&2; }
|
-d "$(jq '.lecteur | {email, password}' "$COMPTES")" | jq -r '.access_token')"
|
||||||
|
|
||||||
connexion() {
|
CODE="$(curl -sS -o /dev/null -w '%{http_code}' "$BASE_URL/api/v1/sites" -H "Authorization: Bearer $JETON")"
|
||||||
local email="$1" mot_de_passe="$2"
|
[[ "$CODE" == "200" ]] || { echo "dast-token: GET /sites répond $CODE avec le jeton du lecteur, attendu 200" >&2; exit 1; }
|
||||||
curl -fsS -X POST "$API/auth/login" -H 'Content-Type: application/json' \
|
|
||||||
-d "$(jq -n --arg e "$email" --arg p "$mot_de_passe" '{email:$e, password:$p}')" \
|
|
||||||
| jq -r '.access_token'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Rend le nouveau jeton d'accès : `/auth/password` en émet un (avec l'`iat` de la session en
|
|
||||||
# cours, cf. le piège documenté dans `app/api/deps.py`), pas seulement une confirmation. S'y fier
|
|
||||||
# évite une reconnexion, donc un second hachage Argon2id (19456 Kio) et un aller-retour de
|
|
||||||
# refresh-token superflus sur le chemin critique de la CI.
|
|
||||||
changer_mot_de_passe() {
|
|
||||||
local jeton="$1" ancien="$2" nouveau="$3"
|
|
||||||
curl -fsS -X POST "$API/auth/password" \
|
|
||||||
-H "Authorization: Bearer $jeton" -H 'Content-Type: application/json' \
|
|
||||||
-d "$(jq -n --arg a "$ancien" --arg n "$nouveau" '{current_password:$a, new_password:$n}')" \
|
|
||||||
| jq -r '.access_token'
|
|
||||||
}
|
|
||||||
|
|
||||||
journal "création de l'administrateur $EMAIL_ADMIN"
|
|
||||||
if ! SORTIE="$(uv run --frozen --no-sync --no-build python -m app.cli create-admin --email "$EMAIL_ADMIN" --generate)"; then
|
|
||||||
journal "la création de l'administrateur a échoué :"
|
|
||||||
journal "$SORTIE"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
MDP_ADMIN="$(sed -n 's/^Mot de passe généré, il ne sera plus affiché : //p' <<<"$SORTIE")"
|
|
||||||
[[ -n "$MDP_ADMIN" ]] || { journal "mot de passe administrateur introuvable dans la sortie :"; journal "$SORTIE"; exit 1; }
|
|
||||||
|
|
||||||
JETON="$(connexion "$EMAIL_ADMIN" "$MDP_ADMIN")"
|
|
||||||
NOUVEAU_ADMIN="$(nouveau_mot_de_passe)"
|
|
||||||
JETON="$(changer_mot_de_passe "$JETON" "$MDP_ADMIN" "$NOUVEAU_ADMIN")"
|
|
||||||
|
|
||||||
journal "création du lecteur $EMAIL_LECTEUR"
|
|
||||||
REPONSE="$(curl -fsS -X POST "$API/users" -H "Authorization: Bearer $JETON" \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d "$(jq -n --arg e "$EMAIL_LECTEUR" '{email:$e, role:"lecteur"}')")"
|
|
||||||
MDP_TEMPORAIRE="$(jq -r '.temporary_password // empty' <<<"$REPONSE")"
|
|
||||||
[[ -n "$MDP_TEMPORAIRE" ]] || { journal "mot de passe temporaire introuvable dans la réponse de POST /users :"; journal "$REPONSE"; exit 1; }
|
|
||||||
|
|
||||||
JETON="$(connexion "$EMAIL_LECTEUR" "$MDP_TEMPORAIRE")"
|
|
||||||
NOUVEAU_LECTEUR="$(nouveau_mot_de_passe)"
|
|
||||||
JETON="$(changer_mot_de_passe "$JETON" "$MDP_TEMPORAIRE" "$NOUVEAU_LECTEUR")"
|
|
||||||
|
|
||||||
# Vérifie que le jeton ouvre bien une route gardée avant de le rendre.
|
|
||||||
CODE="$(curl -sS -o /dev/null -w '%{http_code}' "$API/sites" -H "Authorization: Bearer $JETON")"
|
|
||||||
[[ "$CODE" == "200" ]] || { journal "GET /sites répond $CODE avec le jeton du lecteur, attendu 200"; exit 1; }
|
|
||||||
|
|
||||||
journal "jeton du lecteur prêt"
|
|
||||||
echo "$JETON"
|
echo "$JETON"
|
||||||
|
|||||||
Reference in New Issue
Block a user