Browse Source

feat(generator): Password generator can generate and expose multiple passwords (#5669)

* Password generator can generate multiple passwords

Signed-off-by: Wojciech Trojanowski <W.Trojan27@gmail.com>

* Update docs and tests

Signed-off-by: Wojciech Trojanowski <W.Trojan27@gmail.com>

---------

Signed-off-by: Wojciech Trojanowski <W.Trojan27@gmail.com>
Co-authored-by: Gergely Brautigam <skarlso777@gmail.com>
Wojciech Trojanowski 4 months ago
parent
commit
883162d2bc

+ 7 - 0
apis/generators/v1alpha1/types_password.go

@@ -47,6 +47,13 @@ type PasswordSpec struct {
 	// +kubebuilder:default=false
 	AllowRepeat bool `json:"allowRepeat"`
 
+	// SecretKeys defines the keys that will be populated with generated passwords.
+	// Defaults to "password" when not set.
+	// +optional
+	// +kubebuilder:validation:MinItems=1
+	// +kubebuilder:validation:Items:MinLength=1
+	SecretKeys []string `json:"secretKeys,omitempty"`
+
 	// Encoding specifies the encoding of the generated password.
 	// Valid values are:
 	// - "raw" (default): no encoding

+ 5 - 0
apis/generators/v1alpha1/zz_generated.deepcopy.go

@@ -1423,6 +1423,11 @@ func (in *PasswordSpec) DeepCopyInto(out *PasswordSpec) {
 		*out = new(string)
 		**out = **in
 	}
+	if in.SecretKeys != nil {
+		in, out := &in.SecretKeys, &out.SecretKeys
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
 	if in.Encoding != nil {
 		in, out := &in.Encoding, &out.Encoding
 		*out = new(string)

+ 8 - 0
config/crds/bases/generators.external-secrets.io_clustergenerators.yaml

@@ -899,6 +899,14 @@ spec:
                         default: false
                         description: Set NoUpper to disable uppercase characters
                         type: boolean
+                      secretKeys:
+                        description: |-
+                          SecretKeys defines the keys that will be populated with generated passwords.
+                          Defaults to "password" when not set.
+                        items:
+                          type: string
+                        minItems: 1
+                        type: array
                       symbolCharacters:
                         description: |-
                           SymbolCharacters specifies the special characters that should be used

+ 8 - 0
config/crds/bases/generators.external-secrets.io_passwords.yaml

@@ -82,6 +82,14 @@ spec:
                 default: false
                 description: Set NoUpper to disable uppercase characters
                 type: boolean
+              secretKeys:
+                description: |-
+                  SecretKeys defines the keys that will be populated with generated passwords.
+                  Defaults to "password" when not set.
+                items:
+                  type: string
+                minItems: 1
+                type: array
               symbolCharacters:
                 description: |-
                   SymbolCharacters specifies the special characters that should be used

+ 16 - 0
deploy/crds/bundle.yaml

@@ -24386,6 +24386,14 @@ spec:
                           default: false
                           description: Set NoUpper to disable uppercase characters
                           type: boolean
+                        secretKeys:
+                          description: |-
+                            SecretKeys defines the keys that will be populated with generated passwords.
+                            Defaults to "password" when not set.
+                          items:
+                            type: string
+                          minItems: 1
+                          type: array
                         symbolCharacters:
                           description: |-
                             SymbolCharacters specifies the special characters that should be used
@@ -26819,6 +26827,14 @@ spec:
                   default: false
                   description: Set NoUpper to disable uppercase characters
                   type: boolean
+                secretKeys:
+                  description: |-
+                    SecretKeys defines the keys that will be populated with generated passwords.
+                    Defaults to "password" when not set.
+                  items:
+                    type: string
+                  minItems: 1
+                  type: array
                 symbolCharacters:
                   description: |-
                     SymbolCharacters specifies the special characters that should be used

+ 1 - 1
docs/api/generator/password.md

@@ -7,7 +7,7 @@ The Password generator provides random passwords that you can feed into your app
 
 | Key      | Description            |
 | -------- | ---------------------- |
-| password | the generated password |
+| password | the generated password. If `spec.secretKeys` is set, each listed key is populated with its own unique password |
 
 ## Parameters
 

+ 26 - 0
docs/api/spec.md

@@ -26772,6 +26772,19 @@ bool
 </tr>
 <tr>
 <td>
+<code>secretKeys</code></br>
+<em>
+[]string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>SecretKeys defines the keys that will be populated with generated passwords.
+Defaults to &ldquo;password&rdquo; when not set.</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>encoding</code></br>
 <em>
 string
@@ -26882,6 +26895,19 @@ bool
 </tr>
 <tr>
 <td>
+<code>secretKeys</code></br>
+<em>
+[]string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>SecretKeys defines the keys that will be populated with generated passwords.
+Defaults to &ldquo;password&rdquo; when not set.</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>encoding</code></br>
 <em>
 string

+ 32 - 17
generators/v1/password/password.go

@@ -45,6 +45,7 @@ const (
 	errNoSpec    = "no config spec provided"
 	errParseSpec = "unable to parse spec: %w"
 	errGetToken  = "unable to get authorization token: %w"
+	errSecretKey = "secretKeys must be non-empty and unique"
 )
 
 type generateFunc func(
@@ -93,29 +94,44 @@ func (g *Generator) generate(jsonSpec *apiextensions.JSON, passGen generateFunc)
 	if res.Spec.Symbols != nil {
 		symbols = *res.Spec.Symbols
 	}
-	pass, err := passGen(
-		passLen,
-		symbols,
-		symbolCharacters,
-		digits,
-		res.Spec.NoUpper,
-		res.Spec.AllowRepeat,
-	)
-	if err != nil {
-		return nil, nil, err
-	}
 
-	// Apply encoding
 	encoding := "raw"
 	if res.Spec.Encoding != nil {
 		encoding = *res.Spec.Encoding
 	}
 
-	encodedPass := encodePassword([]byte(pass), encoding)
+	keys := res.Spec.SecretKeys
+	if len(keys) == 0 {
+		keys = []string{"password"}
+	}
+	seen := make(map[string]struct{}, len(keys))
+	for _, key := range keys {
+		if key == "" {
+			return nil, nil, errors.New(errSecretKey)
+		}
+		if _, ok := seen[key]; ok {
+			return nil, nil, errors.New(errSecretKey)
+		}
+		seen[key] = struct{}{}
+	}
 
-	return map[string][]byte{
-		"password": encodedPass,
-	}, nil, nil
+	passwords := make(map[string][]byte, len(keys))
+	for _, key := range keys {
+		pass, err := passGen(
+			passLen,
+			symbols,
+			symbolCharacters,
+			digits,
+			res.Spec.NoUpper,
+			res.Spec.AllowRepeat,
+		)
+		if err != nil {
+			return nil, nil, err
+		}
+		passwords[key] = encodePassword([]byte(pass), encoding)
+	}
+
+	return passwords, nil, nil
 }
 
 func generateSafePassword(
@@ -166,7 +182,6 @@ func parseSpec(data []byte) (*genv1alpha1.Password, error) {
 	return &spec, err
 }
 
-
 // NewGenerator creates a new Generator instance.
 func NewGenerator() genv1alpha1.Generator {
 	return &Generator{}

+ 56 - 0
generators/v1/password/password_test.go

@@ -160,6 +160,62 @@ func TestGenerate(t *testing.T) {
 			},
 			wantErr: false,
 		},
+		{
+			name: "secretKeys overrides default output key",
+			args: args{
+				jsonSpec: &apiextensions.JSON{
+					Raw: []byte(`{"spec":{"secretKeys":["custom"]}}`),
+				},
+				passGen: func(len int, symbols int, symbolCharacters string, digits int, noUpper bool, allowRepeat bool,
+				) (string, error) {
+					return "custom-pwd", nil
+				},
+			},
+			want: map[string][]byte{
+				"custom": []byte(`custom-pwd`),
+			},
+			wantErr: false,
+		},
+		{
+			name: "multiple secretKeys generate multiple passwords",
+			args: args{
+				jsonSpec: &apiextensions.JSON{
+					Raw: []byte(`{"spec":{"secretKeys":["first","second"]}}`),
+				},
+				passGen: func() func(int, int, string, int, bool, bool) (string, error) {
+					passwords := []string{"first-pass", "second-pass"}
+					idx := 0
+					return func(len int, symbols int, symbolCharacters string, digits int, noUpper bool, allowRepeat bool) (string, error) {
+						p := passwords[idx]
+						idx++
+						return p, nil
+					}
+				}(),
+			},
+			want: map[string][]byte{
+				"first":  []byte(`first-pass`),
+				"second": []byte(`second-pass`),
+			},
+			wantErr: false,
+		},
+		{
+			name: "empty secretKeys entry should error",
+			args: args{
+				jsonSpec: &apiextensions.JSON{
+					Raw: []byte(`{"spec":{"secretKeys":[""]}}`),
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "duplicate secretKeys entry should error",
+			args: args{
+				jsonSpec: &apiextensions.JSON{
+					Raw: []byte(`{"spec":{"secretKeys":["dup","dup"]}}`),
+				},
+			},
+			wantErr: true,
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {

+ 1 - 0
tests/__snapshot__/clustergenerator-v1alpha1.yaml

@@ -133,6 +133,7 @@ spec:
       encoding: "raw"
       length: 24
       noUpper: false
+      secretKeys: [string] # minItems 1 of type string
       symbolCharacters: string
       symbols: 1
     quayAccessTokenSpec:

+ 1 - 0
tests/__snapshot__/password-v1alpha1.yaml

@@ -7,5 +7,6 @@ spec:
   encoding: "raw"
   length: 24
   noUpper: false
+  secretKeys: [string] # minItems 1 of type string
   symbolCharacters: string
   symbols: 1