Skip to content

Commit 2a623a9

Browse files
Facilitate OPA decision correlation with business flows (#3041)
* feature: OPA decision correlation with business flows Signed-off-by: Magnus Jungsbluth <[email protected]> Signed-off-by: Janardhan Sharma <[email protected]>
1 parent 8e2c2cc commit 2a623a9

File tree

4 files changed

+106
-15
lines changed

4 files changed

+106
-15
lines changed

docs/tutorials/auth.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,15 @@ The second argument is parsed as YAML, cannot be nested and values need to be st
466466
467467
In Rego this can be used like this `input.attributes.contextExtensions["com.mycompany.myprop"] == "my value"`
468468
469+
### Decision ID in Policies
470+
471+
Each evaluation yields a distinct decision, identifiable by its unique decision ID.
472+
This decision ID can be located within the input at:
473+
474+
`input.attributes.metadataContext.filterMetadata.open_policy_agent.decision_id`
475+
476+
Typical use cases are either propagation of the decision ID to downstream systems or returning it as part of the response. As an example this can allow to trouble shoot deny requests by looking up details using the full decision in a control plane.
477+
469478
### Quick Start Rego Playground
470479
471480
A quick way without setting up Backend APIs is to use the [Rego Playground](https://play.openpolicyagent.org/).

filters/openpolicyagent/evaluation.go

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ package openpolicyagent
33
import (
44
"context"
55
"fmt"
6-
"time"
7-
6+
ext_authz_v3_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
87
ext_authz_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
98
"github.com/open-policy-agent/opa-envoy-plugin/envoyauth"
109
"github.com/open-policy-agent/opa-envoy-plugin/opa/decisionlog"
@@ -13,6 +12,8 @@ import (
1312
"github.com/open-policy-agent/opa/server"
1413
"github.com/open-policy-agent/opa/topdown"
1514
"github.com/opentracing/opentracing-go"
15+
pbstruct "google.golang.org/protobuf/types/known/structpb"
16+
"time"
1617
)
1718

1819
func (opa *OpenPolicyAgentInstance) Eval(ctx context.Context, req *ext_authz_v3.CheckRequest) (*envoyauth.EvalResult, error) {
@@ -23,6 +24,12 @@ func (opa *OpenPolicyAgentInstance) Eval(ctx context.Context, req *ext_authz_v3.
2324
return nil, err
2425
}
2526

27+
err = setDecisionIdInRequest(req, decisionId)
28+
if err != nil {
29+
opa.Logger().WithFields(map[string]interface{}{"err": err}).Error("Unable to set decision ID in Request.")
30+
return nil, err
31+
}
32+
2633
result, stopeval, err := envoyauth.NewEvalResult(withDecisionID(decisionId))
2734
if err != nil {
2835
opa.Logger().WithFields(map[string]interface{}{"err": err}).Error("Unable to generate new result with decision ID.")
@@ -71,6 +78,29 @@ func (opa *OpenPolicyAgentInstance) Eval(ctx context.Context, req *ext_authz_v3.
7178
return result, nil
7279
}
7380

81+
func setDecisionIdInRequest(req *ext_authz_v3.CheckRequest, decisionId string) error {
82+
if req.Attributes.MetadataContext == nil {
83+
req.Attributes.MetadataContext = &ext_authz_v3_core.Metadata{
84+
FilterMetadata: map[string]*pbstruct.Struct{},
85+
}
86+
}
87+
88+
filterMeta, err := formOpenPolicyAgentMetaDataObject(decisionId)
89+
if err != nil {
90+
return err
91+
}
92+
req.Attributes.MetadataContext.FilterMetadata["open_policy_agent"] = filterMeta
93+
return nil
94+
}
95+
96+
func formOpenPolicyAgentMetaDataObject(decisionId string) (*pbstruct.Struct, error) {
97+
98+
innerFields := make(map[string]interface{})
99+
innerFields["decision_id"] = decisionId
100+
101+
return pbstruct.NewStruct(innerFields)
102+
}
103+
74104
func (opa *OpenPolicyAgentInstance) logDecision(ctx context.Context, input interface{}, result *envoyauth.EvalResult, err error) error {
75105
info := &server.Info{
76106
Timestamp: time.Now(),

filters/openpolicyagent/opaauthorizerequest/opaauthorizerequest_test.go

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,21 @@ func TestAuthorizeRequestFilter(t *testing.T) {
255255
backendHeaders: make(http.Header),
256256
removeHeaders: make(http.Header),
257257
},
258+
{
259+
msg: "Decision id in request header",
260+
filterName: "opaAuthorizeRequest",
261+
bundleName: "somebundle.tar.gz",
262+
regoQuery: "envoy/authz/allow_object_decision_id_in_header",
263+
requestMethod: "POST",
264+
body: `{ "target_id" : "123456" }`,
265+
requestHeaders: map[string][]string{"content-type": {"application/json"}},
266+
requestPath: "/allow/structured",
267+
expectedStatus: http.StatusOK,
268+
expectedBody: "Welcome!",
269+
expectedHeaders: map[string][]string{"Decision-Id": {"some-random-decision-id-generated-during-evaluation"}},
270+
backendHeaders: make(http.Header),
271+
removeHeaders: make(http.Header),
272+
},
258273
{
259274
msg: "Invalid UTF-8 in Path",
260275
filterName: "opaAuthorizeRequest",
@@ -345,8 +360,21 @@ func TestAuthorizeRequestFilter(t *testing.T) {
345360
346361
allow_body {
347362
input.parsed_body.target_id == "123456"
348-
}
349-
`,
363+
}
364+
365+
decision_id := input.attributes.metadataContext.filterMetadata.open_policy_agent.decision_id
366+
367+
allow_object_decision_id_in_header = response {
368+
input.parsed_path = ["allow", "structured"]
369+
decision_id
370+
response := {
371+
"allowed": true,
372+
"response_headers_to_add": {
373+
"decision-id": decision_id
374+
}
375+
}
376+
}
377+
`,
350378
}),
351379
)
352380

@@ -374,10 +402,24 @@ func TestAuthorizeRequestFilter(t *testing.T) {
374402
}
375403
}`, opaControlPlane.URL(), ti.regoQuery))
376404

405+
envoyMetaDataConfig := []byte(`{
406+
"filter_metadata": {
407+
"envoy.filters.http.header_to_metadata": {
408+
"policy_type": "ingress"
409+
}
410+
}
411+
}`)
412+
413+
opts := make([]func(*openpolicyagent.OpenPolicyAgentInstanceConfig) error, 0)
414+
opts = append(opts,
415+
openpolicyagent.WithConfigTemplate(config),
416+
openpolicyagent.WithEnvoyMetadataBytes(envoyMetaDataConfig))
417+
377418
opaFactory := openpolicyagent.NewOpenPolicyAgentRegistry(openpolicyagent.WithTracer(&tracingtest.Tracer{}))
378-
ftSpec := NewOpaAuthorizeRequestSpec(opaFactory, openpolicyagent.WithConfigTemplate(config))
419+
ftSpec := NewOpaAuthorizeRequestSpec(opaFactory, opts...)
420+
379421
fr.Register(ftSpec)
380-
ftSpec = NewOpaAuthorizeRequestWithBodySpec(opaFactory, openpolicyagent.WithConfigTemplate(config))
422+
ftSpec = NewOpaAuthorizeRequestWithBodySpec(opaFactory, opts...)
381423
fr.Register(ftSpec)
382424

383425
r := eskip.MustParse(fmt.Sprintf(`* -> %s("%s", "%s") %s -> "%s"`, ti.filterName, ti.bundleName, ti.contextExtensions, ti.extraeskip, clientServer.URL))
@@ -435,8 +477,12 @@ func isHeadersPresent(t *testing.T, expectedHeaders http.Header, headers http.He
435477
if !headerFound {
436478
return false
437479
}
438-
439-
assert.ElementsMatch(t, expectedValues, actualValues)
480+
// since decision id is randomly generated we are just checking for not nil
481+
if headerName == "Decision-Id" {
482+
assert.NotNil(t, actualValues)
483+
} else {
484+
assert.ElementsMatch(t, expectedValues, actualValues)
485+
}
440486
}
441487
return true
442488
}

filters/openpolicyagent/openpolicyagent_test.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import (
2929
"github.com/zalando/skipper/routing"
3030
"github.com/zalando/skipper/tracing/tracingtest"
3131
"google.golang.org/protobuf/encoding/protojson"
32-
_struct "google.golang.org/protobuf/types/known/structpb"
32+
pbstruct "google.golang.org/protobuf/types/known/structpb"
3333
)
3434

3535
type MockOpenPolicyAgentFilter struct {
@@ -63,22 +63,22 @@ func TestInterpolateTemplate(t *testing.T) {
6363

6464
func TestLoadEnvoyMetadata(t *testing.T) {
6565
cfg := &OpenPolicyAgentInstanceConfig{}
66-
WithEnvoyMetadataBytes([]byte(`
66+
_ = WithEnvoyMetadataBytes([]byte(`
6767
{
6868
"filter_metadata": {
6969
"envoy.filters.http.header_to_metadata": {
7070
"policy_type": "ingress"
71-
}
71+
},
7272
}
7373
}
7474
`))(cfg)
7575

7676
expectedBytes, err := protojson.Marshal(&ext_authz_v3_core.Metadata{
77-
FilterMetadata: map[string]*_struct.Struct{
77+
FilterMetadata: map[string]*pbstruct.Struct{
7878
"envoy.filters.http.header_to_metadata": {
79-
Fields: map[string]*_struct.Value{
79+
Fields: map[string]*pbstruct.Value{
8080
"policy_type": {
81-
Kind: &_struct.Value_StringValue{StringValue: "ingress"},
81+
Kind: &pbstruct.Value_StringValue{StringValue: "ingress"},
8282
},
8383
},
8484
},
@@ -411,7 +411,13 @@ func TestEval(t *testing.T) {
411411
span := tracer.StartSpan("open-policy-agent")
412412
ctx := opentracing.ContextWithSpan(context.Background(), span)
413413

414-
result, err := inst.Eval(ctx, &authv3.CheckRequest{})
414+
result, err := inst.Eval(ctx, &authv3.CheckRequest{
415+
Attributes: &authv3.AttributeContext{
416+
Request: nil,
417+
ContextExtensions: nil,
418+
MetadataContext: nil,
419+
},
420+
})
415421
assert.NoError(t, err)
416422

417423
allowed, err := result.IsAllowed()

0 commit comments

Comments
 (0)