179 lines
5.9 KiB
Go
179 lines
5.9 KiB
Go
package exectx
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"slices"
|
|
|
|
"github.com/ipfs/go-cid"
|
|
"github.com/ipld/go-ipld-prime/datamodel"
|
|
|
|
"code.sonr.org/go/ucan/pkg/args"
|
|
"code.sonr.org/go/ucan/pkg/command"
|
|
"code.sonr.org/go/ucan/pkg/container"
|
|
"code.sonr.org/go/ucan/pkg/meta"
|
|
"code.sonr.org/go/ucan/pkg/policy"
|
|
"code.sonr.org/go/ucan/token/delegation"
|
|
"code.sonr.org/go/ucan/token/invocation"
|
|
"code.sonr.org/go/ucan/toolkit/server/extargs"
|
|
)
|
|
|
|
var _ delegation.Loader = &UcanCtx{}
|
|
|
|
// UcanCtx is a UCAN execution context meant to be inserted in the go context while handling a request.
|
|
// It allows to handle the control of the execution in multiple steps across different middlewares,
|
|
// as well as doing "bearer" types of controls, when arguments are derived from the request itself (HTTP, JsonRpc).
|
|
type UcanCtx struct {
|
|
inv *invocation.Token
|
|
dlgs map[cid.Cid]*delegation.Token
|
|
|
|
policies policy.Policy // all policies combined
|
|
meta *meta.Meta // all meta combined, with no overwriting
|
|
|
|
// argument sources
|
|
http *extargs.HttpExtArgs
|
|
custom map[string]*extargs.CustomExtArgs
|
|
}
|
|
|
|
// FromContainer prepare a UcanCtx from a UCAN container, for further evaluation in a server pipeline.
|
|
// It is expected that the container holds a single invocation and the matching delegations. If not,
|
|
// an error is returned.
|
|
func FromContainer(cont container.Reader) (*UcanCtx, error) {
|
|
inv, err := cont.GetInvocation()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("no invocation: %w", err)
|
|
}
|
|
|
|
ctx := &UcanCtx{
|
|
inv: inv,
|
|
dlgs: make(map[cid.Cid]*delegation.Token, len(cont)-1),
|
|
meta: meta.NewMeta(),
|
|
}
|
|
|
|
// iterate in reverse, from the root delegation to the leaf
|
|
for _, c := range slices.Backward(inv.Proof()) {
|
|
// make sure we have the delegation
|
|
dlg, err := cont.GetDelegation(c)
|
|
if errors.Is(err, delegation.ErrDelegationNotFound) {
|
|
return nil, fmt.Errorf("delegation proof %s is missing", c)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ctx.dlgs[c] = dlg
|
|
// accumulate the policies
|
|
ctx.policies = append(ctx.policies, dlg.Policy()...)
|
|
// accumulate the meta values, with no overwriting
|
|
ctx.meta.Include(dlg.Meta())
|
|
}
|
|
|
|
// DX: As the invocation is created without the delegation, no check is done that the proof chain (CIDs only)
|
|
// is ordered properly and not broken. We don't check that in the container either as it doesn't make any assumption
|
|
// on what is being carried around. That UcanCtx is the first place where we enforce having a single invocation and
|
|
// only the matching delegation.
|
|
// For sanity, we verify that the proofs are ordered properly. This will be checked later anyway, but it's cheap to
|
|
// verify here and catch an easy mistake.
|
|
chainTo := inv.Issuer()
|
|
for _, c := range inv.Proof() {
|
|
dlg := ctx.dlgs[c]
|
|
if !dlg.Audience().Equal(chainTo) {
|
|
return nil, fmt.Errorf("proof chain is broken or not ordered correctly")
|
|
}
|
|
chainTo = dlg.Issuer()
|
|
}
|
|
|
|
return ctx, nil
|
|
}
|
|
|
|
// Command returns the command triggered by the invocation.
|
|
func (ctn *UcanCtx) Command() command.Command {
|
|
return ctn.inv.Command()
|
|
}
|
|
|
|
// Invocation returns the invocation.Token.
|
|
func (ctn *UcanCtx) Invocation() *invocation.Token {
|
|
return ctn.inv
|
|
}
|
|
|
|
// GetDelegation returns the delegation.Token matching the given CID.
|
|
// If not found, delegation.ErrDelegationNotFound is returned.
|
|
func (ctn *UcanCtx) GetDelegation(cid cid.Cid) (*delegation.Token, error) {
|
|
if dlg, ok := ctn.dlgs[cid]; ok {
|
|
return dlg, nil
|
|
}
|
|
return nil, delegation.ErrDelegationNotFound
|
|
}
|
|
|
|
// GetRootDelegation returns the delegation.Token at the root of the proof chain.
|
|
func (ctn *UcanCtx) GetRootDelegation() *delegation.Token {
|
|
proofs := ctn.inv.Proof()
|
|
c := proofs[len(proofs)-1]
|
|
return ctn.dlgs[c]
|
|
}
|
|
|
|
// Policies return the full set of policy statements to satisfy.
|
|
func (ctn *UcanCtx) Policies() policy.Policy {
|
|
return ctn.policies
|
|
}
|
|
|
|
// Meta returns all the meta values from the delegations.
|
|
// They are accumulated from the root delegation to the leaf delegation, with no overwriting.
|
|
func (ctn *UcanCtx) Meta() meta.ReadOnly {
|
|
return ctn.meta.ReadOnly()
|
|
}
|
|
|
|
// VerifyHttp verify the delegation's policies against arguments constructed from the HTTP request.
|
|
// These arguments will be set in the `.http` argument key, at the root.
|
|
// This function can only be called once per context.
|
|
// After being used, those constructed arguments will be used in ExecutionAllowed as well.
|
|
func (ctn *UcanCtx) VerifyHttp(req *http.Request) error {
|
|
if ctn.http != nil {
|
|
panic("only use once per request context")
|
|
}
|
|
ctn.http = extargs.NewHttpExtArgs(ctn.policies, ctn.inv.Arguments(), req)
|
|
return ctn.http.Verify()
|
|
}
|
|
|
|
// VerifyCustom verify the delegation's policies against arbitrary arguments provider through an IPLD MapAssembler.
|
|
// These arguments will be set under the given argument key, at the root.
|
|
// This function can only be called once per context and key.
|
|
// After being used, those constructed arguments will be used in ExecutionAllowed as well.
|
|
func (ctn *UcanCtx) VerifyCustom(key string, assembler func(ma datamodel.MapAssembler)) error {
|
|
if ctn.custom == nil {
|
|
ctn.custom = make(map[string]*extargs.CustomExtArgs)
|
|
}
|
|
if _, ok := ctn.custom[key]; ok {
|
|
panic("only use once per request context and key")
|
|
}
|
|
ctn.custom[key] = extargs.NewCustomExtArgs(key, ctn.policies, assembler)
|
|
return ctn.custom[key].Verify()
|
|
}
|
|
|
|
// ExecutionAllowed does the final verification of the invocation.
|
|
// If VerifyHttp or VerifyJsonRpc has been used, those arguments are part of the verification.
|
|
func (ctn *UcanCtx) ExecutionAllowed() error {
|
|
return ctn.inv.ExecutionAllowedWithArgsHook(ctn, func(args args.ReadOnly) (*args.Args, error) {
|
|
newArgs := args.WriteableClone()
|
|
|
|
if ctn.http != nil {
|
|
httpArgs, err := ctn.http.Args()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
newArgs.Include(httpArgs)
|
|
}
|
|
if ctn.custom != nil {
|
|
for _, cea := range ctn.custom {
|
|
customArgs, err := cea.Args()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
newArgs.Include(customArgs)
|
|
}
|
|
}
|
|
|
|
return newArgs, nil
|
|
})
|
|
}
|