1package dependency
2
3import (
4	"fmt"
5	"log"
6	"net/url"
7	"path"
8	"sort"
9	"strings"
10	"time"
11
12	"github.com/pkg/errors"
13)
14
15var (
16	// Ensure implements
17	_ Dependency = (*VaultListQuery)(nil)
18)
19
20// VaultListQuery is the dependency to Vault for a secret
21type VaultListQuery struct {
22	stopCh chan struct{}
23
24	path string
25}
26
27// NewVaultListQuery creates a new datacenter dependency.
28func NewVaultListQuery(s string) (*VaultListQuery, error) {
29	s = strings.TrimSpace(s)
30	s = strings.Trim(s, "/")
31	if s == "" {
32		return nil, fmt.Errorf("vault.list: invalid format: %q", s)
33	}
34
35	return &VaultListQuery{
36		stopCh: make(chan struct{}, 1),
37		path:   s,
38	}, nil
39}
40
41// Fetch queries the Vault API
42func (d *VaultListQuery) Fetch(clients *ClientSet, opts *QueryOptions) (interface{}, *ResponseMetadata, error) {
43	select {
44	case <-d.stopCh:
45		return nil, nil, ErrStopped
46	default:
47	}
48
49	opts = opts.Merge(&QueryOptions{})
50
51	// If this is not the first query, poll to simulate blocking-queries.
52	if opts.WaitIndex != 0 {
53		dur := VaultDefaultLeaseDuration
54		log.Printf("[TRACE] %s: long polling for %s", d, dur)
55
56		select {
57		case <-d.stopCh:
58			return nil, nil, ErrStopped
59		case <-time.After(dur):
60		}
61	}
62
63	secretsPath := d.path
64
65	// Checking secret engine version. If it's v2, we should shim /metadata/
66	// to secret path if necessary.
67	mountPath, isV2, _ := isKVv2(clients.Vault(), secretsPath)
68	if isV2 {
69		secretsPath = shimKvV2ListPath(secretsPath, mountPath)
70	}
71
72	// If we got this far, we either didn't have a secret to renew, the secret was
73	// not renewable, or the renewal failed, so attempt a fresh list.
74	log.Printf("[TRACE] %s: LIST %s", d, &url.URL{
75		Path:     "/v1/" + secretsPath,
76		RawQuery: opts.String(),
77	})
78	secret, err := clients.Vault().Logical().List(secretsPath)
79	if err != nil {
80		return nil, nil, errors.Wrap(err, d.String())
81	}
82
83	var result []string
84
85	// The secret could be nil if it does not exist.
86	if secret == nil || secret.Data == nil {
87		log.Printf("[TRACE] %s: no data", d)
88		return respWithMetadata(result)
89	}
90
91	// This is a weird thing that happened once...
92	keys, ok := secret.Data["keys"]
93	if !ok {
94		log.Printf("[TRACE] %s: no keys", d)
95		return respWithMetadata(result)
96	}
97
98	list, ok := keys.([]interface{})
99	if !ok {
100		log.Printf("[TRACE] %s: not list", d)
101		return nil, nil, fmt.Errorf("%s: unexpected response", d)
102	}
103
104	for _, v := range list {
105		typed, ok := v.(string)
106		if !ok {
107			return nil, nil, fmt.Errorf("%s: non-string in list", d)
108		}
109		result = append(result, typed)
110	}
111	sort.Strings(result)
112
113	log.Printf("[TRACE] %s: returned %d results", d, len(result))
114
115	return respWithMetadata(result)
116}
117
118// CanShare returns if this dependency is shareable.
119func (d *VaultListQuery) CanShare() bool {
120	return false
121}
122
123// Stop halts the given dependency's fetch.
124func (d *VaultListQuery) Stop() {
125	close(d.stopCh)
126}
127
128// String returns the human-friendly version of this dependency.
129func (d *VaultListQuery) String() string {
130	return fmt.Sprintf("vault.list(%s)", d.path)
131}
132
133// Type returns the type of this dependency.
134func (d *VaultListQuery) Type() Type {
135	return TypeVault
136}
137
138// shimKvV2ListPath aligns the supported legacy path to KV v2 specs by inserting
139// /metadata/ into the path for listing secrets. Paths with /metadata/ are not modified.
140func shimKvV2ListPath(rawPath, mountPath string) string {
141	mountPath = strings.TrimSuffix(mountPath, "/")
142
143	if strings.HasPrefix(rawPath, path.Join(mountPath, "metadata")) {
144		// It doesn't need modifying.
145		return rawPath
146	}
147
148	switch {
149	case rawPath == mountPath:
150		return path.Join(mountPath, "metadata")
151	default:
152		rawPath = strings.TrimPrefix(rawPath, mountPath)
153		return path.Join(mountPath, "metadata", rawPath)
154	}
155}
156