- Updated the Policy Enforcer to consolidate policy source configuration under a single `policyPaths` key, allowing for auto-detection of URLs, directories, and files. - Removed deprecated keys such as `policyFile` and `policyUrls` from the configuration structure. - Adjusted related code and tests to ensure compatibility with the new configuration format. - Enhanced documentation to clarify the usage of `policyPaths` and provide examples for various configurations.
107 lines
3.2 KiB
Go
107 lines
3.2 KiB
Go
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.PolicyPaths, 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 ""
|
|
}
|