diff --git a/toolkit/issuer/issuer.go b/toolkit/issuer/issuer.go new file mode 100644 index 0000000..6403d47 --- /dev/null +++ b/toolkit/issuer/issuer.go @@ -0,0 +1,112 @@ +package issuer + +import ( + "context" + "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" +) + +var _ client.DelegationRequester = &Issuer{} + +type Issuer struct { + did did.DID + privKey crypto.PrivKey + + pool *client.Pool + requester client.DelegationRequester + logic IssuingLogic +} + +func NewIssuer(privKey crypto.PrivKey, requester client.DelegationRequester, logic IssuingLogic) (*Issuer, error) { + d, err := did.FromPrivKey(privKey) + 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 +} + +// 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. +// - 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) 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) { + 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 + } + + // do we have the power to delegate this? + if proof = i.pool.FindProof(i.did, cmd, subject); len(proof) == 0 { + // we need to request a new proof + proofBundles, err := i.requester.RequestDelegation(ctx, i.did, cmd, subject) + if err != nil { + return nil, err + } + + // cache the new proofs + for bundle, err := range proofBundles { + if err != nil { + return nil, err + } + proof = append(proof, bundle.Cid) + i.pool.AddBundle(bundle) + } + } + + // run the custom logic to get what we actually issue + dlg, err := i.logic(i.did, audience, cmd, subject) + if err != nil { + return nil, err + } + + // sign and cache the new token + dlgBytes, dlgCid, err := dlg.ToSealed(i.privKey) + if err != nil { + return nil, err + } + bundle := &delegation.Bundle{ + Cid: dlgCid, + Decoded: dlg, + Sealed: dlgBytes, + } + + // output the relevant delegations + return func(yield func(*delegation.Bundle, error) bool) { + if !yield(bundle, nil) { + return + } + for b, err := range i.pool.GetBundles(proof) { + if !yield(b, err) { + return + } + } + }, nil +}