Description
Sur le nouveau serveur server-nestjs, il arrive de façon intermittente que le secret Vault SONAR d'un projet contienne SONAR_PASSWORD: 'not initialized' au lieu du vrai mot de passe. Avec l'ancien plugin plugins/sonarqube, ce problÚme n'apparaßt jamais.
La valeur 'not initialized' n'est pas un bug en soi : c'est un repli lĂ©gitime lorsque l'utilisateur SonarQube existe mais que son secret Vault a rĂ©ellement disparu (SonarQube ne permet pas de relire un mot de passe). Le problĂšme est que, dans le nouveau serveur, cette valeur est Ă©crite alors qu'un mot de passe valide vient d'ĂȘtre créé â il y a donc perte de donnĂ©es.
Cause : une race condition, pas un bug de chemin. La logique et le chemin Vault (<PROJECTS_ROOT_DIR>/<slug>/SONAR) sont identiques entre l'ancien plugin et le nouveau serveur. La différence est la concurrence :
- l'événement
project.upsert est Ă©mis depuis de nombreux endroits (project, project-roles, environment, deployment, project-hooksâŠ) ;
- certaines émissions se font sans
await (fire-and-forget, ex. deployment.service.ts:98, environment.service.ts:76), donc deux rĂ©conciliations du mĂȘme projet peuvent se chevaucher ;
- rien ne sérialise le traitement par projet (
emitAsync n'a aucun verrou par projet).
ensureUser effectue une sĂ©quence lecture â vĂ©rification â Ă©criture non atomique sur le secret. Sur un projet fraĂźchement créé, deux exĂ©cutions concurrentes se dĂ©roulent ainsi :
| Ătape |
Run A |
Run B |
| 1 |
lit le secret â 404 |
lit le secret â 404 |
| 2 |
utilisateur absent |
â |
| 3 |
crée l'utilisateur + écrit le vrai mot de passe |
â |
| 4 |
â |
utilisateur dĂ©sormais trouvĂ© (créé par A), lecture initiale = 404 â branche else |
| 5 |
â |
Ă©crit SONAR_PASSWORD: 'not initialized' â Ă©crase le mot de passe de A |
Le rĂ©sultat dĂ©pend de l'ordre d'exĂ©cution â comportement non dĂ©terministe (« parfois »).
â ïž PortĂ©e plus large. SonarQube n'est que le symptĂŽme le plus visible. Le vrai sujet est l'absence de sĂ©rialisation par projet dans le nouveau modĂšle de gestion des plugins : tout plugin faisant un cycle lecture-modification-Ă©criture sur un secret Vault (ou toute autre ressource) est exposĂ© au mĂȘme type de race lors de provisionnements concurrents. Ă traiter idĂ©alement de façon transverse.
Etapes de reproduction
# Reproduction déterministe (illustre la branche de code fautive)
1. Provisionner un projet (secret SONAR créé avec un vrai mot de passe)
2. Supprimer manuellement tous les secrets Vault du projet
(l'utilisateur SonarQube, lui, n'est PAS supprimé)
3. Reprovisionner le projet
4. Constater SONAR_PASSWORD = 'not initialized' :
le secret est lu en 404, mais l'utilisateur existe dĂ©jĂ â branche de repli
# Note : cette reproduction dĂ©clenche la mĂȘme branche sur l'ancien plugin.
# Ce qui est propre au nouveau serveur, c'est d'atteindre cette branche
# SANS suppression manuelle, via une race condition (perte de données) :
# Reproduction de la régression (intermittente)
5. DĂ©clencher plusieurs provisionnements rapprochĂ©s d'un mĂȘme projet neuf
(création du projet + ajout d'un rÎle / environnement / déploiement quasi simultanés)
6. Laisser les événements project.upsert se chevaucher
7. Constater par intermittence SONAR_PASSWORD = 'not initialized'
alors qu'un vrai mot de passe venait d'ĂȘtre Ă©crit
Piste technique (oĂč se trouve l'erreur & suggestions)
SymptÎme localisé dans apps/server-nestjs/src/modules/sonarqube/sonarqube.service.ts, méthode ensureUser (lecture ligne 201, branche else ligne 213-216, écriture ligne 220).
Suggestions, du plus ciblé au plus transverse :
- Ăcriture non destructrice : ne jamais Ă©craser un
SONAR_PASSWORD valide existant. Relire le secret juste avant d'écrire, ou n'écrire 'not initialized' qu'aprÚs confirmation que le secret est réellement absent (idéalement via un check-and-set / CAS Vault).
- Sérialisation par projet : introduire un verrou/mutex par slug autour de la réconciliation d'un projet, afin que deux
project.upsert du mĂȘme projet ne se chevauchent pas.
- Transverse (recommandé) : traiter la sérialisation par projet au niveau du bus d'événements / gestionnaire de plugins (
app-events.service.ts), plutĂŽt que plugin par plugin â cela protĂšge l'ensemble des plugins (GitLab, Nexus, Registry, VaultâŠ) contre la mĂȘme classe de bug. Corollaire : les Ă©missions project.upsert en fire-and-forget (deployment.service.ts:98, environment.service.ts:76) devraient passer par ce mĂ©canisme sĂ©rialisĂ©.
Version de la console impactée
v9.23.0
Description
Sur le nouveau serveur
server-nestjs, il arrive de façon intermittente que le secret VaultSONARd'un projet contienneSONAR_PASSWORD: 'not initialized'au lieu du vrai mot de passe. Avec l'ancien pluginplugins/sonarqube, ce problÚme n'apparaßt jamais.La valeur
'not initialized'n'est pas un bug en soi : c'est un repli lĂ©gitime lorsque l'utilisateur SonarQube existe mais que son secret Vault a rĂ©ellement disparu (SonarQube ne permet pas de relire un mot de passe). Le problĂšme est que, dans le nouveau serveur, cette valeur est Ă©crite alors qu'un mot de passe valide vient d'ĂȘtre créé â il y a donc perte de donnĂ©es.Cause : une race condition, pas un bug de chemin. La logique et le chemin Vault (
<PROJECTS_ROOT_DIR>/<slug>/SONAR) sont identiques entre l'ancien plugin et le nouveau serveur. La diffĂ©rence est la concurrence :project.upsertest Ă©mis depuis de nombreux endroits (project,project-roles,environment,deployment,project-hooksâŠ) ;await(fire-and-forget, ex.deployment.service.ts:98,environment.service.ts:76), donc deux rĂ©conciliations du mĂȘme projet peuvent se chevaucher ;emitAsyncn'a aucun verrou par projet).ensureUsereffectue une sĂ©quence lecture â vĂ©rification â Ă©criture non atomique sur le secret. Sur un projet fraĂźchement créé, deux exĂ©cutions concurrentes se dĂ©roulent ainsi :elseSONAR_PASSWORD: 'not initialized'â Ă©crase le mot de passe de ALe rĂ©sultat dĂ©pend de l'ordre d'exĂ©cution â comportement non dĂ©terministe (« parfois »).
Etapes de reproduction
Piste technique (oĂč se trouve l'erreur & suggestions)
SymptÎme localisé dans
apps/server-nestjs/src/modules/sonarqube/sonarqube.service.ts, méthodeensureUser(lecture ligne 201, brancheelseligne 213-216, écriture ligne 220).Suggestions, du plus ciblé au plus transverse :
SONAR_PASSWORDvalide existant. Relire le secret juste avant d'Ă©crire, ou n'Ă©crire'not initialized'qu'aprĂšs confirmation que le secret est rĂ©ellement absent (idĂ©alement via un check-and-set / CAS Vault).project.upsertdu mĂȘme projet ne se chevauchent pas.app-events.service.ts), plutĂŽt que plugin par plugin â cela protĂšge l'ensemble des plugins (GitLab, Nexus, Registry, VaultâŠ) contre la mĂȘme classe de bug. Corollaire : les Ă©missionsproject.upserten fire-and-forget (deployment.service.ts:98,environment.service.ts:76) devraient passer par ce mĂ©canisme sĂ©rialisĂ©.Version de la console impactĂ©e
v9.23.0