8000 feat: specify allowed symbols in password generation by cjc7373 · Pull Request #9480 · apecloud/kubeblocks · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat: specify allowed symbols in password generation #9480

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 26, 2025
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
6 changes: 6 additions & 0 deletions apis/apps/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,12 @@ type PasswordConfig struct {
// +optional
NumSymbols int32 `json:"numSymbols,omitempty"`

// The set of symbols allowed when generating password. If empty, kubeblocks will
// use a default symbol set, which is "!@#&*".
//
// +optional
SymbolCharacters string `json:"symbolCharacters,omitempty"`

// The case of the letters in the password.
//
// +kubebuilder:default=MixedCases
Expand Down
10 changes: 10 additions & 0 deletions config/crd/bases/apps.kubeblocks.io_clusters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4207,6 +4207,11 @@ spec:
Seed to generate the account's password.
Cannot be updated.
type: string
symbolCharacters:
description: |-
The set of symbols allowed when generating password. If empty, kubeblocks will
use a default symbol set, which is "!@#&*".
type: string
type: object
secretRef:
description: |-
Expand Down Expand Up @@ -11933,6 +11938,11 @@ spec:
Seed to generate the account's password.
Cannot be updated.
type: string
symbolCharacters:
description: |-
The set of symbols allowed when generating password. If empty, kubeblocks will
use a default symbol set, which is "!@#&*".
type: string
type: object
secretRef:
description: |-
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16658,6 +16658,11 @@ spec:
Seed to generate the account's password.
Cannot be updated.
type: string
symbolCharacters:
description: |-
The set of symbols allowed when generating password. If empty, kubeblocks will
use a default symbol set, which is "!@#&*".
type: string
type: object
statement:
description: |-
Expand Down
5 changes: 5 additions & 0 deletions config/crd/bases/apps.kubeblocks.io_components.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4403,6 +4403,11 @@ spec:
Seed to generate the account's password.
Cannot be updated.
type: string
symbolCharacters:
description: |-
The set of symbols allowed when generating password. If empty, kubeblocks will
use a default symbol set, which is "!@#&*".
type: string
type: object
secretRef:
description: |-
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ package cluster

import (
"fmt"
"strings"

corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
Expand Down Expand Up @@ -163,25 +162,12 @@ func (t *clusterShardingAccountTransformer) buildPassword(transCtx *clusterTrans
return nil, fmt.Errorf("failed to restore password for system account %s of shard %s from annotation", account.Name, shardingName)
}
if len(password) == 0 {
password = t.generatePassword(account)
password, err := common.GeneratePasswordByConfig(account.PasswordGenerationPolicy)
return []byte(password), err
}
return password, nil
}

func (t *clusterShardingAccountTransformer) generatePassword(account appsv1.SystemAccount) []byte {
config := account.PasswordGenerationPolicy
passwd, _ := common.GeneratePassword((int)(config.Length), (int)(config.NumDigits), (int)(config.NumSymbols), config.Seed)
switch config.LetterCase {
case appsv1.UpperCases:
passwd = strings.ToUpper(passwd)
case appsv1.LowerCases:
passwd = strings.ToLower(passwd)
case appsv1.MixedCases:
passwd, _ = common.EnsureMixedCase(passwd, config.Seed)
}
return []byte(passwd)
}

func (t *clusterShardingAccountTransformer) newAccountSecretWithPassword(transCtx *clusterTransformContext,
sharding *appsv1.ClusterSharding, accountName string, password []byte) (*corev1.Secret, error) {
var (
Expand Down
18 changes: 2 additions & 16 deletions controllers/apps/component/transformer_component_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ package component
import (
"fmt"
"reflect"
"strings"

"golang.org/x/crypto/bcrypt"
"golang.org/x/exp/maps"
Expand Down Expand Up @@ -219,25 +218,12 @@ func (t *componentAccountTransformer) buildPassword(ctx *componentTransformConte
password = []byte(factory.GetRestorePassword(ctx.SynthesizeComponent))
}
if len(password) == 0 {
return t.generatePassword(account), nil
password, err := common.GeneratePasswordByConfig(account.PasswordGenerationPolicy)
return []byte(password), err
}
return password, nil
}

func (t *componentAccountTransformer) generatePassword(account synthesizedSystemAccount) []byte {
config := account.PasswordGenerationPolicy
passwd, _ := common.GeneratePassword((int)(config.Length), (int)(config.NumDigits), (int)(config.NumSymbols), config.Seed)
switch config.LetterCase {
case appsv1.UpperCases:
passwd = strings.ToUpper(passwd)
case appsv1.LowerCases:
passwd = strings.ToLower(passwd)
case appsv1.MixedCases:
passwd, _ = common.EnsureMixedCase(passwd, config.Seed)
}
return []byte(passwd)
}

func (t *componentAccountTransformer) buildAccountSecretWithPassword(ctx *componentTransformContext,
synthesizeComp *component.SynthesizedComponent, account synthesizedSystemAccount, password []byte) (*corev1.Secret, error) {
secretName := constant.GenerateAccountSecretName(synthesizeComp.ClusterName, synthesizeComp.Name, account.Name)
Expand Down
10 changes: 10 additions & 0 deletions deploy/helm/crds/apps.kubeblocks.io_clusters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4207,6 +4207,11 @@ spec:
Seed to generate the account's password.
Cannot be updated.
type: string
symbolCharacters:
description: |-
The set of symbols allowed when generating password. If empty, kubeblocks will
use a default symbol set, which is "!@#&*".
type: string
type: object
secretRef:
description: |-
Expand Down Expand Up @@ -11933,6 +11938,11 @@ spec:
Seed to generate the account's password.
Cannot be updated.
type: string
symbolCharacters:
description: |-
The set of symbols allowed when generating password. If empty, kubeblocks will
use a default symbol set, which is "!@#&*".
type: string
type: object
secretRef:
description: |-
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16658,6 +16658,11 @@ spec:
Seed to generate the account's password.
Cannot be updated.
type: string
symbolCharacters:
description: |-
The set of symbols allowed when generating password. If empty, kubeblocks will
use a default symbol set, which is "!@#&*".
type: string
type: object
statement:
description: |-
Expand Down
5 changes: 5 additions & 0 deletions deploy/helm/crds/apps.kubeblocks.io_components.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4403,6 +4403,11 @@ spec:
Seed to generate the account's password.
Cannot be updated.
type: string
symbolCharacters:
description: |-
The set of symbols allowed when generating password. If empty, kubeblocks will
use a default symbol set, which is "!@#&*".
type: string
type: object
secretRef:
description: |-
Expand Down
13 changes: 13 additions & 0 deletions docs/developer_docs/api-reference/cluster.md
Original file line number Diff line number Diff line change
Expand Up @@ -8634,6 +8634,19 @@ int32
</tr>
<tr>
<td>
<code>symbolCharacters</code><br/>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>The set of symbols allowed when generating password. If empty, kubeblocks will
use a default symbol set, which is &ldquo;!@#&amp;*&rdquo;.</p>
</td>
</tr>
<tr>
<td>
<code>letterCase</code><br/>
<em>
<a href="#apps.kubeblocks.io/v1.LetterCase">
Expand Down
33 changes: 28 additions & 5 deletions pkg/common/password.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,18 @@ import (
"crypto/sha256"
"encoding/binary"
mathrand "math/rand"
"strings"
"time"
"unicode"

"github.com/sethvargo/go-password/password"

appsv1 "github.com/apecloud/kubeblocks/apis/apps/v1"
)

const (
// Symbols is the list of symbols.
Symbols = "!@#&*"
// DefaultSymbols is the list of default symbols to generate password.
DefaultSymbols = "!@#&*"
)

type PasswordReader struct {
Expand All @@ -46,17 +49,36 @@ func (r *PasswordReader) Seed(seed int64) {
r.rand.Seed(seed)
}

func GeneratePasswordByConfig(config appsv1.PasswordConfig) (string, error) {
passwd, err := GeneratePassword((int)(config.Length), (int)(config.NumDigits), (int)(config.NumSymbols), config.Seed, config.SymbolCharacters)
if err != nil {
return "", err
}
switch config.LetterCase {
case appsv1.UpperCases:
passwd = strings.ToUpper(passwd)
case appsv1.LowerCases:
passwd = strings.ToLower(passwd)
case appsv1.MixedCases:
passwd, err = EnsureMixedCase(passwd, config.Seed)
}
return passwd, err
}

// GeneratePassword generates a password with the given requirements and seed in lowercase.
func GeneratePassword(length, numDigits, numSymbols int, seed string) (string, error) {
func GeneratePassword(length, numDigits, numSymbols int, seed string, symbols string) (string, error) {
rand, err := newRngFromSeed(seed)
if err != nil {
return "", err
}
passwordReader := &PasswordReader{rand: rand}
if symbols == "" {
symbols = DefaultSymbols
}
gen, err := password.NewGenerator(&password.GeneratorInput{
LowerLetters: password.LowerLetters,
UpperLetters: password.UpperLetters,
Symbols: Symbols,
Symbols: symbols,
Digits: password.Digits,
Reader: passwordReader,
})
Expand All @@ -67,7 +89,8 @@ func GeneratePassword(length, numDigits, numSymbols int, seed string) (string, e
}

// EnsureMixedCase randomizes the letter casing in the given string, ensuring
// that the result contains at least one uppercase and one lowercase letter
// that the result contains at least one uppercase and one lowercase letter.
// If the give string only has one letter, it is returned unmodified.
func EnsureMixedCase(in, seed string) (string, error) {
runes := []rune(in)
letterIndices := make([]int, 0, len(runes))
Expand Down
34 changes: 28 additions & 6 deletions pkg/common/password_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package common

import (
"strings"
"testing"

"github.com/sethvargo/go-password/password"
Expand All @@ -34,7 +35,7 @@ func testGeneratorGeneratePasswordWithSeed(t *testing.T) {
resultSeedFirstTime := ""
resultSeedEachTime := ""
for i := 0; i < N; i++ {
res, err := GeneratePassword(10, 5, 0, seed)
res, err := GeneratePassword(10, 5, 0, seed, "")
if err != nil {
t.Error(err)
}
Expand All @@ -52,22 +53,43 @@ func testGeneratorGeneratePassword(t *testing.T) {
t.Run("exceeds_length", func(t *testing.T) {
t.Parallel()

if _, err := GeneratePassword(0, 1, 0, ""); err != password.ErrExceedsTotalLength {
if _, err := GeneratePassword(0, 1, 0, "", ""); err != password.ErrExceedsTotalLength {
t.Errorf("expected %q to be %q", err, password.ErrExceedsTotalLength)
}

if _, err := GeneratePassword(0, 0, 1, ""); err != password.ErrExceedsTotalLength {
if _, err := GeneratePassword(0, 0, 1, "", ""); err != password.ErrExceedsTotalLength {
t.Errorf("expected %q to be %q", err, password.ErrExceedsTotalLength)
}
})

t.Run("should respect allowed symbols", func(t *testing.T) {
t.Parallel()

symbols := "!$_#"
for i := 0; i < N; i++ {
res, err := GeneratePassword(10, 0, 5, "", symbols)
if err != nil {
t.Error(err)
}
for _, r := range res {
if r >= '0' && r <= '9' || r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' {
continue
}
if !strings.ContainsRune(symbols, r) {
t.Errorf("unexpected symbol %q in password %q", r, res)
}
}
}

})

t.Run("should be different when seed is empty", func(t *testing.T) {
t.Parallel()
resultSeedFirstTime := ""
resultSeedEachTime := ""
hasDiffPassword := false
for i := 0; i < N; i++ {
res, err := GeneratePassword(i%(len(password.LowerLetters)+len(password.UpperLetters)), 0, 0, "")
res, err := GeneratePassword(i%(len(password.LowerLetters)+len(password.UpperLetters)), 0, 0, "", "")
if err != nil {
t.Error(err)
}
Expand Down Expand Up @@ -126,7 +148,7 @@ func TestGeneratorEnsureMixedCase(t *testing.T) {

// Generate multiple passwords and check they have both upper and lower letters.
for i := 0; i < 100; i++ {
pwd, err := GeneratePassword(length, numDigits, numSymbols, seed)
pwd, err := GeneratePassword(length, numDigits, numSymbols, seed, "")
if err != nil {
t.Fatalf("unexpected error generating password: %v", err)
}
Expand All @@ -148,7 +170,7 @@ func TestGeneratorEnsureMixedCase(t *testing.T) {

var firstPwd string
for i := 0; i < 50; i++ {
pwd, err := GeneratePassword(length, numDigits, numSymbols, seed)
pwd, err := GeneratePassword(length, numDigits, numSymbols, seed, "")
if err != nil {
t.Fatalf("unexpected error generating password with seed: %v", err)
}
Expand Down
0