Signed-off-by: hongming <talonwan@yunify.com>
This commit is contained in:
hongming
2020-03-19 22:44:05 +08:00
parent 23f6be88c6
commit 9769357005
332 changed files with 69808 additions and 4129 deletions

View File

@@ -0,0 +1,6 @@
// 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 storage exposes the policy engine's storage layer.
package storage

View File

@@ -0,0 +1,135 @@
// 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 storage
import (
"fmt"
)
const (
// InternalErr indicates an unknown, internal error has occurred.
InternalErr = "storage_internal_error"
// NotFoundErr indicates the path used in the storage operation does not
// locate a document.
NotFoundErr = "storage_not_found_error"
// WriteConflictErr indicates a write on the path enocuntered a conflicting
// value inside the transaction.
WriteConflictErr = "storage_write_conflict_error"
// InvalidPatchErr indicates an invalid patch/write was issued. The patch
// was rejected.
InvalidPatchErr = "storage_invalid_patch_error"
// InvalidTransactionErr indicates an invalid operation was performed
// inside of the transaction.
InvalidTransactionErr = "storage_invalid_txn_error"
// TriggersNotSupportedErr indicates the caller attempted to register a
// trigger against a store that does not support them.
TriggersNotSupportedErr = "storage_triggers_not_supported_error"
// WritesNotSupportedErr indicate the caller attempted to perform a write
// against a store that does not support them.
WritesNotSupportedErr = "storage_writes_not_supported_error"
// PolicyNotSupportedErr indicate the caller attempted to perform a policy
// management operation against a store that does not support them.
PolicyNotSupportedErr = "storage_policy_not_supported_error"
// IndexingNotSupportedErr indicate the caller attempted to perform an
// indexing operation against a store that does not support them.
IndexingNotSupportedErr = "storage_indexing_not_supported_error"
)
// Error is the error type returned by the storage layer.
type Error struct {
Code string `json:"code"`
Message string `json:"message"`
}
func (err *Error) Error() string {
if err.Message != "" {
return fmt.Sprintf("%v: %v", err.Code, err.Message)
}
return string(err.Code)
}
// IsNotFound returns true if this error is a NotFoundErr.
func IsNotFound(err error) bool {
switch err := err.(type) {
case *Error:
return err.Code == NotFoundErr
}
return false
}
// IsWriteConflictError returns true if this error a WriteConflictErr.
func IsWriteConflictError(err error) bool {
switch err := err.(type) {
case *Error:
return err.Code == WriteConflictErr
}
return false
}
// IsInvalidPatch returns true if this error is a InvalidPatchErr.
func IsInvalidPatch(err error) bool {
switch err := err.(type) {
case *Error:
return err.Code == InvalidPatchErr
}
return false
}
// IsInvalidTransaction returns true if this error is a InvalidTransactionErr.
func IsInvalidTransaction(err error) bool {
switch err := err.(type) {
case *Error:
return err.Code == InvalidTransactionErr
}
return false
}
// IsIndexingNotSupported returns true if this error is a IndexingNotSupportedErr.
func IsIndexingNotSupported(err error) bool {
switch err := err.(type) {
case *Error:
return err.Code == IndexingNotSupportedErr
}
return false
}
func writeConflictError(path Path) *Error {
return &Error{
Code: WriteConflictErr,
Message: fmt.Sprint(path),
}
}
func triggersNotSupportedError() *Error {
return &Error{
Code: TriggersNotSupportedErr,
}
}
func writesNotSupportedError() *Error {
return &Error{
Code: WritesNotSupportedErr,
}
}
func policyNotSupportedError() *Error {
return &Error{
Code: PolicyNotSupportedErr,
}
}
func indexingNotSupportedError() *Error {
return &Error{
Code: IndexingNotSupportedErr,
}
}

View File

@@ -0,0 +1,350 @@
// 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 inmem
import (
"context"
"encoding/json"
"fmt"
"hash/fnv"
"strings"
"sync"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/storage"
"github.com/open-policy-agent/opa/util"
)
// indices contains a mapping of non-ground references to values to sets of bindings.
//
// +------+------------------------------------+
// | ref1 | val1 | bindings-1, bindings-2, ... |
// | +------+-----------------------------+
// | | val2 | bindings-m, bindings-m, ... |
// | +------+-----------------------------+
// | | .... | ... |
// +------+------+-----------------------------+
// | ref2 | .... | ... |
// +------+------+-----------------------------+
// | ... |
// +-------------------------------------------+
//
// The "value" is the data value stored at the location referred to by the ground
// reference obtained by plugging bindings into the non-ground reference that is the
// index key.
//
type indices struct {
mu sync.Mutex
table map[int]*indicesNode
}
type indicesNode struct {
key ast.Ref
val *bindingIndex
next *indicesNode
}
func newIndices() *indices {
return &indices{
table: map[int]*indicesNode{},
}
}
func (ind *indices) Build(ctx context.Context, store storage.Store, txn storage.Transaction, ref ast.Ref) (*bindingIndex, error) {
ind.mu.Lock()
defer ind.mu.Unlock()
if exist := ind.get(ref); exist != nil {
return exist, nil
}
index := newBindingIndex()
if err := iterStorage(ctx, store, txn, ref, ast.EmptyRef(), ast.NewValueMap(), index.Add); err != nil {
return nil, err
}
hashCode := ref.Hash()
head := ind.table[hashCode]
entry := &indicesNode{
key: ref,
val: index,
next: head,
}
ind.table[hashCode] = entry
return index, nil
}
func (ind *indices) get(ref ast.Ref) *bindingIndex {
node := ind.getNode(ref)
if node != nil {
return node.val
}
return nil
}
func (ind *indices) iter(iter func(ast.Ref, *bindingIndex) error) error {
for _, head := range ind.table {
for entry := head; entry != nil; entry = entry.next {
if err := iter(entry.key, entry.val); err != nil {
return err
}
}
}
return nil
}
func (ind *indices) getNode(ref ast.Ref) *indicesNode {
hashCode := ref.Hash()
for entry := ind.table[hashCode]; entry != nil; entry = entry.next {
if entry.key.Equal(ref) {
return entry
}
}
return nil
}
func (ind *indices) String() string {
buf := []string{}
for _, head := range ind.table {
for entry := head; entry != nil; entry = entry.next {
str := fmt.Sprintf("%v: %v", entry.key, entry.val)
buf = append(buf, str)
}
}
return "{" + strings.Join(buf, ", ") + "}"
}
// bindingIndex contains a mapping of values to bindings.
type bindingIndex struct {
table map[int]*indexNode
}
type indexNode struct {
key interface{}
val *bindingSet
next *indexNode
}
func newBindingIndex() *bindingIndex {
return &bindingIndex{
table: map[int]*indexNode{},
}
}
func (ind *bindingIndex) Add(val interface{}, bindings *ast.ValueMap) {
node := ind.getNode(val)
if node != nil {
node.val.Add(bindings)
return
}
hashCode := hash(val)
bindingsSet := newBindingSet()
bindingsSet.Add(bindings)
entry := &indexNode{
key: val,
val: bindingsSet,
next: ind.table[hashCode],
}
ind.table[hashCode] = entry
}
func (ind *bindingIndex) Lookup(_ context.Context, _ storage.Transaction, val interface{}, iter storage.IndexIterator) error {
node := ind.getNode(val)
if node == nil {
return nil
}
return node.val.Iter(iter)
}
func (ind *bindingIndex) getNode(val interface{}) *indexNode {
hashCode := hash(val)
head := ind.table[hashCode]
for entry := head; entry != nil; entry = entry.next {
if util.Compare(entry.key, val) == 0 {
return entry
}
}
return nil
}
func (ind *bindingIndex) String() string {
buf := []string{}
for _, head := range ind.table {
for entry := head; entry != nil; entry = entry.next {
str := fmt.Sprintf("%v: %v", entry.key, entry.val)
buf = append(buf, str)
}
}
return "{" + strings.Join(buf, ", ") + "}"
}
type bindingSetNode struct {
val *ast.ValueMap
next *bindingSetNode
}
type bindingSet struct {
table map[int]*bindingSetNode
}
func newBindingSet() *bindingSet {
return &bindingSet{
table: map[int]*bindingSetNode{},
}
}
func (set *bindingSet) Add(val *ast.ValueMap) {
node := set.getNode(val)
if node != nil {
return
}
hashCode := val.Hash()
head := set.table[hashCode]
set.table[hashCode] = &bindingSetNode{val, head}
}
func (set *bindingSet) Iter(iter func(*ast.ValueMap) error) error {
for _, head := range set.table {
for entry := head; entry != nil; entry = entry.next {
if err := iter(entry.val); err != nil {
return err
}
}
}
return nil
}
func (set *bindingSet) String() string {
buf := []string{}
set.Iter(func(bindings *ast.ValueMap) error {
buf = append(buf, bindings.String())
return nil
})
return "{" + strings.Join(buf, ", ") + "}"
}
func (set *bindingSet) getNode(val *ast.ValueMap) *bindingSetNode {
hashCode := val.Hash()
for entry := set.table[hashCode]; entry != nil; entry = entry.next {
if entry.val.Equal(val) {
return entry
}
}
return nil
}
func hash(v interface{}) int {
switch v := v.(type) {
case []interface{}:
var h int
for _, e := range v {
h += hash(e)
}
return h
case map[string]interface{}:
var h int
for k, v := range v {
h += hash(k) + hash(v)
}
return h
case string:
h := fnv.New64a()
h.Write([]byte(v))
return int(h.Sum64())
case bool:
if v {
return 1
}
return 0
case nil:
return 0
case json.Number:
h := fnv.New64a()
h.Write([]byte(v))
return int(h.Sum64())
}
panic(fmt.Sprintf("illegal argument: %v (%T)", v, v))
}
func iterStorage(ctx context.Context, store storage.Store, txn storage.Transaction, nonGround, ground ast.Ref, bindings *ast.ValueMap, iter func(interface{}, *ast.ValueMap)) error {
if len(nonGround) == 0 {
path, err := storage.NewPathForRef(ground)
if err != nil {
return err
}
node, err := store.Read(ctx, txn, path)
if err != nil {
if storage.IsNotFound(err) {
return nil
}
return err
}
iter(node, bindings)
return nil
}
head := nonGround[0]
tail := nonGround[1:]
headVar, isVar := head.Value.(ast.Var)
if !isVar || len(ground) == 0 {
ground = append(ground, head)
return iterStorage(ctx, store, txn, tail, ground, bindings, iter)
}
path, err := storage.NewPathForRef(ground)
if err != nil {
return err
}
node, err := store.Read(ctx, txn, path)
if err != nil {
if storage.IsNotFound(err) {
return nil
}
return err
}
switch node := node.(type) {
case map[string]interface{}:
for key := range node {
ground = append(ground, ast.StringTerm(key))
cpy := bindings.Copy()
cpy.Put(headVar, ast.String(key))
err := iterStorage(ctx, store, txn, tail, ground, cpy, iter)
if err != nil {
return err
}
ground = ground[:len(ground)-1]
}
case []interface{}:
for i := range node {
idx := ast.IntNumberTerm(i)
ground = append(ground, idx)
cpy := bindings.Copy()
cpy.Put(headVar, idx.Value)
err := iterStorage(ctx, store, txn, tail, ground, cpy, iter)
if err != nil {
return err
}
ground = ground[:len(ground)-1]
}
}
return nil
}

View File

@@ -0,0 +1,287 @@
// 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 inmem implements an in-memory version of the policy engine's storage
// layer.
//
// The in-memory store is used as the default storage layer implementation. The
// in-memory store supports multi-reader/single-writer concurrency with
// rollback.
//
// Callers should assume the in-memory store does not make copies of written
// data. Once data is written to the in-memory store, it should not be modified
// (outside of calling Store.Write). Furthermore, data read from the in-memory
// store should be treated as read-only.
package inmem
import (
"context"
"fmt"
"io"
"sync"
"sync/atomic"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/storage"
"github.com/open-policy-agent/opa/util"
)
// New returns an empty in-memory store.
func New() storage.Store {
return &store{
data: map[string]interface{}{},
triggers: map[*handle]storage.TriggerConfig{},
policies: map[string][]byte{},
indices: newIndices(),
}
}
// NewFromObject returns a new in-memory store from the supplied data object.
func NewFromObject(data map[string]interface{}) storage.Store {
db := New()
ctx := context.Background()
txn, err := db.NewTransaction(ctx, storage.WriteParams)
if err != nil {
panic(err)
}
if err := db.Write(ctx, txn, storage.AddOp, storage.Path{}, data); err != nil {
panic(err)
}
if err := db.Commit(ctx, txn); err != nil {
panic(err)
}
return db
}
// NewFromReader returns a new in-memory store from a reader that produces a
// JSON serialized object. This function is for test purposes.
func NewFromReader(r io.Reader) storage.Store {
d := util.NewJSONDecoder(r)
var data map[string]interface{}
if err := d.Decode(&data); err != nil {
panic(err)
}
return NewFromObject(data)
}
type store struct {
rmu sync.RWMutex // reader-writer lock
wmu sync.Mutex // writer lock
xid uint64 // last generated transaction id
data map[string]interface{} // raw data
policies map[string][]byte // raw policies
triggers map[*handle]storage.TriggerConfig // registered triggers
indices *indices // data ref indices
}
type handle struct {
db *store
}
func (db *store) NewTransaction(ctx context.Context, params ...storage.TransactionParams) (storage.Transaction, error) {
var write bool
var context *storage.Context
if len(params) > 0 {
write = params[0].Write
context = params[0].Context
}
xid := atomic.AddUint64(&db.xid, uint64(1))
if write {
db.wmu.Lock()
} else {
db.rmu.RLock()
}
return newTransaction(xid, write, context, db), nil
}
func (db *store) Commit(ctx context.Context, txn storage.Transaction) error {
underlying, err := db.underlying(txn)
if err != nil {
return err
}
if underlying.write {
db.rmu.Lock()
event := underlying.Commit()
db.indices = newIndices()
db.runOnCommitTriggers(ctx, txn, event)
// Mark the transaction stale after executing triggers so they can
// perform store operations if needed.
underlying.stale = true
db.rmu.Unlock()
db.wmu.Unlock()
} else {
db.rmu.RUnlock()
}
return nil
}
func (db *store) Abort(ctx context.Context, txn storage.Transaction) {
underlying, err := db.underlying(txn)
if err != nil {
panic(err)
}
underlying.stale = true
if underlying.write {
db.wmu.Unlock()
} else {
db.rmu.RUnlock()
}
}
func (db *store) ListPolicies(_ context.Context, txn storage.Transaction) ([]string, error) {
underlying, err := db.underlying(txn)
if err != nil {
return nil, err
}
return underlying.ListPolicies(), nil
}
func (db *store) GetPolicy(_ context.Context, txn storage.Transaction, id string) ([]byte, error) {
underlying, err := db.underlying(txn)
if err != nil {
return nil, err
}
return underlying.GetPolicy(id)
}
func (db *store) UpsertPolicy(_ context.Context, txn storage.Transaction, id string, bs []byte) error {
underlying, err := db.underlying(txn)
if err != nil {
return err
}
return underlying.UpsertPolicy(id, bs)
}
func (db *store) DeletePolicy(_ context.Context, txn storage.Transaction, id string) error {
underlying, err := db.underlying(txn)
if err != nil {
return err
}
if _, err := underlying.GetPolicy(id); err != nil {
return err
}
return underlying.DeletePolicy(id)
}
func (db *store) Register(ctx context.Context, txn storage.Transaction, config storage.TriggerConfig) (storage.TriggerHandle, error) {
underlying, err := db.underlying(txn)
if err != nil {
return nil, err
}
if !underlying.write {
return nil, &storage.Error{
Code: storage.InvalidTransactionErr,
Message: "triggers must be registered with a write transaction",
}
}
h := &handle{db}
db.triggers[h] = config
return h, nil
}
func (db *store) Read(ctx context.Context, txn storage.Transaction, path storage.Path) (interface{}, error) {
underlying, err := db.underlying(txn)
if err != nil {
return nil, err
}
return underlying.Read(path)
}
func (db *store) Write(ctx context.Context, txn storage.Transaction, op storage.PatchOp, path storage.Path, value interface{}) error {
underlying, err := db.underlying(txn)
if err != nil {
return err
}
val := util.Reference(value)
if err := util.RoundTrip(val); err != nil {
return err
}
return underlying.Write(op, path, *val)
}
func (db *store) Build(ctx context.Context, txn storage.Transaction, ref ast.Ref) (storage.Index, error) {
underlying, err := db.underlying(txn)
if err != nil {
return nil, err
}
if underlying.write {
return nil, &storage.Error{
Code: storage.IndexingNotSupportedErr,
Message: "in-memory store does not support indexing on write transactions",
}
}
return db.indices.Build(ctx, db, txn, ref)
}
func (h *handle) Unregister(ctx context.Context, txn storage.Transaction) {
underlying, err := h.db.underlying(txn)
if err != nil {
panic(err)
}
if !underlying.write {
panic(&storage.Error{
Code: storage.InvalidTransactionErr,
Message: "triggers must be unregistered with a write transaction",
})
}
delete(h.db.triggers, h)
}
func (db *store) runOnCommitTriggers(ctx context.Context, txn storage.Transaction, event storage.TriggerEvent) {
for _, t := range db.triggers {
t.OnCommit(ctx, txn, event)
}
}
func (db *store) underlying(txn storage.Transaction) (*transaction, error) {
underlying, ok := txn.(*transaction)
if !ok {
return nil, &storage.Error{
Code: storage.InvalidTransactionErr,
Message: fmt.Sprintf("unexpected transaction type %T", txn),
}
}
if underlying.db != db {
return nil, &storage.Error{
Code: storage.InvalidTransactionErr,
Message: "unknown transaction",
}
}
if underlying.stale {
return nil, &storage.Error{
Code: storage.InvalidTransactionErr,
Message: "stale transaction",
}
}
return underlying, nil
}
var doesNotExistMsg = "document does not exist"
var rootMustBeObjectMsg = "root must be object"
var rootCannotBeRemovedMsg = "root cannot be removed"
var outOfRangeMsg = "array index out of range"
var arrayIndexTypeMsg = "array index must be integer"
func invalidPatchError(f string, a ...interface{}) *storage.Error {
return &storage.Error{
Code: storage.InvalidPatchErr,
Message: fmt.Sprintf(f, a...),
}
}
func notFoundError(path storage.Path) *storage.Error {
return notFoundErrorHint(path, doesNotExistMsg)
}
func notFoundErrorHint(path storage.Path, hint string) *storage.Error {
return notFoundErrorf("%v: %v", path.String(), hint)
}
func notFoundErrorf(f string, a ...interface{}) *storage.Error {
msg := fmt.Sprintf(f, a...)
return &storage.Error{
Code: storage.NotFoundErr,
Message: msg,
}
}

View File

@@ -0,0 +1,444 @@
// Copyright 2017 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 inmem
import (
"container/list"
"encoding/json"
"strconv"
"github.com/open-policy-agent/opa/storage"
)
// transaction implements the low-level read/write operations on the in-memory
// store and contains the state required for pending transactions.
//
// For write transactions, the struct contains a logical set of updates
// performed by write operations in the transaction. Each write operation
// compacts the set such that two updates never overlap:
//
// - If new update path is a prefix of existing update path, existing update is
// removed, new update is added.
//
// - If existing update path is a prefix of new update path, existing update is
// modified.
//
// - Otherwise, new update is added.
//
// Read transactions do not require any special handling and simply passthrough
// to the underlying store. Read transactions do not support upgrade.
type transaction struct {
xid uint64
write bool
stale bool
db *store
updates *list.List
policies map[string]policyUpdate
context *storage.Context
}
type policyUpdate struct {
value []byte
remove bool
}
func newTransaction(xid uint64, write bool, context *storage.Context, db *store) *transaction {
return &transaction{
xid: xid,
write: write,
db: db,
policies: map[string]policyUpdate{},
updates: list.New(),
context: context,
}
}
func (txn *transaction) ID() uint64 {
return txn.xid
}
func (txn *transaction) Write(op storage.PatchOp, path storage.Path, value interface{}) error {
if !txn.write {
return &storage.Error{
Code: storage.InvalidTransactionErr,
Message: "data write during read transaction",
}
}
if len(path) == 0 {
return txn.updateRoot(op, value)
}
for curr := txn.updates.Front(); curr != nil; {
update := curr.Value.(*update)
// Check if new update masks existing update exactly. In this case, the
// existing update can be removed and no other updates have to be
// visited (because no two updates overlap.)
if update.path.Equal(path) {
if update.remove {
if op != storage.AddOp {
return notFoundError(path)
}
}
txn.updates.Remove(curr)
break
}
// Check if new update masks existing update. In this case, the
// existing update has to be removed but other updates may overlap, so
// we must continue.
if update.path.HasPrefix(path) {
remove := curr
curr = curr.Next()
txn.updates.Remove(remove)
continue
}
// Check if new update modifies existing update. In this case, the
// existing update is mutated.
if path.HasPrefix(update.path) {
if update.remove {
return notFoundError(path)
}
suffix := path[len(update.path):]
newUpdate, err := newUpdate(update.value, op, suffix, 0, value)
if err != nil {
return err
}
update.value = newUpdate.Apply(update.value)
return nil
}
curr = curr.Next()
}
update, err := newUpdate(txn.db.data, op, path, 0, value)
if err != nil {
return err
}
txn.updates.PushFront(update)
return nil
}
func (txn *transaction) updateRoot(op storage.PatchOp, value interface{}) error {
if op == storage.RemoveOp {
return invalidPatchError(rootCannotBeRemovedMsg)
}
if _, ok := value.(map[string]interface{}); !ok {
return invalidPatchError(rootMustBeObjectMsg)
}
txn.updates.Init()
txn.updates.PushFront(&update{
path: storage.Path{},
remove: false,
value: value,
})
return nil
}
func (txn *transaction) Commit() (result storage.TriggerEvent) {
result.Context = txn.context
for curr := txn.updates.Front(); curr != nil; curr = curr.Next() {
action := curr.Value.(*update)
updated := action.Apply(txn.db.data)
txn.db.data = updated.(map[string]interface{})
result.Data = append(result.Data, storage.DataEvent{
Path: action.path,
Data: action.value,
Removed: action.remove,
})
}
for id, update := range txn.policies {
if update.remove {
delete(txn.db.policies, id)
} else {
txn.db.policies[id] = update.value
}
result.Policy = append(result.Policy, storage.PolicyEvent{
ID: id,
Data: update.value,
Removed: update.remove,
})
}
return result
}
func (txn *transaction) Read(path storage.Path) (interface{}, error) {
if !txn.write {
return ptr(txn.db.data, path)
}
merge := []*update{}
for curr := txn.updates.Front(); curr != nil; curr = curr.Next() {
update := curr.Value.(*update)
if path.HasPrefix(update.path) {
if update.remove {
return nil, notFoundError(path)
}
return ptr(update.value, path[len(update.path):])
}
if update.path.HasPrefix(path) {
merge = append(merge, update)
}
}
data, err := ptr(txn.db.data, path)
if err != nil {
return nil, err
}
if len(merge) == 0 {
return data, nil
}
cpy := deepCopy(data)
for _, update := range merge {
cpy = update.Relative(path).Apply(cpy)
}
return cpy, nil
}
func (txn *transaction) ListPolicies() []string {
var ids []string
for id := range txn.db.policies {
if _, ok := txn.policies[id]; !ok {
ids = append(ids, id)
}
}
for id, update := range txn.policies {
if !update.remove {
ids = append(ids, id)
}
}
return ids
}
func (txn *transaction) GetPolicy(id string) ([]byte, error) {
if update, ok := txn.policies[id]; ok {
if !update.remove {
return update.value, nil
}
return nil, notFoundErrorf("policy id %q", id)
}
if exist, ok := txn.db.policies[id]; ok {
return exist, nil
}
return nil, notFoundErrorf("policy id %q", id)
}
func (txn *transaction) UpsertPolicy(id string, bs []byte) error {
if !txn.write {
return &storage.Error{
Code: storage.InvalidTransactionErr,
Message: "policy write during read transaction",
}
}
txn.policies[id] = policyUpdate{bs, false}
return nil
}
func (txn *transaction) DeletePolicy(id string) error {
if !txn.write {
return &storage.Error{
Code: storage.InvalidTransactionErr,
Message: "policy write during read transaction",
}
}
txn.policies[id] = policyUpdate{nil, true}
return nil
}
// update contains state associated with an update to be applied to the
// in-memory data store.
type update struct {
path storage.Path // data path modified by update
remove bool // indicates whether update removes the value at path
value interface{} // value to add/replace at path (ignored if remove is true)
}
func newUpdate(data interface{}, op storage.PatchOp, path storage.Path, idx int, value interface{}) (*update, error) {
switch data := data.(type) {
case map[string]interface{}:
return newUpdateObject(data, op, path, idx, value)
case []interface{}:
return newUpdateArray(data, op, path, idx, value)
case nil, bool, json.Number, string:
return nil, notFoundError(path)
}
return nil, &storage.Error{
Code: storage.InternalErr,
Message: "invalid data value encountered",
}
}
func newUpdateArray(data []interface{}, op storage.PatchOp, path storage.Path, idx int, value interface{}) (*update, error) {
if idx == len(path)-1 {
if path[idx] == "-" {
if op != storage.AddOp {
return nil, invalidPatchError("%v: invalid patch path", path)
}
cpy := make([]interface{}, len(data)+1)
copy(cpy, data)
cpy[len(data)] = value
return &update{path[:len(path)-1], false, cpy}, nil
}
pos, err := validateArrayIndex(data, path[idx], path)
if err != nil {
return nil, err
}
if op == storage.AddOp {
cpy := make([]interface{}, len(data)+1)
copy(cpy[:pos], data[:pos])
copy(cpy[pos+1:], data[pos:])
cpy[pos] = value
return &update{path[:len(path)-1], false, cpy}, nil
} else if op == storage.RemoveOp {
cpy := make([]interface{}, len(data)-1)
copy(cpy[:pos], data[:pos])
copy(cpy[pos:], data[pos+1:])
return &update{path[:len(path)-1], false, cpy}, nil
} else {
cpy := make([]interface{}, len(data))
copy(cpy, data)
cpy[pos] = value
return &update{path[:len(path)-1], false, cpy}, nil
}
}
pos, err := validateArrayIndex(data, path[idx], path)
if err != nil {
return nil, err
}
return newUpdate(data[pos], op, path, idx+1, value)
}
func newUpdateObject(data map[string]interface{}, op storage.PatchOp, path storage.Path, idx int, value interface{}) (*update, error) {
if idx == len(path)-1 {
switch op {
case storage.ReplaceOp, storage.RemoveOp:
if _, ok := data[path[idx]]; !ok {
return nil, notFoundError(path)
}
}
return &update{path, op == storage.RemoveOp, value}, nil
}
if data, ok := data[path[idx]]; ok {
return newUpdate(data, op, path, idx+1, value)
}
return nil, notFoundError(path)
}
func (u *update) Apply(data interface{}) interface{} {
if len(u.path) == 0 {
return u.value
}
parent, err := ptr(data, u.path[:len(u.path)-1])
if err != nil {
panic(err)
}
key := u.path[len(u.path)-1]
if u.remove {
obj := parent.(map[string]interface{})
delete(obj, key)
return data
}
switch parent := parent.(type) {
case map[string]interface{}:
parent[key] = u.value
case []interface{}:
idx, err := strconv.Atoi(key)
if err != nil {
panic(err)
}
parent[idx] = u.value
}
return data
}
func (u *update) Relative(path storage.Path) *update {
cpy := *u
cpy.path = cpy.path[len(path):]
return &cpy
}
func deepCopy(val interface{}) interface{} {
switch val := val.(type) {
case []interface{}:
cpy := make([]interface{}, len(val))
for i := range cpy {
cpy[i] = deepCopy(val[i])
}
return cpy
case map[string]interface{}:
cpy := make(map[string]interface{}, len(val))
for k := range val {
cpy[k] = deepCopy(val[k])
}
return cpy
default:
return val
}
}
func ptr(data interface{}, path storage.Path) (interface{}, error) {
node := data
for i := range path {
key := path[i]
switch curr := node.(type) {
case map[string]interface{}:
var ok bool
if node, ok = curr[key]; !ok {
return nil, notFoundError(path)
}
case []interface{}:
pos, err := validateArrayIndex(curr, key, path)
if err != nil {
return nil, err
}
node = curr[pos]
default:
return nil, notFoundError(path)
}
}
return node, nil
}
func validateArrayIndex(arr []interface{}, s string, path storage.Path) (int, error) {
idx, err := strconv.Atoi(s)
if err != nil {
return 0, notFoundErrorHint(path, arrayIndexTypeMsg)
}
if idx < 0 || idx >= len(arr) {
return 0, notFoundErrorHint(path, outOfRangeMsg)
}
return idx, nil
}

View File

@@ -0,0 +1,219 @@
// 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 storage
import (
"context"
"github.com/open-policy-agent/opa/ast"
)
// Transaction defines the interface that identifies a consistent snapshot over
// the policy engine's storage layer.
type Transaction interface {
ID() uint64
}
// Store defines the interface for the storage layer's backend.
type Store interface {
Trigger
Policy
Indexing
// NewTransaction is called create a new transaction in the store.
NewTransaction(ctx context.Context, params ...TransactionParams) (Transaction, error)
// Read is called to fetch a document referred to by path.
Read(ctx context.Context, txn Transaction, path Path) (interface{}, error)
// Write is called to modify a document referred to by path.
Write(ctx context.Context, txn Transaction, op PatchOp, path Path, value interface{}) error
// Commit is called to finish the transaction. If Commit returns an error, the
// transaction must be automatically aborted by the Store implementation.
Commit(ctx context.Context, txn Transaction) error
// Abort is called to cancel the transaction.
Abort(ctx context.Context, txn Transaction)
}
// TransactionParams describes a new transaction.
type TransactionParams struct {
// Write indicates if this transaction will perform any write operations.
Write bool
// Context contains key/value pairs passed to triggers.
Context *Context
}
// Context is a simple container for key/value pairs.
type Context struct {
values map[interface{}]interface{}
}
// NewContext returns a new context object.
func NewContext() *Context {
return &Context{
values: map[interface{}]interface{}{},
}
}
// Get returns the key value in the context.
func (ctx *Context) Get(key interface{}) interface{} {
if ctx == nil {
return nil
}
return ctx.values[key]
}
// Put adds a key/value pair to the context.
func (ctx *Context) Put(key, value interface{}) {
ctx.values[key] = value
}
// WriteParams specifies the TransactionParams for a write transaction.
var WriteParams = TransactionParams{
Write: true,
}
// PatchOp is the enumeration of supposed modifications.
type PatchOp int
// Patch supports add, remove, and replace operations.
const (
AddOp PatchOp = iota
RemoveOp = iota
ReplaceOp = iota
)
// WritesNotSupported provides a default implementation of the write
// interface which may be used if the backend does not support writes.
type WritesNotSupported struct{}
func (WritesNotSupported) Write(ctx context.Context, txn Transaction, op PatchOp, path Path, value interface{}) error {
return writesNotSupportedError()
}
// Policy defines the interface for policy module storage.
type Policy interface {
ListPolicies(context.Context, Transaction) ([]string, error)
GetPolicy(context.Context, Transaction, string) ([]byte, error)
UpsertPolicy(context.Context, Transaction, string, []byte) error
DeletePolicy(context.Context, Transaction, string) error
}
// PolicyNotSupported provides a default implementation of the policy interface
// which may be used if the backend does not support policy storage.
type PolicyNotSupported struct{}
// ListPolicies always returns a PolicyNotSupportedErr.
func (PolicyNotSupported) ListPolicies(context.Context, Transaction) ([]string, error) {
return nil, policyNotSupportedError()
}
// GetPolicy always returns a PolicyNotSupportedErr.
func (PolicyNotSupported) GetPolicy(context.Context, Transaction, string) ([]byte, error) {
return nil, policyNotSupportedError()
}
// UpsertPolicy always returns a PolicyNotSupportedErr.
func (PolicyNotSupported) UpsertPolicy(context.Context, Transaction, string, []byte) error {
return policyNotSupportedError()
}
// DeletePolicy always returns a PolicyNotSupportedErr.
func (PolicyNotSupported) DeletePolicy(context.Context, Transaction, string) error {
return policyNotSupportedError()
}
// PolicyEvent describes a change to a policy.
type PolicyEvent struct {
ID string
Data []byte
Removed bool
}
// DataEvent describes a change to a base data document.
type DataEvent struct {
Path Path
Data interface{}
Removed bool
}
// TriggerEvent describes the changes that caused the trigger to be invoked.
type TriggerEvent struct {
Policy []PolicyEvent
Data []DataEvent
Context *Context
}
// IsZero returns true if the TriggerEvent indicates no changes occurred. This
// function is primarily for test purposes.
func (e TriggerEvent) IsZero() bool {
return !e.PolicyChanged() && !e.DataChanged()
}
// PolicyChanged returns true if the trigger was caused by a policy change.
func (e TriggerEvent) PolicyChanged() bool {
return len(e.Policy) > 0
}
// DataChanged returns true if the trigger was caused by a data change.
func (e TriggerEvent) DataChanged() bool {
return len(e.Data) > 0
}
// TriggerConfig contains the trigger registration configuration.
type TriggerConfig struct {
// OnCommit is invoked when a transaction is successfully committed. The
// callback is invoked with a handle to the write transaction that
// successfully committed before other clients see the changes.
OnCommit func(ctx context.Context, txn Transaction, event TriggerEvent)
}
// Trigger defines the interface that stores implement to register for change
// notifications when the store is changed.
type Trigger interface {
Register(ctx context.Context, txn Transaction, config TriggerConfig) (TriggerHandle, error)
}
// TriggersNotSupported provides default implementations of the Trigger
// interface which may be used if the backend does not support triggers.
type TriggersNotSupported struct{}
// Register always returns an error indicating triggers are not supported.
func (TriggersNotSupported) Register(context.Context, Transaction, TriggerConfig) (TriggerHandle, error) {
return nil, triggersNotSupportedError()
}
// TriggerHandle defines the interface that can be used to unregister triggers that have
// been registered on a Store.
type TriggerHandle interface {
Unregister(ctx context.Context, txn Transaction)
}
// IndexIterator defines the interface for iterating over index results.
type IndexIterator func(*ast.ValueMap) error
// Indexing defines the interface for building an index.
type Indexing interface {
Build(ctx context.Context, txn Transaction, ref ast.Ref) (Index, error)
}
// Index defines the interface for searching a pre-built index.
type Index interface {
Lookup(ctx context.Context, txn Transaction, value interface{}, iter IndexIterator) error
}
// IndexingNotSupported provides default implementations of the Indexing
// interface which may be used if the backend does not support indexing.
type IndexingNotSupported struct{}
// Build always returns an error indicating indexing is not supported.
func (IndexingNotSupported) Build(context.Context, Transaction, ast.Ref) (Index, error) {
return nil, indexingNotSupportedError()
}

154
vendor/github.com/open-policy-agent/opa/storage/path.go generated vendored Normal file
View File

@@ -0,0 +1,154 @@
// 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 storage
import (
"fmt"
"net/url"
"strconv"
"strings"
"github.com/open-policy-agent/opa/ast"
)
// Path refers to a document in storage.
type Path []string
// ParsePath returns a new path for the given str.
func ParsePath(str string) (path Path, ok bool) {
if len(str) == 0 {
return nil, false
}
if str[0] != '/' {
return nil, false
}
if len(str) == 1 {
return Path{}, true
}
parts := strings.Split(str[1:], "/")
return parts, true
}
// ParsePathEscaped returns a new path for the given escaped str.
func ParsePathEscaped(str string) (path Path, ok bool) {
path, ok = ParsePath(str)
if !ok {
return
}
for i := range path {
segment, err := url.PathUnescape(path[i])
if err == nil {
path[i] = segment
}
}
return
}
// NewPathForRef returns a new path for the given ref.
func NewPathForRef(ref ast.Ref) (path Path, err error) {
if len(ref) == 0 {
return nil, fmt.Errorf("empty reference (indicates error in caller)")
}
if len(ref) == 1 {
return Path{}, nil
}
path = make(Path, 0, len(ref)-1)
for _, term := range ref[1:] {
switch v := term.Value.(type) {
case ast.String:
path = append(path, string(v))
case ast.Number:
path = append(path, v.String())
case ast.Boolean, ast.Null:
return nil, &Error{
Code: NotFoundErr,
Message: fmt.Sprintf("%v: does not exist", ref),
}
case ast.Array, ast.Object, ast.Set:
return nil, fmt.Errorf("composites cannot be base document keys: %v", ref)
default:
return nil, fmt.Errorf("unresolved reference (indicates error in caller): %v", ref)
}
}
return path, nil
}
// Compare performs lexigraphical comparison on p and other and returns -1 if p
// is less than other, 0 if p is equal to other, or 1 if p is greater than
// other.
func (p Path) Compare(other Path) (cmp int) {
min := len(p)
if len(other) < min {
min = len(other)
}
for i := 0; i < min; i++ {
if cmp := strings.Compare(p[i], other[i]); cmp != 0 {
return cmp
}
}
if len(p) < len(other) {
return -1
}
if len(p) == len(other) {
return 0
}
return 1
}
// Equal returns true if p is the same as other.
func (p Path) Equal(other Path) bool {
return p.Compare(other) == 0
}
// HasPrefix returns true if p starts with other.
func (p Path) HasPrefix(other Path) bool {
if len(other) > len(p) {
return false
}
for i := range other {
if p[i] != other[i] {
return false
}
}
return true
}
// Ref returns a ref that represents p rooted at head.
func (p Path) Ref(head *ast.Term) (ref ast.Ref) {
ref = make(ast.Ref, len(p)+1)
ref[0] = head
for i := range p {
idx, err := strconv.ParseInt(p[i], 10, 64)
if err == nil {
ref[i+1] = ast.IntNumberTerm(int(idx))
} else {
ref[i+1] = ast.StringTerm(p[i])
}
}
return ref
}
func (p Path) String() string {
buf := make([]string, len(p))
for i := range buf {
buf[i] = url.PathEscape(p[i])
}
return "/" + strings.Join(buf, "/")
}
// MustParsePath returns a new Path for s. If s cannot be parsed, this function
// will panic. This is mostly for test purposes.
func MustParsePath(s string) Path {
path, ok := ParsePath(s)
if !ok {
panic(s)
}
return path
}

View File

@@ -0,0 +1,126 @@
// 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 storage
import (
"context"
)
// NewTransactionOrDie is a helper function to create a new transaction. If the
// storage layer cannot create a new transaction, this function will panic. This
// function should only be used for tests.
func NewTransactionOrDie(ctx context.Context, store Store, params ...TransactionParams) Transaction {
txn, err := store.NewTransaction(ctx, params...)
if err != nil {
panic(err)
}
return txn
}
// ReadOne is a convenience function to read a single value from the provided Store. It
// will create a new Transaction to perform the read with, and clean up after itself
// should an error occur.
func ReadOne(ctx context.Context, store Store, path Path) (interface{}, error) {
txn, err := store.NewTransaction(ctx)
if err != nil {
return nil, err
}
defer store.Abort(ctx, txn)
return store.Read(ctx, txn, path)
}
// WriteOne is a convenience function to write a single value to the provided Store. It
// will create a new Transaction to perform the write with, and clean up after itself
// should an error occur.
func WriteOne(ctx context.Context, store Store, op PatchOp, path Path, value interface{}) error {
txn, err := store.NewTransaction(ctx, WriteParams)
if err != nil {
return err
}
if err := store.Write(ctx, txn, op, path, value); err != nil {
store.Abort(ctx, txn)
return err
}
return store.Commit(ctx, txn)
}
// MakeDir inserts an empty object at path. If the parent path does not exist,
// MakeDir will create it recursively.
func MakeDir(ctx context.Context, store Store, txn Transaction, path Path) (err error) {
if len(path) == 0 {
return nil
}
node, err := store.Read(ctx, txn, path)
if err != nil {
if !IsNotFound(err) {
return err
}
if err := MakeDir(ctx, store, txn, path[:len(path)-1]); err != nil {
return err
} else if err := store.Write(ctx, txn, AddOp, path, map[string]interface{}{}); err != nil {
return err
}
return nil
}
if _, ok := node.(map[string]interface{}); ok {
return nil
}
return writeConflictError(path)
}
// Txn is a convenience function that executes f inside a new transaction
// opened on the store. If the function returns an error, the transaction is
// aborted and the error is returned. Otherwise, the transaction is committed
// and the result of the commit is returned.
func Txn(ctx context.Context, store Store, params TransactionParams, f func(Transaction) error) error {
txn, err := store.NewTransaction(ctx, params)
if err != nil {
return err
}
if err := f(txn); err != nil {
store.Abort(ctx, txn)
return err
}
return store.Commit(ctx, txn)
}
// NonEmpty returns a function that tests if a path is non-empty. A
// path is non-empty if a Read on the path returns a value or a Read
// on any of the path prefixes returns a non-object value.
func NonEmpty(ctx context.Context, store Store, txn Transaction) func([]string) (bool, error) {
return func(path []string) (bool, error) {
if _, err := store.Read(ctx, txn, Path(path)); err == nil {
return true, nil
} else if !IsNotFound(err) {
return false, err
}
for i := len(path) - 1; i > 0; i-- {
val, err := store.Read(ctx, txn, Path(path[:i]))
if err != nil && !IsNotFound(err) {
return false, err
} else if err == nil {
if _, ok := val.(map[string]interface{}); ok {
return false, nil
}
return true, nil
}
}
return false, nil
}
}