mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
12dad370d5
- REST APIs
* CRUDL on policy modules
* Ad-hoc queries
* Query and patch base documents
* Query virtual documents
- Add PolicyStore to manage policy definition/module CRUDL operations.
* Supports persistence of policy definitons.
* Serve REST API CRUDL operations.
* Manage install/uninstall of rules into data store.
* Manage persistence of policy definitions.
- Misc. refactoring
* Move storage creation into runtime Init.
* Make AST types JSON serializable. Tweaked ast.Import to use Term instead
of Value for the path.
744 lines
16 KiB
Go
744 lines
16 KiB
Go
// Copyright 2016 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 ast
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
import "hash/fnv"
|
|
|
|
// Location records a position in source code
|
|
type Location struct {
|
|
Text []byte // The original text fragment from the source.
|
|
File string // The name of the source file (which may be empty).
|
|
Row int // The line in the source.
|
|
Col int // The column in the row.
|
|
}
|
|
|
|
// NewLocation returns a new Location object.
|
|
func NewLocation(text []byte, file string, row int, col int) *Location {
|
|
return &Location{Text: text, File: file, Row: row, Col: col}
|
|
}
|
|
|
|
// Value declares the common interface for all Term values. Every kind of Term value
|
|
// in the language is represented as a type that implements this interface:
|
|
//
|
|
// - Null, Boolean, Number, String
|
|
// - Object, Array
|
|
// - Variables
|
|
// - References
|
|
//
|
|
type Value interface {
|
|
// Equal returns true if this value equals the other value.
|
|
Equal(other Value) bool
|
|
|
|
// IsGround returns true if this value is not a variable or contains no variables.
|
|
IsGround() bool
|
|
|
|
// String returns a human readable string representation of the value.
|
|
String() string
|
|
|
|
// Returns hash code of the value.
|
|
Hash() int
|
|
}
|
|
|
|
// Term is an argument to a function.
|
|
type Term struct {
|
|
Value Value // the value of the Term as represented in Go
|
|
Location *Location `json:"-"` // the location of the Term in the source
|
|
}
|
|
|
|
// Equal returns true if this term equals the other term. Equality is
|
|
// defined for each kind of term.
|
|
func (term *Term) Equal(other *Term) bool {
|
|
if term == nil && other != nil {
|
|
return false
|
|
}
|
|
if term != nil && other == nil {
|
|
return false
|
|
}
|
|
if term == other {
|
|
return true
|
|
}
|
|
return term.Value.Equal(other.Value)
|
|
}
|
|
|
|
// IsGround returns true if this terms' Value is ground.
|
|
func (term *Term) IsGround() bool {
|
|
return term.Value.IsGround()
|
|
}
|
|
|
|
// MarshalJSON returns the JSON encoding of the term.
|
|
// Specialized marshalling logic is required to include a type hint
|
|
// for Value.
|
|
func (term *Term) MarshalJSON() ([]byte, error) {
|
|
var typ string
|
|
switch term.Value.(type) {
|
|
case Null:
|
|
typ = "null"
|
|
case Boolean:
|
|
typ = "boolean"
|
|
case Number:
|
|
typ = "number"
|
|
case String:
|
|
typ = "string"
|
|
case Ref:
|
|
typ = "ref"
|
|
case Var:
|
|
typ = "var"
|
|
case Array:
|
|
typ = "array"
|
|
case Object:
|
|
typ = "object"
|
|
}
|
|
d := map[string]interface{}{
|
|
"Type": typ,
|
|
"Value": term.Value,
|
|
}
|
|
return json.Marshal(d)
|
|
}
|
|
|
|
func (term *Term) String() string {
|
|
return term.Value.String()
|
|
}
|
|
|
|
// UnmarshalJSON parses the byte array and stores the result in term.
|
|
// Specialized unmarshalling is required to handle Value.
|
|
func (term *Term) UnmarshalJSON(bs []byte) error {
|
|
v := map[string]interface{}{}
|
|
if err := json.Unmarshal(bs, &v); err != nil {
|
|
return err
|
|
}
|
|
val, err := unmarshalValue(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
term.Value = val
|
|
return nil
|
|
}
|
|
|
|
// Null represents the null value defined by JSON.
|
|
type Null struct{}
|
|
|
|
// NullTerm creates a new Term with a Null value.
|
|
func NullTerm() *Term {
|
|
return &Term{Value: Null{}}
|
|
}
|
|
|
|
// Equal returns true if the other term Value is also Null.
|
|
func (null Null) Equal(other Value) bool {
|
|
switch other.(type) {
|
|
case Null:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Hash returns the hash code for the Value.
|
|
func (null Null) Hash() int {
|
|
return 0
|
|
}
|
|
|
|
// IsGround always returns true.
|
|
func (null Null) IsGround() bool {
|
|
return true
|
|
}
|
|
|
|
func (null Null) String() string {
|
|
return "null"
|
|
}
|
|
|
|
// Boolean represents a boolean value defined by JSON.
|
|
type Boolean bool
|
|
|
|
// BooleanTerm creates a new Term with a Boolean value.
|
|
func BooleanTerm(b bool) *Term {
|
|
return &Term{Value: Boolean(b)}
|
|
}
|
|
|
|
// Equal returns true if the other Value is a Boolean and is equal.
|
|
func (bol Boolean) Equal(other Value) bool {
|
|
switch other := other.(type) {
|
|
case Boolean:
|
|
return bol == other
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Hash returns the hash code for the Value.
|
|
func (bol Boolean) Hash() int {
|
|
if bol {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// IsGround always returns true.
|
|
func (bol Boolean) IsGround() bool {
|
|
return true
|
|
}
|
|
|
|
func (bol Boolean) String() string {
|
|
return strconv.FormatBool(bool(bol))
|
|
}
|
|
|
|
// Number represents a numeric value as defined by JSON.
|
|
type Number float64
|
|
|
|
// NumberTerm creates a new Term with a Number value.
|
|
func NumberTerm(n float64) *Term {
|
|
return &Term{Value: Number(n)}
|
|
}
|
|
|
|
// Equal returns true if the other Value is a Number and is equal.
|
|
func (num Number) Equal(other Value) bool {
|
|
switch other := other.(type) {
|
|
case Number:
|
|
return num == other
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Hash returns the hash code for the Value.
|
|
func (num Number) Hash() int {
|
|
return int(num)
|
|
}
|
|
|
|
// IsGround always returns true.
|
|
func (num Number) IsGround() bool {
|
|
return true
|
|
}
|
|
|
|
func (num Number) String() string {
|
|
return strconv.FormatFloat(float64(num), 'G', -1, 64)
|
|
}
|
|
|
|
// String represents a string value as defined by JSON.
|
|
type String string
|
|
|
|
// StringTerm creates a new Term with a String value.
|
|
func StringTerm(s string) *Term {
|
|
return &Term{Value: String(s)}
|
|
}
|
|
|
|
// Equal returns true if the other Value is a String and is equal.
|
|
func (str String) Equal(other Value) bool {
|
|
switch other := other.(type) {
|
|
case String:
|
|
return str == other
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// IsGround always returns true.
|
|
func (str String) IsGround() bool {
|
|
return true
|
|
}
|
|
|
|
func (str String) String() string {
|
|
return strconv.Quote(string(str))
|
|
}
|
|
|
|
// Hash returns the hash code for the Value.
|
|
func (str String) Hash() int {
|
|
h := fnv.New64a()
|
|
h.Write([]byte(str))
|
|
return int(h.Sum64())
|
|
}
|
|
|
|
// Var represents a variable as defined by the language.
|
|
type Var string
|
|
|
|
// VarTerm creates a new Term with a Variable value.
|
|
func VarTerm(v string) *Term {
|
|
return &Term{Value: Var(v)}
|
|
}
|
|
|
|
// Equal returns true if the other Value is a Variable and has the same value
|
|
// (name).
|
|
func (variable Var) Equal(other Value) bool {
|
|
switch other := other.(type) {
|
|
case Var:
|
|
return variable == other
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Hash returns the hash code for the Value.
|
|
func (variable Var) Hash() int {
|
|
h := fnv.New64a()
|
|
h.Write([]byte(variable))
|
|
return int(h.Sum64())
|
|
}
|
|
|
|
// IsGround always returns false.
|
|
func (variable Var) IsGround() bool {
|
|
return false
|
|
}
|
|
|
|
func (variable Var) String() string {
|
|
return string(variable)
|
|
}
|
|
|
|
// Ref represents a reference as defined by the language.
|
|
type Ref []*Term
|
|
|
|
// EmptyRef returns a new, empty reference.
|
|
func EmptyRef() Ref {
|
|
return Ref([]*Term{})
|
|
}
|
|
|
|
// RefTerm creates a new Term with a Ref value.
|
|
func RefTerm(r ...*Term) *Term {
|
|
return &Term{Value: Ref(r)}
|
|
}
|
|
|
|
// Equal returns true if the other Value is a Ref and the elements of the
|
|
// other Ref are equal to the this Ref.
|
|
func (ref Ref) Equal(other Value) bool {
|
|
switch other := other.(type) {
|
|
case Ref:
|
|
return termSliceEqual(ref, other)
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Hash returns the hash code for the Value.
|
|
func (ref Ref) Hash() int {
|
|
return termSliceHash(ref)
|
|
}
|
|
|
|
// IsGround returns true if all of the parts of the Ref are ground.
|
|
func (ref Ref) IsGround() bool {
|
|
if len(ref) == 0 {
|
|
return true
|
|
}
|
|
return termSliceIsGround(ref[1:])
|
|
}
|
|
|
|
var varRegexp = regexp.MustCompile("^[[:alpha:]_][[:alpha:][:digit:]_]*$")
|
|
|
|
func (ref Ref) String() string {
|
|
if len(ref) == 0 {
|
|
return ""
|
|
}
|
|
var buf []string
|
|
path := ref
|
|
switch v := ref[0].Value.(type) {
|
|
case Var:
|
|
buf = append(buf, string(v))
|
|
path = path[1:]
|
|
}
|
|
for _, p := range path {
|
|
switch p := p.Value.(type) {
|
|
case String:
|
|
str := string(p)
|
|
if varRegexp.MatchString(str) && len(buf) > 0 {
|
|
buf = append(buf, "."+str)
|
|
} else {
|
|
buf = append(buf, "["+p.String()+"]")
|
|
}
|
|
default:
|
|
buf = append(buf, "["+p.String()+"]")
|
|
}
|
|
}
|
|
return strings.Join(buf, "")
|
|
}
|
|
|
|
// Underlying returns a slice of underlying Go values.
|
|
// If the slice is not ground, an error is returned.
|
|
func (ref Ref) Underlying() ([]interface{}, error) {
|
|
|
|
if !ref.IsGround() {
|
|
return nil, fmt.Errorf("cannot get underlying value for non-ground ref: %v", ref)
|
|
}
|
|
|
|
r := []interface{}{}
|
|
if len(ref) == 0 {
|
|
return r, nil
|
|
}
|
|
|
|
// The reference head is typically a variable, however,
|
|
// in some cases, we use the slice operator to process the reference
|
|
// in which ase the slice result is still a reference but the head
|
|
// may be a string, number, etc.
|
|
switch head := ref[0].Value.(type) {
|
|
case Var:
|
|
r = append(r, string(head))
|
|
case String:
|
|
r = append(r, string(head))
|
|
case Number:
|
|
r = append(r, float64(head))
|
|
case Boolean:
|
|
r = append(r, bool(head))
|
|
case Null:
|
|
r = append(r, nil)
|
|
default:
|
|
panic(fmt.Sprintf("illegal value: %v %v", head, ref))
|
|
}
|
|
|
|
for _, v := range ref[1:] {
|
|
switch v := v.Value.(type) {
|
|
case String:
|
|
r = append(r, string(v))
|
|
case Number:
|
|
r = append(r, float64(v))
|
|
case Boolean:
|
|
r = append(r, bool(v))
|
|
case Null:
|
|
r = append(r, nil)
|
|
default:
|
|
panic(fmt.Sprintf("illegal value: %v %v", v, ref))
|
|
}
|
|
}
|
|
|
|
return r, nil
|
|
}
|
|
|
|
// QueryIterator defines the interface for querying AST documents with references.
|
|
type QueryIterator func(map[Var]Value, Value) error
|
|
|
|
// Array represents an array as defined by the language. Arrays are similar to the
|
|
// same types as defined by JSON with the exception that they can contain Vars
|
|
// and References.
|
|
type Array []*Term
|
|
|
|
// ArrayTerm creates a new Term with an Array value.
|
|
func ArrayTerm(a ...*Term) *Term {
|
|
return &Term{Value: Array(a)}
|
|
}
|
|
|
|
// Equal returns true if the other Value is an Array and the elements of the
|
|
// other Array are equal to the elements of this Array. The elements are
|
|
// ordered.
|
|
func (arr Array) Equal(other Value) bool {
|
|
switch other := other.(type) {
|
|
case Array:
|
|
return termSliceEqual(arr, other)
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Hash returns the hash code for the Value.
|
|
func (arr Array) Hash() int {
|
|
return termSliceHash(arr)
|
|
}
|
|
|
|
// IsGround returns true if all of the Array elements are ground.
|
|
func (arr Array) IsGround() bool {
|
|
return termSliceIsGround(arr)
|
|
}
|
|
|
|
// Query invokes the iterator for each referenced value inside the array.
|
|
func (arr Array) Query(ref Ref, iter QueryIterator) error {
|
|
return arr.queryRec(ref, make(map[Var]Value), iter)
|
|
}
|
|
|
|
func (arr Array) String() string {
|
|
var buf []string
|
|
for _, e := range arr {
|
|
buf = append(buf, e.String())
|
|
}
|
|
return "[" + strings.Join(buf, ", ") + "]"
|
|
}
|
|
|
|
func (arr Array) queryRec(ref Ref, keys map[Var]Value, iter QueryIterator) error {
|
|
if len(ref) == 0 {
|
|
return iter(keys, arr)
|
|
}
|
|
switch head := ref[0].Value.(type) {
|
|
case Var:
|
|
tail := ref[1:]
|
|
for i, v := range arr {
|
|
keys[head] = Number(i)
|
|
if err := queryRec(v.Value, ref, tail, keys, iter, true); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
case Number:
|
|
idx := int(head)
|
|
if len(arr) <= idx {
|
|
return fmt.Errorf("unexpected index in %v: out of bounds: %v", ref, idx)
|
|
}
|
|
tail := ref[1:]
|
|
return queryRec(arr[idx].Value, ref, tail, keys, iter, false)
|
|
default:
|
|
return fmt.Errorf("unexpected non-numeric index in %v: %v (%T)", ref, head, head)
|
|
}
|
|
}
|
|
|
|
// Object represents an object as defined by the language. Objects are similar to
|
|
// the same types as defined by JSON with the exception that they can contain
|
|
// Vars and References.
|
|
type Object [][2]*Term
|
|
|
|
// Item is a helper for constructing an tuple containing two Terms
|
|
// representing a key/value pair in an Object.
|
|
func Item(key, value *Term) [2]*Term {
|
|
return [2]*Term{key, value}
|
|
}
|
|
|
|
// Equal returns true if the other Value is an Object and the key/value pairs
|
|
// of the Other object are equal to the key/value pairs of this Object. The
|
|
// key/value pairs are ordered.
|
|
func (obj Object) Equal(other Value) bool {
|
|
switch other := other.(type) {
|
|
case Object:
|
|
if len(obj) == len(other) {
|
|
for i := range obj {
|
|
if !obj[i][0].Equal(other[i][0]) {
|
|
return false
|
|
}
|
|
if !obj[i][1].Equal(other[i][1]) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Hash returns the hash code for the Value.
|
|
func (obj Object) Hash() int {
|
|
var hash int
|
|
for i := range obj {
|
|
hash += obj[i][0].Value.Hash()
|
|
hash += obj[i][1].Value.Hash()
|
|
}
|
|
return hash
|
|
}
|
|
|
|
// IsGround returns true if all of the Object key/value pairs are ground.
|
|
func (obj Object) IsGround() bool {
|
|
for i := range obj {
|
|
if !obj[i][0].IsGround() {
|
|
return false
|
|
}
|
|
if !obj[i][1].IsGround() {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// ObjectTerm creates a new Term with an Object value.
|
|
func ObjectTerm(o ...[2]*Term) *Term {
|
|
return &Term{Value: Object(o)}
|
|
}
|
|
|
|
// Query invokes the iterator for each referenced value inside the object.
|
|
func (obj Object) Query(ref Ref, iter QueryIterator) error {
|
|
return obj.queryRec(ref, make(map[Var]Value), iter)
|
|
}
|
|
|
|
func (obj Object) String() string {
|
|
var buf []string
|
|
for _, p := range obj {
|
|
buf = append(buf, fmt.Sprintf("%s: %s", p[0], p[1]))
|
|
}
|
|
return "{" + strings.Join(buf, ", ") + "}"
|
|
}
|
|
|
|
func (obj Object) queryRec(ref Ref, keys map[Var]Value, iter QueryIterator) error {
|
|
if len(ref) == 0 {
|
|
return iter(keys, obj)
|
|
}
|
|
switch head := ref[0].Value.(type) {
|
|
case Var:
|
|
tail := ref[1:]
|
|
for _, i := range obj {
|
|
keys[head] = i[0].Value
|
|
if err := queryRec(i[1].Value, ref, tail, keys, iter, true); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
|
|
default:
|
|
found := false
|
|
tail := ref[1:]
|
|
for _, i := range obj {
|
|
if i[0].Value.Equal(head) {
|
|
if err := queryRec(i[1].Value, ref, tail, keys, iter, false); err != nil {
|
|
return err
|
|
}
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return fmt.Errorf("missing key %v: %v", head, ref)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func queryRec(v Value, ref Ref, tail Ref, keys map[Var]Value, iter QueryIterator, skipScalar bool) error {
|
|
if len(tail) == 0 {
|
|
if err := iter(keys, v); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
switch v := v.(type) {
|
|
case Array:
|
|
if err := v.queryRec(tail, keys, iter); err != nil {
|
|
return err
|
|
}
|
|
case Object:
|
|
if err := v.queryRec(tail, keys, iter); err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
if !skipScalar {
|
|
return fmt.Errorf("unexpected non-composite at %v: %v", ref, v)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func termSliceEqual(a, b []*Term) bool {
|
|
if len(a) == len(b) {
|
|
for i := range a {
|
|
if !a[i].Equal(b[i]) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func termSliceHash(a []*Term) int {
|
|
var hash int
|
|
for _, v := range a {
|
|
hash += v.Value.Hash()
|
|
}
|
|
return hash
|
|
}
|
|
|
|
func termSliceIsGround(a []*Term) bool {
|
|
for _, v := range a {
|
|
if !v.IsGround() {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func unmarshalError(v interface{}, e string) error {
|
|
return fmt.Errorf("ast: cannot unmarshal %T into Go value of type %v", v, e)
|
|
}
|
|
|
|
func unmarshalTermSlice(d map[string]interface{}) ([]*Term, error) {
|
|
s, ok := d["Value"].([]interface{})
|
|
if !ok {
|
|
return nil, unmarshalError(d["Value"], "[]interface{}")
|
|
}
|
|
buf := []*Term{}
|
|
for _, i := range s {
|
|
m, ok := i.(map[string]interface{})
|
|
if !ok {
|
|
return nil, unmarshalError(i, "map[string]interface{}")
|
|
}
|
|
v, err := unmarshalValue(m)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
buf = append(buf, &Term{Value: v})
|
|
}
|
|
return buf, nil
|
|
}
|
|
|
|
func unmarshalValue(d map[string]interface{}) (Value, error) {
|
|
switch d["Type"] {
|
|
case "null":
|
|
return Null{}, nil
|
|
case "boolean":
|
|
b, ok := d["Value"].(bool)
|
|
if !ok {
|
|
return nil, unmarshalError(d["Value"], "bool")
|
|
}
|
|
return Boolean(b), nil
|
|
case "number":
|
|
f, ok := d["Value"].(float64)
|
|
if !ok {
|
|
return nil, unmarshalError(d["Value"], "float64")
|
|
}
|
|
return Number(f), nil
|
|
case "string":
|
|
s, ok := d["Value"].(string)
|
|
if !ok {
|
|
return nil, unmarshalError(d["Value"], "string")
|
|
}
|
|
return String(s), nil
|
|
case "ref":
|
|
s, err := unmarshalTermSlice(d)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return Ref(s), nil
|
|
case "var":
|
|
s, ok := d["Value"].(string)
|
|
if !ok {
|
|
return nil, unmarshalError(d["Value"], "ast.Var")
|
|
}
|
|
return Var(s), nil
|
|
case "array":
|
|
s, err := unmarshalTermSlice(d)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return Array(s), nil
|
|
case "object":
|
|
buf := Object{}
|
|
s, ok := d["Value"].([]interface{})
|
|
if !ok {
|
|
return nil, unmarshalError(d["Value"], "[]interface{}")
|
|
}
|
|
for _, i := range s {
|
|
p, ok := i.([]interface{})
|
|
if !ok {
|
|
return nil, unmarshalError(i, "[]interface{}")
|
|
}
|
|
if len(p) != 2 {
|
|
return nil, unmarshalError(p, "[2]interface{}")
|
|
}
|
|
km, ok := p[0].(map[string]interface{})
|
|
if !ok {
|
|
return nil, unmarshalError(p[0], "map[string]interface{}")
|
|
}
|
|
k, err := unmarshalValue(km)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
vm, ok := p[1].(map[string]interface{})
|
|
if !ok {
|
|
return nil, unmarshalError(p[1], "map[string]interface{}")
|
|
}
|
|
v, err := unmarshalValue(vm)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
buf = append(buf, [2]*Term{&Term{Value: k}, &Term{Value: v}})
|
|
}
|
|
return buf, nil
|
|
default:
|
|
return nil, fmt.Errorf("ast: cannot unmarshal Term with Type %v", d["Type"])
|
|
}
|
|
}
|