386 lines
9.9 KiB
Go
386 lines
9.9 KiB
Go
// 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
|
|
}
|