From c22977a3d5c8a0339595615000229aaacba02192 Mon Sep 17 00:00:00 2001 From: IvanPazManetu Date: Wed, 11 Feb 2026 15:44:37 -0500 Subject: [PATCH 1/4] AuxData Feature Signed-off-by: IvanPazManetu --- cmd/mpe/main.go | 12 ++ cmd/mpe/subcommands/serve/core.go | 14 +- cmd/mpe/subcommands/test/core_test.go | 91 ++++++++++++ cmd/mpe/subcommands/test/engine.go | 31 +++-- .../test/test/auxdata-envoy-input.json | 13 ++ .../test/test/auxdata-mapper-domain.yml | 33 +++++ docs/docs/concepts/mappers.md | 21 +++ docs/docs/reference/cli/serve.md | 1 + docs/docs/reference/cli/test.md | 8 ++ docs/docs/reference/configuration.md | 16 +++ pkg/core/auxdata/auxdata.go | 76 ++++++++++ pkg/core/auxdata/auxdata_test.go | 131 ++++++++++++++++++ pkg/core/config/config.go | 8 ++ pkg/decisionpoint/envoy/envoy.go | 8 +- pkg/decisionpoint/envoy/envoy_test.go | 10 +- 15 files changed, 456 insertions(+), 17 deletions(-) create mode 100644 cmd/mpe/subcommands/test/test/auxdata-envoy-input.json create mode 100644 cmd/mpe/subcommands/test/test/auxdata-mapper-domain.yml create mode 100644 pkg/core/auxdata/auxdata.go create mode 100644 pkg/core/auxdata/auxdata_test.go diff --git a/cmd/mpe/main.go b/cmd/mpe/main.go index 0443725..41bd9b4 100644 --- a/cmd/mpe/main.go +++ b/cmd/mpe/main.go @@ -117,6 +117,10 @@ func main() { Name: "no-opa-flags", Usage: "Disable all OPA flags (overrides --opa-flags and MPE_CLI_OPA_FLAGS).", }, + &cli.StringFlag{ + Name: "auxdata", + Usage: "Load auxiliary data from directory `PATH`", + }, }, Action: test.ExecuteMapper, }, @@ -147,6 +151,10 @@ func main() { Name: "no-opa-flags", Usage: "Disable all OPA flags (overrides --opa-flags and MPE_CLI_OPA_FLAGS).", }, + &cli.StringFlag{ + Name: "auxdata", + Usage: "Load auxiliary data from directory `PATH`", + }, }, Action: test.ExecuteEnvoy, }, @@ -191,6 +199,10 @@ func main() { Name: "no-opa-flags", Usage: "Disable all OPA flags (overrides --opa-flags and MPE_CLI_OPA_FLAGS).", }, + &cli.StringFlag{ + Name: "auxdata", + Usage: "Load auxiliary data from directory `PATH`", + }, }, Action: serve.Execute, }, diff --git a/cmd/mpe/subcommands/serve/core.go b/cmd/mpe/subcommands/serve/core.go index 6af4c19..3a3a15f 100644 --- a/cmd/mpe/subcommands/serve/core.go +++ b/cmd/mpe/subcommands/serve/core.go @@ -11,6 +11,8 @@ import ( "github.com/manetu/policyengine/cmd/mpe/common" "github.com/manetu/policyengine/internal/logging" + "github.com/manetu/policyengine/pkg/core/auxdata" + "github.com/manetu/policyengine/pkg/core/config" "github.com/manetu/policyengine/pkg/decisionpoint" "github.com/manetu/policyengine/pkg/decisionpoint/envoy" "github.com/manetu/policyengine/pkg/decisionpoint/generic" @@ -31,12 +33,22 @@ func Execute(ctx context.Context, cmd *cli.Command) error { return err } + // Load auxiliary data from configured path or CLI flag + auxPath := config.VConfig.GetString(config.AuxDataPath) + if p := cmd.String("auxdata"); p != "" { + auxPath = p + } + aux, err := auxdata.LoadAuxData(auxPath) + if err != nil { + return err + } + var server decisionpoint.Server switch cmd.String("protocol") { case "generic": server, err = generic.CreateServer(pe, port) case "envoy": - server, err = envoy.CreateServer(pe, port, cmd.String("name")) + server, err = envoy.CreateServer(pe, port, cmd.String("name"), aux) } if err != nil { return err diff --git a/cmd/mpe/subcommands/test/core_test.go b/cmd/mpe/subcommands/test/core_test.go index ff7573c..d846acc 100644 --- a/cmd/mpe/subcommands/test/core_test.go +++ b/cmd/mpe/subcommands/test/core_test.go @@ -6,6 +6,7 @@ package test import ( "context" + "encoding/json" "fmt" "os" "path/filepath" @@ -209,6 +210,7 @@ func buildTestCommand(action cli.ActionFunc) *cli.Command { &cli.StringFlag{Name: "name", Aliases: []string{"n"}}, &cli.StringFlag{Name: "opa-flags"}, &cli.BoolFlag{Name: "no-opa-flags"}, + &cli.StringFlag{Name: "auxdata"}, }, Action: action, }, @@ -220,6 +222,7 @@ func buildTestCommand(action cli.ActionFunc) *cli.Command { &cli.StringFlag{Name: "name", Aliases: []string{"n"}}, &cli.StringFlag{Name: "opa-flags"}, &cli.BoolFlag{Name: "no-opa-flags"}, + &cli.StringFlag{Name: "auxdata"}, }, Action: action, }, @@ -364,3 +367,91 @@ func TestExecuteEnvoy_InvalidBundle(t *testing.T) { err := cmd.Run(context.Background(), args) assert.Error(t, err, "ExecuteEnvoy should fail with non-existent bundle file") } + +// TestExecuteMapper_WithAuxData tests that auxdata merged into the mapper input +// is accessible to Rego code when computing the PORC. This exercises the full +// CLI pipeline: --auxdata dir -> LoadAuxData -> MergeAuxData -> mapper.Evaluate +// -> input.auxdata.* available in Rego -> PORC output contains auxdata values. +func TestExecuteMapper_WithAuxData(t *testing.T) { + bundleFile := filepath.Join("test", "auxdata-mapper-domain.yml") + inputFile := filepath.Join("test", "auxdata-envoy-input.json") + + require.FileExists(t, bundleFile, "auxdata-mapper-domain.yml should exist") + require.FileExists(t, inputFile, "auxdata-envoy-input.json should exist") + + // Create a temp directory simulating a mounted ConfigMap with auxdata files. + // Each file name becomes a key and its content becomes the value, just like + // a Kubernetes ConfigMap volume mount. + auxDir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(auxDir, "region"), []byte("us-east-1"), 0644)) + require.NoError(t, os.WriteFile(filepath.Join(auxDir, "tier"), []byte("premium"), 0644)) + require.NoError(t, os.WriteFile(filepath.Join(auxDir, "environment"), []byte("staging"), 0644)) + + // Capture stdout so we can inspect the PORC JSON output + oldStdout := os.Stdout + r, w, err := os.Pipe() + require.NoError(t, err) + os.Stdout = w + + cmd := buildTestCommand(ExecuteMapper) + args := []string{ + "mpe", "test", "mapper", + "-i", inputFile, + "-b", bundleFile, + "--auxdata", auxDir, + } + + runErr := cmd.Run(context.Background(), args) + + // Restore stdout and read captured output + w.Close() + os.Stdout = oldStdout + captured, readErr := os.ReadFile(r.Name()) + if readErr != nil { + // os.Pipe files don't have names; read from the reader directly + buf := make([]byte, 4096) + n, _ := r.Read(buf) + captured = buf[:n] + } + r.Close() + + require.NoError(t, runErr, "ExecuteMapper should succeed with auxdata") + require.NotEmpty(t, captured, "Should have PORC JSON output") + + // Parse the PORC JSON output + var porc map[string]interface{} + require.NoError(t, json.Unmarshal(captured, &porc), "Output should be valid JSON: %s", string(captured)) + + // Pretty-print the full PORC for verbose output + prettyJSON, _ := json.MarshalIndent(porc, "", " ") + t.Logf("Auxdata directory: %s", auxDir) + t.Logf("Auxdata files: region=us-east-1, tier=premium, environment=staging") + t.Logf("Bundle: %s", bundleFile) + t.Logf("Input: %s", inputFile) + t.Logf("PORC output:\n%s", string(prettyJSON)) + + // Verify auxdata values flowed into the PORC operation field + t.Logf("Checking operation field contains auxdata region...") + assert.Equal(t, "svc:http:us-east-1", porc["operation"], + "operation should contain auxdata region") + + // Verify auxdata values flowed into the PORC resource field + resource, ok := porc["resource"].(map[string]interface{}) + require.True(t, ok, "resource should be a map") + t.Logf("Checking resource.id contains auxdata tier...") + assert.Equal(t, "http://svc/premium", resource["id"], + "resource id should contain auxdata tier") + + // Verify auxdata values flowed into the PORC context field + ctx, ok := porc["context"].(map[string]interface{}) + require.True(t, ok, "context should be a map") + t.Logf("Checking context fields match auxdata values...") + assert.Equal(t, "us-east-1", ctx["region"], + "context.region should match auxdata") + assert.Equal(t, "premium", ctx["tier"], + "context.tier should match auxdata") + assert.Equal(t, "staging", ctx["environment"], + "context.environment should match auxdata") + + t.Logf("All auxdata values verified in PORC output") +} diff --git a/cmd/mpe/subcommands/test/engine.go b/cmd/mpe/subcommands/test/engine.go index 1571a89..2a4a987 100644 --- a/cmd/mpe/subcommands/test/engine.go +++ b/cmd/mpe/subcommands/test/engine.go @@ -12,15 +12,17 @@ import ( "github.com/manetu/policyengine/cmd/mpe/common" "github.com/manetu/policyengine/pkg/core" + "github.com/manetu/policyengine/pkg/core/auxdata" "github.com/urfave/cli/v3" ) type engine struct { - domain string - pe core.PolicyEngine - cmd *cli.Command - trace bool - stdout *os.File + domain string + pe core.PolicyEngine + cmd *cli.Command + trace bool + stdout *os.File + auxdata map[string]interface{} } func newEngine(cmd *cli.Command) (*engine, error) { @@ -32,12 +34,19 @@ func newEngine(cmd *cli.Command) (*engine, error) { return nil, err } + // Load auxiliary data from CLI flag if provided + aux, err := auxdata.LoadAuxData(cmd.String("auxdata")) + if err != nil { + return nil, err + } + return &engine{ - domain: cmd.String("name"), - pe: pe, - cmd: cmd, - trace: cmd.Root().Bool("trace"), - stdout: originalStdout, + domain: cmd.String("name"), + pe: pe, + cmd: cmd, + trace: cmd.Root().Bool("trace"), + stdout: originalStdout, + auxdata: aux, }, nil } @@ -56,6 +65,8 @@ func (e *engine) executeMapper(ctx context.Context) (string, error) { return "", fmt.Errorf("failed to parse Envoy input JSON: %w", err) } + auxdata.MergeAuxData(envoyInputData, e.auxdata) + porcData, perr := mapper.Evaluate(ctx, envoyInputData) if perr != nil { return "", fmt.Errorf("failed to evaluate mapper Rego: %w", perr) diff --git a/cmd/mpe/subcommands/test/test/auxdata-envoy-input.json b/cmd/mpe/subcommands/test/test/auxdata-envoy-input.json new file mode 100644 index 0000000..1cd147a --- /dev/null +++ b/cmd/mpe/subcommands/test/test/auxdata-envoy-input.json @@ -0,0 +1,13 @@ +{ + "request": { + "http": { + "method": "GET", + "path": "/api/test", + "headers": { + ":method": "GET", + ":path": "/api/test" + } + } + } +} + diff --git a/cmd/mpe/subcommands/test/test/auxdata-mapper-domain.yml b/cmd/mpe/subcommands/test/test/auxdata-mapper-domain.yml new file mode 100644 index 0000000..17db55e --- /dev/null +++ b/cmd/mpe/subcommands/test/test/auxdata-mapper-domain.yml @@ -0,0 +1,33 @@ +apiVersion: iamlite.manetu.io/v1alpha3 +kind: PolicyDomain +metadata: + name: auxdata-mapper-test + domain: auxdata-mapper-test +spec: + mappers: + - mrn: "mrn:iam:mapper:auxdata-test" + name: auxdata-test-mapper + rego: | + package mapper + + import rego.v1 + + # Mapper that reads auxiliary data from input.auxdata and includes + # it in the PORC output. This demonstrates that auxdata loaded from + # a mounted ConfigMap directory is accessible to Rego mapper code. + + porc := { + "principal": {"subject": "test-user"}, + "operation": sprintf("svc:http:%s", [input.auxdata.region]), + "resource": { + "id": sprintf("http://svc/%s", [input.auxdata.tier]), + "group": "mrn:iam:resource-group:allow-all", + }, + "context": { + "region": input.auxdata.region, + "tier": input.auxdata.tier, + "environment": input.auxdata.environment, + "original_request": input.request, + }, + } + diff --git a/docs/docs/concepts/mappers.md b/docs/docs/concepts/mappers.md index ea05084..2cab2e0 100644 --- a/docs/docs/concepts/mappers.md +++ b/docs/docs/concepts/mappers.md @@ -127,6 +127,27 @@ This example uses the simple **MRN string** format, which is the recommended app Use the **Fully Qualified Descriptor** format only when the PEP has context that the backend cannot determine. ::: +## Auxiliary Data + +Mappers can access auxiliary data (auxdata) — environment-specific key/value pairs injected at deployment time. When auxdata is configured, values are merged into the mapper input under `input.auxdata.*`: + +```rego +package mapper +import rego.v1 + +porc := { + "principal": input.claims, + "operation": sprintf("%s:http:%s", [service, method]), + "resource": sprintf("mrn:http:%s%s", [service, path]), + "context": { + "region": input.auxdata.region, + "tier": input.auxdata.tier, + } +} +``` + +Auxdata is supplied via a mounted ConfigMap in Kubernetes (see the [helm chart documentation](/deployment)) or via the `--auxdata` CLI flag for local testing. See [Configuration Reference](/reference/configuration#auxiliary-data) for details. + ## Testing Mappers Test mappers with sample inputs: diff --git a/docs/docs/reference/cli/serve.md b/docs/docs/reference/cli/serve.md index 31177a7..7d2bb6b 100644 --- a/docs/docs/reference/cli/serve.md +++ b/docs/docs/reference/cli/serve.md @@ -29,6 +29,7 @@ The `serve` command starts a gRPC/HTTP server that acts as a Policy Decision Poi | `--name` | `-n` | Domain name for multiple bundles | | | `--opa-flags` | | Additional OPA flags | `--v0-compatible` | | `--no-opa-flags` | | Disable OPA flags | | +| `--auxdata` | | Directory of auxiliary data files to merge into mapper input | | ## Examples diff --git a/docs/docs/reference/cli/test.md b/docs/docs/reference/cli/test.md index 0bf66c6..c84f5d1 100644 --- a/docs/docs/reference/cli/test.md +++ b/docs/docs/reference/cli/test.md @@ -241,11 +241,15 @@ Test mapper transformation of external input to PORC. | `--name` | `-n` | Domain name when using multiple bundles | | `--opa-flags` | | Additional OPA flags | | `--no-opa-flags` | | Disable OPA flags | +| `--auxdata` | | Directory of auxiliary data files to merge into mapper input | ### Example ```bash mpe test mapper -b my-domain.yml -i envoy-input.json + +# With auxiliary data +mpe test mapper -b my-domain.yml -i envoy-input.json --auxdata ./auxdata/ ``` ### Input Format (Envoy-style) @@ -311,11 +315,15 @@ Execute the complete pipeline: Envoy input → mapper → PORC → decision. | `--name` | `-n` | Domain name when using multiple bundles | | `--opa-flags` | | Additional OPA flags | | `--no-opa-flags` | | Disable OPA flags | +| `--auxdata` | | Directory of auxiliary data files to merge into mapper input | ### Example ```bash mpe test envoy -b my-domain.yml -i envoy-request.json + +# With auxiliary data +mpe test envoy -b my-domain.yml -i envoy-request.json --auxdata ./auxdata/ ``` ### Output diff --git a/docs/docs/reference/configuration.md b/docs/docs/reference/configuration.md index 22d0900..ce34214 100644 --- a/docs/docs/reference/configuration.md +++ b/docs/docs/reference/configuration.md @@ -28,6 +28,7 @@ Environment variables and configuration options for the Manetu PolicyEngine. |-----------------------|--------------------------|-------------------| | `MPE_CONFIG_PATH` | Path to config directory | `.` | | `MPE_CONFIG_FILENAME` | Config file name | `mpe-config.yaml` | +| `MPE_AUXDATA_PATH` | Directory containing auxiliary data files | (not set) | ## Configuration File @@ -179,6 +180,21 @@ Or via environment variable: `MPE_AUDIT_K8S_PODINFO=/custom/path/podinfo` - Entries with unknown types are skipped with a warning - Changes to values after startup will not be reflected until the PolicyEngine is restarted +## Auxiliary Data + +Auxiliary data (auxdata) lets you inject environment-specific key/value pairs into mapper input. When `MPE_AUXDATA_PATH` points to a directory, each file in that directory becomes a key (filename) with its content as the value. These are merged into the mapper input under `input.auxdata.*`. + +In Kubernetes, this directory is typically a mounted ConfigMap. The helm chart handles this automatically when `sidecar.auxdata` is configured. + +For the CLI, use the `--auxdata` flag: + +```bash +mpe serve -b domain.yml --auxdata /path/to/auxdata/dir +mpe test mapper -b domain.yml -i input.json --auxdata /path/to/auxdata/dir +``` + +Mapper Rego code can then reference values like `input.auxdata.region`, `input.auxdata.tier`, etc. + ## OPA Flags Default OPA flags used by the CLI: `--v0-compatible` diff --git a/pkg/core/auxdata/auxdata.go b/pkg/core/auxdata/auxdata.go new file mode 100644 index 0000000..ba01196 --- /dev/null +++ b/pkg/core/auxdata/auxdata.go @@ -0,0 +1,76 @@ +// +// Copyright © Manetu Inc. All rights reserved. +// + +// Package auxdata provides functionality for loading auxiliary data from +// a directory of files. When mounted from a Kubernetes ConfigMap, each key +// in the ConfigMap becomes a file in the directory. The contents of each +// file are loaded as the value in a map keyed by filename. +// +// The loaded auxdata is merged into the mapper input under the "auxdata" key, +// making it accessible to Rego policies as input.auxdata.. +package auxdata + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +// LoadAuxData reads all files in the given directory and returns a map +// where each key is the filename (without path) and each value is the +// file's content as a string. Hidden files (starting with ".") are skipped. +// +// Returns nil if path is empty (auxdata not configured). +// Returns an error if the directory cannot be read or any file fails to read. +func LoadAuxData(path string) (map[string]interface{}, error) { + if path == "" { + return nil, nil + } + + entries, err := os.ReadDir(path) + if err != nil { + return nil, fmt.Errorf("failed to read auxdata directory %s: %w", path, err) + } + + result := make(map[string]interface{}) + for _, entry := range entries { + if entry.IsDir() { + continue + } + + name := entry.Name() + // Skip hidden files (e.g., Kubernetes ConfigMap metadata files) + if strings.HasPrefix(name, ".") { + continue + } + + data, err := os.ReadFile(filepath.Join(path, name)) // #nosec G304 -- intentionally reads from configured path + if err != nil { + return nil, fmt.Errorf("failed to read auxdata file %s: %w", name, err) + } + + result[name] = string(data) + } + + return result, nil +} + +// MergeAuxData merges auxdata into the given input map under the "auxdata" key. +// If auxdata is nil or empty, the input is returned unchanged. +// If the input is a map[string]interface{}, auxdata is added as input["auxdata"]. +// For other input types, the input is returned unchanged. +func MergeAuxData(input interface{}, auxdata map[string]interface{}) interface{} { + if len(auxdata) == 0 { + return input + } + + if m, ok := input.(map[string]interface{}); ok { + m["auxdata"] = auxdata + return m + } + + return input +} + diff --git a/pkg/core/auxdata/auxdata_test.go b/pkg/core/auxdata/auxdata_test.go new file mode 100644 index 0000000..c4ee856 --- /dev/null +++ b/pkg/core/auxdata/auxdata_test.go @@ -0,0 +1,131 @@ +// +// Copyright © Manetu Inc. All rights reserved. +// + +package auxdata + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLoadAuxData_EmptyPath(t *testing.T) { + result, err := LoadAuxData("") + assert.NoError(t, err) + assert.Nil(t, result) +} + +func TestLoadAuxData_NonexistentDir(t *testing.T) { + _, err := LoadAuxData("/nonexistent/path") + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to read auxdata directory") +} + +func TestLoadAuxData_EmptyDir(t *testing.T) { + dir := t.TempDir() + + result, err := LoadAuxData(dir) + require.NoError(t, err) + assert.NotNil(t, result) + assert.Empty(t, result) +} + +func TestLoadAuxData_WithFiles(t *testing.T) { + dir := t.TempDir() + + require.NoError(t, os.WriteFile(filepath.Join(dir, "region"), []byte("us-east-1"), 0644)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "tier"), []byte("premium"), 0644)) + + result, err := LoadAuxData(dir) + require.NoError(t, err) + assert.Len(t, result, 2) + assert.Equal(t, "us-east-1", result["region"]) + assert.Equal(t, "premium", result["tier"]) +} + +func TestLoadAuxData_SkipsHiddenFiles(t *testing.T) { + dir := t.TempDir() + + require.NoError(t, os.WriteFile(filepath.Join(dir, "visible"), []byte("data"), 0644)) + require.NoError(t, os.WriteFile(filepath.Join(dir, ".hidden"), []byte("secret"), 0644)) + + result, err := LoadAuxData(dir) + require.NoError(t, err) + assert.Len(t, result, 1) + assert.Equal(t, "data", result["visible"]) + _, exists := result[".hidden"] + assert.False(t, exists) +} + +func TestLoadAuxData_SkipsSubdirectories(t *testing.T) { + dir := t.TempDir() + + require.NoError(t, os.WriteFile(filepath.Join(dir, "key"), []byte("value"), 0644)) + require.NoError(t, os.Mkdir(filepath.Join(dir, "subdir"), 0755)) + + result, err := LoadAuxData(dir) + require.NoError(t, err) + assert.Len(t, result, 1) + assert.Equal(t, "value", result["key"]) +} + +func TestMergeAuxData_NilAuxData(t *testing.T) { + input := map[string]interface{}{"key": "value"} + result := MergeAuxData(input, nil) + + m, ok := result.(map[string]interface{}) + require.True(t, ok) + assert.Equal(t, "value", m["key"]) + _, exists := m["auxdata"] + assert.False(t, exists) +} + +func TestMergeAuxData_EmptyAuxData(t *testing.T) { + input := map[string]interface{}{"key": "value"} + result := MergeAuxData(input, map[string]interface{}{}) + + m, ok := result.(map[string]interface{}) + require.True(t, ok) + _, exists := m["auxdata"] + assert.False(t, exists) +} + +func TestMergeAuxData_MergesIntoMap(t *testing.T) { + input := map[string]interface{}{ + "request": "data", + } + aux := map[string]interface{}{ + "region": "us-east-1", + "tier": "premium", + } + + result := MergeAuxData(input, aux) + + m, ok := result.(map[string]interface{}) + require.True(t, ok) + assert.Equal(t, "data", m["request"]) + + auxResult, ok := m["auxdata"].(map[string]interface{}) + require.True(t, ok) + assert.Equal(t, "us-east-1", auxResult["region"]) + assert.Equal(t, "premium", auxResult["tier"]) +} + +func TestMergeAuxData_NonMapInput(t *testing.T) { + input := "string-input" + aux := map[string]interface{}{"key": "value"} + + result := MergeAuxData(input, aux) + assert.Equal(t, "string-input", result) +} + +func TestMergeAuxData_NilInput(t *testing.T) { + aux := map[string]interface{}{"key": "value"} + result := MergeAuxData(nil, aux) + assert.Nil(t, result) +} + diff --git a/pkg/core/config/config.go b/pkg/core/config/config.go index 6bba86c..c054117 100644 --- a/pkg/core/config/config.go +++ b/pkg/core/config/config.go @@ -170,6 +170,14 @@ const ( // Default: "/etc/podinfo" // Set via environment: MPE_AUDIT_K8S_PODINFO=/custom/path AuditK8sPodinfo string = "audit.k8s.podinfo" + + // AuxDataPath specifies the directory where auxiliary data files are mounted. + // When set, all files in this directory are loaded and made available to + // the mapper as input.auxdata, allowing Rego policies to reference + // external configuration data. + // + // Set via environment: MPE_AUXDATA_PATH=/etc/mpe/auxdata + AuxDataPath string = "auxdata.path" ) var ( diff --git a/pkg/decisionpoint/envoy/envoy.go b/pkg/decisionpoint/envoy/envoy.go index b8ba444..3f21778 100644 --- a/pkg/decisionpoint/envoy/envoy.go +++ b/pkg/decisionpoint/envoy/envoy.go @@ -16,6 +16,7 @@ import ( authv3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" typev3 "github.com/envoyproxy/go-control-plane/envoy/type/v3" "github.com/manetu/policyengine/internal/logging" + "github.com/manetu/policyengine/pkg/core/auxdata" "github.com/manetu/policyengine/pkg/core/backend" "github.com/manetu/policyengine/pkg/decisionpoint" "google.golang.org/genproto/googleapis/rpc/status" @@ -51,6 +52,7 @@ type ExtAuthzServer struct { pe core.PolicyEngine be backend.Service domain string + auxdata map[string]interface{} // For test only grpcPort chan int @@ -130,6 +132,8 @@ func (s *ExtAuthzServer) Check(ctx context.Context, request *authv3.CheckRequest return nil, err } + auxdata.MergeAuxData(mattrs, s.auxdata) + mapper, perr := s.be.GetMapper(ctx, s.domain) if perr != nil { return nil, perr @@ -189,12 +193,14 @@ func (s *ExtAuthzServer) run(grpcAddr string) { // CreateServer creates and starts a new Envoy External Authorization server. // It returns a Server interface that implements the decisionpoint.Server interface. -func CreateServer(pe core.PolicyEngine, port int, domain string) (decisionpoint.Server, error) { +// The auxdata parameter, if non-nil, is merged into the mapper input under the "auxdata" key. +func CreateServer(pe core.PolicyEngine, port int, domain string, aux map[string]interface{}) (decisionpoint.Server, error) { s := &ExtAuthzServer{ grpcPort: make(chan int, 1), pe: pe, be: pe.GetBackend(), domain: domain, + auxdata: aux, } go s.run(fmt.Sprintf(":%d", port)) diff --git a/pkg/decisionpoint/envoy/envoy_test.go b/pkg/decisionpoint/envoy/envoy_test.go index 03f9e23..8515009 100644 --- a/pkg/decisionpoint/envoy/envoy_test.go +++ b/pkg/decisionpoint/envoy/envoy_test.go @@ -118,7 +118,7 @@ func TestEnvoyServer_CreateServer(t *testing.T) { pe := setupTestPolicyEngine(t) port := findFreePort(t) - server, err := CreateServer(pe, port, "") + server, err := CreateServer(pe, port, "", nil) require.NoError(t, err) require.NotNil(t, server) @@ -138,7 +138,7 @@ func TestEnvoyServer_Check_Allow(t *testing.T) { pe := setupTestPolicyEngine(t) port := findFreePort(t) - server, err := CreateServer(pe, port, "") + server, err := CreateServer(pe, port, "", nil) require.NoError(t, err) require.NotNil(t, server) @@ -211,7 +211,7 @@ func TestEnvoyServer_Check_Deny(t *testing.T) { pe := setupTestPolicyEngine(t) port := findFreePort(t) - server, err := CreateServer(pe, port, "") + server, err := CreateServer(pe, port, "", nil) require.NoError(t, err) require.NotNil(t, server) @@ -288,7 +288,7 @@ func TestEnvoyServer_Check_NoMapper(t *testing.T) { // Clear mapper config to test error handling config.VConfig.Set("mock.domain.mappers", nil) - server, err := CreateServer(pe, port, "") + server, err := CreateServer(pe, port, "", nil) require.NoError(t, err) require.NotNil(t, server) @@ -334,7 +334,7 @@ func TestEnvoyServer_Stop(t *testing.T) { pe := setupTestPolicyEngine(t) port := findFreePort(t) - server, err := CreateServer(pe, port, "") + server, err := CreateServer(pe, port, "", nil) require.NoError(t, err) require.NotNil(t, server) From e3400878d40df9522c9b3927436488ba074f52e2 Mon Sep 17 00:00:00 2001 From: IvanPazManetu Date: Wed, 11 Feb 2026 16:02:09 -0500 Subject: [PATCH 2/4] goimports and doc lint fix Signed-off-by: IvanPazManetu --- docs/cspell.json | 4 +++- pkg/core/auxdata/auxdata.go | 1 - pkg/core/auxdata/auxdata_test.go | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/cspell.json b/docs/cspell.json index abd7a62..dfa3daa 100644 --- a/docs/cspell.json +++ b/docs/cspell.json @@ -150,7 +150,9 @@ "deidentified", "Yzlk", "annot", - "podinfo" + "podinfo", + "auxdata", + "AUXDATA" ], "flagWords": [], "dictionaries": [ diff --git a/pkg/core/auxdata/auxdata.go b/pkg/core/auxdata/auxdata.go index ba01196..2b13dbb 100644 --- a/pkg/core/auxdata/auxdata.go +++ b/pkg/core/auxdata/auxdata.go @@ -73,4 +73,3 @@ func MergeAuxData(input interface{}, auxdata map[string]interface{}) interface{} return input } - diff --git a/pkg/core/auxdata/auxdata_test.go b/pkg/core/auxdata/auxdata_test.go index c4ee856..b7c560e 100644 --- a/pkg/core/auxdata/auxdata_test.go +++ b/pkg/core/auxdata/auxdata_test.go @@ -128,4 +128,3 @@ func TestMergeAuxData_NilInput(t *testing.T) { result := MergeAuxData(nil, aux) assert.Nil(t, result) } - From c4f8428201840ba2c81449030b7203cebb49d43b Mon Sep 17 00:00:00 2001 From: IvanPazManetu Date: Wed, 11 Feb 2026 16:14:54 -0500 Subject: [PATCH 3/4] UT Coverage Signed-off-by: IvanPazManetu --- cmd/mpe/subcommands/test/core_test.go | 42 +++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/cmd/mpe/subcommands/test/core_test.go b/cmd/mpe/subcommands/test/core_test.go index d846acc..7ee5d66 100644 --- a/cmd/mpe/subcommands/test/core_test.go +++ b/cmd/mpe/subcommands/test/core_test.go @@ -199,6 +199,7 @@ func buildTestCommand(action cli.ActionFunc) *cli.Command { &cli.StringFlag{Name: "input", Aliases: []string{"i"}}, &cli.StringSliceFlag{Name: "bundle", Aliases: []string{"b"}}, &cli.StringFlag{Name: "name", Aliases: []string{"n"}}, + &cli.StringFlag{Name: "auxdata"}, }, Action: action, }, @@ -368,6 +369,47 @@ func TestExecuteEnvoy_InvalidBundle(t *testing.T) { assert.Error(t, err, "ExecuteEnvoy should fail with non-existent bundle file") } +// TestExecuteDecision_WithTraceEnabled tests the decision command with trace enabled +// to cover the trace branch in executeDecision +func TestExecuteDecision_WithTraceEnabled(t *testing.T) { + bundleFile := testDataPath("consolidated.yml") + inputFile := testDataPath("example-porc-input.json") + + // Verify test files exist + require.FileExists(t, bundleFile, "consolidated.yml should exist") + require.FileExists(t, inputFile, "example-porc-input.json should exist") + + cmd := buildTestCommand(ExecuteDecision) + args := []string{"mpe", "--trace", "test", "decision", "-i", inputFile, "-b", bundleFile} + + // Save original stdout so we can verify it's restored after close() + originalStdout := os.Stdout + defer func() { os.Stdout = originalStdout }() + + err := cmd.Run(context.Background(), args) + assert.NoError(t, err, "ExecuteDecision should succeed with trace enabled") +} + +// TestExecuteEnvoy_WithTrace tests the envoy command with trace enabled +func TestExecuteEnvoy_WithTrace(t *testing.T) { + bundleFile := testDataPath("consolidated.yml") + inputFile := testDataPath("envoy.json") + + // Verify test files exist + require.FileExists(t, bundleFile, "consolidated.yml should exist") + require.FileExists(t, inputFile, "envoy.json should exist") + + cmd := buildTestCommand(ExecuteEnvoy) + args := []string{"mpe", "--trace", "test", "envoy", "-i", inputFile, "-b", bundleFile} + + // Save original stdout so we can verify it's restored after close() + originalStdout := os.Stdout + defer func() { os.Stdout = originalStdout }() + + err := cmd.Run(context.Background(), args) + assert.NoError(t, err, "ExecuteEnvoy should succeed with trace enabled") +} + // TestExecuteMapper_WithAuxData tests that auxdata merged into the mapper input // is accessible to Rego code when computing the PORC. This exercises the full // CLI pipeline: --auxdata dir -> LoadAuxData -> MergeAuxData -> mapper.Evaluate From 93b976297a85e7571895c84c3e3dddc72fcad2a9 Mon Sep 17 00:00:00 2001 From: IvanPazManetu Date: Wed, 11 Feb 2026 16:24:34 -0500 Subject: [PATCH 4/4] Codecov UT Coverage Signed-off-by: IvanPazManetu --- cmd/mpe/subcommands/test/core_test.go | 33 +++++++++++++++++++++++++++ pkg/core/auxdata/auxdata_test.go | 13 +++++++++++ 2 files changed, 46 insertions(+) diff --git a/cmd/mpe/subcommands/test/core_test.go b/cmd/mpe/subcommands/test/core_test.go index 7ee5d66..570214a 100644 --- a/cmd/mpe/subcommands/test/core_test.go +++ b/cmd/mpe/subcommands/test/core_test.go @@ -410,6 +410,39 @@ func TestExecuteEnvoy_WithTrace(t *testing.T) { assert.NoError(t, err, "ExecuteEnvoy should succeed with trace enabled") } +// TestExecuteMapper_InvalidAuxDataPath tests that an invalid auxdata path returns an error +// This covers the error path in newEngine when LoadAuxData fails (engine.go lines 39-41) +func TestExecuteMapper_InvalidAuxDataPath(t *testing.T) { + bundleFile := testDataPath("consolidated.yml") + inputFile := testDataPath("envoy.json") + + require.FileExists(t, bundleFile) + require.FileExists(t, inputFile) + + cmd := buildTestCommand(ExecuteMapper) + args := []string{"mpe", "test", "mapper", "-i", inputFile, "-b", bundleFile, "--auxdata", "/nonexistent/auxdata/path"} + + err := cmd.Run(context.Background(), args) + assert.Error(t, err, "ExecuteMapper should fail with invalid auxdata path") + assert.Contains(t, err.Error(), "failed to read auxdata directory") +} + +// TestExecuteEnvoy_InvalidAuxDataPath tests that an invalid auxdata path returns an error for envoy command +func TestExecuteEnvoy_InvalidAuxDataPath(t *testing.T) { + bundleFile := testDataPath("consolidated.yml") + inputFile := testDataPath("envoy.json") + + require.FileExists(t, bundleFile) + require.FileExists(t, inputFile) + + cmd := buildTestCommand(ExecuteEnvoy) + args := []string{"mpe", "test", "envoy", "-i", inputFile, "-b", bundleFile, "--auxdata", "/nonexistent/auxdata/path"} + + err := cmd.Run(context.Background(), args) + assert.Error(t, err, "ExecuteEnvoy should fail with invalid auxdata path") + assert.Contains(t, err.Error(), "failed to read auxdata directory") +} + // TestExecuteMapper_WithAuxData tests that auxdata merged into the mapper input // is accessible to Rego code when computing the PORC. This exercises the full // CLI pipeline: --auxdata dir -> LoadAuxData -> MergeAuxData -> mapper.Evaluate diff --git a/pkg/core/auxdata/auxdata_test.go b/pkg/core/auxdata/auxdata_test.go index b7c560e..9266183 100644 --- a/pkg/core/auxdata/auxdata_test.go +++ b/pkg/core/auxdata/auxdata_test.go @@ -73,6 +73,19 @@ func TestLoadAuxData_SkipsSubdirectories(t *testing.T) { assert.Equal(t, "value", result["key"]) } +func TestLoadAuxData_UnreadableFile(t *testing.T) { + dir := t.TempDir() + + // Create a readable file and an unreadable file + require.NoError(t, os.WriteFile(filepath.Join(dir, "readable"), []byte("ok"), 0644)) + unreadable := filepath.Join(dir, "unreadable") + require.NoError(t, os.WriteFile(unreadable, []byte("secret"), 0000)) + + _, err := LoadAuxData(dir) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to read auxdata file") +} + func TestMergeAuxData_NilAuxData(t *testing.T) { input := map[string]interface{}{"key": "value"} result := MergeAuxData(input, nil)