Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add upstream request/response validation with openapi (#21) #22

Merged
merged 30 commits into from
Dec 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2583126
upstream validation: implementation, first try (#21)
Sep 17, 2020
062a9e4
add upstream swagger file, referenced in couper.hcl (#21)
Sep 17, 2020
a16854c
rename swagger_definition -> openapi_file (#21)
Sep 18, 2020
0e9bc88
openapi block with properties instead of properties only; request/res…
Oct 8, 2020
5e5c373
invalid upstream request gets 400 - Bad Request; invalid upstream res…
Oct 8, 2020
8622993
keep backend status code in log even in case of response validation e…
Oct 8, 2020
f46c641
validation message in backend log's message field (#21)
Oct 8, 2020
f8839d6
tests for validation (#21)
Oct 9, 2020
627fa8c
extracted OpenAPI validator (#21)
Oct 9, 2020
d1a762d
Fixed validation errors in openapi file to be logged as errors instea…
Oct 14, 2020
92fe110
Fixup openAPI validation test
Dec 2, 2020
02dd6f3
Handle body rewind
Dec 4, 2020
b023a88
Fix set getBody method first #72
Dec 7, 2020
d318402
Add buffer stringer implementation
Dec 7, 2020
99d19c8
Add test for bufferOption interaction
Dec 7, 2020
b6f203e
rm httpbin.yaml
Dec 7, 2020
8e85907
Fix documentation hcl format
Dec 8, 2020
682d51a
Remove loose punctuation mark from documentation
Dec 8, 2020
121edd1
Add openapi documentation and example link
Dec 8, 2020
959bc8d
Fixup obsolete conditions
Dec 9, 2020
543e5ef
Add validation exclude options
Dec 10, 2020
04a8ad5
Fixup validation tests
Dec 10, 2020
3b2166f
Update validation documentation
Dec 10, 2020
4c4e7b5
Use req context
Dec 10, 2020
b96ed86
Fix merge openAPI and use partialContent for deprecated log
Dec 10, 2020
a57c2d7
Upgrade kin-openapi dependency to latest v0.33.0
Dec 11, 2020
82c1bae
Add documentation note about openapi3
Dec 11, 2020
c4660a4
Add additional openapi test
Dec 14, 2020
ee45da9
Revert configurable validation exclude options
Dec 14, 2020
ba3cccd
Add additonal openapi link
Dec 14, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions config/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type Backend struct {
RequestBodyLimit string `hcl:"request_body_limit,optional"`
TTFBTimeout string `hcl:"ttfb_timeout,optional"`
Timeout string `hcl:"timeout,optional"`
OpenAPI *OpenAPI `hcl:"openapi,block"`
}

func (b Backend) Body() hcl.Body {
Expand Down Expand Up @@ -83,6 +84,10 @@ func (b *Backend) Merge(other *Backend) (*Backend, []hcl.Body) {
result.Timeout = other.Timeout
}

if other.OpenAPI != nil {
result.OpenAPI = other.OpenAPI
}

return &result, bodies
}

Expand Down
7 changes: 7 additions & 0 deletions config/openapi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package config

type OpenAPI struct {
File string `hcl:"file"`
IgnoreRequestViolations bool `hcl:"ignore_request_violations,optional"`
IgnoreResponseViolations bool `hcl:"ignore_response_violations,optional"`
}
11 changes: 6 additions & 5 deletions config/runtime/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (
"github.com/avenga/couper/utils"
)

var defaultBackendConf = &config.Backend{
var DefaultBackendConf = &config.Backend{
ConnectTimeout: "10s",
RequestBodyLimit: "64MiB",
TTFBTimeout: "60s",
Expand Down Expand Up @@ -152,6 +152,7 @@ func NewServerConfiguration(conf *config.Gateway, httpConf *HTTPConfig, log *log
if srvConf.API != nil {
// map backends to endpoint
endpoints := make(map[string]bool)

for _, endpoint := range srvConf.API.Endpoint {
pattern := utils.JoinPath("/", serverOptions.APIBasePath, endpoint.Pattern)

Expand Down Expand Up @@ -215,11 +216,11 @@ func newProxy(ctx *hcl.EvalContext, beConf *config.Backend, corsOpts *config.COR

for _, name := range []string{"request_headers", "response_headers"} {
for _, body := range remainCtx {
attr, err := body.JustAttributes()
content, _, err := body.PartialContent(config.Backend{}.Schema(true))
if err != nil {
return nil, err
}
if _, ok := attr[name]; ok {
if _, ok := content.Attributes[name]; ok {
log.Warningf("'%s' is deprecated, use 'set_%s' instead", name, name)
}
}
Expand Down Expand Up @@ -250,7 +251,7 @@ func newBackendsFromDefinitions(conf *config.Gateway, confCtx *hcl.EvalContext,
return nil, e
}

beConf, _ = defaultBackendConf.Merge(beConf)
beConf, _ = DefaultBackendConf.Merge(beConf)

srvOpts, _ := server.NewServerOptions(&config.Server{})
proxy, err := newProxy(confCtx, beConf, nil, []hcl.Body{beConf.Remain}, log, srvOpts)
Expand Down Expand Up @@ -445,7 +446,7 @@ func newInlineBackend(
bodies = append(bodies, backendConf.Body())
}

backendConf, _ = defaultBackendConf.Merge(backendConf)
backendConf, _ = DefaultBackendConf.Merge(backendConf)

// obtain the backend reference and merge with the current override
if inlineBlock != nil && len(inlineBlock.Labels) > 0 {
Expand Down
246 changes: 134 additions & 112 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,38 @@
# Couper Docs - Version 0.1
# Couper Docs - Version 0.2

## Table of contents

* [Introduction](#introduction)
* [Core concepts](#core_concepts)
* [Configuration file](#conf_file)
* [Syntax](#syntax)
* [File name](#file_name)
* [Basic file structure](#basic_conf)
* [Variables](#variables_conf)
* [Expressions](#expressions)
* [Functions](#functions)
* [Core concepts](#core_concepts)
* [Configuration file](#conf_file)
* [Syntax](#syntax)
* [File name](#file_name)
* [Basic file structure](#basic_conf)
* [Variables](#variables_conf)
* [Expressions](#expressions)
* [Functions](#functions)
* [Reference](#reference)
* [The `server` block](#server_block)
* [The `files` block](#files_block)
* [The `spa` block](#spa_block)
* [The `api` block](#api_block)
* [The `endpoint` block](#endpoint_block)
* [The `backend` block](#backend_block)
* [The `request` block](#request_block)
* [The `cors` block](#cors_block)
* [The `access_control` attribute](#access_control_attribute)
* [The `basic_auth` block](#basic_auth_block)
* [The `jwt` block](#jwt_block)
* [The `definitions` block](#definitions_block)
* [The `defaults` block](#defaults_block)
* [The `settings` block](#settings_block)
* [The `server` block](#server_block)
* [The `files` block](#files_block)
* [The `spa` block](#spa_block)
* [The `api` block](#api_block)
* [The `endpoint` block](#endpoint_block)
* [The `backend` block](#backend_block)
* [The `openapi` block](#openapi_block)
* [The `cors` block](#cors_block)
* [The `request` block](#request_block)
* [The `access_control` attribute](#access_control_attribute)
* [The `definitions` block](#definitions_block)
* [The `basic_auth` block](#basic_auth_block)
* [The `jwt` block](#jwt_block)
* [The `defaults` block](#defaults_block)
* [The `settings` block](#settings_block)
* [Examples](#examples)
* [Request routing](#request_routing_ex)
* [Routing configuration](#routing_conf_ex)
* [Web serving configuration](#web_serving_ex)
* [`access_control`configuration](#access_control_conf_ex)
* [`hosts` configuration](#hosts_conf_ex)
* [Request routing](#request_routing_ex)
* [Routing configuration](#routing_conf_ex)
* [Web serving configuration](#web_serving_ex)
* [`access_control`configuration](#access_control_conf_ex)
* [`hosts` configuration](#hosts_conf_ex)

## Introduction <a name="introduction"></a>
Couper is a frontend gateway especially designed to support building and running API-driven Web projects.
Expand Down Expand Up @@ -73,27 +74,31 @@ For orientation compare the following example and the information below:

```hcl
server "my_project" {
files {...}
spa {...}
api {
access_control = "foo"
endpoint "/bar" {
backend {...}
}
}
definitions {...}
files { ... }

spa { ... }

api {
access_control = "foo"
endpoint "/bar" {
backend { ... }
}
}
}

definitions { ... }
```

* `server`: main configuration block
* `files`: configuration block for file serving
* `spa`: configuration block for web serving (spa assets)
* `api`: configuration block that bundles endpoints under a certain base path
* `access_control`: attribute that sets access control for a block context
* `endpoint`: configuration block for Couper's entry points
* `backend`: configuration block for connection to local/remote backend service(s)
* `definitions`: block for predefined configurations, that can be referenced
* `defaults`: block for default configurations
* `settings`: block for server configuration which applies to the running instance
* `server` main configuration block
* `files` configuration block for file serving
* `spa` configuration block for web serving (spa assets)
* `api` configuration block that bundles endpoints under a certain base path
* `access_control` attribute that sets access control for a block context
* `endpoint` configuration block for Couper's entry points
* `backend` configuration block for connection to local/remote backend service(s)
* `definitions` block for predefined configurations, that can be referenced
* `defaults` block for default configurations
* `settings` block for server configuration which applies to the running instance

### Variables <a name="variables_conf"></a>

Expand Down Expand Up @@ -334,19 +339,20 @@ A `backend` defines the connection to a local/remote backend service. Backends c

| Name | Description |
|:-------------------|:---------------------------------------|
|context|<ul><li>`api` block</li><li>`endpoint` block</li><li>`definitions` block (reference purpose)</li></ul>|
| context|<ul><li>`api` block</li><li>`endpoint` block</li><li>`definitions` block (reference purpose)</li></ul>|
| *label*|<ul><li>&#9888; mandatory, when declared in `api` block</li><li>&#9888; mandatory, when declared in `definitions` block</li></ul>|
| `origin`| URL to connect to for backend requests </br> &#9888; must start with `http://...` |
|`base_path`|<ul><li>`base_path` for backend</li><li>won\`t change for `endpoint`</li></ul> |
|`hostname`| value of the HTTP host header field for the `origin` request. Since `hostname` replaces the request host the value will also be used for a server identity check during a TLS handshake with the origin. |
|`path`|changeable part of upstream URL|
|`timeout`| <ul><li>the total deadline duration a backend request has for write and read/pipe</li><li>valid time units are: "ns", "us" (or "µs"), "ms", "s", "m", "h"</li></ul> |
| `base_path`|<ul><li>`base_path` for backend</li><li>won\`t change for `endpoint`</li></ul> |
| `hostname`| value of the HTTP host header field for the `origin` request. Since `hostname` replaces the request host the value will also be used for a server identity check during a TLS handshake with the origin. |
| `path`|changeable part of upstream URL|
| `timeout`| <ul><li>the total deadline duration a backend request has for write and read/pipe</li><li>valid time units are: "ns", "us" (or "µs"), "ms", "s", "m", "h"</li></ul> |
| `set_request_headers` | header map to define additional or override header for the `origin` request |
| `set_response_headers` | same as `set_request_headers` for the client response |
| `request_body_limit` | Limit to configure the maximum buffer size while accessing `req.post` or `req.json_body` content. Valid units are: `KiB, MiB, GiB`. Default: `64MiB`. |
|[**`remove_query_params`**](#query_params)|<ul><li>a list of query parameters to be removed from the upstream request URL</li></ul> |
|[**`set_query_params`**](#query_params)|<ul><li>key/value(s) pairs to set query parameters in the upstream request URL</li></ul> |
|[**`add_query_params`**](#query_params)|<ul><li>key/value(s) pairs to add query parameters to the upstream request URL</li></ul> |
| [`openapi`](#openapi_block) | Definition for validating outgoing requests to the `origin` and incoming responses from the `origin`. |
| [`remove_query_params`](#query_params)|<ul><li>a list of query parameters to be removed from the upstream request URL</li></ul> |
| [`set_query_params`](#query_params)|<ul><li>key/value(s) pairs to set query parameters in the upstream request URL</li></ul> |
| [`add_query_params`](#query_params)|<ul><li>key/value(s) pairs to add query parameters to the upstream request URL</li></ul> |

### The `access_control` attribute <a name="access_control_attribute"></a>
The configuration of access control is twofold in Couper: You define the particular type (such as `jwt` or `basic_auth`) in `definitions`, each with a distinct label. Anywhere in the `server` block those labels can be used in the `access_control` list to protect that block.
Expand Down Expand Up @@ -384,6 +390,22 @@ The `jwt` block let you configure JSON Web Token access control for your gateway
|`signature_algorithm`| valid values are: `RS256` `RS384` `RS512` `HS256` `HS384` `HS512` |
|**`claims`**|equals/in comparison with JWT payload|

#### The `openapi` block <a name="openapi_block"></a>
The `openapi` block configures the backends proxy behaviour to validate outgoing and incoming requests to and from the origin.
Preventing the origin from invalid requests, and the Couper client from invalid answers. An example can be found [here](https://github.com/avenga/couper-examples/blob/master/backend-validation/README.md).
To do so Couper uses the [OpenAPI 3 standard](https://www.openapis.org/) to load the definitions from a given document
defined with the `file` attribute.

| Name | Description | Default |
|:-----------------------------|:---------------------------------------------------|:----------|
| context | `backend` block | |
| `file` | OpenAPI yaml definition file | mandatory |
| `ignore_request_violations` | log request validation results, skip err handling | `false` |
| `ignore_response_violations` | log response validation results, skip err handling | `false` |

**Caveats**: While ignoring request violations an invalid method or path would lead to a non-matching *route* which is still required
for response validations. In this case the response validation will fail if not ignored too.

### The `definitions` block <a name="definitions_block"></a>
Use the `definitions` block to define configurations you want to reuse. `access_control` is **always** defined in the `definitions` block.

Expand Down Expand Up @@ -425,84 +447,84 @@ The shutdown timings cannot be configured at this moment.

```hcl
api "my_api" {
base_path = "/api/novoconnect"

endpoint "/login/**" {
# incoming request: .../login/foo
# implicit proxy
# outgoing request: http://identityprovider:8080/login/foo
backend {
origin = "http://identityprovider:8080"
}
base_path = "/api/novoconnect"

endpoint "/login/**" {
# incoming request: .../login/foo
# implicit proxy
# outgoing request: http://identityprovider:8080/login/foo
backend {
origin = "http://identityprovider:8080"
}

endpoint "/cart/**" {
}

endpoint "/cart/**" {
# incoming request: .../cart/items
# outgoing request: http://cartservice:8080/api/v1/items
path = "/api/v1/**"
backend {
origin = "http://cartservice:8080"
}

endpoint "/account/{id}" {
# incoming request: .../account/brenda
# outgoing request: http://accountservice:8080/user/brenda/info
backend {
path = "/user/${req.param.id}/info"
origin = "http://accountservice:8080"
}

endpoint "/account/{id}" {
# incoming request: .../account/brenda
# outgoing request: http://accountservice:8080/user/brenda/info
backend {
path = "/user/${req.param.id}/info"
origin = "http://accountservice:8080"
}
}
}
}
```

### Web serving configuration example <a name="web_serving_ex"></a>
```hcl
server "my_project" {
files {
document_root = "./htdocs"
error_file = "./my_custom_error_page.html"
}
spa {
bootstrap_file = "./htdocs/index.html"
paths = [
"/app/**",
"/profile/**"
]
}
...
files {
document_root = "./htdocs"
error_file = "./my_custom_error_page.html"
}

spa {
bootstrap_file = "./htdocs/index.html"
paths = [
"/app/**",
"/profile/**"
]
}
}
```

### `access_control` configuration example <a name="access_control_conf_ex"></a>

```hcl
server {
access\_control = ["ac1"]
files {
access\_control = ["ac2"]
}
spa {}
api {
access\_control = ["ac3"]
endpoint "/foo" {
disable\_access_control = "ac3"
}
endpoint "/bar" {
access\_control = ["ac4"]
}
}
access_control = ["ac1"]
files {
access_control = ["ac2"]
}

spa {
bootstrap_file = "myapp.html"
}

api {
access_control = ["ac3"]
endpoint "/foo" {
disable_access_control = "ac3"
}
endpoint "/bar" {
access_control = ["ac4"]
}
}
}

definitions {
basic\_auth "ac1" {
...
}
jwt "ac2" {
...
}
jwt "ac3" {
...
}
jwt "ac4" {
...
}
basic_auth "ac1" { ... }
jwt "ac2" { ... }
jwt "ac3" { ... }
jwt "ac4" { ... }
}
```

Expand Down
Loading