From c670433335c96f30aed834c2368ceb5d26912321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Thu, 6 Feb 2025 14:19:30 +0100 Subject: [PATCH] example: add sub-delegation --- toolkit/_example/Readme.md | 9 ++ .../_protocol-issuer/request-resolver.go | 41 +++++ .../_example/_protocol-issuer/requester.go | 59 +++++++ toolkit/_example/alice-client-issuer/alice.go | 153 ++++++++++++++++++ toolkit/_example/bob-client/bob.go | 111 +++++++++++++ toolkit/_example/client/client.go | 151 ----------------- toolkit/_example/diagram.puml | 57 +++++++ toolkit/_example/scenario1.png | Bin 0 -> 21304 bytes toolkit/_example/scenario2.png | Bin 0 -> 38186 bytes .../{issuer => service-issuer}/issuer.go | 41 ++--- .../{server/server.go => service/service.go} | 11 +- toolkit/_example/shared_values.go | 19 ++- .../dlg_issuer.go => client/clientissuer.go} | 54 ++----- 13 files changed, 478 insertions(+), 228 deletions(-) create mode 100644 toolkit/_example/Readme.md create mode 100644 toolkit/_example/_protocol-issuer/request-resolver.go create mode 100644 toolkit/_example/_protocol-issuer/requester.go create mode 100644 toolkit/_example/alice-client-issuer/alice.go create mode 100644 toolkit/_example/bob-client/bob.go delete mode 100644 toolkit/_example/client/client.go create mode 100644 toolkit/_example/diagram.puml create mode 100644 toolkit/_example/scenario1.png create mode 100644 toolkit/_example/scenario2.png rename toolkit/_example/{issuer => service-issuer}/issuer.go (70%) rename toolkit/_example/{server/server.go => service/service.go} (82%) rename toolkit/{issuer/dlg_issuer.go => client/clientissuer.go} (61%) diff --git a/toolkit/_example/Readme.md b/toolkit/_example/Readme.md new file mode 100644 index 0000000..ce99267 --- /dev/null +++ b/toolkit/_example/Readme.md @@ -0,0 +1,9 @@ +![scenario 1](scenario1.png) +![scenario 2](scenario2.png) + +TODO + +- differences with a real system + - issuer protocol + token exchange + - opinionated with HTTP +- toolkit is helpers, you can change or write your own thing \ No newline at end of file diff --git a/toolkit/_example/_protocol-issuer/request-resolver.go b/toolkit/_example/_protocol-issuer/request-resolver.go new file mode 100644 index 0000000..ff7497a --- /dev/null +++ b/toolkit/_example/_protocol-issuer/request-resolver.go @@ -0,0 +1,41 @@ +package protocol + +import ( + "encoding/json" + "net/http" + + "github.com/ucan-wg/go-ucan/did" + "github.com/ucan-wg/go-ucan/pkg/command" + + "github.com/INFURA/go-ucan-toolkit/issuer" +) + +func RequestResolver(r *http.Request) (*issuer.ResolvedRequest, error) { + // Let's make up a simple json protocol + req := struct { + Audience string `json:"aud"` + Cmd string `json:"cmd"` + Subject string `json:"sub"` + }{} + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + return nil, err + } + aud, err := did.Parse(req.Audience) + if err != nil { + return nil, err + } + cmd, err := command.Parse(req.Cmd) + if err != nil { + return nil, err + } + sub, err := did.Parse(req.Subject) + if err != nil { + return nil, err + } + return &issuer.ResolvedRequest{ + Audience: aud, + Cmd: cmd, + Subject: sub, + }, nil +} diff --git a/toolkit/_example/_protocol-issuer/requester.go b/toolkit/_example/_protocol-issuer/requester.go new file mode 100644 index 0000000..fa98c79 --- /dev/null +++ b/toolkit/_example/_protocol-issuer/requester.go @@ -0,0 +1,59 @@ +package protocol + +import ( + "bytes" + "context" + "encoding/json" + "iter" + "log" + "net/http" + + "github.com/ucan-wg/go-ucan/did" + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/token/delegation" + + "github.com/INFURA/go-ucan-toolkit/client" + "github.com/INFURA/go-ucan-toolkit/issuer" +) + +var _ client.DelegationRequester = &Requester{} + +type Requester struct { + issuerURL string +} + +func NewRequester(issuerURL string) *Requester { + return &Requester{issuerURL: issuerURL} +} + +func (r Requester) RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) { + log.Printf("requesting delegation for %s on %s", cmd, subject) + + // we match the simple json protocol of the issuer + data := struct { + Audience string `json:"aud"` + Cmd string `json:"cmd"` + Subject string `json:"sub"` + }{ + Audience: audience.String(), + Cmd: cmd.String(), + Subject: subject.String(), + } + buf := &bytes.Buffer{} + err := json.NewEncoder(buf).Encode(data) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, "http://"+r.issuerURL, buf) + if err != nil { + return nil, err + } + + res, err := http.DefaultClient.Do(req.WithContext(ctx)) + if err != nil { + return nil, err + } + + return issuer.DecodeResponse(res) +} diff --git a/toolkit/_example/alice-client-issuer/alice.go b/toolkit/_example/alice-client-issuer/alice.go new file mode 100644 index 0000000..85e2190 --- /dev/null +++ b/toolkit/_example/alice-client-issuer/alice.go @@ -0,0 +1,153 @@ +package main + +import ( + "context" + "errors" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "os/signal" + "syscall" + "time" + + "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/pkg/container" + "github.com/ucan-wg/go-ucan/pkg/policy" + "github.com/ucan-wg/go-ucan/pkg/policy/literal" + "github.com/ucan-wg/go-ucan/token/delegation" + + example "github.com/INFURA/go-ucan-toolkit/_example" + protocol "github.com/INFURA/go-ucan-toolkit/_example/_protocol-issuer" + "github.com/INFURA/go-ucan-toolkit/client" + "github.com/INFURA/go-ucan-toolkit/issuer" + "github.com/INFURA/go-ucan-toolkit/server/bearer" +) + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + + // register as handler of the interrupt signal to trigger the teardown + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) + + go func() { + <-quit + cancel() + }() + + err := run(ctx, + example.AliceIssuerUrl, example.AlicePrivKey, example.AliceDid, + example.ServiceIssuerUrl, example.ServiceUrl, example.ServiceDid) + if err != nil { + log.Println(err) + os.Exit(1) + } +} + +func run(ctx context.Context, ownIssuerUrl string, priv crypto.PrivKey, d did.DID, + serviceIssuerUrl string, serviceUrl string, serviceDid did.DID) error { + log.Printf("Alice DID is %s", d.String()) + + issuingLogic := func(iss did.DID, aud did.DID, cmd command.Command, subject did.DID) (*delegation.Token, error) { + log.Printf("issuing delegation to %v for %v to operate on %v", aud, cmd, subject) + + // As another example, we'll force Bob to use a specific HTTP sub-path + policies, err := policy.Construct( + policy.Equal(".http.path", literal.String(fmt.Sprintf("/%s/%s", iss.String(), aud.String()))), + ) + if err != nil { + return nil, err + } + + return delegation.New(iss, aud, cmd, policies, subject) + } + + cli, err := client.NewWithIssuer(priv, protocol.NewRequester(serviceIssuerUrl), issuingLogic) + if err != nil { + return err + } + + go startIssuerHttp(ctx, ownIssuerUrl, cli) + + for { + proofs, err := cli.PrepareInvoke(ctx, command.MustParse("/foo/bar"), serviceDid) + if err != nil { + return err + } + + err = makeRequest(ctx, d, serviceUrl, proofs) + if err != nil { + log.Println(err) + } + + select { + case <-ctx.Done(): + return nil + case <-time.After(20 * time.Second): + } + } +} + +func startIssuerHttp(ctx context.Context, issuerUrl string, cli *client.WithIssuer) { + handler := issuer.HttpWrapper(cli, protocol.RequestResolver) + + srv := &http.Server{ + Addr: issuerUrl, + Handler: handler, + } + + go func() { + if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("listen: %s\n", err) + } + }() + + log.Printf("issuer listening on %s\n", srv.Addr) + + <-ctx.Done() + + if err := srv.Shutdown(ctx); err != nil && !errors.Is(err, context.Canceled) { + log.Printf("issuer error: %v\n", err) + } +} + +func makeRequest(ctx context.Context, clientDid did.DID, serviceUrl string, proofs container.Writer) error { + // we construct a URL that include the client DID as path, as requested by the UCAN policy we get issued + u, err := url.JoinPath("http://"+serviceUrl, clientDid.String()) + if err != nil { + return err + } + + req, err := http.NewRequest(http.MethodGet, u, nil) + if err != nil { + return err + } + + err = bearer.AddBearerContainerCompressed(req.Header, proofs) + if err != nil { + return err + } + + res, err := http.DefaultClient.Do(req.WithContext(ctx)) + if err != nil { + return err + } + + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + body, err := io.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("unexpected status code: %d, error reading body: %w", res.StatusCode, err) + } + return fmt.Errorf("unexpected status code: %d, body: %v", res.StatusCode, string(body)) + } + + log.Printf("response status code: %d", res.StatusCode) + + return nil +} diff --git a/toolkit/_example/bob-client/bob.go b/toolkit/_example/bob-client/bob.go new file mode 100644 index 0000000..9b7ae0f --- /dev/null +++ b/toolkit/_example/bob-client/bob.go @@ -0,0 +1,111 @@ +package main + +import ( + "context" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "os/signal" + "syscall" + "time" + + "github.com/ucan-wg/go-ucan/did" + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/pkg/container" + + example "github.com/INFURA/go-ucan-toolkit/_example" + protocol "github.com/INFURA/go-ucan-toolkit/_example/_protocol-issuer" + "github.com/INFURA/go-ucan-toolkit/client" + "github.com/INFURA/go-ucan-toolkit/server/bearer" +) + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + + // register as handler of the interrupt signal to trigger the teardown + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) + + go func() { + <-quit + cancel() + }() + + err := run(ctx, example.AliceIssuerUrl, example.AliceDid, example.ServiceUrl, example.ServiceDid) + if err != nil { + log.Println(err) + os.Exit(1) + } +} + +func run(ctx context.Context, aliceUrl string, aliceDid did.DID, serverUrl string, serviceDid did.DID) error { + // Let's generate a keypair for our client: + priv, d, err := did.GenerateEd25519() + if err != nil { + return err + } + + log.Printf("Bob DID is %s", d.String()) + + cli, err := client.NewClient(priv, protocol.NewRequester(aliceUrl)) + if err != nil { + return err + } + + for { + proofs, err := cli.PrepareInvoke(ctx, command.MustParse("/foo/bar"), serviceDid) + if err != nil { + return err + } + + err = makeRequest(ctx, d, serverUrl, aliceDid, proofs) + if err != nil { + log.Println(err) + } + + select { + case <-ctx.Done(): + return nil + case <-time.After(5 * time.Second): + } + } +} + +func makeRequest(ctx context.Context, clientDid did.DID, serviceUrl string, aliceDid did.DID, proofs container.Writer) error { + // we construct a URL that include the our DID and Alice DID as path, as requested by the UCAN policy we get issued + u, err := url.JoinPath("http://"+serviceUrl, aliceDid.String(), clientDid.String()) + if err != nil { + return err + } + + req, err := http.NewRequest(http.MethodGet, u, nil) + if err != nil { + return err + } + + err = bearer.AddBearerContainerCompressed(req.Header, proofs) + if err != nil { + return err + } + + res, err := http.DefaultClient.Do(req.WithContext(ctx)) + if err != nil { + return err + } + + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + body, err := io.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("unexpected status code: %d, error reading body: %w", res.StatusCode, err) + } + return fmt.Errorf("unexpected status code: %d, body: %v", res.StatusCode, string(body)) + } + + log.Printf("response status code: %d", res.StatusCode) + + return nil +} diff --git a/toolkit/_example/client/client.go b/toolkit/_example/client/client.go deleted file mode 100644 index 7db6c7d..0000000 --- a/toolkit/_example/client/client.go +++ /dev/null @@ -1,151 +0,0 @@ -package main - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "iter" - "log" - "net/http" - "net/url" - "os" - "os/signal" - "syscall" - "time" - - "github.com/ucan-wg/go-ucan/did" - "github.com/ucan-wg/go-ucan/pkg/command" - "github.com/ucan-wg/go-ucan/pkg/container" - "github.com/ucan-wg/go-ucan/token/delegation" - - example "github.com/INFURA/go-ucan-toolkit/_example" - "github.com/INFURA/go-ucan-toolkit/client" - "github.com/INFURA/go-ucan-toolkit/issuer" - "github.com/INFURA/go-ucan-toolkit/server/bearer" -) - -func main() { - ctx, cancel := context.WithCancel(context.Background()) - - // register as handler of the interrupt signal to trigger the teardown - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) - - go func() { - <-quit - cancel() - }() - - err := run(ctx, example.IssuerUrl, example.ServerUrl, example.ServiceDid) - if err != nil { - log.Println(err) - os.Exit(1) - } -} - -var _ client.DelegationRequester = &requester{} - -type requester struct { - issuerURL string -} - -func (r requester) RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) { - log.Printf("requesting delegation for %s on %s", cmd, subject) - - // we match the simple json protocol of the issuer - data := struct { - Audience string `json:"aud"` - Cmd string `json:"cmd"` - Subject string `json:"sub"` - }{ - Audience: audience.String(), - Cmd: cmd.String(), - Subject: subject.String(), - } - buf := &bytes.Buffer{} - err := json.NewEncoder(buf).Encode(data) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, "http://"+r.issuerURL, buf) - if err != nil { - return nil, err - } - - res, err := http.DefaultClient.Do(req.WithContext(ctx)) - if err != nil { - return nil, err - } - - return issuer.DecodeResponse(res) -} - -func run(ctx context.Context, issuerUrl string, serverUrl string, serviceDid did.DID) error { - // Let's generate a keypair for our client: - priv, d, err := did.GenerateEd25519() - if err != nil { - return err - } - - log.Printf("client DID is %s", d.String()) - - cli, err := client.NewClient(priv, requester{issuerURL: issuerUrl}) - if err != nil { - return err - } - - for { - select { - case <-ctx.Done(): - case <-time.After(5 * time.Second): - proofs, err := cli.PrepareInvoke(ctx, command.MustParse("/foo/bar"), serviceDid) - if err != nil { - return err - } - - err = makeRequest(ctx, d, serverUrl, proofs) - if err != nil { - log.Println(err) - } - } - } -} - -func makeRequest(ctx context.Context, clientDid did.DID, serverUrl string, proofs container.Writer) error { - // we construct a URL that include the client DID as path, as requested by the UCAN policy we get issued - u, err := url.JoinPath("http://"+serverUrl, clientDid.String()) - if err != nil { - return err - } - - req, err := http.NewRequest(http.MethodGet, u, nil) - if err != nil { - return err - } - - err = bearer.AddBearerContainerCompressed(req.Header, proofs) - if err != nil { - return err - } - - res, err := http.DefaultClient.Do(req.WithContext(ctx)) - if err != nil { - return err - } - - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - body, err := io.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("unexpected status code: %d, error reading body: %w", res.StatusCode, err) - } - return fmt.Errorf("unexpected status code: %d, body: %v", res.StatusCode, string(body)) - } - - log.Printf("response status code: %d", res.StatusCode) - - return nil -} diff --git a/toolkit/_example/diagram.puml b/toolkit/_example/diagram.puml new file mode 100644 index 0000000..9891a39 --- /dev/null +++ b/toolkit/_example/diagram.puml @@ -0,0 +1,57 @@ +@startuml +left to right direction + +rectangle Service as owner { + rectangle Issuer as issuer + rectangle Executor as exec +} + +node resource as res + +owner --> res : Controls +exec --> res : Allow access to + +rectangle "Alice" as alice { + rectangle Client as aliceclient + +} + +aliceclient --> issuer : [1] request delegation +aliceclient <-- issuer : [2] issue delegation A + +aliceclient --> exec : [3] make request with A +@enduml + + +@startuml +left to right direction + +rectangle Service as owner { + rectangle Issuer as issuer + rectangle Executor as exec +} + +node resource as res + +owner --> res : Controls +exec --> res : Allow access to + +rectangle "Alice" as alice { + rectangle Client as aliceclient + rectangle Issuer as aliceissuer +} + +aliceclient --> issuer : [1] request delegation +aliceclient <-- issuer : [2] issue delegation A + +aliceclient --> exec : [3] make request with A + +rectangle "Bob" as bob { + rectangle Client as bobclient +} + +bobclient --> aliceissuer : [4] request delegation +bobclient <-- aliceissuer : [5] issue delegation B\nalso returns A + +bobclient -down-> exec : [6] make request with A+B +@enduml \ No newline at end of file diff --git a/toolkit/_example/scenario1.png b/toolkit/_example/scenario1.png new file mode 100644 index 0000000000000000000000000000000000000000..91851be6939065f753e2c995c0e718b99e5bf33b GIT binary patch literal 21304 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 literal 0 HcmV?d00001 diff --git a/toolkit/_example/issuer/issuer.go b/toolkit/_example/service-issuer/issuer.go similarity index 70% rename from toolkit/_example/issuer/issuer.go rename to toolkit/_example/service-issuer/issuer.go index 2cc4890..cac7894 100644 --- a/toolkit/_example/issuer/issuer.go +++ b/toolkit/_example/service-issuer/issuer.go @@ -2,7 +2,6 @@ package main import ( "context" - "encoding/json" "errors" "fmt" "log" @@ -20,6 +19,7 @@ import ( "github.com/ucan-wg/go-ucan/token/delegation" example "github.com/INFURA/go-ucan-toolkit/_example" + protocol "github.com/INFURA/go-ucan-toolkit/_example/_protocol-issuer" "github.com/INFURA/go-ucan-toolkit/issuer" ) @@ -35,7 +35,7 @@ func main() { cancel() }() - err := run(ctx, example.IssuerUrl, example.ServicePrivKey) + err := run(ctx, example.ServiceIssuerUrl, example.ServicePrivKey) if err != nil { log.Println(err) os.Exit(1) @@ -50,7 +50,12 @@ func run(ctx context.Context, issuerUrl string, servicePrivKey crypto.PrivKey) e // Here, we enforce that the caller uses its own DID endpoint (an arbitrary construct for this example). // You will notice that the server doesn't need to know about this logic to enforce it. policies, err := policy.Construct( - policy.Equal(".http.path", literal.String(fmt.Sprintf("/%s", aud.String()))), + policy.Or( + // allow exact path + policy.Equal(".http.path", literal.String(fmt.Sprintf("/%s", aud.String()))), + // allow sub-path + policy.Like(".http.path", fmt.Sprintf("/%s/*", aud.String())), + ), ) if err != nil { return nil, err @@ -67,35 +72,7 @@ func run(ctx context.Context, issuerUrl string, servicePrivKey crypto.PrivKey) e return err } - handler := issuer.HttpWrapper(rootIssuer, func(r *http.Request) (*issuer.ResolvedRequest, error) { - // Let's make up a simple json protocol - req := struct { - Audience string `json:"aud"` - Cmd string `json:"cmd"` - Subject string `json:"sub"` - }{} - err := json.NewDecoder(r.Body).Decode(&req) - if err != nil { - return nil, err - } - aud, err := did.Parse(req.Audience) - if err != nil { - return nil, err - } - cmd, err := command.Parse(req.Cmd) - if err != nil { - return nil, err - } - sub, err := did.Parse(req.Subject) - if err != nil { - return nil, err - } - return &issuer.ResolvedRequest{ - Audience: aud, - Cmd: cmd, - Subject: sub, - }, nil - }) + handler := issuer.HttpWrapper(rootIssuer, protocol.RequestResolver) srv := &http.Server{ Addr: issuerUrl, diff --git a/toolkit/_example/server/server.go b/toolkit/_example/service/service.go similarity index 82% rename from toolkit/_example/server/server.go rename to toolkit/_example/service/service.go index 726042b..f09d532 100644 --- a/toolkit/_example/server/server.go +++ b/toolkit/_example/service/service.go @@ -27,14 +27,16 @@ func main() { cancel() }() - err := run(ctx, example.ServerUrl, example.ServiceDid) + err := run(ctx, example.ServiceUrl, example.ServiceDid) if err != nil { log.Println(err) os.Exit(1) } } -func run(ctx context.Context, serverUrl string, serviceDID did.DID) error { +func run(ctx context.Context, serviceUrl string, serviceDID did.DID) error { + log.Printf("service DID is %s\n", serviceDID.String()) + // we'll make a simple handling pipeline: // - exectx.ExtractMW to extract and decode the UCAN context, verify the service DID // - exectx.HttpExtArgsVerify to verify the HTTP policies @@ -50,7 +52,8 @@ func run(ctx context.Context, serverUrl string, serviceDID did.DID) error { switch ucanCtx.Command().String() { case "/foo/bar": - log.Printf("handled command %v for %v", ucanCtx.Command(), ucanCtx.Invocation().Issuer()) + log.Printf("handled command %v at %v for %v", ucanCtx.Command(), r.URL.Path, ucanCtx.Invocation().Issuer()) + log.Printf("proof is %v", ucanCtx.Invocation().Proof()) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("OK")) default: @@ -64,7 +67,7 @@ func run(ctx context.Context, serverUrl string, serviceDID did.DID) error { handler = exectx.ExtractMW(handler, serviceDID) srv := &http.Server{ - Addr: serverUrl, + Addr: serviceUrl, Handler: handler, } diff --git a/toolkit/_example/shared_values.go b/toolkit/_example/shared_values.go index d71f24d..befa737 100644 --- a/toolkit/_example/shared_values.go +++ b/toolkit/_example/shared_values.go @@ -9,16 +9,27 @@ import ( // Endpoints -var ServerUrl = ":8080" -var IssuerUrl = ":8081" +var ServiceUrl = ":8080" +var ServiceIssuerUrl = ":8081" + +var AliceIssuerUrl = ":8082" // Service var ServicePrivKey crypto.PrivKey var ServiceDid did.DID +// Alice + +var AlicePrivKey crypto.PrivKey +var AliceDid did.DID + func init() { - privRaw, _ := base64.StdEncoding.DecodeString("CAESQGs7hPBRBmxH1UmHrdcPrBkecuFUuCWHK0kMJvZYCBqIa35SGxUdXVGuigQDkMpf7xO4C2C2Acl8QTtSrYS7Cnc=") - ServicePrivKey, _ = crypto.UnmarshalPrivateKey(privRaw) + servPrivRaw, _ := base64.StdEncoding.DecodeString("CAESQGs7hPBRBmxH1UmHrdcPrBkecuFUuCWHK0kMJvZYCBqIa35SGxUdXVGuigQDkMpf7xO4C2C2Acl8QTtSrYS7Cnc=") + ServicePrivKey, _ = crypto.UnmarshalPrivateKey(servPrivRaw) ServiceDid, _ = did.FromPrivKey(ServicePrivKey) + + alicePrivRaw, _ := base64.StdEncoding.DecodeString("CAESQFESA31nDYUhXXwbCNSFvg7M+TOFgyxy0tVX6o+TkJAKqAwDvtGxZeGyUjibGd/op+xOLvzE6BrTIOw62K3yLp8=") + AlicePrivKey, _ = crypto.UnmarshalPrivateKey(alicePrivRaw) + AliceDid, _ = did.FromPrivKey(AlicePrivKey) } diff --git a/toolkit/issuer/dlg_issuer.go b/toolkit/client/clientissuer.go similarity index 61% rename from toolkit/issuer/dlg_issuer.go rename to toolkit/client/clientissuer.go index 00ac364..57afc75 100644 --- a/toolkit/issuer/dlg_issuer.go +++ b/toolkit/client/clientissuer.go @@ -1,18 +1,15 @@ -package issuer +package client import ( "context" "fmt" "iter" - "time" "github.com/ipfs/go-cid" "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/delegation" - - "github.com/INFURA/go-ucan-toolkit/client" ) // DlgIssuingLogic is a function that decides what powers are given to a client. @@ -25,32 +22,19 @@ import ( // expected action. If you don't want to give that power, return an error instead. type DlgIssuingLogic func(iss did.DID, aud did.DID, cmd command.Command, subject did.DID) (*delegation.Token, error) -var _ client.DelegationRequester = &Issuer{} +var _ DelegationRequester = &WithIssuer{} -// Issuer is an implementation of a re-delegating issuer. -// Note: Your actual needs for an issuer can easily be different (caching...) than the choices made here. -// Feel free to replace this component with your own flavor. -type Issuer struct { - did did.DID - privKey crypto.PrivKey - - pool *client.Pool - requester client.DelegationRequester - logic DlgIssuingLogic +type WithIssuer struct { + *Client + logic DlgIssuingLogic } -func NewIssuer(privKey crypto.PrivKey, requester client.DelegationRequester, logic DlgIssuingLogic) (*Issuer, error) { - d, err := did.FromPrivKey(privKey) +func NewWithIssuer(privKey crypto.PrivKey, requester DelegationRequester, logic DlgIssuingLogic) (*WithIssuer, error) { + client, err := NewClient(privKey, requester) if err != nil { return nil, err } - return &Issuer{ - did: d, - privKey: privKey, - pool: client.NewPool(), - requester: client.RequesterWithRetry(requester, time.Second, 3), - logic: logic, - }, nil + return &WithIssuer{Client: client, logic: logic}, nil } // RequestDelegation retrieve chain of delegation for the given parameters. @@ -59,22 +43,18 @@ func NewIssuer(privKey crypto.PrivKey, requester client.DelegationRequester, log // - 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) does (cmd) on (subject)". // Note: the returned delegation(s) don't have to match exactly the parameters, as long as they allow them. -func (i *Issuer) RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) { - if subject != i.did { - return nil, fmt.Errorf("subject DID doesn't match the issuer DID") - } - +func (c *WithIssuer) RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) { var proof []cid.Cid // is there already a valid proof chain? - if proof = i.pool.FindProof(audience, cmd, subject); len(proof) > 0 { - return i.pool.GetBundles(proof), nil + if proof = c.pool.FindProof(audience, cmd, subject); len(proof) > 0 { + return c.pool.GetBundles(proof), nil } // do we have the power to delegate this? - if proof = i.pool.FindProof(i.did, cmd, 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 := i.requester.RequestDelegation(ctx, i.did, cmd, subject) + proofBundles, err := c.requester.RequestDelegation(ctx, c.did, cmd, subject) if err != nil { return nil, err } @@ -85,12 +65,12 @@ func (i *Issuer) RequestDelegation(ctx context.Context, audience did.DID, cmd co return nil, err } proof = append(proof, bundle.Cid) - i.pool.AddBundle(bundle) + c.pool.AddBundle(bundle) } } // run the custom logic to get what we actually issue - dlg, err := i.logic(i.did, audience, cmd, subject) + dlg, err := c.logic(c.did, audience, cmd, subject) if err != nil { return nil, err } @@ -99,7 +79,7 @@ func (i *Issuer) RequestDelegation(ctx context.Context, audience did.DID, cmd co } // sign and cache the new token - dlgBytes, dlgCid, err := dlg.ToSealed(i.privKey) + dlgBytes, dlgCid, err := dlg.ToSealed(c.privKey) if err != nil { return nil, err } @@ -114,7 +94,7 @@ func (i *Issuer) RequestDelegation(ctx context.Context, audience did.DID, cmd co if !yield(bundle, nil) { return } - for b, err := range i.pool.GetBundles(proof) { + for b, err := range c.pool.GetBundles(proof) { if !yield(b, err) { return }