Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions cmd/mpe/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down Expand Up @@ -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,
},
Expand Down Expand Up @@ -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,
},
Expand Down
14 changes: 13 additions & 1 deletion cmd/mpe/subcommands/serve/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down
166 changes: 166 additions & 0 deletions cmd/mpe/subcommands/test/core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package test

import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
Expand Down Expand Up @@ -198,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,
},
Expand All @@ -209,6 +211,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,
},
Expand All @@ -220,6 +223,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,
},
Expand Down Expand Up @@ -364,3 +368,165 @@ func TestExecuteEnvoy_InvalidBundle(t *testing.T) {
err := cmd.Run(context.Background(), args)
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_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
// -> 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")
}
31 changes: 21 additions & 10 deletions cmd/mpe/subcommands/test/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
}

Expand All @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions cmd/mpe/subcommands/test/test/auxdata-envoy-input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"request": {
"http": {
"method": "GET",
"path": "/api/test",
"headers": {
":method": "GET",
":path": "/api/test"
}
}
}
}

33 changes: 33 additions & 0 deletions cmd/mpe/subcommands/test/test/auxdata-mapper-domain.yml
Original file line number Diff line number Diff line change
@@ -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,
},
}

4 changes: 3 additions & 1 deletion docs/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,9 @@
"deidentified",
"Yzlk",
"annot",
"podinfo"
"podinfo",
"auxdata",
"AUXDATA"
],
"flagWords": [],
"dictionaries": [
Expand Down
Loading
Loading