From 078aa172856423c093cd73722252ad5cfc13f67d Mon Sep 17 00:00:00 2001 From: thibaud-lclr Date: Wed, 13 May 2026 12:23:18 +0200 Subject: [PATCH] =?UTF-8?q?fix(secretstore):=20relire=20la=20session=20Bit?= =?UTF-8?q?warden=20depuis=20le=20fichier=20avant=20chaque=20op=C3=A9ratio?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quand un MCP appelle login/unlock, le token est écrit dans le fichier de session mais les autres MCPs conservent leur token obsolète dans l'environnement du processus. Désormais, bitwardenStore.ensureReady() appelle refreshSessionEnv() qui relit le fichier avant chaque vérification, ce qui permet à tous les MCPs de rester opérationnels après une rotation de session. Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 4 ++ secretstore/bitwarden.go | 9 ++++ secretstore/bitwarden_test.go | 87 +++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ba4672..039b5fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ - **Bootstrap — `StandardConfigTestHandler`** : handler de config test standard sans `ManifestCheck`. Accepte `ConfigCheck`, `OpenStore`, `ConnectivityCheck` et `ExtraChecks`. - **CLI — `ManifestCheck` opt-in dans `RunDoctor`** : le check de manifeste n'est inclus que si `ManifestDir` est fourni, supprimant une contrainte runtime inutile. +### Corrections + +- **Secretstore — rafraîchissement automatique de la session Bitwarden** : chaque MCP relit désormais le fichier de session avant toute opération Bitwarden. Cela résout le cas où débloquer le vault depuis un MCP rendait les autres inopérants (session obsolète ou absente de l'environnement). + ### Changements cassants - **Bootstrap — `DefaultLoginHandler` renommé en `BitwardenLoginHandler`** : le nom précédent suggérait à tort que le handler s'applique à tous les MCPs. Les projets sans backend Bitwarden ne définissent pas de hook Login — la commande est masquée automatiquement par `autoDisabledCommands`. diff --git a/secretstore/bitwarden.go b/secretstore/bitwarden.go index 9c580b8..c192db4 100644 --- a/secretstore/bitwarden.go +++ b/secretstore/bitwarden.go @@ -384,6 +384,7 @@ func (s *bitwardenStore) scopedName(name string) string { } func (s *bitwardenStore) ensureReady() error { + s.refreshSessionEnv() return verifyBitwardenCLIReady(Options{ BitwardenCommand: s.command, BitwardenDebug: s.debug, @@ -392,6 +393,14 @@ func (s *bitwardenStore) ensureReady() error { }) } +func (s *bitwardenStore) refreshSessionEnv() { + session, err := LoadBitwardenSession(BitwardenSessionOptions{ServiceName: s.serviceName}) + if err != nil || strings.TrimSpace(session) == "" { + return + } + _ = os.Setenv(bitwardenSessionEnvName, strings.TrimSpace(session)) +} + type bitwardenResolvedItem struct { item bitwardenListItem payload map[string]any diff --git a/secretstore/bitwarden_test.go b/secretstore/bitwarden_test.go index b895c2c..705a467 100644 --- a/secretstore/bitwarden_test.go +++ b/secretstore/bitwarden_test.go @@ -66,6 +66,9 @@ func TestBitwardenStoreMissReturnsUnavailableWhenCommandIsMissing(t *testing.T) } func TestBitwardenStoreMissFailsWhenSessionIsMissing(t *testing.T) { + withBitwardenUserConfigDir(t, func() (string, error) { + return t.TempDir(), nil + }) fakeCLI := newFakeBitwardenCLI("bw") withBitwardenRunner(t, fakeCLI.run) @@ -572,6 +575,9 @@ func TestBitwardenStoreDeleteSecretInvalidatesCache(t *testing.T) { } func TestBitwardenFindItemTreatsEmptyListOutputAsNotFound(t *testing.T) { + withBitwardenUserConfigDir(t, func() (string, error) { + return t.TempDir(), nil + }) store := &bitwardenStore{command: "bw", serviceName: "email-mcp"} withBitwardenRunner(t, func(command string, stdin []byte, args ...string) ([]byte, error) { if len(args) == 4 && args[0] == "list" && args[1] == "items" && args[2] == "--search" { @@ -728,6 +734,84 @@ func stripANSIControlSequences(value string) string { return strings.ReplaceAll(noANSI, "\r", "") } +func TestBitwardenStorePicksUpSessionRotatedByAnotherProcess(t *testing.T) { + t.Setenv("BW_SESSION", "old-session") + configDir := t.TempDir() + withBitwardenUserConfigDir(t, func() (string, error) { + return configDir, nil + }) + withBitwardenUserCacheDir(t, func() (string, error) { + return t.TempDir(), nil + }) + fakeCLI := newFakeBitwardenCLI("bw") + fakeCLI.itemsByID["item-1"] = fakeBitwardenItem{ + ID: "item-1", + Name: "email-mcp/api-token", + Secret: "secret-v1", + MarkerService: "email-mcp", + MarkerSecretName: "api-token", + } + withBitwardenRunner(t, fakeCLI.run) + + store, err := Open(Options{ + ServiceName: "email-mcp", + BackendPolicy: BackendBitwardenCLI, + }) + if err != nil { + t.Fatalf("Open returned error: %v", err) + } + + if _, err := SaveBitwardenSession(BitwardenSessionOptions{ServiceName: "email-mcp"}, "new-session"); err != nil { + t.Fatalf("SaveBitwardenSession returned error: %v", err) + } + + if _, err := store.GetSecret("api-token"); err != nil { + t.Fatalf("GetSecret returned error: %v", err) + } + if got := os.Getenv("BW_SESSION"); got != "new-session" { + t.Fatalf("BW_SESSION = %q, want new-session after session rotation", got) + } +} + +func TestBitwardenStorePicksUpSessionFromFileWhenEnvIsEmpty(t *testing.T) { + t.Setenv("BW_SESSION", "") + configDir := t.TempDir() + withBitwardenUserConfigDir(t, func() (string, error) { + return configDir, nil + }) + withBitwardenUserCacheDir(t, func() (string, error) { + return t.TempDir(), nil + }) + fakeCLI := newFakeBitwardenCLI("bw") + fakeCLI.itemsByID["item-1"] = fakeBitwardenItem{ + ID: "item-1", + Name: "email-mcp/api-token", + Secret: "secret-v1", + MarkerService: "email-mcp", + MarkerSecretName: "api-token", + } + withBitwardenRunner(t, fakeCLI.run) + + store, err := Open(Options{ + ServiceName: "email-mcp", + BackendPolicy: BackendBitwardenCLI, + }) + if err != nil { + t.Fatalf("Open returned error: %v", err) + } + + if _, err := SaveBitwardenSession(BitwardenSessionOptions{ServiceName: "email-mcp"}, "file-session"); err != nil { + t.Fatalf("SaveBitwardenSession returned error: %v", err) + } + + if _, err := store.GetSecret("api-token"); err != nil { + t.Fatalf("GetSecret returned error: %v", err) + } + if got := os.Getenv("BW_SESSION"); got != "file-session" { + t.Fatalf("BW_SESSION = %q, want file-session loaded from file after login by another process", got) + } +} + func TestOpenFromManifestSupportsBitwardenCLIBackendPolicy(t *testing.T) { withBitwardenSession(t) fakeCLI := newFakeBitwardenCLI("bw") @@ -768,6 +852,9 @@ func withBitwardenSession(t *testing.T) { withBitwardenUserCacheDir(t, func() (string, error) { return t.TempDir(), nil }) + withBitwardenUserConfigDir(t, func() (string, error) { + return t.TempDir(), nil + }) } func withBitwardenRunner(