add server/issuer/client examples, and lots of sanding
This commit is contained in:
committed by
Michael Muré
parent
1098a834fb
commit
55f38fef4a
151
toolkit/_example/client/client.go
Normal file
151
toolkit/_example/client/client.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
119
toolkit/_example/issuer/issuer.go
Normal file
119
toolkit/_example/issuer/issuer.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"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/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"
|
||||||
|
"github.com/INFURA/go-ucan-toolkit/issuer"
|
||||||
|
)
|
||||||
|
|
||||||
|
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.ServicePrivKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(ctx context.Context, issuerUrl string, servicePrivKey crypto.PrivKey) error {
|
||||||
|
issuingLogic := func(iss did.DID, aud did.DID, cmd command.Command) (*delegation.Token, error) {
|
||||||
|
log.Printf("issuing delegation to %v for %v", aud, cmd)
|
||||||
|
|
||||||
|
// We construct an arbitrary policy.
|
||||||
|
// 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()))),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return delegation.Root(iss, aud, cmd, policies,
|
||||||
|
// let's add an expiration, this will force the client to renew its token.
|
||||||
|
delegation.WithExpirationIn(10*time.Second),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
rootIssuer, err := issuer.NewRootIssuer(servicePrivKey, issuingLogic)
|
||||||
|
if err != nil {
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
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("listening on %s\n", srv.Addr)
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
|
||||||
|
if err := srv.Shutdown(ctx); err != nil && !errors.Is(err, context.Canceled) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
85
toolkit/_example/server/server.go
Normal file
85
toolkit/_example/server/server.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/did"
|
||||||
|
|
||||||
|
example "github.com/INFURA/go-ucan-toolkit/_example"
|
||||||
|
"github.com/INFURA/go-ucan-toolkit/server/exectx"
|
||||||
|
)
|
||||||
|
|
||||||
|
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.ServerUrl, example.ServiceDid)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(ctx context.Context, serverUrl string, serviceDID did.DID) error {
|
||||||
|
// 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
|
||||||
|
// - exectx.EnforceMW to perform the final UCAN checks
|
||||||
|
// - our handler to execute the commands
|
||||||
|
|
||||||
|
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ucanCtx, ok := exectx.FromContext(r.Context())
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "no ucan-ctx found", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ucanCtx.Command().String() {
|
||||||
|
case "/foo/bar":
|
||||||
|
log.Printf("handled command %v for %v", ucanCtx.Command(), ucanCtx.Invocation().Issuer())
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte("OK"))
|
||||||
|
default:
|
||||||
|
http.Error(w, "unknown UCAN commmand", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
handler = exectx.EnforceMW(handler)
|
||||||
|
handler = exectx.HttpExtArgsVerify(handler)
|
||||||
|
handler = exectx.ExtractMW(handler, serviceDID)
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: serverUrl,
|
||||||
|
Handler: handler,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
log.Fatalf("listen: %s\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Printf("listening on %s\n", srv.Addr)
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
|
||||||
|
if err := srv.Shutdown(ctx); err != nil && !errors.Is(err, context.Canceled) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
24
toolkit/_example/shared_values.go
Normal file
24
toolkit/_example/shared_values.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package example
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
|
||||||
|
"github.com/libp2p/go-libp2p/core/crypto"
|
||||||
|
"github.com/ucan-wg/go-ucan/did"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Endpoints
|
||||||
|
|
||||||
|
var ServerUrl = ":8080"
|
||||||
|
var IssuerUrl = ":8081"
|
||||||
|
|
||||||
|
// Service
|
||||||
|
|
||||||
|
var ServicePrivKey crypto.PrivKey
|
||||||
|
var ServiceDid did.DID
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
privRaw, _ := base64.StdEncoding.DecodeString("CAESQGs7hPBRBmxH1UmHrdcPrBkecuFUuCWHK0kMJvZYCBqIa35SGxUdXVGuigQDkMpf7xO4C2C2Acl8QTtSrYS7Cnc=")
|
||||||
|
ServicePrivKey, _ = crypto.UnmarshalPrivateKey(privRaw)
|
||||||
|
ServiceDid, _ = did.FromPrivKey(ServicePrivKey)
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package issuer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"iter"
|
"iter"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -14,18 +15,31 @@ import (
|
|||||||
"github.com/INFURA/go-ucan-toolkit/client"
|
"github.com/INFURA/go-ucan-toolkit/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DlgIssuingLogic is a function that decides what powers are given to a client.
|
||||||
|
// - issuer: the DID of our issuer
|
||||||
|
// - audience: the DID of the client, also the issuer 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 "(audience) wants to do (cmd) on (subject)".
|
||||||
|
// Note: you can decide to match the input parameters exactly or issue a broader power, as long as it allows the
|
||||||
|
// 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 _ client.DelegationRequester = &Issuer{}
|
||||||
|
|
||||||
|
// 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 {
|
type Issuer struct {
|
||||||
did did.DID
|
did did.DID
|
||||||
privKey crypto.PrivKey
|
privKey crypto.PrivKey
|
||||||
|
|
||||||
pool *client.Pool
|
pool *client.Pool
|
||||||
requester client.DelegationRequester
|
requester client.DelegationRequester
|
||||||
logic IssuingLogic
|
logic DlgIssuingLogic
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIssuer(privKey crypto.PrivKey, requester client.DelegationRequester, logic IssuingLogic) (*Issuer, error) {
|
func NewIssuer(privKey crypto.PrivKey, requester client.DelegationRequester, logic DlgIssuingLogic) (*Issuer, error) {
|
||||||
d, err := did.FromPrivKey(privKey)
|
d, err := did.FromPrivKey(privKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -39,16 +53,6 @@ func NewIssuer(privKey crypto.PrivKey, requester client.DelegationRequester, log
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// IssuingLogic is a function that decides what powers are given to a client.
|
|
||||||
// - issuer: the DID of our issuer
|
|
||||||
// - audience: the DID of the client, also the issuer 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 "(audience) wants to do (cmd) on (subject)".
|
|
||||||
// Note: you can decide to match the input parameters exactly or issue a broader power, as long as it allows the
|
|
||||||
// expected action. If you don't want to give that power, return an error instead.
|
|
||||||
type IssuingLogic func(iss did.DID, aud did.DID, cmd command.Command, subject did.DID) (*delegation.Token, error)
|
|
||||||
|
|
||||||
// RequestDelegation retrieve chain of delegation for the given parameters.
|
// RequestDelegation retrieve chain of delegation for the given parameters.
|
||||||
// - audience: the DID of the client, also the issuer of the invocation token
|
// - audience: the DID of the client, also the issuer of the invocation token
|
||||||
// - cmd: the command to execute
|
// - cmd: the command to execute
|
||||||
@@ -56,6 +60,10 @@ type IssuingLogic func(iss did.DID, aud did.DID, cmd command.Command, subject di
|
|||||||
// Note: you can read it as "(audience) does (cmd) on (subject)".
|
// 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.
|
// 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) {
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
var proof []cid.Cid
|
var proof []cid.Cid
|
||||||
|
|
||||||
// is there already a valid proof chain?
|
// is there already a valid proof chain?
|
||||||
@@ -86,6 +94,9 @@ func (i *Issuer) RequestDelegation(ctx context.Context, audience did.DID, cmd co
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if dlg.IsRoot() {
|
||||||
|
return nil, fmt.Errorf("issuing logic should return a non-root delegation")
|
||||||
|
}
|
||||||
|
|
||||||
// sign and cache the new token
|
// sign and cache the new token
|
||||||
dlgBytes, dlgCid, err := dlg.ToSealed(i.privKey)
|
dlgBytes, dlgCid, err := dlg.ToSealed(i.privKey)
|
||||||
83
toolkit/issuer/http_wrapper.go
Normal file
83
toolkit/issuer/http_wrapper.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package issuer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"iter"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
|
||||||
|
"github.com/INFURA/go-ucan-toolkit/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RequestResolver func(r *http.Request) (*ResolvedRequest, error)
|
||||||
|
|
||||||
|
type ResolvedRequest struct {
|
||||||
|
Audience did.DID
|
||||||
|
Cmd command.Command
|
||||||
|
Subject did.DID
|
||||||
|
}
|
||||||
|
|
||||||
|
// HttpWrapper implements an HTTP transport for a UCAN issuer.
|
||||||
|
// It provides a common response shape, while allowing customisation of the request.
|
||||||
|
func HttpWrapper(requester client.DelegationRequester, resolver RequestResolver) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved, err := resolver(r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dlgs, err := requester.RequestDelegation(r.Context(), resolved.Audience, resolved.Cmd, resolved.Subject)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cont := container.NewWriter()
|
||||||
|
for bundle, err := range dlgs {
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cont.AddSealed(bundle.Sealed)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cont.ToBytesGzippedWriter(w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeResponse(res *http.Response) (iter.Seq2[*delegation.Bundle, error], error) {
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
msg, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("request failed with status %d, then failed to read response body: %w", res.StatusCode, err)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("request failed with status %d: %s", res.StatusCode, msg)
|
||||||
|
}
|
||||||
|
cont, err := container.FromReader(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(yield func(*delegation.Bundle, error) bool) {
|
||||||
|
for bundle := range cont.GetAllDelegations() {
|
||||||
|
if !yield(&bundle, nil) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
78
toolkit/issuer/root_issuer.go
Normal file
78
toolkit/issuer/root_issuer.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package issuer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"iter"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RootIssuingLogic is a function that decides what powers are given to a client.
|
||||||
|
// - issuer: the DID of our issuer
|
||||||
|
// - audience: the DID of the client, also the issuer of the invocation token
|
||||||
|
// - cmd: the command to execute
|
||||||
|
// Note: you can read it as "(audience) wants to do (cmd) on (subject)".
|
||||||
|
// Note: you can decide to match the input parameters exactly or issue a broader power, as long as it allows the
|
||||||
|
// expected action. If you don't want to give that power, return an error instead.
|
||||||
|
type RootIssuingLogic func(iss did.DID, aud did.DID, cmd command.Command) (*delegation.Token, error)
|
||||||
|
|
||||||
|
var _ client.DelegationRequester = &RootIssuer{}
|
||||||
|
|
||||||
|
// RootIssuer is an implementation of a root delegation 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 RootIssuer struct {
|
||||||
|
did did.DID
|
||||||
|
privKey crypto.PrivKey
|
||||||
|
|
||||||
|
logic RootIssuingLogic
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRootIssuer(privKey crypto.PrivKey, logic RootIssuingLogic) (*RootIssuer, error) {
|
||||||
|
d, err := did.FromPrivKey(privKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &RootIssuer{
|
||||||
|
did: d,
|
||||||
|
privKey: privKey,
|
||||||
|
logic: logic,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RootIssuer) RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) {
|
||||||
|
if subject != r.did {
|
||||||
|
return nil, fmt.Errorf("subject DID doesn't match the issuer DID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// run the custom logic to get what we actually issue
|
||||||
|
dlg, err := r.logic(r.did, audience, cmd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !dlg.IsRoot() {
|
||||||
|
return nil, fmt.Errorf("issuing logic should return a root delegation")
|
||||||
|
}
|
||||||
|
|
||||||
|
// sign and cache the new token
|
||||||
|
dlgBytes, dlgCid, err := dlg.ToSealed(r.privKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bundle := &delegation.Bundle{
|
||||||
|
Cid: dlgCid,
|
||||||
|
Decoded: dlg,
|
||||||
|
Sealed: dlgBytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
// output the root delegation
|
||||||
|
return func(yield func(*delegation.Bundle, error) bool) {
|
||||||
|
yield(bundle, nil)
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user