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 }