invocation: round of cleanups/fixes

This commit is contained in:
Michael Muré
2024-11-05 17:39:39 +01:00
committed by Steve Moyer
parent 3dc0011628
commit 7d4f973171
8 changed files with 79 additions and 101 deletions

View File

@@ -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"]),

View File

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

View File

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

View File

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

View File

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

View File

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