mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-15 12:52:44 -06:00
3fda875eee
This corrects an issue where data.json (or yaml) files in a bundle would overwrite data if the file was at the root of the bundle directory and was loaded after other data files. This also helps avoid any issues with merging data files that could have overlapped without any problems. Fixes: #1763 Signed-off-by: Patrick East <east.patrick@gmail.com>
377 lines
8.5 KiB
Go
377 lines
8.5 KiB
Go
// Copyright 2018 The OPA Authors. All rights reserved.
|
|
// Use of this source code is governed by an Apache2
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package bundle
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/open-policy-agent/opa/ast"
|
|
"github.com/open-policy-agent/opa/internal/file/archive"
|
|
)
|
|
|
|
func TestRead(t *testing.T) {
|
|
|
|
files := [][2]string{
|
|
{"/a/b/c/data.json", "[1,2,3]"},
|
|
{"/a/b/d/data.json", "true"},
|
|
{"/a/b/y/data.yaml", `foo: 1`},
|
|
{"/example/example.rego", `package example`},
|
|
{"/data.json", `{"x": {"y": true}, "a": {"b": {"z": true}}}}`},
|
|
}
|
|
|
|
buf := archive.MustWriteTarGz(files)
|
|
bundle, err := NewReader(buf).Read()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
module := `package example`
|
|
|
|
exp := Bundle{
|
|
Data: map[string]interface{}{
|
|
"a": map[string]interface{}{
|
|
"b": map[string]interface{}{
|
|
"c": []interface{}{json.Number("1"), json.Number("2"), json.Number("3")},
|
|
"d": true,
|
|
"y": map[string]interface{}{
|
|
"foo": json.Number("1"),
|
|
},
|
|
"z": true,
|
|
},
|
|
},
|
|
"x": map[string]interface{}{
|
|
"y": true,
|
|
},
|
|
},
|
|
Modules: []ModuleFile{
|
|
{
|
|
Path: "/example/example.rego",
|
|
Parsed: ast.MustParseModule(module),
|
|
Raw: []byte(module),
|
|
},
|
|
},
|
|
}
|
|
|
|
if !exp.Equal(bundle) {
|
|
t.Fatal("\nExp:", exp, "\n\nGot:", bundle)
|
|
}
|
|
}
|
|
|
|
func TestReadWithManifest(t *testing.T) {
|
|
files := [][2]string{
|
|
{"/.manifest", `{"revision": "quickbrownfaux"}`},
|
|
}
|
|
buf := archive.MustWriteTarGz(files)
|
|
bundle, err := NewReader(buf).Read()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if bundle.Manifest.Revision != "quickbrownfaux" {
|
|
t.Fatalf("Unexpected manifest.revision value: %v", bundle.Manifest.Revision)
|
|
}
|
|
}
|
|
|
|
func TestReadWithManifestInData(t *testing.T) {
|
|
files := [][2]string{
|
|
{"/.manifest", `{"revision": "quickbrownfaux"}`},
|
|
}
|
|
buf := archive.MustWriteTarGz(files)
|
|
bundle, err := NewReader(buf).IncludeManifestInData(true).Read()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
system := bundle.Data["system"].(map[string]interface{})
|
|
b := system["bundle"].(map[string]interface{})
|
|
m := b["manifest"].(map[string]interface{})
|
|
|
|
if m["revision"] != "quickbrownfaux" {
|
|
t.Fatalf("Unexpected manifest.revision value: %v. Expected: %v", m["revision"], "quickbrownfaux")
|
|
}
|
|
}
|
|
|
|
func TestReadRootValidation(t *testing.T) {
|
|
cases := []struct {
|
|
note string
|
|
files [][2]string
|
|
err string
|
|
}{
|
|
{
|
|
note: "default: full extent",
|
|
files: [][2]string{
|
|
{"/.manifest", `{"revision": "abcd"}`},
|
|
{"/data.json", `{"a": 1}`},
|
|
{"/x.rego", `package foo`},
|
|
},
|
|
err: "",
|
|
},
|
|
{
|
|
note: "explicit: full extent",
|
|
files: [][2]string{
|
|
{"/.manifest", `{"revision": "abcd", "roots": [""]}`},
|
|
{"/data.json", `{"a": 1}`},
|
|
{"/x.rego", `package foo`},
|
|
},
|
|
err: "",
|
|
},
|
|
{
|
|
note: "implicit: prefixed",
|
|
files: [][2]string{
|
|
{"/.manifest", `{"revision": "abcd", "roots": ["a/b", "foo"]}`},
|
|
{"/data.json", `{"a": {"b": 1}}`},
|
|
{"/x.rego", `package foo.bar`},
|
|
},
|
|
err: "",
|
|
},
|
|
{
|
|
note: "err: empty",
|
|
files: [][2]string{
|
|
{"/.manifest", `{"revision": "abcd", "roots": []}`},
|
|
{"/x.rego", `package foo`},
|
|
},
|
|
err: "manifest roots do not permit 'package foo' in /x.rego",
|
|
},
|
|
{
|
|
note: "err: overlapped",
|
|
files: [][2]string{
|
|
{"/.manifest", `{"revision": "abcd", "roots": ["a/b", "a"]}`},
|
|
},
|
|
err: "manifest has overlapped roots: a/b and a",
|
|
},
|
|
{
|
|
note: "edge: overlapped partial segment",
|
|
files: [][2]string{
|
|
{"/.manifest", `{"revision": "abcd", "roots": ["a", "another_root"]}`},
|
|
},
|
|
err: "",
|
|
},
|
|
{
|
|
note: "err: package outside scope",
|
|
files: [][2]string{
|
|
{"/.manifest", `{"revision": "abcd", "roots": ["a", "b", "c/d"]}`},
|
|
{"/a.rego", `package b.c`},
|
|
{"/x.rego", `package c.e`},
|
|
},
|
|
err: "manifest roots do not permit 'package c.e' in /x.rego",
|
|
},
|
|
{
|
|
note: "err: data outside scope",
|
|
files: [][2]string{
|
|
{"/.manifest", `{"revision": "abcd", "roots": ["a", "b", "c/d"]}`},
|
|
{"/data.json", `{"a": 1}`},
|
|
{"/c/e/data.json", `"bad bad bad"`},
|
|
},
|
|
err: "manifest roots do not permit data at path c/e",
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
buf := archive.MustWriteTarGz(tc.files)
|
|
_, err := NewReader(buf).IncludeManifestInData(true).Read()
|
|
if tc.err == "" && err != nil {
|
|
t.Fatal("Unexpected error occurred:", err)
|
|
} else if tc.err != "" && err == nil {
|
|
t.Fatal("Expected error but got success")
|
|
} else if tc.err != "" && err != nil {
|
|
if !strings.Contains(err.Error(), tc.err) {
|
|
t.Fatalf("Expected error to contain %q but got: %v", tc.err, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
func TestReadErrorBadGzip(t *testing.T) {
|
|
buf := bytes.NewBufferString("bad gzip bytes")
|
|
_, err := NewReader(buf).Read()
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
}
|
|
|
|
func TestReadErrorBadTar(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
gw := gzip.NewWriter(&buf)
|
|
gw.Write([]byte("bad tar bytes"))
|
|
gw.Close()
|
|
_, err := NewReader(&buf).Read()
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
}
|
|
|
|
func TestReadErrorBadContents(t *testing.T) {
|
|
tests := []struct {
|
|
files [][2]string
|
|
}{
|
|
{[][2]string{{"/test.rego", "lkafjasdkljf"}}},
|
|
{[][2]string{{"/data.json", "lskjafkljsdf"}}},
|
|
{[][2]string{{"/data.json", "[1,2,3]"}}},
|
|
{[][2]string{
|
|
{"/a/b/data.json", "[1,2,3]"},
|
|
{"a/b/c/data.json", "true"},
|
|
}},
|
|
{[][2]string{{"/test.rego", ""}}},
|
|
{[][2]string{
|
|
{"/a/b/data.json", `{"c": "foo"}`},
|
|
{"/data.json", `{"a": {"b": {"c": [123]}}}`},
|
|
}},
|
|
}
|
|
for _, test := range tests {
|
|
buf := archive.MustWriteTarGz(test.files)
|
|
_, err := NewReader(buf).Read()
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func TestRoundtrip(t *testing.T) {
|
|
|
|
bundle := Bundle{
|
|
Data: map[string]interface{}{
|
|
"foo": map[string]interface{}{
|
|
"bar": []interface{}{json.Number("1"), json.Number("2"), json.Number("3")},
|
|
"baz": true,
|
|
"qux": "hello",
|
|
},
|
|
},
|
|
Modules: []ModuleFile{
|
|
{
|
|
Path: "/foo/corge/corge.rego",
|
|
Parsed: ast.MustParseModule(`package foo.corge`),
|
|
Raw: []byte(`package foo.corge`),
|
|
},
|
|
},
|
|
Manifest: Manifest{
|
|
Revision: "quickbrownfaux",
|
|
},
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
|
|
if err := Write(&buf, bundle); err != nil {
|
|
t.Fatal("Unexpected error:", err)
|
|
}
|
|
|
|
bundle2, err := NewReader(&buf).Read()
|
|
if err != nil {
|
|
t.Fatal("Unexpected error:", err)
|
|
}
|
|
|
|
if !bundle2.Equal(bundle) {
|
|
t.Fatal("Exp:", bundle, "\n\nGot:", bundle2)
|
|
}
|
|
|
|
}
|
|
|
|
func TestRootPathsOverlap(t *testing.T) {
|
|
cases := []struct {
|
|
note string
|
|
rootA string
|
|
rootB string
|
|
expected bool
|
|
}{
|
|
{"both empty", "", "", true},
|
|
{"a empty", "", "foo/bar", true},
|
|
{"b empty", "foo/bar", "", true},
|
|
{"no overlap", "a/b/c", "x/y", false},
|
|
{"partial segment overlap a", "a/b", "a/banana", false},
|
|
{"partial segment overlap b", "a/banana", "a/b", false},
|
|
{"overlap a", "a/b", "a/b/c", true},
|
|
{"overlap b", "a/b/c", "a/b", true},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
actual := RootPathsOverlap(tc.rootA, tc.rootB)
|
|
if actual != tc.expected {
|
|
t.Errorf("Expected %t, got %t", tc.expected, actual)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParsedModules(t *testing.T) {
|
|
cases := []struct {
|
|
note string
|
|
bundle Bundle
|
|
name string
|
|
expectedModules []string
|
|
}{
|
|
{
|
|
note: "base",
|
|
bundle: Bundle{
|
|
Modules: []ModuleFile{
|
|
{
|
|
Path: "/foo/policy.rego",
|
|
Parsed: ast.MustParseModule(`package foo`),
|
|
Raw: []byte(`package foo`),
|
|
},
|
|
},
|
|
},
|
|
name: "test-bundle",
|
|
expectedModules: []string{
|
|
"test-bundle/foo/policy.rego",
|
|
},
|
|
},
|
|
{
|
|
note: "filepath name",
|
|
bundle: Bundle{
|
|
Modules: []ModuleFile{
|
|
{
|
|
Path: "/foo/policy.rego",
|
|
Parsed: ast.MustParseModule(`package foo`),
|
|
Raw: []byte(`package foo`),
|
|
},
|
|
},
|
|
},
|
|
name: "/some/system/path",
|
|
expectedModules: []string{
|
|
"/some/system/path/foo/policy.rego",
|
|
},
|
|
},
|
|
{
|
|
note: "file url name",
|
|
bundle: Bundle{
|
|
Modules: []ModuleFile{
|
|
{
|
|
Path: "/foo/policy.rego",
|
|
Parsed: ast.MustParseModule(`package foo`),
|
|
Raw: []byte(`package foo`),
|
|
},
|
|
},
|
|
},
|
|
name: "file:///some/system/path",
|
|
expectedModules: []string{
|
|
"/some/system/path/foo/policy.rego",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
parsedMods := tc.bundle.ParsedModules(tc.name)
|
|
|
|
for _, exp := range tc.expectedModules {
|
|
mod, ok := parsedMods[exp]
|
|
if !ok {
|
|
t.Fatalf("Missing expected module %s, got: %+v", exp, parsedMods)
|
|
}
|
|
if mod == nil {
|
|
t.Fatalf("Expected module to be non-nil")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|