api/internal/core/store/validate.go (295 lines of code) (raw):
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package store
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"github.com/xeipuuv/gojsonschema"
"go.uber.org/zap/buffer"
"github.com/apisix/manager-api/internal/conf"
"github.com/apisix/manager-api/internal/core/entity"
"github.com/apisix/manager-api/internal/log"
)
type Validator interface {
Validate(obj interface{}) error
}
type JsonSchemaValidator struct {
schema *gojsonschema.Schema
}
func NewJsonSchemaValidator(jsonPath string) (Validator, error) {
bs, err := ioutil.ReadFile(jsonPath)
if err != nil {
return nil, fmt.Errorf("get abs path failed: %s", err)
}
s, err := gojsonschema.NewSchema(gojsonschema.NewStringLoader(string(bs)))
if err != nil {
return nil, fmt.Errorf("new schema failed: %s", err)
}
return &JsonSchemaValidator{
schema: s,
}, nil
}
func (v *JsonSchemaValidator) Validate(obj interface{}) error {
ret, err := v.schema.Validate(gojsonschema.NewGoLoader(obj))
if err != nil {
return fmt.Errorf("validate failed: %s", err)
}
if !ret.Valid() {
errString := buffer.Buffer{}
for i, vErr := range ret.Errors() {
if i != 0 {
errString.AppendString("\n")
}
errString.AppendString(vErr.String())
}
return errors.New(errString.String())
}
return nil
}
type APISIXJsonSchemaValidator struct {
schema *gojsonschema.Schema
schemaDef string
}
func NewAPISIXJsonSchemaValidator(jsonPath string) (Validator, error) {
schemaDef := conf.Schema.Get(jsonPath).String()
if schemaDef == "" {
log.Errorf("schema validate failed: schema not found, path: %s", jsonPath)
return nil, fmt.Errorf("schema validate failed: schema not found, path: %s", jsonPath)
}
s, err := gojsonschema.NewSchema(gojsonschema.NewStringLoader(schemaDef))
if err != nil {
log.Errorf("new schema failed: %s", err)
return nil, fmt.Errorf("new schema failed: %s", err)
}
return &APISIXJsonSchemaValidator{
schema: s,
schemaDef: schemaDef,
}, nil
}
func getPlugins(reqBody interface{}) (map[string]interface{}, string) {
switch bodyType := reqBody.(type) {
case *entity.Route:
log.Infof("type of reqBody: %#v", bodyType)
route := reqBody.(*entity.Route)
return route.Plugins, "schema"
case *entity.Service:
log.Infof("type of reqBody: %#v", bodyType)
service := reqBody.(*entity.Service)
return service.Plugins, "schema"
case *entity.Consumer:
log.Infof("type of reqBody: %#v", bodyType)
consumer := reqBody.(*entity.Consumer)
return consumer.Plugins, "consumer_schema"
}
return nil, ""
}
func cHashKeySchemaCheck(upstream *entity.UpstreamDef) error {
if upstream.HashOn == "consumer" {
return nil
}
if upstream.HashOn != "vars" &&
upstream.HashOn != "header" &&
upstream.HashOn != "cookie" {
return fmt.Errorf("invalid hash_on type: %s", upstream.HashOn)
}
var schemaDef string
if upstream.HashOn == "vars" {
schemaDef = conf.Schema.Get("main.upstream_hash_vars_schema").String()
if schemaDef == "" {
return fmt.Errorf("schema validate failed: schema not found, path: main.upstream_hash_vars_schema")
}
}
if upstream.HashOn == "header" || upstream.HashOn == "cookie" {
schemaDef = conf.Schema.Get("main.upstream_hash_header_schema").String()
if schemaDef == "" {
return fmt.Errorf("schema validate failed: schema not found, path: main.upstream_hash_header_schema")
}
}
s, err := gojsonschema.NewSchema(gojsonschema.NewStringLoader(schemaDef))
if err != nil {
return fmt.Errorf("schema validate failed: %s", err)
}
ret, err := s.Validate(gojsonschema.NewGoLoader(upstream.Key))
if err != nil {
return fmt.Errorf("schema validate failed: %s", err)
}
if !ret.Valid() {
errString := buffer.Buffer{}
for i, vErr := range ret.Errors() {
if i != 0 {
errString.AppendString("\n")
}
errString.AppendString(vErr.String())
}
return fmt.Errorf("schema validate failed: %s", errString.String())
}
return nil
}
func checkUpstream(upstream *entity.UpstreamDef) error {
if upstream == nil {
return nil
}
if upstream.PassHost == "node" && upstream.Nodes != nil {
nodes, ok := entity.NodesFormat(upstream.Nodes).([]*entity.Node)
if !ok {
return fmt.Errorf("upstrams nodes not support value %v when `pass_host` is `node`", nodes)
} else if len(nodes) != 1 {
return fmt.Errorf("only support single node for `node` mode currentlywhen `pass_host` is `node`")
}
}
if upstream.PassHost == "rewrite" && upstream.UpstreamHost == "" {
return fmt.Errorf("`upstream_host` can't be empty when `pass_host` is `rewrite`")
}
if upstream.Type != "chash" {
return nil
}
//to confirm
if upstream.HashOn == "" {
upstream.HashOn = "vars"
}
if upstream.HashOn != "consumer" && upstream.Key == "" {
return fmt.Errorf("missing key")
}
if err := cHashKeySchemaCheck(upstream); err != nil {
return err
}
return nil
}
func checkRemoteAddr(remoteAddrs []string) error {
for _, remoteAddr := range remoteAddrs {
if remoteAddr == "" {
return fmt.Errorf("schema validate failed: invalid field remote_addrs")
}
}
return nil
}
func checkConf(reqBody interface{}) error {
switch bodyType := reqBody.(type) {
case *entity.Route:
route := reqBody.(*entity.Route)
log.Infof("type of reqBody: %#v", bodyType)
if err := checkUpstream(route.Upstream); err != nil {
return err
}
// todo: this is a temporary method, we'll drop it later
if err := checkRemoteAddr(route.RemoteAddrs); err != nil {
return err
}
case *entity.Service:
service := reqBody.(*entity.Service)
if err := checkUpstream(service.Upstream); err != nil {
return err
}
case *entity.Upstream:
upstream := reqBody.(*entity.Upstream)
if err := checkUpstream(&upstream.UpstreamDef); err != nil {
return err
}
}
return nil
}
func (v *APISIXJsonSchemaValidator) Validate(obj interface{}) error {
ret, err := v.schema.Validate(gojsonschema.NewGoLoader(obj))
if err != nil {
log.Errorf("schema validate failed: %s, s: %v, obj: %v", err, v.schema, obj)
return fmt.Errorf("schema validate failed: %s", err)
}
if !ret.Valid() {
errString := buffer.Buffer{}
for i, vErr := range ret.Errors() {
if i != 0 {
errString.AppendString("\n")
}
errString.AppendString(vErr.String())
}
log.Errorf("schema validate failed:s: %v, obj: %#v", v.schemaDef, obj)
return fmt.Errorf("schema validate failed: %s", errString.String())
}
//custom check
if err := checkConf(obj); err != nil {
return err
}
plugins, schemaType := getPlugins(obj)
for pluginName, pluginConf := range plugins {
schemaValue := conf.Schema.Get("plugins." + pluginName + "." + schemaType).Value()
if schemaValue == nil && schemaType == "consumer_schema" {
schemaValue = conf.Schema.Get("plugins." + pluginName + ".schema").Value()
}
if schemaValue == nil {
log.Errorf("schema validate failed: schema not found, %s, %s", "plugins."+pluginName, schemaType)
return fmt.Errorf("schema validate failed: schema not found, path: %s", "plugins."+pluginName)
}
schemaMap := schemaValue.(map[string]interface{})
schemaByte, err := json.Marshal(schemaMap)
if err != nil {
log.Warnf("schema validate failed: schema json encode failed, path: %s, %w", "plugins."+pluginName, err)
return fmt.Errorf("schema validate failed: schema json encode failed, path: %s, %w", "plugins."+pluginName, err)
}
s, err := gojsonschema.NewSchema(gojsonschema.NewBytesLoader(schemaByte))
if err != nil {
log.Errorf("init schema validate failed: %s", err)
return fmt.Errorf("schema validate failed: %s", err)
}
// check property disable, if is bool, remove from json schema checking
conf := pluginConf.(map[string]interface{})
var exchange bool
disable, ok := conf["disable"]
if ok {
if fmt.Sprintf("%T", disable) == "bool" {
delete(conf, "disable")
exchange = true
}
}
// check schema
ret, err := s.Validate(gojsonschema.NewGoLoader(conf))
if err != nil {
log.Errorf("schema validate failed: %s", err)
return fmt.Errorf("schema validate failed: %s", err)
}
// put the value back to the property disable
if exchange {
conf["disable"] = disable
}
if !ret.Valid() {
errString := buffer.Buffer{}
for i, vErr := range ret.Errors() {
if i != 0 {
errString.AppendString("\n")
}
errString.AppendString(vErr.String())
}
return fmt.Errorf("schema validate failed: %s", errString.String())
}
}
return nil
}
type APISIXSchemaValidator struct {
schema *gojsonschema.Schema
}
func NewAPISIXSchemaValidator(jsonPath string) (Validator, error) {
schemaDef := conf.Schema.Get(jsonPath).String()
if schemaDef == "" {
log.Warnf("schema validate failed: schema not found, path: %s", jsonPath)
return nil, fmt.Errorf("schema validate failed: schema not found, path: %s", jsonPath)
}
s, err := gojsonschema.NewSchema(gojsonschema.NewStringLoader(schemaDef))
if err != nil {
log.Warnf("new schema failed: %w", err)
return nil, fmt.Errorf("new schema failed: %w", err)
}
return &APISIXSchemaValidator{
schema: s,
}, nil
}
func (v *APISIXSchemaValidator) Validate(obj interface{}) error {
ret, err := v.schema.Validate(gojsonschema.NewBytesLoader(obj.([]byte)))
if err != nil {
log.Warnf("schema validate failed: %w", err)
return fmt.Errorf("schema validate failed: %w", err)
}
if !ret.Valid() {
errString := buffer.Buffer{}
for i, vErr := range ret.Errors() {
if i != 0 {
errString.AppendString("\n")
}
errString.AppendString(vErr.String())
}
return fmt.Errorf("schema validate failed: %s", errString.String())
}
return nil
}