diff --git a/cmd/adapter/main_test.go b/cmd/adapter/main_test.go index 4961a3a..6f7864d 100644 --- a/cmd/adapter/main_test.go +++ b/cmd/adapter/main_test.go @@ -83,6 +83,11 @@ func (m *MockPluginManager) SchemaValidator(ctx context.Context, cfg *plugin.Con return nil, nil } +// PolicyEnforcer returns a mock implementation of the PolicyEnforcer interface. +func (m *MockPluginManager) PolicyEnforcer(ctx context.Context, cfg *plugin.Config) (definition.PolicyEnforcer, error) { + 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 +191,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 }() diff --git a/config/local-beckn-one-bap.yaml b/config/local-beckn-one-bap.yaml index 28ca271..c179ddc 100644 --- a/config/local-beckn-one-bap.yaml +++ b/config/local-beckn-one-bap.yaml @@ -86,6 +86,12 @@ modules: id: router config: routingConfig: ./config/local-beckn-one-routing-BAPReceiver.yaml + policyEnforcer: + id: policyenforcer + config: + policySources: "./policies/compliance.rego" + actions: "confirm" + query: "data.policy.violations" middleware: - id: reqpreprocessor config: @@ -93,6 +99,7 @@ modules: role: bap steps: - validateSign + - enforcePolicy - addRoute - validateSchema diff --git a/config/local-beckn-one-bpp.yaml b/config/local-beckn-one-bpp.yaml index 4d98073..1564a85 100644 --- a/config/local-beckn-one-bpp.yaml +++ b/config/local-beckn-one-bpp.yaml @@ -84,8 +84,15 @@ modules: id: router config: routingConfig: ./config/local-beckn-one-routing-BPPReceiver.yaml + policyEnforcer: + id: policyenforcer + config: + policySources: "./policies/compliance.rego" + actions: "confirm" + query: "data.policy.violations" steps: - validateSign + - enforcePolicy - addRoute - validateSchema diff --git a/config/local-simple.yaml b/config/local-simple.yaml index 327e344..7360f90 100644 --- a/config/local-simple.yaml +++ b/config/local-simple.yaml @@ -66,6 +66,12 @@ modules: id: router config: routingConfig: ./config/local-simple-routing.yaml + policyEnforcer: + id: policyenforcer + config: + policySources: "./policies/compliance.rego" + actions: "confirm" + query: "data.policy.violations" middleware: - id: reqpreprocessor config: @@ -73,6 +79,7 @@ modules: role: bap steps: - validateSign + - enforcePolicy - addRoute - name: bapTxnCaller @@ -162,8 +169,15 @@ modules: id: router config: routingConfig: ./config/local-simple-routing-BPPReceiver.yaml + policyEnforcer: + id: policyenforcer + config: + policySources: "./policies/compliance.rego" + actions: "confirm" + query: "data.policy.violations" steps: - validateSign + - enforcePolicy - addRoute - name: bppTxnCaller diff --git a/core/module/handler/config.go b/core/module/handler/config.go index 96b2263..64f38fe 100644 --- a/core/module/handler/config.go +++ b/core/module/handler/config.go @@ -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) + PolicyEnforcer(ctx context.Context, cfg *plugin.Config) (definition.PolicyEnforcer, 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"` + PolicyEnforcer *plugin.Config `yaml:"policyEnforcer,omitempty"` SignValidator *plugin.Config `yaml:"signValidator,omitempty"` Publisher *plugin.Config `yaml:"publisher,omitempty"` Signer *plugin.Config `yaml:"signer,omitempty"` diff --git a/core/module/handler/stdHandler.go b/core/module/handler/stdHandler.go index 63444cc..097400e 100644 --- a/core/module/handler/stdHandler.go +++ b/core/module/handler/stdHandler.go @@ -35,6 +35,7 @@ type stdHandler struct { registry definition.RegistryLookup km definition.KeyManager schemaValidator definition.SchemaValidator + policyEnforcer definition.PolicyEnforcer 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.policyEnforcer, err = loadPlugin(ctx, "PolicyEnforcer", cfg.PolicyEnforcer, mgr.PolicyEnforcer); 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 "enforcePolicy": + s, err = newEnforcePolicyStep(h.policyEnforcer) default: if customStep, exists := steps[step]; exists { s = customStep diff --git a/core/module/handler/step.go b/core/module/handler/step.go index 04d6536..7817709 100644 --- a/core/module/handler/step.go +++ b/core/module/handler/step.go @@ -315,3 +315,11 @@ func extractSchemaVersion(body []byte) string { } return "unknown" } + +// newEnforcePolicyStep creates and returns the enforcePolicy step after validation. +func newEnforcePolicyStep(policyEnforcer definition.PolicyEnforcer) (definition.Step, error) { + if policyEnforcer == nil { + return nil, fmt.Errorf("invalid config: PolicyEnforcer plugin not configured") + } + return policyEnforcer, nil +} diff --git a/core/module/module_test.go b/core/module/module_test.go index f1a0caa..302a2de 100644 --- a/core/module/module_test.go +++ b/core/module/module_test.go @@ -79,6 +79,11 @@ func (m *mockPluginManager) SchemaValidator(ctx context.Context, cfg *plugin.Con return nil, nil } +// PolicyEnforcer returns a mock policy enforcer implementation. +func (m *mockPluginManager) PolicyEnforcer(ctx context.Context, cfg *plugin.Config) (definition.PolicyEnforcer, error) { + return nil, nil +} + // TestRegisterSuccess tests scenarios where the handler registration should succeed. func TestRegisterSuccess(t *testing.T) { mCfgs := []Config{ diff --git a/go.mod b/go.mod index 2030ae7..6fce510 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 0e2eb1a..fbbed95 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/install/build-plugins.sh b/install/build-plugins.sh index 389f235..0cdfcaa 100755 --- a/install/build-plugins.sh +++ b/install/build-plugins.sh @@ -23,6 +23,7 @@ plugins=( "schemav2validator" "signer" "signvalidator" + "policyenforcer" ) for plugin in "${plugins[@]}"; do diff --git a/install/docker-compose-adapter-beckn-one.yml b/install/docker-compose-adapter-beckn-one.yml index f5785a1..435d4e0 100644 --- a/install/docker-compose-adapter-beckn-one.yml +++ b/install/docker-compose-adapter-beckn-one.yml @@ -36,6 +36,7 @@ services: volumes: - ../config:/app/config - ../schemas:/app/schemas + - ../policies:/app/policies command: ["./server", "--config=/app/config/local-beckn-one-bap.yaml"] onix-bpp: @@ -57,6 +58,7 @@ services: volumes: - ../config:/app/config - ../schemas:/app/schemas + - ../policies:/app/policies command: ["./server", "--config=/app/config/local-beckn-one-bpp.yaml"] sandbox-bap: diff --git a/install/docker-compose-adapter.yml b/install/docker-compose-adapter.yml index b2839dd..90bcd1b 100644 --- a/install/docker-compose-adapter.yml +++ b/install/docker-compose-adapter.yml @@ -36,6 +36,7 @@ services: volumes: - ../config:/app/config - ../schemas:/app/schemas + - ../policies:/app/policies command: ["./server", "--config=/app/config/local-simple.yaml"] # Vault - Key Management Service diff --git a/pkg/plugin/definition/policyEnforcer.go b/pkg/plugin/definition/policyEnforcer.go new file mode 100644 index 0000000..659bda0 --- /dev/null +++ b/pkg/plugin/definition/policyEnforcer.go @@ -0,0 +1,17 @@ +package definition + +import ( + "context" + + "github.com/beckn-one/beckn-onix/pkg/model" +) + +// PolicyEnforcer interface for policy enforcement on incoming messages. +type PolicyEnforcer interface { + Run(ctx *model.StepContext) error +} + +// PolicyEnforcerProvider interface for creating policy enforcers. +type PolicyEnforcerProvider interface { + New(ctx context.Context, config map[string]string) (PolicyEnforcer, func(), error) +} diff --git a/pkg/plugin/implementation/policyenforcer/README.md b/pkg/plugin/implementation/policyenforcer/README.md new file mode 100644 index 0000000..686a36f --- /dev/null +++ b/pkg/plugin/implementation/policyenforcer/README.md @@ -0,0 +1,68 @@ +# Policy Enforcer Plugin + +OPA/Rego-based policy enforcement for beckn-onix adapters. Evaluates incoming beckn messages against configurable policies and NACKs non-compliant requests. + +## Overview + +The `policyenforcer` plugin is a **Step plugin** that: +- Loads `.rego` policy files from local directories, files, URLs, or local paths +- Evaluates incoming messages against compiled OPA policies +- Returns a `BadReqErr` (NACK) when policy violations are detected +- Fails closed on evaluation errors (treats as NACK) +- Is strictly **opt-in** — adapters that don't reference it are unaffected + +## Configuration + +All config keys are passed via `map[string]string` in the adapter YAML config. + +| Key | Required | Default | Description | +|-----|----------|---------|-------------| +| `policyDir` | One of `policyDir`, `policyFile`, or `policyUrls` required | — | Local directory containing `.rego` files | +| `policyFile` | | — | Single local `.rego` file path | +| `policyUrls` | | — | Comma-separated list of URLs or local paths to `.rego` files | +| `query` | No | `data.policy.violations` | Rego query returning violation strings | +| `actions` | No | `confirm` | Comma-separated beckn actions to enforce | +| `enabled` | No | `true` | Enable/disable the plugin | +| `debugLogging` | No | `false` | Enable verbose logging | +| *any other key* | No | — | Forwarded to Rego as `data.config.` | + +### Policy URLs + +`policyUrls` accepts both remote URLs and local file paths, separated by commas: + +```yaml +config: + policyUrls: "https://policies.example.com/compliance.rego,/etc/policies/local.rego,https://policies.example.com/safety.rego" +``` + +### Air-Gapped Deployments + +For environments without internet access, replace any URL with a local file path or volume mount: + +```yaml +config: + policyUrls: "/mounted-policies/compliance.rego,/mounted-policies/safety.rego" +``` + +## Example Config + +```yaml +plugins: + steps: + - id: policyenforcer + config: + policyUrls: "https://policies.example.com/compliance.rego,/local/policies/safety.rego" + actions: "confirm,init" + query: "data.policy.violations" + minDeliveryLeadHours: "4" + debugLogging: "true" +``` + +## Relationship with Schema Validator + +`policyenforcer` and `schemavalidator`/`schemav2validator` are **separate plugins** with different responsibilities: + +- **Schema Validator**: Validates message **structure** against OpenAPI/JSON Schema specs +- **Policy Enforcer**: Evaluates **business rules** via OPA/Rego policies + +They use different plugin interfaces (`SchemaValidator` vs `Step`), different engines, and different error types. Configure them side-by-side in your adapter config as needed. diff --git a/pkg/plugin/implementation/policyenforcer/cmd/plugin.go b/pkg/plugin/implementation/policyenforcer/cmd/plugin.go new file mode 100644 index 0000000..91d25ae --- /dev/null +++ b/pkg/plugin/implementation/policyenforcer/cmd/plugin.go @@ -0,0 +1,26 @@ +// Package main provides the plugin entry point for the Policy Enforcer 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/policyenforcer" +) + +// provider implements the PolicyEnforcerProvider interface for plugin loading. +type provider struct{} + +// New creates a new PolicyEnforcer instance. +func (p provider) New(ctx context.Context, cfg map[string]string) (definition.PolicyEnforcer, func(), error) { + enforcer, err := policyenforcer.New(cfg) + if err != nil { + return nil, nil, err + } + + return enforcer, enforcer.Close, nil +} + +// Provider is the exported symbol that beckn-onix plugin manager looks up. +var Provider = provider{} diff --git a/pkg/plugin/implementation/policyenforcer/config.go b/pkg/plugin/implementation/policyenforcer/config.go new file mode 100644 index 0000000..7232b3b --- /dev/null +++ b/pkg/plugin/implementation/policyenforcer/config.go @@ -0,0 +1,129 @@ +package policyenforcer + +import ( + "fmt" + "strings" +) + +// Config holds the configuration for the Policy Enforcer plugin. +type Config struct { + // PolicyDir is a local directory containing .rego policy files (all loaded). + // At least one policy source (PolicyDir, PolicyFile, or PolicyUrls) is required. + PolicyDir string + + // PolicyFile is a single local .rego file path. + PolicyFile string + + // PolicyUrls is a list of URLs (or local file paths) pointing to .rego files, + // fetched at startup or read from disk. + // Parsed from the comma-separated "policyUrls" config key. + PolicyUrls []string + + // Query is the Rego query that returns a set of violation strings. + // Default: "data.policy.violations" + Query string + + // Actions is the list of beckn actions to enforce policies on. + // Default: ["confirm"] + Actions []string + + // Enabled controls whether the plugin is active. + Enabled bool + + // DebugLogging enables verbose logging. + DebugLogging bool + + // RuntimeConfig holds arbitrary key-value pairs passed to Rego as data.config. + // Keys like minDeliveryLeadHours are forwarded here. + RuntimeConfig map[string]string +} + +// Known config keys that are handled directly (not forwarded to RuntimeConfig). +var knownKeys = map[string]bool{ + "policyDir": true, + "policyFile": true, + "policyUrls": true, + "query": true, + "actions": true, + "enabled": true, + "debugLogging": true, +} + +// DefaultConfig returns a Config with sensible defaults. +func DefaultConfig() *Config { + return &Config{ + Query: "data.policy.violations", + Actions: []string{"confirm"}, + Enabled: true, + DebugLogging: false, + RuntimeConfig: make(map[string]string), + } +} + +// ParseConfig parses the plugin configuration map into a Config struct. +func ParseConfig(cfg map[string]string) (*Config, error) { + config := DefaultConfig() + + if dir, ok := cfg["policyDir"]; ok && dir != "" { + config.PolicyDir = dir + } + if file, ok := cfg["policyFile"]; ok && file != "" { + config.PolicyFile = file + } + + // Legacy: comma-separated policyUrls + if urls, ok := cfg["policyUrls"]; ok && urls != "" { + for _, u := range strings.Split(urls, ",") { + u = strings.TrimSpace(u) + if u != "" { + config.PolicyUrls = append(config.PolicyUrls, u) + } + } + } + + if config.PolicyDir == "" && config.PolicyFile == "" && len(config.PolicyUrls) == 0 { + return nil, fmt.Errorf("at least one policy source is required (policyDir, policyFile, or policyUrls)") + } + + if query, ok := cfg["query"]; ok && query != "" { + 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" + } + + // Forward unknown keys to RuntimeConfig (e.g., minDeliveryLeadHours) + for k, v := range cfg { + if !knownKeys[k] { + config.RuntimeConfig[k] = v + } + } + + return config, nil +} + +// IsActionEnabled checks if the given action is in the configured actions list. +func (c *Config) IsActionEnabled(action string) bool { + for _, a := range c.Actions { + if a == action { + return true + } + } + return false +} diff --git a/pkg/plugin/implementation/policyenforcer/enforcer.go b/pkg/plugin/implementation/policyenforcer/enforcer.go new file mode 100644 index 0000000..194c72f --- /dev/null +++ b/pkg/plugin/implementation/policyenforcer/enforcer.go @@ -0,0 +1,106 @@ +package policyenforcer + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/beckn-one/beckn-onix/pkg/log" + "github.com/beckn-one/beckn-onix/pkg/model" +) + +// PolicyEnforcer is a Step plugin that evaluates beckn messages against +// OPA policies and NACKs non-compliant messages. +type PolicyEnforcer struct { + config *Config + evaluator *Evaluator +} + +// New creates a new PolicyEnforcer instance. +func New(cfg map[string]string) (*PolicyEnforcer, error) { + config, err := ParseConfig(cfg) + if err != nil { + return nil, fmt.Errorf("policyenforcer: config error: %w", err) + } + + evaluator, err := NewEvaluator(config.PolicyDir, config.PolicyFile, config.PolicyUrls, config.Query, config.RuntimeConfig) + if err != nil { + return nil, fmt.Errorf("policyenforcer: failed to initialize OPA evaluator: %w", err) + } + + log.Infof(context.TODO(), "PolicyEnforcer initialized (actions=%v, query=%s, policies=%v, debugLogging=%v)", + config.Actions, config.Query, evaluator.ModuleNames(), config.DebugLogging) + + return &PolicyEnforcer{ + config: config, + evaluator: evaluator, + }, nil +} + +// Run implements the Step interface. It 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) Run(ctx *model.StepContext) error { + if !e.config.Enabled { + log.Debug(ctx, "PolicyEnforcer: plugin disabled, skipping") + return nil + } + + // Extract action from the message + action := extractAction(ctx.Request.URL.Path, ctx.Body) + + if !e.config.IsActionEnabled(action) { + if e.config.DebugLogging { + log.Debugf(ctx, "PolicyEnforcer: action %q not in configured actions %v, skipping", action, e.config.Actions) + } + return nil + } + + if e.config.DebugLogging { + log.Debugf(ctx, "PolicyEnforcer: evaluating policies for action %q (modules=%v)", action, e.evaluator.ModuleNames()) + } + + violations, err := e.evaluator.Evaluate(ctx, ctx.Body) + if err != nil { + // Fail closed: evaluation error → NACK + log.Errorf(ctx, err, "PolicyEnforcer: 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, "PolicyEnforcer: message compliant for action %q", action) + } + return nil + } + + // Non-compliant: NACK with all violation messages + msg := fmt.Sprintf("policy violation(s): %s", strings.Join(violations, "; ")) + log.Warnf(ctx, "PolicyEnforcer: %s", msg) + return model.NewBadReqErr(fmt.Errorf("%s", msg)) +} + +// Close is a no-op for the policy enforcer (no resources to release). +func (e *PolicyEnforcer) Close() {} + +// extractAction gets the beckn action from the URL path or message body. +func extractAction(urlPath string, body []byte) string { + // Try URL path first: /bap/receiver/{action} or /bpp/caller/{action} + parts := strings.Split(strings.Trim(urlPath, "/"), "/") + if len(parts) >= 3 { + return parts[len(parts)-1] + } + + // Fallback: extract from body context.action + 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 "" +} diff --git a/pkg/plugin/implementation/policyenforcer/enforcer_test.go b/pkg/plugin/implementation/policyenforcer/enforcer_test.go new file mode 100644 index 0000000..8f6f811 --- /dev/null +++ b/pkg/plugin/implementation/policyenforcer/enforcer_test.go @@ -0,0 +1,518 @@ +package policyenforcer + +import ( + "context" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/beckn-one/beckn-onix/pkg/model" +) + +// Helper: create a StepContext with the given action path and JSON body. +func makeStepCtx(action string, body string) *model.StepContext { + req, _ := http.NewRequest("POST", "/bpp/caller/"+action, nil) + return &model.StepContext{ + Context: context.Background(), + Request: req, + Body: []byte(body), + } +} + +// Helper: write a .rego file to a temp dir and return the dir path. +func writePolicyDir(t *testing.T, filename, content string) string { + t.Helper() + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, filename), []byte(content), 0644) + if err != nil { + t.Fatalf("failed to write policy file: %v", err) + } + return dir +} + +// --- Config Tests --- + +func TestParseConfig_RequiresPolicySource(t *testing.T) { + _, err := ParseConfig(map[string]string{}) + if err == nil { + t.Fatal("expected error when no policyDir, policyFile, or policyUrls given") + } +} + +func TestParseConfig_Defaults(t *testing.T) { + cfg, err := ParseConfig(map[string]string{"policyDir": "/tmp"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.Query != "data.policy.violations" { + t.Errorf("expected default query, got %q", cfg.Query) + } + if len(cfg.Actions) != 1 || cfg.Actions[0] != "confirm" { + t.Errorf("expected default actions [confirm], got %v", cfg.Actions) + } + if !cfg.Enabled { + t.Error("expected enabled=true by default") + } +} + +func TestParseConfig_RuntimeConfigForwarding(t *testing.T) { + cfg, err := ParseConfig(map[string]string{ + "policyDir": "/tmp", + "minDeliveryLeadHours": "6", + "customParam": "value", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.RuntimeConfig["minDeliveryLeadHours"] != "6" { + t.Errorf("expected minDeliveryLeadHours=6, got %q", cfg.RuntimeConfig["minDeliveryLeadHours"]) + } + if cfg.RuntimeConfig["customParam"] != "value" { + t.Errorf("expected customParam=value, got %q", cfg.RuntimeConfig["customParam"]) + } +} + +func TestParseConfig_CustomActions(t *testing.T) { + cfg, err := ParseConfig(map[string]string{ + "policyDir": "/tmp", + "actions": "confirm, select, init", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(cfg.Actions) != 3 { + t.Fatalf("expected 3 actions, got %d: %v", len(cfg.Actions), cfg.Actions) + } + expected := []string{"confirm", "select", "init"} + for i, want := range expected { + if cfg.Actions[i] != want { + t.Errorf("action[%d] = %q, want %q", i, cfg.Actions[i], want) + } + } +} + +func TestParseConfig_PolicyUrls(t *testing.T) { + cfg, err := ParseConfig(map[string]string{ + "policyUrls": "https://example.com/a.rego, https://example.com/b.rego", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(cfg.PolicyUrls) != 2 { + t.Fatalf("expected 2 URLs, got %d: %v", len(cfg.PolicyUrls), cfg.PolicyUrls) + } + if cfg.PolicyUrls[0] != "https://example.com/a.rego" { + t.Errorf("url[0] = %q", cfg.PolicyUrls[0]) + } +} + +// Note: policySources support was removed; we intentionally only support +// comma-separated policyUrls and local paths via policyUrls entries. + +// --- Evaluator Tests (with inline policies) --- + +func TestEvaluator_NoViolations(t *testing.T) { + policy := ` +package policy +import rego.v1 +violations contains msg if { + input.value < 0 + msg := "value is negative" +} +` + dir := writePolicyDir(t, "test.rego", policy) + eval, err := NewEvaluator(dir, "", nil, "data.policy.violations", nil) + if err != nil { + t.Fatalf("NewEvaluator failed: %v", err) + } + + violations, err := eval.Evaluate(context.Background(), []byte(`{"value": 10}`)) + if err != nil { + t.Fatalf("Evaluate failed: %v", err) + } + if len(violations) != 0 { + t.Errorf("expected 0 violations, got %d: %v", len(violations), violations) + } +} + +func TestEvaluator_WithViolation(t *testing.T) { + policy := ` +package policy +import rego.v1 +violations contains msg if { + input.value < 0 + msg := "value is negative" +} +` + dir := writePolicyDir(t, "test.rego", policy) + eval, err := NewEvaluator(dir, "", nil, "data.policy.violations", nil) + if err != nil { + t.Fatalf("NewEvaluator failed: %v", err) + } + + violations, err := eval.Evaluate(context.Background(), []byte(`{"value": -5}`)) + if err != nil { + t.Fatalf("Evaluate failed: %v", err) + } + if len(violations) != 1 { + t.Fatalf("expected 1 violation, got %d: %v", len(violations), violations) + } + if violations[0] != "value is negative" { + t.Errorf("unexpected violation: %q", violations[0]) + } +} + +func TestEvaluator_RuntimeConfig(t *testing.T) { + policy := ` +package policy +import rego.v1 +violations contains msg if { + input.value > to_number(data.config.maxValue) + msg := "value exceeds maximum" +} +` + dir := writePolicyDir(t, "test.rego", policy) + eval, err := NewEvaluator(dir, "", nil, "data.policy.violations", map[string]string{"maxValue": "100"}) + if err != nil { + t.Fatalf("NewEvaluator failed: %v", err) + } + + // Under limit + violations, err := eval.Evaluate(context.Background(), []byte(`{"value": 50}`)) + if err != nil { + t.Fatalf("Evaluate failed: %v", err) + } + if len(violations) != 0 { + t.Errorf("expected 0 violations for value=50, got %v", violations) + } + + // Over limit + violations, err = eval.Evaluate(context.Background(), []byte(`{"value": 150}`)) + if err != nil { + t.Fatalf("Evaluate failed: %v", err) + } + if len(violations) != 1 { + t.Errorf("expected 1 violation for value=150, got %v", violations) + } +} + +func TestEvaluator_SkipsTestFiles(t *testing.T) { + dir := t.TempDir() + + policy := ` +package policy +import rego.v1 +violations contains "always" if { true } +` + os.WriteFile(filepath.Join(dir, "policy.rego"), []byte(policy), 0644) + + // Test file would cause compilation issues if loaded (different package) + testFile := ` +package policy_test +import rego.v1 +import data.policy +test_something if { count(policy.violations) > 0 } +` + os.WriteFile(filepath.Join(dir, "policy_test.rego"), []byte(testFile), 0644) + + eval, err := NewEvaluator(dir, "", nil, "data.policy.violations", nil) + if err != nil { + t.Fatalf("NewEvaluator should skip _test.rego files, but failed: %v", err) + } + + violations, err := eval.Evaluate(context.Background(), []byte(`{}`)) + if err != nil { + t.Fatalf("Evaluate failed: %v", err) + } + if len(violations) != 1 { + t.Errorf("expected 1 violation, got %d", len(violations)) + } +} + +func TestEvaluator_InvalidJSON(t *testing.T) { + policy := ` +package policy +import rego.v1 +violations := set() +` + dir := writePolicyDir(t, "test.rego", policy) + eval, err := NewEvaluator(dir, "", nil, "data.policy.violations", nil) + if err != nil { + t.Fatalf("NewEvaluator failed: %v", err) + } + + _, err = eval.Evaluate(context.Background(), []byte(`not json`)) + if err == nil { + t.Error("expected error for invalid JSON") + } +} + +// --- Evaluator URL Fetch Tests --- + +func TestEvaluator_FetchFromURL(t *testing.T) { + policy := ` +package policy +import rego.v1 +violations contains msg if { + input.value < 0 + msg := "value is negative" +} +` + // Serve the policy via a local HTTP server + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.Write([]byte(policy)) + })) + defer srv.Close() + + eval, err := NewEvaluator("", "", []string{srv.URL + "/test_policy.rego"}, "data.policy.violations", nil) + if err != nil { + t.Fatalf("NewEvaluator with URL failed: %v", err) + } + + // Compliant + violations, err := eval.Evaluate(context.Background(), []byte(`{"value": 10}`)) + if err != nil { + t.Fatalf("Evaluate failed: %v", err) + } + if len(violations) != 0 { + t.Errorf("expected 0 violations, got %v", violations) + } + + // Non-compliant + violations, err = eval.Evaluate(context.Background(), []byte(`{"value": -1}`)) + if err != nil { + t.Fatalf("Evaluate failed: %v", err) + } + if len(violations) != 1 { + t.Errorf("expected 1 violation, got %v", violations) + } +} + +func TestEvaluator_FetchURL_NotFound(t *testing.T) { + srv := httptest.NewServer(http.NotFoundHandler()) + defer srv.Close() + + _, err := NewEvaluator("", "", []string{srv.URL + "/missing.rego"}, "data.policy.violations", nil) + if err == nil { + t.Fatal("expected error for 404 URL") + } +} + +func TestEvaluator_FetchURL_InvalidScheme(t *testing.T) { + _, err := NewEvaluator("", "", []string{"ftp://example.com/policy.rego"}, "data.policy.violations", nil) + if err == nil { + t.Fatal("expected error for ftp:// scheme") + } +} + +func TestEvaluator_MixedLocalAndURL(t *testing.T) { + // Local policy + localPolicy := ` +package policy +import rego.v1 +violations contains "local_violation" if { input.local_bad } +` + dir := writePolicyDir(t, "local.rego", localPolicy) + + // Remote policy (different rule, same package) + remotePolicy := ` +package policy +import rego.v1 +violations contains "remote_violation" if { input.remote_bad } +` + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(remotePolicy)) + })) + defer srv.Close() + + eval, err := NewEvaluator(dir, "", []string{srv.URL + "/remote.rego"}, "data.policy.violations", nil) + if err != nil { + t.Fatalf("NewEvaluator failed: %v", err) + } + + // Trigger both violations + violations, err := eval.Evaluate(context.Background(), []byte(`{"local_bad": true, "remote_bad": true}`)) + if err != nil { + t.Fatalf("Evaluate failed: %v", err) + } + if len(violations) != 2 { + t.Errorf("expected 2 violations (local+remote), got %d: %v", len(violations), violations) + } +} + +// --- Evaluator with local file path in policySources --- + +func TestEvaluator_LocalFilePath(t *testing.T) { + policy := ` +package policy +import rego.v1 +violations contains "from_file" if { input.bad } +` + dir := t.TempDir() + policyPath := filepath.Join(dir, "local_policy.rego") + os.WriteFile(policyPath, []byte(policy), 0644) + + eval, err := NewEvaluator("", "", []string{policyPath}, "data.policy.violations", nil) + if err != nil { + t.Fatalf("NewEvaluator with local path failed: %v", err) + } + + violations, err := eval.Evaluate(context.Background(), []byte(`{"bad": true}`)) + if err != nil { + t.Fatalf("Evaluate failed: %v", err) + } + if len(violations) != 1 || violations[0] != "from_file" { + t.Errorf("expected [from_file], got %v", violations) + } +} + +// --- Enforcer Integration Tests --- + +func TestEnforcer_Compliant(t *testing.T) { + policy := ` +package policy +import rego.v1 +violations contains "blocked" if { input.context.action == "confirm"; input.block } +` + dir := writePolicyDir(t, "test.rego", policy) + + enforcer, err := New(map[string]string{ + "policyDir": dir, + "actions": "confirm", + }) + if err != nil { + t.Fatalf("New failed: %v", err) + } + + ctx := makeStepCtx("confirm", `{"context": {"action": "confirm"}, "block": false}`) + err = enforcer.Run(ctx) + if err != nil { + t.Errorf("expected nil error for compliant message, got: %v", err) + } +} + +func TestEnforcer_NonCompliant(t *testing.T) { + policy := ` +package policy +import rego.v1 +violations contains "blocked" if { input.context.action == "confirm" } +` + dir := writePolicyDir(t, "test.rego", policy) + + enforcer, err := New(map[string]string{ + "policyDir": dir, + "actions": "confirm", + }) + if err != nil { + t.Fatalf("New failed: %v", err) + } + + ctx := makeStepCtx("confirm", `{"context": {"action": "confirm"}}`) + err = enforcer.Run(ctx) + if err == nil { + t.Fatal("expected error for non-compliant message, got nil") + } + + // Should be a BadReqErr + if _, ok := err.(*model.BadReqErr); !ok { + t.Errorf("expected *model.BadReqErr, got %T: %v", err, err) + } +} + +func TestEnforcer_SkipsNonMatchingAction(t *testing.T) { + policy := ` +package policy +import rego.v1 +violations contains "blocked" if { true } +` + dir := writePolicyDir(t, "test.rego", policy) + + enforcer, err := New(map[string]string{ + "policyDir": dir, + "actions": "confirm", + }) + if err != nil { + t.Fatalf("New failed: %v", err) + } + + // Non-compliant body, but action is "search" — not in configured actions + ctx := makeStepCtx("search", `{"context": {"action": "search"}}`) + err = enforcer.Run(ctx) + if err != nil { + t.Errorf("expected nil for non-matching action, got: %v", err) + } +} + +func TestEnforcer_DisabledPlugin(t *testing.T) { + policy := ` +package policy +import rego.v1 +violations contains "blocked" if { true } +` + dir := writePolicyDir(t, "test.rego", policy) + + enforcer, err := New(map[string]string{ + "policyDir": dir, + "enabled": "false", + }) + if err != nil { + t.Fatalf("New failed: %v", err) + } + + ctx := makeStepCtx("confirm", `{"context": {"action": "confirm"}}`) + err = enforcer.Run(ctx) + if err != nil { + t.Errorf("expected nil for disabled plugin, got: %v", err) + } +} + +// --- Enforcer with URL-sourced policy --- + +func TestEnforcer_PolicyFromURL(t *testing.T) { + policy := ` +package policy +import rego.v1 +violations contains "blocked" if { input.context.action == "confirm" } +` + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(policy)) + })) + defer srv.Close() + + enforcer, err := New(map[string]string{ + "policyUrls": srv.URL + "/block_confirm.rego", + "actions": "confirm", + }) + if err != nil { + t.Fatalf("New failed: %v", err) + } + + ctx := makeStepCtx("confirm", `{"context": {"action": "confirm"}}`) + err = enforcer.Run(ctx) + if err == nil { + t.Fatal("expected error from URL-sourced policy, got nil") + } + if _, ok := err.(*model.BadReqErr); !ok { + t.Errorf("expected *model.BadReqErr, got %T", err) + } +} + +// --- extractAction Tests --- + +func TestExtractAction_FromURL(t *testing.T) { + action := extractAction("/bpp/caller/confirm", nil) + if action != "confirm" { + t.Errorf("expected 'confirm', got %q", action) + } +} + +func TestExtractAction_FromBody(t *testing.T) { + body := []byte(`{"context": {"action": "select"}}`) + action := extractAction("/x", body) + if action != "select" { + t.Errorf("expected 'select', got %q", action) + } +} diff --git a/pkg/plugin/implementation/policyenforcer/evaluator.go b/pkg/plugin/implementation/policyenforcer/evaluator.go new file mode 100644 index 0000000..8e93b71 --- /dev/null +++ b/pkg/plugin/implementation/policyenforcer/evaluator.go @@ -0,0 +1,238 @@ +package policyenforcer + +import ( + "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/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 +} + +// ModuleNames returns the names of the loaded .rego policy modules. +func (e *Evaluator) ModuleNames() []string { + return e.moduleNames +} + +// policyFetchTimeout is the HTTP timeout for fetching remote .rego files. +const policyFetchTimeout = 30 * time.Second + +// maxPolicySize is the maximum size of a single .rego file fetched from a URL (1 MB). +const maxPolicySize = 1 << 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. +func NewEvaluator(policyDir, policyFile string, policyUrls []string, query string, runtimeConfig map[string]string) (*Evaluator, error) { + modules := make(map[string]string) + + // Load from local directory + if policyDir != "" { + entries, err := os.ReadDir(policyDir) + if err != nil { + return nil, fmt.Errorf("failed to read policy directory %s: %w", policyDir, err) + } + for _, entry := range entries { + if entry.IsDir() { + continue + } + if !strings.HasSuffix(entry.Name(), ".rego") { + continue + } + // Skip test files — they shouldn't be compiled into the runtime evaluator + if strings.HasSuffix(entry.Name(), "_test.rego") { + continue + } + fpath := filepath.Join(policyDir, 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) + } + } + + // Load single local file + if policyFile != "" { + data, err := os.ReadFile(policyFile) + if err != nil { + return nil, fmt.Errorf("failed to read policy file %s: %w", policyFile, err) + } + modules[filepath.Base(policyFile)] = string(data) + } + + // Load from URLs and local file paths (policyUrls) + for _, rawSource := range policyUrls { + if isURL(rawSource) { + name, content, err := fetchPolicy(rawSource) + if err != nil { + return nil, fmt.Errorf("failed to fetch policy from %s: %w", rawSource, err) + } + modules[name] = content + } else { + // Treat as local file path + data, err := os.ReadFile(rawSource) + if err != nil { + return nil, fmt.Errorf("failed to read local policy source %s: %w", rawSource, err) + } + modules[filepath.Base(rawSource)] = string(data) + } + } + + if len(modules) == 0 { + return nil, fmt.Errorf("no .rego policy files found from any configured source") + } + + // 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 data.config from runtime config + store := map[string]interface{}{ + "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, + }, 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) (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: policyFetchTimeout} + 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) + } + + return extractViolations(rs) +} + +// extractViolations pulls string violations from the OPA result set. +// The query is expected to return a set of strings. +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 []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{}: + // OPA sometimes returns sets as maps with string keys + for key := range v { + violations = append(violations, key) + } + } + } + } + + return violations, nil +} + +// 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 +} diff --git a/pkg/plugin/manager.go b/pkg/plugin/manager.go index ef00dd8..d201945 100644 --- a/pkg/plugin/manager.go +++ b/pkg/plugin/manager.go @@ -257,6 +257,23 @@ func (m *Manager) Step(ctx context.Context, cfg *Config) (definition.Step, error return step, error } +// PolicyEnforcer returns a PolicyEnforcer instance based on the provided configuration. +// It registers a cleanup function for resource management. +func (m *Manager) PolicyEnforcer(ctx context.Context, cfg *Config) (definition.PolicyEnforcer, error) { + pp, err := provider[definition.PolicyEnforcerProvider](m.plugins, cfg.ID) + if err != nil { + return nil, fmt.Errorf("failed to load provider for %s: %w", cfg.ID, err) + } + enforcer, closer, err := pp.New(ctx, cfg.Config) + if err != nil { + return nil, err + } + if closer != nil { + m.closers = append(m.closers, closer) + } + return enforcer, 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) { diff --git a/policies/compliance.rego b/policies/compliance.rego new file mode 100644 index 0000000..5ab3793 --- /dev/null +++ b/policies/compliance.rego @@ -0,0 +1,18 @@ +package policy + +import rego.v1 + +# Example policy: validate confirm action messages. +# This is a sample policy file. Replace with your actual business rules. +# +# The policy evaluates incoming beckn messages and produces a set of +# violation strings. If any violations exist, the adapter will NACK +# the request. +# +# Available inputs: +# - input: the full JSON message body +# - data.config: runtime config from the adapter config (e.g., minDeliveryLeadHours) + +# violations is the set of policy violation messages. +# An empty set means the message is compliant. +violations := set()