174 lines
4.7 KiB
Go
174 lines
4.7 KiB
Go
package command
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/vault/api"
|
|
"github.com/posener/complete"
|
|
)
|
|
|
|
// defaultPredictVaultMounts is the default list of mounts to return to the
|
|
// user. This is a best-guess, given we haven't communicated with the Vault
|
|
// server. If the user has no token or if the token does not have the default
|
|
// policy attached, it won't be able to read cubbyhole/, but it's a better UX
|
|
// that returning nothing.
|
|
var defaultPredictVaultMounts = []string{"cubbyhole/"}
|
|
|
|
// PredictVaultPaths returns a predictor for Vault mounts and paths based on the
|
|
// configured client for the base command. Unfortunately this happens pre-flag
|
|
// parsing, so users must rely on environment variables for autocomplete if they
|
|
// are not using Vault at the default endpoints.
|
|
func (b *BaseCommand) PredictVaultPaths() complete.Predictor {
|
|
client, err := b.Client()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return PredictVaultPaths(client)
|
|
}
|
|
|
|
// PredictVaultPaths returns a predictor for Vault paths. This is a public API
|
|
// for consumers, but you probably want BaseCommand.PredictVaultPaths instead.
|
|
func PredictVaultPaths(client *api.Client) complete.Predictor {
|
|
return predictVaultPaths(client)
|
|
}
|
|
|
|
// predictVaultPaths parses the CLI options and returns the "best" list of
|
|
// possible paths. If there are any errors, this function returns an empty
|
|
// result. All errors are suppressed since this is a prediction function.
|
|
func predictVaultPaths(client *api.Client) complete.PredictFunc {
|
|
return func(args complete.Args) []string {
|
|
// Do not predict more than one paths
|
|
if predictHasPathArg(args.All) {
|
|
return nil
|
|
}
|
|
|
|
path := args.Last
|
|
|
|
var predictions []string
|
|
if strings.Contains(path, "/") {
|
|
predictions = predictPaths(client, path)
|
|
} else {
|
|
predictions = predictMounts(client, path)
|
|
}
|
|
|
|
// Either no results or many results, so return.
|
|
if len(predictions) != 1 {
|
|
return predictions
|
|
}
|
|
|
|
// If this is not a "folder", do not try to recurse.
|
|
if !strings.HasSuffix(predictions[0], "/") {
|
|
return predictions
|
|
}
|
|
|
|
// If the prediction is the same as the last guess, return it (we have no
|
|
// new information and we won't get anymore).
|
|
if predictions[0] == args.Last {
|
|
return predictions
|
|
}
|
|
|
|
// Re-predict with the remaining path
|
|
args.Last = predictions[0]
|
|
return predictVaultPaths(client).Predict(args)
|
|
}
|
|
}
|
|
|
|
// predictMounts predicts all mounts which start with the given prefix. These
|
|
// are predicted on mount path, not "type".
|
|
func predictMounts(client *api.Client, path string) []string {
|
|
mounts := predictListMounts(client)
|
|
|
|
var predictions []string
|
|
for _, m := range mounts {
|
|
if strings.HasPrefix(m, path) {
|
|
predictions = append(predictions, m)
|
|
}
|
|
}
|
|
|
|
return predictions
|
|
}
|
|
|
|
// predictPaths predicts all paths which start with the given path.
|
|
func predictPaths(client *api.Client, path string) []string {
|
|
// Vault does not support listing based on a sub-key, so we have to back-pedal
|
|
// to the last "/" and return all paths on that "folder". Then we perform
|
|
// client-side filtering.
|
|
root := path
|
|
idx := strings.LastIndex(root, "/")
|
|
if idx > 0 && idx < len(root) {
|
|
root = root[:idx+1]
|
|
}
|
|
|
|
paths := predictListPaths(client, root)
|
|
|
|
var predictions []string
|
|
for _, p := range paths {
|
|
// Calculate the absolute "path" for matching.
|
|
p = root + p
|
|
|
|
if strings.HasPrefix(p, path) {
|
|
predictions = append(predictions, p)
|
|
}
|
|
}
|
|
|
|
// Add root to the path
|
|
if len(predictions) == 0 {
|
|
predictions = append(predictions, path)
|
|
}
|
|
|
|
return predictions
|
|
}
|
|
|
|
// predictListMounts returns a sorted list of the mount paths for Vault server
|
|
// for which the client is configured to communicate with. This function returns
|
|
// the default list of mounts if an error occurs.
|
|
func predictListMounts(c *api.Client) []string {
|
|
mounts, err := c.Sys().ListMounts()
|
|
if err != nil {
|
|
return defaultPredictVaultMounts
|
|
}
|
|
|
|
list := make([]string, 0, len(mounts))
|
|
for m := range mounts {
|
|
list = append(list, m)
|
|
}
|
|
sort.Strings(list)
|
|
return list
|
|
}
|
|
|
|
// predictListPaths returns a list of paths (HTTP LIST) for the given path. This
|
|
// function returns an empty list of any errors occur.
|
|
func predictListPaths(c *api.Client, path string) []string {
|
|
secret, err := c.Logical().List(path)
|
|
if err != nil || secret == nil || secret.Data == nil {
|
|
return nil
|
|
}
|
|
|
|
paths, ok := secret.Data["keys"].([]interface{})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
list := make([]string, 0, len(paths))
|
|
for _, p := range paths {
|
|
if str, ok := p.(string); ok {
|
|
list = append(list, str)
|
|
}
|
|
}
|
|
sort.Strings(list)
|
|
return list
|
|
}
|
|
|
|
// predictHasPathArg determines if the args have already accepted a path.
|
|
func predictHasPathArg(args []string) bool {
|
|
var nonFlags []string
|
|
for _, a := range args {
|
|
if !strings.HasPrefix(a, "-") {
|
|
nonFlags = append(nonFlags, a)
|
|
}
|
|
}
|
|
|
|
return len(nonFlags) > 2
|
|
}
|