-
-
Notifications
You must be signed in to change notification settings - Fork 764
/
client_get.go
223 lines (190 loc) · 7.06 KB
/
client_get.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package conjur
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/cyberark/conjur-api-go/conjurapi"
"github.com/tidwall/gjson"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/pkg/find"
)
type conjurResource map[string]interface{}
// resourceFilterFunc is a function that filters resources.
// It takes a resource as input and returns the name of the resource if it should be included.
// If the resource should not be included, it returns an empty string.
// If an error occurs, it returns an empty string and the error.
type resourceFilterFunc func(candidate conjurResource) (name string, err error)
// GetSecret returns a single secret from the provider.
func (c *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
conjurClient, getConjurClientError := c.GetConjurClient(ctx)
if getConjurClientError != nil {
return nil, getConjurClientError
}
secretValue, err := conjurClient.RetrieveSecret(ref.Key)
if err != nil {
return nil, err
}
// If no property is specified, return the secret value as is
if ref.Property == "" {
return secretValue, nil
}
// If a property is specified, parse the secret value as JSON and return the property value
val := gjson.Get(string(secretValue), ref.Property)
if !val.Exists() {
return nil, fmt.Errorf(errSecretKeyFmt, ref.Property)
}
return []byte(val.String()), nil
}
// GetSecretMap returns multiple k/v pairs from the provider.
func (c *Client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
// Gets a secret as normal, expecting secret value to be a json object
data, err := c.GetSecret(ctx, ref)
if err != nil {
return nil, fmt.Errorf("error getting secret %s: %w", ref.Key, err)
}
// Maps the json data to a string:string map
kv := make(map[string]string)
err = json.Unmarshal(data, &kv)
if err != nil {
return nil, fmt.Errorf("unable to unmarshal secret %s: %w", ref.Key, err)
}
// Converts values in K:V pairs into bytes, while leaving keys as strings
secretData := make(map[string][]byte)
for k, v := range kv {
secretData[k] = []byte(v)
}
return secretData, nil
}
// GetAllSecrets gets multiple secrets from the provider and loads into a kubernetes secret.
// First load all secrets from secretStore path configuration
// Then, gets secrets from a matching name or matching custom_metadata.
func (c *Client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
if ref.Name != nil {
return c.findSecretsFromName(ctx, *ref.Name)
}
return c.findSecretsFromTags(ctx, ref.Tags)
}
func (c *Client) findSecretsFromName(ctx context.Context, ref esv1beta1.FindName) (map[string][]byte, error) {
matcher, err := find.New(ref)
if err != nil {
return nil, err
}
var resourceFilterFunc = func(candidate conjurResource) (string, error) {
name := trimConjurResourceName(candidate["id"].(string))
isMatch := matcher.MatchName(name)
if !isMatch {
return "", nil
}
return name, nil
}
return c.listSecrets(ctx, resourceFilterFunc)
}
func (c *Client) findSecretsFromTags(ctx context.Context, tags map[string]string) (map[string][]byte, error) {
var resourceFilterFunc = func(candidate conjurResource) (string, error) {
name := trimConjurResourceName(candidate["id"].(string))
annotations, ok := candidate["annotations"].([]interface{})
if !ok {
// No annotations, skip
return "", nil
}
formattedAnnotations, err := formatAnnotations(annotations)
if err != nil {
return "", err
}
// Check if all tags match
for tk, tv := range tags {
p, ok := formattedAnnotations[tk]
if !ok || p != tv {
return "", nil
}
}
return name, nil
}
return c.listSecrets(ctx, resourceFilterFunc)
}
func (c *Client) listSecrets(ctx context.Context, filterFunc resourceFilterFunc) (map[string][]byte, error) {
conjurClient, getConjurClientError := c.GetConjurClient(ctx)
if getConjurClientError != nil {
return nil, getConjurClientError
}
filteredResourceNames := []string{}
// Loop through all secrets in the Conjur account.
// Ideally this will be only a small list, but we need to handle pagination in the
// case that there are a lot of secrets. To limit load on Conjur and memory usage
// in ESO, we will only load 100 secrets at a time. We will then filter these secrets,
// discarding any that do not match the filterFunc. We will then repeat this process
// until we have loaded all secrets.
for offset := 0; ; offset += 100 {
resFilter := &conjurapi.ResourceFilter{
Kind: "variable",
Limit: 100,
Offset: offset,
}
resources, err := conjurClient.Resources(resFilter)
if err != nil {
return nil, err
}
for _, candidate := range resources {
name, err := filterFunc(candidate)
if err != nil {
return nil, err
}
if name != "" {
filteredResourceNames = append(filteredResourceNames, name)
}
}
// If we have less than 100 resources, we reached the last page
if len(resources) < 100 {
break
}
}
filteredResources, err := c.client.RetrieveBatchSecrets(filteredResourceNames)
if err != nil {
return nil, err
}
// Trim the resource names to just the last part of the ID
return trimConjurResourceNames(filteredResources), nil
}
// trimConjurResourceNames trims the Conjur resource names to the last part of the ID.
// It iterates over a map of secrets and returns a new map with the trimmed names.
func trimConjurResourceNames(resources map[string][]byte) map[string][]byte {
trimmedResources := make(map[string][]byte)
for k, v := range resources {
trimmedResources[trimConjurResourceName(k)] = v
}
return trimmedResources
}
// trimConjurResourceName trims the Conjur resource name to the last part of the ID.
// For example, if the ID is "account:variable:secret", the function will return
// "secret".
func trimConjurResourceName(id string) string {
tokens := strings.SplitN(id, ":", 3)
return tokens[len(tokens)-1]
}
// Convert annotations from objects with "name", "policy", "value" keys (as returned by the Conjur API)
// to a key/value map for easier comparison in code.
func formatAnnotations(annotations []interface{}) (map[string]string, error) {
formattedAnnotations := make(map[string]string)
for _, annot := range annotations {
annot, ok := annot.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("could not parse annotation: %v", annot)
}
name := annot["name"].(string)
value := annot["value"].(string)
formattedAnnotations[name] = value
}
return formattedAnnotations, nil
}