1package dependency
2
3import (
4	"fmt"
5	"log"
6	"net/url"
7	"path"
8	"strings"
9	"time"
10
11	"github.com/hashicorp/vault/api"
12	"github.com/pkg/errors"
13)
14
15var (
16	// Ensure implements
17	_ Dependency = (*VaultReadQuery)(nil)
18)
19
20// VaultReadQuery is the dependency to Vault for a secret
21type VaultReadQuery struct {
22	stopCh  chan struct{}
23	sleepCh chan time.Duration
24
25	rawPath     string
26	queryValues url.Values
27	secret      *Secret
28	isKVv2      *bool
29	secretPath  string
30
31	// vaultSecret is the actual Vault secret which we are renewing
32	vaultSecret *api.Secret
33}
34
35// NewVaultReadQuery creates a new datacenter dependency.
36func NewVaultReadQuery(s string) (*VaultReadQuery, error) {
37	s = strings.TrimSpace(s)
38	s = strings.Trim(s, "/")
39	if s == "" {
40		return nil, fmt.Errorf("vault.read: invalid format: %q", s)
41	}
42
43	secretURL, err := url.Parse(s)
44	if err != nil {
45		return nil, err
46	}
47
48	return &VaultReadQuery{
49		stopCh:      make(chan struct{}, 1),
50		sleepCh:     make(chan time.Duration, 1),
51		rawPath:     secretURL.Path,
52		queryValues: secretURL.Query(),
53	}, nil
54}
55
56// Fetch queries the Vault API
57func (d *VaultReadQuery) Fetch(clients *ClientSet, opts *QueryOptions,
58) (interface{}, *ResponseMetadata, error) {
59	select {
60	case <-d.stopCh:
61		return nil, nil, ErrStopped
62	default:
63	}
64	select {
65	case dur := <-d.sleepCh:
66		time.Sleep(dur)
67	default:
68	}
69
70	firstRun := d.secret == nil
71
72	if !firstRun && vaultSecretRenewable(d.secret) {
73		err := renewSecret(clients, d)
74		if err != nil {
75			return nil, nil, errors.Wrap(err, d.String())
76		}
77	}
78
79	err := d.fetchSecret(clients, opts)
80	if err != nil {
81		return nil, nil, errors.Wrap(err, d.String())
82	}
83
84	if !vaultSecretRenewable(d.secret) {
85		dur := leaseCheckWait(d.secret)
86		log.Printf("[TRACE] %s: non-renewable secret, set sleep for %s", d, dur)
87		d.sleepCh <- dur
88	}
89
90	return respWithMetadata(d.secret)
91}
92
93func (d *VaultReadQuery) fetchSecret(clients *ClientSet, opts *QueryOptions,
94) error {
95	opts = opts.Merge(&QueryOptions{})
96	vaultSecret, err := d.readSecret(clients, opts)
97	if err == nil {
98		printVaultWarnings(d, vaultSecret.Warnings)
99		d.vaultSecret = vaultSecret
100		// the cloned secret which will be exposed to the template
101		d.secret = transformSecret(vaultSecret)
102	}
103	return err
104}
105
106func (d *VaultReadQuery) stopChan() chan struct{} {
107	return d.stopCh
108}
109
110func (d *VaultReadQuery) secrets() (*Secret, *api.Secret) {
111	return d.secret, d.vaultSecret
112}
113
114// CanShare returns if this dependency is shareable.
115func (d *VaultReadQuery) CanShare() bool {
116	return false
117}
118
119// Stop halts the given dependency's fetch.
120func (d *VaultReadQuery) Stop() {
121	close(d.stopCh)
122}
123
124// String returns the human-friendly version of this dependency.
125func (d *VaultReadQuery) String() string {
126	if v := d.queryValues["version"]; len(v) > 0 {
127		return fmt.Sprintf("vault.read(%s.v%s)", d.rawPath, v[0])
128	}
129	return fmt.Sprintf("vault.read(%s)", d.rawPath)
130}
131
132// Type returns the type of this dependency.
133func (d *VaultReadQuery) Type() Type {
134	return TypeVault
135}
136
137func (d *VaultReadQuery) readSecret(clients *ClientSet, opts *QueryOptions) (*api.Secret, error) {
138	vaultClient := clients.Vault()
139
140	// Check whether this secret refers to a KV v2 entry if we haven't yet.
141	if d.isKVv2 == nil {
142		mountPath, isKVv2, err := isKVv2(vaultClient, d.rawPath)
143		if err != nil {
144			log.Printf("[WARN] %s: failed to check if %s is KVv2, "+
145				"assume not: %s", d, d.rawPath, err)
146			isKVv2 = false
147			d.secretPath = d.rawPath
148		} else if isKVv2 {
149			d.secretPath = shimKVv2Path(d.rawPath, mountPath)
150		} else {
151			d.secretPath = d.rawPath
152		}
153		d.isKVv2 = &isKVv2
154	}
155
156	queryString := d.queryValues.Encode()
157	log.Printf("[TRACE] %s: GET %s", d, &url.URL{
158		Path:     "/v1/" + d.secretPath,
159		RawQuery: queryString,
160	})
161	vaultSecret, err := vaultClient.Logical().ReadWithData(d.secretPath,
162		d.queryValues)
163
164	if err != nil {
165		return nil, errors.Wrap(err, d.String())
166	}
167	if vaultSecret == nil || deletedKVv2(vaultSecret) {
168		return nil, fmt.Errorf("no secret exists at %s", d.secretPath)
169	}
170	return vaultSecret, nil
171}
172
173func deletedKVv2(s *api.Secret) bool {
174	switch md := s.Data["metadata"].(type) {
175	case map[string]interface{}:
176		return md["deletion_time"] != ""
177	}
178	return false
179}
180
181// shimKVv2Path aligns the supported legacy path to KV v2 specs by inserting
182// /data/ into the path for reading secrets. Paths for metadata are not modified.
183func shimKVv2Path(rawPath, mountPath string) string {
184	switch {
185	case rawPath == mountPath, rawPath == strings.TrimSuffix(mountPath, "/"):
186		return path.Join(mountPath, "data")
187	default:
188		p := strings.TrimPrefix(rawPath, mountPath)
189
190		// Only add /data/ prefix to the path if neither /data/ or /metadata/ are
191		// present.
192		if strings.HasPrefix(p, "data/") || strings.HasPrefix(p, "metadata/") {
193			return rawPath
194		}
195		return path.Join(mountPath, "data", p)
196	}
197}
198