Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Facilitate OPA decision correlation with business flows #3041

Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/tutorials/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,15 @@ The second argument is parsed as YAML, cannot be nested and values need to be st

In Rego this can be used like this `input.attributes.contextExtensions["com.mycompany.myprop"] == "my value"`

### Decision's Identity
JanardhanSharma marked this conversation as resolved.
Show resolved Hide resolved

Each evaluation yields a distinct decision, identifiable by its unique decision ID.
This decision ID can be located within the input at:

`input.attributes.metadataContext.filterMetadata.open_policy_agent.decision_id`

Leveraging this ID enables seamless correlation of decisions with the flow ID of the originating request.
JanardhanSharma marked this conversation as resolved.
Show resolved Hide resolved

### Quick Start Rego Playground

A quick way without setting up Backend APIs is to use the [Rego Playground](https://play.openpolicyagent.org/).
Expand Down
48 changes: 46 additions & 2 deletions filters/openpolicyagent/evaluation.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ package openpolicyagent

import (
"context"
"encoding/json"
"fmt"
"time"

ext_authz_v3_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
ext_authz_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
"github.com/open-policy-agent/opa-envoy-plugin/envoyauth"
"github.com/open-policy-agent/opa-envoy-plugin/opa/decisionlog"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/rego"
"github.com/open-policy-agent/opa/server"
"github.com/opentracing/opentracing-go"
pbstruct "google.golang.org/protobuf/types/known/structpb"
"time"
)

func (opa *OpenPolicyAgentInstance) Eval(ctx context.Context, req *ext_authz_v3.CheckRequest) (*envoyauth.EvalResult, error) {
Expand All @@ -22,6 +24,8 @@ func (opa *OpenPolicyAgentInstance) Eval(ctx context.Context, req *ext_authz_v3.
return nil, err
}

setDecisionIdInRequest(req, decisionId)

result, stopeval, err := envoyauth.NewEvalResult(withDecisionID(decisionId))
if err != nil {
opa.Logger().WithFields(map[string]interface{}{"err": err}).Error("Unable to generate new result with decision ID.")
Expand Down Expand Up @@ -65,6 +69,46 @@ func (opa *OpenPolicyAgentInstance) Eval(ctx context.Context, req *ext_authz_v3.
return result, nil
}

func setDecisionIdInRequest(req *ext_authz_v3.CheckRequest, decisionId string) {
if metaDataContextDoesNotExist(req) {
JanardhanSharma marked this conversation as resolved.
Show resolved Hide resolved
req.Attributes.MetadataContext = formFilterMetadata(decisionId)
} else {
req.Attributes.MetadataContext.FilterMetadata["open_policy_agent"] = formOpenPolicyAgentMetaDataObject(decisionId)
}
}

func metaDataContextDoesNotExist(req *ext_authz_v3.CheckRequest) bool {
return req.Attributes.MetadataContext == nil
JanardhanSharma marked this conversation as resolved.
Show resolved Hide resolved
}

func formFilterMetadata(decisionId string) *ext_authz_v3_core.Metadata {
metaData := &ext_authz_v3_core.Metadata{
FilterMetadata: map[string]*pbstruct.Struct{
"open_policy_agent": {
Fields: map[string]*pbstruct.Value{
"decision_id": {
Kind: &pbstruct.Value_StringValue{StringValue: decisionId},
},
},
},
},
}
return metaData
}

func formOpenPolicyAgentMetaDataObject(decisionId string) *pbstruct.Struct {
nestedStruct := &pbstruct.Struct{}
nestedStruct.Fields = make(map[string]*pbstruct.Value)

innerFields := make(map[string]interface{})
innerFields["decision_id"] = decisionId

innerBytes, _ := json.Marshal(innerFields)
JanardhanSharma marked this conversation as resolved.
Show resolved Hide resolved
_ = json.Unmarshal(innerBytes, &nestedStruct)

return nestedStruct
}

func (opa *OpenPolicyAgentInstance) logDecision(ctx context.Context, input interface{}, result *envoyauth.EvalResult, err error) error {
info := &server.Info{
Timestamp: time.Now(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,21 @@ func TestAuthorizeRequestFilter(t *testing.T) {
backendHeaders: make(http.Header),
removeHeaders: make(http.Header),
},
{
msg: "Decision id in request header",
filterName: "opaAuthorizeRequest",
bundleName: "somebundle.tar.gz",
regoQuery: "envoy/authz/allow_object_decision_id_in_header",
requestMethod: "POST",
body: `{ "target_id" : "123456" }`,
requestHeaders: map[string][]string{"content-type": {"application/json"}},
requestPath: "/allow/structured",
expectedStatus: http.StatusOK,
expectedBody: "Welcome!",
expectedHeaders: map[string][]string{"Decision-Id": {"some-random-decision-id-generated-during-evaluation"}},
backendHeaders: make(http.Header),
removeHeaders: make(http.Header),
},
} {
t.Run(ti.msg, func(t *testing.T) {
t.Logf("Running test for %v", ti)
Expand Down Expand Up @@ -331,8 +346,21 @@ func TestAuthorizeRequestFilter(t *testing.T) {

allow_body {
input.parsed_body.target_id == "123456"
}
`,
}

decision_id := input.attributes.metadataContext.filterMetadata.open_policy_agent.decision_id

allow_object_decision_id_in_header = response {
input.parsed_path = ["allow", "structured"]
decision_id
response := {
"allowed": true,
"response_headers_to_add": {
"decision-id": decision_id
}
}
}
`,
}),
)

Expand Down Expand Up @@ -360,10 +388,24 @@ func TestAuthorizeRequestFilter(t *testing.T) {
}
}`, opaControlPlane.URL(), ti.regoQuery))

envoyMetaDataConfig := []byte(`{
"filter_metadata": {
"envoy.filters.http.header_to_metadata": {
"policy_type": "ingress"
}
}
}`)

opts := make([]func(*openpolicyagent.OpenPolicyAgentInstanceConfig) error, 0)
opts = append(opts,
openpolicyagent.WithConfigTemplate(config),
openpolicyagent.WithEnvoyMetadataBytes(envoyMetaDataConfig))

opaFactory := openpolicyagent.NewOpenPolicyAgentRegistry(openpolicyagent.WithTracer(&tracingtest.Tracer{}))
ftSpec := NewOpaAuthorizeRequestSpec(opaFactory, openpolicyagent.WithConfigTemplate(config))
ftSpec := NewOpaAuthorizeRequestSpec(opaFactory, opts...)

fr.Register(ftSpec)
ftSpec = NewOpaAuthorizeRequestWithBodySpec(opaFactory, openpolicyagent.WithConfigTemplate(config))
ftSpec = NewOpaAuthorizeRequestWithBodySpec(opaFactory, opts...)
fr.Register(ftSpec)

r := eskip.MustParse(fmt.Sprintf(`* -> %s("%s", "%s") %s -> "%s"`, ti.filterName, ti.bundleName, ti.contextExtensions, ti.extraeskip, clientServer.URL))
Expand Down Expand Up @@ -421,8 +463,12 @@ func isHeadersPresent(t *testing.T, expectedHeaders http.Header, headers http.He
if !headerFound {
return false
}

assert.ElementsMatch(t, expectedValues, actualValues)
// since decision id is randomly generated we are just checking for not nil
if headerName == "Decision-Id" {
assert.NotNil(t, actualValues)
} else {
assert.ElementsMatch(t, expectedValues, actualValues)
}
}
return true
}
Expand Down
29 changes: 23 additions & 6 deletions filters/openpolicyagent/openpolicyagent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (

ext_authz_v3_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
authv3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
ext_authz_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
"github.com/open-policy-agent/opa-envoy-plugin/envoyauth"
opaconf "github.com/open-policy-agent/opa/config"
opasdktest "github.com/open-policy-agent/opa/sdk/test"
Expand All @@ -29,7 +30,7 @@ import (
"github.com/zalando/skipper/routing"
"github.com/zalando/skipper/tracing/tracingtest"
"google.golang.org/protobuf/encoding/protojson"
_struct "google.golang.org/protobuf/types/known/structpb"
pbstruct "google.golang.org/protobuf/types/known/structpb"
)

type MockOpenPolicyAgentFilter struct {
Expand Down Expand Up @@ -63,22 +64,32 @@ func TestInterpolateTemplate(t *testing.T) {

func TestLoadEnvoyMetadata(t *testing.T) {
cfg := &OpenPolicyAgentInstanceConfig{}
WithEnvoyMetadataBytes([]byte(`
_ = WithEnvoyMetadataBytes([]byte(`
{
"filter_metadata": {
"envoy.filters.http.header_to_metadata": {
"policy_type": "ingress"
},
"open_policy_agent" : {
"decision_id" : "3b567656-bf28-4a63-a4c4-14407fbd9544"
}
}
}
`))(cfg)

expectedBytes, err := protojson.Marshal(&ext_authz_v3_core.Metadata{
FilterMetadata: map[string]*_struct.Struct{
FilterMetadata: map[string]*pbstruct.Struct{
"envoy.filters.http.header_to_metadata": {
Fields: map[string]*_struct.Value{
Fields: map[string]*pbstruct.Value{
"policy_type": {
Kind: &_struct.Value_StringValue{StringValue: "ingress"},
Kind: &pbstruct.Value_StringValue{StringValue: "ingress"},
},
},
},
"open_policy_agent": {
JanardhanSharma marked this conversation as resolved.
Show resolved Hide resolved
Fields: map[string]*pbstruct.Value{
"decision_id": {
Kind: &pbstruct.Value_StringValue{StringValue: "3b567656-bf28-4a63-a4c4-14407fbd9544"},
},
},
},
Expand Down Expand Up @@ -411,7 +422,13 @@ func TestEval(t *testing.T) {
span := tracer.StartSpan("open-policy-agent")
ctx := opentracing.ContextWithSpan(context.Background(), span)

result, err := inst.Eval(ctx, &authv3.CheckRequest{})
result, err := inst.Eval(ctx, &authv3.CheckRequest{
Attributes: &ext_authz_v3.AttributeContext{
Request: nil,
ContextExtensions: nil,
MetadataContext: nil,
},
})
assert.NoError(t, err)

allowed, err := result.IsAllowed()
Expand Down