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