chore(v1): merge in other changes

This commit is contained in:
Steve Moyer
2024-09-09 09:09:17 -04:00
13 changed files with 256 additions and 137 deletions

View File

@@ -1,54 +1,11 @@
package command
import (
"errors"
"fmt"
"strings"
)
const (
separator = "/"
)
// ErrNew indicates that the wrapped error was encountered while creating
// a new Command.
var ErrNew = errors.New("failed to create Command from elems")
// ErrParse indicates that the wrapped error was encountered while
// attempting to parse a string as a Command.
var ErrParse = errors.New("failed to parse Command")
// ErrorJoin indicates that the wrapped error was encountered while
// attempting to join a new segment to a Command.
var ErrJoin = errors.New("failed to join segments to Command")
// ErrRequiresLeadingSlash is returned when a parsing a string that
// doesn't start with a [leading slash character].
//
// [leading slash character]: https://github.com/ucan-wg/spec#segment-structure
var ErrRequiresLeadingSlash = parseError("a command requires a leading slash character")
// ErrDisallowsTrailingSlash is returned when parsing a string that [ends
// with a trailing slash character].
//
// [ends with a trailing slash character]: https://github.com/ucan-wg/spec#segment-structure
var ErrDisallowsTrailingSlash = parseError("a command must not include a trailing slash")
// ErrUCANNamespaceReserved is returned to indicate that a Command's
// first segment would contain the [reserved "ucan" namespace].
//
// [reserved "ucan" namespace]: https://github.com/ucan-wg/spec#ucan-namespace
var ErrUCANNamespaceReserved = errors.New("the UCAN namespace is reserved")
// ErrRequiresLowercase is returned if a Command contains, or would contain,
// [uppercase unicode characters].
//
// [uppercase unicode characters]: https://github.com/ucan-wg/spec#segment-structure
var ErrRequiresLowercase = parseError("UCAN path segments must must not contain upper-case characters")
func parseError(msg string) error {
return fmt.Errorf("%w: %s", ErrParse, msg)
}
const separator = "/"
var _ fmt.Stringer = (*Command)(nil)
@@ -66,18 +23,8 @@ type Command struct {
// New creates a validated command from the provided list of segment
// strings. An error is returned if an invalid Command would be
// formed
func New(segments ...string) (*Command, error) {
return newCommand(ErrNew, segments...)
}
func newCommand(err error, segments ...string) (*Command, error) {
if len(segments) > 0 && segments[0] == "ucan" {
return nil, fmt.Errorf("%w: %w", err, ErrUCANNamespaceReserved)
}
cmd := Command{segments}
return &cmd, nil
func New(segments ...string) *Command {
return &Command{segments: segments}
}
// Parse verifies that the provided string contains the required
@@ -100,7 +47,16 @@ func Parse(s string) (*Command, error) {
// The leading slash will result in the first element from strings.Split
// being an empty string which is removed as strings.Join will ignore it.
return newCommand(ErrParse, strings.Split(s, "/")[1:]...)
return &Command{strings.Split(s, "/")[1:]}, nil
}
// MustParse is the same as Parse, but panic() if the parsing fail.
func MustParse(s string) *Command {
c, err := Parse(s)
if err != nil {
panic(err)
}
return c
}
// [Top] is the most powerful capability.
@@ -111,22 +67,19 @@ func Parse(s string) (*Command, error) {
//
// [Top]: https://github.com/ucan-wg/spec#-aka-top
func Top() *Command {
cmd, _ := New()
return cmd
return New()
}
// IsValid returns true if the provided string is a valid UCAN command.
func IsValid(s string) bool {
_, err := Parse(s)
return err == nil
}
// Join appends segments to the end of this command using the required
// segment separator.
func (c *Command) Join(segments ...string) (*Command, error) {
return newCommand(ErrJoin, append(c.segments, segments...)...)
func (c *Command) Join(segments ...string) *Command {
return &Command{append(c.segments, segments...)}
}
// Segments returns the ordered segments that comprise the Command as a
@@ -138,5 +91,5 @@ func (c *Command) Segments() []string {
// String returns the composed representation the command. This is also
// the required wire representation (before IPLD encoding occurs.)
func (c *Command) String() string {
return "/" + strings.Join([]string(c.segments), "/")
return "/" + strings.Join(c.segments, "/")
}

View File

@@ -0,0 +1,21 @@
package command
import "fmt"
// ErrRequiresLeadingSlash is returned when a parsing a string that
// doesn't start with a [leading slash character].
//
// [leading slash character]: https://github.com/ucan-wg/spec#segment-structure
var ErrRequiresLeadingSlash = fmt.Errorf("a command requires a leading slash character")
// ErrDisallowsTrailingSlash is returned when parsing a string that [ends
// with a trailing slash character].
//
// [ends with a trailing slash character]: https://github.com/ucan-wg/spec#segment-structure
var ErrDisallowsTrailingSlash = fmt.Errorf("a command must not include a trailing slash")
// ErrRequiresLowercase is returned if a Command contains, or would contain,
// [uppercase unicode characters].
//
// [uppercase unicode characters]: https://github.com/ucan-wg/spec#segment-structure
var ErrRequiresLowercase = fmt.Errorf("UCAN path segments must must not contain upper-case characters")

View File

@@ -5,7 +5,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/v1/capability/command"
"github.com/ucan-wg/go-ucan/capability/command"
)
func TestTop(t *testing.T) {
@@ -73,7 +73,6 @@ func TestParseCommand(t *testing.T) {
t.Parallel()
cmd, err := command.Parse(testcase.inp)
require.ErrorIs(t, err, command.ErrParse)
require.ErrorIs(t, err, testcase.err)
require.Nil(t, cmd)
})
@@ -134,20 +133,6 @@ func invalidTestcases(t *testing.T) []errorTestcase {
},
err: command.ErrDisallowsTrailingSlash,
},
{
testcase: testcase{
name: "only reserved ucan namespace",
inp: "/ucan",
},
err: command.ErrUCANNamespaceReserved,
},
{
testcase: testcase{
name: "reserved ucan namespace prefix",
inp: "/ucan/elem0/elem1/elem2",
},
err: command.ErrUCANNamespaceReserved,
},
{
testcase: testcase{
name: "uppercase character are present",

View File

@@ -9,7 +9,7 @@ import (
"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"
"github.com/ucan-wg/go-ucan/capability/policy/selector"
)
func FromIPLD(node datamodel.Node) (Policy, error) {
@@ -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,28 +85,31 @@ 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
}
statementsNodes, _ := node.LookupByIndex(2)
statements, err := statementsFromIPLD(path+"1/", statementsNodes)
return quantifier{kind: op, selector: sel, statements: statements}, nil
statementsNode, _ := node.LookupByIndex(2)
statement, err := statementFromIPLD(combinePath(path, op, 1), statementsNode)
if err != nil {
return nil, err
}
return quantifier{kind: op, selector: sel, statement: statement}, nil
default:
return nil, ErrUnrecognizedOperator(path, op)
@@ -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())
@@ -243,7 +246,7 @@ func statementToIPLD(statement Statement) (datamodel.Node, error) {
if err != nil {
return nil, err
}
args, err := statementsToIPLD(statement.statements)
args, err := statementToIPLD(statement.statement)
if err != nil {
return nil, err
}
@@ -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)
}

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ import (
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/must"
"github.com/ucan-wg/go-ucan/v1/capability/policy/selector"
"github.com/ucan-wg/go-ucan/capability/policy/selector"
)
// Match determines if the IPLD node matches the policy document.
@@ -110,7 +110,7 @@ func matchStatement(statement Statement, node ipld.Node) bool {
return false
}
for _, n := range many {
ok := Match(s.statements, n)
ok := matchStatement(s.statement, n)
if !ok {
return false
}
@@ -119,12 +119,13 @@ 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
}
for _, n := range many {
ok := Match(s.statements, n)
ok := matchStatement(s.statement, n)
if ok {
return true
}

View File

@@ -6,12 +6,13 @@ 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"
"github.com/ucan-wg/go-ucan/v1/capability/policy/literal"
"github.com/ucan-wg/go-ucan/v1/capability/policy/selector"
"github.com/ucan-wg/go-ucan/capability/policy/literal"
"github.com/ucan-wg/go-ucan/capability/policy/selector"
)
func TestMatch(t *testing.T) {
@@ -400,8 +401,7 @@ func TestMatch(t *testing.T) {
pol := Policy{
Any(
selector.MustParse(".[]"),
GreaterThan(selector.MustParse(".value"), literal.Int(10)),
LessThan(selector.MustParse(".value"), literal.Int(50)),
GreaterThan(selector.MustParse(".value"), literal.Int(60)),
),
}
ok := Match(pol, nd)
@@ -418,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))
})
}

View File

@@ -6,7 +6,7 @@ import (
"github.com/gobwas/glob"
"github.com/ipld/go-ipld-prime"
"github.com/ucan-wg/go-ucan/v1/capability/policy/selector"
"github.com/ucan-wg/go-ucan/capability/policy/selector"
)
const (
@@ -40,23 +40,23 @@ func (e equality) Kind() string {
}
func Equal(selector selector.Selector, value ipld.Node) Statement {
return equality{KindEqual, selector, value}
return equality{kind: KindEqual, selector: selector, value: value}
}
func GreaterThan(selector selector.Selector, value ipld.Node) Statement {
return equality{KindGreaterThan, selector, value}
return equality{kind: KindGreaterThan, selector: selector, value: value}
}
func GreaterThanOrEqual(selector selector.Selector, value ipld.Node) Statement {
return equality{KindGreaterThanOrEqual, selector, value}
return equality{kind: KindGreaterThanOrEqual, selector: selector, value: value}
}
func LessThan(selector selector.Selector, value ipld.Node) Statement {
return equality{KindLessThan, selector, value}
return equality{kind: KindLessThan, selector: selector, value: value}
}
func LessThanOrEqual(selector selector.Selector, value ipld.Node) Statement {
return equality{KindLessThanOrEqual, selector, value}
return equality{kind: KindLessThanOrEqual, selector: selector, value: value}
}
type negation struct {
@@ -68,7 +68,7 @@ func (n negation) Kind() string {
}
func Not(stmt Statement) Statement {
return negation{stmt}
return negation{statement: stmt}
}
type connective struct {
@@ -81,11 +81,11 @@ func (c connective) Kind() string {
}
func And(stmts ...Statement) Statement {
return connective{KindAnd, stmts}
return connective{kind: KindAnd, statements: stmts}
}
func Or(stmts ...Statement) Statement {
return connective{KindOr, stmts}
return connective{kind: KindOr, statements: stmts}
}
type wildcard struct {
@@ -103,23 +103,23 @@ func Like(selector selector.Selector, pattern string) (Statement, error) {
if err != nil {
return nil, err
}
return wildcard{selector, pattern, g}, nil
return wildcard{selector: selector, pattern: pattern, glob: g}, nil
}
type quantifier struct {
kind string
selector selector.Selector
statements []Statement
kind string
selector selector.Selector
statement Statement
}
func (n quantifier) Kind() string {
return n.kind
}
func All(selector selector.Selector, policy ...Statement) Statement {
return quantifier{KindAll, selector, policy}
func All(selector selector.Selector, statement Statement) Statement {
return quantifier{kind: KindAll, selector: selector, statement: statement}
}
func Any(selector selector.Selector, policy ...Statement) Statement {
return quantifier{KindAny, selector, policy}
func Any(selector selector.Selector, statement Statement) Statement {
return quantifier{kind: KindAny, selector: selector, statement: statement}
}

View File

@@ -12,7 +12,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/v1/capability/policy/selector"
"github.com/ucan-wg/go-ucan/capability/policy/selector"
)
// TestSupported Forms runs tests against the Selector according to the

87
delegation/view.go Normal file
View File

@@ -0,0 +1,87 @@
package delegation
import (
"fmt"
"time"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ucan-wg/go-ucan/capability/command"
"github.com/ucan-wg/go-ucan/capability/policy"
"github.com/ucan-wg/go-ucan/did"
)
type View struct {
// Issuer DID (sender)
Issuer did.DID
// Audience DID (receiver)
Audience did.DID
// Principal that the chain is about (the Subject)
Subject did.DID
// The Command to eventually invoke
Command *command.Command
// The delegation policy
Policy policy.Policy
// A unique, random nonce
Nonce []byte
// Arbitrary Metadata
Meta map[string]datamodel.Node
// "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer
NotBefore time.Time
// The timestamp at which the Invocation becomes invalid
Expiration time.Time
}
// ViewFromModel build a decoded view of the raw IPLD data.
// This function also serves as validation.
func ViewFromModel(m PayloadModel) (*View, error) {
var view View
var err error
view.Issuer, err = did.Parse(m.Iss)
if err != nil {
return nil, fmt.Errorf("parse iss: %w", err)
}
view.Audience, err = did.Parse(m.Aud)
if err != nil {
return nil, fmt.Errorf("parse audience: %w", err)
}
if m.Sub != nil {
view.Subject, err = did.Parse(*m.Sub)
if err != nil {
return nil, fmt.Errorf("parse subject: %w", err)
}
} else {
view.Subject = did.Undef
}
view.Command, err = command.Parse(m.Cmd)
if err != nil {
return nil, fmt.Errorf("parse command: %w", err)
}
view.Policy, err = policy.FromIPLD(m.Pol)
if err != nil {
return nil, fmt.Errorf("parse policy: %w", err)
}
if len(m.Nonce) == 0 {
return nil, fmt.Errorf("nonce is required")
}
view.Nonce = m.Nonce
// TODO: copy?
view.Meta = m.Meta.Values
if m.Nbf != nil {
view.NotBefore = time.Unix(*m.Nbf, 0)
}
if m.Exp != nil {
view.Expiration = time.Unix(*m.Exp, 0)
}
return &view, nil
}

2
go.mod
View File

@@ -1,4 +1,4 @@
module github.com/ucan-wg/go-ucan/v1
module github.com/ucan-wg/go-ucan
go 1.21

View File

@@ -7,7 +7,8 @@ import (
"github.com/libp2p/go-libp2p/core/crypto/pb"
"github.com/stretchr/testify/assert"
"github.com/ucan-wg/go-ucan/v1/internal/varsig"
"github.com/ucan-wg/go-ucan/internal/varsig"
)
func TestDecode(t *testing.T) {