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 }