Skip to content

Commit

Permalink
Adds JSON Schema validation to the gateway
Browse files Browse the repository at this point in the history
Fixes #1163
Based on #1180

Adds a new JSON Validation middleware that can be configured as follows:

```
"version_data": {
  "not_versioned": true,
  "versions": {
    "default": {
      "name": "default",
      "use_extended_paths": true,
      "extended_paths": {
        "validate_json": [{
          "method": "POST",
          "path": "me",
          "validate_with": {
            "title": "Person",
            "type": "object",
            "properties": {
              "firstName": {
                "type": "string"
              },
              "lastName": {
                "type": "string"
              },
              "age": {
                "description": "Age in years",
                "type": "integer",
                "minimum": 0
              }
            },
            "required": ["firstName", "lastName"]
          }
        }]
      }
    }
  }
},
```

The schema must be a draft v4 JSON Schema spec. The gateway will attempt
to validate the inbound request against it, if fields are failing the
validation process, a detailed error response is provided for the user
to fix their payload.

This will require a new Dashboard UI to handle input.

make Base64 Schema readable in tests

removing base64 as not necessary

using make for known length slice
  • Loading branch information
lonelycode authored and asoorm committed Jan 31, 2018
1 parent 0915506 commit 8b894fb
Show file tree
Hide file tree
Showing 7 changed files with 333 additions and 8 deletions.
26 changes: 26 additions & 0 deletions api_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const (
MethodTransformed
RequestTracked
RequestNotTracked
ValidateJSONRequest
)

// RequestStatus is a custom type to avoid collisions
Expand Down Expand Up @@ -80,6 +81,7 @@ const (
StatusRequestSizeControlled RequestStatus = "Request Size Limited"
StatusRequesTracked RequestStatus = "Request Tracked"
StatusRequestNotTracked RequestStatus = "Request Not Tracked"
StatusValidateJSON RequestStatus = "Validate JSON"
)

// URLSpec represents a flattened specification for URLs, used to check if a proxy URL
Expand All @@ -101,6 +103,7 @@ type URLSpec struct {
MethodTransform apidef.MethodTransformMeta
TrackEndpoint apidef.TrackEndpointMeta
DoNotTrackEndpoint apidef.TrackEndpointMeta
ValidatePathMeta apidef.ValidatePathMeta
}

type TransformSpec struct {
Expand Down Expand Up @@ -701,6 +704,20 @@ func (a APIDefinitionLoader) compileTrackedEndpointPathspathSpec(paths []apidef.
return urlSpec
}

func (a APIDefinitionLoader) compileValidateJSONPathspathSpec(paths []apidef.ValidatePathMeta, stat URLStatus) []URLSpec {
urlSpec := make([]URLSpec, len(paths))

for i, stringSpec := range paths {
newSpec := URLSpec{}
a.generateRegex(stringSpec.Path, &newSpec, stat)
// Extend with method actions
newSpec.ValidatePathMeta = stringSpec
urlSpec[i] = newSpec
}

return urlSpec
}

func (a APIDefinitionLoader) compileUnTrackedEndpointPathspathSpec(paths []apidef.TrackEndpointMeta, stat URLStatus) []URLSpec {
urlSpec := []URLSpec{}

Expand Down Expand Up @@ -734,6 +751,7 @@ func (a APIDefinitionLoader) getExtendedPathSpecs(apiVersionDef apidef.VersionIn
methodTransforms := a.compileMethodTransformSpec(apiVersionDef.ExtendedPaths.MethodTransforms, MethodTransformed)
trackedPaths := a.compileTrackedEndpointPathspathSpec(apiVersionDef.ExtendedPaths.TrackEndpoints, RequestTracked)
unTrackedPaths := a.compileUnTrackedEndpointPathspathSpec(apiVersionDef.ExtendedPaths.DoNotTrackEndpoints, RequestNotTracked)
validateJSON := a.compileValidateJSONPathspathSpec(apiVersionDef.ExtendedPaths.ValidateJSON, ValidateJSONRequest)

combinedPath := []URLSpec{}
combinedPath = append(combinedPath, ignoredPaths...)
Expand All @@ -752,6 +770,7 @@ func (a APIDefinitionLoader) getExtendedPathSpecs(apiVersionDef apidef.VersionIn
combinedPath = append(combinedPath, methodTransforms...)
combinedPath = append(combinedPath, trackedPaths...)
combinedPath = append(combinedPath, unTrackedPaths...)
combinedPath = append(combinedPath, validateJSON...)

return combinedPath, len(whiteListPaths) > 0
}
Expand Down Expand Up @@ -797,6 +816,9 @@ func (a *APISpec) getURLStatus(stat URLStatus) RequestStatus {
return StatusRequesTracked
case RequestNotTracked:
return StatusRequestNotTracked
case ValidateJSONRequest:
return StatusValidateJSON

default:
log.Error("URL Status was not one of Ignored, Blacklist or WhiteList! Blocking.")
return EndPointNotAllowed
Expand Down Expand Up @@ -930,6 +952,10 @@ func (a *APISpec) CheckSpecMatchesStatus(r *http.Request, rxPaths []URLSpec, mod
if r.Method == v.DoNotTrackEndpoint.Method {
return true, &v.DoNotTrackEndpoint
}
case ValidateJSONRequest:
if r.Method == v.ValidatePathMeta.Method {
return true, &v.ValidatePathMeta
}
}
}
return false, nil
Expand Down
2 changes: 2 additions & 0 deletions api_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ func processSpec(spec *APISpec, apisByListen map[string]int,
mwAppendEnabled(&chainArray, &CertificateCheckMW{BaseMiddleware: baseMid})
mwAppendEnabled(&chainArray, &OrganizationMonitor{BaseMiddleware: baseMid})
mwAppendEnabled(&chainArray, &RateLimitForAPI{BaseMiddleware: baseMid})
mwAppendEnabled(&chainArray, &ValidateJSON{BaseMiddleware: baseMid})
mwAppendEnabled(&chainArray, &MiddlewareContextVars{BaseMiddleware: baseMid})
mwAppendEnabled(&chainArray, &VersionCheck{BaseMiddleware: baseMid})
mwAppendEnabled(&chainArray, &RequestSizeLimitMiddleware{baseMid})
Expand Down Expand Up @@ -441,6 +442,7 @@ func processSpec(spec *APISpec, apisByListen map[string]int,
mwAppendEnabled(&chainArray, &RateLimitForAPI{BaseMiddleware: baseMid})
mwAppendEnabled(&chainArray, &RateLimitAndQuotaCheck{baseMid})
mwAppendEnabled(&chainArray, &GranularAccessMiddleware{baseMid})
mwAppendEnabled(&chainArray, &ValidateJSON{BaseMiddleware: baseMid})
mwAppendEnabled(&chainArray, &TransformMiddleware{baseMid})
mwAppendEnabled(&chainArray, &TransformHeaders{BaseMiddleware: baseMid})
mwAppendEnabled(&chainArray, &URLRewriteMiddleware{BaseMiddleware: baseMid})
Expand Down
9 changes: 9 additions & 0 deletions apidef/api_definitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,14 @@ type MethodTransformMeta struct {
ToMethod string `bson:"to_method" json:"to_method"`
}

type ValidatePathMeta struct {
Path string `bson:"path" json:"path"`
Method string `bson:"method" json:"method"`
ValidateWith map[string]interface{} `json:"validate_with"`
// Allows override of default 422 Unprocessible Entity response code for validation errors.
ValidationErrorResponseCode int `bson:"validation_error_response_code" json:"validation_error_response_code"`
}

type ExtendedPathsSet struct {
Ignored []EndPointMeta `bson:"ignored" json:"ignored,omitempty"`
WhiteList []EndPointMeta `bson:"white_list" json:"white_list,omitempty"`
Expand All @@ -182,6 +190,7 @@ type ExtendedPathsSet struct {
MethodTransforms []MethodTransformMeta `bson:"method_transforms" json:"method_transforms,omitempty"`
TrackEndpoints []TrackEndpointMeta `bson:"track_endpoints" json:"track_endpoints,omitempty"`
DoNotTrackEndpoints []TrackEndpointMeta `bson:"do_not_track_endpoints" json:"do_not_track_endpoints,omitempty"`
ValidateJSON []ValidatePathMeta `bson:"validate_json" json:"validate_json,omitempty"`
}

type VersionInfo struct {
Expand Down
85 changes: 85 additions & 0 deletions mw_validate_json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package main

import (
"errors"
"fmt"
"io/ioutil"
"net/http"

"github.com/xeipuuv/gojsonschema"

"github.com/TykTechnologies/tyk/apidef"
)

type ValidateJSON struct {
BaseMiddleware
schemaLoader gojsonschema.JSONLoader
}

func (k *ValidateJSON) Name() string {
return "ValidateJSON"
}

func (k *ValidateJSON) EnabledForSpec() bool {
for _, v := range k.Spec.VersionData.Versions {
if len(v.ExtendedPaths.ValidateJSON) > 0 {
return true
}
}

return false
}

// ProcessRequest will run any checks on the request on the way through the system, return an error to have the chain fail
func (k *ValidateJSON) ProcessRequest(w http.ResponseWriter, r *http.Request, _ interface{}) (error, int) {

_, versionPaths, _, _ := k.Spec.Version(r)
found, meta := k.Spec.CheckSpecMatchesStatus(r, versionPaths, ValidateJSONRequest)
if !found {
return nil, http.StatusOK
}

vPathMeta := meta.(*apidef.ValidatePathMeta)
if vPathMeta.ValidateWith == nil {
return errors.New("no schemas to validate against"), http.StatusInternalServerError
}

rCopy := copyRequest(r)
bodyBytes, err := ioutil.ReadAll(rCopy.Body)
if err != nil {
return err, http.StatusInternalServerError
}
defer rCopy.Body.Close()

schema := vPathMeta.ValidateWith

result, err := k.validate(bodyBytes, schema)
if err != nil {
return err, http.StatusInternalServerError
}

if !result.Valid() {
errStr := "payload validation failed"
for _, desc := range result.Errors() {
errStr = fmt.Sprintf("%s, %s", errStr, desc)
}

if vPathMeta.ValidationErrorResponseCode == 0 {
vPathMeta.ValidationErrorResponseCode = http.StatusUnprocessableEntity
}

return errors.New(errStr), vPathMeta.ValidationErrorResponseCode
}

return nil, http.StatusOK
}

func (k *ValidateJSON) validate(input []byte, schema map[string]interface{}) (*gojsonschema.Result, error) {
inputLoader := gojsonschema.NewBytesLoader(input)

if k.schemaLoader == nil {
k.schemaLoader = gojsonschema.NewGoLoader(schema)
}

return gojsonschema.Validate(k.schemaLoader, inputLoader)
}
Loading

0 comments on commit 8b894fb

Please sign in to comment.