Merge pull request #615 from 711ayush711/feat/policyEnforcer

Implement Policy Enforcer Plugin
This commit is contained in:
Mayuresh A Nirhali
2026-03-25 15:31:07 +05:30
committed by GitHub
23 changed files with 3014 additions and 40 deletions

View File

@@ -5,6 +5,7 @@ import (
"errors"
"flag"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
@@ -13,6 +14,7 @@ import (
"github.com/beckn-one/beckn-onix/core/module"
"github.com/beckn-one/beckn-onix/core/module/handler"
"github.com/beckn-one/beckn-onix/pkg/model"
"github.com/beckn-one/beckn-onix/pkg/plugin"
"github.com/beckn-one/beckn-onix/pkg/plugin/definition"
"github.com/stretchr/testify/mock"
@@ -21,6 +23,15 @@ import (
// MockPluginManager implements handler.PluginManager for testing.
type MockPluginManager struct {
mock.Mock
policyCheckerFunc func(ctx context.Context, cfg *plugin.Config) (definition.PolicyChecker, error)
}
type stubPolicyChecker struct {
err error
}
func (s stubPolicyChecker) CheckPolicy(*model.StepContext) error {
return s.err
}
// Middleware returns a middleware function based on the provided configuration.
@@ -83,6 +94,14 @@ func (m *MockPluginManager) SchemaValidator(ctx context.Context, cfg *plugin.Con
return nil, nil
}
// PolicyChecker returns a mock implementation of the PolicyChecker interface.
func (m *MockPluginManager) PolicyChecker(ctx context.Context, cfg *plugin.Config) (definition.PolicyChecker, error) {
if m.policyCheckerFunc != nil {
return m.policyCheckerFunc(ctx, cfg)
}
return nil, nil
}
// mockRun is a mock implementation of the `run` function, simulating a successful run.
func mockRun(ctx context.Context, configPath string) error {
return nil // Simulate a successful run
@@ -186,8 +205,8 @@ func TestRunFailure(t *testing.T) {
}
defer func() { newManagerFunc = originalNewManager }()
originalNewServer := newServerFunc
newServerFunc = func(ctx context.Context, mgr handler.PluginManager, cfg *Config) (http.Handler, error) {
originalNewServer := newServerFunc
newServerFunc = func(ctx context.Context, mgr handler.PluginManager, cfg *Config) (http.Handler, error) {
return tt.mockServer(ctx, mgr, cfg)
}
defer func() { newServerFunc = originalNewServer }()
@@ -330,6 +349,49 @@ func TestNewServerSuccess(t *testing.T) {
}
}
func TestNewServerRejectsPolicyViolation(t *testing.T) {
mockMgr := &MockPluginManager{
policyCheckerFunc: func(ctx context.Context, cfg *plugin.Config) (definition.PolicyChecker, error) {
return stubPolicyChecker{err: model.NewBadReqErr(errors.New("blocked by policy"))}, nil
},
}
cfg := &Config{
Modules: []module.Config{
{
Name: "policy-module",
Path: "/policy",
Handler: handler.Config{
Type: handler.HandlerTypeStd,
Plugins: handler.PluginCfg{
PolicyChecker: &plugin.Config{ID: "mock-policy"},
},
Steps: []string{"checkPolicy"},
},
},
},
}
h, err := newServer(context.Background(), mockMgr, cfg)
if err != nil {
t.Fatalf("expected no error creating server, got %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/policy", strings.NewReader(`{"context":{"action":"confirm"}}`))
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400 for policy violation, got %d", rec.Code)
}
if !strings.Contains(rec.Body.String(), "NACK") {
t.Fatalf("expected NACK response, got %s", rec.Body.String())
}
if !strings.Contains(rec.Body.String(), "blocked by policy") {
t.Fatalf("expected policy error in response, got %s", rec.Body.String())
}
}
// TestNewServerFailure tests failure scenarios when creating a server.
func TestNewServerFailure(t *testing.T) {
tests := []struct {

View File

@@ -86,6 +86,17 @@ modules:
id: router
config:
routingConfig: ./config/local-beckn-one-routing-BAPReceiver.yaml
checkPolicy:
id: opapolicychecker
config:
# Policy source configuration.
# type: url | file | dir | bundle
# location: path or URL to policy source
# query: OPA query path to evaluate (required)
type: file
location: ./pkg/plugin/implementation/opapolicychecker/testdata/example.rego
query: "data.policy.result"
refreshIntervalSeconds: "300"
middleware:
- id: reqpreprocessor
config:
@@ -93,6 +104,7 @@ modules:
role: bap
steps:
- validateSign
- checkPolicy
- addRoute
- validateSchema
@@ -151,7 +163,15 @@ modules:
config:
contextKeys: transaction_id,message_id
role: bap
checkPolicy:
id: opapolicychecker
config:
type: file
location: ./pkg/plugin/implementation/opapolicychecker/testdata/example.rego
query: "data.policy.result"
refreshIntervalSeconds: "300"
steps:
- checkPolicy
- addRoute
- sign
- validateSchema

View File

@@ -84,8 +84,20 @@ modules:
id: router
config:
routingConfig: ./config/local-beckn-one-routing-BPPReceiver.yaml
checkPolicy:
id: opapolicychecker
config:
# Policy source configuration.
# type: url | file | dir | bundle
# location: path or URL to policy source
# query: OPA query path to evaluate (required)
type: file
location: ./pkg/plugin/implementation/opapolicychecker/testdata/example.rego
query: "data.policy.result"
refreshIntervalSeconds: "300"
steps:
- validateSign
- checkPolicy
- addRoute
- validateSchema
@@ -144,7 +156,15 @@ modules:
config:
contextKeys: transaction_id,message_id
role: bpp
checkPolicy:
id: opapolicychecker
config:
type: file
location: ./pkg/plugin/implementation/opapolicychecker/testdata/example.rego
query: "data.policy.result"
refreshIntervalSeconds: "300"
steps:
- checkPolicy
- addRoute
- sign
- validateSchema

View File

@@ -66,6 +66,13 @@ modules:
id: router
config:
routingConfig: ./config/local-simple-routing.yaml
checkPolicy:
id: opapolicychecker
config:
type: file
location: ./pkg/plugin/implementation/opapolicychecker/testdata/example.rego
query: "data.policy.result"
refreshIntervalSeconds: "300"
middleware:
- id: reqpreprocessor
config:
@@ -73,6 +80,7 @@ modules:
role: bap
steps:
- validateSign
- checkPolicy
- addRoute
- name: bapTxnCaller
@@ -162,8 +170,16 @@ modules:
id: router
config:
routingConfig: ./config/local-simple-routing-BPPReceiver.yaml
checkPolicy:
id: opapolicychecker
config:
type: file
location: ./pkg/plugin/implementation/opapolicychecker/testdata/example.rego
query: "data.policy.result"
refreshIntervalSeconds: "300"
steps:
- validateSign
- checkPolicy
- addRoute
- name: bppTxnCaller

View File

@@ -48,6 +48,17 @@ modules:
id: schemavalidator
config:
schemaDir: /mnt/gcs/configs/schemas
checkPolicy:
id: opapolicychecker
config:
# Policy source configuration.
# type: url | file | dir | bundle
# location: path or URL to policy source
# query: OPA query path to evaluate (required)
type: file
location: ./pkg/plugin/implementation/opapolicychecker/testdata/example.rego
query: "data.policy.result"
refreshIntervalSeconds: "300"
signValidator:
id: signvalidator
publisher:
@@ -66,6 +77,7 @@ modules:
role: bap
steps:
- validateSign
- checkPolicy
- addRoute
- validateSchema
- name: bapTxnCaller
@@ -98,6 +110,13 @@ modules:
id: schemavalidator
config:
schemaDir: /mnt/gcs/configs/schemas
checkPolicy:
id: opapolicychecker
config:
type: file
location: ./pkg/plugin/implementation/opapolicychecker/testdata/example.rego
query: "data.policy.result"
refreshIntervalSeconds: "300"
signer:
id: signer
publisher:
@@ -116,6 +135,7 @@ modules:
role: bap
steps:
- validateSchema
- checkPolicy
- addRoute
- sign
- name: bppTxnReciever
@@ -149,6 +169,13 @@ modules:
id: schemavalidator
config:
schemaDir: /mnt/gcs/configs/schemas
checkPolicy:
id: opapolicychecker
config:
type: file
location: ./pkg/plugin/implementation/opapolicychecker/testdata/example.rego
query: "data.policy.result"
refreshIntervalSeconds: "300"
signValidator:
id: signvalidator
publisher:
@@ -167,6 +194,7 @@ modules:
role: bpp
steps:
- validateSign
- checkPolicy
- addRoute
- validateSchema
- name: bppTxnCaller
@@ -199,6 +227,13 @@ modules:
id: schemavalidator
config:
schemaDir: /mnt/gcs/configs/schemas
checkPolicy:
id: opapolicychecker
config:
type: file
location: ./pkg/plugin/implementation/opapolicychecker/testdata/example.rego
query: "data.policy.result"
refreshIntervalSeconds: "300"
signer:
id: signer
publisher:
@@ -217,5 +252,6 @@ modules:
role: bpp
steps:
- validateSchema
- checkPolicy
- addRoute
- sign

View File

@@ -19,6 +19,7 @@ type PluginManager interface {
Publisher(ctx context.Context, cfg *plugin.Config) (definition.Publisher, error)
Signer(ctx context.Context, cfg *plugin.Config) (definition.Signer, error)
Step(ctx context.Context, cfg *plugin.Config) (definition.Step, error)
PolicyChecker(ctx context.Context, cfg *plugin.Config) (definition.PolicyChecker, error)
Cache(ctx context.Context, cfg *plugin.Config) (definition.Cache, error)
Registry(ctx context.Context, cfg *plugin.Config) (definition.RegistryLookup, error)
KeyManager(ctx context.Context, cache definition.Cache, rLookup definition.RegistryLookup, cfg *plugin.Config) (definition.KeyManager, error)
@@ -37,6 +38,7 @@ const (
// PluginCfg holds the configuration for various plugins.
type PluginCfg struct {
SchemaValidator *plugin.Config `yaml:"schemaValidator,omitempty"`
PolicyChecker *plugin.Config `yaml:"checkPolicy,omitempty"`
SignValidator *plugin.Config `yaml:"signValidator,omitempty"`
Publisher *plugin.Config `yaml:"publisher,omitempty"`
Signer *plugin.Config `yaml:"signer,omitempty"`

View File

@@ -35,6 +35,7 @@ type stdHandler struct {
registry definition.RegistryLookup
km definition.KeyManager
schemaValidator definition.SchemaValidator
policyChecker definition.PolicyChecker
router definition.Router
publisher definition.Publisher
transportWrapper definition.TransportWrapper
@@ -318,6 +319,9 @@ func (h *stdHandler) initPlugins(ctx context.Context, mgr PluginManager, cfg *Pl
if h.transportWrapper, err = loadPlugin(ctx, "TransportWrapper", cfg.TransportWrapper, mgr.TransportWrapper); err != nil {
return err
}
if h.policyChecker, err = loadPlugin(ctx, "PolicyChecker", cfg.PolicyChecker, mgr.PolicyChecker); err != nil {
return err
}
log.Debugf(ctx, "All required plugins successfully loaded for stdHandler")
return nil
@@ -350,6 +354,8 @@ func (h *stdHandler) initSteps(ctx context.Context, mgr PluginManager, cfg *Conf
s, err = newValidateSchemaStep(h.schemaValidator)
case "addRoute":
s, err = newAddRouteStep(h.router)
case "checkPolicy":
s, err = newCheckPolicyStep(h.policyChecker)
default:
if customStep, exists := steps[step]; exists {
s = customStep

View File

@@ -1,11 +1,69 @@
package handler
import (
"context"
"net/http"
"strings"
"testing"
"time"
"github.com/beckn-one/beckn-onix/pkg/plugin"
"github.com/beckn-one/beckn-onix/pkg/plugin/definition"
)
// noopPluginManager satisfies PluginManager with nil plugins (unused loaders are never invoked when config is omitted).
type noopPluginManager struct{}
func (noopPluginManager) Middleware(context.Context, *plugin.Config) (func(http.Handler) http.Handler, error) {
return nil, nil
}
func (noopPluginManager) SignValidator(context.Context, *plugin.Config) (definition.SignValidator, error) {
return nil, nil
}
func (noopPluginManager) Validator(context.Context, *plugin.Config) (definition.SchemaValidator, error) {
return nil, nil
}
func (noopPluginManager) Router(context.Context, *plugin.Config) (definition.Router, error) { return nil, nil }
func (noopPluginManager) Publisher(context.Context, *plugin.Config) (definition.Publisher, error) {
return nil, nil
}
func (noopPluginManager) Signer(context.Context, *plugin.Config) (definition.Signer, error) { return nil, nil }
func (noopPluginManager) Step(context.Context, *plugin.Config) (definition.Step, error) { return nil, nil }
func (noopPluginManager) PolicyChecker(context.Context, *plugin.Config) (definition.PolicyChecker, error) {
return nil, nil
}
func (noopPluginManager) Cache(context.Context, *plugin.Config) (definition.Cache, error) { return nil, nil }
func (noopPluginManager) Registry(context.Context, *plugin.Config) (definition.RegistryLookup, error) {
return nil, nil
}
func (noopPluginManager) KeyManager(context.Context, definition.Cache, definition.RegistryLookup, *plugin.Config) (definition.KeyManager, error) {
return nil, nil
}
func (noopPluginManager) TransportWrapper(context.Context, *plugin.Config) (definition.TransportWrapper, error) {
return nil, nil
}
func (noopPluginManager) SchemaValidator(context.Context, *plugin.Config) (definition.SchemaValidator, error) {
return nil, nil
}
func TestNewStdHandler_CheckPolicyStepWithoutPluginFails(t *testing.T) {
ctx := context.Background()
cfg := &Config{
Plugins: PluginCfg{},
Steps: []string{"checkPolicy"},
}
_, err := NewStdHandler(ctx, noopPluginManager{}, cfg, "testModule")
if err == nil {
t.Fatal("expected error when steps list checkPolicy but checkPolicy plugin is omitted")
}
if !strings.Contains(err.Error(), "failed to initialize steps") {
t.Fatalf("expected steps init failure, got: %v", err)
}
if !strings.Contains(err.Error(), "PolicyChecker plugin not configured") {
t.Fatalf("expected explicit PolicyChecker config error, got: %v", err)
}
}
func TestNewHTTPClient(t *testing.T) {
tests := []struct {
name string

View File

@@ -315,3 +315,19 @@ func extractSchemaVersion(body []byte) string {
}
return "unknown"
}
// checkPolicyStep adapts PolicyChecker into the Step interface.
type checkPolicyStep struct {
checker definition.PolicyChecker
}
func newCheckPolicyStep(policyChecker definition.PolicyChecker) (definition.Step, error) {
if policyChecker == nil {
return nil, fmt.Errorf("invalid config: PolicyChecker plugin not configured")
}
return &checkPolicyStep{checker: policyChecker}, nil
}
func (s *checkPolicyStep) Run(ctx *model.StepContext) error {
return s.checker.CheckPolicy(ctx)
}

View File

@@ -5,6 +5,7 @@ import (
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/beckn-one/beckn-onix/core/module/handler"
@@ -16,7 +17,8 @@ import (
// mockPluginManager is a mock implementation of the PluginManager interface
// with support for dynamically setting behavior.
type mockPluginManager struct {
middlewareFunc func(ctx context.Context, cfg *plugin.Config) (func(http.Handler) http.Handler, error)
middlewareFunc func(ctx context.Context, cfg *plugin.Config) (func(http.Handler) http.Handler, error)
policyCheckerFunc func(ctx context.Context, cfg *plugin.Config) (definition.PolicyChecker, error)
}
// Middleware returns a mock middleware function based on the provided configuration.
@@ -79,7 +81,67 @@ func (m *mockPluginManager) SchemaValidator(ctx context.Context, cfg *plugin.Con
return nil, nil
}
// PolicyChecker returns a mock policy checker implementation.
func (m *mockPluginManager) PolicyChecker(ctx context.Context, cfg *plugin.Config) (definition.PolicyChecker, error) {
if m.policyCheckerFunc != nil {
return m.policyCheckerFunc(ctx, cfg)
}
return nil, nil
}
type mockPolicyChecker struct {
err error
}
func (m mockPolicyChecker) CheckPolicy(*model.StepContext) error {
return m.err
}
// TestRegisterSuccess tests scenarios where the handler registration should succeed.
func TestRegisterRejectsPolicyViolation(t *testing.T) {
mCfgs := []Config{
{
Name: "test-module",
Path: "/test",
Handler: handler.Config{
Type: handler.HandlerTypeStd,
Plugins: handler.PluginCfg{
PolicyChecker: &plugin.Config{ID: "mock-policy"},
},
Steps: []string{"checkPolicy"},
},
},
}
mockManager := &mockPluginManager{
middlewareFunc: func(ctx context.Context, cfg *plugin.Config) (func(http.Handler) http.Handler, error) {
return func(next http.Handler) http.Handler { return next }, nil
},
policyCheckerFunc: func(ctx context.Context, cfg *plugin.Config) (definition.PolicyChecker, error) {
return mockPolicyChecker{err: model.NewBadReqErr(errors.New("blocked by policy"))}, nil
},
}
mux := http.NewServeMux()
if err := Register(context.Background(), mCfgs, mux, mockManager); err != nil {
t.Fatalf("unexpected register error: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/test", strings.NewReader(`{"context":{"action":"confirm"}}`))
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400 for policy violation, got %d", rec.Code)
}
if !strings.Contains(rec.Body.String(), "NACK") {
t.Fatalf("expected NACK response, got %s", rec.Body.String())
}
if !strings.Contains(rec.Body.String(), "blocked by policy") {
t.Fatalf("expected policy error in response, got %s", rec.Body.String())
}
}
func TestRegisterSuccess(t *testing.T) {
mCfgs := []Config{
{

44
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/beckn-one/beckn-onix
go 1.24.0
go 1.24.6
require (
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1
@@ -22,16 +22,20 @@ require github.com/zenazn/pkcs7pad v0.0.0-20170308005700-253a5b1f0e03
require golang.org/x/text v0.33.0 // indirect
require (
github.com/agnivade/levenshtein v1.2.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
@@ -41,33 +45,52 @@ require (
github.com/hashicorp/go-sockaddr v1.0.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
github.com/lestrrat-go/dsig v1.0.0 // indirect
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc/v3 v3.0.2 // indirect
github.com/lestrrat-go/jwx/v3 v3.0.13 // indirect
github.com/lestrrat-go/option/v2 v2.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/prometheus/client_model v0.6.0 // indirect
github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.16.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/tchap/go-patricia/v2 v2.3.3 // indirect
github.com/valyala/fastjson v1.6.7 // indirect
github.com/vektah/gqlparser/v2 v2.5.31 // indirect
github.com/woodsbury/decimal128 v1.3.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/yashtewari/glob-intersection v0.2.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
go.opentelemetry.io/otel/log v0.16.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/ini.v1 v1.67.1 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
require (
@@ -76,16 +99,19 @@ require (
github.com/hashicorp/go-retryablehttp v0.7.7
github.com/hashicorp/vault/api v1.16.0
github.com/jsonata-go/jsonata v0.0.0-20250709164031-599f35f32e5f
github.com/prometheus/client_golang v1.18.0
github.com/open-policy-agent/opa v1.13.2
github.com/prometheus/client_golang v1.23.2
github.com/rabbitmq/amqp091-go v1.10.0
github.com/redis/go-redis/extra/redisotel/v9 v9.16.0
github.com/redis/go-redis/v9 v9.16.0
github.com/rs/zerolog v1.34.0
go.opentelemetry.io/contrib/instrumentation/runtime v0.64.0
go.opentelemetry.io/otel v1.40.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0
go.opentelemetry.io/otel/exporters/prometheus v0.46.0
go.opentelemetry.io/otel/log v0.16.0
go.opentelemetry.io/otel/metric v1.40.0
go.opentelemetry.io/otel/sdk v1.40.0
go.opentelemetry.io/otel/sdk/log v0.16.0

137
go.sum
View File

@@ -1,3 +1,9 @@
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
@@ -6,6 +12,8 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytecodealliance/wasmtime-go/v39 v39.0.1 h1:RibaT47yiyCRxMOj/l2cvL8cWiWBSqDXHyqsa9sGcCE=
github.com/bytecodealliance/wasmtime-go/v39 v39.0.1/go.mod h1:miR4NYIEBXeDNamZIzpskhJ0z/p8al+lwMWylQ/ZJb4=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
@@ -17,13 +25,29 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/dgraph-io/badger/v4 v4.9.0 h1:tpqWb0NewSrCYqTvywbcXOhQdWcqephkVkbBmaaqHzc=
github.com/dgraph-io/badger/v4 v4.9.0/go.mod h1:5/MEx97uzdPUHR4KtkNt8asfI2T4JiEiQlV7kWUo8c0=
github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM=
github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0=
github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
@@ -39,15 +63,19 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q=
github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -79,10 +107,28 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jsonata-go/jsonata v0.0.0-20250709164031-599f35f32e5f h1:JnGon8QHtmjFPq0NcSu8OTEnQDDEgFME7gtj/NkjCUo=
github.com/jsonata-go/jsonata v0.0.0-20250709164031-599f35f32e5f/go.mod h1:rYUEOEiieWXHNCE/eDXV/o5s7jZ2VyUzQKbqVns9pik=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38=
github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo=
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY=
github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc/v3 v3.0.2 h1:7u4HUaD0NQbf2/n5+fyp+T10hNCsAnwKfqn4A4Baif0=
github.com/lestrrat-go/httprc/v3 v3.0.2/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0=
github.com/lestrrat-go/jwx/v3 v3.0.13 h1:AdHKiPIYeCSnOJtvdpipPg/0SuFh9rdkN+HF3O0VdSk=
github.com/lestrrat-go/jwx/v3 v3.0.13/go.mod h1:2m0PV1A9tM4b/jVLMx8rh6rBl7F6WGb3EG2hufN9OQU=
github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
@@ -93,8 +139,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -104,10 +150,14 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/open-policy-agent/opa v1.13.2 h1:c72l7DhxP4g8DEUBOdaU9QBKyA24dZxCcIuZNRZ0yP4=
github.com/open-policy-agent/opa v1.13.2/go.mod h1:M3Asy9yp1YTusUU5VQuENDe92GLmamIuceqjw+C8PHY=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -117,16 +167,18 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/redis/go-redis/extra/rediscmd/v9 v9.16.0 h1:zAFQyFxJ3QDwpPUY/CKn22LI5+B8m/lUyffzq2+8ENs=
github.com/redis/go-redis/extra/rediscmd/v9 v9.16.0/go.mod h1:ouOc8ujB2wdUG6o0RrqaPl2tI6cenExC0KkJQ+PHXmw=
github.com/redis/go-redis/extra/redisotel/v9 v9.16.0 h1:+a9h9qxFXdf3gX0FXnDcz7X44ZBFUPq58Gblq7aMU4s=
@@ -143,20 +195,45 @@ github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkB
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc=
github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM=
github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/vektah/gqlparser/v2 v2.5.31 h1:YhWGA1mfTjID7qJhd1+Vxhpk5HTgydrGU9IgkWBTJ7k=
github.com/vektah/gqlparser/v2 v2.5.31/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts=
github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0=
github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg=
github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok=
github.com/zenazn/pkcs7pad v0.0.0-20170308005700-253a5b1f0e03 h1:m1h+vudopHsI67FPT9MOncyndWhTcdUoBtI1R1uajGY=
github.com/zenazn/pkcs7pad v0.0.0-20170308005700-253a5b1f0e03/go.mod h1:8sheVFH84v3PCyFY/O02mIgSQY9I6wMYPWsq7mDnEZY=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/contrib/instrumentation/runtime v0.64.0 h1:/+/+UjlXjFcdDlXxKL1PouzX8Z2Vl0OxolRKeBEgYDw=
go.opentelemetry.io/contrib/instrumentation/runtime v0.64.0/go.mod h1:Ldm/PDuzY2DP7IypudopCR3OCOW42NJlN9+mNEroevo=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
@@ -169,6 +246,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNl
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU=
go.opentelemetry.io/otel/exporters/prometheus v0.46.0 h1:I8WIFXR351FoLJYuloU4EgXbtNX2URfU/85pUPheIEQ=
go.opentelemetry.io/otel/exporters/prometheus v0.46.0/go.mod h1:ztwVUHe5DTR/1v7PeuGRnU5Bbd4QKYwApWmuutKsJSs=
go.opentelemetry.io/otel/log v0.16.0 h1:DeuBPqCi6pQwtCK0pO4fvMB5eBq6sNxEnuTs88pjsN4=
@@ -179,6 +258,8 @@ go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ7
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/sdk/log v0.16.0 h1:e/b4bdlQwC5fnGtG3dlXUrNOnP7c8YLVSpSfEBIkTnI=
go.opentelemetry.io/otel/sdk/log v0.16.0/go.mod h1:JKfP3T6ycy7QEuv3Hj8oKDy7KItrEkus8XJE6EoSzw4=
go.opentelemetry.io/otel/sdk/log/logtest v0.16.0 h1:/XVkpZ41rVRTP4DfMgYv1nEtNmf65XPPyAdqV90TMy4=
go.opentelemetry.io/otel/sdk/log/logtest v0.16.0/go.mod h1:iOOPgQr5MY9oac/F5W86mXdeyWZGleIx3uXO98X2R6Y=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
@@ -189,47 +270,45 @@ go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k=
gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
@@ -237,3 +316,5 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

View File

@@ -23,6 +23,7 @@ plugins=(
"schemav2validator"
"signer"
"signvalidator"
"opapolicychecker"
)
for plugin in "${plugins[@]}"; do

View File

@@ -0,0 +1,17 @@
package definition
import (
"context"
"github.com/beckn-one/beckn-onix/pkg/model"
)
// PolicyChecker interface for policy checking on incoming messages.
type PolicyChecker interface {
CheckPolicy(ctx *model.StepContext) error
}
// PolicyCheckerProvider interface for creating policy checkers.
type PolicyCheckerProvider interface {
New(ctx context.Context, config map[string]string) (PolicyChecker, func(), error)
}

View File

@@ -0,0 +1,199 @@
# OPA Policy Checker Plugin
Validates incoming Beckn messages against network-defined business rules using [Open Policy Agent (OPA)](https://www.openpolicyagent.org/) and the Rego policy language. Non-compliant messages are rejected with a `BadRequest` error code.
## Features
- Evaluates business rules defined in Rego policies
- Supports multiple policy sources: remote URL, local file, directory, or OPA bundle (`.tar.gz`)
- Structured result format: `{"valid": bool, "violations": []string}`
- Fail-closed on empty/undefined query results — misconfigured policies are treated as violations
- Runtime config forwarding: adapter config values are accessible in Rego as `data.config.<key>`
- Action-based enforcement: apply policies only to specific beckn actions (e.g., `confirm`, `search`)
- Configurable fetch timeout for remote policy and bundle sources
- Warns at startup when policy enforcement is explicitly disabled
## Configuration
```yaml
checkPolicy:
id: opapolicychecker
config:
type: file
location: ./pkg/plugin/implementation/opapolicychecker/testdata/example.rego
query: "data.policy.result"
actions: "confirm,search"
steps:
- checkPolicy
- addRoute
```
### Configuration Parameters
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `type` | string | Yes | - | Policy source type: `url`, `file`, `dir`, or `bundle` |
| `location` | string | Yes | - | Path or URL to the policy source (`.tar.gz` for bundles) |
| `query` | string | Yes | - | Rego query path to evaluate (e.g., `data.policy.result`) |
| `actions` | string | No | *(all)* | Comma-separated beckn actions to enforce |
| `enabled` | string | No | `"true"` | Enable or disable the plugin |
| `debugLogging` | string | No | `"false"` | Enable verbose OPA evaluation logging |
| `fetchTimeoutSeconds` | string | No | `"30"` | Timeout in seconds for fetching remote `.rego` files or bundles |
| `refreshIntervalSeconds` | string | No | - | Reload policies every N seconds (0 or omit = disabled) |
| *any other key* | string | No | - | Forwarded to Rego as `data.config.<key>` |
## Policy Hot-Reload
When `refreshIntervalSeconds` is set, a background goroutine periodically re-fetches and recompiles the policy source without restarting the adapter:
- **Atomic swap**: the old evaluator stays fully active until the new one is compiled — no gap in enforcement
- **Non-fatal errors**: if the reload fails (e.g., file temporarily unreachable or parse error), the error is logged and the previous policy stays active
- **Goroutine lifecycle**: the reload loop stops when the adapter context is cancelled or when plugin `Close()` is invoked during shutdown
```yaml
config:
type: file
location: ./policies/compliance.rego
query: "data.policy.result"
refreshIntervalSeconds: "300" # reload every 5 minutes
```
## How It Works
### Initialization (Load Time)
1. **Load Policy Source**: Fetches `.rego` files from the configured `location` — URL, file, directory, or OPA bundle
2. **Compile Policies**: Compiles all Rego modules into a single optimized `PreparedEvalQuery`
3. **Set Query**: Prepares the OPA query from the configured `query` path (e.g., `data.policy.result`)
### Request Evaluation (Runtime)
1. **Check Action Match**: If `actions` is configured, skip evaluation for non-matching actions. The plugin assumes standard adapter routes look like `/{participant}/{direction}/{action}` such as `/bpp/caller/confirm`; non-standard paths fall back to `context.action` from the JSON body.
2. **Evaluate OPA Query**: Run the prepared query with the full beckn message as `input`
3. **Handle Result**:
- If the query returns no result (undefined) → **violation** (fail-closed)
- If result is `{"valid": bool, "violations": []string}` → use structured format
- If result is a `set` or `[]string` → each string is a violation
- If result is a `bool``false` = violation
- If result is a `string` → non-empty = violation
4. **Reject or Allow**: If violations are found, NACK the request with all violation messages
### Supported Query Output Formats
| Rego Output | Behavior |
|-------------|----------|
| `{"valid": bool, "violations": ["string"]}` | Structured result format (recommended) |
| `set()` / `[]string` | Each string is a violation message |
| `bool` (`true`/`false`) | `false` = denied, `true` = allowed |
| `string` | Non-empty = violation |
| Empty/undefined | **Violation** (fail-closed) — indicates misconfigured query path |
## Example Usage
### Local File
```yaml
checkPolicy:
id: opapolicychecker
config:
type: file
location: ./pkg/plugin/implementation/opapolicychecker/testdata/example.rego
query: "data.policy.result"
```
### Remote URL
```yaml
checkPolicy:
id: opapolicychecker
config:
type: url
location: https://policies.example.com/compliance.rego
query: "data.policy.result"
fetchTimeoutSeconds: "10"
```
### Local Directory (multiple `.rego` files)
```yaml
checkPolicy:
id: opapolicychecker
config:
type: dir
location: ./policies
query: "data.policy.result"
```
### OPA Bundle (`.tar.gz`)
```yaml
checkPolicy:
id: opapolicychecker
config:
type: bundle
location: https://nfo.example.org/policies/bundle.tar.gz
query: "data.retail.validation.result"
```
## Writing Policies
Policies are written in [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/). The plugin passes the full beckn message body as `input` and any adapter config values as `data.config`:
```rego
package policy
import rego.v1
# Default result: valid with no violations.
default result := {
"valid": true,
"violations": []
}
# Compute the result from collected violations.
result := {
"valid": count(violations) == 0,
"violations": violations
}
# Require provider on confirm
violations contains "confirm: missing provider" if {
input.context.action == "confirm"
not input.message.order.provider
}
# Configurable threshold from adapter config
violations contains "delivery lead time too short" if {
input.context.action == "confirm"
lead := input.message.order.fulfillments[_].start.time.duration
to_number(lead) < to_number(data.config.minDeliveryLeadHours)
}
```
See [`testdata/example.rego`](./testdata/example.rego) for a full working example.
## Relationship with Schema Validator
`opapolicychecker` and `schemav2validator` serve different purposes:
- **Schemav2Validator**: Validates message **structure** against OpenAPI/JSON Schema specs
- **OPA Policy Checker**: Evaluates **business rules** via OPA/Rego policies
Configure them side-by-side in your adapter steps as needed.
## Plugin ID vs Step Name
- **Plugin ID** (used in `id:`): `opapolicychecker` (lowercase, implementation-specific)
- **Step name** (used in `steps:` list and YAML key): `checkPolicy` (camelCase verb)
## Dependencies
- `github.com/open-policy-agent/opa` — OPA Go SDK for policy evaluation and bundle loading
## Known Limitations
- **No bundle signature verification**: When using `type: bundle`, bundle signature verification is skipped. This is planned for a future enhancement.
- **Network-level scoping**: Policies apply to all messages handled by the adapter instance. Per-network policy mapping (by `networkId`) is tracked for follow-up.
- **Non-standard route shapes**: URL-based action extraction assumes the standard Beckn adapter route shape `/{participant}/{direction}/{action}` and falls back to `context.action` for other path layouts.

View File

@@ -0,0 +1,326 @@
// Benchmarks for policy enforcer evaluation scaling.
// Measures how OPA evaluation time changes with rule count (1 to 500 rules),
// covering both realistic (mostly inactive) and worst-case (all active) scenarios.
// Also benchmarks compilation time (one-time startup cost).
//
// Run human-readable report: go test -run TestBenchmarkReport -v -count=1
// Run Go benchmarks: go test -bench=. -benchmem -count=1
package opapolicychecker
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// generateDummyRules creates a .rego policy file with N rules.
// Only one rule matches the input (action == "confirm"), the rest have impossible
// conditions (action == "foobar1", "foobar2", ...) to simulate realistic rule bloat
// where most rules don't fire.
func generateDummyRules(n int) string {
var sb strings.Builder
sb.WriteString("package policy\nimport rego.v1\n\n")
// One real rule that actually fires
sb.WriteString("violations contains \"real_violation\" if {\n")
sb.WriteString(" input.context.action == \"confirm\"\n")
sb.WriteString(" input.message.order.value > 10000\n")
sb.WriteString("}\n\n")
// N-1 dummy rules with impossible conditions (simulate rule bloat)
for i := 1; i < n; i++ {
sb.WriteString(fmt.Sprintf("violations contains \"dummy_violation_%d\" if {\n", i))
sb.WriteString(fmt.Sprintf(" input.context.action == \"foobar%d\"\n", i))
sb.WriteString(fmt.Sprintf(" input.message.order.value > %d\n", i*100))
sb.WriteString("}\n\n")
}
return sb.String()
}
// generateActiveRules creates N rules that ALL fire on the test input.
// This is the worst case: every rule matches.
func generateActiveRules(n int) string {
var sb strings.Builder
sb.WriteString("package policy\nimport rego.v1\n\n")
for i := 0; i < n; i++ {
sb.WriteString(fmt.Sprintf("violations contains \"active_violation_%d\" if {\n", i))
sb.WriteString(" input.context.action == \"confirm\"\n")
sb.WriteString("}\n\n")
}
return sb.String()
}
// sampleBecknInput is a realistic beckn confirm message for benchmarking.
var sampleBecknInput = []byte(`{
"context": {
"domain": "energy",
"action": "confirm",
"version": "1.1.0",
"bap_id": "buyer-bap.example.com",
"bap_uri": "https://buyer-bap.example.com",
"bpp_id": "seller-bpp.example.com",
"bpp_uri": "https://seller-bpp.example.com",
"transaction_id": "txn-12345",
"message_id": "msg-67890",
"timestamp": "2026-03-04T10:00:00Z"
},
"message": {
"order": {
"id": "order-001",
"provider": {"id": "seller-1"},
"items": [
{"id": "item-1", "quantity": {"selected": {"count": 100}}},
{"id": "item-2", "quantity": {"selected": {"count": 50}}}
],
"value": 15000,
"fulfillment": {
"type": "DELIVERY",
"start": {"time": {"timestamp": "2026-03-05T08:00:00Z"}},
"end": {"time": {"timestamp": "2026-03-05T18:00:00Z"}}
}
}
}
}`)
// --- Go Benchmarks (run with: go test -bench=. -benchmem) ---
// BenchmarkEvaluate_MostlyInactive benchmarks evaluation with N rules where
// only 1 rule fires. This simulates a realistic governance ruleset where
// most rules are for different actions/conditions.
func BenchmarkEvaluate_MostlyInactive(b *testing.B) {
sizes := []int{1, 10, 50, 100, 250, 500}
for _, n := range sizes {
b.Run(fmt.Sprintf("rules=%d", n), func(b *testing.B) {
dir := b.TempDir()
os.WriteFile(filepath.Join(dir, "policy.rego"), []byte(generateDummyRules(n)), 0644)
eval, err := NewEvaluator([]string{dir}, "data.policy.violations", nil, false, 0)
if err != nil {
b.Fatalf("NewEvaluator failed: %v", err)
}
ctx := context.Background()
violations, err := eval.Evaluate(ctx, sampleBecknInput)
if err != nil {
b.Fatalf("correctness check failed: %v", err)
}
if len(violations) != 1 || violations[0] != "real_violation" {
b.Fatalf("expected [real_violation], got %v", violations)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := eval.Evaluate(ctx, sampleBecknInput)
if err != nil {
b.Fatalf("Evaluate failed: %v", err)
}
}
})
}
}
// BenchmarkEvaluate_AllActive benchmarks the worst case where ALL N rules fire.
func BenchmarkEvaluate_AllActive(b *testing.B) {
sizes := []int{1, 10, 50, 100, 250, 500}
for _, n := range sizes {
b.Run(fmt.Sprintf("rules=%d", n), func(b *testing.B) {
dir := b.TempDir()
os.WriteFile(filepath.Join(dir, "policy.rego"), []byte(generateActiveRules(n)), 0644)
eval, err := NewEvaluator([]string{dir}, "data.policy.violations", nil, false, 0)
if err != nil {
b.Fatalf("NewEvaluator failed: %v", err)
}
ctx := context.Background()
violations, err := eval.Evaluate(ctx, sampleBecknInput)
if err != nil {
b.Fatalf("correctness check failed: %v", err)
}
if len(violations) != n {
b.Fatalf("expected %d violations, got %d", n, len(violations))
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := eval.Evaluate(ctx, sampleBecknInput)
if err != nil {
b.Fatalf("Evaluate failed: %v", err)
}
}
})
}
}
// BenchmarkCompilation measures how long it takes to compile policies of various sizes.
// This runs once at startup, so it's less critical but good to know.
func BenchmarkCompilation(b *testing.B) {
sizes := []int{10, 50, 100, 250, 500}
for _, n := range sizes {
b.Run(fmt.Sprintf("rules=%d", n), func(b *testing.B) {
dir := b.TempDir()
os.WriteFile(filepath.Join(dir, "policy.rego"), []byte(generateDummyRules(n)), 0644)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := NewEvaluator([]string{dir}, "data.policy.violations", nil, false, 0)
if err != nil {
b.Fatalf("NewEvaluator failed: %v", err)
}
}
})
}
}
// --- Human-Readable Report (run with: go test -run TestBenchmarkReport -v) ---
// TestBenchmarkReport generates a readable table showing how evaluation time
// scales with rule count. This is the report to share with the team.
func TestBenchmarkReport(t *testing.T) {
sizes := []int{1, 10, 50, 100, 250, 500}
iterations := 1000
fmt.Println()
fmt.Println("╔══════════════════════════════════════════════════════════════════════╗")
fmt.Println("║ Policy Enforcer — Performance Benchmark Report ║")
fmt.Println("╠══════════════════════════════════════════════════════════════════════╣")
fmt.Println()
// --- Compilation time ---
fmt.Println("┌─────────────────────────────────────────────────┐")
fmt.Println("│ Compilation Time (one-time startup cost) │")
fmt.Println("├──────────┬──────────────────────────────────────┤")
fmt.Println("│ Rules │ Compilation Time │")
fmt.Println("├──────────┼──────────────────────────────────────┤")
for _, n := range sizes {
dir := t.TempDir()
os.WriteFile(filepath.Join(dir, "policy.rego"), []byte(generateDummyRules(n)), 0644)
start := time.Now()
_, err := NewEvaluator([]string{dir}, "data.policy.violations", nil, false, 0)
elapsed := time.Since(start)
if err != nil {
t.Fatalf("NewEvaluator(%d rules) failed: %v", n, err)
}
fmt.Printf("│ %-8d │ %-36s │\n", n, elapsed.Round(time.Microsecond))
}
fmt.Println("└──────────┴──────────────────────────────────────┘")
fmt.Println()
// --- Evaluation time (mostly inactive rules) ---
fmt.Println("┌─────────────────────────────────────────────────────────────────┐")
fmt.Printf("│ Evaluation Time — Mostly Inactive Rules (%d iterations) │\n", iterations)
fmt.Println("│ (1 rule fires, rest have non-matching conditions) │")
fmt.Println("├──────────┬──────────────┬──────────────┬────────────────────────┤")
fmt.Println("│ Rules │ Avg/eval │ p99 │ Violations │")
fmt.Println("├──────────┼──────────────┼──────────────┼────────────────────────┤")
for _, n := range sizes {
dir := t.TempDir()
os.WriteFile(filepath.Join(dir, "policy.rego"), []byte(generateDummyRules(n)), 0644)
eval, err := NewEvaluator([]string{dir}, "data.policy.violations", nil, false, 0)
if err != nil {
t.Fatalf("NewEvaluator(%d rules) failed: %v", n, err)
}
ctx := context.Background()
durations := make([]time.Duration, iterations)
var lastViolations []string
for i := 0; i < iterations; i++ {
start := time.Now()
v, err := eval.Evaluate(ctx, sampleBecknInput)
durations[i] = time.Since(start)
if err != nil {
t.Fatalf("Evaluate failed: %v", err)
}
lastViolations = v
}
avg, p99 := calcStats(durations)
fmt.Printf("│ %-8d │ %-12s │ %-12s │ %-22d │\n", n, avg.Round(time.Microsecond), p99.Round(time.Microsecond), len(lastViolations))
}
fmt.Println("└──────────┴──────────────┴──────────────┴────────────────────────┘")
fmt.Println()
// --- Evaluation time (all rules active) ---
fmt.Println("┌─────────────────────────────────────────────────────────────────┐")
fmt.Printf("│ Evaluation Time — All Rules Active (%d iterations) │\n", iterations)
fmt.Println("│ (every rule fires — worst case scenario) │")
fmt.Println("├──────────┬──────────────┬──────────────┬────────────────────────┤")
fmt.Println("│ Rules │ Avg/eval │ p99 │ Violations │")
fmt.Println("├──────────┼──────────────┼──────────────┼────────────────────────┤")
for _, n := range sizes {
dir := t.TempDir()
os.WriteFile(filepath.Join(dir, "policy.rego"), []byte(generateActiveRules(n)), 0644)
eval, err := NewEvaluator([]string{dir}, "data.policy.violations", nil, false, 0)
if err != nil {
t.Fatalf("NewEvaluator(%d rules) failed: %v", n, err)
}
ctx := context.Background()
durations := make([]time.Duration, iterations)
var lastViolations []string
for i := 0; i < iterations; i++ {
start := time.Now()
v, err := eval.Evaluate(ctx, sampleBecknInput)
durations[i] = time.Since(start)
if err != nil {
t.Fatalf("Evaluate failed: %v", err)
}
lastViolations = v
}
avg, p99 := calcStats(durations)
fmt.Printf("│ %-8d │ %-12s │ %-12s │ %-22d │\n", n, avg.Round(time.Microsecond), p99.Round(time.Microsecond), len(lastViolations))
}
fmt.Println("└──────────┴──────────────┴──────────────┴────────────────────────┘")
fmt.Println()
fmt.Println("╚══════════════════════════════════════════════════════════════════════╝")
}
// calcStats returns average and p99 durations from a sorted slice.
func calcStats(durations []time.Duration) (avg, p99 time.Duration) {
n := len(durations)
if n == 0 {
return 0, 0
}
var total time.Duration
for _, d := range durations {
total += d
}
avg = total / time.Duration(n)
// Sort for p99
sorted := make([]time.Duration, n)
copy(sorted, durations)
sortDurations(sorted)
p99 = sorted[int(float64(n)*0.99)]
return avg, p99
}
// sortDurations sorts a slice of durations in ascending order (insertion sort, fine for 1000 items).
func sortDurations(d []time.Duration) {
for i := 1; i < len(d); i++ {
key := d[i]
j := i - 1
for j >= 0 && d[j] > key {
d[j+1] = d[j]
j--
}
d[j+1] = key
}
}

View File

@@ -0,0 +1,26 @@
// Package main provides the plugin entry point for the OPA Policy Checker plugin.
// This file is compiled as a Go plugin (.so) and loaded by beckn-onix at runtime.
package main
import (
"context"
"github.com/beckn-one/beckn-onix/pkg/plugin/definition"
"github.com/beckn-one/beckn-onix/pkg/plugin/implementation/opapolicychecker"
)
// provider implements the PolicyCheckerProvider interface for plugin loading.
type provider struct{}
// New creates a new PolicyChecker instance.
func (p provider) New(ctx context.Context, cfg map[string]string) (definition.PolicyChecker, func(), error) {
checker, err := opapolicychecker.New(ctx, cfg)
if err != nil {
return nil, nil, err
}
return checker, checker.Close, nil
}
// Provider is the exported symbol that beckn-onix plugin manager looks up.
var Provider = provider{}

View File

@@ -0,0 +1,41 @@
package main
import (
"context"
"path/filepath"
"testing"
)
func TestProviderNewSuccess(t *testing.T) {
provider := provider{}
config := map[string]string{
"type": "file",
"location": filepath.Join("..", "testdata", "example.rego"),
"query": "data.policy.result",
}
checker, closer, err := provider.New(context.Background(), config)
if err != nil {
t.Fatalf("New() unexpected error: %v", err)
}
if checker == nil {
t.Fatal("New() returned nil checker")
}
if closer == nil {
t.Fatal("New() returned nil closer")
}
closer()
}
func TestProviderNewFailure(t *testing.T) {
provider := provider{}
_, _, err := provider.New(context.Background(), map[string]string{
"type": "file",
"query": "data.policy.result",
})
if err == nil {
t.Fatal("expected error when required config is missing")
}
}

View File

@@ -0,0 +1,321 @@
package opapolicychecker
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"sync"
"time"
"github.com/beckn-one/beckn-onix/pkg/log"
"github.com/beckn-one/beckn-onix/pkg/model"
)
// Config holds the configuration for the OPA Policy Checker plugin.
type Config struct {
Type string
Location string
PolicyPaths []string
Query string
Actions []string
Enabled bool
DebugLogging bool
FetchTimeout time.Duration
IsBundle bool
RefreshInterval time.Duration // 0 = disabled
RuntimeConfig map[string]string
}
var knownKeys = map[string]bool{
"type": true,
"location": true,
"query": true,
"actions": true,
"enabled": true,
"debugLogging": true,
"fetchTimeoutSeconds": true,
"refreshIntervalSeconds": true,
}
func DefaultConfig() *Config {
return &Config{
Enabled: true,
FetchTimeout: defaultPolicyFetchTimeout,
RuntimeConfig: make(map[string]string),
}
}
// ParseConfig parses the plugin configuration map into a Config struct.
// Uses type + location pattern (matches schemav2validator).
func ParseConfig(cfg map[string]string) (*Config, error) {
config := DefaultConfig()
typ, hasType := cfg["type"]
if !hasType || typ == "" {
return nil, fmt.Errorf("'type' is required (url, file, dir, or bundle)")
}
config.Type = typ
location, hasLoc := cfg["location"]
if !hasLoc || location == "" {
return nil, fmt.Errorf("'location' is required")
}
config.Location = location
switch typ {
case "url":
for _, u := range strings.Split(location, ",") {
u = strings.TrimSpace(u)
if u != "" {
config.PolicyPaths = append(config.PolicyPaths, u)
}
}
case "file":
config.PolicyPaths = append(config.PolicyPaths, location)
case "dir":
config.PolicyPaths = append(config.PolicyPaths, location)
case "bundle":
config.IsBundle = true
config.PolicyPaths = append(config.PolicyPaths, location)
default:
return nil, fmt.Errorf("unsupported type %q (expected: url, file, dir, or bundle)", typ)
}
query, hasQuery := cfg["query"]
if !hasQuery || query == "" {
return nil, fmt.Errorf("'query' is required (e.g., data.policy.violations)")
}
config.Query = query
if actions, ok := cfg["actions"]; ok && actions != "" {
actionList := strings.Split(actions, ",")
config.Actions = make([]string, 0, len(actionList))
for _, action := range actionList {
action = strings.TrimSpace(action)
if action != "" {
config.Actions = append(config.Actions, action)
}
}
}
if enabled, ok := cfg["enabled"]; ok {
config.Enabled = enabled == "true" || enabled == "1"
}
if debug, ok := cfg["debugLogging"]; ok {
config.DebugLogging = debug == "true" || debug == "1"
}
if fts, ok := cfg["fetchTimeoutSeconds"]; ok && fts != "" {
secs, err := strconv.Atoi(fts)
if err != nil || secs <= 0 {
return nil, fmt.Errorf("'fetchTimeoutSeconds' must be a positive integer, got %q", fts)
}
config.FetchTimeout = time.Duration(secs) * time.Second
}
if ris, ok := cfg["refreshIntervalSeconds"]; ok && ris != "" {
secs, err := strconv.Atoi(ris)
if err != nil || secs < 0 {
return nil, fmt.Errorf("'refreshIntervalSeconds' must be a non-negative integer, got %q", ris)
}
config.RefreshInterval = time.Duration(secs) * time.Second
}
for k, v := range cfg {
if !knownKeys[k] {
config.RuntimeConfig[k] = v
}
}
return config, nil
}
func (c *Config) IsActionEnabled(action string) bool {
if len(c.Actions) == 0 {
return true
}
for _, a := range c.Actions {
if a == action {
return true
}
}
return false
}
// PolicyEnforcer evaluates beckn messages against OPA policies and NACKs non-compliant messages.
type PolicyEnforcer struct {
config *Config
evaluator *Evaluator
evaluatorMu sync.RWMutex
closeOnce sync.Once
done chan struct{}
}
// getEvaluator safely returns the current evaluator under a read lock.
func (e *PolicyEnforcer) getEvaluator() *Evaluator {
e.evaluatorMu.RLock()
ev := e.evaluator
e.evaluatorMu.RUnlock()
return ev
}
// setEvaluator safely swaps the evaluator under a write lock.
func (e *PolicyEnforcer) setEvaluator(ev *Evaluator) {
e.evaluatorMu.Lock()
e.evaluator = ev
e.evaluatorMu.Unlock()
}
func New(ctx context.Context, cfg map[string]string) (*PolicyEnforcer, error) {
config, err := ParseConfig(cfg)
if err != nil {
return nil, fmt.Errorf("opapolicychecker: config error: %w", err)
}
enforcer := &PolicyEnforcer{
config: config,
done: make(chan struct{}),
}
if !config.Enabled {
log.Warnf(ctx, "OPAPolicyChecker is disabled via config; policy enforcement will be skipped")
return enforcer, nil
}
evaluator, err := NewEvaluator(config.PolicyPaths, config.Query, config.RuntimeConfig, config.IsBundle, config.FetchTimeout)
if err != nil {
return nil, fmt.Errorf("opapolicychecker: failed to initialize OPA evaluator: %w", err)
}
enforcer.evaluator = evaluator
log.Infof(ctx, "OPAPolicyChecker initialized (actions=%v, query=%s, policies=%v, isBundle=%v, debugLogging=%v, fetchTimeout=%s, refreshInterval=%s)",
config.Actions, config.Query, evaluator.ModuleNames(), config.IsBundle, config.DebugLogging, config.FetchTimeout, config.RefreshInterval)
if config.RefreshInterval > 0 {
go enforcer.refreshLoop(ctx)
}
return enforcer, nil
}
// refreshLoop periodically reloads and recompiles OPA policies.
// Follows the schemav2validator pattern: driven by context cancellation.
func (e *PolicyEnforcer) refreshLoop(ctx context.Context) {
ticker := time.NewTicker(e.config.RefreshInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
log.Infof(ctx, "OPAPolicyChecker: refresh loop stopped")
return
case <-e.done:
log.Infof(ctx, "OPAPolicyChecker: refresh loop stopped by Close()")
return
case <-ticker.C:
e.reloadPolicies(ctx)
}
}
}
// reloadPolicies reloads and recompiles all policies, atomically swapping the evaluator.
// Reload failures are non-fatal; the old evaluator stays active.
func (e *PolicyEnforcer) reloadPolicies(ctx context.Context) {
start := time.Now()
newEvaluator, err := NewEvaluator(
e.config.PolicyPaths,
e.config.Query,
e.config.RuntimeConfig,
e.config.IsBundle,
e.config.FetchTimeout,
)
if err != nil {
log.Errorf(ctx, err, "OPAPolicyChecker: policy reload failed (keeping previous policies): %v", err)
return
}
e.setEvaluator(newEvaluator)
log.Infof(ctx, "OPAPolicyChecker: policies reloaded in %s (modules=%v)", time.Since(start), newEvaluator.ModuleNames())
}
// CheckPolicy evaluates the message body against loaded OPA policies.
// Returns a BadReqErr (causing NACK) if violations are found.
// Returns an error on evaluation failure (fail closed).
func (e *PolicyEnforcer) CheckPolicy(ctx *model.StepContext) error {
if !e.config.Enabled {
log.Debug(ctx, "OPAPolicyChecker: plugin disabled, skipping")
return nil
}
action := extractAction(ctx.Request.URL.Path, ctx.Body)
if !e.config.IsActionEnabled(action) {
if e.config.DebugLogging {
log.Debugf(ctx, "OPAPolicyChecker: action %q not in configured actions %v, skipping", action, e.config.Actions)
}
return nil
}
ev := e.getEvaluator()
if ev == nil {
return model.NewBadReqErr(fmt.Errorf("policy evaluator is not initialized"))
}
if e.config.DebugLogging {
log.Debugf(ctx, "OPAPolicyChecker: evaluating policies for action %q (modules=%v)", action, ev.ModuleNames())
}
violations, err := ev.Evaluate(ctx, ctx.Body)
if err != nil {
log.Errorf(ctx, err, "OPAPolicyChecker: policy evaluation failed: %v", err)
return model.NewBadReqErr(fmt.Errorf("policy evaluation error: %w", err))
}
if len(violations) == 0 {
if e.config.DebugLogging {
log.Debugf(ctx, "OPAPolicyChecker: message compliant for action %q", action)
}
return nil
}
msg := fmt.Sprintf("policy violation(s): %s", strings.Join(violations, "; "))
log.Warnf(ctx, "OPAPolicyChecker: %s", msg)
return model.NewBadReqErr(fmt.Errorf("%s", msg))
}
func (e *PolicyEnforcer) Close() {
e.closeOnce.Do(func() {
close(e.done)
})
}
func extractAction(urlPath string, body []byte) string {
// /bpp/caller/confirm/extra as action "extra".
parts := strings.FieldsFunc(strings.Trim(urlPath, "/"), func(r rune) bool { return r == '/' })
if len(parts) == 3 && isBecknDirection(parts[1]) && parts[2] != "" {
return parts[2]
}
var payload struct {
Context struct {
Action string `json:"action"`
} `json:"context"`
}
if err := json.Unmarshal(body, &payload); err == nil && payload.Context.Action != "" {
return payload.Context.Action
}
return ""
}
func isBecknDirection(part string) bool {
switch part {
case "caller", "receiver", "reciever":
return true
default:
return false
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,393 @@
package opapolicychecker
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/bundle"
"github.com/open-policy-agent/opa/v1/rego"
"github.com/open-policy-agent/opa/v1/storage/inmem"
)
// Evaluator wraps the OPA engine: loads and compiles .rego files at startup,
// then evaluates messages against the compiled policy set.
type Evaluator struct {
preparedQuery rego.PreparedEvalQuery
query string
runtimeConfig map[string]string
moduleNames []string // names of loaded .rego modules
failOnUndefined bool // if true, empty/undefined results are treated as violations
}
// ModuleNames returns the names of the loaded .rego policy modules.
func (e *Evaluator) ModuleNames() []string {
return e.moduleNames
}
// defaultPolicyFetchTimeout bounds remote policy and bundle fetches during startup
// and refresh. This can be overridden via config.fetchTimeoutSeconds.
const defaultPolicyFetchTimeout = 30 * time.Second
// maxPolicySize is the maximum size of a single .rego file fetched from a URL (1 MB).
const maxPolicySize = 1 << 20
// maxBundleSize is the maximum size of a bundle archive (10 MB).
const maxBundleSize = 10 << 20
// NewEvaluator creates an Evaluator by loading .rego files from local paths
// and/or URLs, then compiling them. runtimeConfig is passed to Rego as data.config.
// When isBundle is true, the first policyPath is treated as a URL to an OPA bundle (.tar.gz).
func NewEvaluator(policyPaths []string, query string, runtimeConfig map[string]string, isBundle bool, fetchTimeout time.Duration) (*Evaluator, error) {
if fetchTimeout <= 0 {
fetchTimeout = defaultPolicyFetchTimeout
}
if isBundle {
return newBundleEvaluator(policyPaths, query, runtimeConfig, fetchTimeout)
}
return newRegoEvaluator(policyPaths, query, runtimeConfig, fetchTimeout)
}
// newRegoEvaluator loads raw .rego files from local paths and/or URLs.
func newRegoEvaluator(policyPaths []string, query string, runtimeConfig map[string]string, fetchTimeout time.Duration) (*Evaluator, error) {
modules := make(map[string]string)
// Load from policyPaths (resolved locations based on config Type)
for _, source := range policyPaths {
if isURL(source) {
name, content, err := fetchPolicy(source, fetchTimeout)
if err != nil {
return nil, fmt.Errorf("failed to fetch policy from %s: %w", source, err)
}
modules[name] = content
} else if info, err := os.Stat(source); err == nil && info.IsDir() {
// Directory — load all .rego files inside
entries, err := os.ReadDir(source)
if err != nil {
return nil, fmt.Errorf("failed to read policy directory %s: %w", source, err)
}
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".rego") || strings.HasSuffix(entry.Name(), "_test.rego") {
continue
}
fpath := filepath.Join(source, entry.Name())
data, err := os.ReadFile(fpath)
if err != nil {
return nil, fmt.Errorf("failed to read policy file %s: %w", fpath, err)
}
modules[entry.Name()] = string(data)
}
} else {
// Local file path
data, err := os.ReadFile(source)
if err != nil {
return nil, fmt.Errorf("failed to read policy file %s: %w", source, err)
}
modules[filepath.Base(source)] = string(data)
}
}
if len(modules) == 0 {
return nil, fmt.Errorf("no .rego policy files found from any configured source")
}
return compileAndPrepare(modules, nil, query, runtimeConfig, true)
}
// newBundleEvaluator loads an OPA bundle (.tar.gz) from a URL and compiles it.
func newBundleEvaluator(policyPaths []string, query string, runtimeConfig map[string]string, fetchTimeout time.Duration) (*Evaluator, error) {
if len(policyPaths) == 0 {
return nil, fmt.Errorf("bundle source URL is required")
}
bundleURL := policyPaths[0]
modules, bundleData, err := loadBundle(bundleURL, fetchTimeout)
if err != nil {
return nil, fmt.Errorf("failed to load bundle from %s: %w", bundleURL, err)
}
if len(modules) == 0 {
return nil, fmt.Errorf("no .rego policy modules found in bundle from %s", bundleURL)
}
return compileAndPrepare(modules, bundleData, query, runtimeConfig, true)
}
// loadBundle downloads a .tar.gz OPA bundle from a URL, parses it using OPA's
// bundle reader, and returns the modules and data from the bundle.
func loadBundle(bundleURL string, fetchTimeout time.Duration) (map[string]string, map[string]interface{}, error) {
data, err := fetchBundleArchive(bundleURL, fetchTimeout)
if err != nil {
return nil, nil, err
}
return parseBundleArchive(data)
}
// fetchBundleArchive downloads a bundle .tar.gz from a URL.
func fetchBundleArchive(rawURL string, fetchTimeout time.Duration) ([]byte, error) {
parsed, err := url.Parse(rawURL)
if err != nil {
return nil, fmt.Errorf("invalid URL: %w", err)
}
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return nil, fmt.Errorf("unsupported URL scheme %q (only http and https are supported)", parsed.Scheme)
}
client := &http.Client{Timeout: fetchTimeout}
resp, err := client.Get(rawURL)
if err != nil {
return nil, fmt.Errorf("HTTP request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP %d from %s", resp.StatusCode, rawURL)
}
limited := io.LimitReader(resp.Body, int64(maxBundleSize)+1)
body, err := io.ReadAll(limited)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
if len(body) > maxBundleSize {
return nil, fmt.Errorf("bundle exceeds maximum size of %d bytes", maxBundleSize)
}
return body, nil
}
// parseBundleArchive parses a .tar.gz OPA bundle archive and extracts
// rego modules and data. Signature verification is skipped.
func parseBundleArchive(data []byte) (map[string]string, map[string]interface{}, error) {
loader := bundle.NewTarballLoaderWithBaseURL(bytes.NewReader(data), "")
reader := bundle.NewCustomReader(loader).
WithSkipBundleVerification(true).
WithRegoVersion(ast.RegoV1)
b, err := reader.Read()
if err != nil {
return nil, nil, fmt.Errorf("failed to read bundle: %w", err)
}
modules := make(map[string]string, len(b.Modules))
for _, m := range b.Modules {
modules[m.Path] = string(m.Raw)
}
return modules, b.Data, nil
}
// compileAndPrepare compiles rego modules and prepares the OPA query for evaluation.
func compileAndPrepare(modules map[string]string, bundleData map[string]interface{}, query string, runtimeConfig map[string]string, failOnUndefined bool) (*Evaluator, error) {
// Compile modules to catch syntax errors early
compiler, err := ast.CompileModulesWithOpt(modules, ast.CompileOpts{ParserOptions: ast.ParserOptions{RegoVersion: ast.RegoV1}})
if err != nil {
return nil, fmt.Errorf("failed to compile rego modules: %w", err)
}
// Build store data: merge bundle data with runtime config
store := make(map[string]interface{})
for k, v := range bundleData {
store[k] = v
}
store["config"] = toInterfaceMap(runtimeConfig)
pq, err := rego.New(
rego.Query(query),
rego.Compiler(compiler),
rego.Store(inmem.NewFromObject(store)),
).PrepareForEval(context.Background())
if err != nil {
return nil, fmt.Errorf("failed to prepare rego query %q: %w", query, err)
}
names := make([]string, 0, len(modules))
for name := range modules {
names = append(names, name)
}
return &Evaluator{
preparedQuery: pq,
query: query,
runtimeConfig: runtimeConfig,
moduleNames: names,
failOnUndefined: failOnUndefined,
}, nil
}
// isURL checks if a source string looks like a remote URL.
func isURL(source string) bool {
return strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://")
}
// fetchPolicy downloads a .rego file from a URL and returns (filename, content, error).
func fetchPolicy(rawURL string, fetchTimeout time.Duration) (string, string, error) {
parsed, err := url.Parse(rawURL)
if err != nil {
return "", "", fmt.Errorf("invalid URL: %w", err)
}
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return "", "", fmt.Errorf("unsupported URL scheme %q (only http and https are supported)", parsed.Scheme)
}
client := &http.Client{Timeout: fetchTimeout}
resp, err := client.Get(rawURL)
if err != nil {
return "", "", fmt.Errorf("HTTP request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", "", fmt.Errorf("HTTP %d from %s", resp.StatusCode, rawURL)
}
// Read with size limit
limited := io.LimitReader(resp.Body, maxPolicySize+1)
body, err := io.ReadAll(limited)
if err != nil {
return "", "", fmt.Errorf("failed to read response body: %w", err)
}
if len(body) > maxPolicySize {
return "", "", fmt.Errorf("policy file exceeds maximum size of %d bytes", maxPolicySize)
}
// Derive filename from URL path
name := path.Base(parsed.Path)
if name == "" || name == "." || name == "/" {
name = "policy.rego"
}
if !strings.HasSuffix(name, ".rego") {
name += ".rego"
}
return name, string(body), nil
}
// Evaluate runs the compiled policy against a JSON message body.
// Returns a list of violation strings (empty = compliant).
func (e *Evaluator) Evaluate(ctx context.Context, body []byte) ([]string, error) {
var input interface{}
if err := json.Unmarshal(body, &input); err != nil {
return nil, fmt.Errorf("failed to parse message body as JSON: %w", err)
}
rs, err := e.preparedQuery.Eval(ctx, rego.EvalInput(input))
if err != nil {
return nil, fmt.Errorf("rego evaluation failed: %w", err)
}
// Fail-closed for bundles: if the query returned no result, the policy_query_path
// is likely misconfigured or the rule doesn't exist in the bundle.
if e.failOnUndefined && len(rs) == 0 {
return []string{fmt.Sprintf("policy query %q returned no result (undefined)", e.query)}, nil
}
return extractViolations(rs)
}
// extractViolations pulls violations from the OPA result set.
// Supported query output formats:
// - map with {"valid": bool, "violations": []string}: structured policy_query_path result
// - []string / set of strings: each string is a violation message
// - bool: false = denied ("policy denied the request"), true = allowed
// - string: non-empty = violation message
// - empty/undefined: allowed (no violations)
func extractViolations(rs rego.ResultSet) ([]string, error) {
if len(rs) == 0 {
return nil, nil
}
var violations []string
for _, result := range rs {
for _, expr := range result.Expressions {
switch v := expr.Value.(type) {
case bool:
// allow/deny pattern: false = denied
if !v {
violations = append(violations, "policy denied the request")
}
case string:
// single violation string
if v != "" {
violations = append(violations, v)
}
case []interface{}:
// Result is a list (from set)
for _, item := range v {
if s, ok := item.(string); ok {
violations = append(violations, s)
}
}
case map[string]interface{}:
if vs := extractStructuredViolations(v); vs != nil {
violations = append(violations, vs...)
}
}
}
}
return violations, nil
}
// extractStructuredViolations handles the policy_query_path result format:
// {"valid": bool, "violations": []string}
// Returns the violation strings if the map matches this format, or nil if it doesn't.
func extractStructuredViolations(m map[string]interface{}) []string {
validRaw, hasValid := m["valid"]
violationsRaw, hasViolations := m["violations"]
if !hasValid || !hasViolations {
return nil
}
valid, ok := validRaw.(bool)
if !ok {
return nil
}
violationsList, ok := violationsRaw.([]interface{})
if !ok {
return nil
}
// If valid is true and violations is empty, no violations
if valid && len(violationsList) == 0 {
return []string{}
}
var violations []string
for _, item := range violationsList {
if s, ok := item.(string); ok {
violations = append(violations, s)
}
}
// If valid is false but violations is empty, report a generic violation
if !valid && len(violations) == 0 {
violations = append(violations, "policy denied the request")
}
return violations
}
// toInterfaceMap converts map[string]string to map[string]interface{} for OPA store.
func toInterfaceMap(m map[string]string) map[string]interface{} {
result := make(map[string]interface{}, len(m))
for k, v := range m {
result[k] = v
}
return result
}

View File

@@ -0,0 +1,52 @@
package policy
import rego.v1
# Example policy: validation rules for beckn messages.
# This demonstrates the structured result format used with policy_query_path.
#
# Available inputs:
# - input: the full JSON message body (beckn request)
# - data.config: runtime config from the adapter YAML (e.g., minDeliveryLeadHours)
# Default result: valid with no violations.
default result := {
"valid": true,
"violations": []
}
# Compute the result from collected violations.
result := {
"valid": count(violations) == 0,
"violations": violations
}
# Require provider details on confirm
violations contains "confirm: missing provider in order" if {
input.context.action == "confirm"
not input.message.order.provider
}
# Require at least one fulfillment on confirm
violations contains "confirm: order has no fulfillments" if {
input.context.action == "confirm"
not input.message.order.fulfillments
}
# Require billing details on confirm
violations contains "confirm: missing billing info" if {
input.context.action == "confirm"
not input.message.order.billing
}
# Require payment details on confirm
violations contains "confirm: missing payment info" if {
input.context.action == "confirm"
not input.message.order.payment
}
# Require search intent
violations contains "search: missing intent" if {
input.context.action == "search"
not input.message.intent
}

View File

@@ -257,6 +257,23 @@ func (m *Manager) Step(ctx context.Context, cfg *Config) (definition.Step, error
return step, error
}
// PolicyChecker returns a PolicyChecker instance based on the provided configuration.
// It registers a cleanup function for resource management.
func (m *Manager) PolicyChecker(ctx context.Context, cfg *Config) (definition.PolicyChecker, error) {
pp, err := provider[definition.PolicyCheckerProvider](m.plugins, cfg.ID)
if err != nil {
return nil, fmt.Errorf("failed to load provider for %s: %w", cfg.ID, err)
}
checker, closer, err := pp.New(ctx, cfg.Config)
if err != nil {
return nil, err
}
if closer != nil {
m.closers = append(m.closers, closer)
}
return checker, nil
}
// Cache returns a Cache instance based on the provided configuration.
// It registers a cleanup function for resource management.
func (m *Manager) Cache(ctx context.Context, cfg *Config) (definition.Cache, error) {