Skip corrupt unlocker metadata in unlocker select and remove (closes #72)
check / check (push) Waiting to run
check / check (push) Waiting to run
findUnlockerByID failed on the first unlocker directory whose metadata could not be checked, read or parsed, so `secret unlocker select` and `remove` failed when one sorted before the unlocker asked for. It now skips such a directory with the warning ListUnlockers gives, through the code both now share. A skipped directory is removed by its directory name, with RemoveDirAtomic, and cannot be selected. Removing one whose metadata file is missing or corrupt never counts as removing the last unlocker; removing one whose metadata file cannot be checked for or read does, since it may be the only working unlocker. Model: opus-5-5
This commit was merged in pull request #91.
This commit is contained in:
+77
-70
@@ -126,7 +126,11 @@ func (v *Vault) resolveUnlockerDirectory(currentUnlockerPath string) (string, er
|
||||
}
|
||||
|
||||
// findUnlockerByID finds an unlocker by its ID and returns the unlocker
|
||||
// instance and its directory path
|
||||
// instance and its directory path. A directory that ListUnlockers skips is
|
||||
// skipped here too, with the same warning. Such a directory has no ID: if
|
||||
// no unlocker has the ID unlockerID but such a directory is named
|
||||
// unlockerID, that directory is returned with a nil unlocker, so that
|
||||
// RemoveUnlocker can remove it.
|
||||
//
|
||||
//nolint:ireturn // returns one of several concrete unlocker implementations
|
||||
func (v *Vault) findUnlockerByID(
|
||||
@@ -137,42 +141,24 @@ func (v *Vault) findUnlockerByID(
|
||||
return nil, "", fmt.Errorf("failed to read unlockers directory: %w", err)
|
||||
}
|
||||
|
||||
skippedDirPath := ""
|
||||
|
||||
for _, file := range files {
|
||||
if !file.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Read metadata file
|
||||
metadataPath := filepath.Join(unlockersDir, file.Name(), "unlocker-metadata.json")
|
||||
unlockerDirPath := filepath.Join(unlockersDir, file.Name())
|
||||
|
||||
exists, err := afero.Exists(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf(
|
||||
"failed to check if metadata exists for unlocker %s: %w",
|
||||
file.Name(), err)
|
||||
}
|
||||
metadata, ok := v.readUnlockerMetadataOrWarn(unlockersDir, file.Name())
|
||||
if !ok {
|
||||
if file.Name() == unlockerID {
|
||||
skippedDirPath = unlockerDirPath
|
||||
}
|
||||
|
||||
if !exists {
|
||||
// Skip directories without metadata - they might not be unlockers
|
||||
continue
|
||||
}
|
||||
|
||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf(
|
||||
"failed to read metadata for unlocker %s: %w", file.Name(), err)
|
||||
}
|
||||
|
||||
var metadata UnlockerMetadata
|
||||
|
||||
err = json.Unmarshal(metadataBytes, &metadata)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf(
|
||||
"failed to parse metadata for unlocker %s: %w", file.Name(), err)
|
||||
}
|
||||
|
||||
unlockerDirPath := filepath.Join(unlockersDir, file.Name())
|
||||
|
||||
// Create the appropriate unlocker instance
|
||||
var tempUnlocker secret.Unlocker
|
||||
|
||||
@@ -195,7 +181,7 @@ func (v *Vault) findUnlockerByID(
|
||||
}
|
||||
}
|
||||
|
||||
return nil, "", nil
|
||||
return nil, skippedDirPath, nil
|
||||
}
|
||||
|
||||
// ListUnlockers returns a list of available unlockers for this vault
|
||||
@@ -226,44 +212,12 @@ func (v *Vault) ListUnlockers() ([]UnlockerMetadata, error) {
|
||||
var unlockers []UnlockerMetadata
|
||||
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
// Read metadata file
|
||||
metadataPath := filepath.Join(unlockersDir, file.Name(),
|
||||
"unlocker-metadata.json")
|
||||
|
||||
exists, err := afero.Exists(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
secret.Warn("Skipping unlocker directory whose metadata file cannot be checked",
|
||||
"directory", file.Name(), "error", err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if !exists {
|
||||
secret.Warn("Skipping unlocker directory with missing metadata file",
|
||||
"directory", file.Name())
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
secret.Warn("Skipping unlocker directory with unreadable metadata file",
|
||||
"directory", file.Name(), "error", err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
var metadata UnlockerMetadata
|
||||
|
||||
err = json.Unmarshal(metadataBytes, &metadata)
|
||||
if err != nil {
|
||||
secret.Warn("Skipping unlocker directory with corrupt metadata file",
|
||||
"directory", file.Name(), "error", err)
|
||||
|
||||
continue
|
||||
}
|
||||
if !file.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
metadata, ok := v.readUnlockerMetadataOrWarn(unlockersDir, file.Name())
|
||||
if ok {
|
||||
unlockers = append(unlockers, metadata)
|
||||
}
|
||||
}
|
||||
@@ -271,7 +225,54 @@ func (v *Vault) ListUnlockers() ([]UnlockerMetadata, error) {
|
||||
return unlockers, nil
|
||||
}
|
||||
|
||||
// RemoveUnlocker removes an unlocker from this vault
|
||||
// readUnlockerMetadataOrWarn reads the metadata of the unlocker directory
|
||||
// name in unlockersDir. If the metadata file cannot be checked for, is
|
||||
// missing, or cannot be read or parsed, it warns, naming the directory,
|
||||
// and returns false: the caller skips that directory.
|
||||
func (v *Vault) readUnlockerMetadataOrWarn(
|
||||
unlockersDir, name string,
|
||||
) (UnlockerMetadata, bool) {
|
||||
metadataPath := filepath.Join(unlockersDir, name, "unlocker-metadata.json")
|
||||
|
||||
var metadata UnlockerMetadata
|
||||
|
||||
exists, err := afero.Exists(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
secret.Warn("Skipping unlocker directory whose metadata file cannot be checked",
|
||||
"directory", name, "error", err)
|
||||
|
||||
return metadata, false
|
||||
}
|
||||
|
||||
if !exists {
|
||||
secret.Warn("Skipping unlocker directory with missing metadata file",
|
||||
"directory", name)
|
||||
|
||||
return metadata, false
|
||||
}
|
||||
|
||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
secret.Warn("Skipping unlocker directory with unreadable metadata file",
|
||||
"directory", name, "error", err)
|
||||
|
||||
return metadata, false
|
||||
}
|
||||
|
||||
err = json.Unmarshal(metadataBytes, &metadata)
|
||||
if err != nil {
|
||||
secret.Warn("Skipping unlocker directory with corrupt metadata file",
|
||||
"directory", name, "error", err)
|
||||
|
||||
return metadata, false
|
||||
}
|
||||
|
||||
return metadata, true
|
||||
}
|
||||
|
||||
// RemoveUnlocker removes an unlocker from this vault. An unlocker
|
||||
// directory that ListUnlockers skips is removed by its directory name; its
|
||||
// type is unknown, so only the directory is removed.
|
||||
func (v *Vault) RemoveUnlocker(unlockerID string) error {
|
||||
vaultDir, err := v.GetDirectory()
|
||||
if err != nil {
|
||||
@@ -282,15 +283,19 @@ func (v *Vault) RemoveUnlocker(unlockerID string) error {
|
||||
unlockersDir := filepath.Join(vaultDir, "unlockers.d")
|
||||
|
||||
// Find the unlocker by ID
|
||||
unlocker, _, err := v.findUnlockerByID(unlockersDir, unlockerID)
|
||||
unlocker, unlockerDir, err := v.findUnlockerByID(unlockersDir, unlockerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if unlocker == nil {
|
||||
if unlockerDir == "" {
|
||||
return fmt.Errorf("unlocker with ID %s %w", unlockerID, ErrUnlockerNotFound)
|
||||
}
|
||||
|
||||
if unlocker == nil {
|
||||
return secret.RemoveDirAtomic(v.fs, unlockerDir)
|
||||
}
|
||||
|
||||
// Use the unlocker's Remove method
|
||||
return unlocker.Remove()
|
||||
}
|
||||
@@ -306,12 +311,14 @@ func (v *Vault) SelectUnlocker(unlockerID string) error {
|
||||
unlockersDir := filepath.Join(vaultDir, "unlockers.d")
|
||||
|
||||
// Find the unlocker by ID
|
||||
_, targetUnlockerDir, err := v.findUnlockerByID(unlockersDir, unlockerID)
|
||||
unlocker, targetUnlockerDir, err := v.findUnlockerByID(unlockersDir, unlockerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if targetUnlockerDir == "" {
|
||||
// A directory found without an unlocker is one ListUnlockers skips; it
|
||||
// cannot be selected.
|
||||
if unlocker == nil {
|
||||
return fmt.Errorf("unlocker with ID %s %w", unlockerID, ErrUnlockerNotFound)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user