From 7d4f973171cde87339c0b740863ace6f3015ba1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 5 Nov 2024 17:39:39 +0100 Subject: [PATCH] invocation: round of cleanups/fixes --- token/delegation/delegation.go | 30 +++-------- token/internal/parse/parse.go | 22 ++++++++ token/invocation/examples_test.go | 7 +-- token/invocation/invocation.go | 83 +++++++++++-------------------- token/invocation/ipld.go | 9 +++- token/invocation/ipld_test.go | 22 ++------ token/invocation/options.go | 3 +- token/invocation/schema_test.go | 4 +- 8 files changed, 79 insertions(+), 101 deletions(-) create mode 100644 token/internal/parse/parse.go diff --git a/token/delegation/delegation.go b/token/delegation/delegation.go index 77ba14d..f1e5553 100644 --- a/token/delegation/delegation.go +++ b/token/delegation/delegation.go @@ -21,6 +21,7 @@ import ( "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/pkg/meta" "github.com/ucan-wg/go-ucan/pkg/policy" + "github.com/ucan-wg/go-ucan/token/internal/parse" ) // Token is an immutable type that holds the fields of a UCAN delegation. @@ -184,27 +185,19 @@ func tokenFromModel(m tokenPayloadModel) (*Token, error) { return nil, fmt.Errorf("parse iss: %w", err) } - tkn.audience, err = did.Parse(m.Aud) - if err != nil { + if tkn.audience, err = did.Parse(m.Aud); err != nil { return nil, fmt.Errorf("parse audience: %w", err) } - if m.Sub != nil { - tkn.subject, err = did.Parse(*m.Sub) - if err != nil { - return nil, fmt.Errorf("parse subject: %w", err) - } - } else { - tkn.subject = did.Undef + if tkn.subject, err = parse.OptionalDID(m.Sub); err != nil { + return nil, fmt.Errorf("parse subject: %w", err) } - tkn.command, err = command.Parse(m.Cmd) - if err != nil { + if tkn.command, err = command.Parse(m.Cmd); err != nil { return nil, fmt.Errorf("parse command: %w", err) } - tkn.policy, err = policy.FromIPLD(m.Pol) - if err != nil { + if tkn.policy, err = policy.FromIPLD(m.Pol); err != nil { return nil, fmt.Errorf("parse policy: %w", err) } @@ -215,15 +208,8 @@ func tokenFromModel(m tokenPayloadModel) (*Token, error) { tkn.meta = m.Meta - if m.Nbf != nil { - t := time.Unix(*m.Nbf, 0) - tkn.notBefore = &t - } - - if m.Exp != nil { - t := time.Unix(*m.Exp, 0) - tkn.expiration = &t - } + tkn.notBefore = parse.OptionalTimestamp(m.Nbf) + tkn.expiration = parse.OptionalTimestamp(m.Exp) if err := tkn.validate(); err != nil { return nil, err diff --git a/token/internal/parse/parse.go b/token/internal/parse/parse.go new file mode 100644 index 0000000..147b308 --- /dev/null +++ b/token/internal/parse/parse.go @@ -0,0 +1,22 @@ +package parse + +import ( + "time" + + "github.com/ucan-wg/go-ucan/did" +) + +func OptionalDID(s *string) (did.DID, error) { + if s == nil { + return did.Undef, nil + } + return did.Parse(*s) +} + +func OptionalTimestamp(sec *int64) *time.Time { + if sec == nil { + return nil + } + t := time.Unix(*sec, 0) + return &t +} diff --git a/token/invocation/examples_test.go b/token/invocation/examples_test.go index 83ec04e..f948041 100644 --- a/token/invocation/examples_test.go +++ b/token/invocation/examples_test.go @@ -15,6 +15,7 @@ import ( "github.com/ipld/go-ipld-prime/fluent/qp" "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/libp2p/go-libp2p/core/crypto" + "github.com/ucan-wg/go-ucan/did" "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/token/invocation" @@ -28,11 +29,7 @@ func ExampleNew() { return } - inv, err := invocation.New( - iss, - sub, - cmd, - prf, + inv, err := invocation.New(iss, sub, cmd, prf, invocation.WithArguments(args), invocation.WithMeta("env", "development"), invocation.WithMeta("tags", meta["tags"]), diff --git a/token/invocation/invocation.go b/token/invocation/invocation.go index 38b1c68..a1fdcb9 100644 --- a/token/invocation/invocation.go +++ b/token/invocation/invocation.go @@ -15,9 +15,11 @@ import ( "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ucan-wg/go-ucan/did" "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/pkg/meta" + "github.com/ucan-wg/go-ucan/token/internal/parse" ) // Token is an immutable type that holds the fields of a UCAN invocation. @@ -60,24 +62,18 @@ type Token struct { // WithInvokedAt or WithInvokedAtIn Options to specify a different time // or the WithoutInvokedAt Option to clear the Token's invokedAt field. // -// With the exception of the WithMeta option, all other will overwrite +// With the exception of the WithMeta option, all others will overwrite // the previous contents of their target field. func New(iss, sub did.DID, cmd command.Command, prf []cid.Cid, opts ...Option) (*Token, error) { - nonce, err := generateNonce() - if err != nil { - return nil, err - } - iat := time.Now() - metadata := meta.NewMeta() tkn := Token{ issuer: iss, subject: sub, command: cmd, proof: prf, - meta: metadata, - nonce: nonce, + meta: meta.NewMeta(), + nonce: nil, invokedAt: &iat, } @@ -87,8 +83,15 @@ func New(iss, sub did.DID, cmd command.Command, prf []cid.Cid, opts ...Option) ( } } - if len(tkn.meta.Keys) == 0 { - tkn.meta = nil + if len(tkn.nonce) == 0 { + tkn.nonce, err = generateNonce() + if err != nil { + return nil, err + } + } + + if err := tkn.validate(); err != nil { + return nil, err } return &tkn, nil @@ -120,7 +123,7 @@ func (t *Token) Arguments() map[string]datamodel.Node { return t.arguments } -// Proof() returns the ordered list of cid.Cids which referenced the +// Proof() returns the ordered list of cid.Cid which reference the // delegation Tokens that authorize this invocation. func (t *Token) Proof() []cid.Cid { return t.proof @@ -163,8 +166,7 @@ func (t *Token) validate() error { } requiredDID(t.issuer, "Issuer") - - // TODO + requiredDID(t.subject, "Subject") if len(t.nonce) < 12 { errs = errors.Join(errs, fmt.Errorf("token nonce too small")) @@ -182,35 +184,36 @@ func tokenFromModel(m tokenPayloadModel) (*Token, error) { ) if tkn.issuer, err = did.Parse(m.Iss); err != nil { - return nil, err + return nil, fmt.Errorf("parse iss: %w", err) } if tkn.subject, err = did.Parse(m.Sub); err != nil { - return nil, err + return nil, fmt.Errorf("parse subject: %w", err) } - if tkn.audience, err = parseOptionalDID(m.Aud); err != nil { - return nil, err + if tkn.audience, err = parse.OptionalDID(m.Aud); err != nil { + return nil, fmt.Errorf("parse audience: %w", err) } if tkn.command, err = command.Parse(m.Cmd); err != nil { - return nil, err + return nil, fmt.Errorf("parse command: %w", err) } + if len(m.Nonce) == 0 { + return nil, fmt.Errorf("nonce is required") + } + tkn.nonce = m.Nonce + tkn.arguments = m.Args.Values tkn.proof = m.Prf tkn.meta = m.Meta - tkn.nonce = m.Nonce - if tkn.expiration, err = parseOptionalTimestamp(m.Exp); err != nil { - return nil, err - } + tkn.expiration = parse.OptionalTimestamp(m.Exp) + tkn.invokedAt = parse.OptionalTimestamp(m.Iat) - if tkn.invokedAt, err = parseOptionalTimestamp(m.Iat); err != nil { - return nil, err - } + tkn.cause = m.Cause - if tkn.cause, err = parseOptionalCID(m.Cause); err != nil { + if err := tkn.validate(); err != nil { return nil, err } @@ -227,29 +230,3 @@ func generateNonce() ([]byte, error) { } return res, nil } - -func parseOptionalCID(c *cid.Cid) (*cid.Cid, error) { - if c == nil { - return nil, nil - } - - return c, nil -} - -func parseOptionalDID(s *string) (did.DID, error) { - if s == nil { - return did.Undef, nil - } - - return did.Parse(*s) -} - -func parseOptionalTimestamp(sec *int64) (*time.Time, error) { - if sec == nil { - return nil, nil - } - - t := time.Unix(*sec, 0) - - return &t, nil -} diff --git a/token/invocation/ipld.go b/token/invocation/ipld.go index 817b265..431e3a4 100644 --- a/token/invocation/ipld.go +++ b/token/invocation/ipld.go @@ -218,8 +218,9 @@ func (t *Token) toIPLD(privKey crypto.PrivKey) (datamodel.Node, error) { } argsKey := make([]string, len(t.arguments)) - i := 0 + // TODO: make specialized type and builder? + i := 0 for k := range t.arguments { argsKey[i] = k i++ @@ -230,6 +231,7 @@ func (t *Token) toIPLD(privKey crypto.PrivKey) (datamodel.Node, error) { Values: t.arguments, } + // TODO: reuse instead of copy? it's immutable prf := make([]cid.Cid, len(t.proof)) for i, c := range t.proof { prf[i] = c @@ -249,5 +251,10 @@ func (t *Token) toIPLD(privKey crypto.PrivKey) (datamodel.Node, error) { Cause: t.cause, } + // seems like it's a requirement to have a null meta if there are no values? + if len(model.Meta.Keys) == 0 { + model.Meta = nil + } + return envelope.ToIPLD(privKey, model) } diff --git a/token/invocation/ipld_test.go b/token/invocation/ipld_test.go index ae52685..ec3c04f 100644 --- a/token/invocation/ipld_test.go +++ b/token/invocation/ipld_test.go @@ -4,13 +4,9 @@ import ( "testing" "time" - "github.com/ipfs/go-cid" - "github.com/ipld/go-ipld-prime/datamodel" - "github.com/libp2p/go-libp2p/core/crypto" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/ucan-wg/go-ucan/did" - "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/token/invocation" ) @@ -20,16 +16,13 @@ func TestSealUnsealRoundtrip(t *testing.T) { privKey, iss, sub, cmd, args, prf, meta, err := setupExampleNew() require.NoError(t, err) - tkn1, err := invocation.New( - iss, - sub, - cmd, - prf, + tkn1, err := invocation.New(iss, sub, cmd, prf, invocation.WithArguments(args), invocation.WithMeta("env", "development"), invocation.WithMeta("tags", meta["tags"]), invocation.WithExpirationIn(time.Minute), - invocation.WithoutInvokedAt()) + invocation.WithoutInvokedAt(), + ) require.NoError(t, err) data, cid1, err := tkn1.ToSealed(privKey) @@ -41,10 +34,3 @@ func TestSealUnsealRoundtrip(t *testing.T) { assert.Equal(t, cid1, cid2) assert.Equal(t, tkn1, tkn2) } - -func setupNew(t *testing.T) (privKey crypto.PrivKey, iss, sub did.DID, cmd command.Command, args map[string]datamodel.Node, prf []cid.Cid, meta map[string]datamodel.Node) { - privKey, iss, sub, cmd, args, prf, meta, err := setupExampleNew() - require.NoError(t, err) - - return // WARNING: named return values -} diff --git a/token/invocation/options.go b/token/invocation/options.go index c8656d5..1c557b9 100644 --- a/token/invocation/options.go +++ b/token/invocation/options.go @@ -5,11 +5,12 @@ import ( "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ucan-wg/go-ucan/did" ) // Option is a type that allows optional fields to be set during the -// creation of a invocation Token. +// creation of an invocation Token. type Option func(*Token) error // WithArgument adds a key/value pair to the Token's Arguments field. diff --git a/token/invocation/schema_test.go b/token/invocation/schema_test.go index 8fcc857..77b1afb 100644 --- a/token/invocation/schema_test.go +++ b/token/invocation/schema_test.go @@ -8,9 +8,10 @@ import ( "github.com/libp2p/go-libp2p/core/crypto" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gotest.tools/v3/golden" + "github.com/ucan-wg/go-ucan/token/internal/envelope" "github.com/ucan-wg/go-ucan/token/invocation" - "gotest.tools/v3/golden" ) const ( @@ -78,6 +79,7 @@ func TestSchemaRoundTrip(t *testing.T) { assert.JSONEq(t, string(invocationJson), readJson.String()) }) } + func privKey(t require.TestingT, privKeyCfg string) crypto.PrivKey { privKeyMar, err := crypto.ConfigDecodeKey(privKeyCfg) require.NoError(t, err)