mirror of
https://github.com/external-secrets/external-secrets.git
synced 2024-12-15 17:51:01 +00:00
223 lines
7.1 KiB
Go
223 lines
7.1 KiB
Go
/*
|
|
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
|
|
}
|