Skip to content

Commit

Permalink
implement dynamic_metadata api
Browse files Browse the repository at this point in the history
Signed-off-by: tjons <tyler.schade@solo.io>
  • Loading branch information
tjons authored and ashutosh-narkar committed Nov 6, 2023
1 parent 7b50cd3 commit de4182f
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 4 deletions.
29 changes: 29 additions & 0 deletions envoyauth/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import (

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"
_structpb "github.com/golang/protobuf/ptypes/struct"
"github.com/open-policy-agent/opa-envoy-plugin/internal/util"
"github.com/open-policy-agent/opa/metrics"
"github.com/open-policy-agent/opa/storage"
"github.com/open-policy-agent/opa/topdown/builtins"
"google.golang.org/protobuf/types/known/structpb"
)

// EvalResult - Captures the result from evaluating a query against an input
Expand Down Expand Up @@ -280,6 +282,33 @@ func (result *EvalResult) GetResponseHTTPStatus() (int, error) {
return http.StatusForbidden, result.invalidDecisionErr()
}

// 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{}:
if val, ok = decision["dynamic_metadata"]; !ok {
return nil, nil
}

metadata, ok := val.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("type assertion error")
}

return structpb.NewStruct(metadata)
}

return nil, nil
}

// GetResponseEnvoyHTTPStatus returns the http status to return if they are part of the decision
func (result *EvalResult) GetResponseEnvoyHTTPStatus() (*ext_type_v3.HttpStatus, error) {
status := &ext_type_v3.HttpStatus{
Expand Down
55 changes: 55 additions & 0 deletions envoyauth/response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import (
"encoding/json"
"reflect"
"testing"

_structpb "github.com/golang/protobuf/ptypes/struct"
"google.golang.org/protobuf/proto"
)

func TestIsAllowed(t *testing.T) {
Expand Down Expand Up @@ -341,3 +344,55 @@ func TestGetResponseHttpStatus(t *testing.T) {
t.Fatalf("Expected http status code \"BadRequest\" but got %v", result.GetCode().String())
}
}

func TestGetDynamicMetadata(t *testing.T) {
input := make(map[string]interface{})
er := EvalResult{
Decision: input,
}

result, err := er.GetDynamicMetadata()
if err != nil {
t.Fatalf("Expected no error but got %v", err)
}

if result != nil {
t.Fatalf("Expected no dynamic metadata but got %v", result)
}

input["dynamic_metadata"] = map[string]interface{}{
"foo": "bar",
}
result, err = er.GetDynamicMetadata()
if err != nil {
t.Fatalf("Expected no error but got %v", err)
}

expectedDynamicMetadata := &_structpb.Struct{
Fields: map[string]*_structpb.Value{
"foo": {
Kind: &_structpb.Value_StringValue{
StringValue: "bar",
},
},
},
}
if !proto.Equal(result, expectedDynamicMetadata) {
t.Fatalf("Expected result %v but got %v", expectedDynamicMetadata, result)
}
}

func TestGetDynamicMetadataWithBooleanDecision(t *testing.T) {
er := EvalResult{
Decision: true,
}

result, err := er.GetDynamicMetadata()
if err == nil {
t.Fatal("Expected error error but got none")
}

if result != nil {
t.Fatalf("Expected no result but got %v", result)
}
}
14 changes: 12 additions & 2 deletions internal/internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ package internal
import (
"context"
"fmt"
"github.com/open-policy-agent/opa/topdown"
"math"
"net"
"net/url"
Expand Down Expand Up @@ -41,12 +40,15 @@ import (
"github.com/open-policy-agent/opa/rego"
"github.com/open-policy-agent/opa/server"
"github.com/open-policy-agent/opa/storage"
"github.com/open-policy-agent/opa/topdown"
iCache "github.com/open-policy-agent/opa/topdown/cache"
"github.com/open-policy-agent/opa/tracing"
"github.com/open-policy-agent/opa/util"

"go.opentelemetry.io/otel/trace"

_structpb "github.com/golang/protobuf/ptypes/struct"

"github.com/open-policy-agent/opa-envoy-plugin/envoyauth"
internal_util "github.com/open-policy-agent/opa-envoy-plugin/internal/util"
"github.com/open-policy-agent/opa-envoy-plugin/opa/decisionlog"
Expand Down Expand Up @@ -464,8 +466,16 @@ func (p *envoyExtAuthzGrpcServer) check(ctx context.Context, req interface{}) (*
return nil, stop, &internalErr
}

if status == int32(code.Code_OK) {
var dynamicMetadata *_structpb.Struct
dynamicMetadata, err = result.GetDynamicMetadata()
if err != nil {
err = errors.Wrap(err, "failed to get dynamic metadata")
internalErr = internalError(EnvoyAuthResultErr, err)
return nil, stop, &internalErr
}
resp.DynamicMetadata = dynamicMetadata

if status == int32(code.Code_OK) {
var headersToRemove []string
headersToRemove, err = result.GetRequestHTTPHeadersToRemove()
if err != nil {
Expand Down
121 changes: 119 additions & 2 deletions internal/internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ import (
ext_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
ext_authz_v2 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v2"
ext_authz "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
_structpb "github.com/golang/protobuf/ptypes/struct"
"github.com/prometheus/client_golang/prometheus"
"google.golang.org/genproto/googleapis/rpc/code"
"google.golang.org/protobuf/proto"

"github.com/open-policy-agent/opa-envoy-plugin/envoyauth"
"github.com/open-policy-agent/opa/ast"
Expand Down Expand Up @@ -1261,6 +1263,92 @@ func TestConfigWithProtoDescriptor(t *testing.T) {
}
}

func TestCheckAllowObjectDecisionDynamicMetadata(t *testing.T) {
var req ext_authz.CheckRequest
if err := util.Unmarshal([]byte(exampleAllowedRequestParsedPath), &req); err != nil {
panic(err)
}

module := `
package envoy.authz
default allow = false
allow {
input.parsed_path = ["my", "test", "path"]
}
dynamic_metadata["foo"] = "bar"
dynamic_metadata["bar"] = "baz"
result["allowed"] = allow
result["dynamic_metadata"] = dynamic_metadata
`

server := testAuthzServerWithModule(module, "envoy/authz/result", nil, withCustomLogger(&testPlugin{}))
ctx := context.Background()
output, err := server.Check(ctx, &req)
if err != nil {
t.Fatal(err)
}

if output.Status.Code != int32(code.Code_OK) {
t.Fatalf("Expected request to be allowed but got: %v", output)
}

response := output.GetOkResponse()
if response == nil {
t.Fatal("Expected OkHttpResponse struct but got nil")
}

assertDynamicMetadata(t, &_structpb.Struct{
Fields: map[string]*_structpb.Value{
"foo": {
Kind: &_structpb.Value_StringValue{
StringValue: "bar",
},
},
"bar": {
Kind: &_structpb.Value_StringValue{
StringValue: "baz",
},
},
},
}, output.GetDynamicMetadata())
}

func TestCheckAllowBooleanDecisionDynamicMetadata(t *testing.T) {
var req ext_authz.CheckRequest
if err := util.Unmarshal([]byte(exampleAllowedRequestParsedPath), &req); err != nil {
panic(err)
}

module := `
package envoy.authz
default allow = false
allow {
input.parsed_path = ["my", "test", "path"]
}
`

server := testAuthzServerWithModule(module, "envoy/authz/allow", nil, withCustomLogger(&testPlugin{}))
ctx := context.Background()
output, err := server.Check(ctx, &req)
if err != nil {
t.Fatal(err)
}

if output.Status.Code != int32(code.Code_OK) {
t.Fatalf("Expected request to be allowed but got: %v", output)
}

if output.GetDynamicMetadata() != nil {
t.Fatal("Expected nil dynamic metadata when using boolean decision")
}
}

func TestCheckAllowObjectDecisionReqHeadersToRemove(t *testing.T) {
var req ext_authz.CheckRequest
if err := util.Unmarshal([]byte(exampleAllowedRequestParsedPath), &req); err != nil {
Expand Down Expand Up @@ -1463,6 +1551,11 @@ func TestCheckAllowObjectDecision(t *testing.T) {
expectedHeaders[http.CanonicalHeaderKey("y")] = "world"

assertHeaders(t, headers, expectedHeaders)

dynamicMetadata := output.GetDynamicMetadata()
if dynamicMetadata == nil {
t.Fatal("Expected DynamicMetadata struct but got nil")
}
}

func TestCheckDenyObjectDecision(t *testing.T) {
Expand Down Expand Up @@ -1563,6 +1656,21 @@ func TestCheckAllowWithDryRunObjectDecision(t *testing.T) {
expectedHeaders[http.CanonicalHeaderKey("y")] = "world"

assertHeaders(t, headers, expectedHeaders)

assertDynamicMetadata(t, &_structpb.Struct{
Fields: map[string]*_structpb.Value{
"test": {
Kind: &_structpb.Value_StringValue{
StringValue: "foo",
},
},
"bar": {
Kind: &_structpb.Value_StringValue{
StringValue: "baz",
},
},
},
}, output.GetDynamicMetadata())
}

func TestPluginStatusLifeCycle(t *testing.T) {
Expand Down Expand Up @@ -1741,14 +1849,16 @@ func testAuthzServerWithObjectDecision(customConfig *Config, customPluginFuncs .
"allowed": false,
"headers": {"foo": "bar", "baz": "taz"},
"body": "Unauthorized Request",
"http_status": 301
"http_status": 301,
"dynamic_metadata": {"test": "foo", "bar": "baz"}
}
allow = response {
input.parsed_path = ["my", "test", "path"]
response := {
"allowed": true,
"headers": {"x": "hello", "y": "world"}
"headers": {"x": "hello", "y": "world"},
"dynamic_metadata": {"test": "foo", "bar": "baz"}
}
}`

Expand Down Expand Up @@ -2016,6 +2126,13 @@ func assertErrorCounterMetric(t *testing.T, server *envoyExtAuthzGrpcServer, lab
}
}

func assertDynamicMetadata(t *testing.T, expectedMetadata, actualMetadata *_structpb.Struct) {
t.Helper()
if !proto.Equal(expectedMetadata, actualMetadata) {
t.Fatalf("Expected metadata %v but got %v", expectedMetadata, actualMetadata)
}
}

type testPlugin struct {
events []logs.EventV1
}
Expand Down

0 comments on commit de4182f

Please sign in to comment.