1
0
forked from baron/baron-sso
Files
baron-sso/backend/internal/utils/password_policy.go
chan 31d107ff2e feat(user): support fixed UUID registration and enhance bulk import results
- Added support for fixed UUIDs during bulk registration (Search-first + ExternalID mapping)
- Implemented idempotency and visibility restoration for soft-deleted users
- Enhanced bulk upload UI to show 'New/Updated/Unchanged' status and modified fields
- Added logic to reclaim identifiers (login_id) from colliding records
- Added frontend E2E and backend unit tests for UUID integrity and conflict handling
- Fixed i18n, formatting, and mock tests to satisfy code-check
- Applied 'go fix' for 'omitzero' tags and general Go standards
2026-06-01 15:34:08 +09:00

201 lines
4.5 KiB
Go

package utils
import (
"baron-sso-backend/internal/domain"
"crypto/rand"
"fmt"
"slices"
"strings"
)
const (
lowercaseChars = "abcdefghijklmnopqrstuvwxyz"
uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
numberChars = "0123456789"
symbolChars = "!@#$%^&*()-_=+[]{}<>?/,.~"
)
// ValidatePasswordWithPolicy validates a password against the given policy.
func ValidatePasswordWithPolicy(policy *domain.PasswordPolicy, password string) error {
if policy == nil {
return nil
}
if len(password) < policy.MinLength {
return fmt.Errorf("비밀번호는 최소 %d자 이상이어야 합니다", policy.MinLength)
}
hasLower := false
hasUpper := false
hasNumber := false
hasSymbol := false
types := 0
for _, ch := range password {
switch {
case ch >= 'a' && ch <= 'z':
hasLower = true
case ch >= 'A' && ch <= 'Z':
hasUpper = true
case ch >= '0' && ch <= '9':
hasNumber = true
default:
hasSymbol = true
}
}
if hasLower {
types++
}
if hasUpper {
types++
}
if hasNumber {
types++
}
if hasSymbol {
types++
}
if policy.MinCharacterTypes > 0 && types < policy.MinCharacterTypes {
return fmt.Errorf("비밀번호는 영문 대/소문자/숫자/특수문자 중 %d가지 이상을 포함해야 합니다", policy.MinCharacterTypes)
}
if policy.Lowercase && !hasLower {
return fmt.Errorf("비밀번호에 소문자가 포함되어야 합니다")
}
if policy.Uppercase && !hasUpper {
return fmt.Errorf("비밀번호에 대문자가 포함되어야 합니다")
}
if policy.Number && !hasNumber {
return fmt.Errorf("비밀번호에 숫자가 포함되어야 합니다")
}
if policy.NonAlphanumeric && !hasSymbol {
return fmt.Errorf("비밀번호에 특수문자가 포함되어야 합니다")
}
return nil
}
// GeneratePasswordWithPolicy creates a random password that satisfies the policy.
func GeneratePasswordWithPolicy(policy *domain.PasswordPolicy) (string, error) {
if policy == nil {
policy = &domain.PasswordPolicy{}
}
categories := []struct {
name string
required bool
chars string
}{
{name: "lower", required: policy.Lowercase, chars: lowercaseChars},
{name: "upper", required: policy.Uppercase, chars: uppercaseChars},
{name: "number", required: policy.Number, chars: numberChars},
{name: "symbol", required: policy.NonAlphanumeric, chars: symbolChars},
}
selected := make([]string, 0, len(categories))
required := make([]string, 0, len(categories))
for _, cat := range categories {
if cat.chars == "" {
continue
}
if cat.required {
required = append(required, cat.chars)
}
selected = append(selected, cat.chars)
}
if len(selected) == 0 {
selected = []string{lowercaseChars, uppercaseChars, numberChars, symbolChars}
}
additionalTypes := policy.MinCharacterTypes - len(required)
if additionalTypes > 0 {
pool := make([]string, 0, len(selected))
for _, cat := range selected {
isRequired := slices.Contains(required, cat)
if !isRequired {
pool = append(pool, cat)
}
}
for i := 0; i < additionalTypes && len(pool) > 0; i++ {
idx, err := randomIndex(len(pool))
if err != nil {
return "", err
}
required = append(required, pool[idx])
pool = append(pool[:idx], pool[idx+1:]...)
}
}
minLength := policy.MinLength
if minLength <= 0 {
minLength = 12
}
if minLength < len(required) {
minLength = len(required)
}
passwordRunes := make([]rune, 0, minLength)
for _, charset := range required {
ch, err := randomChar(charset)
if err != nil {
return "", err
}
passwordRunes = append(passwordRunes, ch)
}
var combined strings.Builder
for _, charset := range selected {
combined.WriteString(charset)
}
for len(passwordRunes) < minLength {
ch, err := randomChar(combined.String())
if err != nil {
return "", err
}
passwordRunes = append(passwordRunes, ch)
}
if err := shuffleRunes(passwordRunes); err != nil {
return "", err
}
return string(passwordRunes), nil
}
func randomIndex(max int) (int, error) {
if max <= 0 {
return 0, fmt.Errorf("invalid max")
}
b := make([]byte, 1)
for {
if _, err := rand.Read(b); err != nil {
return 0, err
}
if int(b[0]) < max*(256/max) {
return int(b[0]) % max, nil
}
}
}
func randomChar(chars string) (rune, error) {
idx, err := randomIndex(len(chars))
if err != nil {
return 0, err
}
return rune(chars[idx]), nil
}
func shuffleRunes(values []rune) error {
for i := len(values) - 1; i > 0; i-- {
j, err := randomIndex(i + 1)
if err != nil {
return err
}
values[i], values[j] = values[j], values[i]
}
return nil
}