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.
This commit is contained in:
@@ -89,9 +89,7 @@ modules:
|
|||||||
policyEnforcer:
|
policyEnforcer:
|
||||||
id: policyenforcer
|
id: policyenforcer
|
||||||
config:
|
config:
|
||||||
policySources: "./policies/compliance.rego"
|
policyDir: "./policies"
|
||||||
actions: "confirm"
|
|
||||||
query: "data.policy.violations"
|
|
||||||
middleware:
|
middleware:
|
||||||
- id: reqpreprocessor
|
- id: reqpreprocessor
|
||||||
config:
|
config:
|
||||||
@@ -99,7 +97,7 @@ modules:
|
|||||||
role: bap
|
role: bap
|
||||||
steps:
|
steps:
|
||||||
- validateSign
|
- validateSign
|
||||||
- enforcePolicy
|
- policyEnforcer
|
||||||
- addRoute
|
- addRoute
|
||||||
- validateSchema
|
- validateSchema
|
||||||
|
|
||||||
|
|||||||
@@ -87,12 +87,10 @@ modules:
|
|||||||
policyEnforcer:
|
policyEnforcer:
|
||||||
id: policyenforcer
|
id: policyenforcer
|
||||||
config:
|
config:
|
||||||
policySources: "./policies/compliance.rego"
|
policyDir: "./policies"
|
||||||
actions: "confirm"
|
|
||||||
query: "data.policy.violations"
|
|
||||||
steps:
|
steps:
|
||||||
- validateSign
|
- validateSign
|
||||||
- enforcePolicy
|
- policyEnforcer
|
||||||
- addRoute
|
- addRoute
|
||||||
- validateSchema
|
- validateSchema
|
||||||
|
|
||||||
|
|||||||
@@ -69,9 +69,7 @@ modules:
|
|||||||
policyEnforcer:
|
policyEnforcer:
|
||||||
id: policyenforcer
|
id: policyenforcer
|
||||||
config:
|
config:
|
||||||
policySources: "./policies/compliance.rego"
|
policyDir: "./policies"
|
||||||
actions: "confirm"
|
|
||||||
query: "data.policy.violations"
|
|
||||||
middleware:
|
middleware:
|
||||||
- id: reqpreprocessor
|
- id: reqpreprocessor
|
||||||
config:
|
config:
|
||||||
@@ -79,7 +77,7 @@ modules:
|
|||||||
role: bap
|
role: bap
|
||||||
steps:
|
steps:
|
||||||
- validateSign
|
- validateSign
|
||||||
- enforcePolicy
|
- policyEnforcer
|
||||||
- addRoute
|
- addRoute
|
||||||
|
|
||||||
- name: bapTxnCaller
|
- name: bapTxnCaller
|
||||||
@@ -172,12 +170,10 @@ modules:
|
|||||||
policyEnforcer:
|
policyEnforcer:
|
||||||
id: policyenforcer
|
id: policyenforcer
|
||||||
config:
|
config:
|
||||||
policySources: "./policies/compliance.rego"
|
policyDir: "./policies"
|
||||||
actions: "confirm"
|
|
||||||
query: "data.policy.violations"
|
|
||||||
steps:
|
steps:
|
||||||
- validateSign
|
- validateSign
|
||||||
- enforcePolicy
|
- policyEnforcer
|
||||||
- addRoute
|
- addRoute
|
||||||
|
|
||||||
- name: bppTxnCaller
|
- name: bppTxnCaller
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ modules:
|
|||||||
id: schemavalidator
|
id: schemavalidator
|
||||||
config:
|
config:
|
||||||
schemaDir: /mnt/gcs/configs/schemas
|
schemaDir: /mnt/gcs/configs/schemas
|
||||||
|
policyEnforcer:
|
||||||
|
id: policyenforcer
|
||||||
|
config:
|
||||||
|
policyDir: "./policies"
|
||||||
signValidator:
|
signValidator:
|
||||||
id: signvalidator
|
id: signvalidator
|
||||||
publisher:
|
publisher:
|
||||||
@@ -66,6 +70,7 @@ modules:
|
|||||||
role: bap
|
role: bap
|
||||||
steps:
|
steps:
|
||||||
- validateSign
|
- validateSign
|
||||||
|
- policyEnforcer
|
||||||
- addRoute
|
- addRoute
|
||||||
- validateSchema
|
- validateSchema
|
||||||
- name: bapTxnCaller
|
- name: bapTxnCaller
|
||||||
@@ -98,6 +103,10 @@ modules:
|
|||||||
id: schemavalidator
|
id: schemavalidator
|
||||||
config:
|
config:
|
||||||
schemaDir: /mnt/gcs/configs/schemas
|
schemaDir: /mnt/gcs/configs/schemas
|
||||||
|
policyEnforcer:
|
||||||
|
id: policyenforcer
|
||||||
|
config:
|
||||||
|
policyDir: "./policies"
|
||||||
signer:
|
signer:
|
||||||
id: signer
|
id: signer
|
||||||
publisher:
|
publisher:
|
||||||
@@ -116,6 +125,7 @@ modules:
|
|||||||
role: bap
|
role: bap
|
||||||
steps:
|
steps:
|
||||||
- validateSchema
|
- validateSchema
|
||||||
|
- policyEnforcer
|
||||||
- addRoute
|
- addRoute
|
||||||
- sign
|
- sign
|
||||||
- name: bppTxnReciever
|
- name: bppTxnReciever
|
||||||
@@ -149,6 +159,10 @@ modules:
|
|||||||
id: schemavalidator
|
id: schemavalidator
|
||||||
config:
|
config:
|
||||||
schemaDir: /mnt/gcs/configs/schemas
|
schemaDir: /mnt/gcs/configs/schemas
|
||||||
|
policyEnforcer:
|
||||||
|
id: policyenforcer
|
||||||
|
config:
|
||||||
|
policyDir: "./policies"
|
||||||
signValidator:
|
signValidator:
|
||||||
id: signvalidator
|
id: signvalidator
|
||||||
publisher:
|
publisher:
|
||||||
@@ -167,6 +181,7 @@ modules:
|
|||||||
role: bpp
|
role: bpp
|
||||||
steps:
|
steps:
|
||||||
- validateSign
|
- validateSign
|
||||||
|
- policyEnforcer
|
||||||
- addRoute
|
- addRoute
|
||||||
- validateSchema
|
- validateSchema
|
||||||
- name: bppTxnCaller
|
- name: bppTxnCaller
|
||||||
@@ -199,6 +214,10 @@ modules:
|
|||||||
id: schemavalidator
|
id: schemavalidator
|
||||||
config:
|
config:
|
||||||
schemaDir: /mnt/gcs/configs/schemas
|
schemaDir: /mnt/gcs/configs/schemas
|
||||||
|
policyEnforcer:
|
||||||
|
id: policyenforcer
|
||||||
|
config:
|
||||||
|
policyDir: "./policies"
|
||||||
signer:
|
signer:
|
||||||
id: signer
|
id: signer
|
||||||
publisher:
|
publisher:
|
||||||
@@ -217,5 +236,6 @@ modules:
|
|||||||
role: bpp
|
role: bpp
|
||||||
steps:
|
steps:
|
||||||
- validateSchema
|
- validateSchema
|
||||||
|
- policyEnforcer
|
||||||
- addRoute
|
- addRoute
|
||||||
- sign
|
- sign
|
||||||
@@ -354,7 +354,7 @@ func (h *stdHandler) initSteps(ctx context.Context, mgr PluginManager, cfg *Conf
|
|||||||
s, err = newValidateSchemaStep(h.schemaValidator)
|
s, err = newValidateSchemaStep(h.schemaValidator)
|
||||||
case "addRoute":
|
case "addRoute":
|
||||||
s, err = newAddRouteStep(h.router)
|
s, err = newAddRouteStep(h.router)
|
||||||
case "enforcePolicy":
|
case "policyEnforcer":
|
||||||
s, err = newEnforcePolicyStep(h.policyEnforcer)
|
s, err = newEnforcePolicyStep(h.policyEnforcer)
|
||||||
default:
|
default:
|
||||||
if customStep, exists := steps[step]; exists {
|
if customStep, exists := steps[step]; exists {
|
||||||
|
|||||||
@@ -17,27 +17,63 @@ All config keys are passed via `map[string]string` in the adapter YAML config.
|
|||||||
|
|
||||||
| Key | Required | Default | Description |
|
| 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 |
|
| `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 |
|
| `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 |
|
| `enabled` | No | `true` | Enable/disable the plugin |
|
||||||
| `debugLogging` | No | `false` | Enable verbose logging |
|
| `debugLogging` | No | `false` | Enable verbose logging |
|
||||||
| *any other key* | No | — | Forwarded to Rego as `data.config.<key>` |
|
| *any other key* | No | — | Forwarded to Rego as `data.config.<key>` |
|
||||||
|
|
||||||
### 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
|
```yaml
|
||||||
config:
|
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
|
### 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
|
```yaml
|
||||||
config:
|
config:
|
||||||
@@ -48,14 +84,15 @@ config:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
plugins:
|
plugins:
|
||||||
steps:
|
policyEnforcer:
|
||||||
- id: policyenforcer
|
id: policyenforcer
|
||||||
config:
|
config:
|
||||||
policyUrls: "https://policies.example.com/compliance.rego,/local/policies/safety.rego"
|
policyUrls: "https://policies.example.com/compliance.rego,/local/policies/"
|
||||||
actions: "confirm,init"
|
minDeliveryLeadHours: "4"
|
||||||
query: "data.policy.violations"
|
debugLogging: "true"
|
||||||
minDeliveryLeadHours: "4"
|
steps:
|
||||||
debugLogging: "true"
|
- policyEnforcer
|
||||||
|
- addRoute
|
||||||
```
|
```
|
||||||
|
|
||||||
## Relationship with Schema Validator
|
## Relationship with Schema Validator
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package policyenforcer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,11 +21,12 @@ type Config struct {
|
|||||||
PolicyUrls []string
|
PolicyUrls []string
|
||||||
|
|
||||||
// Query is the Rego query that returns a set of violation strings.
|
// Query is the Rego query that returns a set of violation strings.
|
||||||
// Default: "data.policy.violations"
|
// Default: "data.policy.violations".
|
||||||
Query string
|
Query string
|
||||||
|
|
||||||
// Actions is the list of beckn actions to enforce policies on.
|
// 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
|
Actions []string
|
||||||
|
|
||||||
// Enabled controls whether the plugin is active.
|
// Enabled controls whether the plugin is active.
|
||||||
@@ -53,7 +55,6 @@ var knownKeys = map[string]bool{
|
|||||||
func DefaultConfig() *Config {
|
func DefaultConfig() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
Query: "data.policy.violations",
|
Query: "data.policy.violations",
|
||||||
Actions: []string{"confirm"},
|
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
DebugLogging: false,
|
DebugLogging: false,
|
||||||
RuntimeConfig: make(map[string]string),
|
RuntimeConfig: make(map[string]string),
|
||||||
@@ -71,7 +72,7 @@ func ParseConfig(cfg map[string]string) (*Config, error) {
|
|||||||
config.PolicyFile = file
|
config.PolicyFile = file
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy: comma-separated policyUrls
|
// Comma-separated policyUrls (supports URLs, local files, and directory paths)
|
||||||
if urls, ok := cfg["policyUrls"]; ok && urls != "" {
|
if urls, ok := cfg["policyUrls"]; ok && urls != "" {
|
||||||
for _, u := range strings.Split(urls, ",") {
|
for _, u := range strings.Split(urls, ",") {
|
||||||
u = strings.TrimSpace(u)
|
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 {
|
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 != "" {
|
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.
|
// 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 {
|
func (c *Config) IsActionEnabled(action string) bool {
|
||||||
|
if len(c.Actions) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
for _, a := range c.Actions {
|
for _, a := range c.Actions {
|
||||||
if a == action {
|
if a == action {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -47,10 +47,10 @@ func TestParseConfig_Defaults(t *testing.T) {
|
|||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
if cfg.Query != "data.policy.violations" {
|
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" {
|
if len(cfg.Actions) != 0 {
|
||||||
t.Errorf("expected default actions [confirm], got %v", cfg.Actions)
|
t.Errorf("expected empty default actions (all enabled), got %v", cfg.Actions)
|
||||||
}
|
}
|
||||||
if !cfg.Enabled {
|
if !cfg.Enabled {
|
||||||
t.Error("expected enabled=true by default")
|
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{
|
enforcer, err := New(map[string]string{
|
||||||
"policyDir": dir,
|
"policyDir": dir,
|
||||||
|
"query": "data.policy.violations",
|
||||||
"actions": "confirm",
|
"actions": "confirm",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -404,6 +405,7 @@ violations contains "blocked" if { input.context.action == "confirm" }
|
|||||||
|
|
||||||
enforcer, err := New(map[string]string{
|
enforcer, err := New(map[string]string{
|
||||||
"policyDir": dir,
|
"policyDir": dir,
|
||||||
|
"query": "data.policy.violations",
|
||||||
"actions": "confirm",
|
"actions": "confirm",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -432,6 +434,7 @@ violations contains "blocked" if { true }
|
|||||||
|
|
||||||
enforcer, err := New(map[string]string{
|
enforcer, err := New(map[string]string{
|
||||||
"policyDir": dir,
|
"policyDir": dir,
|
||||||
|
"query": "data.policy.violations",
|
||||||
"actions": "confirm",
|
"actions": "confirm",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -456,6 +459,7 @@ violations contains "blocked" if { true }
|
|||||||
|
|
||||||
enforcer, err := New(map[string]string{
|
enforcer, err := New(map[string]string{
|
||||||
"policyDir": dir,
|
"policyDir": dir,
|
||||||
|
"query": "data.policy.violations",
|
||||||
"enabled": "false",
|
"enabled": "false",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -484,6 +488,7 @@ violations contains "blocked" if { input.context.action == "confirm" }
|
|||||||
|
|
||||||
enforcer, err := New(map[string]string{
|
enforcer, err := New(map[string]string{
|
||||||
"policyUrls": srv.URL + "/block_confirm.rego",
|
"policyUrls": srv.URL + "/block_confirm.rego",
|
||||||
|
"query": "data.policy.violations",
|
||||||
"actions": "confirm",
|
"actions": "confirm",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ func NewEvaluator(policyDir, policyFile string, policyUrls []string, query strin
|
|||||||
modules[filepath.Base(policyFile)] = string(data)
|
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 {
|
for _, rawSource := range policyUrls {
|
||||||
if isURL(rawSource) {
|
if isURL(rawSource) {
|
||||||
name, content, err := fetchPolicy(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)
|
return nil, fmt.Errorf("failed to fetch policy from %s: %w", rawSource, err)
|
||||||
}
|
}
|
||||||
modules[name] = content
|
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 {
|
} else {
|
||||||
// Treat as local file path
|
// Treat as local file path
|
||||||
data, err := os.ReadFile(rawSource)
|
data, err := os.ReadFile(rawSource)
|
||||||
|
|||||||
Reference in New Issue
Block a user