mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-21 07:51:19 -06:00
cc22d5219a
This was tested manually. Fixes #920 Signed-off-by: Torin Sandall <torinsandall@gmail.com>
242 lines
5.3 KiB
Go
242 lines
5.3 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 implements bundle loading.
|
|
package bundle
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/open-policy-agent/opa/ast"
|
|
"github.com/open-policy-agent/opa/util"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// Common file extensions and file names.
|
|
const (
|
|
RegoExt = ".rego"
|
|
JSONExt = ".json"
|
|
ManifestExt = ".manifest"
|
|
DataFileExt = "/data.json"
|
|
)
|
|
|
|
const bundleLimitBytes = (1024 * 1024 * 1024) + 1 // limit bundle reads to 1GB to protect against gzip bombs
|
|
|
|
// Bundle represents a loaded bundle. The bundle can contain data and policies.
|
|
type Bundle struct {
|
|
Manifest Manifest
|
|
Data map[string]interface{}
|
|
Modules []ModuleFile
|
|
}
|
|
|
|
// Manifest represents the manifest from a bundle. The manifest may contain
|
|
// metadata such as the bundle revision.
|
|
type Manifest struct {
|
|
Revision string `json:"revision"`
|
|
}
|
|
|
|
// ModuleFile represents a single module contained a bundle.
|
|
type ModuleFile struct {
|
|
Path string
|
|
Raw []byte
|
|
Parsed *ast.Module
|
|
}
|
|
|
|
// Write serializes the Bundle and writes it to w.
|
|
func Write(w io.Writer, bundle Bundle) error {
|
|
gw := gzip.NewWriter(w)
|
|
tw := tar.NewWriter(gw)
|
|
|
|
var buf bytes.Buffer
|
|
|
|
if err := json.NewEncoder(&buf).Encode(bundle.Data); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := writeFile(tw, "data.json", buf.Bytes()); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, module := range bundle.Modules {
|
|
if err := writeFile(tw, module.Path, module.Raw); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := writeManifest(tw, bundle); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := tw.Close(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return gw.Close()
|
|
}
|
|
|
|
func writeManifest(tw *tar.Writer, bundle Bundle) error {
|
|
|
|
var buf bytes.Buffer
|
|
|
|
if err := json.NewEncoder(&buf).Encode(bundle.Manifest); err != nil {
|
|
return err
|
|
}
|
|
|
|
return writeFile(tw, ManifestExt, buf.Bytes())
|
|
}
|
|
|
|
// Read returns a new Bundle loaded from the reader.
|
|
func Read(r io.Reader) (Bundle, error) {
|
|
|
|
var bundle Bundle
|
|
|
|
bundle.Data = map[string]interface{}{}
|
|
|
|
gr, err := gzip.NewReader(r)
|
|
if err != nil {
|
|
return bundle, errors.Wrap(err, "bundle read failed")
|
|
}
|
|
|
|
tr := tar.NewReader(gr)
|
|
|
|
for {
|
|
header, err := tr.Next()
|
|
if err == io.EOF {
|
|
break
|
|
} else if err != nil {
|
|
return bundle, errors.Wrap(err, "bundle read failed")
|
|
}
|
|
|
|
if header.Typeflag != tar.TypeReg {
|
|
continue
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
n, err := io.CopyN(&buf, tr, bundleLimitBytes)
|
|
if err != nil && err != io.EOF {
|
|
return bundle, err
|
|
} else if err == nil && n >= bundleLimitBytes {
|
|
return bundle, fmt.Errorf("bundle exceeded max size (%v bytes)", bundleLimitBytes-1)
|
|
}
|
|
|
|
path := header.Name
|
|
|
|
if strings.HasSuffix(path, RegoExt) {
|
|
module, err := ast.ParseModule(path, buf.String())
|
|
if err != nil {
|
|
return bundle, errors.Wrap(err, "bundle load failed")
|
|
}
|
|
file := ModuleFile{
|
|
Path: path,
|
|
Raw: buf.Bytes(),
|
|
Parsed: module,
|
|
}
|
|
bundle.Modules = append(bundle.Modules, file)
|
|
|
|
} else if strings.HasSuffix(path, DataFileExt) {
|
|
var value interface{}
|
|
if err := util.NewJSONDecoder(&buf).Decode(&value); err != nil {
|
|
return bundle, errors.Wrapf(err, "bundle load failed on %v", path)
|
|
}
|
|
dirpath := strings.Trim(strings.TrimSuffix(path, DataFileExt), "/")
|
|
var key []string
|
|
if dirpath != "" {
|
|
key = strings.Split(dirpath, "/")
|
|
}
|
|
if err := bundle.insert(key, value); err != nil {
|
|
return bundle, errors.Wrapf(err, "bundle load failed on %v", path)
|
|
}
|
|
|
|
} else if strings.HasSuffix(path, ManifestExt) {
|
|
if err := util.NewJSONDecoder(&buf).Decode(&bundle.Manifest); err != nil {
|
|
return bundle, errors.Wrapf(err, "bundle load failed on manifest")
|
|
}
|
|
}
|
|
}
|
|
|
|
return bundle, nil
|
|
}
|
|
|
|
// Equal returns true if this bundle's contents equal the other bundle's
|
|
// contents.
|
|
func (b Bundle) Equal(other Bundle) bool {
|
|
if !reflect.DeepEqual(b.Data, other.Data) {
|
|
return false
|
|
}
|
|
if len(b.Modules) != len(other.Modules) {
|
|
return false
|
|
}
|
|
for i := range b.Modules {
|
|
if b.Modules[i].Path != other.Modules[i].Path {
|
|
return false
|
|
}
|
|
if !b.Modules[i].Parsed.Equal(other.Modules[i].Parsed) {
|
|
return false
|
|
}
|
|
if !bytes.Equal(b.Modules[i].Raw, other.Modules[i].Raw) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (b *Bundle) insert(key []string, value interface{}) error {
|
|
if len(key) == 0 {
|
|
obj, ok := value.(map[string]interface{})
|
|
if !ok {
|
|
return fmt.Errorf("root value must be object")
|
|
}
|
|
b.Data = obj
|
|
return nil
|
|
}
|
|
|
|
obj, err := b.mkdir(key[:len(key)-1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
obj[key[len(key)-1]] = value
|
|
return nil
|
|
}
|
|
|
|
func (b *Bundle) mkdir(key []string) (map[string]interface{}, error) {
|
|
obj := b.Data
|
|
for i := 0; i < len(key); i++ {
|
|
node, ok := obj[key[i]]
|
|
if !ok {
|
|
node = map[string]interface{}{}
|
|
obj[key[i]] = node
|
|
}
|
|
obj, ok = node.(map[string]interface{})
|
|
if !ok {
|
|
return nil, fmt.Errorf("non-leaf value must be object")
|
|
}
|
|
}
|
|
return obj, nil
|
|
}
|
|
|
|
func writeFile(tw *tar.Writer, path string, bs []byte) error {
|
|
|
|
hdr := &tar.Header{
|
|
Name: "/" + strings.TrimLeft(path, "/"),
|
|
Mode: 0600,
|
|
Typeflag: tar.TypeReg,
|
|
Size: int64(len(bs)),
|
|
}
|
|
|
|
if err := tw.WriteHeader(hdr); err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err := tw.Write(bs)
|
|
return err
|
|
}
|