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

     1  package vault
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"path"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/SAP/jenkins-library/pkg/log"
    15  	"github.com/hashicorp/vault/api"
    16  )
    17  
    18  // Client handles communication with Vault
    19  type Client struct {
    20  	lClient logicalClient
    21  	config  *Config
    22  }
    23  
    24  // Config contains the vault client configuration
    25  type Config struct {
    26  	*api.Config
    27  	AppRoleMountPoint string
    28  	Namespace         string
    29  }
    30  
    31  // logicalClient interface for mocking
    32  type logicalClient interface {
    33  	Read(string) (*api.Secret, error)
    34  	Write(string, map[string]interface{}) (*api.Secret, error)
    35  }
    36  
    37  // NewClient instantiates a Client and sets the specified token
    38  func NewClient(config *Config, token string) (Client, error) {
    39  	if config == nil {
    40  		config = &Config{Config: api.DefaultConfig()}
    41  	}
    42  	client, err := api.NewClient(config.Config)
    43  	if err != nil {
    44  		return Client{}, err
    45  	}
    46  	if config.Namespace != "" {
    47  		client.SetNamespace(config.Namespace)
    48  	}
    49  	client.SetToken(token)
    50  	return Client{client.Logical(), config}, nil
    51  }
    52  
    53  // NewClientWithAppRole instantiates a new client and obtains a token via the AppRole auth method
    54  func NewClientWithAppRole(config *Config, roleID, secretID string) (Client, error) {
    55  	if config == nil {
    56  		config = &Config{Config: api.DefaultConfig()}
    57  	}
    58  	if config.AppRoleMountPoint == "" {
    59  		config.AppRoleMountPoint = "auth/approle"
    60  	}
    61  	client, err := api.NewClient(config.Config)
    62  	if err != nil {
    63  		return Client{}, err
    64  	}
    65  
    66  	client.SetMinRetryWait(time.Second * 5)
    67  	client.SetMaxRetryWait(time.Second * 90)
    68  	client.SetMaxRetries(3)
    69  	client.SetCheckRetry(func(ctx context.Context, resp *http.Response, err error) (bool, error) {
    70  		if resp != nil {
    71  			log.Entry().Debugln("Vault response: ", resp.Status, resp.StatusCode, err)
    72  		} else {
    73  			log.Entry().Debugln("Vault response: ", err)
    74  		}
    75  
    76  		isEOF := false
    77  		if err != nil && strings.Contains(err.Error(), "EOF") {
    78  			log.Entry().Infoln("isEOF is true")
    79  			isEOF = true
    80  		}
    81  
    82  		if err == io.EOF {
    83  			log.Entry().Infoln("err = io.EOF is true")
    84  		}
    85  
    86  		retry, err := api.DefaultRetryPolicy(ctx, resp, err)
    87  
    88  		if err != nil || err == io.EOF || isEOF || retry {
    89  			log.Entry().Infoln("Retrying vault request...")
    90  			return true, nil
    91  		}
    92  		return false, nil
    93  	})
    94  
    95  	if config.Namespace != "" {
    96  		client.SetNamespace(config.Namespace)
    97  	}
    98  
    99  	result, err := client.Logical().Write(path.Join(config.AppRoleMountPoint, "/login"), map[string]interface{}{
   100  		"role_id":   roleID,
   101  		"secret_id": secretID,
   102  	})
   103  	if err != nil {
   104  		return Client{}, err
   105  	}
   106  
   107  	authInfo := result.Auth
   108  	if authInfo == nil || authInfo.ClientToken == "" {
   109  		return Client{}, fmt.Errorf("Could not obtain token from approle with role_id %s", roleID)
   110  	}
   111  
   112  	return NewClient(config, authInfo.ClientToken)
   113  }
   114  
   115  // GetSecret uses the given path to fetch a secret from vault
   116  func (v Client) GetSecret(path string) (*api.Secret, error) {
   117  	path = sanitizePath(path)
   118  	c := v.lClient
   119  
   120  	secret, err := c.Read(path)
   121  	if err != nil {
   122  		return nil, err
   123  	}
   124  
   125  	return secret, nil
   126  }
   127  
   128  // GetKvSecret reads secret from the KV engine.
   129  // It Automatically transforms the logical path to the HTTP API Path for the corresponding KV Engine version
   130  func (v Client) GetKvSecret(path string) (map[string]string, error) {
   131  	path = sanitizePath(path)
   132  	mountpath, version, err := v.getKvInfo(path)
   133  	if err != nil {
   134  		return nil, err
   135  	}
   136  	if version == 2 {
   137  		path = addPrefixToKvPath(path, mountpath, "data")
   138  	} else if version != 1 {
   139  		return nil, fmt.Errorf("KV Engine in version %d is currently not supported", version)
   140  	}
   141  
   142  	secret, err := v.GetSecret(path)
   143  	if secret == nil || err != nil {
   144  		return nil, err
   145  
   146  	}
   147  	var rawData interface{}
   148  	switch version {
   149  	case 1:
   150  		rawData = secret.Data
   151  	case 2:
   152  		var ok bool
   153  		rawData, ok = secret.Data["data"]
   154  		if !ok {
   155  			return nil, fmt.Errorf("Missing 'data' field in response: %v", rawData)
   156  		}
   157  	}
   158  
   159  	data, ok := rawData.(map[string]interface{})
   160  	if !ok {
   161  		return nil, fmt.Errorf("Excpected 'data' field to be a map[string]interface{} but got %T instead", rawData)
   162  	}
   163  
   164  	secretData := make(map[string]string, len(data))
   165  	for k, v := range data {
   166  		valueStr, ok := v.(string)
   167  		if ok {
   168  			secretData[k] = valueStr
   169  		}
   170  	}
   171  	return secretData, nil
   172  }
   173  
   174  // WriteKvSecret writes secret to kv engine
   175  func (v Client) WriteKvSecret(path string, newSecret map[string]string) error {
   176  	oldSecret, err := v.GetKvSecret(path)
   177  	if err != nil {
   178  		return err
   179  	}
   180  	secret := make(map[string]interface{}, len(oldSecret))
   181  	for k, v := range oldSecret {
   182  		secret[k] = v
   183  	}
   184  	for k, v := range newSecret {
   185  		secret[k] = v
   186  	}
   187  	path = sanitizePath(path)
   188  	mountpath, version, err := v.getKvInfo(path)
   189  	if err != nil {
   190  		return err
   191  	}
   192  	if version == 2 {
   193  		path = addPrefixToKvPath(path, mountpath, "data")
   194  		secret = map[string]interface{}{"data": secret}
   195  	} else if version != 1 {
   196  		return fmt.Errorf("KV Engine in version %d is currently not supported", version)
   197  	}
   198  
   199  	_, err = v.lClient.Write(path, secret)
   200  	return err
   201  }
   202  
   203  // GenerateNewAppRoleSecret creates a new secret-id
   204  func (v *Client) GenerateNewAppRoleSecret(secretID, appRoleName string) (string, error) {
   205  	appRolePath := v.getAppRolePath(appRoleName)
   206  	secretIDData, err := v.lookupSecretID(secretID, appRolePath)
   207  	if err != nil {
   208  		return "", err
   209  	}
   210  
   211  	reqPath := sanitizePath(path.Join(appRolePath, "/secret-id"))
   212  
   213  	// we preserve metadata which was attached to the secret-id
   214  	json, err := json.Marshal(secretIDData["metadata"])
   215  	if err != nil {
   216  		return "", err
   217  	}
   218  	secret, err := v.lClient.Write(reqPath, map[string]interface{}{
   219  		"metadata": string(json),
   220  	})
   221  
   222  	if err != nil {
   223  		return "", err
   224  	}
   225  
   226  	if secret == nil || secret.Data == nil {
   227  		return "", fmt.Errorf("Could not generate new approle secret-id for approle path %s", reqPath)
   228  	}
   229  
   230  	secretIDRaw, ok := secret.Data["secret_id"]
   231  	if !ok {
   232  		return "", fmt.Errorf("Vault response for path %s did not contain a new secret-id", reqPath)
   233  	}
   234  
   235  	newSecretID, ok := secretIDRaw.(string)
   236  	if !ok {
   237  		return "", fmt.Errorf("New secret-id from approle path %s has an unexpected type %T expected 'string'", reqPath, secretIDRaw)
   238  	}
   239  
   240  	return newSecretID, nil
   241  }
   242  
   243  // GetAppRoleSecretIDTtl returns the remaining time until the given secret-id expires
   244  func (v *Client) GetAppRoleSecretIDTtl(secretID, roleName string) (time.Duration, error) {
   245  	appRolePath := v.getAppRolePath(roleName)
   246  	data, err := v.lookupSecretID(secretID, appRolePath)
   247  	if err != nil {
   248  		return 0, err
   249  	}
   250  
   251  	if data == nil || data["expiration_time"] == nil {
   252  		return 0, fmt.Errorf("Could not load secret-id information from path %s", appRolePath)
   253  	}
   254  
   255  	expiration, ok := data["expiration_time"].(string)
   256  	if !ok || expiration == "" {
   257  		return 0, fmt.Errorf("Could not handle get expiration time for secret-id from path %s", appRolePath)
   258  	}
   259  
   260  	expirationDate, err := time.Parse(time.RFC3339, expiration)
   261  
   262  	if err != nil {
   263  		return 0, err
   264  	}
   265  
   266  	ttl := expirationDate.Sub(time.Now())
   267  	if ttl < 0 {
   268  		return 0, nil
   269  	}
   270  
   271  	return ttl, nil
   272  }
   273  
   274  // RevokeToken revokes the token which is currently used.
   275  // The client can't be used anymore after this function was called.
   276  func (v Client) RevokeToken() error {
   277  	_, err := v.lClient.Write("auth/token/revoke-self", map[string]interface{}{})
   278  	return err
   279  }
   280  
   281  // MustRevokeToken same as RevokeToken but the programm is terminated with an error if this fails.
   282  // Should be used in defer statements only.
   283  func (v Client) MustRevokeToken() {
   284  	if err := v.RevokeToken(); err != nil {
   285  		log.Entry().WithError(err).Fatal("Could not revoke token")
   286  	}
   287  }
   288  
   289  // GetAppRoleName returns the AppRole role name which was used to authenticate.
   290  // Returns "" when AppRole authentication wasn't used
   291  func (v *Client) GetAppRoleName() (string, error) {
   292  	const lookupPath = "auth/token/lookup-self"
   293  	secret, err := v.GetSecret(lookupPath)
   294  	if err != nil {
   295  		return "", err
   296  	}
   297  
   298  	if secret.Data == nil {
   299  		return "", fmt.Errorf("Could not lookup token information: %s", lookupPath)
   300  	}
   301  
   302  	meta, ok := secret.Data["meta"]
   303  
   304  	if !ok {
   305  		return "", fmt.Errorf("Token info did not contain metadata %s", lookupPath)
   306  	}
   307  
   308  	metaMap, ok := meta.(map[string]interface{})
   309  
   310  	if !ok {
   311  		return "", fmt.Errorf("Token info field 'meta' is not a map: %s", lookupPath)
   312  	}
   313  
   314  	roleName := metaMap["role_name"]
   315  
   316  	if roleName == nil {
   317  		return "", nil
   318  	}
   319  
   320  	roleNameStr, ok := roleName.(string)
   321  	if !ok {
   322  		// when approle authentication is not used vault admins can use the role_name field with other type
   323  		// so no error in this case
   324  		return "", nil
   325  	}
   326  
   327  	return roleNameStr, nil
   328  }
   329  
   330  // SetAppRoleMountPoint sets the path under which the approle auth backend is mounted
   331  func (v *Client) SetAppRoleMountPoint(appRoleMountpoint string) {
   332  	v.config.AppRoleMountPoint = appRoleMountpoint
   333  }
   334  
   335  func (v *Client) getAppRolePath(roleName string) string {
   336  	appRoleMountPoint := v.config.AppRoleMountPoint
   337  	if appRoleMountPoint == "" {
   338  		appRoleMountPoint = "auth/approle"
   339  	}
   340  	return path.Join(appRoleMountPoint, "role", roleName)
   341  }
   342  
   343  func sanitizePath(path string) string {
   344  	path = strings.TrimSpace(path)
   345  	path = strings.TrimPrefix(path, "/")
   346  	path = strings.TrimSuffix(path, "/")
   347  	return path
   348  }
   349  
   350  func addPrefixToKvPath(p, mountPath, apiPrefix string) string {
   351  	switch {
   352  	case p == mountPath, p == strings.TrimSuffix(mountPath, "/"):
   353  		return path.Join(mountPath, apiPrefix)
   354  	default:
   355  		p = strings.TrimPrefix(p, mountPath)
   356  		return path.Join(mountPath, apiPrefix, p)
   357  	}
   358  }
   359  
   360  func (v *Client) getKvInfo(path string) (string, int, error) {
   361  	secret, err := v.GetSecret("sys/internal/ui/mounts/" + path)
   362  	if err != nil {
   363  		return "", 0, err
   364  	}
   365  
   366  	if secret == nil {
   367  		return "", 0, fmt.Errorf("Failed to get version and engine mountpoint for path: %s", path)
   368  	}
   369  
   370  	var mountPath string
   371  	if mountPathRaw, ok := secret.Data["path"]; ok {
   372  		mountPath = mountPathRaw.(string)
   373  	}
   374  
   375  	options := secret.Data["options"]
   376  	if options == nil {
   377  		return mountPath, 1, nil
   378  	}
   379  
   380  	versionRaw := options.(map[string]interface{})["version"]
   381  	if versionRaw == nil {
   382  		return mountPath, 1, nil
   383  	}
   384  
   385  	version := versionRaw.(string)
   386  	if version == "" {
   387  		return mountPath, 1, nil
   388  	}
   389  
   390  	vNumber, err := strconv.Atoi(version)
   391  	if err != nil {
   392  		return mountPath, 0, err
   393  	}
   394  
   395  	return mountPath, vNumber, nil
   396  }
   397  
   398  func (v *Client) lookupSecretID(secretID, appRolePath string) (map[string]interface{}, error) {
   399  	reqPath := sanitizePath(path.Join(appRolePath, "/secret-id/lookup"))
   400  	secret, err := v.lClient.Write(reqPath, map[string]interface{}{
   401  		"secret_id": secretID,
   402  	})
   403  	if err != nil {
   404  		return nil, err
   405  	}
   406  
   407  	return secret.Data, nil
   408  }