add charset modifier to secret generation (#521)
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build is passing
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			since we need special chars in passwords for a recipe we are working on, i have added the option to specify a charset in the same way as the length can be setted. i did not change anything in the behaviour, so if length is not specified, the charset gets ignored whether it is there or not. you can specify the following: `charset=default` - Results in passgen.AlphabetDefault being used `charset=special` - Results in passgen.AlphabetSpecial being used `charset=safespecial` - Results in `!@#%^&*_-+=` being used (so it is AlphabetSpecial without the dollar sign) `charset=default,special` or `charset=special,default` - Results in passgen.AlphabetDefault + passgen.AlphabetSpecial being used `charset=default,safespecial` or `charset=safespecial,default` - Results in passgen.AlphabetDefault + `!@#%^&*_-+=` being used ((so it is AlphabetSpecial without the dollar sign) PR for the docs: toolshed/docs.coopcloud.tech#271 Co-authored-by: p4u1 <p4u1@noreply.git.coopcloud.tech> Reviewed-on: #521 Reviewed-by: p4u1 <p4u1@noreply.git.coopcloud.tech> Co-authored-by: Apfelwurm <Alexander@volzit.de> Co-committed-by: Apfelwurm <Alexander@volzit.de>
This commit is contained in:
		@ -33,6 +33,10 @@ type Secret struct {
 | 
			
		||||
	// variable. For Example:
 | 
			
		||||
	//   SECRET_FOO=v1 # length=12
 | 
			
		||||
	Length int
 | 
			
		||||
	// Charset comes from the charset modifier at the secret version environment
 | 
			
		||||
	// variable. For Example:
 | 
			
		||||
	//   SECRET_FOO=v1 # charset=default,special
 | 
			
		||||
	Charset string
 | 
			
		||||
	// RemoteName is the name of the secret on the server. For example:
 | 
			
		||||
	//   name: ${STACK_NAME}_test_pass_two_${SECRET_TEST_PASS_TWO_VERSION}
 | 
			
		||||
	// With the following:
 | 
			
		||||
@ -43,38 +47,38 @@ type Secret struct {
 | 
			
		||||
	RemoteName string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GeneratePasswords generates passwords.
 | 
			
		||||
func GeneratePasswords(count, length uint) ([]string, error) {
 | 
			
		||||
// GeneratePassword generates passwords.
 | 
			
		||||
func GeneratePassword(length uint, charset string) (string, error) {
 | 
			
		||||
	passwords, err := passgen.GeneratePasswords(
 | 
			
		||||
		count,
 | 
			
		||||
		1,
 | 
			
		||||
		length,
 | 
			
		||||
		passgen.AlphabetDefault,
 | 
			
		||||
		charset,
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Debugf("generated %s", strings.Join(passwords, ", "))
 | 
			
		||||
 | 
			
		||||
	return passwords, nil
 | 
			
		||||
	return passwords[0], nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GeneratePassphrases generates human readable and rememberable passphrases.
 | 
			
		||||
func GeneratePassphrases(count uint) ([]string, error) {
 | 
			
		||||
// GeneratePassphrase generates human readable and rememberable passphrases.
 | 
			
		||||
func GeneratePassphrase() (string, error) {
 | 
			
		||||
	passphrases, err := passgen.GeneratePassphrases(
 | 
			
		||||
		count,
 | 
			
		||||
		1,
 | 
			
		||||
		passgen.PassphraseWordCountDefault,
 | 
			
		||||
		rune('-'),
 | 
			
		||||
		passgen.PassphraseCasingDefault,
 | 
			
		||||
		passgen.WordListDefault,
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Debugf("generated %s", strings.Join(passphrases, ", "))
 | 
			
		||||
 | 
			
		||||
	return passphrases, nil
 | 
			
		||||
	return passphrases[0], nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ReadSecretsConfig reads secret names/versions from the recipe config. The
 | 
			
		||||
@ -150,6 +154,8 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
 | 
			
		||||
				}
 | 
			
		||||
				value.Length = length
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			value.Charset = resolveCharset(modifierValues["charset"])
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		secretValues[secretId] = value
 | 
			
		||||
@ -158,6 +164,22 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
 | 
			
		||||
	return secretValues, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// resolveCharset sets the passgen Alphabet required for a secret
 | 
			
		||||
func resolveCharset(input string) string {
 | 
			
		||||
	switch strings.ToLower(input) {
 | 
			
		||||
	case "special":
 | 
			
		||||
		return passgen.AlphabetSpecial
 | 
			
		||||
	case "safespecial":
 | 
			
		||||
		return "!@#%^&*_-+="
 | 
			
		||||
	case "default,special", "special,default":
 | 
			
		||||
		return passgen.AlphabetDefault + passgen.AlphabetSpecial
 | 
			
		||||
	case "default,safespecial", "safespecial,default":
 | 
			
		||||
		return passgen.AlphabetDefault + "!@#%^&*_-+="
 | 
			
		||||
	default:
 | 
			
		||||
		return passgen.AlphabetDefault // Fallback to default
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GenerateSecrets generates secrets locally and sends them to a remote server for storage.
 | 
			
		||||
func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server string) (map[string]string, error) {
 | 
			
		||||
	secretsGenerated := map[string]string{}
 | 
			
		||||
@ -173,13 +195,13 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
 | 
			
		||||
			log.Debugf("attempting to generate and store %s on %s", secret.RemoteName, server)
 | 
			
		||||
 | 
			
		||||
			if secret.Length > 0 {
 | 
			
		||||
				passwords, err := GeneratePasswords(1, uint(secret.Length))
 | 
			
		||||
				password, err := GeneratePassword(uint(secret.Length), secret.Charset)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					ch <- err
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if err := client.StoreSecret(cl, secret.RemoteName, passwords[0], server); err != nil {
 | 
			
		||||
				if err := client.StoreSecret(cl, secret.RemoteName, password, server); err != nil {
 | 
			
		||||
					if strings.Contains(err.Error(), "AlreadyExists") {
 | 
			
		||||
						log.Warnf("%s already exists", secret.RemoteName)
 | 
			
		||||
						ch <- nil
 | 
			
		||||
@ -191,15 +213,15 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
 | 
			
		||||
 | 
			
		||||
				mutex.Lock()
 | 
			
		||||
				defer mutex.Unlock()
 | 
			
		||||
				secretsGenerated[secretName] = passwords[0]
 | 
			
		||||
				secretsGenerated[secretName] = password
 | 
			
		||||
			} else {
 | 
			
		||||
				passphrases, err := GeneratePassphrases(1)
 | 
			
		||||
				passphrase, err := GeneratePassphrase()
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					ch <- err
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if err := client.StoreSecret(cl, secret.RemoteName, passphrases[0], server); err != nil {
 | 
			
		||||
				if err := client.StoreSecret(cl, secret.RemoteName, passphrase, server); err != nil {
 | 
			
		||||
					if strings.Contains(err.Error(), "AlreadyExists") {
 | 
			
		||||
						log.Warnf("%s already exists", secret.RemoteName)
 | 
			
		||||
						ch <- nil
 | 
			
		||||
@ -211,7 +233,7 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
 | 
			
		||||
 | 
			
		||||
				mutex.Lock()
 | 
			
		||||
				defer mutex.Unlock()
 | 
			
		||||
				secretsGenerated[secretName] = passphrases[0]
 | 
			
		||||
				secretsGenerated[secretName] = passphrase
 | 
			
		||||
			}
 | 
			
		||||
			ch <- nil
 | 
			
		||||
		}(n, v)
 | 
			
		||||
 | 
			
		||||
@ -17,16 +17,37 @@ func TestReadSecretsConfig(t *testing.T) {
 | 
			
		||||
	assert.Equal(t, "test_example_com_test_pass_one_v2", secretsFromConfig["test_pass_one"].RemoteName)
 | 
			
		||||
	assert.Equal(t, "v2", secretsFromConfig["test_pass_one"].Version)
 | 
			
		||||
	assert.Equal(t, 0, secretsFromConfig["test_pass_one"].Length)
 | 
			
		||||
	assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789", secretsFromConfig["test_pass_one"].Charset)
 | 
			
		||||
 | 
			
		||||
	// Has a length modifier
 | 
			
		||||
	assert.Equal(t, "test_example_com_test_pass_two_v1", secretsFromConfig["test_pass_two"].RemoteName)
 | 
			
		||||
	assert.Equal(t, "v1", secretsFromConfig["test_pass_two"].Version)
 | 
			
		||||
	assert.Equal(t, 10, secretsFromConfig["test_pass_two"].Length)
 | 
			
		||||
	assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789", secretsFromConfig["test_pass_two"].Charset)
 | 
			
		||||
 | 
			
		||||
	// Secret name does not include the secret id
 | 
			
		||||
	assert.Equal(t, "test_example_com_pass_three_v2", secretsFromConfig["test_pass_three"].RemoteName)
 | 
			
		||||
	assert.Equal(t, "v2", secretsFromConfig["test_pass_three"].Version)
 | 
			
		||||
	assert.Equal(t, 0, secretsFromConfig["test_pass_three"].Length)
 | 
			
		||||
	assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789", secretsFromConfig["test_pass_three"].Charset)
 | 
			
		||||
 | 
			
		||||
	// Has a length modifier and a charset=default,safespecial modifier
 | 
			
		||||
	assert.Equal(t, "test_example_com_test_pass_four_v1", secretsFromConfig["test_pass_four"].RemoteName)
 | 
			
		||||
	assert.Equal(t, "v1", secretsFromConfig["test_pass_four"].Version)
 | 
			
		||||
	assert.Equal(t, 12, secretsFromConfig["test_pass_four"].Length)
 | 
			
		||||
	assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#%^&*_-+=", secretsFromConfig["test_pass_four"].Charset)
 | 
			
		||||
 | 
			
		||||
	// Has a length modifier and a charset=default,special modifier
 | 
			
		||||
	assert.Equal(t, "test_example_com_test_pass_five_v1", secretsFromConfig["test_pass_five"].RemoteName)
 | 
			
		||||
	assert.Equal(t, "v1", secretsFromConfig["test_pass_five"].Version)
 | 
			
		||||
	assert.Equal(t, 12, secretsFromConfig["test_pass_five"].Length)
 | 
			
		||||
	assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%^&*_-+=", secretsFromConfig["test_pass_five"].Charset)
 | 
			
		||||
 | 
			
		||||
	// Has only a charset=default,special modifier, which gets setted but ignored in the generation
 | 
			
		||||
	assert.Equal(t, "test_example_com_test_pass_six_v1", secretsFromConfig["test_pass_six"].RemoteName)
 | 
			
		||||
	assert.Equal(t, "v1", secretsFromConfig["test_pass_six"].Version)
 | 
			
		||||
	assert.Equal(t, 0, secretsFromConfig["test_pass_six"].Length)
 | 
			
		||||
	assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%^&*_-+=", secretsFromConfig["test_pass_six"].Charset)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestReadSecretsConfigWithLongDomain(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,6 @@
 | 
			
		||||
SECRET_TEST_PASS_ONE_VERSION=v2
 | 
			
		||||
SECRET_TEST_PASS_TWO_VERSION=v1 # length=10
 | 
			
		||||
SECRET_TEST_PASS_THREE_VERSION=v2
 | 
			
		||||
SECRET_TEST_PASS_FOUR_VERSION=v1 # length=12 charset=default,safespecial
 | 
			
		||||
SECRET_TEST_PASS_FIVE_VERSION=v1 # length=12 charset=default,special
 | 
			
		||||
SECRET_TEST_PASS_SIX_VERSION=v1 # charset=default,special
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,9 @@ services:
 | 
			
		||||
      - test_pass_one
 | 
			
		||||
      - test_pass_two
 | 
			
		||||
      - test_pass_three
 | 
			
		||||
      - test_pass_four
 | 
			
		||||
      - test_pass_five
 | 
			
		||||
      - test_pass_six
 | 
			
		||||
 | 
			
		||||
secrets:
 | 
			
		||||
  test_pass_one:
 | 
			
		||||
@ -19,3 +22,12 @@ secrets:
 | 
			
		||||
  test_pass_three:
 | 
			
		||||
    external: true
 | 
			
		||||
    name: ${STACK_NAME}_pass_three_${SECRET_TEST_PASS_THREE_VERSION} # secretId and name don't match
 | 
			
		||||
  test_pass_four:
 | 
			
		||||
    external: true
 | 
			
		||||
    name: ${STACK_NAME}_test_pass_four_${SECRET_TEST_PASS_FOUR_VERSION}
 | 
			
		||||
  test_pass_five:
 | 
			
		||||
    external: true
 | 
			
		||||
    name: ${STACK_NAME}_test_pass_five_${SECRET_TEST_PASS_FIVE_VERSION}
 | 
			
		||||
  test_pass_six:
 | 
			
		||||
    external: true
 | 
			
		||||
    name: ${STACK_NAME}_test_pass_six_${SECRET_TEST_PASS_SIX_VERSION}
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user