Skip to content

Commit

Permalink
Add namespace validator.
Browse files Browse the repository at this point in the history
  • Loading branch information
jakedt committed Mar 17, 2021
1 parent f2b4aa4 commit 99251c4
Show file tree
Hide file tree
Showing 2 changed files with 385 additions and 0 deletions.
131 changes: 131 additions & 0 deletions pkg/validation/namespace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package validation

import (
"errors"
"fmt"

pb "github.com/authzed/spicedb/pkg/REDACTEDapi/api"
)

const errInvalidNamespace = "invalid namespace: %w"

var (
ErrUnknownRewriteOperation = errors.New("unknown rewrite operation")
ErrMissingChildren = errors.New("set operation has no children")
ErrUnknownSetChildType = errors.New("unknown set operation child type")
ErrNilDefinition = errors.New("required definition was undefined/nil")
ErrTupleToUsersetBadRelation = errors.New("tuple to userset bad relation")
ErrTupleToUsersetMissingUserset = errors.New("tuple to userset missing computed userset")
ErrComputedUsersetObject = errors.New("unknown computed userset object type")
)

// NamespaceConfig validates the provided namespace configuration.
func NamespaceConfig(ns *pb.NamespaceDefinition) error {
if ns == nil {
return fmt.Errorf(errInvalidNamespace, ErrNilDefinition)
}

if err := NamespaceName(ns.Name); err != nil {
return fmt.Errorf(errInvalidNamespace, err)
}

for _, relation := range ns.Relation {
if err := RelationName(relation.Name); err != nil {
return fmt.Errorf(errInvalidNamespace, err)
}

if relation.UsersetRewrite != nil {
if err := usersetRewrite(relation.UsersetRewrite); err != nil {
return fmt.Errorf(errInvalidNamespace, err)
}
}
}

return nil
}

func usersetRewrite(rewrite *pb.UsersetRewrite) error {
if rewrite == nil {
return ErrNilDefinition
}

switch rw := rewrite.RewriteOperation.(type) {
case *pb.UsersetRewrite_Union:
return setOperation(rw.Union)
case *pb.UsersetRewrite_Intersection:
return setOperation(rw.Intersection)
case *pb.UsersetRewrite_Exclusion:
return setOperation(rw.Exclusion)
default:
return ErrUnknownRewriteOperation
}
}

func setOperation(op *pb.SetOperation) error {
if op == nil {
return ErrNilDefinition
}

if len(op.Child) == 0 {
return ErrMissingChildren
}

for _, child := range op.Child {
if child == nil {
return ErrNilDefinition
}

switch ch := child.ChildType.(type) {
case *pb.SetOperation_Child_UsersetRewrite:
if err := usersetRewrite(ch.UsersetRewrite); err != nil {
return err
}
case *pb.SetOperation_Child_TupleToUserset:
if err := tupleToUserset(ch.TupleToUserset); err != nil {
return err
}
case *pb.SetOperation_Child_ComputedUserset:
if err := computedUserset(ch.ComputedUserset); err != nil {
return err
}
case *pb.SetOperation_Child_XThis:
// Intentionally blank
default:
return ErrUnknownSetChildType
}
}

return nil
}

func tupleToUserset(ttu *pb.TupleToUserset) error {
if ttu == nil {
return ErrNilDefinition
}

if ttu.Tupleset == nil || RelationName(ttu.Tupleset.Relation) != nil {
return ErrTupleToUsersetBadRelation
}

if ttu.ComputedUserset == nil {
return ErrTupleToUsersetMissingUserset
}

return computedUserset(ttu.ComputedUserset)
}

func computedUserset(cu *pb.ComputedUserset) error {
if cu == nil {
return ErrNilDefinition
}

if err := RelationName(cu.Relation); err != nil {
return err
}

if cu.Object != pb.ComputedUserset_TUPLE_OBJECT && cu.Object != pb.ComputedUserset_TUPLE_USERSET_OBJECT {
return ErrComputedUsersetObject
}

return nil
}
254 changes: 254 additions & 0 deletions pkg/validation/namespace_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
package validation

import (
"errors"
"testing"

"github.com/stretchr/testify/require"

pb "github.com/authzed/spicedb/pkg/REDACTEDapi/api"
ns "github.com/authzed/spicedb/pkg/namespace"
)

func TestNamespaceValidation(t *testing.T) {
testCases := []struct {
name string
cause error
config *pb.NamespaceDefinition
}{
{"empty", ErrInvalidNamespaceName, &pb.NamespaceDefinition{}},
{"simple", nil, ns.Namespace("user")},
{"full", nil, ns.Namespace(
"document",
ns.Relation("owner", nil),
ns.Relation("editor", ns.Union(
ns.This(),
ns.ComputedUserset("owner"),
)),
ns.Relation("parent", nil),
ns.Relation("lock", nil),
ns.Relation("viewer", ns.Union(
ns.This(),
ns.ComputedUserset("editor"),
ns.TupleToUserset("parent", "viewer"),
)),
)},
{"working intersection", nil, ns.Namespace(
"document",
ns.Relation("editor", ns.Intersection(
ns.This(),
ns.ComputedUserset("owner"),
)),
)},
{"working exclusion", nil, ns.Namespace(
"document",
ns.Relation("editor", ns.Exclusion(
ns.This(),
ns.ComputedUserset("owner"),
)),
)},
{"bad relation name", ErrInvalidRelationName, ns.Namespace(
"document",
ns.Relation("a", nil),
)},
{"bad rewrite", ErrUnknownRewriteOperation, ns.Namespace(
"document",
ns.Relation("editor", &pb.UsersetRewrite{}),
)},
{"nil union", ErrNilDefinition, ns.Namespace(
"document",
ns.Relation("editor", &pb.UsersetRewrite{
RewriteOperation: &pb.UsersetRewrite_Union{},
}),
)},
{"no children", ErrMissingChildren, ns.Namespace(
"document",
ns.Relation("editor", &pb.UsersetRewrite{
RewriteOperation: &pb.UsersetRewrite_Union{
Union: &pb.SetOperation{},
},
}),
)},
{"empty child", ErrNilDefinition, ns.Namespace(
"document",
ns.Relation("viewer", ns.Union(
&pb.SetOperation_Child{
ChildType: &pb.SetOperation_Child_TupleToUserset{},
},
)),
)},
{"nil child pointer", ErrNilDefinition, ns.Namespace(
"document",
ns.Relation("viewer", ns.Union(
nil,
)),
)},
{"nil child", ErrUnknownSetChildType, ns.Namespace(
"document",
ns.Relation("viewer", ns.Union(
&pb.SetOperation_Child{},
)),
)},
{"bad ttu", ErrTupleToUsersetBadRelation, ns.Namespace(
"document",
ns.Relation("viewer", ns.Union(
&pb.SetOperation_Child{
ChildType: &pb.SetOperation_Child_TupleToUserset{
TupleToUserset: &pb.TupleToUserset{},
},
},
)),
)},
{"ttu missing tupleset", ErrTupleToUsersetBadRelation, ns.Namespace(
"document",
ns.Relation("viewer", ns.Union(
&pb.SetOperation_Child{
ChildType: &pb.SetOperation_Child_TupleToUserset{
TupleToUserset: &pb.TupleToUserset{
ComputedUserset: &pb.ComputedUserset{
Object: pb.ComputedUserset_TUPLE_USERSET_OBJECT,
Relation: "admin",
},
},
},
},
)),
)},
{"ttu missing rewrite", ErrTupleToUsersetMissingUserset, ns.Namespace(
"document",
ns.Relation("viewer", ns.Union(
&pb.SetOperation_Child{
ChildType: &pb.SetOperation_Child_TupleToUserset{
TupleToUserset: &pb.TupleToUserset{
Tupleset: &pb.TupleToUserset_Tupleset{
Relation: "parent",
},
},
},
},
)),
)},
{"ttu bad relation", ErrTupleToUsersetBadRelation, ns.Namespace(
"document",
ns.Relation("viewer", ns.Union(
&pb.SetOperation_Child{
ChildType: &pb.SetOperation_Child_TupleToUserset{
TupleToUserset: &pb.TupleToUserset{
Tupleset: &pb.TupleToUserset_Tupleset{
Relation: "",
},
ComputedUserset: &pb.ComputedUserset{
Object: pb.ComputedUserset_TUPLE_USERSET_OBJECT,
Relation: "admin",
},
},
},
},
)),
)},
{"ttu bad computed relation", ErrInvalidRelationName, ns.Namespace(
"document",
ns.Relation("viewer", ns.Union(
&pb.SetOperation_Child{
ChildType: &pb.SetOperation_Child_TupleToUserset{
TupleToUserset: &pb.TupleToUserset{
Tupleset: &pb.TupleToUserset_Tupleset{
Relation: "parent",
},
ComputedUserset: &pb.ComputedUserset{
Object: pb.ComputedUserset_TUPLE_USERSET_OBJECT,
Relation: "",
},
},
},
},
)),
)},
{"ttu nil computed relation", ErrInvalidRelationName, ns.Namespace(
"document",
ns.Relation("viewer", ns.Union(
&pb.SetOperation_Child{
ChildType: &pb.SetOperation_Child_TupleToUserset{
TupleToUserset: &pb.TupleToUserset{
Tupleset: &pb.TupleToUserset_Tupleset{
Relation: "parent",
},
ComputedUserset: &pb.ComputedUserset{
Object: pb.ComputedUserset_TUPLE_USERSET_OBJECT,
},
},
},
},
)),
)},
{"empty cu", ErrNilDefinition, ns.Namespace(
"document",
ns.Relation("viewer", ns.Union(
&pb.SetOperation_Child{
ChildType: &pb.SetOperation_Child_ComputedUserset{},
},
)),
)},
{"cu empty relation", ErrInvalidRelationName, ns.Namespace(
"document",
ns.Relation("viewer", ns.Union(
&pb.SetOperation_Child{
ChildType: &pb.SetOperation_Child_ComputedUserset{
ComputedUserset: &pb.ComputedUserset{},
},
},
)),
)},
{"cu bad relation name", ErrInvalidRelationName, ns.Namespace(
"document",
ns.Relation("viewer", ns.Union(
&pb.SetOperation_Child{
ChildType: &pb.SetOperation_Child_ComputedUserset{
ComputedUserset: &pb.ComputedUserset{
Relation: "ab",
},
},
},
)),
)},
{"cu bad object type", ErrComputedUsersetObject, ns.Namespace(
"document",
ns.Relation("viewer", ns.Union(
&pb.SetOperation_Child{
ChildType: &pb.SetOperation_Child_ComputedUserset{
ComputedUserset: &pb.ComputedUserset{
Relation: "admin",
Object: 3,
},
},
},
)),
)},
{"child nil rewrite", ErrNilDefinition, ns.Namespace(
"document",
ns.Relation("viewer", ns.Union(
&pb.SetOperation_Child{
ChildType: &pb.SetOperation_Child_UsersetRewrite{},
},
)),
)},
{"child bad rewrite", ErrUnknownRewriteOperation, ns.Namespace(
"document",
ns.Relation("viewer", ns.Union(
&pb.SetOperation_Child{
ChildType: &pb.SetOperation_Child_UsersetRewrite{
UsersetRewrite: &pb.UsersetRewrite{},
},
},
)),
)},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
err := NamespaceConfig(tc.config)
require.True(errors.Is(err, tc.cause))
})
}
}

0 comments on commit 99251c4

Please sign in to comment.