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`.
|
- **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.
|
- **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
|
### 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`.
|
- **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 {
|
func (s *bitwardenStore) ensureReady() error {
|
||||||
|
s.refreshSessionEnv()
|
||||||
return verifyBitwardenCLIReady(Options{
|
return verifyBitwardenCLIReady(Options{
|
||||||
BitwardenCommand: s.command,
|
BitwardenCommand: s.command,
|
||||||
BitwardenDebug: s.debug,
|
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 {
|
type bitwardenResolvedItem struct {
|
||||||
item bitwardenListItem
|
item bitwardenListItem
|
||||||
payload map[string]any
|
payload map[string]any
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,9 @@ func TestBitwardenStoreMissReturnsUnavailableWhenCommandIsMissing(t *testing.T)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBitwardenStoreMissFailsWhenSessionIsMissing(t *testing.T) {
|
func TestBitwardenStoreMissFailsWhenSessionIsMissing(t *testing.T) {
|
||||||
|
withBitwardenUserConfigDir(t, func() (string, error) {
|
||||||
|
return t.TempDir(), nil
|
||||||
|
})
|
||||||
fakeCLI := newFakeBitwardenCLI("bw")
|
fakeCLI := newFakeBitwardenCLI("bw")
|
||||||
withBitwardenRunner(t, fakeCLI.run)
|
withBitwardenRunner(t, fakeCLI.run)
|
||||||
|
|
||||||
|
|
@ -572,6 +575,9 @@ func TestBitwardenStoreDeleteSecretInvalidatesCache(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBitwardenFindItemTreatsEmptyListOutputAsNotFound(t *testing.T) {
|
func TestBitwardenFindItemTreatsEmptyListOutputAsNotFound(t *testing.T) {
|
||||||
|
withBitwardenUserConfigDir(t, func() (string, error) {
|
||||||
|
return t.TempDir(), nil
|
||||||
|
})
|
||||||
store := &bitwardenStore{command: "bw", serviceName: "email-mcp"}
|
store := &bitwardenStore{command: "bw", serviceName: "email-mcp"}
|
||||||
withBitwardenRunner(t, func(command string, stdin []byte, args ...string) ([]byte, error) {
|
withBitwardenRunner(t, func(command string, stdin []byte, args ...string) ([]byte, error) {
|
||||||
if len(args) == 4 && args[0] == "list" && args[1] == "items" && args[2] == "--search" {
|
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", "")
|
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) {
|
func TestOpenFromManifestSupportsBitwardenCLIBackendPolicy(t *testing.T) {
|
||||||
withBitwardenSession(t)
|
withBitwardenSession(t)
|
||||||
fakeCLI := newFakeBitwardenCLI("bw")
|
fakeCLI := newFakeBitwardenCLI("bw")
|
||||||
|
|
@ -768,6 +852,9 @@ func withBitwardenSession(t *testing.T) {
|
||||||
withBitwardenUserCacheDir(t, func() (string, error) {
|
withBitwardenUserCacheDir(t, func() (string, error) {
|
||||||
return t.TempDir(), nil
|
return t.TempDir(), nil
|
||||||
})
|
})
|
||||||
|
withBitwardenUserConfigDir(t, func() (string, error) {
|
||||||
|
return t.TempDir(), nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func withBitwardenRunner(
|
func withBitwardenRunner(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue