VAULT-12112: add openapi response structures for /sys/config and /sys/generate-root endpoints (#18472)

* some config responses

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* added response structs

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* added changelog

* add test for config/cors

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* add (failing) tests

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* copy-pasta err

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* update tests for /sys/config/ui/headers/{header}

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

---------

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>
This commit is contained in:
Daniel Huckins 2023-02-16 15:06:26 -05:00 committed by GitHub
parent 60488687ad
commit 448f5dd33e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 286 additions and 4 deletions

3
changelog/18472.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
openapi: add openapi response defintions to /sys/config and /sys/generate-root endpoints
```

View File

@ -11,6 +11,7 @@ import (
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/internalshared/configutil"
"github.com/hashicorp/vault/sdk/helper/logging"
"github.com/hashicorp/vault/sdk/helper/testhelpers/schema"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/sdk/physical/inmem"
)
@ -98,6 +99,7 @@ func TestConfigCustomHeaders(t *testing.T) {
func TestCustomResponseHeadersConfigInteractUiConfig(t *testing.T) {
b := testSystemBackend(t)
paths := b.(*SystemBackend).configPaths()
_, barrier, _ := mockBarrier(t)
view := NewBarrierView(barrier, "")
b.(*SystemBackend).Core.systemBarrierView = view
@ -143,6 +145,13 @@ func TestCustomResponseHeadersConfigInteractUiConfig(t *testing.T) {
if err == nil {
t.Fatal("request did not fail on setting a header that is present in custom response headers")
}
schema.ValidateResponse(
t,
schema.FindResponseSchema(t, paths, 3, req.Operation),
resp,
true,
)
if !strings.Contains(resp.Data["error"].(string), fmt.Sprintf("This header already exists in the server configuration and cannot be set in the UI.")) {
t.Fatalf("failed to get the expected error")
}
@ -152,10 +161,17 @@ func TestCustomResponseHeadersConfigInteractUiConfig(t *testing.T) {
req.Data["values"] = []string{"400"}
req.ResponseWriter = hw
_, err = b.HandleRequest(namespace.RootContext(nil), req)
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err == nil {
t.Fatal("request did not fail on setting a header that is present in custom response headers")
}
schema.ValidateResponse(
t,
schema.FindResponseSchema(t, paths, 3, req.Operation),
resp,
true,
)
h, err := b.(*SystemBackend).Core.uiConfig.Headers(context.Background())
if err != nil {
t.Fatal(err)
@ -169,10 +185,16 @@ func TestCustomResponseHeadersConfigInteractUiConfig(t *testing.T) {
req.Data["values"] = []string{"Ui header value"}
req.ResponseWriter = hw
_, err = b.HandleRequest(namespace.RootContext(nil), req)
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil {
t.Fatal("request failed on setting a header that is not present in custom response headers.", "error:", err)
}
schema.ValidateResponse(
t,
schema.FindResponseSchema(t, paths, 3, req.Operation),
resp,
true,
)
h, err = b.(*SystemBackend).Core.uiConfig.Headers(context.Background())
if err != nil {

View File

@ -33,15 +33,44 @@ func (b *SystemBackend) configPaths() []*framework.Path {
Callback: b.handleCORSRead,
Summary: "Return the current CORS settings.",
Description: "",
Responses: map[int][]framework.Response{
http.StatusOK: {{
Description: "OK",
Fields: map[string]*framework.FieldSchema{
"enabled": {
Type: framework.TypeBool,
Required: true,
},
"allowed_origins": {
Type: framework.TypeCommaStringSlice,
Required: false,
},
"allowed_headers": {
Type: framework.TypeCommaStringSlice,
Required: false,
},
},
}},
},
},
logical.UpdateOperation: &framework.PathOperation{
Callback: b.handleCORSUpdate,
Summary: "Configure the CORS settings.",
Description: "",
Responses: map[int][]framework.Response{
http.StatusNoContent: {{
Description: "OK",
}},
},
},
logical.DeleteOperation: &framework.PathOperation{
Callback: b.handleCORSDelete,
Summary: "Remove any CORS settings.",
Responses: map[int][]framework.Response{
http.StatusNoContent: {{
Description: "OK",
}},
},
},
},
@ -56,6 +85,13 @@ func (b *SystemBackend) configPaths() []*framework.Path {
Callback: b.handleConfigStateSanitized,
Summary: "Return a sanitized version of the Vault server configuration.",
Description: "The sanitized output strips configuration values in the storage, HA storage, and seals stanzas, which may contain sensitive values such as API tokens. It also removes any token or secret fields in other stanzas, such as the circonus_api_token from telemetry.",
Responses: map[int][]framework.Response{
http.StatusOK: {{
Description: "OK",
// response has dynamic keys
Fields: map[string]*framework.FieldSchema{},
}},
},
},
},
},
@ -73,6 +109,11 @@ func (b *SystemBackend) configPaths() []*framework.Path {
Callback: b.handleConfigReload,
Summary: "Reload the given subsystem",
Description: "",
Responses: map[int][]framework.Response{
http.StatusNoContent: {{
Description: "OK",
}},
},
},
},
},
@ -99,14 +140,42 @@ func (b *SystemBackend) configPaths() []*framework.Path {
logical.ReadOperation: &framework.PathOperation{
Callback: b.handleConfigUIHeadersRead,
Summary: "Return the given UI header's configuration",
Responses: map[int][]framework.Response{
http.StatusOK: {{
Description: "OK",
Fields: map[string]*framework.FieldSchema{
"value": {
Type: framework.TypeString,
Required: false,
Description: "returns the first header value when `multivalue` request parameter is false",
},
"values": {
Type: framework.TypeCommaStringSlice,
Required: false,
Description: "returns all header values when `multivalue` request parameter is true",
},
},
}},
},
},
logical.UpdateOperation: &framework.PathOperation{
Callback: b.handleConfigUIHeadersUpdate,
Summary: "Configure the values to be returned for the UI header.",
Responses: map[int][]framework.Response{
http.StatusOK: {{
// returns 200 with null `data`
Description: "OK",
}},
},
},
logical.DeleteOperation: &framework.PathOperation{
Callback: b.handleConfigUIHeadersDelete,
Summary: "Remove a UI header.",
Responses: map[int][]framework.Response{
http.StatusNoContent: {{
Description: "OK",
}},
},
},
},
@ -121,6 +190,17 @@ func (b *SystemBackend) configPaths() []*framework.Path {
logical.ListOperation: &framework.PathOperation{
Callback: b.handleConfigUIHeadersList,
Summary: "Return a list of configured UI headers.",
Responses: map[int][]framework.Response{
http.StatusOK: {{
Fields: map[string]*framework.FieldSchema{
"keys": {
Type: framework.TypeCommaStringSlice,
Description: "Lists of configured UI headers. Omitted if list is empty",
Required: false,
},
},
}},
},
},
},
@ -139,13 +219,112 @@ func (b *SystemBackend) configPaths() []*framework.Path {
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Summary: "Read the configuration and progress of the current root generation attempt.",
Responses: map[int][]framework.Response{
http.StatusOK: {{
Description: "OK",
Fields: map[string]*framework.FieldSchema{
"nonce": {
Type: framework.TypeString,
Required: true,
},
"started": {
Type: framework.TypeBool,
Required: true,
},
"progress": {
Type: framework.TypeInt,
Required: true,
},
"required": {
Type: framework.TypeInt,
Required: true,
},
"complete": {
Type: framework.TypeBool,
Required: true,
},
"encoded_token": {
Type: framework.TypeString,
Required: true,
},
"encoded_root_token": {
Type: framework.TypeString,
Required: true,
},
"pgp_fingerprint": {
Type: framework.TypeString,
Required: true,
},
"otp": {
Type: framework.TypeString,
Required: true,
},
"otp_length": {
Type: framework.TypeInt,
Required: true,
},
},
}},
},
},
logical.UpdateOperation: &framework.PathOperation{
Summary: "Initializes a new root generation attempt.",
Description: "Only a single root generation attempt can take place at a time. One (and only one) of otp or pgp_key are required.",
Responses: map[int][]framework.Response{
http.StatusOK: {{
Description: "OK",
Fields: map[string]*framework.FieldSchema{
"nonce": {
Type: framework.TypeString,
Required: true,
},
"started": {
Type: framework.TypeBool,
Required: true,
},
"progress": {
Type: framework.TypeInt,
Required: true,
},
"required": {
Type: framework.TypeInt,
Required: true,
},
"complete": {
Type: framework.TypeBool,
Required: true,
},
"encoded_token": {
Type: framework.TypeString,
Required: true,
},
"encoded_root_token": {
Type: framework.TypeString,
Required: true,
},
"pgp_fingerprint": {
Type: framework.TypeString,
Required: true,
},
"otp": {
Type: framework.TypeString,
Required: true,
},
"otp_length": {
Type: framework.TypeInt,
Required: true,
},
},
}},
},
},
logical.DeleteOperation: &framework.PathOperation{
Summary: "Cancels any in-progress root generation attempt.",
Responses: map[int][]framework.Response{
http.StatusNoContent: {{
Description: "OK",
}},
},
},
},
@ -168,6 +347,53 @@ func (b *SystemBackend) configPaths() []*framework.Path {
logical.UpdateOperation: &framework.PathOperation{
Summary: "Enter a single unseal key share to progress the root generation attempt.",
Description: "If the threshold number of unseal key shares is reached, Vault will complete the root generation and issue the new token. Otherwise, this API must be called multiple times until that threshold is met. The attempt nonce must be provided with each call.",
Responses: map[int][]framework.Response{
http.StatusOK: {{
Description: "OK",
Fields: map[string]*framework.FieldSchema{
"nonce": {
Type: framework.TypeString,
Required: true,
},
"started": {
Type: framework.TypeBool,
Required: true,
},
"progress": {
Type: framework.TypeInt,
Required: true,
},
"required": {
Type: framework.TypeInt,
Required: true,
},
"complete": {
Type: framework.TypeBool,
Required: true,
},
"encoded_token": {
Type: framework.TypeString,
Required: true,
},
"encoded_root_token": {
Type: framework.TypeString,
Required: true,
},
"pgp_fingerprint": {
Type: framework.TypeString,
Required: true,
},
"otp": {
Type: framework.TypeString,
Required: true,
},
"otp_length": {
Type: framework.TypeInt,
Required: true,
},
},
}},
},
},
},

View File

@ -75,6 +75,7 @@ func TestSystemBackend_RootPaths(t *testing.T) {
func TestSystemConfigCORS(t *testing.T) {
b := testSystemBackend(t)
paths := b.(*SystemBackend).configPaths()
_, barrier, _ := mockBarrier(t)
view := NewBarrierView(barrier, "")
b.(*SystemBackend).Core.systemBarrierView = view
@ -104,37 +105,67 @@ func TestSystemConfigCORS(t *testing.T) {
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
schema.ValidateResponse(
t,
schema.FindResponseSchema(t, paths, 0, req.Operation),
actual,
true,
)
// Do it again. Bug #6182
req = logical.TestRequest(t, logical.UpdateOperation, "config/cors")
req.Data["allowed_origins"] = "http://www.example.com"
req.Data["allowed_headers"] = "X-Custom-Header"
_, err = b.HandleRequest(namespace.RootContext(nil), req)
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
if err != nil {
t.Fatal(err)
}
schema.ValidateResponse(
t,
schema.FindResponseSchema(t, paths, 0, req.Operation),
resp,
true,
)
req = logical.TestRequest(t, logical.ReadOperation, "config/cors")
actual, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil {
t.Fatalf("err: %v", err)
}
schema.ValidateResponse(
t,
schema.FindResponseSchema(t, paths, 0, req.Operation),
actual,
true,
)
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
req = logical.TestRequest(t, logical.DeleteOperation, "config/cors")
_, err = b.HandleRequest(namespace.RootContext(nil), req)
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil {
t.Fatalf("err: %v", err)
}
schema.ValidateResponse(
t,
schema.FindResponseSchema(t, paths, 0, req.Operation),
resp,
true,
)
req = logical.TestRequest(t, logical.ReadOperation, "config/cors")
actual, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil {
t.Fatalf("err: %v", err)
}
schema.ValidateResponse(
t,
schema.FindResponseSchema(t, paths, 0, req.Operation),
actual,
true,
)
expected = &logical.Response{
Data: map[string]interface{}{