github.com/crossplane/upjet@v1.3.0/pkg/resource/sensitive.go (about)

     1  // SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io>
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package resource
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"regexp"
    11  	"strings"
    12  
    13  	v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
    14  	"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
    15  	"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
    16  	"github.com/crossplane/crossplane-runtime/pkg/resource"
    17  	"github.com/pkg/errors"
    18  	kerrors "k8s.io/apimachinery/pkg/api/errors"
    19  	"k8s.io/apimachinery/pkg/runtime"
    20  
    21  	"github.com/crossplane/upjet/pkg/config"
    22  )
    23  
    24  const (
    25  	errCannotExpandWildcards               = "cannot expand wildcards"
    26  	errFmtCannotGetValueForFieldPath       = "cannot not get a value for fieldpath %q"
    27  	errFmtCannotGetStringForFieldPath      = "cannot not get a string for fieldpath %q"
    28  	errFmtCannotGetSecretKeySelector       = "cannot get SecretKeySelector from xp resource for fieldpath %q"
    29  	errFmtCannotGetSecretKeySelectorAsList = "cannot get SecretKeySelector list from xp resource for fieldpath %q"
    30  	errFmtCannotGetSecretKeySelectorAsMap  = "cannot get SecretKeySelector map from xp resource for fieldpath %q"
    31  	errFmtCannotGetSecretValue             = "cannot get secret value for %v"
    32  )
    33  
    34  const (
    35  	// prefixAttribute used to prefix connection detail keys for sensitive
    36  	// Terraform attributes. We need this prefix to ensure that they are not
    37  	// overridden by any custom connection key configured which would break
    38  	// our ability to build tfstate back.
    39  	prefixAttribute = "attribute."
    40  
    41  	pluralSuffix = "s"
    42  
    43  	errGetAdditionalConnectionDetails = "cannot get additional connection details"
    44  	errFmtCannotOverrideExistingKey   = "overriding a reserved connection key (%q) is not allowed"
    45  )
    46  
    47  var reEndsWithIndex *regexp.Regexp
    48  var reMiddleIndex *regexp.Regexp
    49  var reInsideThreeDotsBlock *regexp.Regexp
    50  
    51  func init() {
    52  	reEndsWithIndex = regexp.MustCompile(`\.(\d+?)$`)
    53  	reMiddleIndex = regexp.MustCompile(`\.(\d+?)\.`)
    54  	reInsideThreeDotsBlock = regexp.MustCompile(`\.\.\.(.*?)\.\.\.`)
    55  }
    56  
    57  // SecretClient is the client to get sensitive data from kubernetes secrets
    58  //
    59  //go:generate go run github.com/golang/mock/mockgen -copyright_file ../../hack/boilerplate.txt -destination ./fake/mocks/mock.go -package mocks github.com/crossplane/upjet/pkg/resource SecretClient
    60  type SecretClient interface {
    61  	GetSecretData(ctx context.Context, ref *v1.SecretReference) (map[string][]byte, error)
    62  	GetSecretValue(ctx context.Context, sel v1.SecretKeySelector) ([]byte, error)
    63  }
    64  
    65  // GetConnectionDetails returns connection details including the sensitive
    66  // Terraform attributes and additions connection details configured.
    67  func GetConnectionDetails(attr map[string]any, tr Terraformed, cfg *config.Resource) (managed.ConnectionDetails, error) {
    68  	conn, err := GetSensitiveAttributes(attr, tr.GetConnectionDetailsMapping())
    69  	if err != nil {
    70  		return nil, errors.Wrap(err, "cannot get connection details")
    71  	}
    72  
    73  	add, err := cfg.Sensitive.AdditionalConnectionDetailsFn(attr)
    74  	if err != nil {
    75  		return nil, errors.Wrap(err, errGetAdditionalConnectionDetails)
    76  	}
    77  	for k, v := range add {
    78  		if _, ok := conn[k]; ok {
    79  			// We return error if a custom key tries to override an existing
    80  			// connection key. This is because we use connection keys to rebuild
    81  			// the tfstate, i.e. otherwise we would lose the original value in
    82  			// tfstate.
    83  			// Indeed, we are prepending "attribute_" to the Terraform
    84  			// state sensitive keys and connection keys starting with this
    85  			// prefix are reserved and should not be used as a custom connection
    86  			// key.
    87  			return nil, errors.Errorf(errFmtCannotOverrideExistingKey, k)
    88  		}
    89  		if conn == nil {
    90  			conn = map[string][]byte{}
    91  		}
    92  		conn[k] = v
    93  	}
    94  
    95  	return conn, nil
    96  }
    97  
    98  // GetSensitiveAttributes returns strings matching provided field paths in the
    99  // input data.
   100  // See the unit tests for examples.
   101  func GetSensitiveAttributes(from map[string]any, mapping map[string]string) (map[string][]byte, error) { //nolint: gocyclo
   102  	if len(mapping) == 0 {
   103  		return nil, nil
   104  	}
   105  	paved := fieldpath.Pave(from)
   106  	var vals map[string][]byte
   107  	for tf := range mapping {
   108  		fieldPaths, err := paved.ExpandWildcards(tf)
   109  		if err != nil {
   110  			if fieldpath.IsNotFound(err) {
   111  				continue
   112  			}
   113  			return nil, errors.Wrap(err, errCannotExpandWildcards)
   114  		}
   115  
   116  		for _, fp := range fieldPaths {
   117  			v, err := paved.GetValue(fp)
   118  			if err != nil {
   119  				return nil, errors.Wrapf(err, errFmtCannotGetValueForFieldPath, fp)
   120  			}
   121  			// Gracefully skip if v is nil which implies that this field is
   122  			// optional and not provided.
   123  			if v == nil {
   124  				continue
   125  			}
   126  
   127  			// Note(turkenh): k8s secrets uses a strict regex to validate secret
   128  			// keys which does not allow having brackets inside. So, we need to
   129  			// do a conversion to be able to store as connection secret keys.
   130  			// See https://github.com/crossplane/upjet/pull/94 for
   131  			// more details.
   132  			k, err := fieldPathToSecretKey(fp)
   133  			if err != nil {
   134  				return nil, errors.Wrapf(err, "cannot convert fieldpath %q to secret key", fp)
   135  			}
   136  			if vals == nil {
   137  				vals = map[string][]byte{}
   138  			}
   139  			switch s := v.(type) {
   140  			case map[string]any:
   141  				for i, e := range s {
   142  					if err := setSensitiveAttributesToValuesMap(e, i, k, fp, vals); err != nil {
   143  						return nil, err
   144  					}
   145  				}
   146  			case []any:
   147  				for i, e := range s {
   148  					if err := setSensitiveAttributesToValuesMap(e, i, k, fp, vals); err != nil {
   149  						return nil, err
   150  					}
   151  				}
   152  			case string:
   153  				vals[fmt.Sprintf("%s%s", prefixAttribute, k)] = []byte(s)
   154  			default:
   155  				return nil, errors.Errorf(errFmtCannotGetStringForFieldPath, fp)
   156  			}
   157  		}
   158  	}
   159  	return vals, nil
   160  }
   161  
   162  // GetSensitiveParameters will collect sensitive information as terraform state
   163  // attributes by following secret references in the spec.
   164  func GetSensitiveParameters(ctx context.Context, client SecretClient, from runtime.Object, into map[string]any, mapping map[string]string) error { //nolint: gocyclo
   165  	// Note(turkenh): Cyclomatic complexity of this function is slightly higher
   166  	// than the threshold but preferred to use nolint directive for better
   167  	// readability and not to split the logic.
   168  
   169  	if len(mapping) == 0 {
   170  		return nil
   171  	}
   172  
   173  	pavedJSON, err := fieldpath.PaveObject(from)
   174  	if err != nil {
   175  		return err
   176  	}
   177  	pavedTF := fieldpath.Pave(into)
   178  
   179  	var sensitive []byte
   180  	for tfPath, jsonPath := range mapping {
   181  		jsonPathSet, err := pavedJSON.ExpandWildcards(jsonPath)
   182  		if err != nil {
   183  			return errors.Wrapf(err, "cannot expand wildcard for xp resource")
   184  		}
   185  		for _, expandedJSONPath := range jsonPathSet {
   186  			v, err := pavedJSON.GetValue(expandedJSONPath)
   187  			if err != nil {
   188  				return errors.Wrapf(err, errFmtCannotGetValueForFieldPath, expandedJSONPath)
   189  			}
   190  			// ExpandWildcards call above already skips "nested" optional fields
   191  			// as they won't be available in the data but added this as an
   192  			// additional check here. Please note, here all path starts with
   193  			// spec.forProvider., so, all is "nested" different from GetAttributes
   194  			if v == nil {
   195  				continue
   196  			}
   197  
   198  			switch k := v.(type) {
   199  			case map[string]any:
   200  				_, ok := k["key"]
   201  				if !ok {
   202  					// This is a special case where we have a "SecretReference" without a selected "key". This happens
   203  					// when there is an input field of type map[string]string (or map[string]*string).
   204  					// In this case, we need to get the entire secret data and fill it in the terraform state as a map.
   205  					// This is the only case where we have one-to-many mapping between json and tf paths.
   206  					ref := &v1.SecretReference{}
   207  					if err = pavedJSON.GetValueInto(expandedJSONPath, ref); err != nil {
   208  						return errors.Wrapf(err, errFmtCannotGetSecretKeySelectorAsMap, expandedJSONPath)
   209  					}
   210  					data, err := client.GetSecretData(ctx, ref)
   211  					// We don't want to fail if the secret is not found. Otherwise, we won't be able to delete the
   212  					// resource if secret is deleted before. This is quite expected when both secret and resource
   213  					// got deleted in parallel.
   214  					if resource.IgnoreNotFound(err) != nil {
   215  						return errors.Wrapf(err, errFmtCannotGetSecretValue, ref)
   216  					}
   217  					for key, value := range data {
   218  						if err = pavedTF.SetValue(fmt.Sprintf("%s.%s", tfPath, key), string(value)); err != nil {
   219  							return errors.Wrapf(err, "cannot set string as terraform attribute for fieldpath %q", fmt.Sprintf("%s.%s", tfPath, key))
   220  						}
   221  					}
   222  					continue
   223  				}
   224  
   225  				sel := &v1.SecretKeySelector{}
   226  				if err = pavedJSON.GetValueInto(expandedJSONPath, sel); err != nil {
   227  					return errors.Wrapf(err, errFmtCannotGetSecretKeySelector, expandedJSONPath)
   228  				}
   229  				sensitive, err = client.GetSecretValue(ctx, *sel)
   230  				if resource.IgnoreNotFound(err) != nil {
   231  					return errors.Wrapf(err, errFmtCannotGetSecretValue, sel)
   232  				}
   233  				if err := setSensitiveParametersWithPaved(pavedTF, expandedJSONPath, tfPath, mapping, string(sensitive)); err != nil {
   234  					return err
   235  				}
   236  			case []any:
   237  				sel := &[]v1.SecretKeySelector{}
   238  				if err = pavedJSON.GetValueInto(expandedJSONPath, sel); err != nil {
   239  					return errors.Wrapf(err, errFmtCannotGetSecretKeySelectorAsList, expandedJSONPath)
   240  				}
   241  				var sensitives []any
   242  				for _, s := range *sel {
   243  					sensitive, err = client.GetSecretValue(ctx, s)
   244  					if resource.IgnoreNotFound(err) != nil {
   245  						return errors.Wrapf(err, errFmtCannotGetSecretValue, sel)
   246  					}
   247  
   248  					// If referenced k8s secret is deleted before the MR, we pass empty string for the sensitive
   249  					// field to be able to destroy the resource.
   250  					if kerrors.IsNotFound(err) {
   251  						sensitive = []byte("")
   252  					}
   253  					sensitives = append(sensitives, string(sensitive))
   254  				}
   255  				if err := setSensitiveParametersWithPaved(pavedTF, expandedJSONPath, tfPath, mapping, sensitives); err != nil {
   256  					return err
   257  				}
   258  			default:
   259  				return errors.Wrapf(err, errFmtCannotGetSecretKeySelector, expandedJSONPath)
   260  			}
   261  		}
   262  	}
   263  
   264  	return nil
   265  }
   266  
   267  // GetSensitiveObservation will return sensitive information as terraform state
   268  // attributes by reading them from connection details.
   269  func GetSensitiveObservation(ctx context.Context, client SecretClient, from *v1.SecretReference, into map[string]any) error {
   270  	if from == nil {
   271  		// No secret reference set
   272  		return nil
   273  	}
   274  	conn, err := client.GetSecretData(ctx, from)
   275  	if kerrors.IsNotFound(err) {
   276  		// Secret not available/created yet
   277  		return nil
   278  	}
   279  	if err != nil {
   280  		return errors.Wrapf(err, "cannot get connection secret")
   281  	}
   282  
   283  	paveTF := fieldpath.Pave(into)
   284  	for k, v := range conn {
   285  		if !strings.HasPrefix(k, prefixAttribute) {
   286  			// this is not an attribute key (e.g. custom key), we don't put it
   287  			// into tfstate attributes.
   288  			continue
   289  		}
   290  		fp, err := secretKeyToFieldPath(strings.TrimPrefix(k, prefixAttribute))
   291  		if err != nil {
   292  			return errors.Wrapf(err, "cannot convert secret key %q to fieldpath", k)
   293  		}
   294  		if err = paveTF.SetString(fp, string(v)); err != nil {
   295  			return errors.Wrapf(err, "cannot set sensitive string in tf attributes for fieldpath %q", fp)
   296  		}
   297  	}
   298  	return nil
   299  }
   300  
   301  func expandedTFPath(expandedXP string, mapping map[string]string) (string, error) {
   302  	sExp, err := fieldpath.Parse(normalizeJSONPath(expandedXP))
   303  	if err != nil {
   304  		return "", err
   305  	}
   306  	tfWildcard := ""
   307  	for tf, xp := range mapping {
   308  		sxp, err := fieldpath.Parse(normalizeJSONPath(xp))
   309  		if err != nil {
   310  			return "", err
   311  		}
   312  		if expandedFor(sExp, sxp) {
   313  			tfWildcard = tf
   314  			break
   315  		}
   316  	}
   317  	if tfWildcard == "" {
   318  		return "", errors.Errorf("cannot find corresponding fieldpath mapping for %q", expandedXP)
   319  	}
   320  	sTF, err := fieldpath.Parse(tfWildcard)
   321  	if err != nil {
   322  		return "", err
   323  	}
   324  	for i, s := range sTF {
   325  		if s.Field == "*" {
   326  			sTF[i] = sExp[i]
   327  		}
   328  	}
   329  
   330  	return sTF.String(), nil
   331  }
   332  
   333  func expandedFor(expanded fieldpath.Segments, withWildcard fieldpath.Segments) bool {
   334  	if len(withWildcard) != len(expanded) {
   335  		return false
   336  	}
   337  	for i, w := range withWildcard {
   338  		exp := expanded[i]
   339  		if w.Field == "*" {
   340  			continue
   341  		}
   342  		if w.Type != exp.Type {
   343  			return false
   344  		}
   345  		if w.Field != exp.Field {
   346  			return false
   347  		}
   348  		if w.Index != exp.Index {
   349  			return false
   350  		}
   351  	}
   352  	return true
   353  }
   354  
   355  func normalizeJSONPath(s string) string {
   356  	return strings.TrimPrefix(strings.TrimPrefix(s, "spec.forProvider."), "status.atProvider.")
   357  }
   358  
   359  func secretKeyToFieldPath(s string) (string, error) {
   360  	s1 := reInsideThreeDotsBlock.ReplaceAllString(s, "[$1]")
   361  	s2 := reEndsWithIndex.ReplaceAllString(s1, "[$1]")
   362  	s3 := reMiddleIndex.ReplaceAllString(s2, "[$1].")
   363  	seg, err := fieldpath.Parse(s3)
   364  	if err != nil {
   365  		return "", errors.Wrapf(err, "cannot parse secret key %q as fieldpath", s3)
   366  	}
   367  	return seg.String(), nil
   368  }
   369  
   370  func fieldPathToSecretKey(s string) (string, error) {
   371  	sg, err := fieldpath.Parse(s)
   372  	if err != nil {
   373  		return "", errors.Wrapf(err, "cannot parse %q as fieldpath", s)
   374  	}
   375  
   376  	var b strings.Builder
   377  	for _, s := range sg {
   378  		switch s.Type {
   379  		case fieldpath.SegmentField:
   380  			if strings.ContainsRune(s.Field, '.') {
   381  				b.WriteString(fmt.Sprintf("...%s...", s.Field))
   382  				continue
   383  			}
   384  			b.WriteString(fmt.Sprintf(".%s", s.Field))
   385  		case fieldpath.SegmentIndex:
   386  			b.WriteString(fmt.Sprintf(".%d", s.Index))
   387  		}
   388  	}
   389  
   390  	return strings.TrimPrefix(b.String(), "."), nil
   391  }
   392  
   393  func setSensitiveParametersWithPaved(pavedTF *fieldpath.Paved, expandedJSONPath, tfPath string, mapping map[string]string, sensitives any) error {
   394  	expTF, err := expandedTFPath(expandedJSONPath, mapping)
   395  	if err != nil {
   396  		return err
   397  	}
   398  	if err = pavedTF.SetValue(expTF, sensitives); err != nil {
   399  		return errors.Wrapf(err, "cannot set string as terraform attribute for fieldpath %q", tfPath)
   400  	}
   401  	return nil
   402  }
   403  
   404  func setSensitiveAttributesToValuesMap(e, i any, k, fp string, vals map[string][]byte) error {
   405  	k = strings.TrimSuffix(k, pluralSuffix)
   406  	value, ok := e.(string)
   407  	if !ok {
   408  		return errors.Errorf(errFmtCannotGetStringForFieldPath, fp)
   409  	}
   410  	vals[fmt.Sprintf("%s%s.%v", prefixAttribute, k, i)] = []byte(value)
   411  	return nil
   412  }