github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/pkg/config/vault.go (about)

     1  package config
     2  
     3  import (
     4  	"os"
     5  	"path"
     6  	"regexp"
     7  	"strings"
     8  
     9  	"github.com/SAP/jenkins-library/pkg/config/interpolation"
    10  	"github.com/SAP/jenkins-library/pkg/log"
    11  	"github.com/SAP/jenkins-library/pkg/piperutils"
    12  	"github.com/SAP/jenkins-library/pkg/vault"
    13  	"github.com/hashicorp/vault/api"
    14  )
    15  
    16  const (
    17  	vaultRootPaths                      = "vaultRootPaths"
    18  	vaultTestCredentialPath             = "vaultTestCredentialPath"
    19  	vaultCredentialPath                 = "vaultCredentialPath"
    20  	vaultTestCredentialKeys             = "vaultTestCredentialKeys"
    21  	vaultCredentialKeys                 = "vaultCredentialKeys"
    22  	vaultAppRoleID                      = "vaultAppRoleID"
    23  	vaultAppRoleSecretID                = "vaultAppRoleSecreId"
    24  	vaultServerUrl                      = "vaultServerUrl"
    25  	vaultNamespace                      = "vaultNamespace"
    26  	vaultBasePath                       = "vaultBasePath"
    27  	vaultPipelineName                   = "vaultPipelineName"
    28  	vaultPath                           = "vaultPath"
    29  	skipVault                           = "skipVault"
    30  	vaultDisableOverwrite               = "vaultDisableOverwrite"
    31  	vaultTestCredentialEnvPrefix        = "vaultTestCredentialEnvPrefix"
    32  	vaultCredentialEnvPrefix            = "vaultCredentialEnvPrefix"
    33  	vaultTestCredentialEnvPrefixDefault = "PIPER_TESTCREDENTIAL_"
    34  	VaultCredentialEnvPrefixDefault     = "PIPER_VAULTCREDENTIAL_"
    35  	vaultSecretName                     = ".+VaultSecretName$"
    36  )
    37  
    38  var (
    39  	vaultFilter = []string{
    40  		vaultRootPaths,
    41  		vaultAppRoleID,
    42  		vaultAppRoleSecretID,
    43  		vaultServerUrl,
    44  		vaultNamespace,
    45  		vaultBasePath,
    46  		vaultPipelineName,
    47  		vaultPath,
    48  		skipVault,
    49  		vaultDisableOverwrite,
    50  		vaultTestCredentialPath,
    51  		vaultTestCredentialKeys,
    52  		vaultTestCredentialEnvPrefix,
    53  		vaultCredentialPath,
    54  		vaultCredentialKeys,
    55  		vaultCredentialEnvPrefix,
    56  		vaultSecretName,
    57  	}
    58  
    59  	// VaultRootPaths are the lookup paths piper tries to use during the vault lookup.
    60  	// A path is only used if it's variables can be interpolated from the config
    61  	VaultRootPaths = []string{
    62  		"$(vaultPath)",
    63  		"$(vaultBasePath)/$(vaultPipelineName)",
    64  		"$(vaultBasePath)/GROUP-SECRETS",
    65  	}
    66  
    67  	// VaultSecretFileDirectory holds the directory for the current step run to temporarily store secret files fetched from vault
    68  	VaultSecretFileDirectory = ""
    69  )
    70  
    71  // VaultCredentials hold all the auth information needed to fetch configuration from vault
    72  type VaultCredentials struct {
    73  	AppRoleID       string
    74  	AppRoleSecretID string
    75  	VaultToken      string
    76  }
    77  
    78  // vaultClient interface for mocking
    79  type vaultClient interface {
    80  	GetKvSecret(string) (map[string]string, error)
    81  	MustRevokeToken()
    82  }
    83  
    84  func (s *StepConfig) mixinVaultConfig(parameters []StepParameters, configs ...map[string]interface{}) {
    85  	for _, config := range configs {
    86  		s.mixIn(config, vaultFilter)
    87  		// when an empty filter is returned we skip the mixin call since an empty filter will allow everything
    88  		if referencesFilter := getFilterForResourceReferences(parameters); len(referencesFilter) > 0 {
    89  			s.mixIn(config, referencesFilter)
    90  		}
    91  	}
    92  }
    93  
    94  func getVaultClientFromConfig(config StepConfig, creds VaultCredentials) (vaultClient, error) {
    95  	address, addressOk := config.Config["vaultServerUrl"].(string)
    96  	// if vault isn't used it's not an error
    97  	if !addressOk || creds.VaultToken == "" && (creds.AppRoleID == "" || creds.AppRoleSecretID == "") {
    98  		log.Entry().Debug("Vault not configured")
    99  		return nil, nil
   100  	}
   101  	log.Entry().Info("Logging into Vault")
   102  	log.Entry().Debugf("  with URL %s", address)
   103  	namespace := ""
   104  	// namespaces are only available in vault enterprise so using them should be optional
   105  	if config.Config["vaultNamespace"] != nil {
   106  		namespace = config.Config["vaultNamespace"].(string)
   107  		log.Entry().Debugf("  with namespace %s", namespace)
   108  	}
   109  	var client vaultClient
   110  	var err error
   111  	clientConfig := &vault.Config{Config: &api.Config{Address: address}, Namespace: namespace}
   112  	if creds.VaultToken != "" {
   113  		log.Entry().Debugf("  with Token authentication")
   114  		client, err = vault.NewClient(clientConfig, creds.VaultToken)
   115  	} else {
   116  		log.Entry().Debugf("  with AppRole authentication")
   117  		client, err = vault.NewClientWithAppRole(clientConfig, creds.AppRoleID, creds.AppRoleSecretID)
   118  	}
   119  	if err != nil {
   120  		log.Entry().Info("  failed")
   121  		return nil, err
   122  	}
   123  	log.Entry().Info("  succeeded")
   124  	return client, nil
   125  }
   126  
   127  func resolveAllVaultReferences(config *StepConfig, client vaultClient, params []StepParameters) {
   128  	for _, param := range params {
   129  		if ref := param.GetReference("vaultSecret"); ref != nil {
   130  			resolveVaultReference(ref, config, client, param)
   131  		}
   132  		if ref := param.GetReference("vaultSecretFile"); ref != nil {
   133  			resolveVaultReference(ref, config, client, param)
   134  		}
   135  	}
   136  }
   137  
   138  func resolveVaultReference(ref *ResourceReference, config *StepConfig, client vaultClient, param StepParameters) {
   139  	vaultDisableOverwrite, _ := config.Config["vaultDisableOverwrite"].(bool)
   140  	if _, ok := config.Config[param.Name].(string); vaultDisableOverwrite && ok {
   141  		log.Entry().Debugf("Not fetching '%s' from Vault since it has already been set", param.Name)
   142  		return
   143  	}
   144  
   145  	log.Entry().Infof("Resolving '%s'", param.Name)
   146  
   147  	var secretValue *string
   148  	for _, vaultPath := range getSecretReferencePaths(ref, config.Config) {
   149  		// it should be possible to configure the root path were the secret is stored
   150  		vaultPath, ok := interpolation.ResolveString(vaultPath, config.Config)
   151  		if !ok {
   152  			continue
   153  		}
   154  
   155  		secretValue = lookupPath(client, vaultPath, &param)
   156  		if secretValue != nil {
   157  			log.Entry().Infof("  succeeded with Vault path '%s'", vaultPath)
   158  			if ref.Type == "vaultSecret" {
   159  				config.Config[param.Name] = *secretValue
   160  			} else if ref.Type == "vaultSecretFile" {
   161  				filePath, err := createTemporarySecretFile(param.Name, *secretValue)
   162  				if err != nil {
   163  					log.Entry().WithError(err).Warnf("Couldn't create temporary secret file for '%s'", param.Name)
   164  					return
   165  				}
   166  				config.Config[param.Name] = filePath
   167  			}
   168  			break
   169  		}
   170  	}
   171  	if secretValue == nil {
   172  		log.Entry().Warn("  failed")
   173  	}
   174  }
   175  
   176  func resolveVaultTestCredentialsWrapper(config *StepConfig, client vaultClient) {
   177  	log.Entry().Infof("Resolving test credentials wrapper")
   178  	resolveVaultTestCredentialsWrapperBase(config, client, vaultTestCredentialPath, vaultTestCredentialKeys, resolveVaultTestCredentials)
   179  }
   180  
   181  func resolveVaultCredentialsWrapper(config *StepConfig, client vaultClient) {
   182  	log.Entry().Infof("Resolving credentials wrapper")
   183  	resolveVaultTestCredentialsWrapperBase(config, client, vaultCredentialPath, vaultCredentialKeys, resolveVaultCredentials)
   184  }
   185  
   186  func resolveVaultTestCredentialsWrapperBase(
   187  	config *StepConfig, client vaultClient,
   188  	vaultCredPath, vaultCredKeys string,
   189  	resolveVaultCredentials func(config *StepConfig, client vaultClient),
   190  ) {
   191  	switch config.Config[vaultCredPath].(type) {
   192  	case string:
   193  		resolveVaultCredentials(config, client)
   194  	case []interface{}:
   195  		vaultCredentialPathCopy := config.Config[vaultCredPath]
   196  		vaultCredentialKeysCopy := config.Config[vaultCredKeys]
   197  
   198  		if _, ok := vaultCredentialKeysCopy.([]interface{}); !ok {
   199  			log.Entry().Debugf("  failed, unknown type of keys")
   200  			return
   201  		}
   202  
   203  		if len(vaultCredentialKeysCopy.([]interface{})) != len(vaultCredentialPathCopy.([]interface{})) {
   204  			log.Entry().Debugf("  failed, not same count of values and keys")
   205  			return
   206  		}
   207  
   208  		for i := 0; i < len(vaultCredentialPathCopy.([]interface{})); i++ {
   209  			config.Config[vaultCredPath] = vaultCredentialPathCopy.([]interface{})[i]
   210  			config.Config[vaultCredKeys] = vaultCredentialKeysCopy.([]interface{})[i]
   211  			resolveVaultCredentials(config, client)
   212  		}
   213  
   214  		config.Config[vaultCredPath] = vaultCredentialPathCopy
   215  		config.Config[vaultCredKeys] = vaultCredentialKeysCopy
   216  	default:
   217  		log.Entry().Debugf("  failed, unknown type of path")
   218  		return
   219  	}
   220  }
   221  
   222  // resolve test credential keys and expose as environment variables
   223  func resolveVaultTestCredentials(config *StepConfig, client vaultClient) {
   224  	credPath, pathOk := config.Config[vaultTestCredentialPath].(string)
   225  	keys := getTestCredentialKeys(config)
   226  	if !(pathOk && keys != nil) || credPath == "" || len(keys) == 0 {
   227  		log.Entry().Debugf("Not fetching test credentials from Vault since they are not (properly) configured")
   228  		return
   229  	}
   230  
   231  	lookupPath := make([]string, 3)
   232  	lookupPath[0] = "$(vaultPath)/" + credPath
   233  	lookupPath[1] = "$(vaultBasePath)/$(vaultPipelineName)/" + credPath
   234  	lookupPath[2] = "$(vaultBasePath)/GROUP-SECRETS/" + credPath
   235  
   236  	for _, path := range lookupPath {
   237  		vaultPath, ok := interpolation.ResolveString(path, config.Config)
   238  		if !ok {
   239  			continue
   240  		}
   241  
   242  		secret, err := client.GetKvSecret(vaultPath)
   243  		if err != nil {
   244  			log.Entry().WithError(err).Debugf("Couldn't fetch secret at '%s'", vaultPath)
   245  			continue
   246  		}
   247  		if secret == nil {
   248  			continue
   249  		}
   250  		secretsResolved := false
   251  		secretsResolved = populateTestCredentialsAsEnvs(config, secret, keys)
   252  		if secretsResolved {
   253  			// prevent overwriting resolved secrets
   254  			// only allows vault test credentials on one / the same vault path
   255  			break
   256  		}
   257  	}
   258  }
   259  
   260  func resolveVaultCredentials(config *StepConfig, client vaultClient) {
   261  	credPath, pathOk := config.Config[vaultCredentialPath].(string)
   262  	keys := getCredentialKeys(config)
   263  	if !(pathOk && keys != nil) || credPath == "" || len(keys) == 0 {
   264  		log.Entry().Debugf("Not fetching credentials from vault since they are not (properly) configured")
   265  		return
   266  	}
   267  
   268  	lookupPath := make([]string, 3)
   269  	lookupPath[0] = "$(vaultPath)/" + credPath
   270  	lookupPath[1] = "$(vaultBasePath)/$(vaultPipelineName)/" + credPath
   271  	lookupPath[2] = "$(vaultBasePath)/GROUP-SECRETS/" + credPath
   272  
   273  	for _, path := range lookupPath {
   274  		vaultPath, ok := interpolation.ResolveString(path, config.Config)
   275  		if !ok {
   276  			continue
   277  		}
   278  
   279  		secret, err := client.GetKvSecret(vaultPath)
   280  		if err != nil {
   281  			log.Entry().WithError(err).Debugf("Couldn't fetch secret at '%s'", vaultPath)
   282  			continue
   283  		}
   284  		if secret == nil {
   285  			continue
   286  		}
   287  		secretsResolved := false
   288  		secretsResolved = populateCredentialsAsEnvs(config, secret, keys)
   289  		if secretsResolved {
   290  			// prevent overwriting resolved secrets
   291  			// only allows vault test credentials on one / the same vault path
   292  			break
   293  		}
   294  	}
   295  }
   296  
   297  func populateTestCredentialsAsEnvs(config *StepConfig, secret map[string]string, keys []string) (matched bool) {
   298  
   299  	vaultTestCredentialEnvPrefix, ok := config.Config["vaultTestCredentialEnvPrefix"].(string)
   300  	if !ok || len(vaultTestCredentialEnvPrefix) == 0 {
   301  		vaultTestCredentialEnvPrefix = vaultTestCredentialEnvPrefixDefault
   302  	}
   303  	for secretKey, secretValue := range secret {
   304  		for _, key := range keys {
   305  			if secretKey == key {
   306  				log.RegisterSecret(secretValue)
   307  				envVariable := vaultTestCredentialEnvPrefix + ConvertEnvVar(secretKey)
   308  				log.Entry().Debugf("Exposing test credential '%v' as '%v'", key, envVariable)
   309  				os.Setenv(envVariable, secretValue)
   310  				matched = true
   311  			}
   312  		}
   313  	}
   314  	return
   315  }
   316  
   317  func populateCredentialsAsEnvs(config *StepConfig, secret map[string]string, keys []string) (matched bool) {
   318  
   319  	vaultCredentialEnvPrefix, ok := config.Config["vaultCredentialEnvPrefix"].(string)
   320  	isCredentialEnvPrefixDefault := false
   321  
   322  	if !ok {
   323  		vaultCredentialEnvPrefix = VaultCredentialEnvPrefixDefault
   324  		isCredentialEnvPrefixDefault = true
   325  	}
   326  	for secretKey, secretValue := range secret {
   327  		for _, key := range keys {
   328  			if secretKey == key {
   329  				log.RegisterSecret(secretValue)
   330  				envVariable := vaultCredentialEnvPrefix + ConvertEnvVar(secretKey)
   331  				log.Entry().Debugf("Exposing general purpose credential '%v' as '%v'", key, envVariable)
   332  				os.Setenv(envVariable, secretValue)
   333  
   334  				log.RegisterSecret(piperutils.EncodeString(secretValue))
   335  				envVariable = vaultCredentialEnvPrefix + ConvertEnvVar(secretKey) + "_BASE64"
   336  				log.Entry().Debugf("Exposing general purpose base64 encoded credential '%v' as '%v'", key, envVariable)
   337  				os.Setenv(envVariable, piperutils.EncodeString(secretValue))
   338  				matched = true
   339  			}
   340  		}
   341  	}
   342  
   343  	// we always create a standard env variable with the default prefx so that
   344  	// we can always refer to it in steps if its to be hard-coded
   345  	if !isCredentialEnvPrefixDefault {
   346  		for secretKey, secretValue := range secret {
   347  			for _, key := range keys {
   348  				if secretKey == key {
   349  					log.RegisterSecret(secretValue)
   350  					envVariable := VaultCredentialEnvPrefixDefault + ConvertEnvVar(secretKey)
   351  					log.Entry().Debugf("Exposing general purpose credential '%v' as '%v'", key, envVariable)
   352  					os.Setenv(envVariable, secretValue)
   353  
   354  					log.RegisterSecret(piperutils.EncodeString(secretValue))
   355  					envVariable = VaultCredentialEnvPrefixDefault + ConvertEnvVar(secretKey) + "_BASE64"
   356  					log.Entry().Debugf("Exposing general purpose base64 encoded credential '%v' as '%v'", key, envVariable)
   357  					os.Setenv(envVariable, piperutils.EncodeString(secretValue))
   358  					matched = true
   359  				}
   360  			}
   361  		}
   362  	}
   363  	return
   364  }
   365  
   366  func getTestCredentialKeys(config *StepConfig) []string {
   367  	keysRaw, ok := config.Config[vaultTestCredentialKeys].([]interface{})
   368  	if !ok {
   369  		return nil
   370  	}
   371  	keys := make([]string, 0, len(keysRaw))
   372  	for _, keyRaw := range keysRaw {
   373  		key, ok := keyRaw.(string)
   374  		if !ok {
   375  			log.Entry().Warnf("%s needs to be an array of strings", vaultTestCredentialKeys)
   376  			return nil
   377  		}
   378  		keys = append(keys, key)
   379  	}
   380  	return keys
   381  }
   382  
   383  func getCredentialKeys(config *StepConfig) []string {
   384  	keysRaw, ok := config.Config[vaultCredentialKeys].([]interface{})
   385  	if !ok {
   386  		log.Entry().Debugf("Not fetching general purpose credentials from vault since they are not (properly) configured")
   387  		return nil
   388  	}
   389  	keys := make([]string, 0, len(keysRaw))
   390  	for _, keyRaw := range keysRaw {
   391  		key, ok := keyRaw.(string)
   392  		if !ok {
   393  			log.Entry().Warnf("%s is needs to be an array of strings", vaultCredentialKeys)
   394  			return nil
   395  		}
   396  		keys = append(keys, key)
   397  	}
   398  	return keys
   399  }
   400  
   401  // ConvertEnvVar converts to a valid environment variable string
   402  func ConvertEnvVar(s string) string {
   403  	r := strings.ToUpper(s)
   404  	r = strings.ReplaceAll(r, "-", "_")
   405  	reg, err := regexp.Compile("[^a-zA-Z0-9_]*")
   406  	if err != nil {
   407  		log.Entry().Debugf("could not compile regex of convertEnvVar: %v", err)
   408  	}
   409  	replacedString := reg.ReplaceAllString(r, "")
   410  	return replacedString
   411  }
   412  
   413  // RemoveVaultSecretFiles removes all secret files that have been created during execution
   414  func RemoveVaultSecretFiles() {
   415  	if VaultSecretFileDirectory != "" {
   416  		os.RemoveAll(VaultSecretFileDirectory)
   417  	}
   418  }
   419  
   420  func createTemporarySecretFile(namePattern string, content string) (string, error) {
   421  	if VaultSecretFileDirectory == "" {
   422  		var err error
   423  		fileUtils := &piperutils.Files{}
   424  		VaultSecretFileDirectory, err = fileUtils.TempDir("", "vault")
   425  		if err != nil {
   426  			return "", err
   427  		}
   428  	}
   429  
   430  	file, err := os.CreateTemp(VaultSecretFileDirectory, namePattern)
   431  	if err != nil {
   432  		return "", err
   433  	}
   434  	defer file.Close()
   435  	_, err = file.WriteString(content)
   436  	if err != nil {
   437  		return "", err
   438  	}
   439  	return file.Name(), nil
   440  }
   441  
   442  func lookupPath(client vaultClient, path string, param *StepParameters) *string {
   443  	log.Entry().Debugf("  with Vault path '%s'", path)
   444  	secret, err := client.GetKvSecret(path)
   445  	if err != nil {
   446  		log.Entry().WithError(err).Warnf("Couldn't fetch secret at '%s'", path)
   447  		return nil
   448  	}
   449  	if secret == nil {
   450  		return nil
   451  	}
   452  
   453  	field := secret[param.Name]
   454  	if field != "" {
   455  		log.RegisterSecret(field)
   456  		return &field
   457  	}
   458  	log.Entry().Debugf("Secret did not contain a field name '%s'", param.Name)
   459  	// try parameter aliases
   460  	for _, alias := range param.Aliases {
   461  		log.Entry().Debugf("Trying alias field name '%s'", alias.Name)
   462  		field := secret[alias.Name]
   463  		if field != "" {
   464  			log.RegisterSecret(field)
   465  			if alias.Deprecated {
   466  				log.Entry().WithField("package", "SAP/jenkins-library/pkg/config").Warningf("DEPRECATION NOTICE: old step config key '%s' used in Vault. Please switch to '%s'!", alias.Name, param.Name)
   467  			}
   468  			return &field
   469  		}
   470  	}
   471  	return nil
   472  }
   473  
   474  func getSecretReferencePaths(reference *ResourceReference, config map[string]interface{}) []string {
   475  	retPaths := make([]string, 0, len(VaultRootPaths))
   476  	secretName := reference.Default
   477  	if providedName, ok := config[reference.Name].(string); ok && providedName != "" {
   478  		secretName = providedName
   479  	}
   480  	for _, rootPath := range VaultRootPaths {
   481  		fullPath := path.Join(rootPath, secretName)
   482  		retPaths = append(retPaths, fullPath)
   483  	}
   484  	return retPaths
   485  }
   486  
   487  func toStringSlice(interfaceSlice []interface{}) []string {
   488  	retSlice := make([]string, 0, len(interfaceSlice))
   489  	for _, vRaw := range interfaceSlice {
   490  		if v, ok := vRaw.(string); ok {
   491  			retSlice = append(retSlice, v)
   492  			continue
   493  		}
   494  		log.Entry().Warnf("'%s' needs to be of type string or an array of strings but got %T (%[2]v)", vaultPath, vRaw)
   495  	}
   496  	return retSlice
   497  }