github.com/jaylevin/jenkins-library@v1.230.4/pkg/vault/client.go (about)

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