Skip to content

Commit ac9c338

Browse files
authored
Merge pull request #140 from engalar/fix/demo-user-password-policy
fix(security): validate demo user password against project policy
2 parents 547ce4a + 25c9fcc commit ac9c338

File tree

3 files changed

+113
-0
lines changed

3 files changed

+113
-0
lines changed

mdl/executor/cmd_security_write.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -947,6 +947,11 @@ func (e *Executor) execCreateDemoUser(s *ast.CreateDemoUserStmt) error {
947947
return fmt.Errorf("failed to read project security: %w", err)
948948
}
949949

950+
// Validate password against project password policy
951+
if err := ps.PasswordPolicy.ValidatePassword(s.Password); err != nil {
952+
return fmt.Errorf("password policy violation for demo user '%s': %w\nhint: check your project's password policy with SHOW PROJECT SECURITY", s.UserName, err)
953+
}
954+
950955
// Check if user already exists
951956
for _, du := range ps.DemoUsers {
952957
if du.UserName == s.UserName {

sdk/security/security.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
package security
55

66
import (
7+
"fmt"
8+
"unicode"
9+
710
"github.com/mendixlabs/mxcli/model"
811
)
912

@@ -55,6 +58,62 @@ type PasswordPolicy struct {
5558
RequireSymbol bool `json:"requireSymbol"`
5659
}
5760

61+
// ValidatePassword checks a password against the policy.
62+
// Returns nil if the password is compliant, or an error describing the first violation.
63+
// A nil policy accepts any password.
64+
func (p *PasswordPolicy) ValidatePassword(password string) error {
65+
if p == nil {
66+
return nil
67+
}
68+
if p.MinimumLength > 0 && len(password) < p.MinimumLength {
69+
return fmt.Errorf("password must be at least %d characters (got %d)", p.MinimumLength, len(password))
70+
}
71+
if p.RequireDigit && !containsDigit(password) {
72+
return fmt.Errorf("password must contain at least one digit")
73+
}
74+
if p.RequireMixedCase && !containsMixedCase(password) {
75+
return fmt.Errorf("password must contain both uppercase and lowercase letters")
76+
}
77+
if p.RequireSymbol && !containsSymbol(password) {
78+
return fmt.Errorf("password must contain at least one symbol")
79+
}
80+
return nil
81+
}
82+
83+
func containsDigit(s string) bool {
84+
for _, r := range s {
85+
if unicode.IsDigit(r) {
86+
return true
87+
}
88+
}
89+
return false
90+
}
91+
92+
func containsMixedCase(s string) bool {
93+
hasUpper, hasLower := false, false
94+
for _, r := range s {
95+
if unicode.IsUpper(r) {
96+
hasUpper = true
97+
}
98+
if unicode.IsLower(r) {
99+
hasLower = true
100+
}
101+
if hasUpper && hasLower {
102+
return true
103+
}
104+
}
105+
return false
106+
}
107+
108+
func containsSymbol(s string) bool {
109+
for _, r := range s {
110+
if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
111+
return true
112+
}
113+
}
114+
return false
115+
}
116+
58117
// ModuleSecurity represents the security configuration for a module.
59118
type ModuleSecurity struct {
60119
model.BaseElement

sdk/security/security_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package security
4+
5+
import "testing"
6+
7+
func TestPasswordPolicy_ValidatePassword(t *testing.T) {
8+
policy := &PasswordPolicy{
9+
MinimumLength: 8,
10+
RequireDigit: true,
11+
RequireMixedCase: true,
12+
RequireSymbol: true,
13+
}
14+
15+
tests := []struct {
16+
name string
17+
password string
18+
wantErr bool
19+
}{
20+
{"valid", "Passw0rd!", false},
21+
{"too short", "Pa0!", true},
22+
{"no digit", "Password!", true},
23+
{"no mixed case", "passw0rd!", true},
24+
{"no symbol", "Passw0rdd", true},
25+
{"empty", "", true},
26+
}
27+
for _, tt := range tests {
28+
t.Run(tt.name, func(t *testing.T) {
29+
err := policy.ValidatePassword(tt.password)
30+
if (err != nil) != tt.wantErr {
31+
t.Errorf("ValidatePassword(%q) error = %v, wantErr %v", tt.password, err, tt.wantErr)
32+
}
33+
})
34+
}
35+
}
36+
37+
func TestPasswordPolicy_ValidatePassword_NilPolicy(t *testing.T) {
38+
var policy *PasswordPolicy
39+
if err := policy.ValidatePassword("anything"); err != nil {
40+
t.Errorf("nil policy should accept any password, got: %v", err)
41+
}
42+
}
43+
44+
func TestPasswordPolicy_ValidatePassword_ZeroPolicy(t *testing.T) {
45+
policy := &PasswordPolicy{}
46+
if err := policy.ValidatePassword("x"); err != nil {
47+
t.Errorf("zero policy should accept any password, got: %v", err)
48+
}
49+
}

0 commit comments

Comments
 (0)