From 1a76cae00668e25710e0ec844bc787de39d4ad4b Mon Sep 17 00:00:00 2001 From: Harold Cheng Date: Wed, 25 Jun 2025 15:26:43 +0800 Subject: [PATCH 1/3] a new api --- apis/apps/v1/types.go | 6 ++++++ config/crd/bases/apps.kubeblocks.io_clusters.yaml | 10 ++++++++++ .../bases/apps.kubeblocks.io_componentdefinitions.yaml | 5 +++++ config/crd/bases/apps.kubeblocks.io_components.yaml | 5 +++++ deploy/helm/crds/apps.kubeblocks.io_clusters.yaml | 10 ++++++++++ .../crds/apps.kubeblocks.io_componentdefinitions.yaml | 5 +++++ deploy/helm/crds/apps.kubeblocks.io_components.yaml | 5 +++++ 7 files changed, 46 insertions(+) diff --git a/apis/apps/v1/types.go b/apis/apps/v1/types.go index e5f12812ad2..e540d385442 100644 --- a/apis/apps/v1/types.go +++ b/apis/apps/v1/types.go @@ -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 diff --git a/config/crd/bases/apps.kubeblocks.io_clusters.yaml b/config/crd/bases/apps.kubeblocks.io_clusters.yaml index e3f2817e352..21c212f0ad3 100644 --- a/config/crd/bases/apps.kubeblocks.io_clusters.yaml +++ b/config/crd/bases/apps.kubeblocks.io_clusters.yaml @@ -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: |- @@ -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: |- diff --git a/config/crd/bases/apps.kubeblocks.io_componentdefinitions.yaml b/config/crd/bases/apps.kubeblocks.io_componentdefinitions.yaml index ca5173fb166..67e86437ccc 100644 --- a/config/crd/bases/apps.kubeblocks.io_componentdefinitions.yaml +++ b/config/crd/bases/apps.kubeblocks.io_componentdefinitions.yaml @@ -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: |- diff --git a/config/crd/bases/apps.kubeblocks.io_components.yaml b/config/crd/bases/apps.kubeblocks.io_components.yaml index f3eb3260981..46c78de8b23 100644 --- a/config/crd/bases/apps.kubeblocks.io_components.yaml +++ b/config/crd/bases/apps.kubeblocks.io_components.yaml @@ -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: |- diff --git a/deploy/helm/crds/apps.kubeblocks.io_clusters.yaml b/deploy/helm/crds/apps.kubeblocks.io_clusters.yaml index e3f2817e352..21c212f0ad3 100644 --- a/deploy/helm/crds/apps.kubeblocks.io_clusters.yaml +++ b/deploy/helm/crds/apps.kubeblocks.io_clusters.yaml @@ -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: |- @@ -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: |- diff --git a/deploy/helm/crds/apps.kubeblocks.io_componentdefinitions.yaml b/deploy/helm/crds/apps.kubeblocks.io_componentdefinitions.yaml index ca5173fb166..67e86437ccc 100644 --- a/deploy/helm/crds/apps.kubeblocks.io_componentdefinitions.yaml +++ b/deploy/helm/crds/apps.kubeblocks.io_componentdefinitions.yaml @@ -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: |- diff --git a/deploy/helm/crds/apps.kubeblocks.io_components.yaml b/deploy/helm/crds/apps.kubeblocks.io_components.yaml index f3eb3260981..46c78de8b23 100644 --- a/deploy/helm/crds/apps.kubeblocks.io_components.yaml +++ b/deploy/helm/crds/apps.kubeblocks.io_components.yaml @@ -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: |- From 7421dc68f234a7d4adc0072dab8fb95dc9ae44fd Mon Sep 17 00:00:00 2001 From: Harold Cheng Date: Wed, 25 Jun 2025 16:01:47 +0800 Subject: [PATCH 2/3] implementation --- .../transformer_cluster_sharding_account.go | 18 ++-------- .../transformer_component_account.go | 18 ++-------- pkg/common/password.go | 33 +++++++++++++++--- pkg/common/password_test.go | 34 +++++++++++++++---- 4 files changed, 60 insertions(+), 43 deletions(-) diff --git a/controllers/apps/cluster/transformer_cluster_sharding_account.go b/controllers/apps/cluster/transformer_cluster_sharding_account.go index 2bb1b047d34..b3578ff5966 100644 --- a/controllers/apps/cluster/transformer_cluster_sharding_account.go +++ b/controllers/apps/cluster/transformer_cluster_sharding_account.go @@ -21,7 +21,6 @@ package cluster import ( "fmt" - "strings" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -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 ( diff --git a/controllers/apps/component/transformer_component_account.go b/controllers/apps/component/transformer_component_account.go index acf42da7eac..0aa9ecf4eae 100644 --- a/controllers/apps/component/transformer_component_account.go +++ b/controllers/apps/component/transformer_component_account.go @@ -22,7 +22,6 @@ package component import ( "fmt" "reflect" - "strings" "golang.org/x/crypto/bcrypt" "golang.org/x/exp/maps" @@ -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) diff --git a/pkg/common/password.go b/pkg/common/password.go index 93dc49c4769..305ddebd012 100644 --- a/pkg/common/password.go +++ b/pkg/common/password.go @@ -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 { @@ -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, }) @@ -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)) diff --git a/pkg/common/password_test.go b/pkg/common/password_test.go index 45d86a18f8d..c8b834f205c 100644 --- a/pkg/common/password_test.go +++ b/pkg/common/password_test.go @@ -20,6 +20,7 @@ along with this program. If not, see . package common import ( + "strings" "testing" "github.com/sethvargo/go-password/password" @@ -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) } @@ -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) } @@ -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) } @@ -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) } From a69e900bba05ee655bce9988b03244e32baaf8c5 Mon Sep 17 00:00:00 2001 From: Harold Cheng Date: Wed, 25 Jun 2025 16:11:22 +0800 Subject: [PATCH 3/3] api-doc --- docs/developer_docs/api-reference/cluster.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/developer_docs/api-reference/cluster.md b/docs/developer_docs/api-reference/cluster.md index 76680a3b82b..4b4dafbc58a 100644 --- a/docs/developer_docs/api-reference/cluster.md +++ b/docs/developer_docs/api-reference/cluster.md @@ -8634,6 +8634,19 @@ int32 +symbolCharacters
+ +string + + + +(Optional) +

The set of symbols allowed when generating password. If empty, kubeblocks will +use a default symbol set, which is “!@#&*”.

+ + + + letterCase