From e22b79e13725a84feaada59941e9926c7f239780 Mon Sep 17 00:00:00 2001 From: Ayush Rawat Date: Tue, 3 Mar 2026 10:04:22 +0530 Subject: [PATCH] Refactor Policy Enforcer Configuration - Updated the Policy Enforcer configuration across multiple YAML files to use a unified `policyDir` instead of individual policy sources. - Changed the step name from `enforcePolicy` to `policyEnforcer` for consistency. - Enhanced the documentation to clarify the use of `policyUrls`, `policyDir`, and `policyFile` for policy sources. - Adjusted related code and tests to accommodate the new configuration structure. - Added documentation for using YAML folded scalar (>-) to keep long comma-separated policyUrls values readable across multiple lines. --- config/local-beckn-one-bap.yaml | 6 +- config/local-beckn-one-bpp.yaml | 6 +- config/local-simple.yaml | 12 ++-- config/onix/adapter.yaml | 20 ++++++ core/module/handler/stdHandler.go | 2 +- .../implementation/policyenforcer/README.md | 67 ++++++++++++++----- .../implementation/policyenforcer/config.go | 21 ++++-- .../policyenforcer/enforcer_test.go | 11 ++- .../policyenforcer/evaluator.go | 19 +++++- 9 files changed, 123 insertions(+), 41 deletions(-) diff --git a/config/local-beckn-one-bap.yaml b/config/local-beckn-one-bap.yaml index c179ddc..84f6090 100644 --- a/config/local-beckn-one-bap.yaml +++ b/config/local-beckn-one-bap.yaml @@ -89,9 +89,7 @@ modules: policyEnforcer: id: policyenforcer config: - policySources: "./policies/compliance.rego" - actions: "confirm" - query: "data.policy.violations" + policyDir: "./policies" middleware: - id: reqpreprocessor config: @@ -99,7 +97,7 @@ modules: role: bap steps: - validateSign - - enforcePolicy + - policyEnforcer - addRoute - validateSchema diff --git a/config/local-beckn-one-bpp.yaml b/config/local-beckn-one-bpp.yaml index 1564a85..812c588 100644 --- a/config/local-beckn-one-bpp.yaml +++ b/config/local-beckn-one-bpp.yaml @@ -87,12 +87,10 @@ modules: policyEnforcer: id: policyenforcer config: - policySources: "./policies/compliance.rego" - actions: "confirm" - query: "data.policy.violations" + policyDir: "./policies" steps: - validateSign - - enforcePolicy + - policyEnforcer - addRoute - validateSchema diff --git a/config/local-simple.yaml b/config/local-simple.yaml index 7360f90..270d046 100644 --- a/config/local-simple.yaml +++ b/config/local-simple.yaml @@ -69,9 +69,7 @@ modules: policyEnforcer: id: policyenforcer config: - policySources: "./policies/compliance.rego" - actions: "confirm" - query: "data.policy.violations" + policyDir: "./policies" middleware: - id: reqpreprocessor config: @@ -79,7 +77,7 @@ modules: role: bap steps: - validateSign - - enforcePolicy + - policyEnforcer - addRoute - name: bapTxnCaller @@ -172,12 +170,10 @@ modules: policyEnforcer: id: policyenforcer config: - policySources: "./policies/compliance.rego" - actions: "confirm" - query: "data.policy.violations" + policyDir: "./policies" steps: - validateSign - - enforcePolicy + - policyEnforcer - addRoute - name: bppTxnCaller diff --git a/config/onix/adapter.yaml b/config/onix/adapter.yaml index 200388c..77485af 100644 --- a/config/onix/adapter.yaml +++ b/config/onix/adapter.yaml @@ -48,6 +48,10 @@ modules: id: schemavalidator config: schemaDir: /mnt/gcs/configs/schemas + policyEnforcer: + id: policyenforcer + config: + policyDir: "./policies" signValidator: id: signvalidator publisher: @@ -66,6 +70,7 @@ modules: role: bap steps: - validateSign + - policyEnforcer - addRoute - validateSchema - name: bapTxnCaller @@ -98,6 +103,10 @@ modules: id: schemavalidator config: schemaDir: /mnt/gcs/configs/schemas + policyEnforcer: + id: policyenforcer + config: + policyDir: "./policies" signer: id: signer publisher: @@ -116,6 +125,7 @@ modules: role: bap steps: - validateSchema + - policyEnforcer - addRoute - sign - name: bppTxnReciever @@ -149,6 +159,10 @@ modules: id: schemavalidator config: schemaDir: /mnt/gcs/configs/schemas + policyEnforcer: + id: policyenforcer + config: + policyDir: "./policies" signValidator: id: signvalidator publisher: @@ -167,6 +181,7 @@ modules: role: bpp steps: - validateSign + - policyEnforcer - addRoute - validateSchema - name: bppTxnCaller @@ -199,6 +214,10 @@ modules: id: schemavalidator config: schemaDir: /mnt/gcs/configs/schemas + policyEnforcer: + id: policyenforcer + config: + policyDir: "./policies" signer: id: signer publisher: @@ -217,5 +236,6 @@ modules: role: bpp steps: - validateSchema + - policyEnforcer - addRoute - sign \ No newline at end of file diff --git a/core/module/handler/stdHandler.go b/core/module/handler/stdHandler.go index 097400e..6b3e1c3 100644 --- a/core/module/handler/stdHandler.go +++ b/core/module/handler/stdHandler.go @@ -354,7 +354,7 @@ 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": + case "policyEnforcer": s, err = newEnforcePolicyStep(h.policyEnforcer) default: if customStep, exists := steps[step]; exists { diff --git a/pkg/plugin/implementation/policyenforcer/README.md b/pkg/plugin/implementation/policyenforcer/README.md index 686a36f..df77fab 100644 --- a/pkg/plugin/implementation/policyenforcer/README.md +++ b/pkg/plugin/implementation/policyenforcer/README.md @@ -17,27 +17,63 @@ 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 | +| `policyUrls` | At least one of `policyUrls`, `policyDir`, or `policyFile` required | — | Comma-separated list of URLs, local file paths, or directory paths to `.rego` files | +| `policyDir` | | `./policies` | 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 | +| `actions` | No | *(empty — all actions)* | Comma-separated beckn actions to enforce. When omitted, all actions are evaluated and the Rego policy itself decides which to gate. | | `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 +### Policy Sources -`policyUrls` accepts both remote URLs and local file paths, separated by commas: +`policyUrls` is the primary configuration key. It accepts a comma-separated list of: +- **Remote URLs**: `https://policies.example.com/compliance.rego` +- **Local file paths**: `/etc/policies/local.rego` +- **Directory paths**: `/etc/policies/` (all `.rego` files loaded, `_test.rego` excluded) ```yaml config: - policyUrls: "https://policies.example.com/compliance.rego,/etc/policies/local.rego,https://policies.example.com/safety.rego" + policyUrls: "https://policies.example.com/compliance.rego,/etc/policies/,/local/safety.rego" +``` + +When specifying many URLs, use the YAML folded scalar (`>-`) to keep the config readable: + +```yaml +config: + policyUrls: >- + https://policies.example.com/compliance.rego, + https://policies.example.com/safety.rego, + https://policies.example.com/rate-limit.rego, + /local/policies/, + https://policies.example.com/auth.rego +``` + +The `>-` folds newlines into spaces, so the value is parsed as a single comma-separated string. + +### Minimal Config + +By default, the plugin loads `.rego` files from `./policies` and uses the query `data.policy.violations`. A zero-config setup works if your policies are in the default directory: + +```yaml +policyEnforcer: + id: policyenforcer + config: {} +``` + +Or specify a custom policy location: + +```yaml +policyEnforcer: + id: policyenforcer + config: + policyUrls: "./policies/compliance.rego" ``` ### Air-Gapped Deployments -For environments without internet access, replace any URL with a local file path or volume mount: +For environments without internet access, use local file paths or volume mounts: ```yaml config: @@ -48,14 +84,15 @@ 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" + policyEnforcer: + id: policyenforcer + config: + policyUrls: "https://policies.example.com/compliance.rego,/local/policies/" + minDeliveryLeadHours: "4" + debugLogging: "true" +steps: + - policyEnforcer + - addRoute ``` ## Relationship with Schema Validator diff --git a/pkg/plugin/implementation/policyenforcer/config.go b/pkg/plugin/implementation/policyenforcer/config.go index 7232b3b..1bdeca7 100644 --- a/pkg/plugin/implementation/policyenforcer/config.go +++ b/pkg/plugin/implementation/policyenforcer/config.go @@ -2,6 +2,7 @@ package policyenforcer import ( "fmt" + "os" "strings" ) @@ -20,11 +21,12 @@ type Config struct { PolicyUrls []string // Query is the Rego query that returns a set of violation strings. - // Default: "data.policy.violations" + // Default: "data.policy.violations". Query string // Actions is the list of beckn actions to enforce policies on. - // Default: ["confirm"] + // When empty or nil, all actions are considered and the Rego policy + // is responsible for deciding which actions to gate. Actions []string // Enabled controls whether the plugin is active. @@ -53,7 +55,6 @@ var knownKeys = map[string]bool{ func DefaultConfig() *Config { return &Config{ Query: "data.policy.violations", - Actions: []string{"confirm"}, Enabled: true, DebugLogging: false, RuntimeConfig: make(map[string]string), @@ -71,7 +72,7 @@ func ParseConfig(cfg map[string]string) (*Config, error) { config.PolicyFile = file } - // Legacy: comma-separated policyUrls + // Comma-separated policyUrls (supports URLs, local files, and directory paths) if urls, ok := cfg["policyUrls"]; ok && urls != "" { for _, u := range strings.Split(urls, ",") { u = strings.TrimSpace(u) @@ -82,7 +83,12 @@ func ParseConfig(cfg map[string]string) (*Config, error) { } if config.PolicyDir == "" && config.PolicyFile == "" && len(config.PolicyUrls) == 0 { - return nil, fmt.Errorf("at least one policy source is required (policyDir, policyFile, or policyUrls)") + // Fall back to the default ./policies directory if it exists on disk. + if info, err := os.Stat("./policies"); err == nil && info.IsDir() { + config.PolicyDir = "./policies" + } else { + return nil, fmt.Errorf("at least one policy source is required (policyDir, policyFile, or policyUrls)") + } } if query, ok := cfg["query"]; ok && query != "" { @@ -119,7 +125,12 @@ func ParseConfig(cfg map[string]string) (*Config, error) { } // IsActionEnabled checks if the given action is in the configured actions list. +// When the actions list is empty/nil, all actions are enabled and action-gating +// is delegated entirely to the Rego policy. func (c *Config) IsActionEnabled(action string) bool { + if len(c.Actions) == 0 { + return true + } for _, a := range c.Actions { if a == action { return true diff --git a/pkg/plugin/implementation/policyenforcer/enforcer_test.go b/pkg/plugin/implementation/policyenforcer/enforcer_test.go index 8f6f811..e8ff4d0 100644 --- a/pkg/plugin/implementation/policyenforcer/enforcer_test.go +++ b/pkg/plugin/implementation/policyenforcer/enforcer_test.go @@ -47,10 +47,10 @@ func TestParseConfig_Defaults(t *testing.T) { t.Fatalf("unexpected error: %v", err) } if cfg.Query != "data.policy.violations" { - t.Errorf("expected default query, got %q", cfg.Query) + t.Errorf("expected default query 'data.policy.violations', got %q", cfg.Query) } - if len(cfg.Actions) != 1 || cfg.Actions[0] != "confirm" { - t.Errorf("expected default actions [confirm], got %v", cfg.Actions) + if len(cfg.Actions) != 0 { + t.Errorf("expected empty default actions (all enabled), got %v", cfg.Actions) } if !cfg.Enabled { t.Error("expected enabled=true by default") @@ -381,6 +381,7 @@ violations contains "blocked" if { input.context.action == "confirm"; input.bloc enforcer, err := New(map[string]string{ "policyDir": dir, + "query": "data.policy.violations", "actions": "confirm", }) if err != nil { @@ -404,6 +405,7 @@ violations contains "blocked" if { input.context.action == "confirm" } enforcer, err := New(map[string]string{ "policyDir": dir, + "query": "data.policy.violations", "actions": "confirm", }) if err != nil { @@ -432,6 +434,7 @@ violations contains "blocked" if { true } enforcer, err := New(map[string]string{ "policyDir": dir, + "query": "data.policy.violations", "actions": "confirm", }) if err != nil { @@ -456,6 +459,7 @@ violations contains "blocked" if { true } enforcer, err := New(map[string]string{ "policyDir": dir, + "query": "data.policy.violations", "enabled": "false", }) if err != nil { @@ -484,6 +488,7 @@ violations contains "blocked" if { input.context.action == "confirm" } enforcer, err := New(map[string]string{ "policyUrls": srv.URL + "/block_confirm.rego", + "query": "data.policy.violations", "actions": "confirm", }) if err != nil { diff --git a/pkg/plugin/implementation/policyenforcer/evaluator.go b/pkg/plugin/implementation/policyenforcer/evaluator.go index 8e93b71..f6e4807 100644 --- a/pkg/plugin/implementation/policyenforcer/evaluator.go +++ b/pkg/plugin/implementation/policyenforcer/evaluator.go @@ -78,7 +78,7 @@ func NewEvaluator(policyDir, policyFile string, policyUrls []string, query strin modules[filepath.Base(policyFile)] = string(data) } - // Load from URLs and local file paths (policyUrls) + // Load from URLs, local file paths, and directory paths (policyUrls) for _, rawSource := range policyUrls { if isURL(rawSource) { name, content, err := fetchPolicy(rawSource) @@ -86,6 +86,23 @@ func NewEvaluator(policyDir, policyFile string, policyUrls []string, query strin return nil, fmt.Errorf("failed to fetch policy from %s: %w", rawSource, err) } modules[name] = content + } else if info, err := os.Stat(rawSource); err == nil && info.IsDir() { + // Treat as directory — load all .rego files inside + entries, err := os.ReadDir(rawSource) + if err != nil { + return nil, fmt.Errorf("failed to read policy directory %s: %w", rawSource, err) + } + for _, entry := range entries { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".rego") || strings.HasSuffix(entry.Name(), "_test.rego") { + continue + } + fpath := filepath.Join(rawSource, 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 { // Treat as local file path data, err := os.ReadFile(rawSource)