From 2eeaaccc6d58859cb08df649a1695e4635a6e20e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 10 Dec 2024 14:50:29 +0100 Subject: [PATCH] client: follow go-ucan changes, improvements, example --- toolkit/client/client.go | 6 +-- toolkit/client/client_test.go | 76 +++++++++++++++++++++++++++++++++++ toolkit/client/pool.go | 9 +++-- toolkit/client/proof.go | 19 ++++----- toolkit/client/proof_test.go | 12 +++--- toolkit/client/requester.go | 37 +++++++++++++++-- 6 files changed, 134 insertions(+), 25 deletions(-) create mode 100644 toolkit/client/client_test.go diff --git a/toolkit/client/client.go b/toolkit/client/client.go index 3611ef2..d783605 100644 --- a/toolkit/client/client.go +++ b/toolkit/client/client.go @@ -38,9 +38,9 @@ func (c *Client) PrepareInvoke(ctx context.Context, cmd command.Command, subject var proof []cid.Cid // do we already have a valid proof? - if proof = c.pool.FindProof(cmd, c.did, subject); len(proof) == 0 { + if proof = c.pool.FindProof(c.did, cmd, subject); len(proof) == 0 { // we need to request a new proof - proofBundles, err := c.requester.RequestDelegation(ctx, cmd, c.did, subject) + proofBundles, err := c.requester.RequestDelegation(ctx, c.did, cmd, subject) if err != nil { return nil, fmt.Errorf("requesting delegation: %w", err) } @@ -55,7 +55,7 @@ func (c *Client) PrepareInvoke(ctx context.Context, cmd command.Command, subject } } - inv, err := invocation.New(c.did, subject, cmd, proof, opts...) + inv, err := invocation.New(c.did, cmd, subject, proof, opts...) if err != nil { return nil, err } diff --git a/toolkit/client/client_test.go b/toolkit/client/client_test.go new file mode 100644 index 0000000..0e0d25f --- /dev/null +++ b/toolkit/client/client_test.go @@ -0,0 +1,76 @@ +package client + +import ( + "context" + "fmt" + "iter" + "time" + + "github.com/ucan-wg/go-ucan/did" + "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/token/delegation" + "github.com/ucan-wg/go-ucan/token/invocation" +) + +func ExampleNewClient() { + servicePersona := didtest.PersonaAlice + clientPersona := didtest.PersonaBob + + // requester is an adaptor for a real world issuer, we use a mock in that example + requester := &requesterMock{persona: servicePersona} + + client, err := NewClient(clientPersona.PrivKey(), requester) + handleError(err) + + cont, err := client.PrepareInvoke( + context.Background(), + command.New("crud", "add"), + servicePersona.DID(), + // extra invocation parameters: + invocation.WithExpirationIn(10*time.Minute), + invocation.WithArgument("foo", "bar"), + invocation.WithMeta("baz", 1234), + ) + handleError(err) + + // this container holds the invocation and all the delegation proofs + b64, err := cont.ToCborBase64() + handleError(err) + + fmt.Println(string(b64)) +} + +func handleError(err error) { + if err != nil { + panic(err) + } +} + +type requesterMock struct { + persona didtest.Persona +} + +func (r requesterMock) RequestDelegation(_ context.Context, audience did.DID, cmd command.Command, _ did.DID) (iter.Seq2[*delegation.Bundle, error], error) { + // the mock issue whatever the client asks: + dlg, err := delegation.Root(r.persona.DID(), audience, cmd, policy.Policy{}) + if err != nil { + return nil, err + } + + dlgBytes, dlgCid, err := dlg.ToSealed(r.persona.PrivKey()) + if err != nil { + return nil, err + } + + bundle := &delegation.Bundle{ + Cid: dlgCid, + Decoded: dlg, + Sealed: dlgBytes, + } + + return func(yield func(*delegation.Bundle, error) bool) { + yield(bundle, nil) + }, nil +} diff --git a/toolkit/client/pool.go b/toolkit/client/pool.go index de2b526..13603cc 100644 --- a/toolkit/client/pool.go +++ b/toolkit/client/pool.go @@ -34,12 +34,13 @@ func (p *Pool) AddBundles(bundles iter.Seq[*delegation.Bundle]) { } // FindProof find in the pool the best (shortest, smallest in bytes) chain of delegation(s) matching the given invocation parameters. -// - cmd: the command to execute // - issuer: the DID of the client, also the issuer of the invocation token -// - audience: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token +// - cmd: the command to execute +// - subject: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token +// Note: you can read it as "(issuer) wants to do (cmd) on (subject)". // Note: the returned delegation(s) don't have to match exactly the parameters, as long as they allow them. // Note: the implemented algorithm won't perform well with a large number of delegations. -func (p *Pool) FindProof(cmd command.Command, iss did.DID, aud did.DID) []cid.Cid { +func (p *Pool) FindProof(issuer did.DID, cmd command.Command, subject did.DID) []cid.Cid { // TODO: move to some kind of background trim job? p.trim() @@ -54,7 +55,7 @@ func (p *Pool) FindProof(cmd command.Command, iss did.DID, aud did.DID) []cid.Ci } } } - }, cmd, iss, aud) + }, issuer, cmd, subject) } func (p *Pool) GetBundles(cids []cid.Cid) iter.Seq2[*delegation.Bundle, error] { diff --git a/toolkit/client/proof.go b/toolkit/client/proof.go index 2b33fd8..2b933d4 100644 --- a/toolkit/client/proof.go +++ b/toolkit/client/proof.go @@ -11,21 +11,22 @@ import ( ) // FindProof find in the pool the best (shortest, smallest in bytes) chain of delegation(s) matching the given invocation parameters. -// - cmd: the command to execute // - issuer: the DID of the client, also the issuer of the invocation token -// - audience: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token +// - cmd: the command to execute +// - subject: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token +// Note: you can read it as "(issuer) wants to do (cmd) on (subject)". // Note: the returned delegation(s) don't have to match exactly the parameters, as long as they allow them. // Note: the implemented algorithm won't perform well with a large number of delegations. -func FindProof(dlgs func() iter.Seq[*delegation.Bundle], cmd command.Command, iss did.DID, aud did.DID) []cid.Cid { +func FindProof(dlgs func() iter.Seq[*delegation.Bundle], issuer did.DID, cmd command.Command, subject did.DID) []cid.Cid { // TODO: maybe that should be part of delegation.Token directly? - dlgMatch := func(dlg *delegation.Token, cmd command.Command, aud, iss did.DID) bool { - // The Subject of each delegation must equal the invocation's Audience field. - 4f - if dlg.Subject() != aud { + dlgMatch := func(dlg *delegation.Token, issuer did.DID, cmd command.Command, subject did.DID) bool { + // The Subject of each delegation must equal the invocation's Subject (or Audience if defined). - 4f + if dlg.Subject() != subject { return false } // The first proof must be issued to the Invoker (audience DID). - 4c // The Issuer of each delegation must be the Audience in the next one. - 4d - if dlg.Audience() != iss { + if dlg.Audience() != issuer { return false } // The command of each delegation must "allow" the one before it. - 4g @@ -43,7 +44,7 @@ func FindProof(dlgs func() iter.Seq[*delegation.Bundle], cmd command.Command, is var candidateLeaf []*delegation.Bundle for bundle := range dlgs() { - if dlgMatch(bundle.Decoded, cmd, iss, aud) { + if dlgMatch(bundle.Decoded, issuer, cmd, subject) { continue } candidateLeaf = append(candidateLeaf, bundle) @@ -79,7 +80,7 @@ func FindProof(dlgs func() iter.Seq[*delegation.Bundle], cmd command.Command, is // find parent delegation for our current delegation for candidate := range dlgs() { - if !dlgMatch(candidate.Decoded, at.Decoded.Command(), aud, at.Decoded.Issuer()) { + if !dlgMatch(candidate.Decoded, at.Decoded.Issuer(), at.Decoded.Command(), subject) { continue } diff --git a/toolkit/client/proof_test.go b/toolkit/client/proof_test.go index cce0d55..5f0738c 100644 --- a/toolkit/client/proof_test.go +++ b/toolkit/client/proof_test.go @@ -18,16 +18,16 @@ func TestFindProof(t *testing.T) { } require.Equal(t, delegationtest.ProofAliceBob, - FindProof(dlgs, delegationtest.NominalCommand, didtest.PersonaBob.DID(), didtest.PersonaAlice.DID())) + FindProof(dlgs, didtest.PersonaBob.DID(), delegationtest.NominalCommand, didtest.PersonaAlice.DID())) require.Equal(t, delegationtest.ProofAliceBobCarol, - FindProof(dlgs, delegationtest.NominalCommand, didtest.PersonaCarol.DID(), didtest.PersonaAlice.DID())) + FindProof(dlgs, didtest.PersonaCarol.DID(), delegationtest.NominalCommand, didtest.PersonaAlice.DID())) require.Equal(t, delegationtest.ProofAliceBobCarolDan, - FindProof(dlgs, delegationtest.NominalCommand, didtest.PersonaDan.DID(), didtest.PersonaAlice.DID())) + FindProof(dlgs, didtest.PersonaDan.DID(), delegationtest.NominalCommand, didtest.PersonaAlice.DID())) require.Equal(t, delegationtest.ProofAliceBobCarolDanErin, - FindProof(dlgs, delegationtest.NominalCommand, didtest.PersonaErin.DID(), didtest.PersonaAlice.DID())) + FindProof(dlgs, didtest.PersonaErin.DID(), delegationtest.NominalCommand, didtest.PersonaAlice.DID())) require.Equal(t, delegationtest.ProofAliceBobCarolDanErinFrank, - FindProof(dlgs, delegationtest.NominalCommand, didtest.PersonaFrank.DID(), didtest.PersonaAlice.DID())) + FindProof(dlgs, didtest.PersonaFrank.DID(), delegationtest.NominalCommand, didtest.PersonaAlice.DID())) // wrong command - require.Empty(t, FindProof(dlgs, command.New("foo"), didtest.PersonaBob.DID(), didtest.PersonaAlice.DID())) + require.Empty(t, FindProof(dlgs, didtest.PersonaBob.DID(), command.New("foo"), didtest.PersonaAlice.DID())) } diff --git a/toolkit/client/requester.go b/toolkit/client/requester.go index cf28bc4..8420a49 100644 --- a/toolkit/client/requester.go +++ b/toolkit/client/requester.go @@ -3,7 +3,9 @@ package client import ( "context" "iter" + "time" + "github.com/avast/retry-go/v4" "github.com/ucan-wg/go-ucan/did" "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/token/delegation" @@ -12,8 +14,37 @@ import ( type DelegationRequester interface { // RequestDelegation retrieve a delegation or chain of delegation for the given parameters. // - cmd: the command to execute - // - issuer: the DID of the client, also the issuer of the invocation token - // - audience: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token + // - audience: the DID of the client, also the issuer of the invocation token + // - subject: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token + // Note: you can read it as "(audience) wants to do (cmd) on (subject)". // Note: the returned delegation(s) don't have to match exactly the parameters, as long as they allow them. - RequestDelegation(ctx context.Context, cmd command.Command, audience did.DID, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) + RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) +} + +var _ DelegationRequester = &withRetry{} + +type withRetry struct { + requester DelegationRequester + initialDelay time.Duration + maxAttempts uint +} + +// RequesterWithRetry wraps a DelegationRequester to perform exponential backoff, +// with an initial delay and a maximum attempt count. +func RequesterWithRetry(requester DelegationRequester, initialDelay time.Duration, maxAttempt uint) DelegationRequester { + return &withRetry{ + requester: requester, + initialDelay: initialDelay, + maxAttempts: maxAttempt, + } +} + +func (w withRetry) RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) { + return retry.DoWithData(func() (iter.Seq2[*delegation.Bundle, error], error) { + return w.requester.RequestDelegation(ctx, audience, cmd, subject) + }, + retry.Context(ctx), + retry.Delay(w.initialDelay), + retry.Attempts(w.maxAttempts), + ) }