Browse Source

✨Implements dataFrom key rewrite (#1381)

* Implements dataFrom key rewrite

Co-authored-by: Moritz Johner <moolen@users.noreply.github.com>
Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com>

* docs: add example to remove invalid characters

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>

Co-authored-by: Moritz Johner <moolen@users.noreply.github.com>
Co-authored-by: Moritz Johner <beller.moritz@googlemail.com>
Gustavo Fernandes de Carvalho 3 years ago
parent
commit
b4e7acfaa9
30 changed files with 949 additions and 33 deletions
  1. 21 4
      apis/externalsecrets/v1beta1/externalsecret_types.go
  2. 42 0
      apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
  3. 29 5
      config/crds/bases/external-secrets.io_clusterexternalsecrets.yaml
  4. 28 5
      config/crds/bases/external-secrets.io_externalsecrets.yaml
  5. 44 10
      deploy/crds/bundle.yaml
  6. 105 0
      docs/guides-datafrom-rewrite.md
  7. 7 2
      docs/guides-getallsecrets.md
  8. 24 0
      docs/snippets/datafrom-rewrite-conflict.yaml
  9. 18 0
      docs/snippets/datafrom-rewrite-invalid-characters.yaml
  10. 20 0
      docs/snippets/datafrom-rewrite-remove-path.yaml
  11. 14 0
      docs/snippets/full-external-secret.yaml
  12. 86 0
      docs/spec.md
  13. 1 0
      e2e/suites/provider/cases/akeyless/akeyless.go
  14. 1 0
      e2e/suites/provider/cases/alibaba/alibaba.go
  15. 1 0
      e2e/suites/provider/cases/aws/parameterstore/parameterstore.go
  16. 1 0
      e2e/suites/provider/cases/aws/secretsmanager/secretsmanager.go
  17. 1 0
      e2e/suites/provider/cases/azure/azure_secret.go
  18. 37 0
      e2e/suites/provider/cases/common/common.go
  19. 50 2
      e2e/suites/provider/cases/common/find_by_name.go
  20. 2 0
      e2e/suites/provider/cases/gcp/gcp.go
  21. 1 0
      e2e/suites/provider/cases/gitlab/gitlab.go
  22. 1 0
      e2e/suites/provider/cases/kubernetes/kubernetes.go
  23. 1 0
      e2e/suites/provider/cases/oracle/oracle.go
  24. 12 0
      e2e/suites/provider/cases/vault/vault.go
  25. 1 0
      hack/api-docs/mkdocs.yml
  26. 26 5
      pkg/controllers/externalsecret/externalsecret_controller.go
  27. 168 0
      pkg/controllers/externalsecret/externalsecret_controller_test.go
  28. 3 0
      pkg/provider/vault/vault.go
  29. 46 0
      pkg/utils/utils.go
  30. 158 0
      pkg/utils/utils_test.go

+ 21 - 4
apis/externalsecrets/v1beta1/externalsecret_types.go

@@ -184,7 +184,7 @@ type ExternalSecretDataRemoteRef struct {
 	ConversionStrategy ExternalSecretConversionStrategy `json:"conversionStrategy,omitempty"`
 
 	// +optional
-	// Used to define a conversion Strategy
+	// Used to define a decoding Strategy
 	// +kubebuilder:default="None"
 	DecodingStrategy ExternalSecretDecodingStrategy `json:"decodingStrategy,omitempty"`
 }
@@ -212,8 +212,6 @@ const (
 	ExternalSecretDecodeNone      ExternalSecretDecodingStrategy = "None"
 )
 
-// +kubebuilder:validation:MinProperties=1
-// +kubebuilder:validation:MaxProperties=1
 type ExternalSecretDataFromRemoteRef struct {
 	// Used to extract multiple key/value pairs from one secret
 	// +optional
@@ -221,8 +219,26 @@ type ExternalSecretDataFromRemoteRef struct {
 	// Used to find secrets based on tags or regular expressions
 	// +optional
 	Find *ExternalSecretFind `json:"find,omitempty"`
+
+	// Used to rewrite secret Keys after getting them from the secret Provider
+	// Multiple Rewrite operations can be provided. They are applied in a layered order (first to last)
+	// +optional
+	Rewrite []ExternalSecretRewrite `json:"rewrite,omitempty"`
 }
 
+type ExternalSecretRewrite struct {
+	// Used to rewrite with regular expressions.
+	// The resulting key will be the output of a regexp.ReplaceAll operation.
+	// +optional
+	Regexp *ExternalSecretRewriteRegexp `json:"regexp,omitempty"`
+}
+
+type ExternalSecretRewriteRegexp struct {
+	// Used to define the regular expression of a re.Compiler.
+	Source string `json:"source"`
+	// Used to define the target pattern of a ReplaceAll operation.
+	Target string `json:"target"`
+}
 type ExternalSecretFind struct {
 	// A root path to start the find operations.
 	// +optional
@@ -241,7 +257,7 @@ type ExternalSecretFind struct {
 	ConversionStrategy ExternalSecretConversionStrategy `json:"conversionStrategy,omitempty"`
 
 	// +optional
-	// Used to define a conversion Strategy
+	// Used to define a decoding Strategy
 	// +kubebuilder:default="None"
 	DecodingStrategy ExternalSecretDecodingStrategy `json:"decodingStrategy,omitempty"`
 }
@@ -307,6 +323,7 @@ const (
 	ReasonUnavailableStore     = "UnavailableStore"
 	ReasonProviderClientConfig = "InvalidProviderClientConfig"
 	ReasonUpdateFailed         = "UpdateFailed"
+	ReasonDeprecated           = "ParameterDeprecated"
 	ReasonUpdated              = "Updated"
 	ReasonDeleted              = "Deleted"
 )

+ 42 - 0
apis/externalsecrets/v1beta1/zz_generated.deepcopy.go

@@ -578,6 +578,13 @@ func (in *ExternalSecretDataFromRemoteRef) DeepCopyInto(out *ExternalSecretDataF
 		*out = new(ExternalSecretFind)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.Rewrite != nil {
+		in, out := &in.Rewrite, &out.Rewrite
+		*out = make([]ExternalSecretRewrite, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSecretDataFromRemoteRef.
@@ -670,6 +677,41 @@ func (in *ExternalSecretList) DeepCopyObject() runtime.Object {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ExternalSecretRewrite) DeepCopyInto(out *ExternalSecretRewrite) {
+	*out = *in
+	if in.Regexp != nil {
+		in, out := &in.Regexp, &out.Regexp
+		*out = new(ExternalSecretRewriteRegexp)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSecretRewrite.
+func (in *ExternalSecretRewrite) DeepCopy() *ExternalSecretRewrite {
+	if in == nil {
+		return nil
+	}
+	out := new(ExternalSecretRewrite)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ExternalSecretRewriteRegexp) DeepCopyInto(out *ExternalSecretRewriteRegexp) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSecretRewriteRegexp.
+func (in *ExternalSecretRewriteRegexp) DeepCopy() *ExternalSecretRewriteRegexp {
+	if in == nil {
+		return nil
+	}
+	out := new(ExternalSecretRewriteRegexp)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ExternalSecretSpec) DeepCopyInto(out *ExternalSecretSpec) {
 	*out = *in
 	out.SecretStoreRef = in.SecretStoreRef

+ 29 - 5
config/crds/bases/external-secrets.io_clusterexternalsecrets.yaml

@@ -77,7 +77,7 @@ spec:
                               type: string
                             decodingStrategy:
                               default: None
-                              description: Used to define a conversion Strategy
+                              description: Used to define a decoding Strategy
                               type: string
                             key:
                               description: Key is the key used in the Provider, mandatory
@@ -110,8 +110,6 @@ spec:
                       Provider data If multiple entries are specified, the Secret
                       keys are merged in the specified order
                     items:
-                      maxProperties: 1
-                      minProperties: 1
                       properties:
                         extract:
                           description: Used to extract multiple key/value pairs from
@@ -123,7 +121,7 @@ spec:
                               type: string
                             decodingStrategy:
                               default: None
-                              description: Used to define a conversion Strategy
+                              description: Used to define a decoding Strategy
                               type: string
                             key:
                               description: Key is the key used in the Provider, mandatory
@@ -154,7 +152,7 @@ spec:
                               type: string
                             decodingStrategy:
                               default: None
-                              description: Used to define a conversion Strategy
+                              description: Used to define a decoding Strategy
                               type: string
                             name:
                               description: Finds secrets based on the name.
@@ -172,6 +170,32 @@ spec:
                               description: Find secrets based on tags.
                               type: object
                           type: object
+                        rewrite:
+                          description: Used to rewrite secret Keys after getting them
+                            from the secret Provider Multiple Rewrite operations can
+                            be provided. They are applied in a layered order (first
+                            to last)
+                          items:
+                            properties:
+                              regexp:
+                                description: Used to rewrite with regular expressions.
+                                  The resulting key will be the output of a regexp.ReplaceAll
+                                  operation.
+                                properties:
+                                  source:
+                                    description: Used to define the regular expression
+                                      of a re.Compiler.
+                                    type: string
+                                  target:
+                                    description: Used to define the target pattern
+                                      of a ReplaceAll operation.
+                                    type: string
+                                required:
+                                - source
+                                - target
+                                type: object
+                            type: object
+                          type: array
                       type: object
                     type: array
                   refreshInterval:

+ 28 - 5
config/crds/bases/external-secrets.io_externalsecrets.yaml

@@ -312,7 +312,7 @@ spec:
                           type: string
                         decodingStrategy:
                           default: None
-                          description: Used to define a conversion Strategy
+                          description: Used to define a decoding Strategy
                           type: string
                         key:
                           description: Key is the key used in the Provider, mandatory
@@ -345,8 +345,6 @@ spec:
                   Provider data If multiple entries are specified, the Secret keys
                   are merged in the specified order
                 items:
-                  maxProperties: 1
-                  minProperties: 1
                   properties:
                     extract:
                       description: Used to extract multiple key/value pairs from one
@@ -358,7 +356,7 @@ spec:
                           type: string
                         decodingStrategy:
                           default: None
-                          description: Used to define a conversion Strategy
+                          description: Used to define a decoding Strategy
                           type: string
                         key:
                           description: Key is the key used in the Provider, mandatory
@@ -388,7 +386,7 @@ spec:
                           type: string
                         decodingStrategy:
                           default: None
-                          description: Used to define a conversion Strategy
+                          description: Used to define a decoding Strategy
                           type: string
                         name:
                           description: Finds secrets based on the name.
@@ -406,6 +404,31 @@ spec:
                           description: Find secrets based on tags.
                           type: object
                       type: object
+                    rewrite:
+                      description: Used to rewrite secret Keys after getting them
+                        from the secret Provider Multiple Rewrite operations can be
+                        provided. They are applied in a layered order (first to last)
+                      items:
+                        properties:
+                          regexp:
+                            description: Used to rewrite with regular expressions.
+                              The resulting key will be the output of a regexp.ReplaceAll
+                              operation.
+                            properties:
+                              source:
+                                description: Used to define the regular expression
+                                  of a re.Compiler.
+                                type: string
+                              target:
+                                description: Used to define the target pattern of
+                                  a ReplaceAll operation.
+                                type: string
+                            required:
+                            - source
+                            - target
+                            type: object
+                        type: object
+                      type: array
                   type: object
                 type: array
               refreshInterval:

+ 44 - 10
deploy/crds/bundle.yaml

@@ -67,7 +67,7 @@ spec:
                                 type: string
                               decodingStrategy:
                                 default: None
-                                description: Used to define a conversion Strategy
+                                description: Used to define a decoding Strategy
                                 type: string
                               key:
                                 description: Key is the key used in the Provider, mandatory
@@ -94,8 +94,6 @@ spec:
                     dataFrom:
                       description: DataFrom is used to fetch all properties from a specific Provider data If multiple entries are specified, the Secret keys are merged in the specified order
                       items:
-                        maxProperties: 1
-                        minProperties: 1
                         properties:
                           extract:
                             description: Used to extract multiple key/value pairs from one secret
@@ -106,7 +104,7 @@ spec:
                                 type: string
                               decodingStrategy:
                                 default: None
-                                description: Used to define a conversion Strategy
+                                description: Used to define a decoding Strategy
                                 type: string
                               key:
                                 description: Key is the key used in the Provider, mandatory
@@ -132,7 +130,7 @@ spec:
                                 type: string
                               decodingStrategy:
                                 default: None
-                                description: Used to define a conversion Strategy
+                                description: Used to define a decoding Strategy
                                 type: string
                               name:
                                 description: Finds secrets based on the name.
@@ -150,6 +148,25 @@ spec:
                                 description: Find secrets based on tags.
                                 type: object
                             type: object
+                          rewrite:
+                            description: Used to rewrite secret Keys after getting them from the secret Provider Multiple Rewrite operations can be provided. They are applied in a layered order (first to last)
+                            items:
+                              properties:
+                                regexp:
+                                  description: Used to rewrite with regular expressions. The resulting key will be the output of a regexp.ReplaceAll operation.
+                                  properties:
+                                    source:
+                                      description: Used to define the regular expression of a re.Compiler.
+                                      type: string
+                                    target:
+                                      description: Used to define the target pattern of a ReplaceAll operation.
+                                      type: string
+                                  required:
+                                    - source
+                                    - target
+                                  type: object
+                              type: object
+                            type: array
                         type: object
                       type: array
                     refreshInterval:
@@ -2813,7 +2830,7 @@ spec:
                             type: string
                           decodingStrategy:
                             default: None
-                            description: Used to define a conversion Strategy
+                            description: Used to define a decoding Strategy
                             type: string
                           key:
                             description: Key is the key used in the Provider, mandatory
@@ -2840,8 +2857,6 @@ spec:
                 dataFrom:
                   description: DataFrom is used to fetch all properties from a specific Provider data If multiple entries are specified, the Secret keys are merged in the specified order
                   items:
-                    maxProperties: 1
-                    minProperties: 1
                     properties:
                       extract:
                         description: Used to extract multiple key/value pairs from one secret
@@ -2852,7 +2867,7 @@ spec:
                             type: string
                           decodingStrategy:
                             default: None
-                            description: Used to define a conversion Strategy
+                            description: Used to define a decoding Strategy
                             type: string
                           key:
                             description: Key is the key used in the Provider, mandatory
@@ -2878,7 +2893,7 @@ spec:
                             type: string
                           decodingStrategy:
                             default: None
-                            description: Used to define a conversion Strategy
+                            description: Used to define a decoding Strategy
                             type: string
                           name:
                             description: Finds secrets based on the name.
@@ -2896,6 +2911,25 @@ spec:
                             description: Find secrets based on tags.
                             type: object
                         type: object
+                      rewrite:
+                        description: Used to rewrite secret Keys after getting them from the secret Provider Multiple Rewrite operations can be provided. They are applied in a layered order (first to last)
+                        items:
+                          properties:
+                            regexp:
+                              description: Used to rewrite with regular expressions. The resulting key will be the output of a regexp.ReplaceAll operation.
+                              properties:
+                                source:
+                                  description: Used to define the regular expression of a re.Compiler.
+                                  type: string
+                                target:
+                                  description: Used to define the target pattern of a ReplaceAll operation.
+                                  type: string
+                              required:
+                                - source
+                                - target
+                              type: object
+                          type: object
+                        type: array
                     type: object
                   type: array
                 refreshInterval:

+ 105 - 0
docs/guides-datafrom-rewrite.md

@@ -0,0 +1,105 @@
+# Rewriting Keys in DataFrom
+
+When calling out an ExternalSecret with `dataFrom.extract` or `dataFrom.find`, it is possible that you end up with a kubernetes secret that has conflicts in the key names, or that you simply want to remove a common path from the secret keys.
+
+In order to do so, it is possible to define a set of rewrite operations using `dataFrom.rewrite`. These operations can be stacked, hence allowing complex manipulations of the secret keys.
+
+Rewrite operations are all applied before `ConversionStrategy` is applied.
+
+## Methods
+
+### Regexp
+This method implements rewriting through the use of regular expressions. It needs a `source` and a `target` field. The source field is where the definition of the matching regular expression goes, where the `target` field is where the replacing expression goes.
+
+Some considerations about the impelementation of Regexp Rewrite:
+
+1. The input of a subsequent rewrite operation are the outputs of the previous rewrite.
+2. If a given set of keys do not match any Rewrite operation, there will be no error. Rather, the original keys will be used.
+3. If a `source` is not a compilable `regexp` expression, an error will be produced and the external secret goes into a error state.
+
+## Examples
+### Removing a common path from find operations
+The following ExternalSecret:
+```yaml
+{% include 'datafrom-rewrite-remove-path.yaml' %}
+```
+Will get all the secrets matching `path/to/my/secrets/*` and then rewrite them by removing the common path away.
+
+In this example, if we had the following secrets available in the provider:
+```
+path/to/my/secrets/username
+path/to/my/secrets/password
+```
+the output kubernetes secret would be:
+```yaml
+apiVersion: v1
+kind: Secret
+type: Opaque
+data:
+    username: ...
+    password: ...
+```
+### Avoiding key collisions
+The following ExternalSecret:
+```yaml
+{% include 'datafrom-rewrite-conflict.yaml' %}
+
+```
+Will allow two secrets with the same JSON keys to be imported into a Kubernetes Secret without any conflict.
+In this example, if we had the following secrets available in the provider:
+```json
+{
+    "my-secrets-dev": {
+        "password": "bar",
+     },
+    "my-secrets-prod": {
+        "password": "safebar",
+     }
+}
+```
+the output kubernetes secret would be:
+```yaml
+apiVersion: v1
+kind: Secret
+type: Opaque
+data:
+    dev_password: YmFy #bar
+    prod_password: c2FmZWJhcg== #safebar
+```
+
+### Remove invalid characters
+The following ExternalSecret:
+```yaml
+{% include 'datafrom-rewrite-invalid-characters.yaml' %}
+
+```
+Will remove invalid characters from the secret key.
+In this example, if we had the following secrets available in the provider:
+```json
+{
+    "development": {
+        "foo/bar": "1111",
+        "foo$baz": "2222"
+    }
+}
+```
+the output kubernetes secret would be:
+```yaml
+apiVersion: v1
+kind: Secret
+type: Opaque
+data:
+    foo_bar: MTExMQ== #1111
+    foo_baz: MjIyMg== #2222
+```
+
+## Limitations
+
+Regexp Rewrite is based on golang `regexp`, which in turns implements `RE2` regexp language. There a a series of known limitations to this implementation, such as:
+
+* Lack of ability to do lookaheads or lookbehinds;
+* Lack of negation expressions;
+* Lack of support to conditionl branches;
+* Lack of support to possessive repetitions.
+
+A list of compatibility and known limitations considering other commonly used regexp frameworks (such as PCRE and PERL) are listed [here](https://github.com/google/re2/wiki/Syntax).

+ 7 - 2
docs/guides-getallsecrets.md

@@ -3,7 +3,7 @@
 In some use cases, it might be impractical to bundle all sensitive information into a single secret, or even it is not possible to fully know a given secret name. In such cases, it is possible that an user might need to sync multiple secrets from an external provider into a single Kubernetes Secret. This is possible to be done in external-secrets with the `dataFrom.find` option.
 
 !!! note
-    The secret's contents as defined in the provider are going to be stored in the kubernetes secret as a single key. Currently, it is not possible to apply any decoding Strategy during a find operation.
+    The secret's contents as defined in the provider are going to be stored in the kubernetes secret as a single key. Currently, it possible to apply a decoding Strategy during a find operation, but only at the secret level (e.g. if a secret is a JSON with some B64 encoded data within, `decodingStrategy: Auto` would not decode it)
 
 
 ### Fetching secrets matching a given name pattern
@@ -33,7 +33,12 @@ Some providers support filtering out a find operation only to a given path, inst
 ### Avoiding name conflicts
 By default, kubernetes Secrets accepts only a given range of characters. `Find` operations will automatically replace any not allowed character with a `_`. So if we have a given secret `a_c` and `a/c` would lead to a naming conflict. 
 
-It is not entirely possible to avoid this behavior, but setting `dataFrom.find.conversionStrategy: Unicode` reduces the collision probability. When using `Unicode`, any invalid character will be replaced by its unicode, in the form of `_UXXXX_`. In this case, the available kubernetes keys would be `a_c` and `a_U2215_c`, hence avoiding most of possible conflicts.
+
+If you happen to have a case where a conflict is happening, you can use the `rewrite` block to apply a regexp on one of the find operations (for more information please refer to [Rewriting Keys from DataFrom](guides-datafrom-rewrite.md)).
+
+You can also set  `dataFrom.find.conversionStrategy: Unicode` to reduce the collistion probability. When using `Unicode`, any invalid character will be replaced by its unicode, in the form of `_UXXXX_`. In this case, the available kubernetes keys would be `a_c` and `a_U2215_c`, hence avoiding most of possible conflicts.
+
+
 
 !!! note "PRs welcome"
     Some providers might not have the implementation needed for fetching multiple secrets. If that's your case, please feel free to contribute!

+ 24 - 0
docs/snippets/datafrom-rewrite-conflict.yaml

@@ -0,0 +1,24 @@
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: example
+spec:
+  refreshInterval: 1h
+  secretStoreRef:
+    kind: SecretStore
+    name: backend
+  target:
+    name: secret-to-be-created
+  dataFrom:
+  - extract:
+      key: my-secrets-dev
+    rewrite:
+    - regexp:
+        source: "(.*)"
+        target: "dev-$1"      
+  - extract:
+      key: my-secrets-prod
+    rewrite:
+    - regexp:
+        source: "(.*)"
+        target: "prod-$1"

+ 18 - 0
docs/snippets/datafrom-rewrite-invalid-characters.yaml

@@ -0,0 +1,18 @@
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: example
+spec:
+  refreshInterval: 1h
+  secretStoreRef:
+    kind: SecretStore
+    name: backend
+  target:
+    name: secret-to-be-created
+  dataFrom:
+  - extract:
+      key: development
+    rewrite:
+    - regexp:
+        source: "[^a-zA-Z0-9 -]"
+        target: "_"

+ 20 - 0
docs/snippets/datafrom-rewrite-remove-path.yaml

@@ -0,0 +1,20 @@
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: example
+spec:
+  refreshInterval: 1h
+  secretStoreRef:
+    kind: SecretStore
+    name: backend
+  target:
+    name: secret-to-be-created
+  dataFrom:
+  - find:
+      path: path/to/my
+      name: 
+        regexp: secrets
+    rewrite:
+    - regexp:
+        source: "path/to/my/secrets/(.*)"
+        target: "$1"

+ 14 - 0
docs/snippets/full-external-secret.yaml

@@ -85,14 +85,28 @@ spec:
       property: provider-key-property
       conversionStrategy: Default
       decodingStrategy: Auto
+    rewrite:
+    - regexp:
+        source: "foo"
+        target: "bar"
+    - regexp:
+        source: "exp-(.*?)-ression"
+        target: "rewriting-$1-with-groups"
   - find:
       path: path-to-filter
+          source: "exp-(.*?)-ression"
+          target: "rewriting-$1-with-groups"
       name:
         regexp: ".*foobar.*"
       tags:
         foo: bar
       conversionStrategy: Unicode
       decodingStrategy: Base64
+    rewrite:
+    - regexp:
+        source: "foo"
+        target: "bar"
+    - regexp:
 
 status:
   # refreshTime is the time and date the external secret was fetched and

+ 86 - 0
docs/spec.md

@@ -1522,6 +1522,20 @@ ExternalSecretFind
 <p>Used to find secrets based on tags or regular expressions</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>rewrite</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.ExternalSecretRewrite">
+[]ExternalSecretRewrite
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Used to rewrite secret Keys after getting them from the secret Provider</p>
+</td>
+</tr>
 </tbody>
 </table>
 <h3 id="external-secrets.io/v1beta1.ExternalSecretDataRemoteRef">ExternalSecretDataRemoteRef
@@ -1787,6 +1801,78 @@ ExternalSecretDecodingStrategy
 <td></td>
 </tr></tbody>
 </table>
+<h3 id="external-secrets.io/v1beta1.ExternalSecretRewrite">ExternalSecretRewrite
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.ExternalSecretDataFromRemoteRef">ExternalSecretDataFromRemoteRef</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>regexp</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.ExternalSecretRewriteRegexp">
+ExternalSecretRewriteRegexp
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Rewrite using regular expressions</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1beta1.ExternalSecretRewriteRegexp">ExternalSecretRewriteRegexp
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.ExternalSecretRewrite">ExternalSecretRewrite</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>source</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>Regular expression to use as a re.Compiler.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>target</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>Target output for a replace operation.</p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1beta1.ExternalSecretSpec">ExternalSecretSpec
 </h3>
 <p>

+ 1 - 0
e2e/suites/provider/cases/akeyless/akeyless.go

@@ -31,6 +31,7 @@ var _ = Describe("[akeyless]", Label("akeyless"), func() {
 		Entry(common.SimpleDataSync(f)),
 		Entry(common.NestedJSONWithGJSON(f)),
 		Entry(common.JSONDataFromSync(f)),
+		Entry(common.JSONDataFromRewrite(f)),
 		Entry(common.JSONDataWithProperty(f)),
 		Entry(common.JSONDataWithTemplate(f)),
 		Entry(common.DockerJSONConfig(f)),

+ 1 - 0
e2e/suites/provider/cases/alibaba/alibaba.go

@@ -31,6 +31,7 @@ var _ = Describe("[alibaba]", Label("alibaba"), func() {
 		Entry(common.SimpleDataSync(f)),
 		Entry(common.NestedJSONWithGJSON(f)),
 		Entry(common.JSONDataFromSync(f)),
+		Entry(common.JSONDataFromRewrite(f)),
 		Entry(common.JSONDataWithProperty(f)),
 		Entry(common.JSONDataWithTemplate(f)),
 		Entry(common.DockerJSONConfig(f)),

+ 1 - 0
e2e/suites/provider/cases/aws/parameterstore/parameterstore.go

@@ -33,6 +33,7 @@ var _ = Describe("[aws] ", Label("aws", "parameterstore"), func() {
 		Entry(common.SimpleDataSync(f)),
 		Entry(common.NestedJSONWithGJSON(f)),
 		Entry(common.JSONDataFromSync(f)),
+		Entry(common.JSONDataFromRewrite(f)),
 		Entry(common.JSONDataWithProperty(f)),
 		Entry(common.JSONDataWithTemplate(f)),
 		Entry(common.DockerJSONConfig(f)),

+ 1 - 0
e2e/suites/provider/cases/aws/secretsmanager/secretsmanager.go

@@ -33,6 +33,7 @@ var _ = Describe("[aws] ", Label("aws", "secretsmanager"), func() {
 		Entry(common.SimpleDataSync(f)),
 		Entry(common.NestedJSONWithGJSON(f)),
 		Entry(common.JSONDataFromSync(f)),
+		Entry(common.JSONDataFromRewrite(f)),
 		Entry(common.JSONDataWithProperty(f)),
 		Entry(common.JSONDataWithTemplate(f)),
 		Entry(common.DockerJSONConfig(f)),

+ 1 - 0
e2e/suites/provider/cases/azure/azure_secret.go

@@ -30,6 +30,7 @@ var _ = Describe("[azure]", Label("azure", "keyvault", "secret"), func() {
 		Entry(common.SimpleDataSync(f)),
 		Entry(common.NestedJSONWithGJSON(f)),
 		Entry(common.JSONDataFromSync(f)),
+		Entry(common.JSONDataFromRewrite(f)),
 		Entry(common.JSONDataWithProperty(f)),
 		Entry(common.JSONDataWithTemplate(f)),
 		Entry(common.DockerJSONConfig(f)),

+ 37 - 0
e2e/suites/provider/cases/common/common.go

@@ -291,6 +291,43 @@ func JSONDataFromSync(f *framework.Framework) (string, func(*framework.TestCase)
 	}
 }
 
+// This case creates one secret with json values and syncs them using a single .Spec.DataFrom block.
+func JSONDataFromRewrite(f *framework.Framework) (string, func(*framework.TestCase)) {
+	return "[common] should sync and rewrite secrets with dataFrom", func(tc *framework.TestCase) {
+		secretKey1 := fmt.Sprintf("%s-%s", f.Namespace.Name, "one")
+		targetSecretKey1 := "username"
+		targetSecretValue1 := "myuser.name"
+		targetSecretKey2 := "address"
+		targetSecretValue2 := "happy street"
+		secretValue := fmt.Sprintf("{ %q: %q, %q: %q }", targetSecretKey1, targetSecretValue1, targetSecretKey2, targetSecretValue2)
+		tc.Secrets = map[string]framework.SecretEntry{
+			secretKey1: {Value: secretValue},
+		}
+		tc.ExpectedSecret = &v1.Secret{
+			Type: v1.SecretTypeOpaque,
+			Data: map[string][]byte{
+				"my_username": []byte(targetSecretValue1),
+				"my_address":  []byte(targetSecretValue2),
+			},
+		}
+		tc.ExternalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
+			{
+				Extract: &esv1beta1.ExternalSecretDataRemoteRef{
+					Key: secretKey1,
+				},
+				Rewrite: []esv1beta1.ExternalSecretRewrite{
+					{
+						Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+							Source: "(.*)",
+							Target: "my_$1",
+						},
+					},
+				},
+			},
+		}
+	}
+}
+
 // This case creates a secret with a nested json value. It is synced into two secrets.
 // The values from the nested data are extracted using gjson.
 // not supported by: vault.

+ 50 - 2
e2e/suites/provider/cases/common/find_by_name.go

@@ -21,6 +21,10 @@ import (
 	"github.com/external-secrets/external-secrets/e2e/framework"
 )
 
+const (
+	findValue = "{\"foo1\":\"foo1-val\"}"
+)
+
 // This case creates multiple secrets with simple key/value pairs and syncs them using multiple .Spec.Data blocks.
 func FindByName(f *framework.Framework) (string, func(*framework.TestCase)) {
 	return "[common] should find secrets by name using .DataFrom[]", func(tc *framework.TestCase) {
@@ -28,7 +32,7 @@ func FindByName(f *framework.Framework) (string, func(*framework.TestCase)) {
 		secretKeyOne := fmt.Sprintf(namePrefix, f.Namespace.Name, "one")
 		secretKeyTwo := fmt.Sprintf(namePrefix, f.Namespace.Name, "two")
 		secretKeyThree := fmt.Sprintf(namePrefix, f.Namespace.Name, "three")
-		secretValue := "{\"foo1\":\"foo1-val\"}"
+		secretValue := findValue
 		tc.Secrets = map[string]framework.SecretEntry{
 			secretKeyOne:   {Value: secretValue},
 			secretKeyTwo:   {Value: secretValue},
@@ -54,12 +58,56 @@ func FindByName(f *framework.Framework) (string, func(*framework.TestCase)) {
 	}
 }
 
+// This case creates multiple secrets with simple key/value pairs and syncs them using multiple .Spec.Data blocks.
+func FindByNameAndRewrite(f *framework.Framework) (string, func(*framework.TestCase)) {
+	return "[common] should find and rewrite secrets by name using .DataFrom[]", func(tc *framework.TestCase) {
+		const namePrefix = "e2e_find_and_rewrite_%s_%s"
+		secretKeyOne := fmt.Sprintf(namePrefix, f.Namespace.Name, "one")
+		secretKeyTwo := fmt.Sprintf(namePrefix, f.Namespace.Name, "two")
+		secretKeyThree := fmt.Sprintf(namePrefix, f.Namespace.Name, "three")
+		expectedKeyOne := fmt.Sprintf("%s_%s", f.Namespace.Name, "one")
+		expectedKeyTwo := fmt.Sprintf("%s_%s", f.Namespace.Name, "two")
+		expectedKeyThree := fmt.Sprintf("%s_%s", f.Namespace.Name, "three")
+		secretValue := findValue
+		tc.Secrets = map[string]framework.SecretEntry{
+			secretKeyOne:   {Value: secretValue},
+			secretKeyTwo:   {Value: secretValue},
+			secretKeyThree: {Value: secretValue},
+		}
+		tc.ExpectedSecret = &v1.Secret{
+			Type: v1.SecretTypeOpaque,
+			Data: map[string][]byte{
+				expectedKeyOne:   []byte(secretValue),
+				expectedKeyTwo:   []byte(secretValue),
+				expectedKeyThree: []byte(secretValue),
+			},
+		}
+		tc.ExternalSecret.Spec.DataFrom = []esapi.ExternalSecretDataFromRemoteRef{
+			{
+				Find: &esapi.ExternalSecretFind{
+					Name: &esapi.FindName{
+						RegExp: fmt.Sprintf("e2e_find_and_rewrite_%s.+", f.Namespace.Name),
+					},
+				},
+				Rewrite: []esapi.ExternalSecretRewrite{
+					{
+						Regexp: &esapi.ExternalSecretRewriteRegexp{
+							Source: "e2e_find_and_rewrite_(.*)",
+							Target: "$1",
+						},
+					},
+				},
+			},
+		}
+	}
+}
+
 func FindByNameWithPath(f *framework.Framework) (string, func(*framework.TestCase)) {
 	return "[common] should find secrets by name with path", func(tc *framework.TestCase) {
 		secretKeyOne := fmt.Sprintf("e2e-find-name-%s-one", f.Namespace.Name)
 		secretKeyTwo := fmt.Sprintf("%s-two", f.Namespace.Name)
 		secretKeythree := fmt.Sprintf("%s-three", f.Namespace.Name)
-		secretValue := "{\"foo1\":\"foo1-val\"}"
+		secretValue := findValue
 		tc.Secrets = map[string]framework.SecretEntry{
 			secretKeyOne:   {Value: secretValue},
 			secretKeyTwo:   {Value: secretValue},

+ 2 - 0
e2e/suites/provider/cases/gcp/gcp.go

@@ -38,6 +38,7 @@ var _ = Describe("[gcp]", Label("gcp", "secretsmanager"), func() {
 		Entry(common.SimpleDataSync(f)),
 		Entry(common.JSONDataWithProperty(f)),
 		Entry(common.JSONDataFromSync(f)),
+		Entry(common.JSONDataFromRewrite(f)),
 		Entry(common.NestedJSONWithGJSON(f)),
 		Entry(common.JSONDataWithTemplate(f)),
 		Entry(common.DockerJSONConfig(f)),
@@ -47,6 +48,7 @@ var _ = Describe("[gcp]", Label("gcp", "secretsmanager"), func() {
 		Entry(common.SyncWithoutTargetName(f)),
 		Entry(common.JSONDataWithoutTargetName(f)),
 		Entry(common.FindByName(f)),
+		Entry(common.FindByNameAndRewrite(f)),
 		Entry(common.FindByNameWithPath(f)),
 		Entry(common.FindByTag(f)),
 		Entry(common.FindByTagWithPath(f)),

+ 1 - 0
e2e/suites/provider/cases/gitlab/gitlab.go

@@ -34,6 +34,7 @@ var _ = Describe("[gitlab]", Label("gitlab"), func() {
 		Entry(common.SimpleDataSync(f)),
 		Entry(common.JSONDataWithProperty(f)),
 		Entry(common.JSONDataFromSync(f)),
+		Entry(common.JSONDataFromRewrite(f)),
 		Entry(common.NestedJSONWithGJSON(f)),
 		Entry(common.JSONDataWithTemplate(f)),
 		Entry(common.SyncWithoutTargetName(f)),

+ 1 - 0
e2e/suites/provider/cases/kubernetes/kubernetes.go

@@ -41,6 +41,7 @@ var _ = Describe("[kubernetes] ", Label("kubernetes"), func() {
 		Entry(common.DataPropertyDockerconfigJSON(f)),
 		Entry(common.SSHKeySyncDataProperty(f)),
 		Entry(common.JSONDataFromSync(f)),
+		Entry(common.JSONDataFromRewrite(f)),
 		Entry(FindByTag(f)),
 		Entry(FindByName(f)),
 

+ 1 - 0
e2e/suites/provider/cases/oracle/oracle.go

@@ -29,6 +29,7 @@ var _ = Describe("[oracle]", Label("oracle"), func() {
 		Entry(common.SimpleDataSync(f)),
 		Entry(common.NestedJSONWithGJSON(f)),
 		Entry(common.JSONDataFromSync(f)),
+		Entry(common.JSONDataFromRewrite(f)),
 		Entry(common.JSONDataWithProperty(f)),
 		Entry(common.JSONDataWithTemplate(f)),
 		Entry(common.DockerJSONConfig(f)),

+ 12 - 0
e2e/suites/provider/cases/vault/vault.go

@@ -43,7 +43,9 @@ var _ = Describe("[vault]", Label("vault"), func() {
 		framework.TableFunc(f, prov),
 		// uses token auth
 		framework.Compose(withTokenAuth, f, common.FindByName, useTokenAuth),
+		framework.Compose(withTokenAuth, f, common.FindByNameAndRewrite, useTokenAuth),
 		framework.Compose(withTokenAuth, f, common.JSONDataFromSync, useTokenAuth),
+		framework.Compose(withTokenAuth, f, common.JSONDataFromRewrite, useTokenAuth),
 		framework.Compose(withTokenAuth, f, common.JSONDataWithProperty, useTokenAuth),
 		framework.Compose(withTokenAuth, f, common.JSONDataWithTemplate, useTokenAuth),
 		framework.Compose(withTokenAuth, f, common.DataPropertyDockerconfigJSON, useTokenAuth),
@@ -52,40 +54,50 @@ var _ = Describe("[vault]", Label("vault"), func() {
 		framework.Compose(withTokenAuth, f, common.DecodingPolicySync, useTokenAuth),
 		// use cert auth
 		framework.Compose(withCertAuth, f, common.FindByName, useCertAuth),
+		framework.Compose(withCertAuth, f, common.FindByNameAndRewrite, useCertAuth),
 		framework.Compose(withCertAuth, f, common.JSONDataFromSync, useCertAuth),
+		framework.Compose(withCertAuth, f, common.JSONDataFromRewrite, useCertAuth),
 		framework.Compose(withCertAuth, f, common.JSONDataWithProperty, useCertAuth),
 		framework.Compose(withCertAuth, f, common.JSONDataWithTemplate, useCertAuth),
 		framework.Compose(withCertAuth, f, common.DataPropertyDockerconfigJSON, useCertAuth),
 		framework.Compose(withCertAuth, f, common.JSONDataWithoutTargetName, useCertAuth),
 		// use approle auth
 		framework.Compose(withApprole, f, common.FindByName, useApproleAuth),
+		framework.Compose(withApprole, f, common.FindByNameAndRewrite, useApproleAuth),
 		framework.Compose(withApprole, f, common.JSONDataFromSync, useApproleAuth),
+		framework.Compose(withApprole, f, common.JSONDataFromRewrite, useApproleAuth),
 		framework.Compose(withApprole, f, common.JSONDataWithProperty, useApproleAuth),
 		framework.Compose(withApprole, f, common.JSONDataWithTemplate, useApproleAuth),
 		framework.Compose(withApprole, f, common.DataPropertyDockerconfigJSON, useApproleAuth),
 		framework.Compose(withApprole, f, common.JSONDataWithoutTargetName, useApproleAuth),
 		// use v1 provider
 		framework.Compose(withV1, f, common.JSONDataFromSync, useV1Provider),
+		framework.Compose(withV1, f, common.JSONDataFromRewrite, useV1Provider),
 		framework.Compose(withV1, f, common.JSONDataWithProperty, useV1Provider),
 		framework.Compose(withV1, f, common.JSONDataWithTemplate, useV1Provider),
 		framework.Compose(withV1, f, common.DataPropertyDockerconfigJSON, useV1Provider),
 		framework.Compose(withV1, f, common.JSONDataWithoutTargetName, useV1Provider),
 		// use jwt provider
 		framework.Compose(withJWT, f, common.FindByName, useJWTProvider),
+		framework.Compose(withJWT, f, common.FindByNameAndRewrite, useJWTProvider),
 		framework.Compose(withJWT, f, common.JSONDataFromSync, useJWTProvider),
+		framework.Compose(withJWT, f, common.JSONDataFromRewrite, useJWTProvider),
 		framework.Compose(withJWT, f, common.JSONDataWithProperty, useJWTProvider),
 		framework.Compose(withJWT, f, common.JSONDataWithTemplate, useJWTProvider),
 		framework.Compose(withJWT, f, common.DataPropertyDockerconfigJSON, useJWTProvider),
 		framework.Compose(withJWT, f, common.JSONDataWithoutTargetName, useJWTProvider),
 		// use jwt k8s provider
 		framework.Compose(withJWTK8s, f, common.JSONDataFromSync, useJWTK8sProvider),
+		framework.Compose(withJWTK8s, f, common.JSONDataFromRewrite, useJWTK8sProvider),
 		framework.Compose(withJWTK8s, f, common.JSONDataWithProperty, useJWTK8sProvider),
 		framework.Compose(withJWTK8s, f, common.JSONDataWithTemplate, useJWTK8sProvider),
 		framework.Compose(withJWTK8s, f, common.DataPropertyDockerconfigJSON, useJWTK8sProvider),
 		framework.Compose(withJWTK8s, f, common.JSONDataWithoutTargetName, useJWTK8sProvider),
 		// use kubernetes provider
 		framework.Compose(withK8s, f, common.FindByName, useKubernetesProvider),
+		framework.Compose(withK8s, f, common.FindByNameAndRewrite, useKubernetesProvider),
 		framework.Compose(withK8s, f, common.JSONDataFromSync, useKubernetesProvider),
+		framework.Compose(withK8s, f, common.JSONDataFromRewrite, useKubernetesProvider),
 		framework.Compose(withK8s, f, common.JSONDataWithProperty, useKubernetesProvider),
 		framework.Compose(withK8s, f, common.JSONDataWithTemplate, useKubernetesProvider),
 		framework.Compose(withK8s, f, common.DataPropertyDockerconfigJSON, useKubernetesProvider),

+ 1 - 0
hack/api-docs/mkdocs.yml

@@ -52,6 +52,7 @@ nav:
     - Getting Multiple Secrets: guides-getallsecrets.md
     - Multi Tenancy: guides-multi-tenancy.md
     - Metrics: guides-metrics.md
+    - Rewriting Keys: guides-datafrom-rewrite.md
     - Upgrading to v1beta1: guides-v1beta1.md
     - Using Latest Image: guides-using-latest-image.md
   - Provider:

+ 26 - 5
pkg/controllers/externalsecret/externalsecret_controller.go

@@ -51,6 +51,8 @@ const (
 	errGetES                 = "could not get ExternalSecret"
 	errConvert               = "could not apply conversion strategy to keys: %v"
 	errDecode                = "could not apply decoding strategy to %v[%d]: %v"
+	errRewrite               = "could not rewrite spec.dataFrom[%d]: %v"
+	errInvalidKeys           = "secret keys from spec.dataFrom.%v[%d] can only have alphanumeric,'-', '_' or '.' characters. Convert them using rewrite (https://external-secrets.io/latest/guides-datafrom-rewrite)"
 	errUpdateSecret          = "could not update Secret"
 	errPatchStatus           = "unable to patch status"
 	errGetSecretStore        = "could not get SecretStore %q, %w"
@@ -533,9 +535,20 @@ func (r *Reconciler) getProviderSecretData(ctx context.Context, providerClient e
 			if err != nil {
 				return nil, err
 			}
-			secretMap, err = utils.ConvertKeys(remoteRef.Find.ConversionStrategy, secretMap)
+			secretMap, err = utils.RewriteMap(remoteRef.Rewrite, secretMap)
 			if err != nil {
-				return nil, fmt.Errorf(errConvert, err)
+				return nil, fmt.Errorf(errRewrite, i, err)
+			}
+			if len(remoteRef.Rewrite) == 0 {
+				// ConversionStrategy is deprecated. Use RewriteMap instead.
+				r.recorder.Event(externalSecret, v1.EventTypeWarning, esv1beta1.ReasonDeprecated, fmt.Sprintf("dataFrom[%d].find.conversionStrategy=%v is deprecated and will be removed in further releases. Use dataFrom.rewrite instead", i, remoteRef.Find.ConversionStrategy))
+				secretMap, err = utils.ConvertKeys(remoteRef.Find.ConversionStrategy, secretMap)
+				if err != nil {
+					return nil, fmt.Errorf(errConvert, err)
+				}
+			}
+			if !utils.ValidateKeys(secretMap) {
+				return nil, fmt.Errorf(errInvalidKeys, "find", i)
 			}
 			secretMap, err = utils.DecodeMap(remoteRef.Find.DecodingStrategy, secretMap)
 			if err != nil {
@@ -550,16 +563,24 @@ func (r *Reconciler) getProviderSecretData(ctx context.Context, providerClient e
 			if err != nil {
 				return nil, err
 			}
-			secretMap, err = utils.ConvertKeys(remoteRef.Extract.ConversionStrategy, secretMap)
+			secretMap, err = utils.RewriteMap(remoteRef.Rewrite, secretMap)
 			if err != nil {
-				return nil, fmt.Errorf(errConvert, err)
+				return nil, fmt.Errorf(errRewrite, i, err)
+			}
+			if len(remoteRef.Rewrite) == 0 {
+				secretMap, err = utils.ConvertKeys(remoteRef.Extract.ConversionStrategy, secretMap)
+				if err != nil {
+					return nil, fmt.Errorf(errConvert, err)
+				}
+			}
+			if !utils.ValidateKeys(secretMap) {
+				return nil, fmt.Errorf(errInvalidKeys, "extract", i)
 			}
 			secretMap, err = utils.DecodeMap(remoteRef.Extract.DecodingStrategy, secretMap)
 			if err != nil {
 				return nil, fmt.Errorf(errDecode, "spec.dataFrom", i, err)
 			}
 		}
-
 		providerData = utils.MergeByteMap(providerData, secretMap)
 	}
 

+ 168 - 0
pkg/controllers/externalsecret/externalsecret_controller_test.go

@@ -945,6 +945,139 @@ var _ = Describe("ExternalSecret controller", func() {
 		}
 	}
 
+	// with rewrite all keys from a dataFrom operation
+	// should be put with new rewriting into the secret
+	syncAndRewriteWithDataFrom := func(tc *testCase) {
+		tc.externalSecret.Spec.Data = nil
+		tc.externalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
+			{
+				Extract: &esv1beta1.ExternalSecretDataRemoteRef{
+					Key: remoteKey,
+				},
+				Rewrite: []esv1beta1.ExternalSecretRewrite{{
+					Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+						Source: "(.*)",
+						Target: "new-$1",
+					},
+				}},
+			},
+			{
+				Extract: &esv1beta1.ExternalSecretDataRemoteRef{
+					Key: remoteKey,
+				},
+				Rewrite: []esv1beta1.ExternalSecretRewrite{{
+					Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+						Source: "(.*)",
+						Target: "old-$1",
+					},
+				}},
+			},
+		}
+		fakeProvider.WithGetSecretMap(map[string][]byte{
+			"foo": []byte(FooValue),
+			"bar": []byte(BarValue),
+		}, nil)
+		tc.checkSecret = func(es *esv1beta1.ExternalSecret, secret *v1.Secret) {
+			// check values
+			Expect(string(secret.Data["new-foo"])).To(Equal(FooValue))
+			Expect(string(secret.Data["new-bar"])).To(Equal(BarValue))
+			Expect(string(secret.Data["old-foo"])).To(Equal(FooValue))
+			Expect(string(secret.Data["old-bar"])).To(Equal(BarValue))
+		}
+	}
+	// with rewrite keys from dataFrom
+	// should error if keys are not compliant
+	invalidExtractKeysErrCondition := func(tc *testCase) {
+		tc.externalSecret.Spec.Data = nil
+		tc.externalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
+			{
+				Extract: &esv1beta1.ExternalSecretDataRemoteRef{
+					Key: remoteKey,
+				},
+				Rewrite: []esv1beta1.ExternalSecretRewrite{{
+					Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+						Source: "(.*)",
+						Target: "$1",
+					},
+				}},
+			},
+		}
+		fakeProvider.WithGetSecretMap(map[string][]byte{
+			"foo/bar": []byte(FooValue),
+			"bar/foo": []byte(BarValue),
+		}, nil)
+		tc.checkCondition = func(es *esv1beta1.ExternalSecret) bool {
+			cond := GetExternalSecretCondition(es.Status, esv1beta1.ExternalSecretReady)
+			if cond == nil || cond.Status != v1.ConditionFalse || cond.Reason != esv1beta1.ConditionReasonSecretSyncedError {
+				return false
+			}
+			return true
+		}
+		tc.checkCondition = func(es *esv1beta1.ExternalSecret) bool {
+			cond := GetExternalSecretCondition(es.Status, esv1beta1.ExternalSecretReady)
+			if cond == nil || cond.Status != v1.ConditionFalse || cond.Reason != esv1beta1.ConditionReasonSecretSyncedError {
+				return false
+			}
+			return true
+		}
+		tc.checkExternalSecret = func(es *esv1beta1.ExternalSecret) {
+			Eventually(func() bool {
+				Expect(syncCallsError.WithLabelValues(ExternalSecretName, ExternalSecretNamespace).Write(&metric)).To(Succeed())
+				return metric.GetCounter().GetValue() >= 2.0
+			}, timeout, interval).Should(BeTrue())
+			Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1beta1.ExternalSecretReady, v1.ConditionFalse, 1.0)).To(BeTrue())
+			Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1beta1.ExternalSecretReady, v1.ConditionTrue, 0.0)).To(BeTrue())
+		}
+
+	}
+	// with rewrite keys from dataFrom
+	// should error if keys are not compliant
+	invalidFindKeysErrCondition := func(tc *testCase) {
+		tc.externalSecret.Spec.Data = nil
+		tc.externalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
+			{
+				Find: &esv1beta1.ExternalSecretFind{
+					Name: &esv1beta1.FindName{
+						RegExp: ".*",
+					},
+				},
+				Rewrite: []esv1beta1.ExternalSecretRewrite{{
+					Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+						Source: "(.*)",
+						Target: "$1",
+					},
+				}},
+			},
+		}
+		fakeProvider.WithGetAllSecrets(map[string][]byte{
+			"foo/bar": []byte(FooValue),
+			"bar/foo": []byte(BarValue),
+		}, nil)
+		tc.checkCondition = func(es *esv1beta1.ExternalSecret) bool {
+			cond := GetExternalSecretCondition(es.Status, esv1beta1.ExternalSecretReady)
+			if cond == nil || cond.Status != v1.ConditionFalse || cond.Reason != esv1beta1.ConditionReasonSecretSyncedError {
+				return false
+			}
+			return true
+		}
+		tc.checkCondition = func(es *esv1beta1.ExternalSecret) bool {
+			cond := GetExternalSecretCondition(es.Status, esv1beta1.ExternalSecretReady)
+			if cond == nil || cond.Status != v1.ConditionFalse || cond.Reason != esv1beta1.ConditionReasonSecretSyncedError {
+				return false
+			}
+			return true
+		}
+		tc.checkExternalSecret = func(es *esv1beta1.ExternalSecret) {
+			Eventually(func() bool {
+				Expect(syncCallsError.WithLabelValues(ExternalSecretName, ExternalSecretNamespace).Write(&metric)).To(Succeed())
+				return metric.GetCounter().GetValue() >= 2.0
+			}, timeout, interval).Should(BeTrue())
+			Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1beta1.ExternalSecretReady, v1.ConditionFalse, 1.0)).To(BeTrue())
+			Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1beta1.ExternalSecretReady, v1.ConditionTrue, 0.0)).To(BeTrue())
+		}
+
+	}
+
 	// with dataFrom all properties from the specified secret
 	// should be put into the secret
 	syncWithDataFrom := func(tc *testCase) {
@@ -966,6 +1099,37 @@ var _ = Describe("ExternalSecret controller", func() {
 			Expect(string(secret.Data["bar"])).To(Equal(BarValue))
 		}
 	}
+	// with dataFrom.Find the change is on the called method GetAllSecrets
+	// all keys should be put into the secret
+	syncAndRewriteDataFromFind := func(tc *testCase) {
+		tc.externalSecret.Spec.Data = nil
+		tc.externalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
+			{
+				Find: &esv1beta1.ExternalSecretFind{
+					Name: &esv1beta1.FindName{
+						RegExp: "foobar",
+					},
+				},
+				Rewrite: []esv1beta1.ExternalSecretRewrite{
+					{
+						Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+							Source: "(.*)",
+							Target: "new-$1",
+						},
+					},
+				},
+			},
+		}
+		fakeProvider.WithGetAllSecrets(map[string][]byte{
+			"foo": []byte(FooValue),
+			"bar": []byte(BarValue),
+		}, nil)
+		tc.checkSecret = func(es *esv1beta1.ExternalSecret, secret *v1.Secret) {
+			// check values
+			Expect(string(secret.Data["new-foo"])).To(Equal(FooValue))
+			Expect(string(secret.Data["new-bar"])).To(Equal(BarValue))
+		}
+	}
 
 	// with dataFrom.Find the change is on the called method GetAllSecrets
 	// all keys should be put into the secret
@@ -1284,7 +1448,11 @@ var _ = Describe("ExternalSecret controller", func() {
 		Entry("should refresh secret map when provider secret changes when using a template", refreshSecretValueMapTemplate),
 		Entry("should not refresh secret value when provider secret changes but refreshInterval is zero", refreshintervalZero),
 		Entry("should fetch secret using dataFrom", syncWithDataFrom),
+		Entry("should rewrite secret using dataFrom", syncAndRewriteWithDataFrom),
+		Entry("should not automatically convert from extract if rewrite is used", invalidExtractKeysErrCondition),
 		Entry("should fetch secret using dataFrom.find", syncDataFromFind),
+		Entry("should rewrite secret using dataFrom.find", syncAndRewriteDataFromFind),
+		Entry("should not automatically convert from find if rewrite is used", invalidFindKeysErrCondition),
 		Entry("should fetch secret using dataFrom and a template", syncWithDataFromTemplate),
 		Entry("should set error condition when provider errors", providerErrCondition),
 		Entry("should set an error condition when store does not exist", storeMissingErrCondition),

+ 3 - 0
pkg/provider/vault/vault.go

@@ -429,6 +429,9 @@ func (v *client) listSecrets(ctx context.Context, path string) ([]string, error)
 		return nil, err
 	}
 	secret, err := v.logical.ListWithContext(ctx, url)
+	if secret == nil {
+		return nil, fmt.Errorf("provided path %v does not contain any secrets", url)
+	}
 	if err != nil {
 		return nil, fmt.Errorf(errReadSecret, err)
 	}

+ 46 - 0
pkg/utils/utils.go

@@ -24,6 +24,7 @@ import (
 	"net"
 	"net/url"
 	"reflect"
+	"regexp"
 	"strings"
 	"time"
 	"unicode"
@@ -40,6 +41,34 @@ func MergeByteMap(dst, src map[string][]byte) map[string][]byte {
 	return dst
 }
 
+func RewriteMap(operations []esv1beta1.ExternalSecretRewrite, in map[string][]byte) (map[string][]byte, error) {
+	out := in
+	var err error
+	for i, op := range operations {
+		if op.Regexp != nil {
+			out, err = RewriteRegexp(*op.Regexp, out)
+			if err != nil {
+				return nil, fmt.Errorf("failed rewriting operation[%v]: %w", i, err)
+			}
+		}
+	}
+	return out, nil
+}
+
+// RewriteRegexp rewrites a single Regexp Rewrite Operation.
+func RewriteRegexp(operation esv1beta1.ExternalSecretRewriteRegexp, in map[string][]byte) (map[string][]byte, error) {
+	out := make(map[string][]byte)
+	re, err := regexp.Compile(operation.Source)
+	if err != nil {
+		return nil, err
+	}
+	for key, value := range in {
+		newKey := re.ReplaceAllString(key, operation.Target)
+		out[newKey] = value
+	}
+	return out, nil
+}
+
 // DecodeValues decodes values from a secretMap.
 func DecodeMap(strategy esv1beta1.ExternalSecretDecodingStrategy, in map[string][]byte) (map[string][]byte, error) {
 	out := make(map[string][]byte, len(in))
@@ -87,6 +116,21 @@ func Decode(strategy esv1beta1.ExternalSecretDecodingStrategy, in []byte) ([]byt
 	}
 }
 
+func ValidateKeys(in map[string][]byte) bool {
+	for key := range in {
+		for _, v := range key {
+			if !unicode.IsNumber(v) &&
+				!unicode.IsLetter(v) &&
+				v != '-' &&
+				v != '.' &&
+				v != '_' {
+				return false
+			}
+		}
+	}
+	return true
+}
+
 // ConvertKeys converts a secret map into a valid key.
 // Replaces any non-alphanumeric characters depending on convert strategy.
 func ConvertKeys(strategy esv1beta1.ExternalSecretConversionStrategy, in map[string][]byte) (map[string][]byte, error) {
@@ -115,6 +159,8 @@ func convert(strategy esv1beta1.ExternalSecretConversionStrategy, str string) st
 				newName[rk] = "_"
 			case esv1beta1.ExternalSecretConversionUnicode:
 				newName[rk] = fmt.Sprintf("_U%04x_", rv)
+			default:
+				newName[rk] = string(rv)
 			}
 		} else {
 			newName[rk] = string(rv)

+ 158 - 0
pkg/utils/utils_test.go

@@ -335,3 +335,161 @@ func TestValidate(t *testing.T) {
 		t.Errorf("Connection problem: %v", err)
 	}
 }
+
+func TestRewriteRegexp(t *testing.T) {
+	type args struct {
+		operations []esv1beta1.ExternalSecretRewrite
+		in         map[string][]byte
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    map[string][]byte
+		wantErr bool
+	}{
+		{
+			name: "replace of a single key",
+			args: args{
+				operations: []esv1beta1.ExternalSecretRewrite{
+					{
+						Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+							Source: "-",
+							Target: "_",
+						},
+					},
+				},
+				in: map[string][]byte{
+					"foo-bar": []byte("bar"),
+				},
+			},
+			want: map[string][]byte{
+				"foo_bar": []byte("bar"),
+			},
+		},
+		{
+			name: "no operation",
+			args: args{
+				operations: []esv1beta1.ExternalSecretRewrite{
+					{
+						Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+							Source: "hello",
+							Target: "world",
+						},
+					},
+				},
+				in: map[string][]byte{
+					"foo": []byte("bar"),
+				},
+			},
+			want: map[string][]byte{
+				"foo": []byte("bar"),
+			},
+		},
+		{
+			name: "removing prefix from keys",
+			args: args{
+				operations: []esv1beta1.ExternalSecretRewrite{
+					{
+						Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+							Source: "^my/initial/path/",
+							Target: "",
+						},
+					},
+				},
+				in: map[string][]byte{
+					"my/initial/path/foo": []byte("bar"),
+				},
+			},
+			want: map[string][]byte{
+				"foo": []byte("bar"),
+			},
+		},
+		{
+			name: "using un-named capture groups",
+			args: args{
+				operations: []esv1beta1.ExternalSecretRewrite{
+					{
+						Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+							Source: "f(.*)o",
+							Target: "a_new_path_$1",
+						},
+					},
+				},
+				in: map[string][]byte{
+					"foo":      []byte("bar"),
+					"foodaloo": []byte("barr"),
+				},
+			},
+			want: map[string][]byte{
+				"a_new_path_o":      []byte("bar"),
+				"a_new_path_oodalo": []byte("barr"),
+			},
+		},
+		{
+			name: "using named and numbered capture groups",
+			args: args{
+				operations: []esv1beta1.ExternalSecretRewrite{
+					{
+						Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+							Source: "f(?P<content>.*)o",
+							Target: "a_new_path_${content}_${1}",
+						},
+					},
+				},
+				in: map[string][]byte{
+					"foo":  []byte("bar"),
+					"floo": []byte("barr"),
+				},
+			},
+			want: map[string][]byte{
+				"a_new_path_o_o":   []byte("bar"),
+				"a_new_path_lo_lo": []byte("barr"),
+			},
+		},
+		{
+			name: "using sequenced rewrite operations",
+			args: args{
+				operations: []esv1beta1.ExternalSecretRewrite{
+					{
+						Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+							Source: "my/(.*?)/bar/(.*)",
+							Target: "$1-$2",
+						},
+					},
+					{
+						Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+							Source: "-",
+							Target: "_",
+						},
+					},
+					{
+						Regexp: &esv1beta1.ExternalSecretRewriteRegexp{
+							Source: "ass",
+							Target: "***",
+						},
+					},
+				},
+				in: map[string][]byte{
+					"my/app/bar/key":      []byte("bar"),
+					"my/app/bar/password": []byte("barr"),
+				},
+			},
+			want: map[string][]byte{
+				"app_key":      []byte("bar"),
+				"app_p***word": []byte("barr"),
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := RewriteMap(tt.args.operations, tt.args.in)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("RewriteMap() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("RewriteMap() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}