Skip to content

Commit

Permalink
refact: unify get headers approach from a decision (#628)
Browse files Browse the repository at this point in the history
Both `headers` and `response_headers_to_add` attributes returned by the
same function.

Remove intermediate http.Header transformation between decision output
and Envoy headers.

Signed-off-by: Anthony Regeda <regedaster@gmail.com>
  • Loading branch information
regeda authored Jan 8, 2025
1 parent 805caa6 commit 99d5942
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 137 deletions.
166 changes: 59 additions & 107 deletions envoyauth/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"slices"

ext_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
ext_type_v3 "github.com/envoyproxy/go-control-plane/envoy/type/v3"
Expand Down Expand Up @@ -159,68 +160,30 @@ func (result *EvalResult) GetRequestHTTPHeadersToRemove() ([]string, error) {
return result.getStringSliceFromDecision("request_headers_to_remove")
}

// GetResponseHTTPHeaders - returns the http headers to return if they are part of the decision
func (result *EvalResult) GetResponseHTTPHeaders() (http.Header, error) {
var responseHeaders = make(http.Header)

func (result *EvalResult) getHeadersFromDecision(fieldName string) ([]*ext_core_v3.HeaderValueOption, error) {
switch decision := result.Decision.(type) {
case bool:
return responseHeaders, nil
return nil, nil
case map[string]interface{}:
var ok bool
var val interface{}

if val, ok = decision["headers"]; !ok {
return responseHeaders, nil
}

err := transformToHTTPHeaderFormat(val, &responseHeaders)
if err != nil {
return nil, err
val, ok := decision[fieldName]
if !ok {
return nil, nil
}

return responseHeaders, nil
return transformHeadersToEnvoy(val)
default:
return nil, result.invalidDecisionErr()
}

return nil, result.invalidDecisionErr()
}

// GetResponseEnvoyHeaderValueOptions - returns the http headers to return if they are part of the decision as envoy header value options
func (result *EvalResult) GetResponseEnvoyHeaderValueOptions() ([]*ext_core_v3.HeaderValueOption, error) {
headers, err := result.GetResponseHTTPHeaders()
if err != nil {
return nil, err
}

return transformHTTPHeaderToEnvoyHeaderValueOption(headers)
return result.getHeadersFromDecision("headers")
}

// GetResponseHTTPHeadersToAdd - returns the http headers to send to the downstream client
func (result *EvalResult) GetResponseHTTPHeadersToAdd() ([]*ext_core_v3.HeaderValueOption, error) {
var responseHeaders = make(http.Header)

finalHeaders := []*ext_core_v3.HeaderValueOption{}

switch decision := result.Decision.(type) {
case bool:
return finalHeaders, nil
case map[string]interface{}:
var ok bool
var val interface{}

if val, ok = decision["response_headers_to_add"]; !ok {
return finalHeaders, nil
}

err := transformToHTTPHeaderFormat(val, &responseHeaders)
if err != nil {
return nil, err
}
default:
return nil, result.invalidDecisionErr()
}

return transformHTTPHeaderToEnvoyHeaderValueOption(responseHeaders)
return result.getHeadersFromDecision("response_headers_to_add")
}

// HasResponseBody returns true if the decision defines a body (only true for structured decisions)
Expand Down Expand Up @@ -299,16 +262,16 @@ func (result *EvalResult) GetResponseHTTPStatus() (int, error) {

// GetDynamicMetadata returns the dynamic metadata to return if part of the decision
func (result *EvalResult) GetDynamicMetadata() (*_structpb.Struct, error) {
var (
val interface{}
ok bool
)
switch decision := result.Decision.(type) {
case bool:
if decision {
return nil, fmt.Errorf("dynamic metadata undefined for boolean decision")
}
case map[string]interface{}:
var (
val interface{}
ok bool
)
if val, ok = decision["dynamic_metadata"]; !ok {
return nil, nil
}
Expand Down Expand Up @@ -346,74 +309,63 @@ func (result *EvalResult) GetResponseEnvoyHTTPStatus() (*ext_type_v3.HttpStatus,
return status, nil
}

func transformToHTTPHeaderFormat(input interface{}, result *http.Header) error {
func makeHeaderValueOption(k, v string) *ext_core_v3.HeaderValueOption {
return &ext_core_v3.HeaderValueOption{
Header: &ext_core_v3.HeaderValue{
Key: k,
Value: v,
},
}
}

takeResponseHeaders := func(headers map[string]interface{}, targetHeaders *http.Header) error {
for key, value := range headers {
switch values := value.(type) {
case string:
targetHeaders.Add(key, values)
case []string:
for _, v := range values {
targetHeaders.Add(key, v)
}
case []interface{}:
for _, value := range values {
if headerVal, ok := value.(string); ok {
targetHeaders.Add(key, headerVal)
} else {
return fmt.Errorf("invalid value type for header '%s'", key)
}
func makeEnvoyHeaderValueOptionsFromHeadersMap(hvo []*ext_core_v3.HeaderValueOption, headers map[string]any) ([]*ext_core_v3.HeaderValueOption, error) {
hvo = slices.Grow(hvo, len(headers))
for key, value := range headers {
switch val := value.(type) {
case string:
hvo = append(hvo, makeHeaderValueOption(key, val))
case []string:
hvo = slices.Grow(hvo, len(val))
for _, v := range val {
hvo = append(hvo, makeHeaderValueOption(key, v))
}
case []interface{}:
hvo = slices.Grow(hvo, len(val))
for _, v := range val {
s, ok := v.(string)
if !ok {
return nil, fmt.Errorf("invalid value type %T for header '%s'", v, key)
}
default:
return fmt.Errorf("type assertion error for header '%s'", key)
hvo = append(hvo, makeHeaderValueOption(key, s))
}
default:
return nil, fmt.Errorf("type assertion error for header '%s'", key)
}
return nil
}
return hvo, nil
}

func transformHeadersToEnvoy(input any) ([]*ext_core_v3.HeaderValueOption, error) {
switch input := input.(type) {
case []interface{}:
case []any:
var (
hvo []*ext_core_v3.HeaderValueOption
err error
)
for _, val := range input {
headers, ok := val.(map[string]interface{})
headers, ok := val.(map[string]any)
if !ok {
return fmt.Errorf("type assertion error, expected headers to be of type 'object' but got '%T'", val)
return nil, fmt.Errorf("type assertion error, expected headers to be of type 'object' but got '%T'", val)
}

err := takeResponseHeaders(headers, result)
hvo, err = makeEnvoyHeaderValueOptionsFromHeadersMap(hvo, headers)
if err != nil {
return err
return nil, err
}
}

case map[string]interface{}:
err := takeResponseHeaders(input, result)
if err != nil {
return err
}

default:
return fmt.Errorf("type assertion error, expected headers to be of type 'object' but got '%T'", input)
return hvo, nil
case map[string]any:
return makeEnvoyHeaderValueOptionsFromHeadersMap(nil, input)
}

return nil
}

func transformHTTPHeaderToEnvoyHeaderValueOption(headers http.Header) ([]*ext_core_v3.HeaderValueOption, error) {
responseHeaders := []*ext_core_v3.HeaderValueOption{}

for key, values := range headers {
for idx := range values {
headerValue := &ext_core_v3.HeaderValue{
Key: key,
Value: values[idx],
}
headerValueOption := &ext_core_v3.HeaderValueOption{
Header: headerValue,
}
responseHeaders = append(responseHeaders, headerValueOption)
}
}

return responseHeaders, nil
return nil, fmt.Errorf("type assertion error, expected headers to be of type 'object' but got '%T'", input)
}
49 changes: 19 additions & 30 deletions internal/internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1708,15 +1708,29 @@ func TestCheckAllowObjectDecisionMultiValuedHeaders(t *testing.T) {
t.Fatalf("Expected two headers to add but got %v", headersToAdd)
}

expected := []*ext_core.HeaderValueOption{
{
Header: &ext_core.HeaderValue{
Key: "x",
Value: "hello",
},
},
{
Header: &ext_core.HeaderValue{
Key: "x",
Value: "world",
},
},
}

if !reflect.DeepEqual(expected, headersToAdd) {
t.Fatal("Unexpected response_headers_to_add")
}

headers := response.GetHeaders()
if len(headers) != 0 {
t.Fatalf("Expected no headers but got %v", len(headers))
}

expectedHeaders := http.Header{}
expectedHeaders.Set("x", "hello")
expectedHeaders.Add("x", "world")
assertHeaderValues(t, expectedHeaders, headersToAdd)
}

func TestCheckAllowObjectDecision(t *testing.T) {
Expand Down Expand Up @@ -2287,31 +2301,6 @@ func assertHeaders(t *testing.T, actualHeaders []*ext_core.HeaderValueOption, ex
}
}

func assertHeaderValues(t *testing.T, expectedHeaders http.Header, headersToAdd []*ext_core.HeaderValueOption) {
t.Helper()
for _, header := range headersToAdd {
key := header.GetHeader().GetKey()
value := header.GetHeader().GetValue()

expectedValues := expectedHeaders[key]
if expectedValues == nil {
t.Fatalf("unexpected header '%s'", key)
}

found := false
for _, expectedValue := range expectedValues {
if expectedValue == value {
found = true
break
}
}

if !found {
t.Fatalf("unexpected value '%s' for header '%s'", value, key)
}
}
}

func assertErrorCounterMetric(t *testing.T, server *envoyExtAuthzGrpcServer, labelValues ...string) {
reg := prometheus.NewPedanticRegistry()
if err := reg.Register(server.metricErrorCounter); err != nil {
Expand Down

0 comments on commit 99d5942

Please sign in to comment.