Merge pull request #100 from ucan-wg/pol-neq

policy: implement the missing '!='
This commit is contained in:
Michael Muré
2025-01-23 12:38:17 +01:00
committed by GitHub
8 changed files with 256 additions and 23 deletions

View File

@@ -57,7 +57,7 @@ Not implemented yet:
Besides that, `go-ucan` also includes: Besides that, `go-ucan` also includes:
- a simplified [DID](https://www.w3.org/TR/did-core/) and [did-key](https://w3c-ccg.github.io/did-method-key/) implementation - a simplified [DID](https://www.w3.org/TR/did-core/) and [did-key](https://w3c-ccg.github.io/did-method-key/) implementation
- a [token container](https://github.com/ucan-wg/go-ucan/tree/v1/pkg/container) with CBOR and CAR format, to package and carry tokens together - a [token container](https://github.com/ucan-wg/go-ucan/tree/v1/pkg/container) with CBOR and CAR format, to package and carry tokens together, see [SPEC](pkg/container/SPEC.md)
- support for encrypted values in token's metadata - support for encrypted values in token's metadata
## Getting Help ## Getting Help

View File

@@ -81,7 +81,7 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
} }
case 3: case 3:
switch op { switch op {
case KindEqual, KindLessThan, KindLessThanOrEqual, KindGreaterThan, KindGreaterThanOrEqual: case KindEqual, KindNotEqual, KindLessThan, KindLessThanOrEqual, KindGreaterThan, KindGreaterThanOrEqual:
sel, err := arg2AsSelector(op) sel, err := arg2AsSelector(op)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -1,11 +1,14 @@
package policy package policy
import ( import (
"fmt"
"testing" "testing"
"github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagjson" "github.com/ipld/go-ipld-prime/codec/dagjson"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
) )
func TestIpldRoundTrip(t *testing.T) { 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 { for _, tc := range []struct {
name, dagJsonStr string name, dagJsonStr string
}{ }{
{"illustrativeExample", illustrativeExample}, {"illustrativeExample", illustrativeExample},
{"allOps", allOps},
} { } {
nodes, err := ipld.Decode([]byte(tc.dagJsonStr), dagjson.Decode) nodes, err := ipld.Decode([]byte(tc.dagJsonStr), dagjson.Decode)
require.NoError(t, err) require.NoError(t, err)
@@ -41,3 +65,19 @@ func TestIpldRoundTrip(t *testing.T) {
require.JSONEq(t, tc.dagJsonStr, string(wroteAsDagJson)) 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))),
))
}

View File

@@ -82,6 +82,17 @@ func matchStatement(cur Statement, node ipld.Node) (_ matchResult, leafMost Stat
} }
return boolToRes(datamodel.DeepEqual(s.value, res)) 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: case KindGreaterThan:
if s, ok := cur.(equality); ok { if s, ok := cur.(equality); ok {
res, err := s.selector.Select(node) res, err := s.selector.Select(node)

View File

@@ -16,7 +16,7 @@ import (
func TestMatch(t *testing.T) { func TestMatch(t *testing.T) {
t.Run("equality", func(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") nd := literal.String("test")
pol := MustConstruct(Equal(".", literal.String("test"))) pol := MustConstruct(Equal(".", literal.String("test")))
@@ -35,7 +35,7 @@ func TestMatch(t *testing.T) {
require.Equal(t, pol[0], leaf) 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) nd := literal.Int(138)
pol := MustConstruct(Equal(".", literal.Int(138))) pol := MustConstruct(Equal(".", literal.Int(138)))
@@ -54,7 +54,7 @@ func TestMatch(t *testing.T) {
require.Equal(t, pol[0], leaf) 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) nd := literal.Float(1.138)
pol := MustConstruct(Equal(".", 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) 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")} l0 := cidlink.Link{Cid: cid.MustParse("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")}
l1 := cidlink.Link{Cid: cid.MustParse("bafkreifau35r7vi37tvbvfy3hdwvgb4tlflqf7zcdzeujqcjk3rsphiwte")} l1 := cidlink.Link{Cid: cid.MustParse("bafkreifau35r7vi37tvbvfy3hdwvgb4tlflqf7zcdzeujqcjk3rsphiwte")}
@@ -95,7 +95,7 @@ func TestMatch(t *testing.T) {
require.Equal(t, pol[0], leaf) 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{ nd, _ := literal.Map(map[string]any{
"foo": "bar", "foo": "bar",
}) })
@@ -121,7 +121,7 @@ func TestMatch(t *testing.T) {
require.Equal(t, pol[0], leaf) 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"}) nd, _ := literal.List([]any{"foo"})
pol := MustConstruct(Equal(".[0]", literal.String("foo"))) pol := MustConstruct(Equal(".[0]", literal.String("foo")))
@@ -134,6 +134,132 @@ func TestMatch(t *testing.T) {
require.False(t, ok) require.False(t, ok)
require.Equal(t, pol[0], leaf) 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)
// missing data will fail, as not optional
pol = MustConstruct(NotEqual(".foobar", literal.String("bar")))
ok, leaf = pol.Match(nd)
require.False(t, ok)
require.Equal(t, pol[0], 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)
pol = MustConstruct(NotEqual(".[0]", literal.String("bar")))
ok, leaf = pol.Match(nd)
require.True(t, ok)
require.Nil(t, leaf)
// missing data will fail, as not optional
pol = MustConstruct(NotEqual(".[1]", literal.String("foo")))
ok, leaf = pol.Match(nd)
require.False(t, ok)
require.Equal(t, pol[0], leaf)
})
}) })
t.Run("inequality", func(t *testing.T) { t.Run("inequality", func(t *testing.T) {
@@ -245,20 +371,61 @@ func TestMatch(t *testing.T) {
require.False(t, ok) require.False(t, ok)
require.Equal(t, pol[0], leaf) 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) { 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) ok, leaf := pol.Match(nd)
require.True(t, ok) require.True(t, ok)
require.Nil(t, leaf) require.Nil(t, leaf)
pol = MustConstruct(Not(Equal(".", literal.Bool(false)))) pol = MustConstruct(Not(Equal(".foo", literal.Bool(false))))
ok, leaf = pol.Match(nd) ok, leaf = pol.Match(nd)
require.False(t, ok) require.False(t, ok)
require.Equal(t, pol[0], leaf) require.Equal(t, pol[0], leaf)
// missing data will fail, as not optional
pol = MustConstruct(Not(Equal(".foobar", literal.Bool(true))))
ok, leaf = pol.Match(nd)
require.False(t, ok)
require.Equal(t, MustConstruct(Equal(".foobar", literal.Bool(true)))[0], leaf)
}) })
t.Run("conjunction", func(t *testing.T) { t.Run("conjunction", func(t *testing.T) {
@@ -482,6 +649,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(`[["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(`[["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(`[["!=", ".name", "Alice"]]`), []byte(`{"name": "Alice"}`))
f.Add([]byte(`[[">", ".age", 30]]`), []byte(`{"age": 31}`)) f.Add([]byte(`[[">", ".age", 30]]`), []byte(`{"age": 31}`))
f.Add([]byte(`[["<=", ".height", 180]]`), []byte(`{"height": 170}`)) f.Add([]byte(`[["<=", ".height", 180]]`), []byte(`{"height": 170}`))
f.Add([]byte(`[["not", ["==", ".status", "inactive"]]]`), []byte(`{"status": "active"}`)) f.Add([]byte(`[["not", ["==", ".status", "inactive"]]]`), []byte(`{"status": "active"}`))

View File

@@ -14,6 +14,7 @@ import (
const ( const (
KindEqual = "==" // implemented by equality KindEqual = "==" // implemented by equality
KindNotEqual = "!=" // implemented by equality
KindGreaterThan = ">" // implemented by equality KindGreaterThan = ">" // implemented by equality
KindGreaterThanOrEqual = ">=" // implemented by equality KindGreaterThanOrEqual = ">=" // implemented by equality
KindLessThan = "<" // 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 { func GreaterThan(selector string, value ipld.Node) Constructor {
return func() (Statement, error) { return func() (Statement, error) {
sel, err := selpkg.Parse(selector) sel, err := selpkg.Parse(selector)
@@ -125,7 +133,7 @@ func (n negation) Kind() string {
func (n negation) String() string { func (n negation) String() string {
child := n.statement.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 { func Not(cstor Constructor) Constructor {
@@ -149,7 +157,7 @@ func (c connective) String() string {
for i, statement := range c.statements { for i, statement := range c.statements {
childs[i] = strings.ReplaceAll(statement.String(), "\n", "\n ") 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 { func And(cstors ...Constructor) Constructor {
@@ -208,7 +216,7 @@ func (n quantifier) Kind() string {
func (n quantifier) String() string { func (n quantifier) String() string {
child := n.statement.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 { func All(selector string, cstor Constructor) Constructor {

View File

@@ -28,11 +28,13 @@ func ExamplePolicy() {
// [ // [
// ["==", ".status", "draft"], // ["==", ".status", "draft"],
// ["all", ".reviewer", // ["all", ".reviewer",
// ["like", ".email", "*@example.com"]], // ["like", ".email", "*@example.com"]
// ],
// ["any", ".tags", // ["any", ".tags",
// ["or", [ // ["or", [
// ["==", ".", "news"], // ["==", ".", "news"],
// ["==", ".", "press"]]] // ["==", ".", "press"]
// ]]
// ] // ]
// ] // ]
} }
@@ -59,11 +61,13 @@ func ExamplePolicy_accumulate() {
// [ // [
// ["==", ".status", "draft"], // ["==", ".status", "draft"],
// ["all", ".reviewer", // ["all", ".reviewer",
// ["like", ".email", "*@example.com"]], // ["like", ".email", "*@example.com"]
// ],
// ["any", ".tags", // ["any", ".tags",
// ["or", [ // ["or", [
// ["==", ".", "news"], // ["==", ".", "news"],
// ["==", ".", "press"]]] // ["==", ".", "press"]
// ]]
// ] // ]
// ] // ]
} }

View File

@@ -272,11 +272,13 @@ func ExampleFromSealed() {
// Policy (pol): [ // Policy (pol): [
// ["==", ".status", "draft"], // ["==", ".status", "draft"],
// ["all", ".reviewer", // ["all", ".reviewer",
// ["like", ".email", "*@example.com"]], // ["like", ".email", "*@example.com"]
// ],
// ["any", ".tags", // ["any", ".tags",
// ["or", [ // ["or", [
// ["==", ".", "news"], // ["==", ".", "news"],
// ["==", ".", "press"]]] // ["==", ".", "press"]
// ]]
// ] // ]
// ] // ]
// Nonce (nonce): 000102030405060708090a0b // Nonce (nonce): 000102030405060708090a0b