Merge pull request #678 from hashicorp/response-warnings
Add the ability for warnings to be added to responses.
This commit is contained in:
commit
1a9f2ae00a
|
@ -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:
|
||||
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue