diff --git a/capability/policy/ipld.go b/capability/policy/ipld.go index c08b984..1417afc 100644 --- a/capability/policy/ipld.go +++ b/capability/policy/ipld.go @@ -40,14 +40,14 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) { } op := must.String(opNode) - arg2AsSelector := func() (selector.Selector, error) { + arg2AsSelector := func(op string) (selector.Selector, error) { nd, _ := node.LookupByIndex(1) if nd.Kind() != datamodel.Kind_String { - return nil, ErrNotAString(path + "1/") + return nil, ErrNotAString(combinePath(path, op, 1)) } sel, err := selector.Parse(must.String(nd)) if err != nil { - return nil, ErrInvalidSelector(path+"1/", err) + return nil, ErrInvalidSelector(combinePath(path, op, 1), err) } return sel, nil } @@ -57,7 +57,7 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) { switch op { case KindNot: arg2, _ := node.LookupByIndex(1) - statement, err := statementFromIPLD(path+"1/", arg2) + statement, err := statementFromIPLD(combinePath(path, op, 1), arg2) if err != nil { return nil, err } @@ -65,7 +65,7 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) { case KindAnd, KindOr: arg2, _ := node.LookupByIndex(1) - statement, err := statementsFromIPLD(path+"1/", arg2) + statement, err := statementsFromIPLD(combinePath(path, op, 1), arg2) if err != nil { return nil, err } @@ -77,7 +77,7 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) { case 3: switch op { case KindEqual, KindLessThan, KindLessThanOrEqual, KindGreaterThan, KindGreaterThanOrEqual: - sel, err := arg2AsSelector() + sel, err := arg2AsSelector(op) if err != nil { return nil, err } @@ -85,27 +85,30 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) { return equality{kind: op, selector: sel, value: arg3}, nil case KindLike: - sel, err := arg2AsSelector() + sel, err := arg2AsSelector(op) if err != nil { return nil, err } pattern, _ := node.LookupByIndex(2) if pattern.Kind() != datamodel.Kind_String { - return nil, ErrNotAString(path + "2/") + return nil, ErrNotAString(combinePath(path, op, 2)) } res, err := Like(sel, must.String(pattern)) if err != nil { - return nil, ErrInvalidPattern(path+"2/", err) + return nil, ErrInvalidPattern(combinePath(path, op, 2), err) } return res, nil case KindAll, KindAny: - sel, err := arg2AsSelector() + sel, err := arg2AsSelector(op) if err != nil { return nil, err } statementsNode, _ := node.LookupByIndex(2) - statement, err := statementFromIPLD(path+"1/", statementsNode) + statement, err := statementFromIPLD(combinePath(path, op, 1), statementsNode) + if err != nil { + return nil, err + } return quantifier{kind: op, selector: sel, statement: statement}, nil default: @@ -123,7 +126,7 @@ func statementsFromIPLD(path string, node datamodel.Node) ([]Statement, error) { return nil, ErrNotATuple(path) } if node.Length() == 0 { - return nil, ErrEmptyList(path) + return nil, nil } res := make([]Statement, node.Length()) @@ -260,3 +263,7 @@ func statementToIPLD(statement Statement) (datamodel.Node, error) { return list.Build(), nil } + +func combinePath(prev string, operator string, index int) string { + return fmt.Sprintf("%s%d-%s/", prev, index, operator) +} diff --git a/capability/policy/ipld_errors.go b/capability/policy/ipld_errors.go index 303e10b..26b6dde 100644 --- a/capability/policy/ipld_errors.go +++ b/capability/policy/ipld_errors.go @@ -35,10 +35,6 @@ func ErrNotATuple(path string) error { return errWithPath{path: path, msg: "not a tuple"} } -func ErrEmptyList(path string) error { - return errWithPath{path: path, msg: "empty list"} -} - func safeStr(str string) string { if len(str) > 10 { return str[:10] diff --git a/capability/policy/ipld_test.go b/capability/policy/ipld_test.go index c3aa729..b400f8c 100644 --- a/capability/policy/ipld_test.go +++ b/capability/policy/ipld_test.go @@ -11,16 +11,12 @@ import ( func TestIpldRoundTrip(t *testing.T) { const illustrativeExample = ` [ - ["==", ".status", "draft"], - ["all", ".reviewer", [ - ["like", ".email", "*@example.com"]] - ], - ["any", ".tags", [ - ["or", [ - ["==", ".", "news"], - ["==", ".", "press"]] - ]] - ] + ["==", ".status", "draft"], + ["all", ".reviewer", ["like", ".email", "*@example.com"]], + ["any", ".tags", + ["or", [ + ["==", ".", "news"], + ["==", ".", "press"]]] ]` for _, tc := range []struct { diff --git a/capability/policy/match.go b/capability/policy/match.go index eaca407..61f72c4 100644 --- a/capability/policy/match.go +++ b/capability/policy/match.go @@ -119,6 +119,7 @@ func matchStatement(statement Statement, node ipld.Node) bool { } case KindAny: if s, ok := statement.(quantifier); ok { + // FIXME: line below return a single node, not many _, many, err := selector.Select(s.selector, node) if err != nil || many == nil { return false diff --git a/capability/policy/match_test.go b/capability/policy/match_test.go index 5a84982..2323272 100644 --- a/capability/policy/match_test.go +++ b/capability/policy/match_test.go @@ -6,6 +6,7 @@ import ( "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/codec/dagjson" cidlink "github.com/ipld/go-ipld-prime/linking/cid" "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/stretchr/testify/require" @@ -417,3 +418,75 @@ func TestMatch(t *testing.T) { }) }) } + +func TestPolicyExamples(t *testing.T) { + makeNode := func(data string) ipld.Node { + nd, err := ipld.Decode([]byte(data), dagjson.Decode) + require.NoError(t, err) + return nd + } + + evaluate := func(statement string, data ipld.Node) bool { + // we need to wrap statement with [] to make them a policy + policy := fmt.Sprintf("[%s]", statement) + + pol, err := FromDagJson(policy) + require.NoError(t, err) + return Match(pol, data) + } + + t.Run("And", func(t *testing.T) { + data := makeNode(`{ "name": "Katie", "age": 35, "nationalities": ["Canadian", "South African"] }`) + + require.True(t, evaluate(`["and", []]`, data)) + require.True(t, evaluate(` +["and", [ + ["==", ".name", "Katie"], + [">=", ".age", 21] +]]`, data)) + require.False(t, evaluate(` +["and", [ + ["==", ".name", "Katie"], + [">=", ".age", 21], + ["==", ".nationalities", ["American"]] +]]`, data)) + }) + + t.Run("Or", func(t *testing.T) { + data := makeNode(`{ "name": "Katie", "age": 35, "nationalities": ["Canadian", "South African"] }`) + + require.True(t, evaluate(`["or", []]`, data)) + require.True(t, evaluate(` + ["or", [ + ["==", ".name", "Katie"], + [">", ".age", 45] + ]] + `, data)) + + }) + + t.Run("Not", func(t *testing.T) { + data := makeNode(`{ "name": "Katie", "nationalities": ["Canadian", "South African"] }`) + + require.True(t, evaluate(` +["not", + ["and", [ + ["==", ".name", "Katie"], + ["==", ".nationalities", ["American"]] + ]] +] +`, data)) + }) + + t.Run("All", func(t *testing.T) { + data := makeNode(`{"a": [{"b": 1}, {"b": 2}, {"z": [7, 8, 9]}]}`) + + require.False(t, evaluate(`["all", ".a", [">", ".b", 0]]`, data)) + }) + + t.Run("Any", func(t *testing.T) { + data := makeNode(`{"a": [{"b": 1}, {"b": 2}, {"z": [7, 8, 9]}]}`) + + require.True(t, evaluate(`["any", ".a", ["==", ".b", 2]]`, data)) + }) +}