| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225 |
- /*
- Copyright © The ESO Authors
- 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
- https://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"
- esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
- "github.com/external-secrets/external-secrets/runtime/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 esv1.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 esv1.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 esv1.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 esv1.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
- }
|