// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package api import ( "bytes" "encoding/json" "fmt" "io" "reflect" "strings" "time" "github.com/hashicorp/errwrap" "github.com/hashicorp/go-secure-stdlib/parseutil" ) // Secret is the structure returned for every secret within Vault. type Secret struct { // The request ID that generated this response RequestID string `json:"request_id"` LeaseID string `json:"lease_id"` LeaseDuration int `json:"lease_duration"` Renewable bool `json:"renewable"` // Data is the actual contents of the secret. The format of the data // is arbitrary and up to the secret backend. Data map[string]interface{} `json:"data"` // Warnings contains any warnings related to the operation. These // are not issues that caused the command to fail, but that the // client should be aware of. Warnings []string `json:"warnings"` // Auth, if non-nil, means that there was authentication information // attached to this response. Auth *SecretAuth `json:"auth,omitempty"` // WrapInfo, if non-nil, means that the initial response was wrapped in the // cubbyhole of the given token (which has a TTL of the given number of // seconds) WrapInfo *SecretWrapInfo `json:"wrap_info,omitempty"` } // TokenID returns the standardized token ID (token) for the given secret. func (s *Secret) TokenID() (string, error) { if s == nil { return "", nil } if s.Auth != nil && len(s.Auth.ClientToken) > 0 { return s.Auth.ClientToken, nil } if s.Data == nil || s.Data["id"] == nil { return "", nil } id, ok := s.Data["id"].(string) if !ok { return "", fmt.Errorf("token found but in the wrong format") } return id, nil } // TokenAccessor returns the standardized token accessor for the given secret. // If the secret is nil or does not contain an accessor, this returns the empty // string. func (s *Secret) TokenAccessor() (string, error) { if s == nil { return "", nil } if s.Auth != nil && len(s.Auth.Accessor) > 0 { return s.Auth.Accessor, nil } if s.Data == nil || s.Data["accessor"] == nil { return "", nil } accessor, ok := s.Data["accessor"].(string) if !ok { return "", fmt.Errorf("token found but in the wrong format") } return accessor, nil } // TokenRemainingUses returns the standardized remaining uses for the given // secret. If the secret is nil or does not contain the "num_uses", this // returns -1. On error, this will return -1 and a non-nil error. func (s *Secret) TokenRemainingUses() (int, error) { if s == nil || s.Data == nil || s.Data["num_uses"] == nil { return -1, nil } return parseutil.SafeParseInt(s.Data["num_uses"]) } // TokenPolicies returns the standardized list of policies for the given secret. // If the secret is nil or does not contain any policies, this returns nil. It // also populates the secret's Auth info with identity/token policy info. func (s *Secret) TokenPolicies() ([]string, error) { if s == nil { return nil, nil } if s.Auth != nil && len(s.Auth.Policies) > 0 { return s.Auth.Policies, nil } if s.Data == nil || s.Data["policies"] == nil { return nil, nil } var tokenPolicies []string // Token policies { _, ok := s.Data["policies"] if !ok { goto TOKEN_DONE } sList, ok := s.Data["policies"].([]string) if ok { tokenPolicies = sList goto TOKEN_DONE } list, ok := s.Data["policies"].([]interface{}) if !ok { return nil, fmt.Errorf("unable to convert token policies to expected format") } for _, v := range list { p, ok := v.(string) if !ok { return nil, fmt.Errorf("unable to convert policy %v to string", v) } tokenPolicies = append(tokenPolicies, p) } } TOKEN_DONE: var identityPolicies []string // Identity policies { v, ok := s.Data["identity_policies"] if !ok || v == nil { goto DONE } sList, ok := s.Data["identity_policies"].([]string) if ok { identityPolicies = sList goto DONE } list, ok := s.Data["identity_policies"].([]interface{}) if !ok { return nil, fmt.Errorf("unable to convert identity policies to expected format") } for _, v := range list { p, ok := v.(string) if !ok { return nil, fmt.Errorf("unable to convert policy %v to string", v) } identityPolicies = append(identityPolicies, p) } } DONE: if s.Auth == nil { s.Auth = &SecretAuth{} } policies := append(tokenPolicies, identityPolicies...) s.Auth.TokenPolicies = tokenPolicies s.Auth.IdentityPolicies = identityPolicies s.Auth.Policies = policies return policies, nil } // TokenMetadata returns the map of metadata associated with this token, if any // exists. If the secret is nil or does not contain the "metadata" key, this // returns nil. func (s *Secret) TokenMetadata() (map[string]string, error) { if s == nil { return nil, nil } if s.Auth != nil && len(s.Auth.Metadata) > 0 { return s.Auth.Metadata, nil } if s.Data == nil || (s.Data["metadata"] == nil && s.Data["meta"] == nil) { return nil, nil } data, ok := s.Data["metadata"].(map[string]interface{}) if !ok { data, ok = s.Data["meta"].(map[string]interface{}) if !ok { return nil, fmt.Errorf("unable to convert metadata field to expected format") } } metadata := make(map[string]string, len(data)) for k, v := range data { typed, ok := v.(string) if !ok { return nil, fmt.Errorf("unable to convert metadata value %v to string", v) } metadata[k] = typed } return metadata, nil } // TokenIsRenewable returns the standardized token renewability for the given // secret. If the secret is nil or does not contain the "renewable" key, this // returns false. func (s *Secret) TokenIsRenewable() (bool, error) { if s == nil { return false, nil } if s.Auth != nil && s.Auth.Renewable { return s.Auth.Renewable, nil } if s.Data == nil || s.Data["renewable"] == nil { return false, nil } renewable, err := parseutil.ParseBool(s.Data["renewable"]) if err != nil { return false, errwrap.Wrapf("could not convert renewable value to a boolean: {{err}}", err) } return renewable, nil } // TokenTTL returns the standardized remaining token TTL for the given secret. // If the secret is nil or does not contain a TTL, this returns 0. func (s *Secret) TokenTTL() (time.Duration, error) { if s == nil { return 0, nil } if s.Auth != nil && s.Auth.LeaseDuration > 0 { return time.Duration(s.Auth.LeaseDuration) * time.Second, nil } if s.Data == nil || s.Data["ttl"] == nil { return 0, nil } ttl, err := parseutil.ParseDurationSecond(s.Data["ttl"]) if err != nil { return 0, err } return ttl, nil } // SecretWrapInfo contains wrapping information if we have it. If what is // contained is an authentication token, the accessor for the token will be // available in WrappedAccessor. type SecretWrapInfo struct { Token string `json:"token"` Accessor string `json:"accessor"` TTL int `json:"ttl"` CreationTime time.Time `json:"creation_time"` CreationPath string `json:"creation_path"` WrappedAccessor string `json:"wrapped_accessor"` } type MFAMethodID struct { Type string `json:"type,omitempty"` ID string `json:"id,omitempty"` UsesPasscode bool `json:"uses_passcode,omitempty"` Name string `json:"name,omitempty"` } type MFAConstraintAny struct { Any []*MFAMethodID `json:"any,omitempty"` } type MFARequirement struct { MFARequestID string `json:"mfa_request_id,omitempty"` MFAConstraints map[string]*MFAConstraintAny `json:"mfa_constraints,omitempty"` } // SecretAuth is the structure containing auth information if we have it. type SecretAuth struct { ClientToken string `json:"client_token"` Accessor string `json:"accessor"` Policies []string `json:"policies"` TokenPolicies []string `json:"token_policies"` IdentityPolicies []string `json:"identity_policies"` Metadata map[string]string `json:"metadata"` Orphan bool `json:"orphan"` EntityID string `json:"entity_id"` LeaseDuration int `json:"lease_duration"` Renewable bool `json:"renewable"` MFARequirement *MFARequirement `json:"mfa_requirement"` } // ParseSecret is used to parse a secret value from JSON from an io.Reader. func ParseSecret(r io.Reader) (*Secret, error) { // First read the data into a buffer. Not super efficient but we want to // know if we actually have a body or not. var buf bytes.Buffer // io.Reader is treated like a stream and cannot be read // multiple times. Duplicating this stream using TeeReader // to use this data in case there is no top-level data from // api response var teebuf bytes.Buffer tee := io.TeeReader(r, &teebuf) _, err := buf.ReadFrom(tee) if err != nil { return nil, err } if buf.Len() == 0 { return nil, nil } // First decode the JSON into a map[string]interface{} var secret Secret dec := json.NewDecoder(&buf) dec.UseNumber() if err := dec.Decode(&secret); err != nil { return nil, err } // If the secret is null, add raw data to secret data if present if reflect.DeepEqual(secret, Secret{}) { data := make(map[string]interface{}) dec := json.NewDecoder(&teebuf) dec.UseNumber() if err := dec.Decode(&data); err != nil { return nil, err } errRaw, errPresent := data["errors"] // if only errors are present in the resp.Body return nil // to return value not found as it does not have any raw data if len(data) == 1 && errPresent { return nil, nil } // if errors are present along with raw data return the error if errPresent { var errStrArray []string errBytes, err := json.Marshal(errRaw) if err != nil { return nil, err } if err := json.Unmarshal(errBytes, &errStrArray); err != nil { return nil, err } return nil, fmt.Errorf(strings.Join(errStrArray, " ")) } // if any raw data is present in resp.Body, add it to secret if len(data) > 0 { secret.Data = data } } return &secret, nil }