178 lines
5.3 KiB
Go
178 lines
5.3 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var metaCmd = &cobra.Command{
|
|
Use: "meta KEY[@STORE]",
|
|
Short: "View or modify metadata for a key",
|
|
Long: `View or modify metadata (TTL, encryption, read-only, pinned) for a key
|
|
without changing its value.
|
|
|
|
With no flags, displays the key's current metadata. Pass flags to modify.`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: meta,
|
|
SilenceUsage: true,
|
|
}
|
|
|
|
func meta(cmd *cobra.Command, args []string) error {
|
|
store := &Store{}
|
|
|
|
spec, err := store.parseKey(args[0], true)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot meta '%s': %v", args[0], err)
|
|
}
|
|
|
|
identity, _ := loadIdentity()
|
|
|
|
p, err := store.storePath(spec.DB)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot meta '%s': %v", args[0], err)
|
|
}
|
|
entries, err := readStoreFile(p, identity)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot meta '%s': %v", args[0], err)
|
|
}
|
|
idx := findEntry(entries, spec.Key)
|
|
if idx < 0 {
|
|
keys := make([]string, len(entries))
|
|
for i, e := range entries {
|
|
keys[i] = e.Key
|
|
}
|
|
return fmt.Errorf("cannot meta '%s': %w", args[0], suggestKey(spec.Key, keys))
|
|
}
|
|
entry := &entries[idx]
|
|
|
|
ttlStr, _ := cmd.Flags().GetString("ttl")
|
|
encryptFlag, _ := cmd.Flags().GetBool("encrypt")
|
|
decryptFlag, _ := cmd.Flags().GetBool("decrypt")
|
|
readonlyFlag, _ := cmd.Flags().GetBool("readonly")
|
|
writableFlag, _ := cmd.Flags().GetBool("writable")
|
|
pinFlag, _ := cmd.Flags().GetBool("pin")
|
|
unpinFlag, _ := cmd.Flags().GetBool("unpin")
|
|
force, _ := cmd.Flags().GetBool("force")
|
|
|
|
if encryptFlag && decryptFlag {
|
|
return fmt.Errorf("cannot meta '%s': --encrypt and --decrypt are mutually exclusive", args[0])
|
|
}
|
|
if readonlyFlag && writableFlag {
|
|
return fmt.Errorf("cannot meta '%s': --readonly and --writable are mutually exclusive", args[0])
|
|
}
|
|
if pinFlag && unpinFlag {
|
|
return fmt.Errorf("cannot meta '%s': --pin and --unpin are mutually exclusive", args[0])
|
|
}
|
|
|
|
// View mode: no flags set
|
|
isModify := ttlStr != "" || encryptFlag || decryptFlag || readonlyFlag || writableFlag || pinFlag || unpinFlag
|
|
if !isModify {
|
|
expiresStr := "never"
|
|
if entry.ExpiresAt > 0 {
|
|
expiresStr = formatExpiry(entry.ExpiresAt)
|
|
}
|
|
fmt.Fprintf(cmd.OutOrStdout(), " key: %s\n", spec.Full())
|
|
fmt.Fprintf(cmd.OutOrStdout(), " secret: %v\n", entry.Secret)
|
|
fmt.Fprintf(cmd.OutOrStdout(), " writable: %v\n", !entry.ReadOnly)
|
|
fmt.Fprintf(cmd.OutOrStdout(), " pinned: %v\n", entry.Pinned)
|
|
fmt.Fprintf(cmd.OutOrStdout(), " expires: %s\n", expiresStr)
|
|
return nil
|
|
}
|
|
|
|
// Read-only enforcement: --readonly and --writable always work without --force,
|
|
// but other modifications on a read-only key require --force.
|
|
if entry.ReadOnly && !force && !readonlyFlag && !writableFlag {
|
|
onlyPinChange := !encryptFlag && !decryptFlag && ttlStr == "" && (pinFlag || unpinFlag)
|
|
if !onlyPinChange {
|
|
return fmt.Errorf("cannot meta '%s': key is read-only", args[0])
|
|
}
|
|
}
|
|
|
|
// Modification mode — may need identity for encrypt
|
|
if encryptFlag {
|
|
identity, err = ensureIdentity()
|
|
if err != nil {
|
|
return fmt.Errorf("cannot meta '%s': %v", args[0], err)
|
|
}
|
|
}
|
|
recipients, err := allRecipients(identity)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot meta '%s': %v", args[0], err)
|
|
}
|
|
|
|
var changes []string
|
|
|
|
if ttlStr != "" {
|
|
expiresAt, err := parseTTLString(ttlStr)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot meta '%s': %v", args[0], err)
|
|
}
|
|
entry.ExpiresAt = expiresAt
|
|
if expiresAt == 0 {
|
|
changes = append(changes, "cleared ttl")
|
|
} else {
|
|
changes = append(changes, "set ttl to "+ttlStr)
|
|
}
|
|
}
|
|
|
|
if encryptFlag {
|
|
if entry.Secret {
|
|
return fmt.Errorf("cannot meta '%s': already encrypted", args[0])
|
|
}
|
|
if entry.Locked {
|
|
return fmt.Errorf("cannot meta '%s': secret is locked (identity file missing)", args[0])
|
|
}
|
|
entry.Secret = true
|
|
changes = append(changes, "encrypted")
|
|
}
|
|
|
|
if decryptFlag {
|
|
if !entry.Secret {
|
|
return fmt.Errorf("cannot meta '%s': not encrypted", args[0])
|
|
}
|
|
if entry.Locked {
|
|
return fmt.Errorf("cannot meta '%s': secret is locked (identity file missing)", args[0])
|
|
}
|
|
entry.Secret = false
|
|
changes = append(changes, "decrypted")
|
|
}
|
|
|
|
if readonlyFlag {
|
|
entry.ReadOnly = true
|
|
changes = append(changes, "made readonly")
|
|
}
|
|
if writableFlag {
|
|
entry.ReadOnly = false
|
|
changes = append(changes, "made writable")
|
|
}
|
|
if pinFlag {
|
|
entry.Pinned = true
|
|
changes = append(changes, "pinned")
|
|
}
|
|
if unpinFlag {
|
|
entry.Pinned = false
|
|
changes = append(changes, "unpinned")
|
|
}
|
|
|
|
if err := writeStoreFile(p, entries, recipients); err != nil {
|
|
return fmt.Errorf("cannot meta '%s': %v", args[0], err)
|
|
}
|
|
|
|
summary := strings.Join(changes, ", ")
|
|
okf("%s %s", summary, spec.Display())
|
|
return autoSync(summary + " " + spec.Display())
|
|
}
|
|
|
|
func init() {
|
|
metaCmd.Flags().String("ttl", "", "set expiry (e.g. 30m, 2h) or 'never' to clear")
|
|
metaCmd.Flags().BoolP("encrypt", "e", false, "encrypt the value at rest")
|
|
metaCmd.Flags().BoolP("decrypt", "d", false, "decrypt the value (store as plaintext)")
|
|
metaCmd.Flags().Bool("readonly", false, "mark the key as read-only")
|
|
metaCmd.Flags().Bool("writable", false, "clear the read-only flag")
|
|
metaCmd.Flags().Bool("pin", false, "pin the key (sorts to top in list)")
|
|
metaCmd.Flags().Bool("unpin", false, "unpin the key")
|
|
metaCmd.Flags().Bool("force", false, "bypass read-only protection for metadata changes")
|
|
rootCmd.AddCommand(metaCmd)
|
|
}
|