feat(wp-c): génération token par poste à l'enroll (patch 2, inerte runtime)

Génère un token unique (secrets.token_hex(32)) à chaque (ré)enrôlement,
persiste uniquement son empreinte SHA-256 dans token_hash, renseigne
token_issued_at, retourne le clair une seule fois dans le résultat de
enroll. Le clair n'est jamais journalisé ni persisté.

Inerte au runtime : api_stream.py intouché, l'endpoint /agents/enroll ne
propage ni le clair ni le hash (api_token global inchangé). Auth runtime
non modifiée. Aucun branchement _verify_token. TDD : 8 tests + non-régression
WP-B/WP-C (47 verts). Voir PLAN-WPC-TDD-EXECUTABLE / DETTE-015.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-06-10 11:36:44 +02:00
parent f7f6926410
commit 9fb2c7bfee
2 changed files with 148 additions and 7 deletions

View File

@@ -28,13 +28,15 @@ Schema de la table `enrolled_agents` :
from __future__ import annotations
import hashlib
import logging
import os
import secrets
import sqlite3
import threading
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Tuple
logger = logging.getLogger(__name__)
@@ -48,6 +50,19 @@ def _utc_now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def _new_token() -> Tuple[str, str]:
"""WP-C : genere un token poste (clair) et son empreinte SHA-256.
Le clair est retourne UNE seule fois a l'appelant (resultat de enroll) ; seul
le hash est persiste dans `token_hash`. Le clair n'est jamais journalise ni
stocke. L'auth runtime reste inchangee (aucun branchement ici sur la
verification de token cote api_stream).
"""
clear = secrets.token_hex(32)
token_hash = hashlib.sha256(clear.encode("utf-8")).hexdigest()
return clear, token_hash
def _fleet_enroll_locked() -> bool:
"""WP-B : parc verrouille -> aucun NOUVEAU machine_id ne peut s'enroler.
@@ -206,6 +221,8 @@ class AgentRegistry:
if not allow_reactivate:
raise AgentAlreadyEnrolledError(dict(existing))
# WP-C : rotation du token a chaque (re)enrolement.
token, token_hash = _new_token()
conn.execute(
"""
UPDATE enrolled_agents
@@ -219,13 +236,17 @@ class AgentRegistry:
enrolled_at = ?,
last_seen_at = ?,
uninstalled_at = NULL,
uninstall_reason = NULL
uninstall_reason = NULL,
token_hash = ?,
token_issued_at = ?
WHERE machine_id = ?
""",
(
user_name, user_email, user_id,
hostname, os_info, version,
now, now, machine_id,
now, now,
token_hash, now,
machine_id,
),
)
conn.commit()
@@ -233,23 +254,32 @@ class AgentRegistry:
"SELECT * FROM enrolled_agents WHERE machine_id = ?",
(machine_id,),
).fetchone()
return {"created": False, "reactivated": True, "agent": dict(row)}
return {
"created": False,
"reactivated": True,
"agent": dict(row),
"token": token,
}
# Nouvelle inscription — WP-B : refusee si le parc est verrouille
if _fleet_enroll_locked():
raise FleetEnrollLockedError(machine_id)
# WP-C : token poste genere a la creation.
token, token_hash = _new_token()
conn.execute(
"""
INSERT INTO enrolled_agents (
machine_id, user_name, user_email, user_id,
hostname, os_info, version,
status, enrolled_at, last_seen_at
) VALUES (?, ?, ?, ?, ?, ?, ?, 'active', ?, ?)
status, enrolled_at, last_seen_at,
token_hash, token_issued_at
) VALUES (?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, ?)
""",
(
machine_id, user_name, user_email, user_id,
hostname, os_info, version,
now, now,
token_hash, now,
),
)
conn.commit()
@@ -257,7 +287,12 @@ class AgentRegistry:
"SELECT * FROM enrolled_agents WHERE machine_id = ?",
(machine_id,),
).fetchone()
return {"created": True, "reactivated": False, "agent": dict(row)}
return {
"created": True,
"reactivated": False,
"agent": dict(row),
"token": token,
}
def uninstall(
self,