From 448f5dd33e7ee818dcbbdec190b2921d6d394972 Mon Sep 17 00:00:00 2001 From: Daniel Huckins Date: Thu, 16 Feb 2023 15:06:26 -0500 Subject: [PATCH] VAULT-12112: add openapi response structures for /sys/config and /sys/generate-root endpoints (#18472) * some config responses Signed-off-by: Daniel Huckins * added response structs Signed-off-by: Daniel Huckins * added changelog * add test for config/cors Signed-off-by: Daniel Huckins * add (failing) tests Signed-off-by: Daniel Huckins * copy-pasta err Signed-off-by: Daniel Huckins * update tests for /sys/config/ui/headers/{header} Signed-off-by: Daniel Huckins --------- Signed-off-by: Daniel Huckins --- changelog/18472.txt | 3 + vault/custom_response_headers_test.go | 26 ++- vault/logical_system_paths.go | 226 ++++++++++++++++++++++++++ vault/logical_system_test.go | 35 +++- 4 files changed, 286 insertions(+), 4 deletions(-) create mode 100644 changelog/18472.txt diff --git a/changelog/18472.txt b/changelog/18472.txt new file mode 100644 index 000000000..e34d53afc --- /dev/null +++ b/changelog/18472.txt @@ -0,0 +1,3 @@ +```release-note:improvement +openapi: add openapi response defintions to /sys/config and /sys/generate-root endpoints +``` \ No newline at end of file diff --git a/vault/custom_response_headers_test.go b/vault/custom_response_headers_test.go index e5844ae19..b6fe85ae2 100644 --- a/vault/custom_response_headers_test.go +++ b/vault/custom_response_headers_test.go @@ -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 { diff --git a/vault/logical_system_paths.go b/vault/logical_system_paths.go index 4fffcdd30..670e80961 100644 --- a/vault/logical_system_paths.go +++ b/vault/logical_system_paths.go @@ -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, + }, + }, + }}, + }, }, }, diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index 68e6b50a1..b3a77619a 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -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{}{