Merge pull request #86 from ucan-wg/test/invocation-verifies-args-vs-pols

test(invocation): verify arguments versus aggregated policies
This commit is contained in:
Steve Moyer
2024-11-27 14:56:05 -05:00
committed by GitHub
16 changed files with 321 additions and 61 deletions

71
pkg/args/builder.go Normal file
View File

@@ -0,0 +1,71 @@
package args
import (
"errors"
"github.com/ipld/go-ipld-prime"
)
// Builder allows the fluid construction of an Args.
type Builder struct {
args *Args
errs error
}
// NewBuilder returns a Builder which will assemble the Args.
func NewBuilder() *Builder {
return &Builder{
args: New(),
}
}
// Add inserts a new key/val into the Args being assembled while collecting
// any errors caused by duplicate keys.
func (b *Builder) Add(key string, val any) *Builder {
b.errs = errors.Join(b.errs, b.args.Add(key, val))
return b
}
// Build returns the assembled Args or an error containing a list of
// errors encountered while trying to build the Args.
func (b *Builder) Build() (*Args, error) {
if b.errs != nil {
return nil, b.errs
}
return b.args, nil
}
// BuildIPLD is the same as Build except it takes the additional step of
// converting the Args to an ipld.Node.
func (b *Builder) BuildIPLD() (ipld.Node, error) {
args, err := b.Build()
if err != nil {
return nil, err
}
return args.ToIPLD()
}
// MustBuild is the same as Build except it panics if an error occurs.
func (b *Builder) MustBuild() *Args {
args, err := b.Build()
if err != nil {
panic(b.errs)
}
return args
}
// MustBuildIPLD is the same as BuildIPLD except it panics if an error
// occurs.
func (b *Builder) MustBuildIPLD() ipld.Node {
node, err := b.BuildIPLD()
if err != nil {
panic(err)
}
return node
}

81
pkg/args/builder_test.go Normal file
View File

@@ -0,0 +1,81 @@
package args_test
import (
"testing"
"github.com/ipld/go-ipld-prime"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/pkg/args"
)
func TestBuilder_XXX(t *testing.T) {
t.Parallel()
const (
keyOne = "key1"
valOne = "string"
keyTwo = "key2"
valTwo = 42
)
exp := args.New()
exp.Add(keyOne, valOne)
exp.Add(keyTwo, valTwo)
expNode, err := exp.ToIPLD()
require.NoError(t, err)
disjointKeys := args.NewBuilder().
Add(keyOne, valOne).
Add(keyTwo, valTwo)
duplicateKeys := args.NewBuilder().
Add(keyOne, valOne).
Add(keyTwo, valTwo).
Add(keyOne, "oh no!")
t.Run("MustBuild succeeds with disjoint keys", func(t *testing.T) {
t.Parallel()
var act *args.Args
require.NotPanics(t, func() {
act = disjointKeys.MustBuild()
})
assert.Equal(t, exp, act)
})
t.Run("MustBuild fails with duplicate keys", func(t *testing.T) {
t.Parallel()
var act *args.Args
require.Panics(t, func() {
act = duplicateKeys.MustBuild()
})
assert.Nil(t, act)
})
t.Run("MustBuildIPLD succeeds with disjoint keys", func(t *testing.T) {
t.Parallel()
var act ipld.Node
require.NotPanics(t, func() {
act = disjointKeys.MustBuildIPLD()
})
assert.Equal(t, expNode, act)
})
t.Run("MustBuildIPLD fails with duplicate keys", func(t *testing.T) {
t.Parallel()
var act ipld.Node
require.Panics(t, func() {
act = duplicateKeys.MustBuildIPLD()
})
assert.Nil(t, act)
})
}

View File

@@ -7,11 +7,8 @@ import (
"github.com/ipfs/go-cid"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagjson"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/fluent/qp"
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
"github.com/ipld/go-ipld-prime/node/basicnode"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
@@ -904,55 +901,3 @@ func TestPartialMatch(t *testing.T) {
})
}
}
// TestInvocationValidation applies the example policy to the second
// example arguments as defined in the [Validation] section of the
// invocation specification.
//
// [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation
func TestInvocationValidationSpecExamples(t *testing.T) {
t.Parallel()
pol := MustConstruct(
Equal(".from", literal.String("alice@example.com")),
Any(".to", Like(".", "*@example.com")),
)
t.Run("with passing args", func(t *testing.T) {
t.Parallel()
argsNode, err := qp.BuildMap(basicnode.Prototype.Any, 2, func(ma datamodel.MapAssembler) {
qp.MapEntry(ma, "from", qp.String("alice@example.com"))
qp.MapEntry(ma, "to", qp.List(2, func(la datamodel.ListAssembler) {
qp.ListEntry(la, qp.String("bob@example.com"))
qp.ListEntry(la, qp.String("carol@not.example.com"))
}))
qp.MapEntry(ma, "title", qp.String("Coffee"))
qp.MapEntry(ma, "body", qp.String("Still on for coffee"))
})
require.NoError(t, err)
exec, stmt := pol.Match(argsNode)
assert.True(t, exec)
assert.Nil(t, stmt)
})
t.Run("fails on recipients (second statement)", func(t *testing.T) {
t.Parallel()
argsNode, err := qp.BuildMap(basicnode.Prototype.Any, 2, func(ma datamodel.MapAssembler) {
qp.MapEntry(ma, "from", qp.String("alice@example.com"))
qp.MapEntry(ma, "to", qp.List(2, func(la datamodel.ListAssembler) {
qp.ListEntry(la, qp.String("bob@null.com"))
qp.ListEntry(la, qp.String("carol@elsewhere.example.com"))
}))
qp.MapEntry(ma, "title", qp.String("Coffee"))
qp.MapEntry(ma, "body", qp.String("Still on for coffee"))
})
require.NoError(t, err)
exec, stmt := pol.Match(argsNode)
assert.False(t, exec)
assert.NotNil(t, stmt)
})
}

View File

@@ -0,0 +1,67 @@
package policytest
import (
"github.com/ipld/go-ipld-prime"
"github.com/ucan-wg/go-ucan/pkg/args"
"github.com/ucan-wg/go-ucan/pkg/policy"
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
)
// EmptyPolicy provides a Policy with no statements.
var EmptyPolicy = policy.Policy{}
// ExampleValidationPolicy provides a instantiated SpecPolicy containing the
// statements that are included in the second code block of the [Validation]
// section of the delegation specification.
//
// [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation
var SpecPolicy = policy.MustConstruct(
policy.Equal(".from", literal.String("alice@example.com")),
policy.Any(".to", policy.Like(".", "*@example.com")),
)
// TODO: Replace the URL for [Validation] above when the delegation
// specification has been finished/merged.
// SpecValidArguments provides valid, instantiated Arguments containing
// the key/value pairs that are included in portion of the the second code
// block of the [Validation] section of the delegation specification.
//
// [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation
var SpecValidArguments = args.NewBuilder().
Add("from", "alice@example.com").
Add("to", []string{
"bob@example.com",
"carol@not.example.com",
}).
Add("title", "Coffee").
Add("body", "Still on for coffee").
MustBuild()
var specValidArgumentsIPLD = mustIPLD(SpecValidArguments)
// SpecInvalidArguments provides invalid, instantiated Arguments containing
// the key/value pairs that are included in portion of the the second code
// block of the [Validation] section of the delegation specification.
//
// [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation
var SpecInvalidArguments = args.NewBuilder().
Add("from", "alice@example.com").
Add("to", []string{
"bob@null.com",
"carol@elsewhere.example.com",
}).
Add("title", "Coffee").
Add("body", "Still on for coffee").
MustBuild()
var specInvalidArgumentsIPLD = mustIPLD(SpecInvalidArguments)
func mustIPLD(args *args.Args) ipld.Node {
node, err := args.ToIPLD()
if err != nil {
panic(err)
}
return node
}

View File

@@ -0,0 +1,32 @@
package policytest
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestInvocationValidation applies the example policy to the second
// example arguments as defined in the [Validation] section of the
// invocation specification.
//
// [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation
func TestInvocationValidationSpecExamples(t *testing.T) {
t.Parallel()
t.Run("with passing args", func(t *testing.T) {
t.Parallel()
exec, stmt := SpecPolicy.Match(specValidArgumentsIPLD)
assert.True(t, exec)
assert.Nil(t, stmt)
})
t.Run("fails on recipients (second statement)", func(t *testing.T) {
t.Parallel()
exec, stmt := SpecPolicy.Match(specInvalidArgumentsIPLD)
assert.False(t, exec)
assert.NotNil(t, stmt)
})
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/ucan-wg/go-ucan/did/didtest"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/pkg/policy"
"github.com/ucan-wg/go-ucan/pkg/policy/policytest"
"github.com/ucan-wg/go-ucan/token/delegation"
"github.com/ucan-wg/go-ucan/token/delegation/delegationtest"
)
@@ -86,7 +87,7 @@ func (g *generator) chainPersonas(personas []didtest.Persona, acc acc, vari vari
privKey: personas[0].PrivKey(),
aud: personas[1].DID(),
cmd: delegationtest.NominalCommand,
pol: policy.Policy{},
pol: policytest.EmptyPolicy,
opts: []delegation.Option{
delegation.WithSubject(didtest.PersonaAlice.DID()),
delegation.WithNonce(constantNonce),
@@ -128,6 +129,9 @@ func (g *generator) chainPersonas(personas []didtest.Persona, acc acc, vari vari
}
p.opts = append(p.opts, delegation.WithNotBefore(nbf))
}},
{name: "ValidExamplePolicy", variant: func(p *newDelegationParams) {
p.pol = policytest.SpecPolicy
}},
}
// Start a branch in the recursion for each of the variants

View File

@@ -75,17 +75,17 @@ var (
)
var (
TokenCarolDan_InvalidExpiredCID = gocid.MustParse("bafyreigenypixaxvhzlry5rjnywvjyl4xvzlzxz2ui74uzys7qdhos4bbu")
TokenCarolDan_InvalidExpiredCID = gocid.MustParse("bafyreifrbm6bgyqdzhhcubbb7dnhq3aq6udvdbfs7mhqjs3d2ihraelufu")
TokenCarolDan_InvalidExpired = mustGetDelegation(TokenCarolDan_InvalidExpiredCID)
)
var (
TokenDanErin_InvalidExpiredCID = gocid.MustParse("bafyreifvnfb7zqocpdysedcvjkb4y7tqfuziuqjhbbdoay4zg33pwpbzqi")
TokenDanErin_InvalidExpiredCID = gocid.MustParse("bafyreibbh5ujs6udphkl3exffohxsg5mdknoqzjb3gdhmuncg3qnomzemy")
TokenDanErin_InvalidExpired = mustGetDelegation(TokenDanErin_InvalidExpiredCID)
)
var (
TokenErinFrank_InvalidExpiredCID = gocid.MustParse("bafyreicvydzt3obkqx7krmoi3zu4tlirlksibxfks5jc7vlvjxjamv2764")
TokenErinFrank_InvalidExpiredCID = gocid.MustParse("bafyreiggzczmqlybhxljmlfot5t7o4w6fhdv7fme77a466ku73dhxtqzdq")
TokenErinFrank_InvalidExpired = mustGetDelegation(TokenErinFrank_InvalidExpiredCID)
)
@@ -104,6 +104,21 @@ var (
TokenErinFrank_InvalidInactive = mustGetDelegation(TokenErinFrank_InvalidInactiveCID)
)
var (
TokenCarolDan_ValidExamplePolicyCID = gocid.MustParse("bafyreibtfrp2njnkjrcuhxd4ebaecmpcql5knek2h2j2fjzu2sij2tv6ei")
TokenCarolDan_ValidExamplePolicy = mustGetDelegation(TokenCarolDan_ValidExamplePolicyCID)
)
var (
TokenDanErin_ValidExamplePolicyCID = gocid.MustParse("bafyreidxfwbkzujpu7ivulkc7b6ff4cpbzrkeklmxqvyhhmkmym5b45e2e")
TokenDanErin_ValidExamplePolicy = mustGetDelegation(TokenDanErin_ValidExamplePolicyCID)
)
var (
TokenErinFrank_ValidExamplePolicyCID = gocid.MustParse("bafyreiatkvtvgakqcrdk6vgrv7tbq5rbeiqct52ep4plcftp2agffjyvp4")
TokenErinFrank_ValidExamplePolicy = mustGetDelegation(TokenErinFrank_ValidExamplePolicyCID)
)
var ProofAliceBob = []gocid.Cid{
TokenAliceBobCID,
}
@@ -238,3 +253,24 @@ var ProofAliceBobCarolDanErinFrank_InvalidInactive = []gocid.Cid{
TokenBobCarolCID,
TokenAliceBobCID,
}
var ProofAliceBobCarolDan_ValidExamplePolicy = []gocid.Cid{
TokenCarolDan_ValidExamplePolicyCID,
TokenBobCarolCID,
TokenAliceBobCID,
}
var ProofAliceBobCarolDanErin_ValidExamplePolicy = []gocid.Cid{
TokenDanErin_ValidExamplePolicyCID,
TokenCarolDan_ValidExamplePolicyCID,
TokenBobCarolCID,
TokenAliceBobCID,
}
var ProofAliceBobCarolDanErinFrank_ValidExamplePolicy = []gocid.Cid{
TokenErinFrank_ValidExamplePolicyCID,
TokenDanErin_ValidExamplePolicyCID,
TokenCarolDan_ValidExamplePolicyCID,
TokenBobCarolCID,
TokenAliceBobCID,
}

View File

@@ -35,3 +35,7 @@ var (
// next delegation or invocation's command.
ErrCommandNotCovered = errors.New("allowed command doesn't cover the next delegation or invocation")
)
// ErrPolicyNotSatisfied is returned when the provided Arguments don't
// satisfy one or more of the aggregated Policy Statements
var ErrPolicyNotSatisfied = errors.New("the following UCAN policy is not satisfied")

View File

@@ -9,6 +9,7 @@ import (
"github.com/ucan-wg/go-ucan/did/didtest"
"github.com/ucan-wg/go-ucan/pkg/args"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/pkg/policy/policytest"
"github.com/ucan-wg/go-ucan/token/delegation/delegationtest"
"github.com/ucan-wg/go-ucan/token/invocation"
)
@@ -48,6 +49,18 @@ func TestToken_ExecutionAllowed(t *testing.T) {
testPasses(t, didtest.PersonaFrank, delegationtest.AttenuatedCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank)
})
t.Run("passes - arguments satisfy empty policy", func(t *testing.T) {
t.Parallel()
testPasses(t, didtest.PersonaFrank, delegationtest.NominalCommand, policytest.SpecValidArguments, delegationtest.ProofAliceBobCarolDanErinFrank)
})
t.Run("passes - arguments satify example policy", func(t *testing.T) {
t.Parallel()
testPasses(t, didtest.PersonaFrank, delegationtest.NominalCommand, policytest.SpecValidArguments, delegationtest.ProofAliceBobCarolDanErinFrank_ValidExamplePolicy)
})
t.Run("fails - no proof", func(t *testing.T) {
t.Parallel()
@@ -115,12 +128,19 @@ func TestToken_ExecutionAllowed(t *testing.T) {
testFails(t, invocation.ErrWrongSub, didtest.PersonaFrank, delegationtest.ExpandedCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank_InvalidSubject)
})
t.Run("passes - arguments satify example policy", func(t *testing.T) {
t.Parallel()
testFails(t, invocation.ErrPolicyNotSatisfied, didtest.PersonaFrank, delegationtest.NominalCommand, policytest.SpecInvalidArguments, delegationtest.ProofAliceBobCarolDanErinFrank_ValidExamplePolicy)
})
}
func test(t *testing.T, persona didtest.Persona, cmd command.Command, args *args.Args, prf []cid.Cid, opts ...invocation.Option) error {
t.Helper()
// TODO: use the args and add minimal test to check that they are verified against the policy
opts = append(opts, invocation.WithArguments(args))
tkn, err := invocation.New(persona.DID(), didtest.PersonaAlice.DID(), cmd, prf, opts...)
require.NoError(t, err)

View File

@@ -134,7 +134,7 @@ func (t *Token) verifyArgs(delegations []*delegation.Token, arguments *args.Args
ok, statement := policies.Match(argsIpld)
if !ok {
return fmt.Errorf("the following UCAN policy is not satisfied: %v", statement.String())
return fmt.Errorf("%w: %v", ErrPolicyNotSatisfied, statement.String())
}
return nil