mirror of
https://github.com/SAP/jenkins-library.git
synced 2024-12-14 11:03:09 +02:00
0673d3fed6
Co-authored-by: Kevin Stiehl <kevin.stiehl@numericas.de> Co-authored-by: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com>
384 lines
9.6 KiB
Go
384 lines
9.6 KiB
Go
package vault
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/SAP/jenkins-library/pkg/log"
|
|
"github.com/hashicorp/vault/api"
|
|
)
|
|
|
|
// Client handles communication with Vault
|
|
type Client struct {
|
|
lClient logicalClient
|
|
config *Config
|
|
}
|
|
|
|
// Config contains the vault client configuration
|
|
type Config struct {
|
|
*api.Config
|
|
AppRoleMountPoint string
|
|
Namespace string
|
|
}
|
|
|
|
// logicalClient interface for mocking
|
|
type logicalClient interface {
|
|
Read(string) (*api.Secret, error)
|
|
Write(string, map[string]interface{}) (*api.Secret, error)
|
|
}
|
|
|
|
// NewClient instantiates a Client and sets the specified token
|
|
func NewClient(config *Config, token string) (Client, error) {
|
|
if config == nil {
|
|
config = &Config{Config: api.DefaultConfig()}
|
|
}
|
|
client, err := api.NewClient(config.Config)
|
|
if err != nil {
|
|
return Client{}, err
|
|
}
|
|
|
|
if config.Namespace != "" {
|
|
client.SetNamespace(config.Namespace)
|
|
}
|
|
|
|
client.SetToken(token)
|
|
log.Entry().Debugf("Login to vault %s in namespace %s successfull", config.Address, config.Namespace)
|
|
return Client{client.Logical(), config}, nil
|
|
}
|
|
|
|
// NewClientWithAppRole instantiates a new client and obtains a token via the AppRole auth method
|
|
func NewClientWithAppRole(config *Config, roleID, secretID string) (Client, error) {
|
|
if config == nil {
|
|
config = &Config{Config: api.DefaultConfig()}
|
|
}
|
|
|
|
if config.AppRoleMountPoint == "" {
|
|
config.AppRoleMountPoint = "auth/approle"
|
|
}
|
|
|
|
client, err := api.NewClient(config.Config)
|
|
if err != nil {
|
|
return Client{}, err
|
|
}
|
|
|
|
if config.Namespace != "" {
|
|
client.SetNamespace(config.Namespace)
|
|
}
|
|
|
|
log.Entry().Debug("Using approle login")
|
|
result, err := client.Logical().Write(path.Join(config.AppRoleMountPoint, "/login"), map[string]interface{}{
|
|
"role_id": roleID,
|
|
"secret_id": secretID,
|
|
})
|
|
|
|
if err != nil {
|
|
return Client{}, err
|
|
}
|
|
|
|
authInfo := result.Auth
|
|
if authInfo == nil || authInfo.ClientToken == "" {
|
|
return Client{}, fmt.Errorf("Could not obtain token from approle with role_id %s", roleID)
|
|
}
|
|
|
|
return NewClient(config, authInfo.ClientToken)
|
|
}
|
|
|
|
// GetSecret uses the given path to fetch a secret from vault
|
|
func (v Client) GetSecret(path string) (*api.Secret, error) {
|
|
path = sanitizePath(path)
|
|
c := v.lClient
|
|
|
|
secret, err := c.Read(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return secret, nil
|
|
}
|
|
|
|
// GetKvSecret reads secret from the KV engine.
|
|
// It Automatically transforms the logical path to the HTTP API Path for the corresponding KV Engine version
|
|
func (v Client) GetKvSecret(path string) (map[string]string, error) {
|
|
path = sanitizePath(path)
|
|
mountpath, version, err := v.getKvInfo(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if version == 2 {
|
|
path = addPrefixToKvPath(path, mountpath, "data")
|
|
} else if version != 1 {
|
|
return nil, fmt.Errorf("KV Engine in version %d is currently not supported", version)
|
|
}
|
|
|
|
secret, err := v.GetSecret(path)
|
|
if secret == nil || err != nil {
|
|
return nil, err
|
|
|
|
}
|
|
var rawData interface{}
|
|
switch version {
|
|
case 1:
|
|
rawData = secret.Data
|
|
case 2:
|
|
var ok bool
|
|
rawData, ok = secret.Data["data"]
|
|
if !ok {
|
|
return nil, fmt.Errorf("Missing 'data' field in response: %v", rawData)
|
|
}
|
|
}
|
|
|
|
data, ok := rawData.(map[string]interface{})
|
|
if !ok {
|
|
return nil, fmt.Errorf("Excpected 'data' field to be a map[string]interface{} but got %T instead", rawData)
|
|
}
|
|
|
|
secretData := make(map[string]string, len(data))
|
|
for k, v := range data {
|
|
valueStr, ok := v.(string)
|
|
if ok {
|
|
secretData[k] = valueStr
|
|
}
|
|
}
|
|
return secretData, nil
|
|
}
|
|
|
|
// WriteKvSecret writes secret to kv engine
|
|
func (v Client) WriteKvSecret(path string, newSecret map[string]string) error {
|
|
oldSecret, err := v.GetKvSecret(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
secret := make(map[string]interface{}, len(oldSecret))
|
|
for k, v := range oldSecret {
|
|
secret[k] = v
|
|
}
|
|
for k, v := range newSecret {
|
|
secret[k] = v
|
|
}
|
|
path = sanitizePath(path)
|
|
mountpath, version, err := v.getKvInfo(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if version == 2 {
|
|
path = addPrefixToKvPath(path, mountpath, "data")
|
|
secret = map[string]interface{}{"data": secret}
|
|
} else if version != 1 {
|
|
return fmt.Errorf("KV Engine in version %d is currently not supported", version)
|
|
}
|
|
|
|
_, err = v.lClient.Write(path, secret)
|
|
return err
|
|
}
|
|
|
|
// GenerateNewAppRoleSecret creates a new secret-id
|
|
func (v *Client) GenerateNewAppRoleSecret(secretID, appRoleName string) (string, error) {
|
|
appRolePath := v.getAppRolePath(appRoleName)
|
|
secretIDData, err := v.lookupSecretID(secretID, appRolePath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
reqPath := sanitizePath(path.Join(appRolePath, "/secret-id"))
|
|
|
|
// we preserve metadata which was attached to the secret-id
|
|
json, err := json.Marshal(secretIDData["metadata"])
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
secret, err := v.lClient.Write(reqPath, map[string]interface{}{
|
|
"metadata": string(json),
|
|
})
|
|
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if secret == nil || secret.Data == nil {
|
|
return "", fmt.Errorf("Could not generate new approle secret-id for approle path %s", reqPath)
|
|
}
|
|
|
|
secretIDRaw, ok := secret.Data["secret_id"]
|
|
if !ok {
|
|
return "", fmt.Errorf("Vault response for path %s did not contain a new secret-id", reqPath)
|
|
}
|
|
|
|
newSecretID, ok := secretIDRaw.(string)
|
|
if !ok {
|
|
return "", fmt.Errorf("New secret-id from approle path %s has an unexpected type %T expected 'string'", reqPath, secretIDRaw)
|
|
}
|
|
|
|
return newSecretID, nil
|
|
}
|
|
|
|
// GetAppRoleSecretIDTtl returns the remaining time until the given secret-id expires
|
|
func (v *Client) GetAppRoleSecretIDTtl(secretID, roleName string) (time.Duration, error) {
|
|
appRolePath := v.getAppRolePath(roleName)
|
|
data, err := v.lookupSecretID(secretID, appRolePath)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
if data == nil || data["expiration_time"] == nil {
|
|
return 0, fmt.Errorf("Could not load secret-id information from path %s", appRolePath)
|
|
}
|
|
|
|
expiration, ok := data["expiration_time"].(string)
|
|
if !ok || expiration == "" {
|
|
return 0, fmt.Errorf("Could not handle get expiration time for secret-id from path %s", appRolePath)
|
|
}
|
|
|
|
expirationDate, err := time.Parse(time.RFC3339, expiration)
|
|
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
ttl := expirationDate.Sub(time.Now())
|
|
if ttl < 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
return ttl, nil
|
|
}
|
|
|
|
// RevokeToken revokes the token which is currently used.
|
|
// The client can't be used anymore after this function was called.
|
|
func (v Client) RevokeToken() error {
|
|
_, err := v.lClient.Write("auth/token/revoke-self", map[string]interface{}{})
|
|
return err
|
|
}
|
|
|
|
// MustRevokeToken same as RevokeToken but the programm is terminated with an error if this fails.
|
|
// Should be used in defer statements only.
|
|
func (v Client) MustRevokeToken() {
|
|
if err := v.RevokeToken(); err != nil {
|
|
log.Entry().WithError(err).Fatal("Could not revoke token")
|
|
}
|
|
}
|
|
|
|
// GetAppRoleName returns the AppRole role name which was used to authenticate.
|
|
// Returns "" when AppRole authentication wasn't used
|
|
func (v *Client) GetAppRoleName() (string, error) {
|
|
const lookupPath = "auth/token/lookup-self"
|
|
secret, err := v.GetSecret(lookupPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if secret.Data == nil {
|
|
return "", fmt.Errorf("Could not lookup token information: %s", lookupPath)
|
|
}
|
|
|
|
meta, ok := secret.Data["meta"]
|
|
|
|
if !ok {
|
|
return "", fmt.Errorf("Token info did not contain metadata %s", lookupPath)
|
|
}
|
|
|
|
metaMap, ok := meta.(map[string]interface{})
|
|
|
|
if !ok {
|
|
return "", fmt.Errorf("Token info field 'meta' is not a map: %s", lookupPath)
|
|
}
|
|
|
|
roleName := metaMap["role_name"]
|
|
|
|
if roleName == nil {
|
|
return "", nil
|
|
}
|
|
|
|
roleNameStr, ok := roleName.(string)
|
|
if !ok {
|
|
// when approle authentication is not used vault admins can use the role_name field with other type
|
|
// so no error in this case
|
|
return "", nil
|
|
}
|
|
|
|
return roleNameStr, nil
|
|
}
|
|
|
|
// SetAppRoleMountPoint sets the path under which the approle auth backend is mounted
|
|
func (v *Client) SetAppRoleMountPoint(appRoleMountpoint string) {
|
|
v.config.AppRoleMountPoint = appRoleMountpoint
|
|
}
|
|
|
|
func (v *Client) getAppRolePath(roleName string) string {
|
|
appRoleMountPoint := v.config.AppRoleMountPoint
|
|
if appRoleMountPoint == "" {
|
|
appRoleMountPoint = "auth/approle"
|
|
}
|
|
return path.Join(appRoleMountPoint, "role", roleName)
|
|
}
|
|
|
|
func sanitizePath(path string) string {
|
|
path = strings.TrimSpace(path)
|
|
path = strings.TrimPrefix(path, "/")
|
|
path = strings.TrimSuffix(path, "/")
|
|
return path
|
|
}
|
|
|
|
func addPrefixToKvPath(p, mountPath, apiPrefix string) string {
|
|
switch {
|
|
case p == mountPath, p == strings.TrimSuffix(mountPath, "/"):
|
|
return path.Join(mountPath, apiPrefix)
|
|
default:
|
|
p = strings.TrimPrefix(p, mountPath)
|
|
return path.Join(mountPath, apiPrefix, p)
|
|
}
|
|
}
|
|
|
|
func (v *Client) getKvInfo(path string) (string, int, error) {
|
|
secret, err := v.GetSecret("sys/internal/ui/mounts/" + path)
|
|
if err != nil {
|
|
return "", 0, err
|
|
}
|
|
|
|
if secret == nil {
|
|
return "", 0, fmt.Errorf("Failed to get version and engine mountpoint for path: %s", path)
|
|
}
|
|
|
|
var mountPath string
|
|
if mountPathRaw, ok := secret.Data["path"]; ok {
|
|
mountPath = mountPathRaw.(string)
|
|
}
|
|
|
|
options := secret.Data["options"]
|
|
if options == nil {
|
|
return mountPath, 1, nil
|
|
}
|
|
|
|
versionRaw := options.(map[string]interface{})["version"]
|
|
if versionRaw == nil {
|
|
return mountPath, 1, nil
|
|
}
|
|
|
|
version := versionRaw.(string)
|
|
if version == "" {
|
|
return mountPath, 1, nil
|
|
}
|
|
|
|
vNumber, err := strconv.Atoi(version)
|
|
if err != nil {
|
|
return mountPath, 0, err
|
|
}
|
|
|
|
return mountPath, vNumber, nil
|
|
}
|
|
|
|
func (v *Client) lookupSecretID(secretID, appRolePath string) (map[string]interface{}, error) {
|
|
reqPath := sanitizePath(path.Join(appRolePath, "/secret-id/lookup"))
|
|
secret, err := v.lClient.Write(reqPath, map[string]interface{}{
|
|
"secret_id": secretID,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return secret.Data, nil
|
|
}
|