diff --git a/capability/policy/ipld.go b/capability/policy/ipld.go new file mode 100644 index 0000000..d11620c --- /dev/null +++ b/capability/policy/ipld.go @@ -0,0 +1,252 @@ +package policy + +import ( + "fmt" + + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/must" + "github.com/ipld/go-ipld-prime/node/basicnode" + + "github.com/ucan-wg/go-ucan/v1/capability/policy/selector" +) + +func PolicyFromIPLD(node datamodel.Node) (Policy, error) { + return statementsFromIPLD("/", node) +} + +func statementFromIPLD(path string, node datamodel.Node) (Statement, error) { + // sanity checks + if node.Kind() != datamodel.Kind_List { + return nil, ErrNotATuple(path) + } + if node.Length() != 2 && node.Length() != 3 { + return nil, ErrUnrecognizedShape(path) + } + + // extract operator + opNode, _ := node.LookupByIndex(0) + if opNode.Kind() != datamodel.Kind_String { + return nil, ErrNotAString(path) + } + op := must.String(opNode) + + arg2AsSelector := func() (selector.Selector, error) { + nd, _ := node.LookupByIndex(1) + if nd.Kind() != datamodel.Kind_String { + return nil, ErrNotAString(path + "1/") + } + sel, err := selector.Parse(must.String(nd)) + if err != nil { + return nil, ErrInvalidSelector(path+"1/", err) + } + return sel, nil + } + + switch node.Length() { + case 2: + switch op { + case KindNot: + arg2, _ := node.LookupByIndex(1) + statement, err := statementFromIPLD(path+"1/", arg2) + if err != nil { + return nil, err + } + return Not(statement), nil + + case KindAnd, KindOr: + arg2, _ := node.LookupByIndex(1) + statement, err := statementsFromIPLD(path+"1/", arg2) + if err != nil { + return nil, err + } + return connective{kind: op, statements: statement}, nil + + default: + return nil, ErrUnrecognizedOperator(path, op) + } + case 3: + switch op { + case KindEqual, KindLessThan, KindLessThanOrEqual, KindGreaterThan, KindGreaterThanOrEqual: + sel, err := arg2AsSelector() + if err != nil { + return nil, err + } + arg3, _ := node.LookupByIndex(2) + return equality{kind: op, selector: sel, value: arg3}, nil + + case KindLike: + sel, err := arg2AsSelector() + if err != nil { + return nil, err + } + pattern, _ := node.LookupByIndex(2) + if pattern.Kind() != datamodel.Kind_String { + return nil, ErrNotAString(path + "2/") + } + res, err := Like(sel, must.String(pattern)) + if err != nil { + return nil, ErrInvalidPattern(path+"2/", err) + } + return res, nil + + case KindAll, KindAny: + sel, err := arg2AsSelector() + if err != nil { + return nil, err + } + statementsNodes, _ := node.LookupByIndex(2) + statements, err := statementsFromIPLD(path+"1/", statementsNodes) + return quantifier{kind: op, selector: sel, statements: statements}, nil + + default: + return nil, ErrUnrecognizedOperator(path, op) + } + + default: + return nil, ErrUnrecognizedShape(path) + } +} + +func statementsFromIPLD(path string, node datamodel.Node) ([]Statement, error) { + // sanity checks + if node.Kind() != datamodel.Kind_List { + return nil, ErrNotATuple(path) + } + if node.Length() == 0 { + return nil, ErrEmptyList(path) + } + + res := make([]Statement, node.Length()) + + for i := int64(0); i < node.Length(); i++ { + nd, _ := node.LookupByIndex(i) + statement, err := statementFromIPLD(fmt.Sprintf("%s%d/", path, i), nd) + if err != nil { + return nil, err + } + res[i] = statement + } + + return res, nil +} + +func (p Policy) ToIPLD() (datamodel.Node, error) { + return statementsToIPLD(p) +} + +func statementsToIPLD(statements []Statement) (datamodel.Node, error) { + list := basicnode.Prototype.List.NewBuilder() + // can't error, we have the right builder. + listBuilder, _ := list.BeginList(int64(len(statements))) + for _, argStatement := range statements { + node, err := statementToIPLD(argStatement) + if err != nil { + return nil, err + } + err = listBuilder.AssembleValue().AssignNode(node) + if err != nil { + return nil, err + } + } + err := listBuilder.Finish() + if err != nil { + return nil, err + } + return list.Build(), nil +} + +func statementToIPLD(statement Statement) (datamodel.Node, error) { + list := basicnode.Prototype.List.NewBuilder() + + length := int64(3) + switch statement.(type) { + case negation, connective: + length = 2 + } + + // can't error, we have the right builder. + listBuilder, _ := list.BeginList(length) + + switch statement := statement.(type) { + case equality: + err := listBuilder.AssembleValue().AssignString(statement.kind) + if err != nil { + return nil, err + } + err = listBuilder.AssembleValue().AssignString(statement.selector.String()) + if err != nil { + return nil, err + } + err = listBuilder.AssembleValue().AssignNode(statement.value) + if err != nil { + return nil, err + } + + case negation: + err := listBuilder.AssembleValue().AssignString(statement.Kind()) + if err != nil { + return nil, err + } + node, err := statementToIPLD(statement.statement) + if err != nil { + return nil, err + } + err = listBuilder.AssembleValue().AssignNode(node) + if err != nil { + return nil, err + } + + case connective: + err := listBuilder.AssembleValue().AssignString(statement.kind) + if err != nil { + return nil, err + } + args, err := statementsToIPLD(statement.statements) + if err != nil { + return nil, err + } + err = listBuilder.AssembleValue().AssignNode(args) + if err != nil { + return nil, err + } + + case wildcard: + err := listBuilder.AssembleValue().AssignString(statement.Kind()) + if err != nil { + return nil, err + } + err = listBuilder.AssembleValue().AssignString(statement.selector.String()) + if err != nil { + return nil, err + } + err = listBuilder.AssembleValue().AssignString(statement.pattern) + if err != nil { + return nil, err + } + + case quantifier: + err := listBuilder.AssembleValue().AssignString(statement.kind) + if err != nil { + return nil, err + } + err = listBuilder.AssembleValue().AssignString(statement.selector.String()) + if err != nil { + return nil, err + } + args, err := statementsToIPLD(statement.statements) + if err != nil { + return nil, err + } + err = listBuilder.AssembleValue().AssignNode(args) + if err != nil { + return nil, err + } + } + + err := listBuilder.Finish() + if err != nil { + return nil, err + } + + return list.Build(), nil +} diff --git a/capability/policy/ipld_errors.go b/capability/policy/ipld_errors.go new file mode 100644 index 0000000..303e10b --- /dev/null +++ b/capability/policy/ipld_errors.go @@ -0,0 +1,47 @@ +package policy + +import "fmt" + +type errWithPath struct { + path string + msg string +} + +func (e errWithPath) Error() string { + return fmt.Sprintf("IPLD path '%s': %s", e.path, e.msg) +} + +func ErrInvalidSelector(path string, err error) error { + return errWithPath{path: path, msg: fmt.Sprintf("invalid selector: %s", err)} +} + +func ErrInvalidPattern(path string, err error) error { + return errWithPath{path: path, msg: fmt.Sprintf("invalid pattern: %s", err)} +} + +func ErrNotAString(path string) error { + return errWithPath{path: path, msg: ""} +} + +func ErrUnrecognizedOperator(path string, op string) error { + return errWithPath{path: path, msg: fmt.Sprintf("unrecognized operator '%s'", safeStr(op))} +} + +func ErrUnrecognizedShape(path string) error { + return errWithPath{path: path, msg: "unrecognized shape"} +} + +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] + } + return str +} diff --git a/capability/policy/ipld_test.go b/capability/policy/ipld_test.go new file mode 100644 index 0000000..dfb3216 --- /dev/null +++ b/capability/policy/ipld_test.go @@ -0,0 +1,48 @@ +package policy + +import ( + "fmt" + "strings" + "testing" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/codec/dagjson" + "github.com/stretchr/testify/require" +) + +func TestIpldRoundTrip(t *testing.T) { + const illustrativeExample = `[ + ["==", ".status", "draft"], + ["all", ".reviewer", [["like", ".email", "*@example.com"]]], + ["any", ".tags", + ["or", [ + ["==", ".", "news"], + ["==", ".", "press"]] + ]] +]` + + for _, tc := range []struct { + name, dagjson string + }{ + {"illustrativeExample", illustrativeExample}, + } { + // strip all spaces and carriage return + asDagJson := strings.Join(strings.Fields(tc.dagjson), "") + + nodes, err := ipld.Decode([]byte(asDagJson), dagjson.Decode) + require.NoError(t, err) + + pol, err := PolicyFromIPLD(nodes) + require.NoError(t, err) + + fmt.Println(pol) + + wroteIpld, err := pol.ToIPLD() + require.NoError(t, err) + + wroteAsDagJson, err := ipld.Encode(wroteIpld, dagjson.Encode) + require.NoError(t, err) + + require.Equal(t, asDagJson, string(wroteAsDagJson)) + } +} diff --git a/capability/policy/match_test.go b/capability/policy/match_test.go index 7704e62..e41650d 100644 --- a/capability/policy/match_test.go +++ b/capability/policy/match_test.go @@ -4,7 +4,6 @@ import ( "fmt" "testing" - "github.com/gobwas/glob" "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime" cidlink "github.com/ipld/go-ipld-prime/linking/cid" @@ -294,8 +293,7 @@ func TestMatch(t *testing.T) { }) t.Run("wildcard", func(t *testing.T) { - glb, err := glob.Compile(`Alice\*, Bob*, Carol.`) - require.NoError(t, err) + pattern := `Alice\*, Bob*, Carol.` for _, s := range []string{ "Alice*, Bob, Carol.", @@ -310,7 +308,10 @@ func TestMatch(t *testing.T) { nb.AssignString(s) nd := nb.Build() - pol := Policy{Like(selector.MustParse("."), glb)} + statement, err := Like(selector.MustParse("."), pattern) + require.NoError(t, err) + + pol := Policy{statement} ok := Match(pol, nd) require.True(t, ok) }) @@ -331,7 +332,10 @@ func TestMatch(t *testing.T) { nb.AssignString(s) nd := nb.Build() - pol := Policy{Like(selector.MustParse("."), glb)} + statement, err := Like(selector.MustParse("."), pattern) + require.NoError(t, err) + + pol := Policy{statement} ok := Match(pol, nd) require.False(t, ok) }) diff --git a/capability/policy/policy.go b/capability/policy/policy.go index 1825407..33e1a02 100644 --- a/capability/policy/policy.go +++ b/capability/policy/policy.go @@ -23,7 +23,7 @@ const ( KindAny = "any" ) -type Policy = []Statement +type Policy []Statement type Statement interface { Kind() string @@ -126,41 +126,31 @@ func Not(stmt Statement) NegationStatement { return negation{stmt} } -type conjunction struct { +type connective struct { + kind string statements []Statement } -func (n conjunction) Kind() string { - return KindAnd +func (c connective) Kind() string { + return c.kind } -func (n conjunction) Value() []Statement { - return n.statements +func (c connective) Value() []Statement { + return c.statements } func And(stmts ...Statement) ConjunctionStatement { - return conjunction{stmts} -} - -type disjunction struct { - statements []Statement -} - -func (n disjunction) Kind() string { - return KindOr -} - -func (n disjunction) Value() []Statement { - return n.statements + return connective{KindAnd, stmts} } func Or(stmts ...Statement) DisjunctionStatement { - return disjunction{stmts} + return connective{KindOr, stmts} } type wildcard struct { selector selector.Selector - glob glob.Glob + pattern string + glob glob.Glob // not serialized } func (n wildcard) Kind() string { @@ -175,14 +165,18 @@ func (n wildcard) Value() glob.Glob { return n.glob } -func Like(selector selector.Selector, glob glob.Glob) WildcardStatement { - return wildcard{selector, glob} +func Like(selector selector.Selector, pattern string) (WildcardStatement, error) { + g, err := glob.Compile(pattern) + if err != nil { + return nil, err + } + return wildcard{selector, pattern, g}, nil } type quantifier struct { - kind string - selector selector.Selector - policy Policy + kind string + selector selector.Selector + statements []Statement } func (n quantifier) Kind() string { @@ -194,7 +188,7 @@ func (n quantifier) Selector() selector.Selector { } func (n quantifier) Value() Policy { - return n.policy + return n.statements } func All(selector selector.Selector, policy ...Statement) QuantifierStatement {