feat(meta): add meta command for viewing/modifying key metadata
This commit is contained in:
parent
a382e8dc79
commit
618842b285
7 changed files with 189 additions and 0 deletions
124
cmd/meta.go
Normal file
124
cmd/meta.go
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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) for a key without changing its value.
|
||||
|
||||
With no flags, displays the key's current metadata. Use flags to modify:
|
||||
--ttl DURATION Set expiry (e.g. 30m, 2h)
|
||||
--ttl never Remove expiry
|
||||
--encrypt Encrypt the value at rest
|
||||
--decrypt Decrypt the value (store as plaintext)`,
|
||||
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")
|
||||
|
||||
if encryptFlag && decryptFlag {
|
||||
return fmt.Errorf("cannot meta '%s': --encrypt and --decrypt are mutually exclusive", args[0])
|
||||
}
|
||||
|
||||
// View mode: no flags set
|
||||
if ttlStr == "" && !encryptFlag && !decryptFlag {
|
||||
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(), " expires: %s\n", expiresStr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
if ttlStr != "" {
|
||||
expiresAt, err := parseTTLString(ttlStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot meta '%s': %v", args[0], err)
|
||||
}
|
||||
entry.ExpiresAt = expiresAt
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if err := writeStoreFile(p, entries, recipients); err != nil {
|
||||
return fmt.Errorf("cannot meta '%s': %v", args[0], err)
|
||||
}
|
||||
|
||||
return autoSync("meta " + 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)")
|
||||
rootCmd.AddCommand(metaCmd)
|
||||
}
|
||||
|
|
@ -70,6 +70,7 @@ func init() {
|
|||
cpCmd.GroupID = "keys"
|
||||
delCmd.GroupID = "keys"
|
||||
listCmd.GroupID = "keys"
|
||||
metaCmd.GroupID = "keys"
|
||||
identityCmd.GroupID = "keys"
|
||||
|
||||
rootCmd.AddGroup(&cobra.Group{ID: "stores", Title: "Store commands:"})
|
||||
|
|
|
|||
9
testdata/meta-decrypt.ct
vendored
Normal file
9
testdata/meta-decrypt.ct
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Decrypt an existing encrypted key
|
||||
$ pda set --encrypt hello@md world
|
||||
$ pda meta hello@md --decrypt
|
||||
$ pda meta hello@md
|
||||
key: hello@md
|
||||
secret: false
|
||||
expires: never
|
||||
$ pda get hello@md
|
||||
world
|
||||
10
testdata/meta-encrypt.ct
vendored
Normal file
10
testdata/meta-encrypt.ct
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Encrypt an existing plaintext key
|
||||
$ pda set hello@me world
|
||||
$ pda meta hello@me --encrypt
|
||||
$ pda meta hello@me
|
||||
key: hello@me
|
||||
secret: true
|
||||
expires: never
|
||||
# Value should still be retrievable
|
||||
$ pda get hello@me
|
||||
world
|
||||
21
testdata/meta-err.ct
vendored
Normal file
21
testdata/meta-err.ct
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Error: key doesn't exist
|
||||
$ pda meta nonexistent@me --> FAIL
|
||||
FAIL cannot meta 'nonexistent@me': no such key
|
||||
|
||||
# Error: --encrypt and --decrypt are mutually exclusive
|
||||
$ pda set hello@me world
|
||||
$ pda meta hello@me --encrypt --decrypt --> FAIL
|
||||
FAIL cannot meta 'hello@me': --encrypt and --decrypt are mutually exclusive
|
||||
|
||||
# Error: already encrypted
|
||||
$ pda set --encrypt secret@me val
|
||||
$ pda meta secret@me --encrypt --> FAIL
|
||||
FAIL cannot meta 'secret@me': already encrypted
|
||||
|
||||
# Error: not encrypted (can't decrypt)
|
||||
$ pda meta hello@me --decrypt --> FAIL
|
||||
FAIL cannot meta 'hello@me': not encrypted
|
||||
|
||||
# Error: invalid TTL
|
||||
$ pda meta hello@me --ttl "abc" --> FAIL
|
||||
FAIL cannot meta 'hello@me': invalid ttl '"abc"': expected a duration (e.g. 30m, 2h) or 'never'
|
||||
11
testdata/meta-ttl.ct
vendored
Normal file
11
testdata/meta-ttl.ct
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Set TTL on a key, then view it (just verify no error, can't match dynamic time)
|
||||
$ pda set hello@mt world
|
||||
$ pda meta hello@mt --ttl 1h
|
||||
|
||||
# Clear TTL with --ttl never
|
||||
$ pda set --ttl 1h expiring@mt val
|
||||
$ pda meta expiring@mt --ttl never
|
||||
$ pda meta expiring@mt
|
||||
key: expiring@mt
|
||||
secret: false
|
||||
expires: never
|
||||
13
testdata/meta.ct
vendored
Normal file
13
testdata/meta.ct
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# View metadata for a plaintext key
|
||||
$ pda set hello@m world
|
||||
$ pda meta hello@m
|
||||
key: hello@m
|
||||
secret: false
|
||||
expires: never
|
||||
|
||||
# View metadata for an encrypted key
|
||||
$ pda set --encrypt secret@m hunter2
|
||||
$ pda meta secret@m
|
||||
key: secret@m
|
||||
secret: true
|
||||
expires: never
|
||||
Loading…
Add table
Add a link
Reference in a new issue