diff --git a/pkg/policy/match_test.go b/pkg/policy/match_test.go index 6a85512..108037a 100644 --- a/pkg/policy/match_test.go +++ b/pkg/policy/match_test.go @@ -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) - }) -} diff --git a/pkg/policy/policytest/example.go b/pkg/policy/policytest/example.go new file mode 100644 index 0000000..e9d20c6 --- /dev/null +++ b/pkg/policy/policytest/example.go @@ -0,0 +1,98 @@ +package policytest + +import ( + "errors" + + "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 Policy 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 ExamplePolicy = 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. + +// ExampleValidArguments 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 ExampleValidArguments = newBuilder(nil). + 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 exampleValidArgumentsIPLD = mustIPLD(ExampleValidArguments) + +// ExampleInvalidArguments 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 ExampleInvalidArguments = newBuilder(nil). + 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 exampleInvalidArgumentsIPLD = mustIPLD(ExampleInvalidArguments) + +type builder struct { + args *args.Args + errs error +} + +func newBuilder(a *args.Args) *builder { + if a == nil { + a = args.New() + } + + return &builder{ + args: a, + } +} + +func (b *builder) add(key string, val any) *builder { + b.errs = errors.Join(b.errs, b.args.Add(key, val)) + + return b +} + +func (b *builder) mustBuild() *args.Args { + if b.errs != nil { + panic(b.errs) + } + + return b.args +} + +func mustIPLD(args *args.Args) ipld.Node { + node, err := args.ToIPLD() + if err != nil { + panic(err) + } + + return node +} diff --git a/pkg/policy/policytest/example_test.go b/pkg/policy/policytest/example_test.go new file mode 100644 index 0000000..4025d8a --- /dev/null +++ b/pkg/policy/policytest/example_test.go @@ -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 := ExamplePolicy.Match(exampleValidArgumentsIPLD) + assert.True(t, exec) + assert.Nil(t, stmt) + }) + + t.Run("fails on recipients (second statement)", func(t *testing.T) { + t.Parallel() + + exec, stmt := ExamplePolicy.Match(exampleInvalidArgumentsIPLD) + assert.False(t, exec) + assert.NotNil(t, stmt) + }) +} diff --git a/token/delegation/delegationtest/data/TokenCarolDan_InvalidExpired.dagcbor b/token/delegation/delegationtest/data/TokenCarolDan_InvalidExpired.dagcbor index c67b5e2..2c3342f 100644 Binary files a/token/delegation/delegationtest/data/TokenCarolDan_InvalidExpired.dagcbor and b/token/delegation/delegationtest/data/TokenCarolDan_InvalidExpired.dagcbor differ diff --git a/token/delegation/delegationtest/data/TokenCarolDan_ValidExamplePolicy.dagcbor b/token/delegation/delegationtest/data/TokenCarolDan_ValidExamplePolicy.dagcbor new file mode 100644 index 0000000..d53ec6b Binary files /dev/null and b/token/delegation/delegationtest/data/TokenCarolDan_ValidExamplePolicy.dagcbor differ diff --git a/token/delegation/delegationtest/data/TokenDanErin_InvalidExpired.dagcbor b/token/delegation/delegationtest/data/TokenDanErin_InvalidExpired.dagcbor index 2cbeeba..d491d6b 100644 Binary files a/token/delegation/delegationtest/data/TokenDanErin_InvalidExpired.dagcbor and b/token/delegation/delegationtest/data/TokenDanErin_InvalidExpired.dagcbor differ diff --git a/token/delegation/delegationtest/data/TokenDanErin_ValidExamplePolicy.dagcbor b/token/delegation/delegationtest/data/TokenDanErin_ValidExamplePolicy.dagcbor new file mode 100644 index 0000000..2fe6276 Binary files /dev/null and b/token/delegation/delegationtest/data/TokenDanErin_ValidExamplePolicy.dagcbor differ diff --git a/token/delegation/delegationtest/data/TokenErinFrank_InvalidExpired.dagcbor b/token/delegation/delegationtest/data/TokenErinFrank_InvalidExpired.dagcbor index ef55b22..f6596d6 100644 Binary files a/token/delegation/delegationtest/data/TokenErinFrank_InvalidExpired.dagcbor and b/token/delegation/delegationtest/data/TokenErinFrank_InvalidExpired.dagcbor differ diff --git a/token/delegation/delegationtest/data/TokenErinFrank_ValidExamplePolicy.dagcbor b/token/delegation/delegationtest/data/TokenErinFrank_ValidExamplePolicy.dagcbor new file mode 100644 index 0000000..7a6f541 Binary files /dev/null and b/token/delegation/delegationtest/data/TokenErinFrank_ValidExamplePolicy.dagcbor differ diff --git a/token/delegation/delegationtest/generator/generator.go b/token/delegation/delegationtest/generator/generator.go index f70cb0f..3fca615 100644 --- a/token/delegation/delegationtest/generator/generator.go +++ b/token/delegation/delegationtest/generator/generator.go @@ -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.ExamplePolicy + }}, } // Start a branch in the recursion for each of the variants diff --git a/token/delegation/delegationtest/token_gen.go b/token/delegation/delegationtest/token_gen.go index 2a24424..63359ae 100644 --- a/token/delegation/delegationtest/token_gen.go +++ b/token/delegation/delegationtest/token_gen.go @@ -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, +} diff --git a/token/invocation/errors.go b/token/invocation/errors.go index 5335763..9ea0d8b 100644 --- a/token/invocation/errors.go +++ b/token/invocation/errors.go @@ -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") diff --git a/token/invocation/invocation_test.go b/token/invocation/invocation_test.go index 20ce554..5e991fc 100644 --- a/token/invocation/invocation_test.go +++ b/token/invocation/invocation_test.go @@ -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.ExampleValidArguments, delegationtest.ProofAliceBobCarolDanErinFrank) + }) + + t.Run("passes - arguments satify example policy", func(t *testing.T) { + t.Parallel() + + testPasses(t, didtest.PersonaFrank, delegationtest.NominalCommand, policytest.ExampleValidArguments, 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.ExampleInvalidArguments, 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) diff --git a/token/invocation/proof.go b/token/invocation/proof.go index a61e022..0d130a2 100644 --- a/token/invocation/proof.go +++ b/token/invocation/proof.go @@ -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