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 0000000..91851be Binary files /dev/null and b/toolkit/_example/scenario1.png differ diff --git a/toolkit/_example/scenario2.png b/toolkit/_example/scenario2.png new file mode 100644 index 0000000..92a49e9 Binary files /dev/null and b/toolkit/_example/scenario2.png differ 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 }