diff --git a/pkg/policy/ipld.go b/pkg/policy/ipld.go index 752dd96..8642035 100644 --- a/pkg/policy/ipld.go +++ b/pkg/policy/ipld.go @@ -81,7 +81,7 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) { } case 3: switch op { - case KindEqual, KindLessThan, KindLessThanOrEqual, KindGreaterThan, KindGreaterThanOrEqual: + case KindEqual, KindNotEqual, KindLessThan, KindLessThanOrEqual, KindGreaterThan, KindGreaterThanOrEqual: sel, err := arg2AsSelector(op) if err != nil { return nil, err diff --git a/pkg/policy/ipld_test.go b/pkg/policy/ipld_test.go index 7a23394..689dc73 100644 --- a/pkg/policy/ipld_test.go +++ b/pkg/policy/ipld_test.go @@ -1,11 +1,14 @@ package policy import ( + "fmt" "testing" "github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime/codec/dagjson" "github.com/stretchr/testify/require" + + "github.com/ucan-wg/go-ucan/pkg/policy/literal" ) func TestIpldRoundTrip(t *testing.T) { @@ -21,10 +24,31 @@ func TestIpldRoundTrip(t *testing.T) { ] ]` + // must contain all the operators + const allOps = ` +[ + ["and", [ + ["==", ".foo1", ".bar1"], + ["!=", ".foo2", ".bar2"] + ]], + ["or", [ + [">", ".foo5", 5.2], + [">=", ".foo6", 6.2] + ]], + ["not", ["like", ".foo7", "*@example.com"]], + ["all", ".foo8", + ["<", ".foo3", 3] + ], + ["any", ".foo9", + ["<=", ".foo4", 4] + ] +]` + for _, tc := range []struct { name, dagJsonStr string }{ {"illustrativeExample", illustrativeExample}, + {"allOps", allOps}, } { nodes, err := ipld.Decode([]byte(tc.dagJsonStr), dagjson.Decode) require.NoError(t, err) @@ -41,3 +65,19 @@ func TestIpldRoundTrip(t *testing.T) { require.JSONEq(t, tc.dagJsonStr, string(wroteAsDagJson)) } } + +func TestFoo(t *testing.T) { + fmt.Println(MustConstruct( + And( + Equal(".foo1", literal.String(".bar1")), + NotEqual(".foo2", literal.String(".bar2")), + ), + Or( + GreaterThan(".foo5", literal.Float(5.2)), + GreaterThanOrEqual(".foo6", literal.Float(6.2)), + ), + Not(Like(".foo7", "*@example.com")), + All(".foo8", LessThan(".foo3", literal.Int(3))), + Any(".foo9", LessThanOrEqual(".foo4", literal.Int(4))), + )) +} diff --git a/pkg/policy/match.go b/pkg/policy/match.go index 648b877..9bf6b82 100644 --- a/pkg/policy/match.go +++ b/pkg/policy/match.go @@ -82,6 +82,17 @@ func matchStatement(cur Statement, node ipld.Node) (_ matchResult, leafMost Stat } return boolToRes(datamodel.DeepEqual(s.value, res)) } + case KindNotEqual: + if s, ok := cur.(equality); ok { + res, err := s.selector.Select(node) + if err != nil { + return matchResultNoData, cur + } + if res == nil { // optional selector didn't match + return matchResultOptionalNoData, nil + } + return boolToRes(!datamodel.DeepEqual(s.value, res)) + } case KindGreaterThan: if s, ok := cur.(equality); ok { res, err := s.selector.Select(node) diff --git a/pkg/policy/match_test.go b/pkg/policy/match_test.go index 108037a..a8ad842 100644 --- a/pkg/policy/match_test.go +++ b/pkg/policy/match_test.go @@ -16,7 +16,7 @@ import ( func TestMatch(t *testing.T) { t.Run("equality", func(t *testing.T) { - t.Run("string", func(t *testing.T) { + t.Run("eq string", func(t *testing.T) { nd := literal.String("test") pol := MustConstruct(Equal(".", literal.String("test"))) @@ -35,7 +35,7 @@ func TestMatch(t *testing.T) { require.Equal(t, pol[0], leaf) }) - t.Run("int", func(t *testing.T) { + t.Run("eq int", func(t *testing.T) { nd := literal.Int(138) pol := MustConstruct(Equal(".", literal.Int(138))) @@ -54,7 +54,7 @@ func TestMatch(t *testing.T) { require.Equal(t, pol[0], leaf) }) - t.Run("float", func(t *testing.T) { + t.Run("eq float", func(t *testing.T) { nd := literal.Float(1.138) pol := MustConstruct(Equal(".", literal.Float(1.138))) @@ -73,7 +73,7 @@ func TestMatch(t *testing.T) { require.Equal(t, pol[0], leaf) }) - t.Run("IPLD Link", func(t *testing.T) { + t.Run("eq IPLD Link", func(t *testing.T) { l0 := cidlink.Link{Cid: cid.MustParse("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")} l1 := cidlink.Link{Cid: cid.MustParse("bafkreifau35r7vi37tvbvfy3hdwvgb4tlflqf7zcdzeujqcjk3rsphiwte")} @@ -95,7 +95,7 @@ func TestMatch(t *testing.T) { require.Equal(t, pol[0], leaf) }) - t.Run("string in map", func(t *testing.T) { + t.Run("eq string in map", func(t *testing.T) { nd, _ := literal.Map(map[string]any{ "foo": "bar", }) @@ -121,7 +121,7 @@ func TestMatch(t *testing.T) { require.Equal(t, pol[0], leaf) }) - t.Run("string in list", func(t *testing.T) { + t.Run("eq string in list", func(t *testing.T) { nd, _ := literal.List([]any{"foo"}) pol := MustConstruct(Equal(".[0]", literal.String("foo"))) @@ -134,6 +134,129 @@ func TestMatch(t *testing.T) { require.False(t, ok) require.Equal(t, pol[0], leaf) }) + + // ------------ + + t.Run("neq string", func(t *testing.T) { + nd := literal.String("test") + + pol := MustConstruct(NotEqual(".", literal.String("test"))) + ok, leaf := pol.Match(nd) + require.False(t, ok) + require.Equal(t, pol[0], leaf) + + pol = MustConstruct(NotEqual(".", literal.String("test2"))) + ok, leaf = pol.Match(nd) + require.True(t, ok) + require.Nil(t, leaf) + + pol = MustConstruct(NotEqual(".", literal.Int(138))) + ok, leaf = pol.Match(nd) + require.True(t, ok) + require.Nil(t, leaf) + }) + + t.Run("neq int", func(t *testing.T) { + nd := literal.Int(138) + + pol := MustConstruct(NotEqual(".", literal.Int(138))) + ok, leaf := pol.Match(nd) + require.False(t, ok) + require.Equal(t, pol[0], leaf) + + pol = MustConstruct(NotEqual(".", literal.Int(1138))) + ok, leaf = pol.Match(nd) + require.True(t, ok) + require.Nil(t, leaf) + + pol = MustConstruct(NotEqual(".", literal.String("138"))) + ok, leaf = pol.Match(nd) + require.True(t, ok) + require.Nil(t, leaf) + }) + + t.Run("neq float", func(t *testing.T) { + nd := literal.Float(1.138) + + pol := MustConstruct(NotEqual(".", literal.Float(1.138))) + ok, leaf := pol.Match(nd) + require.False(t, ok) + require.Equal(t, pol[0], leaf) + + pol = MustConstruct(NotEqual(".", literal.Float(11.38))) + ok, leaf = pol.Match(nd) + require.True(t, ok) + require.Nil(t, leaf) + + pol = MustConstruct(NotEqual(".", literal.String("138"))) + ok, leaf = pol.Match(nd) + require.True(t, ok) + require.Nil(t, leaf) + }) + + t.Run("neq IPLD Link", func(t *testing.T) { + l0 := cidlink.Link{Cid: cid.MustParse("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")} + l1 := cidlink.Link{Cid: cid.MustParse("bafkreifau35r7vi37tvbvfy3hdwvgb4tlflqf7zcdzeujqcjk3rsphiwte")} + + nd := literal.Link(l0) + + pol := MustConstruct(NotEqual(".", literal.Link(l0))) + ok, leaf := pol.Match(nd) + require.False(t, ok) + require.Equal(t, pol[0], leaf) + + pol = MustConstruct(NotEqual(".", literal.Link(l1))) + ok, leaf = pol.Match(nd) + require.True(t, ok) + require.Nil(t, leaf) + + pol = MustConstruct(NotEqual(".", literal.String("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq"))) + ok, leaf = pol.Match(nd) + require.True(t, ok) + require.Nil(t, leaf) + }) + + t.Run("neq string in map", func(t *testing.T) { + nd, _ := literal.Map(map[string]any{ + "foo": "bar", + }) + + pol := MustConstruct(NotEqual(".foo", literal.String("bar"))) + ok, leaf := pol.Match(nd) + require.False(t, ok) + require.Equal(t, pol[0], leaf) + + pol = MustConstruct(NotEqual(".[\"foo\"]", literal.String("bar"))) + ok, leaf = pol.Match(nd) + require.False(t, ok) + require.Equal(t, pol[0], leaf) + + pol = MustConstruct(NotEqual(".foo", literal.String("baz"))) + ok, leaf = pol.Match(nd) + require.True(t, ok) + require.Nil(t, leaf) + + // TODO: clarify how it behave when the data is missing + pol = MustConstruct(NotEqual(".foobar", literal.String("bar"))) + ok, leaf = pol.Match(nd) + require.True(t, ok) + require.Nil(t, leaf) + }) + + t.Run("neq string in list", func(t *testing.T) { + nd, _ := literal.List([]any{"foo"}) + + pol := MustConstruct(NotEqual(".[0]", literal.String("foo"))) + ok, leaf := pol.Match(nd) + require.False(t, ok) + require.Equal(t, pol[0], leaf) + + // TODO: clarify how it behave when the data is missing + pol = MustConstruct(NotEqual(".[1]", literal.String("foo"))) + ok, leaf = pol.Match(nd) + require.True(t, ok) + require.Nil(t, leaf) + }) }) t.Run("inequality", func(t *testing.T) { @@ -245,20 +368,61 @@ func TestMatch(t *testing.T) { require.False(t, ok) require.Equal(t, pol[0], leaf) }) + + t.Run("lt float", func(t *testing.T) { + nd := literal.Float(1.38) + + pol := MustConstruct(LessThan(".", literal.Float(1))) + ok, leaf := pol.Match(nd) + require.False(t, ok) + require.Equal(t, pol[0], leaf) + + pol = MustConstruct(LessThan(".", literal.Float(2))) + ok, leaf = pol.Match(nd) + require.True(t, ok) + require.Nil(t, leaf) + }) + + t.Run("lte float", func(t *testing.T) { + nd := literal.Float(1.38) + + pol := MustConstruct(LessThanOrEqual(".", literal.Float(1))) + ok, leaf := pol.Match(nd) + require.False(t, ok) + require.Equal(t, pol[0], leaf) + + pol = MustConstruct(GreaterThanOrEqual(".", literal.Float(1.38))) + ok, leaf = pol.Match(nd) + require.True(t, ok) + require.Nil(t, leaf) + + pol = MustConstruct(LessThanOrEqual(".", literal.Float(2))) + ok, leaf = pol.Match(nd) + require.True(t, ok) + require.Nil(t, leaf) + }) }) t.Run("negation", func(t *testing.T) { - nd := literal.Bool(false) + nd, _ := literal.Map(map[string]any{ + "foo": false, + }) - pol := MustConstruct(Not(Equal(".", literal.Bool(true)))) + pol := MustConstruct(Not(Equal(".foo", literal.Bool(true)))) ok, leaf := pol.Match(nd) require.True(t, ok) require.Nil(t, leaf) - pol = MustConstruct(Not(Equal(".", literal.Bool(false)))) + pol = MustConstruct(Not(Equal(".foo", literal.Bool(false)))) ok, leaf = pol.Match(nd) require.False(t, ok) require.Equal(t, pol[0], leaf) + + // TODO: clarify how it works on missing data + pol = MustConstruct(Not(Equal(".foobar", literal.Bool(true)))) + ok, leaf = pol.Match(nd) + require.True(t, ok) + require.Nil(t, leaf) }) t.Run("conjunction", func(t *testing.T) { @@ -482,6 +646,7 @@ func FuzzMatch(f *testing.F) { f.Add([]byte(`[["all", ".reviewer", ["like", ".email", "*@example.com"]]]`), []byte(`{"reviewer": [{"email": "alice@example.com"}, {"email": "bob@example.com"}]}`)) f.Add([]byte(`[["any", ".tags", ["or", [["==", ".", "news"], ["==", ".", "press"]]]]]`), []byte(`{"tags": ["news", "press"]}`)) f.Add([]byte(`[["==", ".name", "Alice"]]`), []byte(`{"name": "Alice"}`)) + f.Add([]byte(`[["!=", ".name", "Alice"]]`), []byte(`{"name": "Alice"}`)) f.Add([]byte(`[[">", ".age", 30]]`), []byte(`{"age": 31}`)) f.Add([]byte(`[["<=", ".height", 180]]`), []byte(`{"height": 170}`)) f.Add([]byte(`[["not", ["==", ".status", "inactive"]]]`), []byte(`{"status": "active"}`)) diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go index 5da2ea9..aada2d8 100644 --- a/pkg/policy/policy.go +++ b/pkg/policy/policy.go @@ -14,6 +14,7 @@ import ( const ( KindEqual = "==" // implemented by equality + KindNotEqual = "!=" // implemented by equality KindGreaterThan = ">" // implemented by equality KindGreaterThanOrEqual = ">=" // implemented by equality KindLessThan = "<" // implemented by equality @@ -87,6 +88,13 @@ func Equal(selector string, value ipld.Node) Constructor { } } +func NotEqual(selector string, value ipld.Node) Constructor { + return func() (Statement, error) { + sel, err := selpkg.Parse(selector) + return equality{kind: KindNotEqual, selector: sel, value: value}, err + } +} + func GreaterThan(selector string, value ipld.Node) Constructor { return func() (Statement, error) { sel, err := selpkg.Parse(selector) @@ -125,7 +133,7 @@ func (n negation) Kind() string { func (n negation) String() string { child := n.statement.String() - return fmt.Sprintf(`["%s", "%s"]`, n.Kind(), strings.ReplaceAll(child, "\n", "\n ")) + return fmt.Sprintf(`["%s", %s]`, n.Kind(), strings.ReplaceAll(child, "\n", "\n ")) } func Not(cstor Constructor) Constructor { @@ -149,7 +157,7 @@ func (c connective) String() string { for i, statement := range c.statements { childs[i] = strings.ReplaceAll(statement.String(), "\n", "\n ") } - return fmt.Sprintf("[\"%s\", [\n %s]]\n", c.kind, strings.Join(childs, ",\n ")) + return fmt.Sprintf("[\"%s\", [\n %s\n]]", c.kind, strings.Join(childs, ",\n ")) } func And(cstors ...Constructor) Constructor { @@ -208,7 +216,7 @@ func (n quantifier) Kind() string { func (n quantifier) String() string { child := n.statement.String() - return fmt.Sprintf("[\"%s\", \"%s\",\n %s]", n.Kind(), n.selector, strings.ReplaceAll(child, "\n", "\n ")) + return fmt.Sprintf("[\"%s\", \"%s\",\n %s\n]", n.Kind(), n.selector, strings.ReplaceAll(child, "\n", "\n ")) } func All(selector string, cstor Constructor) Constructor { diff --git a/pkg/policy/policy_test.go b/pkg/policy/policy_test.go index c7f8e9f..f46f5c9 100644 --- a/pkg/policy/policy_test.go +++ b/pkg/policy/policy_test.go @@ -28,12 +28,14 @@ func ExamplePolicy() { // [ // ["==", ".status", "draft"], // ["all", ".reviewer", - // ["like", ".email", "*@example.com"]], + // ["like", ".email", "*@example.com"] + // ], // ["any", ".tags", // ["or", [ // ["==", ".", "news"], - // ["==", ".", "press"]]] - // ] + // ["==", ".", "press"] + // ]] + // ] // ] } @@ -59,12 +61,14 @@ func ExamplePolicy_accumulate() { // [ // ["==", ".status", "draft"], // ["all", ".reviewer", - // ["like", ".email", "*@example.com"]], + // ["like", ".email", "*@example.com"] + // ], // ["any", ".tags", // ["or", [ // ["==", ".", "news"], - // ["==", ".", "press"]]] - // ] + // ["==", ".", "press"] + // ]] + // ] // ] } diff --git a/token/delegation/examples_test.go b/token/delegation/examples_test.go index 9848ed9..f494d7a 100644 --- a/token/delegation/examples_test.go +++ b/token/delegation/examples_test.go @@ -272,12 +272,14 @@ func ExampleFromSealed() { // Policy (pol): [ // ["==", ".status", "draft"], // ["all", ".reviewer", - // ["like", ".email", "*@example.com"]], + // ["like", ".email", "*@example.com"] + // ], // ["any", ".tags", // ["or", [ // ["==", ".", "news"], - // ["==", ".", "press"]]] - // ] + // ["==", ".", "press"] + // ]] + // ] // ] // Nonce (nonce): 000102030405060708090a0b // Meta (meta): {}