mirror of
https://github.com/status-im/consul.git
synced 2025-02-07 19:35:35 +00:00
d9206fc7e2
mesh: add options for HTTP incoming request normalization Expose global mesh configuration to enforce inbound HTTP request normalization on mesh traffic via Envoy xDS config. mesh: enable inbound URL path normalization by default mesh: add support for L7 header match contains and ignore_case Enable partial string and case-insensitive matching in L7 intentions header match rules. ui: support L7 header match contains and ignore_case Co-authored-by: Phil Renaud <phil@riotindustries.com> test: add request normalization integration bats tests Add both "positive" and "negative" test suites, showing normalization in action as well as expected results when it is not enabled, for the same set of test cases. Also add some alternative service container test helpers for verifying raw HTTP request paths, which is difficult to do with Fortio. docs: update security and reference docs for L7 intentions bypass prevention - Update security docs with best practices for service intentions configuration - Update configuration entry references for mesh and intentions to reflect new values and add guidance on usage
211 lines
6.0 KiB
Go
211 lines
6.0 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package structs
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestMeshConfigEntry_PeerThroughMeshGateways(t *testing.T) {
|
|
tests := map[string]struct {
|
|
input *MeshConfigEntry
|
|
want bool
|
|
}{
|
|
"nil entry": {
|
|
input: nil,
|
|
want: false,
|
|
},
|
|
"nil peering config": {
|
|
input: &MeshConfigEntry{
|
|
Peering: nil,
|
|
},
|
|
want: false,
|
|
},
|
|
"not peering through gateways": {
|
|
input: &MeshConfigEntry{
|
|
Peering: &PeeringMeshConfig{
|
|
PeerThroughMeshGateways: false,
|
|
},
|
|
},
|
|
want: false,
|
|
},
|
|
"peering through gateways": {
|
|
input: &MeshConfigEntry{
|
|
Peering: &PeeringMeshConfig{
|
|
PeerThroughMeshGateways: true,
|
|
},
|
|
},
|
|
want: true,
|
|
},
|
|
}
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
assert.Equalf(t, tc.want, tc.input.PeerThroughMeshGateways(), "PeerThroughMeshGateways()")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMeshConfigEntry_GetHTTPIncomingRequestNormalization(t *testing.T) {
|
|
tests := map[string]struct {
|
|
input *MeshConfigEntry
|
|
want *RequestNormalizationMeshConfig
|
|
}{
|
|
// Ensure nil is gracefully handled at each level of config path.
|
|
"nil entry": {
|
|
input: nil,
|
|
want: nil,
|
|
},
|
|
"nil http config": {
|
|
input: &MeshConfigEntry{
|
|
HTTP: nil,
|
|
},
|
|
want: nil,
|
|
},
|
|
"nil http incoming config": {
|
|
input: &MeshConfigEntry{
|
|
HTTP: &MeshHTTPConfig{
|
|
Incoming: nil,
|
|
},
|
|
},
|
|
want: nil,
|
|
},
|
|
"nil http incoming request normalization config": {
|
|
input: &MeshConfigEntry{
|
|
HTTP: &MeshHTTPConfig{
|
|
Incoming: &MeshDirectionalHTTPConfig{
|
|
RequestNormalization: nil,
|
|
},
|
|
},
|
|
},
|
|
want: nil,
|
|
},
|
|
}
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
assert.Equal(t, tc.want, tc.input.GetHTTPIncomingRequestNormalization())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMeshConfigEntry_RequestNormalizationMeshConfig(t *testing.T) {
|
|
tests := map[string]struct {
|
|
input *RequestNormalizationMeshConfig
|
|
getFn func(*RequestNormalizationMeshConfig) any
|
|
want any
|
|
}{
|
|
// Ensure defaults are returned when config is not set.
|
|
"nil entry gets false GetInsecureDisablePathNormalization": {
|
|
input: nil,
|
|
getFn: func(c *RequestNormalizationMeshConfig) any {
|
|
return c.GetInsecureDisablePathNormalization()
|
|
},
|
|
want: false,
|
|
},
|
|
"nil entry gets false GetMergeSlashes": {
|
|
input: nil,
|
|
getFn: func(c *RequestNormalizationMeshConfig) any {
|
|
return c.GetMergeSlashes()
|
|
},
|
|
want: false,
|
|
},
|
|
"nil entry gets default GetPathWithEscapedSlashesAction": {
|
|
input: nil,
|
|
getFn: func(c *RequestNormalizationMeshConfig) any {
|
|
return c.GetPathWithEscapedSlashesAction()
|
|
},
|
|
want: PathWithEscapedSlashesAction("IMPLEMENTATION_SPECIFIC_DEFAULT"),
|
|
},
|
|
"nil entry gets default GetHeadersWithUnderscoresAction": {
|
|
input: nil,
|
|
getFn: func(c *RequestNormalizationMeshConfig) any {
|
|
return c.GetHeadersWithUnderscoresAction()
|
|
},
|
|
want: HeadersWithUnderscoresAction("ALLOW"),
|
|
},
|
|
"empty entry gets default GetPathWithEscapedSlashesAction": {
|
|
input: &RequestNormalizationMeshConfig{},
|
|
getFn: func(c *RequestNormalizationMeshConfig) any {
|
|
return c.GetPathWithEscapedSlashesAction()
|
|
},
|
|
want: PathWithEscapedSlashesAction("IMPLEMENTATION_SPECIFIC_DEFAULT"),
|
|
},
|
|
"empty entry gets default GetHeadersWithUnderscoresAction": {
|
|
input: &RequestNormalizationMeshConfig{},
|
|
getFn: func(c *RequestNormalizationMeshConfig) any {
|
|
return c.GetHeadersWithUnderscoresAction()
|
|
},
|
|
want: HeadersWithUnderscoresAction("ALLOW"),
|
|
},
|
|
// Ensure values are returned when set.
|
|
"non-default entry gets expected InsecureDisablePathNormalization": {
|
|
input: &RequestNormalizationMeshConfig{InsecureDisablePathNormalization: true},
|
|
getFn: func(c *RequestNormalizationMeshConfig) any {
|
|
return c.GetInsecureDisablePathNormalization()
|
|
},
|
|
want: true,
|
|
},
|
|
"non-default entry gets expected MergeSlashes": {
|
|
input: &RequestNormalizationMeshConfig{MergeSlashes: true},
|
|
getFn: func(c *RequestNormalizationMeshConfig) any {
|
|
return c.GetMergeSlashes()
|
|
},
|
|
want: true,
|
|
},
|
|
"non-default entry gets expected PathWithEscapedSlashesAction": {
|
|
input: &RequestNormalizationMeshConfig{PathWithEscapedSlashesAction: "UNESCAPE_AND_FORWARD"},
|
|
getFn: func(c *RequestNormalizationMeshConfig) any {
|
|
return c.GetPathWithEscapedSlashesAction()
|
|
},
|
|
want: PathWithEscapedSlashesAction("UNESCAPE_AND_FORWARD"),
|
|
},
|
|
"non-default entry gets expected HeadersWithUnderscoresAction": {
|
|
input: &RequestNormalizationMeshConfig{HeadersWithUnderscoresAction: "REJECT_REQUEST"},
|
|
getFn: func(c *RequestNormalizationMeshConfig) any {
|
|
return c.GetHeadersWithUnderscoresAction()
|
|
},
|
|
want: HeadersWithUnderscoresAction("REJECT_REQUEST"),
|
|
},
|
|
}
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
assert.Equal(t, tc.want, tc.getFn(tc.input))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMeshConfigEntry_validateRequestNormalizationMeshConfig(t *testing.T) {
|
|
tests := map[string]struct {
|
|
input *RequestNormalizationMeshConfig
|
|
wantErr string
|
|
}{
|
|
"nil entry is valid": {
|
|
input: nil,
|
|
wantErr: "",
|
|
},
|
|
"invalid PathWithEscapedSlashesAction is rejected": {
|
|
input: &RequestNormalizationMeshConfig{
|
|
PathWithEscapedSlashesAction: PathWithEscapedSlashesAction("INVALID"),
|
|
},
|
|
wantErr: "no matching PathWithEscapedSlashesAction value found for INVALID, please specify one of [IMPLEMENTATION_SPECIFIC_DEFAULT, KEEP_UNCHANGED, REJECT_REQUEST, UNESCAPE_AND_REDIRECT, UNESCAPE_AND_FORWARD]",
|
|
},
|
|
"invalid HeadersWithUnderscoresAction is rejected": {
|
|
input: &RequestNormalizationMeshConfig{
|
|
HeadersWithUnderscoresAction: HeadersWithUnderscoresAction("INVALID"),
|
|
},
|
|
wantErr: "no matching HeadersWithUnderscoresAction value found for INVALID, please specify one of [ALLOW, REJECT_REQUEST, DROP_HEADER]",
|
|
},
|
|
}
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
if tc.wantErr == "" {
|
|
assert.NoError(t, validateRequestNormalizationMeshConfig(tc.input))
|
|
} else {
|
|
assert.EqualError(t, validateRequestNormalizationMeshConfig(tc.input), tc.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|