diff --git a/integration/secrets/testdata/generated/Acornfile b/integration/secrets/testdata/generated/Acornfile index 86e124e32..d3be5f2cc 100644 --- a/integration/secrets/testdata/generated/Acornfile +++ b/integration/secrets/testdata/generated/Acornfile @@ -7,9 +7,9 @@ jobs: { cmd: ["sh", "-c", "echo -n $PASS > /run/secrets/output"] } cronpass: { - pass - schedule: "* * * * * " - } + pass + schedule: "* * * * * " + } } secrets: { diff --git a/pkg/appdefinition/app.acorn b/pkg/appdefinition/app.acorn index e6fa95007..9274e622a 100644 --- a/pkg/appdefinition/app.acorn +++ b/pkg/appdefinition/app.acorn @@ -336,6 +336,16 @@ SecretBasicAuth: { SecretBase type: string == "basic" + params: { + // The character set used in the generated string + passwordCharacters: string || default "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%^&*_-=+" + // The length of the token to be generated + passwordLength: (int >= 0 && int <= 256) || default 16 + // The character set used in the generated string + usernameCharacters: string || default "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%^&*_-=+" + // The length of the token to be generated + usernameLength: (int >= 0 && int <= 256) || default 8 + } data?: { username?: string password?: string diff --git a/pkg/appdefinition/appdefinition_test.go b/pkg/appdefinition/appdefinition_test.go index be2353ec2..2e51a7055 100644 --- a/pkg/appdefinition/appdefinition_test.go +++ b/pkg/appdefinition/appdefinition_test.go @@ -1454,6 +1454,12 @@ secrets: { }, appSpec.Secrets["explicit"]) assert.Equal(t, v1.Secret{ Type: "basic", + Params: v1.NewGenericMap(map[string]any{ + "passwordCharacters": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%^&*_-=+", + "passwordLength": int64(16), + "usernameCharacters": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%^&*_-=+", + "usernameLength": int64(8), + }), Data: map[string]string{ "username": "bardata", "password": "barpass", diff --git a/pkg/controller/secrets/secret_test.go b/pkg/controller/secrets/secret_test.go index ce7d38295..cddfb4737 100644 --- a/pkg/controller/secrets/secret_test.go +++ b/pkg/controller/secrets/secret_test.go @@ -10,6 +10,7 @@ import ( "github.com/acorn-io/runtime/pkg/labels" "github.com/acorn-io/runtime/pkg/scheme" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -86,7 +87,8 @@ func TestBasic_Gen(t *testing.T) { }, AppSpec: v1.AppSpec{ Secrets: map[string]v1.Secret{ - "pass": {Type: "basic", + "pass": { + Type: "basic", Data: map[string]string{ // cue will populate empty string if not sent "username": "", @@ -108,8 +110,8 @@ func TestBasic_Gen(t *testing.T) { t.Fatal(err) } - assert.Len(t, resp.Client.Created, 2) - assert.Len(t, resp.Collected, 2) + require.Len(t, resp.Client.Created, 2) + require.Len(t, resp.Collected, 2) secret := resp.Client.Created[0].(*corev1.Secret) assert.Equal(t, "pass", secret.Labels[labels.AcornSecretName]) diff --git a/pkg/secrets/generate.go b/pkg/secrets/generate.go new file mode 100644 index 000000000..8dfeae2fa --- /dev/null +++ b/pkg/secrets/generate.go @@ -0,0 +1,36 @@ +package secrets + +import ( + "crypto/rand" + "math/big" +) + +const ( + defaultLength = 54 + defaultCharacterSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%^&*_-=+" +) + +// GenerateRandomSecret generates a random secret with the specified length and character set. +// If the length is less than 1, a default value of 54 will be used. +// If the character set is empty, a default value of "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%^&*_-=+" +// will be used. +func GenerateRandomSecret(length int, characterSet string) (string, error) { + if length < 1 { + length = defaultLength + } + if characterSet == "" { + characterSet = defaultCharacterSet + } + + // Generate a random secret by randomly selecting characters from the given character set. + secret := make([]byte, length) + for i := 0; i < length; i++ { + index, err := rand.Int(rand.Reader, big.NewInt(int64(len(characterSet)))) + if err != nil { + return "", err + } + secret[i] = characterSet[index.Int64()] + } + + return string(secret), nil +} diff --git a/pkg/secrets/generateSecret.go b/pkg/secrets/generateSecret.go deleted file mode 100644 index e42df5e6e..000000000 --- a/pkg/secrets/generateSecret.go +++ /dev/null @@ -1,32 +0,0 @@ -package secrets - -import ( - "crypto/rand" - "math/big" -) - -// GenerateRandomSecret generates a random secret with the specified length -// using a mix of uppercase letters, lowercase letters, numbers, and special characters. -func GenerateRandomSecret(length int) (string, error) { - const ( - uppercase = "ABCDEFGHJKLMNPQRSTUVWXYZ" - lowercase = "abcdefghijkmnopqrstuvwxyz" - numbers = "23456789" - special = "!#$%^&*_-=+" - ) - - // Create a pool of characters to choose from - pool := uppercase + lowercase + numbers + special - - // Generate a random secret by randomly selecting characters from the pool - secret := make([]byte, length) - for i := 0; i < length; i++ { - index, err := rand.Int(rand.Reader, big.NewInt(int64(len(pool)))) - if err != nil { - return "", err - } - secret[i] = pool[index.Int64()] - } - - return string(secret), nil -} diff --git a/pkg/secrets/secret.go b/pkg/secrets/secret.go index 112070682..15ceca386 100644 --- a/pkg/secrets/secret.go +++ b/pkg/secrets/secret.go @@ -249,15 +249,40 @@ func generateBasic(req router.Request, appInstance *v1.AppInstance, secretName s Type: v1.SecretTypeBasic, } - for i, key := range []string{corev1.BasicAuthUsernameKey, corev1.BasicAuthPasswordKey} { - if len(secret.Data[key]) == 0 { - v, err := GenerateRandomSecret(54) - v = v[:(i+1)*8] - if err != nil { + for _, keys := range []struct { + dataKey, lengthKey, charactersKey string + }{ + { + dataKey: corev1.BasicAuthUsernameKey, + lengthKey: "usernameLength", + charactersKey: "usernameCharacters", + }, + { + dataKey: corev1.BasicAuthPasswordKey, + lengthKey: "passwordLength", + charactersKey: "passwordCharacters", + }, + } { + if len(secret.Data[keys.dataKey]) > 0 { + // Explicitly set by user, don't generate + continue + } + + var length int64 + if lengthParam, ok := secretRef.Params.GetData()[keys.lengthKey]; ok { + var err error + if length, err = convert.ToNumber(lengthParam); err != nil { return nil, err } - secret.Data[key] = []byte(v) } + characters := convert.ToString(secretRef.Params.GetData()[keys.charactersKey]) + + v, err := GenerateRandomSecret(int(length), characters) + if err != nil { + return nil, err + } + + secret.Data[keys.dataKey] = []byte(v) } return updateOrCreate(req, existing, secret) diff --git a/pkg/server/registry/apigroups/acorn/secrets/strategy.go b/pkg/server/registry/apigroups/acorn/secrets/strategy.go index a34b6ae2e..12733ee44 100644 --- a/pkg/server/registry/apigroups/acorn/secrets/strategy.go +++ b/pkg/server/registry/apigroups/acorn/secrets/strategy.go @@ -18,12 +18,12 @@ func (d *defaultSecretGenerateStrategy) Create(ctx context.Context, object types // If the secret is of type 'basic' and data is empty, // default username and password values are set. if secret.Type == "basic" && secret.Data == nil { - username, err := sec.GenerateRandomSecret(8) + username, err := sec.GenerateRandomSecret(8, "") if err != nil { return nil, err } - password, err := sec.GenerateRandomSecret(16) + password, err := sec.GenerateRandomSecret(16, "") if err != nil { return nil, err }