Skip to content

Commit

Permalink
spike named/anonymous caveats
Browse files Browse the repository at this point in the history
  • Loading branch information
vroldanbet committed Sep 8, 2022
1 parent 5369a0e commit 9f0d6b6
Show file tree
Hide file tree
Showing 10 changed files with 1,537 additions and 558 deletions.
97 changes: 97 additions & 0 deletions internal/datastore/memdb/caveat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package memdb

import (
"github.com/authzed/spicedb/pkg/datastore"
core "github.com/authzed/spicedb/pkg/proto/core/v1"

"github.com/hashicorp/go-memdb"
)

const tableCaveats = "caveats"

type caveat struct {
digest string
logic []byte
caveatType core.Caveat_Type
}

func (c *caveat) CoreCaveat() *core.Caveat {
return &core.Caveat{
Digest: c.digest,
Logic: c.logic,
Type: c.caveatType,
}
}

func (r *memdbReader) ReadCaveat(digest string) (datastore.CaveatIterator, error) {
r.lockOrPanic()
defer r.Unlock()

tx, err := r.txSource()
if err != nil {
return nil, err
}
return r.readCaveat(tx, digest)
}

func (r *memdbReader) readCaveat(tx *memdb.Txn, digest string) (datastore.CaveatIterator, error) {
it, err := tx.Get(tableCaveats, indexID, digest)
if err != nil {
return nil, err
}
return &memdbCaveatIterator{it: it}, nil
}

func (rwt *memdbReadWriteTx) WriteCaveat(caveats []*core.Caveat) error {
rwt.lockOrPanic()
defer rwt.Unlock()
tx, err := rwt.txSource()
if err != nil {
return err
}
return rwt.writeCaveat(tx, caveats)
}

func (rwt *memdbReadWriteTx) writeCaveat(tx *memdb.Txn, caveats []*core.Caveat) error {
for _, coreCaveat := range caveats {
c := caveat{
digest: coreCaveat.Digest,
logic: coreCaveat.Logic,
caveatType: coreCaveat.Type,
}
if err := tx.Insert(tableCaveats, &c); err != nil {
return err
}
}
return nil
}

func (rwt *memdbReadWriteTx) DeleteCaveat(caveats []*core.Caveat) error {
rwt.lockOrPanic()
defer rwt.Unlock()
tx, err := rwt.txSource()
if err != nil {
return err
}
return tx.Delete(tableCaveats, caveats)
}

type memdbCaveatIterator struct {
it memdb.ResultIterator
}

func (mci *memdbCaveatIterator) Next() *core.Caveat {
foundRaw := mci.it.Next()
if foundRaw == nil {
return nil
}

c := foundRaw.(*caveat)
return c.CoreCaveat()
}

func (mci *memdbCaveatIterator) Err() error {
return nil
}

func (mci *memdbCaveatIterator) Close() {}
135 changes: 135 additions & 0 deletions internal/datastore/memdb/caveat_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package memdb

import (
"context"
"testing"
"time"

"github.com/authzed/spicedb/internal/datastore/common"
"github.com/authzed/spicedb/internal/testfixtures"
"github.com/authzed/spicedb/pkg/caveats"
"github.com/authzed/spicedb/pkg/datastore"
core "github.com/authzed/spicedb/pkg/proto/core/v1"
"github.com/authzed/spicedb/pkg/tuple"

"github.com/stretchr/testify/require"
)

func TestWriteReadCaveat(t *testing.T) {
req := require.New(t)

ds, err := NewMemdbDatastore(0, 1*time.Hour, 1*time.Hour)
req.NoError(err)
c := createCompiledCaveat(t)
coreCaveat := createCoreCaveat(t, c, false)
ctx := context.Background()
rev, err := writeCaveat(ctx, ds, coreCaveat)
req.NoError(err)
cr, ok := ds.SnapshotReader(rev).(datastore.CaveatReader)
req.True(ok, "expected a CaveatStorer value")
it, err := cr.ReadCaveat(c.Name())
req.NoError(err)
cv := it.Next()
req.Equal(coreCaveat, cv)
req.Equal(core.Caveat_NAMED, cv.Type)
req.NoError(err)

it, err = cr.ReadCaveat("doesnotexist")
req.NoError(err)
cv = it.Next()
req.Nil(cv)
}

func TestWriteTupleWithNamedCaveat(t *testing.T) {
req := require.New(t)
ctx := context.Background()

ds, err := NewMemdbDatastore(0, 1*time.Hour, 1*time.Hour)
req.NoError(err)
sds, _ := testfixtures.StandardDatastoreWithSchema(ds, req)
tpl := tuple.MustParse("document:companyplan#parent@folder:company#...")
c := createCompiledCaveat(t)
coreCaveat := createCoreCaveat(t, c, false)
tpl.Caveat = &core.CaveatReference{
Caveat: coreCaveat,
}
_, err = common.WriteTuples(ctx, sds, core.RelationTupleUpdate_CREATE, tpl)
// should fail because the name caveat is not present in the datastore
req.Error(err)
// let's write the named caveat and try again
_, err = writeCaveat(ctx, ds, coreCaveat)
req.NoError(err)
rev, err := common.WriteTuples(ctx, sds, core.RelationTupleUpdate_CREATE, tpl)
req.NoError(err)
iter, err := ds.SnapshotReader(rev).QueryRelationships(ctx, datastore.RelationshipsFilter{
ResourceType: tpl.ResourceAndRelation.Namespace,
})
req.NoError(err)
defer iter.Close()
readTpl := iter.Next()
req.Equal(tpl, readTpl)
}

func TestWriteTupleWithAnonymousCaveat(t *testing.T) {
req := require.New(t)
ctx := context.Background()

ds, err := NewMemdbDatastore(0, 1*time.Hour, 1*time.Hour)
req.NoError(err)
sds, _ := testfixtures.StandardDatastoreWithSchema(ds, req)
tpl := tuple.MustParse("document:companyplan#parent@folder:company#...")
c := createCompiledCaveat(t)
coreCaveat := createCoreCaveat(t, c, true)
tpl.Caveat = &core.CaveatReference{
Caveat: coreCaveat,
}
rev, err := common.WriteTuples(ctx, sds, core.RelationTupleUpdate_CREATE, tpl)
// the caveat is anonymous and is created alongside the tuple
req.NoError(err)
iter, err := ds.SnapshotReader(rev).QueryRelationships(ctx, datastore.RelationshipsFilter{
ResourceType: tpl.ResourceAndRelation.Namespace,
})
req.NoError(err)
defer iter.Close()
readTpl := iter.Next()
req.Equal(tpl, readTpl)
}

func writeCaveat(ctx context.Context, ds datastore.Datastore, coreCaveat *core.Caveat) (datastore.Revision, error) {
return ds.ReadWriteTx(ctx, func(ctx context.Context, tx datastore.ReadWriteTransaction) error {
cs, ok := tx.(datastore.CaveatStorer)
if !ok {
panic("expected a CaveatStorer value")
}
return cs.WriteCaveat([]*core.Caveat{coreCaveat})
})
}

func createCoreCaveat(t *testing.T, c *caveats.CompiledCaveat, anonymous bool) *core.Caveat {
t.Helper()
cBytes, err := c.Serialize()
require.NoError(t, err)
ty := core.Caveat_NAMED
if anonymous {
ty = core.Caveat_ANONYMOUS
}
coreCaveat := &core.Caveat{
Digest: c.Name(),
Logic: cBytes,
Type: ty,
}
require.NoError(t, err)
return coreCaveat
}

func createCompiledCaveat(t *testing.T) *caveats.CompiledCaveat {
t.Helper()
env, err := caveats.EnvForVariables(map[string]caveats.VariableType{
"a": caveats.IntType,
"b": caveats.IntType,
})
require.NoError(t, err)
c, err := caveats.CompileCaveatWithName(env, "a == b", "test")
require.NoError(t, err)
return c
}
35 changes: 35 additions & 0 deletions internal/datastore/memdb/readwrite.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,18 @@ func (rwt *memdbReadWriteTx) WriteRelationships(mutations []*core.RelationTupleU
func (rwt *memdbReadWriteTx) write(tx *memdb.Txn, mutations ...*core.RelationTupleUpdate) error {
// Apply the mutations
for _, mutation := range mutations {
cr, err := rwt.prepareCaveat(tx, mutation)
if err != nil {
return err
}
rel := &relationship{
mutation.Tuple.ResourceAndRelation.Namespace,
mutation.Tuple.ResourceAndRelation.ObjectId,
mutation.Tuple.ResourceAndRelation.Relation,
mutation.Tuple.Subject.Namespace,
mutation.Tuple.Subject.ObjectId,
mutation.Tuple.Subject.Relation,
cr,
}

found, err := tx.First(
Expand Down Expand Up @@ -87,6 +92,36 @@ func (rwt *memdbReadWriteTx) write(tx *memdb.Txn, mutations ...*core.RelationTup
return nil
}

func (rwt *memdbReadWriteTx) prepareCaveat(tx *memdb.Txn, mutation *core.RelationTupleUpdate) (*caveatReference, error) {
var cr *caveatReference
if mutation.Tuple.Caveat != nil {
cr = &caveatReference{
caveat: &caveat{
digest: mutation.Tuple.Caveat.Caveat.Digest,
logic: mutation.Tuple.Caveat.Caveat.Logic,
caveatType: mutation.Tuple.Caveat.Caveat.Type,
},
}
switch mutation.Tuple.Caveat.Caveat.Type {
case core.Caveat_ANONYMOUS:
if err := rwt.writeCaveat(tx, []*core.Caveat{mutation.Tuple.Caveat.Caveat}); err != nil {
return nil, err
}
case core.Caveat_NAMED:
it, err := rwt.readCaveat(tx, mutation.Tuple.Caveat.Caveat.Digest)
if err != nil {
return nil, err
}
if it.Next() == nil {
return nil, fmt.Errorf("tuple referenced a non-existing named caveat %s", mutation.Tuple.Caveat.Caveat.Digest)
}
default:
return nil, fmt.Errorf("unknown caveat type")
}
}
return cr, nil
}

func (rwt *memdbReadWriteTx) DeleteRelationships(filter *v1.RelationshipFilter) error {
rwt.lockOrPanic()
defer rwt.Unlock()
Expand Down
24 changes: 24 additions & 0 deletions internal/datastore/memdb/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,19 @@ type relationship struct {
subjectNamespace string
subjectObjectID string
subjectRelation string
caveat *caveatReference
}

type caveatReference struct {
predefined map[string]any
caveat *caveat
}

func (cr *caveatReference) CaveatReference() *core.CaveatReference {
return &core.CaveatReference{
Caveat: cr.caveat.CoreCaveat(),
//Predefined: cr.predefined // TODO vroldanbet type mismatch
}
}

func (r relationship) MarshalZerologObject(e *zerolog.Event) {
Expand Down Expand Up @@ -88,6 +101,7 @@ func (r relationship) RelationTuple() *core.RelationTuple {
ObjectId: r.subjectObjectID,
Relation: r.subjectRelation,
},
Caveat: r.caveat.CaveatReference(),
}
}

Expand Down Expand Up @@ -178,5 +192,15 @@ var schema = &memdb.DBSchema{
},
},
},
tableCaveats: {
Name: tableCaveats,
Indexes: map[string]*memdb.IndexSchema{
indexID: {
Name: indexID,
Unique: true,
Indexer: &memdb.StringFieldIndex{Field: "digest"},
},
},
},
},
}
20 changes: 18 additions & 2 deletions pkg/caveats/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ type CompiledCaveat struct {

// ast is the AST form of the CEL program.
ast *cel.Ast

name string
}

// Name
func (cc CompiledCaveat) Name() string {
return cc.name
}

// ExprString returns the string-form of the caveat.
Expand Down Expand Up @@ -48,6 +55,15 @@ type CompilationErrors struct {
issues *cel.Issues
}

func CompileCaveatWithName(env *Environment, exprString, name string) (*CompiledCaveat, error) {
c, err := CompileCaveat(env, exprString)
if err != nil {
return nil, err
}
c.name = name
return c, nil
}

// CompileCaveat compiles a caveat string into a compiled caveat, or returns the compilation errors.
func CompileCaveat(env *Environment, exprString string) (*CompiledCaveat, error) {
celEnv, err := env.asCelEnvironment()
Expand All @@ -65,7 +81,7 @@ func CompileCaveat(env *Environment, exprString string) (*CompiledCaveat, error)
return nil, CompilationErrors{fmt.Errorf("caveat expression must result in a boolean value: found `%s`", ast.OutputType().String()), nil}
}

return &CompiledCaveat{celEnv, ast}, nil
return &CompiledCaveat{celEnv, ast, ""}, nil
}

// DeserializeCaveat deserializes a byte-serialized caveat back into a CompiledCaveat.
Expand All @@ -82,5 +98,5 @@ func DeserializeCaveat(env *Environment, serialized []byte) (*CompiledCaveat, er
}

ast := cel.CheckedExprToAst(caveat.GetCel())
return &CompiledCaveat{celEnv, ast}, nil
return &CompiledCaveat{celEnv, ast, ""}, nil
}
2 changes: 1 addition & 1 deletion pkg/caveats/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (cr CaveatResult) PartialValue() (*CompiledCaveat, error) {
}

expr := interpreter.PruneAst(cr.parentCaveat.ast.Expr(), cr.details.State())
return &CompiledCaveat{cr.parentCaveat.celEnv, cel.ParsedExprToAst(&exprpb.ParsedExpr{Expr: expr})}, nil
return &CompiledCaveat{cr.parentCaveat.celEnv, cel.ParsedExprToAst(&exprpb.ParsedExpr{Expr: expr}), ""}, nil
}

// EvaluateCaveat evaluates the compiled caveat with the specified values, and returns
Expand Down
Loading

0 comments on commit 9f0d6b6

Please sign in to comment.