Merge pull request #678 from hashicorp/response-warnings

Add the ability for warnings to be added to responses.
This commit is contained in:
Jeff Mitchell 2015-10-07 20:18:30 -04:00
commit 1a9f2ae00a
8 changed files with 132 additions and 6 deletions

View file

@ -8,6 +8,7 @@ IMPROVEMENTS:
* init: Base64-encoded PGP keys can be used with the CLI for `init` and `rekey` operations [GH-653]
* core: Tokens can now renew themselves [GH-455]
* logical: Responses now contain a "warnings" key containing a list of warnings returned from the server. These are conditions that did not require failing an operation, but of which the client should be aware. [GH-676]
BUG FIXES:

View file

@ -15,6 +15,11 @@ type Secret struct {
// 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"`

View file

@ -14,7 +14,10 @@ func TestParseSecret(t *testing.T) {
"lease_duration": 10,
"data": {
"key": "value"
}
},
"warnings": [
"a warning!"
]
}`)
secret, err := ParseSecret(strings.NewReader(raw))
@ -29,6 +32,9 @@ func TestParseSecret(t *testing.T) {
Data: map[string]interface{}{
"key": "value",
},
Warnings: []string{
"a warning!",
},
}
if !reflect.DeepEqual(secret, expected) {
t.Fatalf("bad: %#v %#v", secret, expected)

View file

@ -43,6 +43,7 @@ func outputFormatTable(ui cli.Ui, s *api.Secret, whitespace bool) int {
config.Prefix = ""
input := make([]string, 0, 5)
input = append(input, fmt.Sprintf("Key %s Value", config.Delim))
if s.LeaseDuration > 0 {
@ -71,6 +72,14 @@ func outputFormatTable(ui cli.Ui, s *api.Secret, whitespace bool) int {
input = append(input, fmt.Sprintf("%s %s %v", k, config.Delim, v))
}
if len(s.Warnings) != 0 {
input = append(input, "")
input = append(input, "The following warnings were returned from the Vault server:")
for _, warning := range s.Warnings {
input = append(input, fmt.Sprintf("* %s", warning))
}
}
ui.Output(columnize.Format(input, config))
return 0
}

View file

@ -96,7 +96,10 @@ func respondLogical(w http.ResponseWriter, r *http.Request, path string, dataOnl
return
}
logicalResp := &LogicalResponse{Data: resp.Data}
logicalResp := &LogicalResponse{
Data: resp.Data,
Warnings: resp.Warnings(),
}
if resp.Secret != nil {
logicalResp.LeaseID = resp.Secret.LeaseID
logicalResp.Renewable = resp.Secret.Renewable
@ -196,6 +199,7 @@ type LogicalResponse struct {
Renewable bool `json:"renewable"`
LeaseDuration int `json:"lease_duration"`
Data map[string]interface{} `json:"data"`
Warnings []string `json:"warnings"`
Auth *Auth `json:"auth"`
}

View file

@ -27,19 +27,21 @@ func TestLogical(t *testing.T) {
resp = testHttpGet(t, token, addr+"/v1/secret/foo")
var actual map[string]interface{}
var nilWarnings interface{}
expected := map[string]interface{}{
"renewable": false,
"lease_duration": float64((30 * 24 * time.Hour) / time.Second),
"data": map[string]interface{}{
"data": "bar",
},
"auth": nil,
"auth": nil,
"warnings": nilWarnings,
}
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &actual)
delete(actual, "lease_id")
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v %#v", actual, expected)
t.Fatalf("bad:\nactual:\n%#v\nexpected:\n%#v", actual, expected)
}
// DELETE
@ -109,6 +111,7 @@ func TestLogical_StandbyRedirect(t *testing.T) {
//// READ to standby
resp = testHttpGet(t, root, addr2+"/v1/auth/token/lookup-self")
var actual map[string]interface{}
var nilWarnings interface{}
expected := map[string]interface{}{
"renewable": false,
"lease_duration": float64(0),
@ -121,7 +124,8 @@ func TestLogical_StandbyRedirect(t *testing.T) {
"id": root,
"ttl": float64(0),
},
"auth": nil,
"warnings": nilWarnings,
"auth": nil,
}
testResponseStatus(t, resp, 200)
@ -162,12 +166,13 @@ func TestLogical_CreateToken(t *testing.T) {
"lease_duration": float64(0),
"renewable": false,
},
"warnings": []interface{}{"policy \"root\" does not exist"},
}
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &actual)
delete(actual["auth"].(map[string]interface{}), "client_token")
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v %#v", actual, expected)
t.Fatalf("bad:\nexpected:\n%#v\nactual:\n%#v", expected, actual)
}
}

View file

@ -1,5 +1,12 @@
package logical
import (
"fmt"
"reflect"
"github.com/mitchellh/copystructure"
)
const (
// HTTPContentType can be specified in the Data field of a Response
// so that the HTTP front end can specify a custom Content-Type associated
@ -40,6 +47,72 @@ type Response struct {
// This is only valid for credential backends. This will be blanked
// for any logical backend and ignored.
Redirect string
// Warnings allow operations or backends to return warnings in response
// to user actions without failing the action outright.
// Making it private helps ensure that it is easy for various parts of
// Vault (backend, core, etc.) to add warnings without accidentally
// replacing what exists.
warnings []string
}
func init() {
copystructure.Copiers[reflect.TypeOf(Response{})] = func(v interface{}) (interface{}, error) {
input := v.(Response)
ret := Response{
Redirect: input.Redirect,
}
if input.Secret != nil {
retSec, err := copystructure.Copy(input.Secret)
if err != nil {
return nil, fmt.Errorf("error copying Secret: %v", err)
}
ret.Secret = retSec.(*Secret)
}
if input.Auth != nil {
retAuth, err := copystructure.Copy(input.Auth)
if err != nil {
return nil, fmt.Errorf("error copying Secret: %v", err)
}
ret.Auth = retAuth.(*Auth)
}
if input.Data != nil {
retData, err := copystructure.Copy(&input.Data)
if err != nil {
return nil, fmt.Errorf("error copying Secret: %v", err)
}
ret.Data = retData.(map[string]interface{})
}
if input.Warnings() != nil {
for _, warning := range input.Warnings() {
ret.AddWarning(warning)
}
}
return &ret, nil
}
}
// AddWarning adds a warning into the response's warning list
func (r *Response) AddWarning(warning string) {
if r.warnings == nil {
r.warnings = make([]string, 0, 1)
}
r.warnings = append(r.warnings, warning)
}
// Warnings returns the list of warnings set on the response
func (r *Response) Warnings() []string {
return r.warnings
}
// ClearWarnings clears the response's warning list
func (r *Response) ClearWarnings() {
r.warnings = make([]string, 0, 1)
}
// IsError returns true if this response seems to indicate an error.

View file

@ -46,6 +46,8 @@ type TokenStore struct {
expiration *ExpirationManager
cubbyholeBackend *CubbyholeBackend
policyLookupFunc func() ([]string, error)
}
// NewTokenStore is used to construct a token store that is
@ -59,6 +61,10 @@ func NewTokenStore(c *Core, config *logical.BackendConfig) (*TokenStore, error)
view: view,
}
if c.policy != nil {
t.policyLookupFunc = c.policy.ListPolicies
}
// Setup the salt
salt, err := salt.NewSalt(view, &salt.Config{
HashFunc: salt.SHA1Hash,
@ -636,6 +642,23 @@ func (ts *TokenStore) handleCreate(
},
}
if ts.policyLookupFunc != nil {
availPolicies, err := ts.policyLookupFunc()
if err == nil {
policies := map[string]bool{}
if availPolicies != nil && len(availPolicies) > 0 {
for _, p := range availPolicies {
policies[p] = true
}
}
for _, p := range te.Policies {
if !policies[p] {
resp.AddWarning(fmt.Sprintf("policy \"%s\" does not exist", p))
}
}
}
}
return resp, nil
}