mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-23 00:34:48 -06:00
338583c18a
These changes add support for digital signatures for policy bundles which can be used to verify their authenticity. Bundle signature verification involves the following steps: * Verify the JWT signature * Verify the files in the JWT payload exist in the bundle * Verify the file content of the files in bundle match with those in the payload This commit adds a new `sign` command to generate a digital signature for policy bundles. For more details, run "opa sign --help" The signatures generated by the 'sign' command can be verified by the 'build' command. The 'build' command can also sign the bundle it generates. The 'run' command can verify a signed bundle or skip verification altogether. OPA 'sign', 'build' and 'run' can be used to sign/verify bundles in bundle mode (--bundle) mode only. Verification can be also be performed when bundle downloading is enabled. Fixes: #1757 Signed-off-by: Ashutosh Narkar <anarkar4387@gmail.com>
254 lines
6.2 KiB
Go
254 lines
6.2 KiB
Go
// Copyright 2020 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 init
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/open-policy-agent/opa/storage"
|
|
"github.com/open-policy-agent/opa/storage/inmem"
|
|
"github.com/open-policy-agent/opa/util"
|
|
"github.com/open-policy-agent/opa/util/test"
|
|
"github.com/open-policy-agent/opa/version"
|
|
)
|
|
|
|
func TestInit(t *testing.T) {
|
|
|
|
mod1 := `package a.b.c
|
|
|
|
import data.a.foo
|
|
|
|
p = true { foo = "bar" }
|
|
p = true { 1 = 2 }`
|
|
|
|
mod2 := `package b.c.d
|
|
|
|
import data.b.foo
|
|
|
|
p = true { foo = "bar" }
|
|
p = true { 1 = 2 }`
|
|
|
|
tests := []struct {
|
|
note string
|
|
fs map[string]string
|
|
loadParams []string
|
|
expectedData map[string]string
|
|
expectedMods []string
|
|
asBundle bool
|
|
}{
|
|
{
|
|
note: "load files",
|
|
fs: map[string]string{
|
|
"datafile": `{"foo": "bar", "x": {"y": {"z": [1]}}}`,
|
|
"policyFile": mod1,
|
|
},
|
|
loadParams: []string{"datafile", "policyFile"},
|
|
expectedData: map[string]string{
|
|
"/foo": "bar",
|
|
},
|
|
expectedMods: []string{mod1},
|
|
asBundle: false,
|
|
},
|
|
{
|
|
note: "load bundle",
|
|
fs: map[string]string{
|
|
"datafile": `{"foo": "bar", "x": {"y": {"z": [1]}}}`, // Should be ignored
|
|
"data.json": `{"foo": "not-bar"}`,
|
|
"policy.rego": mod1,
|
|
},
|
|
loadParams: []string{"/"},
|
|
expectedData: map[string]string{
|
|
"/foo": "not-bar",
|
|
},
|
|
expectedMods: []string{mod1},
|
|
asBundle: true,
|
|
},
|
|
{
|
|
note: "load multiple bundles",
|
|
fs: map[string]string{
|
|
"/bundle1/a/data.json": `{"foo": "bar1", "x": {"y": {"z": [1]}}}`, // Should be ignored
|
|
"/bundle1/a/policy.rego": mod1,
|
|
"/bundle1/a/.manifest": `{"roots": ["a"]}`,
|
|
"/bundle2/b/data.json": `{"foo": "bar2"}`,
|
|
"/bundle2/b/policy.rego": mod2,
|
|
"/bundle2/b/.manifest": `{"roots": ["b"]}`,
|
|
},
|
|
loadParams: []string{"bundle1", "bundle2"},
|
|
expectedData: map[string]string{
|
|
"/a/foo": "bar1",
|
|
"/b/foo": "bar2",
|
|
},
|
|
expectedMods: []string{mod1, mod2},
|
|
asBundle: true,
|
|
},
|
|
{
|
|
note: "preserve OPA version",
|
|
fs: map[string]string{
|
|
"/root/system/version/data.json": `{"version": "XYZ"}`, // Should be overwritten
|
|
},
|
|
loadParams: []string{"root"},
|
|
expectedData: map[string]string{
|
|
"/system/version/version": version.Version,
|
|
},
|
|
asBundle: true,
|
|
},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
test.WithTempFS(tc.fs, func(rootDir string) {
|
|
|
|
paths := []string{}
|
|
|
|
for _, fileName := range tc.loadParams {
|
|
paths = append(paths, filepath.Join(rootDir, fileName))
|
|
}
|
|
|
|
// Create a new store and perform a file load/insert sequence.
|
|
store := inmem.New()
|
|
|
|
err := storage.Txn(ctx, store, storage.WriteParams, func(txn storage.Transaction) error {
|
|
|
|
loaded, err := LoadPaths(paths, nil, tc.asBundle, nil, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = InsertAndCompile(ctx, InsertAndCompileOptions{
|
|
Store: store,
|
|
Txn: txn,
|
|
Files: loaded.Files,
|
|
Bundles: loaded.Bundles,
|
|
MaxErrors: -1,
|
|
})
|
|
|
|
return err
|
|
})
|
|
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Verify the loading was successful as expected.
|
|
txn := storage.NewTransactionOrDie(ctx, store)
|
|
|
|
for storePath, expected := range tc.expectedData {
|
|
node, err := store.Read(ctx, txn, storage.MustParsePath(storePath))
|
|
if util.Compare(node, expected) != 0 || err != nil {
|
|
t.Fatalf("Expected %v but got %v (err: %v)", expected, node, err)
|
|
}
|
|
}
|
|
|
|
ids, err := store.ListPolicies(ctx, txn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(tc.expectedMods) != len(ids) {
|
|
t.Fatalf("Expected %d modules, got %d", len(tc.expectedMods), len(ids))
|
|
}
|
|
|
|
actualMods := map[string]struct{}{}
|
|
for _, id := range ids {
|
|
result, err := store.GetPolicy(ctx, txn, id)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
actualMods[string(result)] = struct{}{}
|
|
}
|
|
|
|
for _, expectedMod := range tc.expectedMods {
|
|
if _, found := actualMods[expectedMod]; !found {
|
|
t.Fatalf("Expected %v but got: %v", expectedMod, actualMods)
|
|
}
|
|
}
|
|
|
|
_, err = store.Read(ctx, txn, storage.MustParsePath("/system/version"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWalkPaths(t *testing.T) {
|
|
files := map[string]string{
|
|
"/bundle1/a/data.json": `{"foo": "bar1", "x": {"y": {"z": [1]}}}`,
|
|
"/bundle1/a/policy.rego": `package example.foo`,
|
|
"/bundle1/a/.manifest": `{"roots": ["a"]}`,
|
|
"/bundle2/b/data.json": `{"foo": "bar2"}`,
|
|
"/bundle2/b/policy.rego": `package authz`,
|
|
"/bundle2/b/.manifest": `{"roots": ["b"]}`,
|
|
}
|
|
|
|
test.WithTempFS(files, func(rootDir string) {
|
|
|
|
paths := []string{}
|
|
paths = append(paths, filepath.Join(rootDir, "bundle1"))
|
|
paths = append(paths, filepath.Join(rootDir, "bundle2"))
|
|
|
|
// bundle mode
|
|
loaded, err := WalkPaths(paths, nil, true)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
if len(loaded.BundlesLoader) != len(paths) {
|
|
t.Fatalf("Expected %v bundle loaders but got %v", len(paths), len(loaded.BundlesLoader))
|
|
}
|
|
|
|
// check files
|
|
result := []string{}
|
|
for _, bl := range loaded.BundlesLoader {
|
|
for {
|
|
f, err := bl.DirectoryLoader.NextFile()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
result = append(result, f.Path())
|
|
|
|
if _, ok := files[strings.TrimPrefix(f.URL(), rootDir)]; !ok {
|
|
t.Fatalf("unexpected file %v", f.Path())
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(result) != len(files) {
|
|
t.Fatalf("Expected %v files across bundles but got %v", len(files), len(result))
|
|
}
|
|
|
|
// non-bundle mode
|
|
loaded, err = WalkPaths(paths, nil, false)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
if len(loaded.FileDescriptors) != len(files) {
|
|
t.Fatalf("Expected %v files across directories but got %v", len(files), len(loaded.FileDescriptors))
|
|
}
|
|
|
|
for _, d := range loaded.FileDescriptors {
|
|
path := path.Join(d.Root, d.Path)
|
|
path = strings.TrimPrefix(path, rootDir)
|
|
if _, ok := files[path]; !ok {
|
|
t.Fatalf("unexpected file %v", path)
|
|
}
|
|
}
|
|
})
|
|
}
|