Skip to content

Commit 52da8f0

Browse files
authored
Benchmark test for split opa policy and data bundles (#3524)
BenchmarkSplitPolicyAndDataBundles measures the performance of the OPA authorization filter when using a pre-compiled split policy and data bundles loaded from a .tar.gz file. This benchmark evaluates the filter's authorization decision overhead with a pre-compiled bundles, serving as a representative use case. A sample bundles for this benchmark are located at testResources/split-bundles. Users can also use their own bundles. --------- Signed-off-by: amak3 <aleksandra.mak3@gmail.com>
1 parent ad0e685 commit 52da8f0

5 files changed

Lines changed: 179 additions & 30 deletions

File tree

filters/openpolicyagent/opaauthorizerequest/benchmark_test.go

Lines changed: 159 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ func BenchmarkMinimalPolicyWithDecisionLogs(b *testing.B) {
121121
OpaControlPlaneUrl: opaControlPlane.URL(),
122122
DecisionConsumerUrl: decisionLogsConsumer.URL,
123123
DecisionPath: testDecisionPath,
124-
BundleName: testBundleName,
124+
BundleNames: []string{testBundleName},
125125
DecisionLogging: true,
126126
ContextExtensions: "",
127127
}
@@ -297,16 +297,19 @@ func BenchmarkJwtValidation(b *testing.B) {
297297
// bundlePath, and filterOpts variables are correctly configured to match your bundle.
298298
func BenchmarkMinimalPolicyBundle(b *testing.B) {
299299
bundleName := "simple-opa-bundle.tar.gz"
300-
bundlePath := fmt.Sprintf("testResources/%s", bundleName)
301300

302-
opaControlPlane := newOpaControlPlaneServingBundle(bundlePath, bundleName, b)
301+
bundleFiles := map[string]string{
302+
bundleName: fmt.Sprintf("testResources/%s", bundleName),
303+
}
304+
305+
opaControlPlane := newOpaControlPlaneServingDataAndPolicyBundles(b, bundleFiles)
303306
defer opaControlPlane.Close()
304307

305308
filterOpts := FilterOptions{
306309
OpaControlPlaneUrl: opaControlPlane.URL,
307310
DecisionConsumerUrl: opaControlPlane.URL,
308311
DecisionPath: "envoy/authz/allow",
309-
BundleName: bundleName,
312+
BundleNames: []string{bundleName},
310313
DecisionLogging: false,
311314
}
312315
f, err := createOpaFilter(filterOpts)
@@ -333,56 +336,120 @@ func BenchmarkMinimalPolicyBundle(b *testing.B) {
333336
})
334337
}
335338

336-
func newDecisionConsumer() *httptest.Server {
337-
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
338-
w.WriteHeader(http.StatusOK)
339-
}))
340-
}
339+
// BenchmarkSplitPolicyAndDataBundles measures the performance of the OPA authorization filter
340+
// when using a pre-compiled split policy and data bundles loaded from a .tar.gz file.
341341

342-
func newOpaControlPlaneServingBundle(bundlePath, bundleName string, b *testing.B) *httptest.Server {
343-
if !(strings.HasSuffix(bundlePath, ".tar.gz") || strings.HasSuffix(bundlePath, ".tgz")) {
344-
b.Fatalf("bundle file %q does not have the expected .tar.gz or .tgz extension", bundlePath)
342+
// This benchmark evaluates the filter's authorization decision overhead with a
343+
// pre-compiled bundles, serving as a representative use case.
344+
345+
// A sample bundles for this benchmark are located at testResources/split-bundles.
346+
// To generate a .tgz bundle, use the following command:
347+
//
348+
// opa build -b <bundle_directory> -o <output_file.tgz>
349+
//
350+
// For example:
351+
//
352+
// cd testResources/split-bundles
353+
// opa build -b data -o context-data.tgz
354+
// opa build -b policy -o policy.tgz
355+
356+
// You can also use your own bundles. If you do so, ensure that the bundlePaths,
357+
// and filterOpts variables are correctly configured to match your bundle and
358+
// the roots in .manifest files in your bundles do not overlap.
359+
func BenchmarkSplitPolicyAndDataBundles(b *testing.B) {
360+
policyBundle := "policy"
361+
dataBundle := "context-data"
362+
363+
bundleFiles := map[string]string{
364+
policyBundle: "testResources/split-bundles/policy.tgz",
365+
dataBundle: "testResources/split-bundles/context-data.tgz",
345366
}
346367

347-
fileData, err := os.ReadFile(bundlePath)
348-
if err != nil {
349-
b.Fatalf("failed to read bundle file from path %q: %v", bundlePath, err)
368+
opaControlPlane := newOpaControlPlaneServingDataAndPolicyBundles(b, bundleFiles)
369+
defer opaControlPlane.Close()
370+
371+
filterOpts := FilterOptions{
372+
OpaControlPlaneUrl: opaControlPlane.URL,
373+
DecisionConsumerUrl: opaControlPlane.URL,
374+
DecisionPath: "policy/allow",
375+
BundleNames: []string{policyBundle, dataBundle},
376+
DecisionLogging: false,
350377
}
351378

352-
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
353-
if r.URL.Path == "/bundles/"+bundleName {
354-
w.Header().Set("Content-Type", "application/gzip")
355-
w.Header().Set("Content-Disposition", "attachment; filename="+bundleName)
356-
_, err := w.Write(fileData)
357-
if err != nil {
358-
fmt.Printf("failed to write bundle file: %v", err)
359-
w.WriteHeader(http.StatusInternalServerError)
379+
f, err := createOpaFilterForMultipleBundles(filterOpts)
380+
require.NoError(b, err)
381+
382+
requestUrl, err := url.Parse("http://opa-authorized.test/allow/alice")
383+
require.NoError(b, err)
384+
385+
b.ResetTimer()
386+
b.RunParallel(func(pb *testing.PB) {
387+
for pb.Next() {
388+
ctx := &filtertest.Context{
389+
FStateBag: map[string]interface{}{},
390+
FResponse: &http.Response{},
391+
FRequest: &http.Request{
392+
URL: requestUrl,
393+
Method: "GET",
394+
},
395+
FMetrics: &metricstest.MockMetrics{},
396+
}
397+
f.Request(ctx)
398+
assert.False(b, ctx.FServed)
399+
assert.NotEqual(b, 403, ctx.FResponse.StatusCode, "Expected 403 Forbidden response")
400+
}
401+
})
402+
403+
}
404+
405+
func newOpaControlPlaneServingDataAndPolicyBundles(b *testing.B, bundleFiles map[string]string) *httptest.Server {
406+
407+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
408+
for bundleName, bundlePath := range bundleFiles {
409+
if r.URL.Path == "/bundles/"+bundleName {
410+
fileData, err := os.ReadFile(bundlePath)
411+
if err != nil {
412+
b.Fatalf("failed to read bundle file from path %q: %v", bundlePath, err)
413+
}
414+
w.Header().Set("Content-Type", "application/gzip")
415+
w.Header().Set("Content-Disposition", "attachment; filename="+bundleName)
416+
_, err = w.Write(fileData)
417+
if err != nil {
418+
fmt.Printf("failed to write bundle file: %v", err)
419+
w.WriteHeader(http.StatusInternalServerError)
420+
}
421+
return
360422
}
361-
return
362423
}
363424

364-
// Decision logs consumer endpoint
365425
if r.URL.Path == "/logs" {
366426
w.WriteHeader(http.StatusOK)
367427
return
368428
}
369-
370429
w.WriteHeader(http.StatusNotFound)
371430
}))
431+
432+
return server
433+
}
434+
435+
func newDecisionConsumer() *httptest.Server {
436+
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
437+
w.WriteHeader(http.StatusOK)
438+
}))
372439
}
373440

374441
func createOpaFilter(opts FilterOptions) (filters.Filter, error) {
375442
config := generateConfig(opts.OpaControlPlaneUrl, opts.DecisionConsumerUrl, opts.DecisionPath, opts.DecisionLogging)
376443
opaFactory := openpolicyagent.NewOpenPolicyAgentRegistry()
377444
spec := NewOpaAuthorizeRequestSpec(opaFactory, openpolicyagent.WithConfigTemplate(config))
378-
return spec.CreateFilter([]interface{}{opts.BundleName, opts.ContextExtensions})
445+
return spec.CreateFilter([]interface{}{opts.BundleNames[0], opts.ContextExtensions})
379446
}
380447

381448
func createBodyBasedOpaFilter(opts FilterOptions) (filters.Filter, error) {
382449
config := generateConfig(opts.OpaControlPlaneUrl, opts.DecisionConsumerUrl, opts.DecisionPath, opts.DecisionLogging)
383450
opaFactory := openpolicyagent.NewOpenPolicyAgentRegistry()
384451
spec := NewOpaAuthorizeRequestWithBodySpec(opaFactory, openpolicyagent.WithConfigTemplate(config))
385-
return spec.CreateFilter([]interface{}{opts.BundleName, opts.ContextExtensions})
452+
return spec.CreateFilter([]interface{}{opts.BundleNames[0], opts.ContextExtensions})
386453
}
387454

388455
func generateConfig(opaControlPlane string, decisionLogConsumer string, decisionPath string, decisionLogging bool) []byte {
@@ -432,11 +499,73 @@ func generateConfig(opaControlPlane string, decisionLogConsumer string, decision
432499
}`, opaControlPlane, decisionLogConsumer, decisionPlugin, decisionPath))
433500
}
434501

502+
func createOpaFilterForMultipleBundles(opts FilterOptions) (filters.Filter, error) {
503+
config := generateConfigForMultipleBundles(opts.BundleNames, opts.OpaControlPlaneUrl, opts.DecisionConsumerUrl, opts.DecisionPath, opts.DecisionLogging)
504+
opaFactory := openpolicyagent.NewOpenPolicyAgentRegistry()
505+
spec := NewOpaAuthorizeRequestSpec(opaFactory, openpolicyagent.WithConfigTemplate(config))
506+
return spec.CreateFilter([]interface{}{opts.BundleNames[0], opts.ContextExtensions})
507+
}
508+
509+
func generateConfigForMultipleBundles(bundlesNames []string, opaControlPlane string, decisionLogConsumer string, decisionPath string, decisionLogging bool) []byte {
510+
var decisionPlugin string
511+
if decisionLogging {
512+
decisionPlugin = `
513+
"decision_logs": {
514+
"console": false,
515+
"service": "decision_svc",
516+
"reporting": {
517+
"min_delay_seconds": 300,
518+
"max_delay_seconds": 600
519+
}
520+
},
521+
`
522+
}
523+
524+
return []byte(fmt.Sprintf(`{
525+
"services": {
526+
"bundle_svc": {
527+
"url": %q
528+
},
529+
"decision_svc": {
530+
"url": %q
531+
}
532+
},
533+
"bundles": {
534+
"policy": {
535+
"service": "bundle_svc",
536+
"resource": "/bundles/%s",
537+
"polling": {
538+
"min_delay_seconds": 600,
539+
"max_delay_seconds": 1200
540+
}
541+
},
542+
"context-data": {
543+
"service": "bundle_svc",
544+
"resource": "/bundles/%s",
545+
"polling": {
546+
"min_delay_seconds": 600,
547+
"max_delay_seconds": 1200
548+
}
549+
}
550+
},
551+
"labels": {
552+
"environment": "test"
553+
},
554+
%s
555+
"plugins": {
556+
"envoy_ext_authz_grpc": {
557+
"path": %q,
558+
"dry-run": false
559+
}
560+
}
561+
}`, opaControlPlane, decisionLogConsumer, bundlesNames[0], bundlesNames[1], decisionPlugin, decisionPath))
562+
}
563+
435564
type FilterOptions struct {
436565
OpaControlPlaneUrl string
437566
DecisionConsumerUrl string
438567
DecisionPath string
439-
BundleName string
568+
BundleNames []string
440569
DecisionLogging bool
441570
ContextExtensions string
442571
}
@@ -446,7 +575,7 @@ func NewFilterOptionsWithDefaults(opaControlPlaneURL string) FilterOptions {
446575
OpaControlPlaneUrl: opaControlPlaneURL,
447576
DecisionConsumerUrl: "",
448577
DecisionPath: testDecisionPath,
449-
BundleName: testBundleName,
578+
BundleNames: []string{testBundleName},
450579
DecisionLogging: false,
451580
ContextExtensions: "",
452581
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"roots": ["roles"]
3+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"roles": {
3+
"alice": "admin",
4+
"bob": "viewer"
5+
}
6+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"roots": ["policy"]
3+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package policy
2+
3+
default allow = false
4+
5+
allow if {
6+
user := input.parsed_path[1]
7+
data.roles[user] == "admin"
8+
}

0 commit comments

Comments
 (0)