mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
config: migrate server.encoding and server.decoding validation to Rego (#8903)
Follow-up to #8900. Moves the gzip encoding and decoding config validation off the Go `validateAndInjectDefaults` methods and onto embedded Rego policies, injecting defaults and reporting value errors. Each config registers its recognized options via `config.RegisterConfigSpec` so unknown-option warnings live with the owning struct. Field type validation stays in the Go decode step. --------- Signed-off-by: Sebastian Spaink <sebastianspaink@gmail.com>
This commit is contained in:
@@ -14,8 +14,11 @@ rules:
|
|||||||
- "docs"
|
- "docs"
|
||||||
- "internal/wasm/sdk/examples/*"
|
- "internal/wasm/sdk/examples/*"
|
||||||
- "v1/config/*.rego"
|
- "v1/config/*.rego"
|
||||||
|
- "internal/configpolicy/*.rego"
|
||||||
- "internal/metricsexport/*.rego"
|
- "internal/metricsexport/*.rego"
|
||||||
- "v1/plugins/server/metrics/*.rego"
|
- "v1/plugins/server/metrics/*.rego"
|
||||||
|
- "v1/plugins/server/encoding/*.rego"
|
||||||
|
- "v1/plugins/server/decoding/*.rego"
|
||||||
style:
|
style:
|
||||||
line-length:
|
line-length:
|
||||||
ignore:
|
ignore:
|
||||||
|
|||||||
+11
-2
@@ -7,6 +7,11 @@ set -euo pipefail
|
|||||||
|
|
||||||
OPA="${OPA:-opa}"
|
OPA="${OPA:-opa}"
|
||||||
|
|
||||||
|
# The config validation policies import the helpers in this module, which the Go
|
||||||
|
# layer compiles into every policy, so it is loaded alongside each directory here.
|
||||||
|
CONFIG_UTIL=internal/configpolicy/util.rego
|
||||||
|
CONFIG_UTIL_DIR="./$(dirname "$CONFIG_UTIL")"
|
||||||
|
|
||||||
dirs=$(find . -name '*_test.rego' \
|
dirs=$(find . -name '*_test.rego' \
|
||||||
-not -path './build/policy/*' \
|
-not -path './build/policy/*' \
|
||||||
-not -path '*/testdata/*' \
|
-not -path '*/testdata/*' \
|
||||||
@@ -23,8 +28,12 @@ fi
|
|||||||
|
|
||||||
status=0
|
status=0
|
||||||
for d in $dirs; do
|
for d in $dirs; do
|
||||||
echo "==> ${OPA} test ${d}"
|
paths=("$d")
|
||||||
"$OPA" test "$d" || status=1
|
if [ "$d" != "$CONFIG_UTIL_DIR" ]; then
|
||||||
|
paths+=("$CONFIG_UTIL")
|
||||||
|
fi
|
||||||
|
echo "==> ${OPA} test ${paths[*]}"
|
||||||
|
"$OPA" test "${paths[@]}" || status=1
|
||||||
done
|
done
|
||||||
|
|
||||||
exit "$status"
|
exit "$status"
|
||||||
|
|||||||
@@ -9,10 +9,14 @@
|
|||||||
// processed - the input config with defaults injected (required)
|
// processed - the input config with defaults injected (required)
|
||||||
// errors - a set/array of fatal error strings (joined into one error)
|
// errors - a set/array of fatal error strings (joined into one error)
|
||||||
// warnings - a set/array of non-fatal warning strings
|
// warnings - a set/array of non-fatal warning strings
|
||||||
|
//
|
||||||
|
// Every policy is compiled together with util.rego, so a policy can import
|
||||||
|
// data.opa.config.util for the helpers shared across the validation policies.
|
||||||
package configpolicy
|
package configpolicy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
_ "embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -26,6 +30,13 @@ import (
|
|||||||
"github.com/open-policy-agent/opa/v1/util"
|
"github.com/open-policy-agent/opa/v1/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// The shared helpers are compiled into every policy, so a validation policy can
|
||||||
|
// import data.opa.config.util rather than repeat them.
|
||||||
|
const utilModuleName = "opa/config/util.rego"
|
||||||
|
|
||||||
|
//go:embed util.rego
|
||||||
|
var utilModule string
|
||||||
|
|
||||||
// Policy is an embedded validation policy, compiled once on first use and
|
// Policy is an embedded validation policy, compiled once on first use and
|
||||||
// evaluated repeatedly. Safe for concurrent use.
|
// evaluated repeatedly. Safe for concurrent use.
|
||||||
type Policy struct {
|
type Policy struct {
|
||||||
@@ -110,14 +121,19 @@ func (p *Policy) Eval(ctx context.Context, input any) (map[string]any, []string,
|
|||||||
|
|
||||||
// EvalConfigInto decodes raw config bytes (absent/empty/null → empty object),
|
// EvalConfigInto decodes raw config bytes (absent/empty/null → empty object),
|
||||||
// evaluates the policy with input {"config": <raw>} to inject defaults, and
|
// evaluates the policy with input {"config": <raw>} to inject defaults, and
|
||||||
// decodes the processed config into out, returning any warnings. The typed
|
// decodes the processed config into out, returning any warnings. Field types the
|
||||||
// unmarshal into out is what enforces field types, so a mistyped option
|
// policy does not check are enforced by the typed unmarshal into out, so a
|
||||||
// surfaces here rather than in the policy.
|
// mistyped option surfaces here rather than in the policy.
|
||||||
func EvalConfigInto[T any](ctx context.Context, p *Policy, raw []byte, out *T) ([]string, error) {
|
func EvalConfigInto[T any](ctx context.Context, p *Policy, raw []byte, out *T) ([]string, error) {
|
||||||
rawConfig, err := unmarshalRawConfig(raw)
|
rawConfig, err := unmarshalRawConfig(raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if _, ok := rawConfig.(map[string]any); !ok {
|
||||||
|
// Caught here rather than in the policy, which would fail to produce a
|
||||||
|
// processed config and report the far less obvious infrastructure error.
|
||||||
|
return nil, fmt.Errorf("%s: config must be an object", p.name)
|
||||||
|
}
|
||||||
|
|
||||||
processed, warnings, err := p.Eval(ctx, map[string]any{"config": rawConfig})
|
processed, warnings, err := p.Eval(ctx, map[string]any{"config": rawConfig})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -160,8 +176,14 @@ func (p *Policy) Compiler() (*ast.Compiler, error) {
|
|||||||
p.compileErr = fmt.Errorf("%s: %w", p.name, err)
|
p.compileErr = fmt.Errorf("%s: %w", p.name, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
helpers, err := ast.ParseModuleWithOpts(utilModuleName, utilModule, popts)
|
||||||
|
if err != nil {
|
||||||
|
p.compileErr = fmt.Errorf("%s: %w", utilModuleName, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
modules := map[string]*ast.Module{p.name: module, utilModuleName: helpers}
|
||||||
compiler := ast.NewCompiler()
|
compiler := ast.NewCompiler()
|
||||||
if compiler.Compile(map[string]*ast.Module{p.name: module}); compiler.Failed() {
|
if compiler.Compile(modules); compiler.Failed() {
|
||||||
p.compileErr = fmt.Errorf("%s: %w", p.name, compiler.Errors)
|
p.compileErr = fmt.Errorf("%s: %w", p.name, compiler.Errors)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
package configpolicy
|
package configpolicy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -95,6 +96,36 @@ func TestPolicyCompilerCompilesOnce(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const helpersModule = `package test.helpers
|
||||||
|
|
||||||
|
import data.opa.config.util
|
||||||
|
|
||||||
|
processed := object.union_n(array.concat([input.config], [patch | some patch in _patches]))
|
||||||
|
|
||||||
|
_patches contains {"size": 10} if util.absent(["size"])
|
||||||
|
|
||||||
|
errors contains "size must be a positive number" if util.not_positive_number(["size"])
|
||||||
|
`
|
||||||
|
|
||||||
|
// The shared helpers are compiled into every policy, so a policy can use them
|
||||||
|
// without embedding its own copy.
|
||||||
|
func TestPolicySharedHelpers(t *testing.T) {
|
||||||
|
p := New("test/helpers.rego", helpersModule, "data.test.helpers = x")
|
||||||
|
|
||||||
|
processed, _, err := p.Eval(t.Context(), map[string]any{"config": map[string]any{}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(processed, map[string]any{"size": json.Number("10")}) {
|
||||||
|
t.Fatalf("expected the helper-guarded default to be injected, got %v", processed)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err = p.Eval(t.Context(), map[string]any{"config": map[string]any{"size": -1}})
|
||||||
|
if err == nil || err.Error() != "size must be a positive number" {
|
||||||
|
t.Fatalf("expected the helper to reject a non-positive value, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const intoModule = `package test.into
|
const intoModule = `package test.into
|
||||||
|
|
||||||
processed := object.union({"name": "default"}, input.config)
|
processed := object.union({"name": "default"}, input.config)
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# METADATA
|
||||||
|
# description: |
|
||||||
|
# Helpers shared by the embedded configuration validation policies. Compiled
|
||||||
|
# into every configpolicy.Policy, so a policy can import data.opa.config.util
|
||||||
|
# instead of carrying its own copy of these rules.
|
||||||
|
#
|
||||||
|
# The helpers read the raw configuration from input.config, the part of the
|
||||||
|
# input document every validation policy is given.
|
||||||
|
package opa.config.util
|
||||||
|
|
||||||
|
# METADATA
|
||||||
|
# description: the configured value at path, or null when the option is absent.
|
||||||
|
value(path) := object.get(input.config, path, null)
|
||||||
|
|
||||||
|
# METADATA
|
||||||
|
# description: |
|
||||||
|
# true when the option at path is missing or explicitly null, the cases where a
|
||||||
|
# default is injected, matching the pre-Rego behavior where a nil pointer was
|
||||||
|
# replaced with a default.
|
||||||
|
absent(path) if value(path) == null
|
||||||
|
|
||||||
|
# METADATA
|
||||||
|
# description: |
|
||||||
|
# true when the option at path is present but not an object. The shape of an
|
||||||
|
# option holding an object has to be checked in the policy rather than left to
|
||||||
|
# the Go unmarshal: the default patches would otherwise be merged over the bad
|
||||||
|
# value, silently replacing it with a well-formed object.
|
||||||
|
not_object(path) if {
|
||||||
|
v := value(path)
|
||||||
|
v != null
|
||||||
|
not is_object(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
# METADATA
|
||||||
|
# description: true when the option at path is present but not a number above zero.
|
||||||
|
not_positive_number(path) if {
|
||||||
|
v := value(path)
|
||||||
|
v != null
|
||||||
|
not _positive_number(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
_positive_number(v) if {
|
||||||
|
is_number(v)
|
||||||
|
v > 0
|
||||||
|
}
|
||||||
@@ -5,11 +5,13 @@
|
|||||||
# handled by unmarshaling into the Go struct.
|
# handled by unmarshaling into the Go struct.
|
||||||
#
|
#
|
||||||
# Input: {"config": <raw metrics_export config>}
|
# Input: {"config": <raw metrics_export config>}
|
||||||
# Entrypoints: processed (config + defaults), errors (fatal).
|
# Rules read by the Go layer: processed (config + defaults), errors (fatal).
|
||||||
package opa.config.metrics_export
|
package opa.config.metrics_export
|
||||||
|
|
||||||
import future.keywords.not
|
import future.keywords.not
|
||||||
|
|
||||||
|
import data.opa.config.util
|
||||||
|
|
||||||
_default_grpc_address := "localhost:4317"
|
_default_grpc_address := "localhost:4317"
|
||||||
|
|
||||||
_default_http_address := "localhost:4318"
|
_default_http_address := "localhost:4318"
|
||||||
@@ -31,19 +33,18 @@ _default_address := _default_grpc_address if lower(input.config.type) == "otlp/g
|
|||||||
_default_address := _default_http_address if lower(input.config.type) == "otlp/http"
|
_default_address := _default_http_address if lower(input.config.type) == "otlp/http"
|
||||||
|
|
||||||
# METADATA
|
# METADATA
|
||||||
# entrypoint: true
|
|
||||||
# description: the config with metrics_export defaults injected for absent options.
|
# description: the config with metrics_export defaults injected for absent options.
|
||||||
processed := object.union_n(array.concat([input.config], [patch | some patch in _patches]))
|
processed := object.union_n(array.concat([input.config], [patch | some patch in _patches]))
|
||||||
|
|
||||||
_patches contains {"address": _default_address} if _empty(["address"])
|
_patches contains {"address": _default_address} if _empty(["address"])
|
||||||
|
|
||||||
_patches contains {"export_interval_ms": _default_export_interval_ms} if _absent(["export_interval_ms"])
|
_patches contains {"export_interval_ms": _default_export_interval_ms} if util.absent(["export_interval_ms"])
|
||||||
|
|
||||||
_patches contains {"service_name": _default_service_name} if _empty(["service_name"])
|
_patches contains {"service_name": _default_service_name} if _empty(["service_name"])
|
||||||
|
|
||||||
_patches contains {"encryption": _default_encryption_scheme} if _empty(["encryption"])
|
_patches contains {"encryption": _default_encryption_scheme} if _empty(["encryption"])
|
||||||
|
|
||||||
_patches contains {"allow_insecure_tls": false} if _absent(["allow_insecure_tls"])
|
_patches contains {"allow_insecure_tls": false} if util.absent(["allow_insecure_tls"])
|
||||||
|
|
||||||
errors contains msg if {
|
errors contains msg if {
|
||||||
input.config.type
|
input.config.type
|
||||||
@@ -61,16 +62,12 @@ errors contains msg if {
|
|||||||
msg := $`unsupported metrics_export.encryption "{processed.encryption}"`
|
msg := $`unsupported metrics_export.encryption "{processed.encryption}"`
|
||||||
}
|
}
|
||||||
|
|
||||||
# _absent is true when the option at path is missing or explicitly null (a nil
|
|
||||||
# pointer in Go).
|
|
||||||
_absent(path) if object.get(input.config, path, null) == null
|
|
||||||
|
|
||||||
# _empty is true when a string option is missing, null, or the empty string,
|
# _empty is true when a string option is missing, null, or the empty string,
|
||||||
# matching the pre-Rego behavior that defaulted on "".
|
# matching the pre-Rego behavior that defaulted on "".
|
||||||
_empty(path) if not _nonempty(path)
|
_empty(path) if not _nonempty(path)
|
||||||
|
|
||||||
_nonempty(path) if {
|
_nonempty(path) if {
|
||||||
v := object.get(input.config, path, null)
|
v := util.value(path)
|
||||||
v != null
|
v != null
|
||||||
v != ""
|
v != ""
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-11
@@ -8,36 +8,30 @@
|
|||||||
# replace each plugin's own config validation.
|
# replace each plugin's own config validation.
|
||||||
#
|
#
|
||||||
# Input: {"config": <raw config>, "runtime": {"id", "version"}}
|
# Input: {"config": <raw config>, "runtime": {"id", "version"}}
|
||||||
# Entrypoints: processed (config + defaults), errors (fatal), warnings.
|
# Rules read by the Go layer: processed (config + defaults), errors (fatal), warnings.
|
||||||
package opa.config
|
package opa.config
|
||||||
|
|
||||||
|
import data.opa.config.util
|
||||||
|
|
||||||
_default_decision := "/system/main"
|
_default_decision := "/system/main"
|
||||||
|
|
||||||
_default_authorization_decision := "/system/authz/allow"
|
_default_authorization_decision := "/system/authz/allow"
|
||||||
|
|
||||||
# METADATA
|
# METADATA
|
||||||
# entrypoint: true
|
|
||||||
# description: |
|
# description: |
|
||||||
# The user config with every _patches fragment merged over it. A fragment
|
# The user config with every _patches fragment merged over it. A fragment
|
||||||
# wins where it overlaps, so enforced values (labels id/version) always apply,
|
# wins where it overlaps, so enforced values (labels id/version) always apply,
|
||||||
# while defaults are contributed only when the option is absent.
|
# while defaults are contributed only when the option is absent.
|
||||||
processed := object.union_n(array.concat([input.config], [patch | some patch in _patches]))
|
processed := object.union_n(array.concat([input.config], [patch | some patch in _patches]))
|
||||||
|
|
||||||
_patches contains {"default_decision": _default_decision} if _absent_or_null("default_decision")
|
_patches contains {"default_decision": _default_decision} if util.absent(["default_decision"])
|
||||||
|
|
||||||
_patches contains {"default_authorization_decision": _default_authorization_decision} if {
|
_patches contains {"default_authorization_decision": _default_authorization_decision} if {
|
||||||
_absent_or_null("default_authorization_decision")
|
util.absent(["default_authorization_decision"])
|
||||||
}
|
}
|
||||||
|
|
||||||
_patches contains {"labels": {"id": input.runtime.id, "version": input.runtime.version}}
|
_patches contains {"labels": {"id": input.runtime.id, "version": input.runtime.version}}
|
||||||
|
|
||||||
# _absent_or_null is true when a config field is missing or explicitly null. In
|
|
||||||
# both cases the default is injected, matching the pre-Rego behavior where a nil
|
|
||||||
# pointer was treated as unset.
|
|
||||||
_absent_or_null(field) if not input.config[field]
|
|
||||||
|
|
||||||
_absent_or_null(field) if input.config[field] == null
|
|
||||||
|
|
||||||
errors contains msg if {
|
errors contains msg if {
|
||||||
some field in {"default_decision", "default_authorization_decision"}
|
some field in {"default_decision", "default_authorization_decision"}
|
||||||
value := input.config[field]
|
value := input.config[field]
|
||||||
|
|||||||
@@ -17,16 +17,26 @@
|
|||||||
package decoding
|
package decoding
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"context"
|
||||||
|
_ "embed"
|
||||||
|
|
||||||
"github.com/open-policy-agent/opa/v1/util"
|
"github.com/open-policy-agent/opa/internal/configpolicy"
|
||||||
|
"github.com/open-policy-agent/opa/v1/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
//go:embed validate.rego
|
||||||
defaultMaxRequestLength = int64(268435456) // 256 MB
|
var validationModule string
|
||||||
defaultGzipMaxContentLength = int64(536870912) // 512 MB
|
|
||||||
|
var validationPolicy = configpolicy.New(
|
||||||
|
"opa/config/server/decoding/validate.rego",
|
||||||
|
validationModule,
|
||||||
|
"data.opa.config.server.decoding = x",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
config.RegisterConfigSpec(config.SpecsFromStruct[Config]("server", "decoding")...)
|
||||||
|
}
|
||||||
|
|
||||||
// Config represents the configuration for the Server.Decoding settings
|
// Config represents the configuration for the Server.Decoding settings
|
||||||
type Config struct {
|
type Config struct {
|
||||||
MaxLength *int64 `json:"max_length,omitempty"` // maximum request size that will be read, regardless of compression.
|
MaxLength *int64 `json:"max_length,omitempty"` // maximum request size that will be read, regardless of compression.
|
||||||
@@ -56,47 +66,15 @@ func (b *ConfigBuilder) WithBytes(config []byte) *ConfigBuilder {
|
|||||||
|
|
||||||
// Parse returns a valid Config object with defaults injected.
|
// Parse returns a valid Config object with defaults injected.
|
||||||
func (b *ConfigBuilder) Parse() (*Config, error) {
|
func (b *ConfigBuilder) Parse() (*Config, error) {
|
||||||
if b.raw == nil {
|
return b.ParseWithContext(context.Background())
|
||||||
defaultConfig := &Config{
|
}
|
||||||
MaxLength: &defaultMaxRequestLength,
|
|
||||||
Gzip: &Gzip{
|
|
||||||
MaxLength: &defaultGzipMaxContentLength,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return defaultConfig, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// ParseWithContext returns a valid Config object with defaults injected, using
|
||||||
|
// ctx to evaluate the validation policy.
|
||||||
|
func (b *ConfigBuilder) ParseWithContext(ctx context.Context) (*Config, error) {
|
||||||
var result Config
|
var result Config
|
||||||
|
if _, err := configpolicy.EvalConfigInto(ctx, validationPolicy, b.raw, &result); err != nil {
|
||||||
if err := util.Unmarshal(b.raw, &result); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return &result, nil
|
||||||
return &result, result.validateAndInjectDefaults()
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateAndInjectDefaults populates defaults if the fields are nil, then
|
|
||||||
// validates the config values.
|
|
||||||
func (c *Config) validateAndInjectDefaults() error {
|
|
||||||
if c.MaxLength == nil {
|
|
||||||
c.MaxLength = &defaultMaxRequestLength
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Gzip == nil {
|
|
||||||
c.Gzip = &Gzip{
|
|
||||||
MaxLength: &defaultGzipMaxContentLength,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if c.Gzip.MaxLength == nil {
|
|
||||||
c.Gzip.MaxLength = &defaultGzipMaxContentLength
|
|
||||||
}
|
|
||||||
|
|
||||||
if *c.MaxLength <= 0 {
|
|
||||||
return errors.New("invalid value for server.decoding.max_length field, should be a positive number")
|
|
||||||
}
|
|
||||||
if *c.Gzip.MaxLength <= 0 {
|
|
||||||
return errors.New("invalid value for server.decoding.gzip.max_length field, should be a positive number")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ package decoding
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/open-policy-agent/opa/v1/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConfigValidation(t *testing.T) {
|
func TestConfigValidation(t *testing.T) {
|
||||||
@@ -58,6 +62,14 @@ func TestConfigValidation(t *testing.T) {
|
|||||||
input: `{"max_length": 42, "gzip":{"max_length": 42}}`,
|
input: `{"max_length": 42, "gzip":{"max_length": 42}}`,
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
input: `{"gzip": [1, 2, 3]}`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{"gzip": "nope"}`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
@@ -106,3 +118,88 @@ func TestConfigValue(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestConfigRejectsWrongShapedOptions covers options the validation policy has to
|
||||||
|
// reject itself. Defaults are merged over the raw config, so an option that
|
||||||
|
// should hold an object would otherwise be silently replaced by the defaults
|
||||||
|
// instead of reaching the Go unmarshal that used to report the type error.
|
||||||
|
func TestConfigRejectsWrongShapedOptions(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: `{"gzip": [1, 2, 3]}`,
|
||||||
|
wantErr: "invalid value for server.decoding.gzip field, should be an object",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{"gzip": "nope"}`,
|
||||||
|
wantErr: "invalid value for server.decoding.gzip field, should be an object",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{"max_length": true}`,
|
||||||
|
wantErr: "invalid value for server.decoding.max_length field, should be a positive number",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{"max_length": "foobar"}`,
|
||||||
|
wantErr: "invalid value for server.decoding.max_length field, should be a positive number",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `[1, 2, 3]`,
|
||||||
|
wantErr: "config must be an object",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.input, func(t *testing.T) {
|
||||||
|
_, err := NewConfigBuilder().WithBytes([]byte(test.input)).Parse()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error containing %q, got none", test.wantErr)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), test.wantErr) {
|
||||||
|
t.Fatalf("expected error containing %q, got %q", test.wantErr, err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConfigNoWarningsForKnownDecodingOptions verifies the server.decoding spec
|
||||||
|
// registered by this package lets config.ParseConfig recognize the section's
|
||||||
|
// options. Without the registration the section is treated as open, so this also
|
||||||
|
// guards against the unknown-option warning below silently going missing.
|
||||||
|
func TestConfigNoWarningsForKnownDecodingOptions(t *testing.T) {
|
||||||
|
raw := []byte(`{"server": {"decoding": {"max_length": 5, "gzip": {"max_length": 42}}}}`)
|
||||||
|
conf, err := config.ParseConfig(raw, "id")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(conf.Warnings) != 0 {
|
||||||
|
t.Fatalf("expected no warnings, got %v", conf.Warnings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigWarnsOnUnknownDecodingOption(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
raw string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
raw: `{"server": {"decoding": {"typo": 5}}}`,
|
||||||
|
want: `unknown configuration option "server.decoding.typo" encountered`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
raw: `{"server": {"decoding": {"gzip": {"typo": 5}}}}`,
|
||||||
|
want: `unknown configuration option "server.decoding.gzip.typo" encountered`,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.raw, func(t *testing.T) {
|
||||||
|
conf, err := config.ParseConfig([]byte(tc.raw), "id")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !slices.Contains(conf.Warnings, tc.want) {
|
||||||
|
t.Fatalf("expected warning %q, got %v", tc.want, conf.Warnings)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# METADATA
|
||||||
|
# description: |
|
||||||
|
# Injects defaults and validates the server.decoding configuration (request
|
||||||
|
# size limits and gzip decompression). Evaluated by the decoding config
|
||||||
|
# builder. The policy rejects options of the wrong shape; the remaining field
|
||||||
|
# type validation is handled by unmarshaling into the Go struct.
|
||||||
|
#
|
||||||
|
# Input: {"config": <raw server.decoding config>}
|
||||||
|
# Rules read by the Go layer: processed (config + defaults), errors (fatal).
|
||||||
|
package opa.config.server.decoding
|
||||||
|
|
||||||
|
import data.opa.config.util
|
||||||
|
|
||||||
|
# Defaults mirror decoding/config.go.
|
||||||
|
_default_max_length := 268435456 # 256 MB
|
||||||
|
|
||||||
|
_default_gzip_max_length := 536870912 # 512 MB
|
||||||
|
|
||||||
|
# METADATA
|
||||||
|
# description: the config with decoding defaults injected for absent options.
|
||||||
|
processed := object.union_n(array.concat([input.config], [patch | some patch in _patches]))
|
||||||
|
|
||||||
|
_patches contains {"max_length": _default_max_length} if util.absent(["max_length"])
|
||||||
|
|
||||||
|
_patches contains {"gzip": {"max_length": _default_gzip_max_length}} if util.absent(["gzip", "max_length"])
|
||||||
|
|
||||||
|
errors contains "invalid value for server.decoding.max_length field, should be a positive number" if {
|
||||||
|
util.not_positive_number(["max_length"])
|
||||||
|
}
|
||||||
|
|
||||||
|
errors contains "invalid value for server.decoding.gzip field, should be an object" if {
|
||||||
|
util.not_object(["gzip"])
|
||||||
|
}
|
||||||
|
|
||||||
|
errors contains "invalid value for server.decoding.gzip.max_length field, should be a positive number" if {
|
||||||
|
util.not_positive_number(["gzip", "max_length"])
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package opa.config.server.decoding_test
|
||||||
|
|
||||||
|
import data.opa.config.server.decoding
|
||||||
|
|
||||||
|
# _non_numbers are values a max_length option must be rejected for, shared by the
|
||||||
|
# top-level and gzip cases.
|
||||||
|
_non_numbers := [
|
||||||
|
{"note": "string", "value": "foobar"},
|
||||||
|
{"note": "boolean", "value": true},
|
||||||
|
{"note": "array", "value": [1]},
|
||||||
|
]
|
||||||
|
|
||||||
|
test_injects_defaults[tc.note] if {
|
||||||
|
some tc in [
|
||||||
|
{"note": "empty config", "config": {}},
|
||||||
|
{"note": "gzip present", "config": {"gzip": {}}},
|
||||||
|
{"note": "max_length null", "config": {"max_length": null}},
|
||||||
|
{"note": "gzip.max_length null", "config": {"gzip": {"max_length": null}}},
|
||||||
|
]
|
||||||
|
|
||||||
|
result := decoding.processed with input as {"config": tc.config}
|
||||||
|
result.max_length == 268435456
|
||||||
|
result.gzip.max_length == 536870912
|
||||||
|
}
|
||||||
|
|
||||||
|
test_preserves_configured_values if {
|
||||||
|
raw := {"max_length": 5, "gzip": {"max_length": 42}}
|
||||||
|
result := decoding.processed with input as {"config": raw}
|
||||||
|
result.max_length == 5
|
||||||
|
result.gzip.max_length == 42
|
||||||
|
}
|
||||||
|
|
||||||
|
test_preserves_unknown_keys if {
|
||||||
|
result := decoding.processed with input as {"config": {"gzip": {"random_key": 0}}}
|
||||||
|
result.gzip.random_key == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
test_rejects_non_positive_gzip_max_length[tc.note] if {
|
||||||
|
some tc in [
|
||||||
|
{"note": "zero", "config": {"gzip": {"max_length": 0}}},
|
||||||
|
{"note": "negative", "config": {"gzip": {"max_length": -10}}},
|
||||||
|
]
|
||||||
|
|
||||||
|
result := decoding.errors with input as {"config": tc.config}
|
||||||
|
"invalid value for server.decoding.gzip.max_length field, should be a positive number" in result
|
||||||
|
}
|
||||||
|
|
||||||
|
test_rejects_non_positive_top_level_max_length[tc.note] if {
|
||||||
|
some tc in [
|
||||||
|
{"note": "zero", "config": {"max_length": 0}},
|
||||||
|
{"note": "negative", "config": {"max_length": -10}},
|
||||||
|
]
|
||||||
|
|
||||||
|
result := decoding.errors with input as {"config": tc.config}
|
||||||
|
"invalid value for server.decoding.max_length field, should be a positive number" in result
|
||||||
|
}
|
||||||
|
|
||||||
|
# A non-number max_length is rejected here rather than left to the Go unmarshal,
|
||||||
|
# so the message names the option instead of the Go field.
|
||||||
|
test_rejects_non_number_top_level_max_length[tc.note] if {
|
||||||
|
some tc in _non_numbers
|
||||||
|
|
||||||
|
result := decoding.errors with input as {"config": {"max_length": tc.value}}
|
||||||
|
"invalid value for server.decoding.max_length field, should be a positive number" in result
|
||||||
|
}
|
||||||
|
|
||||||
|
test_rejects_non_number_gzip_max_length[tc.note] if {
|
||||||
|
some tc in _non_numbers
|
||||||
|
|
||||||
|
result := decoding.errors with input as {"config": {"gzip": {"max_length": tc.value}}}
|
||||||
|
"invalid value for server.decoding.gzip.max_length field, should be a positive number" in result
|
||||||
|
}
|
||||||
|
|
||||||
|
# Without this check the gzip defaults would be merged over the bad value,
|
||||||
|
# silently replacing it with a well-formed object.
|
||||||
|
test_rejects_non_object_gzip[tc.note] if {
|
||||||
|
some tc in [
|
||||||
|
{"note": "array", "config": {"gzip": [1, 2, 3]}},
|
||||||
|
{"note": "string", "config": {"gzip": "nope"}},
|
||||||
|
{"note": "number", "config": {"gzip": 7}},
|
||||||
|
]
|
||||||
|
|
||||||
|
result := decoding.errors with input as {"config": tc.config}
|
||||||
|
"invalid value for server.decoding.gzip field, should be an object" in result
|
||||||
|
}
|
||||||
|
|
||||||
|
test_valid_config_has_no_errors if {
|
||||||
|
result := decoding.errors with input as {"config": {"max_length": 42, "gzip": {"max_length": 42}}}
|
||||||
|
count(result) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
test_empty_config_has_no_errors if {
|
||||||
|
result := decoding.errors with input as {"config": {}}
|
||||||
|
count(result) == 0
|
||||||
|
}
|
||||||
@@ -1,14 +1,25 @@
|
|||||||
package encoding
|
package encoding
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"compress/gzip"
|
"context"
|
||||||
"errors"
|
_ "embed"
|
||||||
|
|
||||||
"github.com/open-policy-agent/opa/v1/util"
|
"github.com/open-policy-agent/opa/internal/configpolicy"
|
||||||
|
"github.com/open-policy-agent/opa/v1/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
var defaultGzipMinLength = 1024
|
//go:embed validate.rego
|
||||||
var defaultGzipCompressionLevel = gzip.BestCompression
|
var validationModule string
|
||||||
|
|
||||||
|
var validationPolicy = configpolicy.New(
|
||||||
|
"opa/config/server/encoding/validate.rego",
|
||||||
|
validationModule,
|
||||||
|
"data.opa.config.server.encoding = x",
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
config.RegisterConfigSpec(config.SpecsFromStruct[Config]("server", "encoding")...)
|
||||||
|
}
|
||||||
|
|
||||||
// Config represents the configuration for the Server.Encoding settings
|
// Config represents the configuration for the Server.Encoding settings
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -39,53 +50,15 @@ func (b *ConfigBuilder) WithBytes(config []byte) *ConfigBuilder {
|
|||||||
|
|
||||||
// Parse returns a valid Config object with defaults injected.
|
// Parse returns a valid Config object with defaults injected.
|
||||||
func (b *ConfigBuilder) Parse() (*Config, error) {
|
func (b *ConfigBuilder) Parse() (*Config, error) {
|
||||||
if b.raw == nil {
|
return b.ParseWithContext(context.Background())
|
||||||
defaultConfig := &Config{
|
}
|
||||||
Gzip: &Gzip{
|
|
||||||
MinLength: &defaultGzipMinLength,
|
|
||||||
CompressionLevel: &defaultGzipCompressionLevel,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return defaultConfig, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// ParseWithContext returns a valid Config object with defaults injected, using
|
||||||
|
// ctx to evaluate the validation policy.
|
||||||
|
func (b *ConfigBuilder) ParseWithContext(ctx context.Context) (*Config, error) {
|
||||||
var result Config
|
var result Config
|
||||||
|
if _, err := configpolicy.EvalConfigInto(ctx, validationPolicy, b.raw, &result); err != nil {
|
||||||
if err := util.Unmarshal(b.raw, &result); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return &result, nil
|
||||||
return &result, result.validateAndInjectDefaults()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) validateAndInjectDefaults() error {
|
|
||||||
if c.Gzip == nil {
|
|
||||||
c.Gzip = &Gzip{
|
|
||||||
MinLength: &defaultGzipMinLength,
|
|
||||||
CompressionLevel: &defaultGzipCompressionLevel,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if c.Gzip.MinLength == nil {
|
|
||||||
c.Gzip.MinLength = &defaultGzipMinLength
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Gzip.CompressionLevel == nil {
|
|
||||||
c.Gzip.CompressionLevel = &defaultGzipCompressionLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
if *c.Gzip.MinLength <= 0 {
|
|
||||||
return errors.New("invalid value for server.encoding.gzip.min_length field, should be a positive number")
|
|
||||||
}
|
|
||||||
|
|
||||||
acceptedCompressionLevels := map[int]bool{
|
|
||||||
gzip.NoCompression: true,
|
|
||||||
gzip.BestSpeed: true,
|
|
||||||
gzip.BestCompression: true,
|
|
||||||
}
|
|
||||||
_, compressionLevelAccepted := acceptedCompressionLevels[*c.Gzip.CompressionLevel]
|
|
||||||
if !compressionLevelAccepted {
|
|
||||||
return errors.New("invalid value for server.encoding.gzip.compression_level field, accepted values are 0, 1 or 9")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ package encoding
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/open-policy-agent/opa/v1/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConfigValidation(t *testing.T) {
|
func TestConfigValidation(t *testing.T) {
|
||||||
@@ -58,6 +62,14 @@ func TestConfigValidation(t *testing.T) {
|
|||||||
input: `{"gzip":{"min_length": 42, "compression_level": 9}}`,
|
input: `{"gzip":{"min_length": 42, "compression_level": 9}}`,
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
input: `{"gzip": [1, 2, 3]}`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{"gzip": "nope"}`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
@@ -103,3 +115,88 @@ func TestConfigValue(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestConfigRejectsWrongShapedOptions covers options the validation policy has to
|
||||||
|
// reject itself. Defaults are merged over the raw config, so an option that
|
||||||
|
// should hold an object would otherwise be silently replaced by the defaults
|
||||||
|
// instead of reaching the Go unmarshal that used to report the type error.
|
||||||
|
func TestConfigRejectsWrongShapedOptions(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: `{"gzip": [1, 2, 3]}`,
|
||||||
|
wantErr: "invalid value for server.encoding.gzip field, should be an object",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{"gzip": 7}`,
|
||||||
|
wantErr: "invalid value for server.encoding.gzip field, should be an object",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{"gzip": {"min_length": true}}`,
|
||||||
|
wantErr: "invalid value for server.encoding.gzip.min_length field, should be a positive number",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{"gzip": {"min_length": "foobar"}}`,
|
||||||
|
wantErr: "invalid value for server.encoding.gzip.min_length field, should be a positive number",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `[1, 2, 3]`,
|
||||||
|
wantErr: "config must be an object",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.input, func(t *testing.T) {
|
||||||
|
_, err := NewConfigBuilder().WithBytes([]byte(test.input)).Parse()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error containing %q, got none", test.wantErr)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), test.wantErr) {
|
||||||
|
t.Fatalf("expected error containing %q, got %q", test.wantErr, err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConfigNoWarningsForKnownEncodingOptions verifies the server.encoding spec
|
||||||
|
// registered by this package lets config.ParseConfig recognize the section's
|
||||||
|
// options. Without the registration the section is treated as open, so this also
|
||||||
|
// guards against the unknown-option warning below silently going missing.
|
||||||
|
func TestConfigNoWarningsForKnownEncodingOptions(t *testing.T) {
|
||||||
|
raw := []byte(`{"server": {"encoding": {"gzip": {"min_length": 42, "compression_level": 1}}}}`)
|
||||||
|
conf, err := config.ParseConfig(raw, "id")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(conf.Warnings) != 0 {
|
||||||
|
t.Fatalf("expected no warnings, got %v", conf.Warnings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigWarnsOnUnknownEncodingOption(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
raw string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
raw: `{"server": {"encoding": {"typo": 5}}}`,
|
||||||
|
want: `unknown configuration option "server.encoding.typo" encountered`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
raw: `{"server": {"encoding": {"gzip": {"typo": 5}}}}`,
|
||||||
|
want: `unknown configuration option "server.encoding.gzip.typo" encountered`,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.raw, func(t *testing.T) {
|
||||||
|
conf, err := config.ParseConfig([]byte(tc.raw), "id")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !slices.Contains(conf.Warnings, tc.want) {
|
||||||
|
t.Fatalf("expected warning %q, got %v", tc.want, conf.Warnings)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# METADATA
|
||||||
|
# description: |
|
||||||
|
# Injects defaults and validates the server.encoding configuration (gzip
|
||||||
|
# response compression). Evaluated by the encoding config builder. The policy
|
||||||
|
# rejects options of the wrong shape; the remaining field type validation is
|
||||||
|
# handled by unmarshaling into the Go struct.
|
||||||
|
#
|
||||||
|
# Input: {"config": <raw server.encoding config>}
|
||||||
|
# Rules read by the Go layer: processed (config + defaults), errors (fatal).
|
||||||
|
package opa.config.server.encoding
|
||||||
|
|
||||||
|
import data.opa.config.util
|
||||||
|
|
||||||
|
# Defaults mirror encoding/config.go.
|
||||||
|
_default_min_length := 1024
|
||||||
|
|
||||||
|
_default_compression_level := 9
|
||||||
|
|
||||||
|
# _accepted_compression_levels mirrors gzip.NoCompression, gzip.BestSpeed and
|
||||||
|
# gzip.BestCompression.
|
||||||
|
_accepted_compression_levels := {0, 1, 9}
|
||||||
|
|
||||||
|
# METADATA
|
||||||
|
# description: the config with gzip encoding defaults injected for absent options.
|
||||||
|
processed := object.union_n(array.concat([input.config], [patch | some patch in _patches]))
|
||||||
|
|
||||||
|
_patches contains {"gzip": {"min_length": _default_min_length}} if util.absent(["gzip", "min_length"])
|
||||||
|
|
||||||
|
_patches contains {"gzip": {"compression_level": _default_compression_level}} if {
|
||||||
|
util.absent(["gzip", "compression_level"])
|
||||||
|
}
|
||||||
|
|
||||||
|
errors contains "invalid value for server.encoding.gzip field, should be an object" if {
|
||||||
|
util.not_object(["gzip"])
|
||||||
|
}
|
||||||
|
|
||||||
|
errors contains "invalid value for server.encoding.gzip.min_length field, should be a positive number" if {
|
||||||
|
util.not_positive_number(["gzip", "min_length"])
|
||||||
|
}
|
||||||
|
|
||||||
|
errors contains "invalid value for server.encoding.gzip.compression_level field, accepted values are 0, 1 or 9" if {
|
||||||
|
value := util.value(["gzip", "compression_level"])
|
||||||
|
value != null
|
||||||
|
not value in _accepted_compression_levels
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package opa.config.server.encoding_test
|
||||||
|
|
||||||
|
import data.opa.config.server.encoding
|
||||||
|
|
||||||
|
test_injects_defaults[tc.note] if {
|
||||||
|
some tc in [
|
||||||
|
{"note": "empty config", "config": {}},
|
||||||
|
{"note": "gzip present", "config": {"gzip": {}}},
|
||||||
|
{"note": "min_length null", "config": {"gzip": {"min_length": null}}},
|
||||||
|
{"note": "compression_level null", "config": {"gzip": {"compression_level": null}}},
|
||||||
|
]
|
||||||
|
|
||||||
|
result := encoding.processed with input as {"config": tc.config}
|
||||||
|
result.gzip.min_length == 1024
|
||||||
|
result.gzip.compression_level == 9
|
||||||
|
}
|
||||||
|
|
||||||
|
test_preserves_configured_values if {
|
||||||
|
raw := {"gzip": {"min_length": 42, "compression_level": 1}}
|
||||||
|
result := encoding.processed with input as {"config": raw}
|
||||||
|
result.gzip.min_length == 42
|
||||||
|
result.gzip.compression_level == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
test_preserves_unknown_keys if {
|
||||||
|
result := encoding.processed with input as {"config": {"gzip": {"random_key": 0}}}
|
||||||
|
result.gzip.random_key == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
test_rejects_non_positive_min_length[tc.note] if {
|
||||||
|
some tc in [
|
||||||
|
{"note": "zero", "min_length": 0},
|
||||||
|
{"note": "negative", "min_length": -10},
|
||||||
|
]
|
||||||
|
|
||||||
|
result := encoding.errors with input as {"config": {"gzip": {"min_length": tc.min_length}}}
|
||||||
|
"invalid value for server.encoding.gzip.min_length field, should be a positive number" in result
|
||||||
|
}
|
||||||
|
|
||||||
|
# A non-number min_length is rejected here rather than left to the Go unmarshal,
|
||||||
|
# so the message names the option instead of the Go field.
|
||||||
|
test_rejects_non_number_min_length[tc.note] if {
|
||||||
|
some tc in [
|
||||||
|
{"note": "string", "min_length": "foobar"},
|
||||||
|
{"note": "boolean", "min_length": true},
|
||||||
|
{"note": "array", "min_length": [1]},
|
||||||
|
]
|
||||||
|
|
||||||
|
result := encoding.errors with input as {"config": {"gzip": {"min_length": tc.min_length}}}
|
||||||
|
"invalid value for server.encoding.gzip.min_length field, should be a positive number" in result
|
||||||
|
}
|
||||||
|
|
||||||
|
# Without this check the gzip defaults would be merged over the bad value,
|
||||||
|
# silently replacing it with a well-formed object.
|
||||||
|
test_rejects_non_object_gzip[tc.note] if {
|
||||||
|
some tc in [
|
||||||
|
{"note": "array", "config": {"gzip": [1, 2, 3]}},
|
||||||
|
{"note": "string", "config": {"gzip": "nope"}},
|
||||||
|
{"note": "number", "config": {"gzip": 7}},
|
||||||
|
]
|
||||||
|
|
||||||
|
result := encoding.errors with input as {"config": tc.config}
|
||||||
|
"invalid value for server.encoding.gzip field, should be an object" in result
|
||||||
|
}
|
||||||
|
|
||||||
|
test_rejects_unaccepted_compression_level[tc.note] if {
|
||||||
|
some tc in [
|
||||||
|
{"note": "out of range", "compression_level": 13},
|
||||||
|
{"note": "negative", "compression_level": -1},
|
||||||
|
{"note": "string", "compression_level": "9"},
|
||||||
|
]
|
||||||
|
|
||||||
|
result := encoding.errors with input as {"config": {"gzip": {"compression_level": tc.compression_level}}}
|
||||||
|
"invalid value for server.encoding.gzip.compression_level field, accepted values are 0, 1 or 9" in result
|
||||||
|
}
|
||||||
|
|
||||||
|
test_accepts_valid_compression_levels[tc.note] if {
|
||||||
|
some tc in [
|
||||||
|
{"note": "none", "compression_level": 0},
|
||||||
|
{"note": "best speed", "compression_level": 1},
|
||||||
|
{"note": "best compression", "compression_level": 9},
|
||||||
|
]
|
||||||
|
|
||||||
|
result := encoding.errors with input as {"config": {"gzip": {"compression_level": tc.compression_level}}}
|
||||||
|
count(result) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
test_empty_config_has_no_errors if {
|
||||||
|
result := encoding.errors with input as {"config": {}}
|
||||||
|
count(result) == 0
|
||||||
|
}
|
||||||
@@ -67,8 +67,14 @@ func (b *ConfigBuilder) WithBytes(config []byte) *ConfigBuilder {
|
|||||||
|
|
||||||
// Parse returns a valid Config object with defaults injected.
|
// Parse returns a valid Config object with defaults injected.
|
||||||
func (b *ConfigBuilder) Parse() (*Config, error) {
|
func (b *ConfigBuilder) Parse() (*Config, error) {
|
||||||
|
return b.ParseWithContext(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseWithContext returns a valid Config object with defaults injected, using
|
||||||
|
// ctx to evaluate the validation policy.
|
||||||
|
func (b *ConfigBuilder) ParseWithContext(ctx context.Context) (*Config, error) {
|
||||||
var result Config
|
var result Config
|
||||||
if _, err := configpolicy.EvalConfigInto(context.TODO(), validationPolicy, b.raw, &result); err != nil {
|
if _, err := configpolicy.EvalConfigInto(ctx, validationPolicy, b.raw, &result); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &result, nil
|
return &result, nil
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ package metrics
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/open-policy-agent/opa/v1/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConfigValidation(t *testing.T) {
|
func TestConfigValidation(t *testing.T) {
|
||||||
@@ -59,6 +63,14 @@ func TestConfigValidation(t *testing.T) {
|
|||||||
input: `{"prom": {"http_request_duration_seconds": {"random_key": 0}}}`,
|
input: `{"prom": {"http_request_duration_seconds": {"random_key": 0}}}`,
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
input: `{"prom": [1, 2, 3]}`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{"prom": {"http_request_duration_seconds": [1]}}`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
@@ -127,3 +139,88 @@ func valuesAreEqual(a []float64, b []float64) bool {
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestConfigRejectsWrongShapedOptions covers options the validation policy has to
|
||||||
|
// reject itself. Defaults are merged over the raw config, so an option that
|
||||||
|
// should hold an object would otherwise be silently replaced by the defaults
|
||||||
|
// instead of reaching the Go unmarshal that used to report the type error.
|
||||||
|
func TestConfigRejectsWrongShapedOptions(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: `{"prom": [1, 2, 3]}`,
|
||||||
|
wantErr: "invalid value for server.metrics.prom field, should be an object",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{"prom": "nope"}`,
|
||||||
|
wantErr: "invalid value for server.metrics.prom field, should be an object",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{"prom": {"http_request_duration_seconds": [1]}}`,
|
||||||
|
wantErr: "invalid value for server.metrics.prom.http_request_duration_seconds field, should be an object",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `{"prom": {"http_request_duration_seconds": {"buckets": ["a"]}}}`,
|
||||||
|
wantErr: "buckets field, should be an array of numbers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `[1, 2, 3]`,
|
||||||
|
wantErr: "config must be an object",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.input, func(t *testing.T) {
|
||||||
|
_, err := NewConfigBuilder().WithBytes([]byte(test.input)).Parse()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error containing %q, got none", test.wantErr)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), test.wantErr) {
|
||||||
|
t.Fatalf("expected error containing %q, got %q", test.wantErr, err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConfigNoWarningsForKnownMetricsOptions verifies the server.metrics spec
|
||||||
|
// registered by this package lets config.ParseConfig recognize the section's
|
||||||
|
// options. Without the registration the section is treated as open, so this also
|
||||||
|
// guards against the unknown-option warning below silently going missing.
|
||||||
|
func TestConfigNoWarningsForKnownMetricsOptions(t *testing.T) {
|
||||||
|
raw := []byte(`{"server": {"metrics": {"prom": {"http_request_duration_seconds": {"buckets": [0.1, 1]}}}}}`)
|
||||||
|
conf, err := config.ParseConfig(raw, "id")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(conf.Warnings) != 0 {
|
||||||
|
t.Fatalf("expected no warnings, got %v", conf.Warnings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigWarnsOnUnknownMetricsOption(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
raw string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
raw: `{"server": {"metrics": {"typo": 5}}}`,
|
||||||
|
want: `unknown configuration option "server.metrics.typo" encountered`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
raw: `{"server": {"metrics": {"prom": {"typo": 5}}}}`,
|
||||||
|
want: `unknown configuration option "server.metrics.prom.typo" encountered`,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.raw, func(t *testing.T) {
|
||||||
|
conf, err := config.ParseConfig([]byte(tc.raw), "id")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !slices.Contains(conf.Warnings, tc.want) {
|
||||||
|
t.Fatalf("expected warning %q, got %v", tc.want, conf.Warnings)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,26 +1,57 @@
|
|||||||
# METADATA
|
# METADATA
|
||||||
# description: |
|
# description: |
|
||||||
# Injects defaults for the server.metrics configuration (Prometheus HTTP
|
# Injects defaults and validates the server.metrics configuration (Prometheus
|
||||||
# request duration histogram buckets). Evaluated by the metrics config
|
# HTTP request duration histogram buckets). Evaluated by the metrics config
|
||||||
# builder. Value/type validation of the buckets is handled by unmarshaling
|
# builder. The policy rejects options of the wrong shape; the remaining field
|
||||||
# into the Go struct.
|
# type validation is handled by unmarshaling into the Go struct.
|
||||||
#
|
#
|
||||||
# Input: {"config": <raw server.metrics config>}
|
# Input: {"config": <raw server.metrics config>}
|
||||||
# Entrypoint: processed (config + defaults).
|
# Rules read by the Go layer: processed (config + defaults), errors (fatal).
|
||||||
package opa.config.server.metrics
|
package opa.config.server.metrics
|
||||||
|
|
||||||
|
import data.opa.config.util
|
||||||
|
|
||||||
# _default_buckets mirrors defaultHTTPRequestBuckets in config.go.
|
# _default_buckets mirrors defaultHTTPRequestBuckets in config.go.
|
||||||
_default_buckets := [1e-6, 5e-6, 1e-5, 5e-5, 1e-4, 5e-4, 1e-3, 0.01, 0.1, 1]
|
_default_buckets := [1e-6, 5e-6, 1e-5, 5e-5, 1e-4, 5e-4, 1e-3, 0.01, 0.1, 1]
|
||||||
|
|
||||||
# METADATA
|
# METADATA
|
||||||
# entrypoint: true
|
|
||||||
# description: the config with the default histogram buckets injected when absent.
|
# description: the config with the default histogram buckets injected when absent.
|
||||||
processed := object.union_n(array.concat([input.config], [patch | some patch in _patches]))
|
processed := object.union_n(array.concat([input.config], [patch | some patch in _patches]))
|
||||||
|
|
||||||
_patches contains {"prom": {"http_request_duration_seconds": {"buckets": _default_buckets}}} if {
|
_patches contains {"prom": {"http_request_duration_seconds": {"buckets": _default_buckets}}} if {
|
||||||
_absent(["prom", "http_request_duration_seconds", "buckets"])
|
util.absent(["prom", "http_request_duration_seconds", "buckets"])
|
||||||
}
|
}
|
||||||
|
|
||||||
# _absent is true when the option at path is missing or explicitly null, matching
|
errors contains "invalid value for server.metrics.prom field, should be an object" if {
|
||||||
# the pre-Rego behavior where a nil slice was replaced with defaults.
|
util.not_object(["prom"])
|
||||||
_absent(path) if object.get(input.config, path, null) == null
|
}
|
||||||
|
|
||||||
|
errors contains "invalid value for server.metrics.prom.http_request_duration_seconds field, should be an object" if {
|
||||||
|
util.not_object(["prom", "http_request_duration_seconds"])
|
||||||
|
}
|
||||||
|
|
||||||
|
# The message is a rule of its own because naming the option in the rule head would
|
||||||
|
# put the line over the length limit.
|
||||||
|
errors contains _buckets_msg if {
|
||||||
|
_not_number_array(["prom", "http_request_duration_seconds", "buckets"])
|
||||||
|
}
|
||||||
|
|
||||||
|
_buckets_field := "server.metrics.prom.http_request_duration_seconds.buckets"
|
||||||
|
|
||||||
|
_buckets_msg := $`invalid value for {_buckets_field} field, should be an array of numbers`
|
||||||
|
|
||||||
|
# _not_number_array rejects a present-but-wrong-shaped buckets option here rather
|
||||||
|
# than leaving it to the Go unmarshal, so the message names the config option
|
||||||
|
# instead of the Go struct field.
|
||||||
|
_not_number_array(path) if {
|
||||||
|
value := util.value(path)
|
||||||
|
value != null
|
||||||
|
not _number_array(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
_number_array(value) if {
|
||||||
|
is_array(value)
|
||||||
|
every item in value {
|
||||||
|
is_number(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import data.opa.config.server.metrics
|
|||||||
|
|
||||||
_default_buckets := [1e-6, 5e-6, 1e-5, 5e-5, 1e-4, 5e-4, 1e-3, 0.01, 0.1, 1]
|
_default_buckets := [1e-6, 5e-6, 1e-5, 5e-5, 1e-4, 5e-4, 1e-3, 0.01, 0.1, 1]
|
||||||
|
|
||||||
|
_buckets_field := "server.metrics.prom.http_request_duration_seconds.buckets"
|
||||||
|
|
||||||
|
_buckets_msg := $`invalid value for {_buckets_field} field, should be an array of numbers`
|
||||||
|
|
||||||
test_injects_default_buckets[tc.note] if {
|
test_injects_default_buckets[tc.note] if {
|
||||||
some tc in [
|
some tc in [
|
||||||
{"note": "empty config", "config": {}},
|
{"note": "empty config", "config": {}},
|
||||||
@@ -31,3 +35,55 @@ test_preserves_unknown_keys if {
|
|||||||
result := metrics.processed with input as {"config": {"prom": {"random_key": 0}}}
|
result := metrics.processed with input as {"config": {"prom": {"random_key": 0}}}
|
||||||
result.prom.random_key == 0
|
result.prom.random_key == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Without these checks the bucket defaults would be merged over the bad value,
|
||||||
|
# silently replacing it with a well-formed object.
|
||||||
|
test_rejects_non_object_prom[tc.note] if {
|
||||||
|
some tc in [
|
||||||
|
{"note": "array", "config": {"prom": [1, 2, 3]}},
|
||||||
|
{"note": "string", "config": {"prom": "nope"}},
|
||||||
|
{"note": "number", "config": {"prom": 7}},
|
||||||
|
]
|
||||||
|
|
||||||
|
result := metrics.errors with input as {"config": tc.config}
|
||||||
|
"invalid value for server.metrics.prom field, should be an object" in result
|
||||||
|
}
|
||||||
|
|
||||||
|
test_rejects_non_object_http_request_duration_seconds[tc.note] if {
|
||||||
|
some tc in [
|
||||||
|
{"note": "array", "value": [1]},
|
||||||
|
{"note": "string", "value": "x"},
|
||||||
|
]
|
||||||
|
|
||||||
|
result := metrics.errors with input as {"config": {"prom": {"http_request_duration_seconds": tc.value}}}
|
||||||
|
"invalid value for server.metrics.prom.http_request_duration_seconds field, should be an object" in result
|
||||||
|
}
|
||||||
|
|
||||||
|
# A non-numeric buckets value is rejected here rather than left to the Go
|
||||||
|
# unmarshal, so the message names the option instead of the Go field.
|
||||||
|
test_rejects_non_number_array_buckets[tc.note] if {
|
||||||
|
some tc in [
|
||||||
|
{"note": "string", "buckets": "x"},
|
||||||
|
{"note": "object", "buckets": {}},
|
||||||
|
{"note": "array of strings", "buckets": ["a"]},
|
||||||
|
{"note": "mixed array", "buckets": [1, "a"]},
|
||||||
|
]
|
||||||
|
|
||||||
|
config := {"prom": {"http_request_duration_seconds": {"buckets": tc.buckets}}}
|
||||||
|
result := metrics.errors with input as {"config": config}
|
||||||
|
_buckets_msg in result
|
||||||
|
}
|
||||||
|
|
||||||
|
test_valid_config_has_no_errors[tc.note] if {
|
||||||
|
some tc in [
|
||||||
|
{"note": "empty config", "config": {}},
|
||||||
|
{"note": "prom present", "config": {"prom": {}}},
|
||||||
|
{"note": "section present", "config": {"prom": {"http_request_duration_seconds": {}}}},
|
||||||
|
{"note": "custom buckets", "config": {"prom": {"http_request_duration_seconds": {"buckets": [0.1, 4]}}}},
|
||||||
|
{"note": "empty buckets", "config": {"prom": {"http_request_duration_seconds": {"buckets": []}}}},
|
||||||
|
{"note": "buckets null", "config": {"prom": {"http_request_duration_seconds": {"buckets": null}}}},
|
||||||
|
]
|
||||||
|
|
||||||
|
result := metrics.errors with input as {"config": tc.config}
|
||||||
|
count(result) == 0
|
||||||
|
}
|
||||||
|
|||||||
@@ -457,7 +457,7 @@ func NewRuntime(ctx context.Context, params Params) (*Runtime, error) {
|
|||||||
params.Router = http.NewServeMux()
|
params.Router = http.NewServeMux()
|
||||||
}
|
}
|
||||||
|
|
||||||
metricsConfig, parseConfigErr := extractMetricsConfig(config, params)
|
metricsConfig, parseConfigErr := extractMetricsConfig(ctx, config, params)
|
||||||
if parseConfigErr != nil {
|
if parseConfigErr != nil {
|
||||||
return nil, parseConfigErr
|
return nil, parseConfigErr
|
||||||
}
|
}
|
||||||
@@ -589,7 +589,7 @@ func NewRuntime(ctx context.Context, params Params) (*Runtime, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// extractMetricsConfig returns the configuration for server metrics and parsing errors if any
|
// extractMetricsConfig returns the configuration for server metrics and parsing errors if any
|
||||||
func extractMetricsConfig(config []byte, params Params) (*metrics_config.Config, error) {
|
func extractMetricsConfig(ctx context.Context, config []byte, params Params) (*metrics_config.Config, error) {
|
||||||
opaParsedConfig, opaParsedConfigErr := opa_config.ParseConfig(config, params.ID)
|
opaParsedConfig, opaParsedConfigErr := opa_config.ParseConfig(config, params.ID)
|
||||||
if opaParsedConfigErr != nil {
|
if opaParsedConfigErr != nil {
|
||||||
return nil, opaParsedConfigErr
|
return nil, opaParsedConfigErr
|
||||||
@@ -601,7 +601,7 @@ func extractMetricsConfig(config []byte, params Params) (*metrics_config.Config,
|
|||||||
}
|
}
|
||||||
|
|
||||||
configBuilder := metrics_config.NewConfigBuilder()
|
configBuilder := metrics_config.NewConfigBuilder()
|
||||||
metricsParsedConfig, metricsParsedConfigErr := configBuilder.WithBytes(serverMetricsData).Parse()
|
metricsParsedConfig, metricsParsedConfigErr := configBuilder.WithBytes(serverMetricsData).ParseWithContext(ctx)
|
||||||
if metricsParsedConfigErr != nil {
|
if metricsParsedConfigErr != nil {
|
||||||
return nil, fmt.Errorf("server metrics configuration parse error: %w", metricsParsedConfigErr)
|
return nil, fmt.Errorf("server metrics configuration parse error: %w", metricsParsedConfigErr)
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-6
@@ -232,13 +232,13 @@ func (s *Server) Init(ctx context.Context) (*Server, error) {
|
|||||||
s.Handler = s.initHandlerAuthn(s.Handler)
|
s.Handler = s.initHandlerAuthn(s.Handler)
|
||||||
|
|
||||||
// compression handler
|
// compression handler
|
||||||
s.Handler, err = s.initHandlerCompression(s.Handler)
|
s.Handler, err = s.initHandlerCompression(ctx, s.Handler)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
s.DiagnosticHandler = s.initHandlerAuthn(s.DiagnosticHandler)
|
s.DiagnosticHandler = s.initHandlerAuthn(s.DiagnosticHandler)
|
||||||
|
|
||||||
s.Handler, err = s.initHandlerDecodingLimits(s.Handler)
|
s.Handler, err = s.initHandlerDecodingLimits(ctx, s.Handler)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -812,13 +812,13 @@ func (s *Server) initHandlerAuthz(handler http.Handler) http.Handler {
|
|||||||
// Enforces request body size limits on incoming requests. For gzipped requests,
|
// Enforces request body size limits on incoming requests. For gzipped requests,
|
||||||
// it passes the size limit down the body-reading method via the request
|
// it passes the size limit down the body-reading method via the request
|
||||||
// context.
|
// context.
|
||||||
func (s *Server) initHandlerDecodingLimits(handler http.Handler) (http.Handler, error) {
|
func (s *Server) initHandlerDecodingLimits(ctx context.Context, handler http.Handler) (http.Handler, error) {
|
||||||
cfg := s.manager.GetConfig()
|
cfg := s.manager.GetConfig()
|
||||||
var decodingRawConfig []byte
|
var decodingRawConfig []byte
|
||||||
if cfg.Server != nil {
|
if cfg.Server != nil {
|
||||||
decodingRawConfig = []byte(cfg.Server.Decoding)
|
decodingRawConfig = []byte(cfg.Server.Decoding)
|
||||||
}
|
}
|
||||||
decodingConfig, err := serverDecodingPlugin.NewConfigBuilder().WithBytes(decodingRawConfig).Parse()
|
decodingConfig, err := serverDecodingPlugin.NewConfigBuilder().WithBytes(decodingRawConfig).ParseWithContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -827,13 +827,13 @@ func (s *Server) initHandlerDecodingLimits(handler http.Handler) (http.Handler,
|
|||||||
return decodingHandler, nil
|
return decodingHandler, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) initHandlerCompression(handler http.Handler) (http.Handler, error) {
|
func (s *Server) initHandlerCompression(ctx context.Context, handler http.Handler) (http.Handler, error) {
|
||||||
cfg := s.manager.GetConfig()
|
cfg := s.manager.GetConfig()
|
||||||
var encodingRawConfig []byte
|
var encodingRawConfig []byte
|
||||||
if cfg.Server != nil {
|
if cfg.Server != nil {
|
||||||
encodingRawConfig = []byte(cfg.Server.Encoding)
|
encodingRawConfig = []byte(cfg.Server.Encoding)
|
||||||
}
|
}
|
||||||
encodingConfig, err := serverEncodingPlugin.NewConfigBuilder().WithBytes(encodingRawConfig).Parse()
|
encodingConfig, err := serverEncodingPlugin.NewConfigBuilder().WithBytes(encodingRawConfig).ParseWithContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user