Skip to content

Commit 8a2950f

Browse files
authored
Merge pull request #677 from dciabrin/custom-pw-validation
Enable custom password requirements and rejects
2 parents e3be8a4 + f38e6c8 commit 8a2950f

3 files changed

Lines changed: 154 additions & 31 deletions

File tree

modules/common/secret/password.go

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ import (
2222
)
2323

2424
// PasswordValidator implements the Validator interface
25-
type PasswordValidator struct{}
26-
27-
// Validate - implements the Validator interface and calls the underlying
28-
// ValidatePassword
29-
func (v PasswordValidator) Validate(value string) error {
30-
return ValidatePassword(value)
25+
// +kubebuilder:object:generate=false
26+
type PasswordValidator struct {
27+
// Patterns that the password should adhere to
28+
Requirements *[]Rule
29+
// Patterns that are forbidden for the password
30+
Rejects *[]Rule
3131
}
3232

3333
// Rule - pattern to match when rejecting or accepting a string
@@ -76,22 +76,32 @@ var (
7676
ErrPasswordRequirements = errors.New("password does not meet the requirements")
7777
)
7878

79-
// ValidatePassword validates a password against security requirements
80-
// Returns error when invalid/rejected
81-
func ValidatePassword(pwd string) error {
79+
// Validate - implements the Validator interface
80+
// If requirements or rejects rules are not specified in the
81+
// structure, the function uses the default rule set defined
82+
// in this package.
83+
func (v PasswordValidator) Validate(value string) error {
8284
// Check if password is empty
83-
if pwd == "" {
85+
if value == "" {
8486
return ErrEmptyPassword
8587
}
8688

87-
for _, rule := range requirements {
88-
if !rule.pattern.MatchString(pwd) {
89+
reqs := &requirements
90+
if v.Requirements != nil {
91+
reqs = v.Requirements
92+
}
93+
for _, rule := range *reqs {
94+
if !rule.pattern.MatchString(value) {
8995
return ErrPasswordRequirements
9096
}
9197
}
9298

93-
for _, rule := range rejects {
94-
if rule.pattern.MatchString(pwd) {
99+
rejs := &rejects
100+
if v.Rejects != nil {
101+
rejs = v.Rejects
102+
}
103+
for _, rule := range *rejs {
104+
if rule.pattern.MatchString(value) {
95105
return ErrPasswordRequirements
96106
}
97107
}

modules/common/secret/password_test.go

Lines changed: 130 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ limitations under the License.
1717
package secret
1818

1919
import (
20-
. "github.com/onsi/gomega" // nolint:revive
2120
"regexp"
2221
"slices"
2322
"testing"
23+
24+
. "github.com/onsi/gomega" // nolint:revive
2425
)
2526

2627
const ErrMsg string = "password does not meet the requirements"
@@ -279,12 +280,15 @@ func TestValidatePassword(t *testing.T) {
279280
alphaNumPattern,
280281
fernetPattern,
281282
)
283+
284+
validator := PasswordValidator{}
285+
282286
// Execute ValidatePassword against the generated TestVector
283287
for _, tt := range tests {
284288
t.Run(tt.name, func(t *testing.T) {
285289
g := NewWithT(t)
286290

287-
err := ValidatePassword(tt.password)
291+
err := validator.Validate(tt.password)
288292

289293
if tt.wantErr {
290294
g.Expect(err).To(HaveOccurred())
@@ -295,3 +299,127 @@ func TestValidatePassword(t *testing.T) {
295299
})
296300
}
297301
}
302+
303+
func TestValidatePasswordWithCustomRules(t *testing.T) {
304+
// Define custom requirements (strict rules)
305+
customRequirements := []Rule{
306+
{
307+
description: "Must only contain alphanumerical and safe special characters",
308+
pattern: *regexp.MustCompile(`^[a-zA-Z0-9@#%^*-_=+:,.!~]+$`),
309+
},
310+
}
311+
312+
// Define custom rejects (forbid specific patterns)
313+
customRejects := []Rule{
314+
{
315+
description: "Must not contain carriage return",
316+
pattern: *regexp.MustCompile(`[\n]`),
317+
},
318+
}
319+
320+
tests := []struct {
321+
name string
322+
validator PasswordValidator
323+
password string
324+
wantErr bool
325+
errMsg string
326+
}{
327+
// Test with nil rules (should use defaults)
328+
{
329+
name: "nil rules uses defaults - valid",
330+
validator: PasswordValidator{},
331+
password: "ValidPassword123",
332+
wantErr: false,
333+
},
334+
{
335+
name: "nil rules uses defaults - shell expansion rejected",
336+
validator: PasswordValidator{},
337+
password: "Password123$HOME",
338+
wantErr: true,
339+
errMsg: ErrMsg,
340+
},
341+
{
342+
name: "nil rules uses defaults - empty password rejected",
343+
validator: PasswordValidator{},
344+
password: "",
345+
wantErr: true,
346+
errMsg: "empty password not allowed",
347+
},
348+
349+
// Test with custom requirements only
350+
{
351+
name: "custom requirements - safe special characters",
352+
validator: PasswordValidator{
353+
Requirements: &customRequirements,
354+
},
355+
password: "#S3cure!Pass#",
356+
wantErr: false,
357+
},
358+
359+
// Test with custom rejects only
360+
{
361+
name: "custom rejects - must not contains '\n'",
362+
validator: PasswordValidator{
363+
Rejects: &customRejects,
364+
},
365+
password: "MyPASSWORD123\n",
366+
wantErr: true,
367+
errMsg: ErrMsg,
368+
},
369+
370+
// Test with both custom requirements and rejects
371+
{
372+
name: "custom both - fully valid password",
373+
validator: PasswordValidator{
374+
Requirements: &customRequirements,
375+
Rejects: &customRejects,
376+
},
377+
password: "MyS3cure!Pass",
378+
wantErr: false,
379+
},
380+
381+
// Test empty requirements/rejects slices
382+
{
383+
name: "empty requirements slice - allows any non-empty password",
384+
validator: PasswordValidator{
385+
Requirements: &[]Rule{},
386+
},
387+
password: "a",
388+
wantErr: false,
389+
},
390+
{
391+
name: "empty rejects slice - no rejections",
392+
validator: PasswordValidator{
393+
Rejects: &[]Rule{},
394+
},
395+
password: "anything$HOME$(cmd)`test`",
396+
wantErr: false,
397+
},
398+
{
399+
name: "both empty - allows any non-empty password",
400+
validator: PasswordValidator{
401+
Requirements: &[]Rule{},
402+
Rejects: &[]Rule{},
403+
},
404+
password: "x",
405+
wantErr: false,
406+
},
407+
}
408+
409+
for _, tt := range tests {
410+
t.Run(tt.name, func(t *testing.T) {
411+
g := NewWithT(t)
412+
413+
err := tt.validator.Validate(tt.password)
414+
415+
if tt.wantErr {
416+
g.Expect(err).To(HaveOccurred())
417+
if tt.errMsg != "" {
418+
g.Expect(err.Error()).To(ContainSubstring(tt.errMsg))
419+
}
420+
} else {
421+
g.Expect(err).ToNot(HaveOccurred())
422+
}
423+
})
424+
}
425+
}

modules/common/secret/zz_generated.deepcopy.go

Lines changed: 0 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)