policy: IPLD encode/decode

Still WIP
This commit is contained in:
Michael Muré
2024-09-01 20:27:54 +02:00
parent 4d0a0a2bcc
commit 8c09024003
5 changed files with 377 additions and 32 deletions

252
capability/policy/ipld.go Normal file
View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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))
}
}

View File

@@ -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)
})

View File

@@ -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 {