diff --git a/cmd/policy/policy.go b/cmd/policy/policy.go index 3a47ea2f1..c2d989156 100644 --- a/cmd/policy/policy.go +++ b/cmd/policy/policy.go @@ -261,6 +261,7 @@ This group of commands allows the management of polices to be verified against b var ( inputPath string policyPath string + meta string metaFile string ownerID string context string @@ -284,15 +285,9 @@ This group of commands allows the management of polices to be verified against b return fmt.Errorf("failed to read input file: %w", err) } - var metadata map[string]interface{} - if metaFile != "" { - raw, err := os.ReadFile(metaFile) - if err != nil { - return fmt.Errorf("failed to read meta file: %w", err) - } - if err := yaml.Unmarshal(raw, &metadata); err != nil { - return fmt.Errorf("failed to decode meta content: %w", err) - } + metadata, err := readMetadata(meta, metaFile) + if err != nil { + return fmt.Errorf("failed to read metadata: %w", err) } decision, err := func() (*cpa.Decision, error) { @@ -324,6 +319,7 @@ This group of commands allows the management of polices to be verified against b cmd.Flags().StringVar(&ownerID, "owner-id", "", "the id of the policy's owner") cmd.Flags().StringVar(&context, "context", "config", "policy context for decision") cmd.Flags().StringVar(&inputPath, "input", "", "path to input file") + cmd.Flags().StringVar(&meta, "meta", "", "decision metadata (json string)") cmd.Flags().StringVar(&metaFile, "metafile", "", "decision metadata file") cmd.Flags().BoolVar(&strict, "strict", false, "return non-zero status code for decision resulting in HARD_FAIL") @@ -335,7 +331,7 @@ This group of commands allows the management of polices to be verified against b }() eval := func() *cobra.Command { - var inputPath, metaFile, query string + var inputPath, meta, metaFile, query string cmd := &cobra.Command{ Short: "perform raw opa evaluation locally", Use: "eval ", @@ -346,15 +342,9 @@ This group of commands allows the management of polices to be verified against b return fmt.Errorf("failed to read input file: %w", err) } - var metadata map[string]interface{} - if metaFile != "" { - raw, err := os.ReadFile(metaFile) - if err != nil { - return fmt.Errorf("failed to read meta file: %w", err) - } - if err := yaml.Unmarshal(raw, &metadata); err != nil { - return fmt.Errorf("failed to decode meta content: %w", err) - } + metadata, err := readMetadata(meta, metaFile) + if err != nil { + return fmt.Errorf("failed to read metadata: %w", err) } decision, err := getPolicyEvaluationLocally(policyPath, input, metadata, query) @@ -373,6 +363,7 @@ This group of commands allows the management of polices to be verified against b } cmd.Flags().StringVar(&inputPath, "input", "", "path to input file") + cmd.Flags().StringVar(&meta, "meta", "", "decision metadata (json string)") cmd.Flags().StringVar(&metaFile, "metafile", "", "decision metadata file") cmd.Flags().StringVar(&query, "query", "data", "policy decision query") @@ -510,6 +501,28 @@ This group of commands allows the management of polices to be verified against b return cmd } +func readMetadata(meta string, metaFile string) (map[string]interface{}, error) { + var metadata map[string]interface{} + if meta != "" && metaFile != "" { + return nil, fmt.Errorf("use either --meta or --metafile flag, but not both") + } + if meta != "" { + if err := json.Unmarshal([]byte(meta), &metadata); err != nil { + return nil, fmt.Errorf("failed to decode meta content: %w", err) + } + } + if metaFile != "" { + raw, err := os.ReadFile(metaFile) + if err != nil { + return nil, fmt.Errorf("failed to read meta file: %w", err) + } + if err := yaml.Unmarshal(raw, &metadata); err != nil { + return nil, fmt.Errorf("failed to decode metafile content: %w", err) + } + } + return metadata, nil +} + // prettyJSONEncoder takes a writer and returns a new json encoder with indent set to two space characters func prettyJSONEncoder(dst io.Writer) *json.Encoder { enc := json.NewEncoder(dst) diff --git a/cmd/policy/policy_test.go b/cmd/policy/policy_test.go index cf98e1a10..0c833c241 100644 --- a/cmd/policy/policy_test.go +++ b/cmd/policy/policy_test.go @@ -698,7 +698,29 @@ func TestMakeDecisionCommand(t *testing.T) { ExpectedOutput: "{\n \"status\": \"PASS\"\n}\n", }, { - Name: "sends expected request with metadata", + Name: "sends expected request with meta", + Args: []string{"decide", "--owner-id", "test-owner", "--input", "./testdata/test1/test.yml", "--context", "custom", "--meta", `{"project_id": "test-project-id","vcs": {"branch": "main"}}`}, + ServerHandler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "POST") + assert.Equal(t, r.URL.Path, "/api/v1/owner/test-owner/context/custom/decision") + + var payload map[string]interface{} + assert.NilError(t, json.NewDecoder(r.Body).Decode(&payload)) + + assert.DeepEqual(t, payload, map[string]interface{}{ + "input": "test: config\n", + "metadata": map[string]interface{}{ + "project_id": "test-project-id", + "vcs": map[string]any{"branch": "main"}, + }, + }) + + _, _ = io.WriteString(w, `{"status":"PASS"}`) + }, + ExpectedOutput: "{\n \"status\": \"PASS\"\n}\n", + }, + { + Name: "sends expected request with metafile", Args: []string{"decide", "--owner-id", "test-owner", "--input", "./testdata/test1/test.yml", "--context", "custom", "--metafile", "./testdata/test1/meta.yml"}, ServerHandler: func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, r.Method, "POST") @@ -749,6 +771,11 @@ func TestMakeDecisionCommand(t *testing.T) { Args: []string{"decide", "./testdata/no_such_file.rego", "--input", "./testdata/test1/test.yml"}, ExpectedErr: "failed to make decision: failed to load policy files: failed to walk root: ", }, + { + Name: "fails if both meta and metafile are provided", + Args: []string{"decide", "./testdata/test0/policy.rego", "--input", "./testdata/test1/test.yml", "--meta", "{}", "--metafile", "somefile"}, + ExpectedErr: "failed to read metadata: use either --meta or --metafile flag, but not both", + }, { Name: "successfully performs decision for policy FILE provided locally", Args: []string{"decide", "./testdata/test0/policy.rego", "--input", "./testdata/test0/config.yml"}, @@ -797,7 +824,21 @@ func TestMakeDecisionCommand(t *testing.T) { ExpectedErr: "policy decision status: ERROR", }, { - Name: "successfully performs decision with metadata for policy FILE provided locally", + Name: "successfully performs decision with meta for policy FILE provided locally", + Args: []string{ + "decide", "./testdata/test0/subdir/meta-policy-subdir/meta-policy.rego", "--meta", + `{"project_id": "test-project-id","vcs": {"branch": "main"}}`, "--input", "./testdata/test0/config.yml", + }, + ExpectedOutput: `{ + "status": "PASS", + "enabled_rules": [ + "enabled" + ] +} +`, + }, + { + Name: "successfully performs decision with metafile for policy FILE provided locally", Args: []string{ "decide", "./testdata/test0/subdir/meta-policy-subdir/meta-policy.rego", "--metafile", "./testdata/test1/meta.yml", "--input", "./testdata/test0/config.yml", @@ -866,7 +907,37 @@ func TestRawOPAEvaluationCommand(t *testing.T) { ExpectedErr: "failed to make decision: failed to load policy files: failed to walk root: ", }, { - Name: "successfully performs raw opa evaluation for policy FILE provided locally, input and metadata", + Name: "fails if both meta and metafile are provided", + Args: []string{"eval", "./testdata/test0/policy.rego", "--input", "./testdata/test1/test.yml", "--meta", "{}", "--metafile", "somefile"}, + ExpectedErr: "failed to read metadata: use either --meta or --metafile flag, but not both", + }, + { + Name: "successfully performs raw opa evaluation for policy FILE provided locally, input and meta", + Args: []string{ + "eval", "./testdata/test0/subdir/meta-policy-subdir/meta-policy.rego", + "--meta", `{"project_id": "test-project-id","vcs": {"branch": "main"}}`, + "--input", "./testdata/test0/config.yml", + }, + ExpectedOutput: `{ + "meta": { + "vcs": { + "branch": "main" + }, + "project_id": "test-project-id" + }, + "org": { + "enable_rule": [ + "enabled" + ], + "policy_name": [ + "meta_policy_test" + ] + } +} +`, + }, + { + Name: "successfully performs raw opa evaluation for policy FILE provided locally, input and metafile", Args: []string{ "eval", "./testdata/test0/subdir/meta-policy-subdir/meta-policy.rego", "--metafile", "./testdata/test1/meta.yml", @@ -891,7 +962,20 @@ func TestRawOPAEvaluationCommand(t *testing.T) { `, }, { - Name: "successfully performs raw opa evaluation for policy FILE provided locally, input, metadata and query", + Name: "successfully performs raw opa evaluation for policy FILE provided locally, input, meta and query", + Args: []string{ + "eval", "./testdata/test0/subdir/meta-policy-subdir/meta-policy.rego", + "--meta", `{"project_id": "test-project-id","vcs": {"branch": "main"}}`, + "--input", "./testdata/test0/config.yml", + "--query", "data.org.enable_rule", + }, + ExpectedOutput: `[ + "enabled" +] +`, + }, + { + Name: "successfully performs raw opa evaluation for policy FILE provided locally, input, metafile and query", Args: []string{ "eval", "./testdata/test0/subdir/meta-policy-subdir/meta-policy.rego", "--metafile", "./testdata/test1/meta.yml", diff --git a/cmd/policy/testdata/policy/decide-expected-usage.txt b/cmd/policy/testdata/policy/decide-expected-usage.txt index cd6f3bf3e..20307a876 100644 --- a/cmd/policy/testdata/policy/decide-expected-usage.txt +++ b/cmd/policy/testdata/policy/decide-expected-usage.txt @@ -7,6 +7,7 @@ policy decide ./policies --input ./.circleci/config.yml Flags: --context string policy context for decision (default "config") --input string path to input file + --meta string decision metadata (json string) --metafile string decision metadata file --owner-id string the id of the policy's owner --strict return non-zero status code for decision resulting in HARD_FAIL diff --git a/cmd/policy/testdata/policy/eval-expected-usage.txt b/cmd/policy/testdata/policy/eval-expected-usage.txt index 68624f672..9f4e23364 100644 --- a/cmd/policy/testdata/policy/eval-expected-usage.txt +++ b/cmd/policy/testdata/policy/eval-expected-usage.txt @@ -6,6 +6,7 @@ policy eval ./policies --input ./.circleci/config.yml Flags: --input string path to input file + --meta string decision metadata (json string) --metafile string decision metadata file --query string policy decision query (default "data")