forked from baron/baron-sso
- 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
201 lines
4.5 KiB
Go
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
|
|
}
|