Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions mdl/executor/cmd_security_write.go
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,11 @@ func (e *Executor) execCreateDemoUser(s *ast.CreateDemoUserStmt) error {
return fmt.Errorf("failed to read project security: %w", err)
}

// Validate password against project password policy
if err := ps.PasswordPolicy.ValidatePassword(s.Password); err != nil {
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)
}

// Check if user already exists
for _, du := range ps.DemoUsers {
if du.UserName == s.UserName {
Expand Down
59 changes: 59 additions & 0 deletions sdk/security/security.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
package security

import (
"fmt"
"unicode"

"github.com/mendixlabs/mxcli/model"
)

Expand Down Expand Up @@ -55,6 +58,62 @@ type PasswordPolicy struct {
RequireSymbol bool `json:"requireSymbol"`
}

// ValidatePassword checks a password against the policy.
// Returns nil if the password is compliant, or an error describing the first violation.
// A nil policy accepts any password.
func (p *PasswordPolicy) ValidatePassword(password string) error {
if p == nil {
return nil
}
if p.MinimumLength > 0 && len(password) < p.MinimumLength {
return fmt.Errorf("password must be at least %d characters (got %d)", p.MinimumLength, len(password))
}
if p.RequireDigit && !containsDigit(password) {
return fmt.Errorf("password must contain at least one digit")
}
if p.RequireMixedCase && !containsMixedCase(password) {
return fmt.Errorf("password must contain both uppercase and lowercase letters")
}
if p.RequireSymbol && !containsSymbol(password) {
return fmt.Errorf("password must contain at least one symbol")
}
return nil
}

func containsDigit(s string) bool {
for _, r := range s {
if unicode.IsDigit(r) {
return true
}
}
return false
}

func containsMixedCase(s string) bool {
hasUpper, hasLower := false, false
for _, r := range s {
if unicode.IsUpper(r) {
hasUpper = true
}
if unicode.IsLower(r) {
hasLower = true
}
if hasUpper && hasLower {
return true
}
}
return false
}

func containsSymbol(s string) bool {
for _, r := range s {
if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
return true
}
}
return false
}

// ModuleSecurity represents the security configuration for a module.
type ModuleSecurity struct {
model.BaseElement
Expand Down
49 changes: 49 additions & 0 deletions sdk/security/security_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: Apache-2.0

package security

import "testing"

func TestPasswordPolicy_ValidatePassword(t *testing.T) {
policy := &PasswordPolicy{
MinimumLength: 8,
RequireDigit: true,
RequireMixedCase: true,
RequireSymbol: true,
}

tests := []struct {
name string
password string
wantErr bool
}{
{"valid", "Passw0rd!", false},
{"too short", "Pa0!", true},
{"no digit", "Password!", true},
{"no mixed case", "passw0rd!", true},
{"no symbol", "Passw0rdd", true},
{"empty", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := policy.ValidatePassword(tt.password)
if (err != nil) != tt.wantErr {
t.Errorf("ValidatePassword(%q) error = %v, wantErr %v", tt.password, err, tt.wantErr)
}
})
}
}

func TestPasswordPolicy_ValidatePassword_NilPolicy(t *testing.T) {
var policy *PasswordPolicy
if err := policy.ValidatePassword("anything"); err != nil {
t.Errorf("nil policy should accept any password, got: %v", err)
}
}

func TestPasswordPolicy_ValidatePassword_ZeroPolicy(t *testing.T) {
policy := &PasswordPolicy{}
if err := policy.ValidatePassword("x"); err != nil {
t.Errorf("zero policy should accept any password, got: %v", err)
}
}
Loading