fix(secretstore): relire la session Bitwarden depuis le fichier avant chaque opération
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 <noreply@anthropic.com>
This commit is contained in:
parent
200674778b
commit
078aa17285
3 changed files with 100 additions and 0 deletions
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue