reorg packages, remove outdated server example

This commit is contained in:
Michael Muré
2024-11-28 14:26:27 +01:00
committed by Michael Muré
parent 4f4331b677
commit 4167bf44bd
11 changed files with 1074 additions and 113 deletions

View File

@@ -0,0 +1,16 @@
package exectx
import "context"
type ctxKey struct{}
// AddUcanCtxToContext insert a UcanCtx into a go context.
func AddUcanCtxToContext(ctx context.Context, ucanCtx *UcanCtx) context.Context {
return context.WithValue(ctx, ctxKey{}, ucanCtx)
}
// FromContext retrieve a UcanCtx from a go context.
func FromContext(ctx context.Context) (*UcanCtx, bool) {
ucanCtx, ok := ctx.Value(ctxKey{}).(*UcanCtx)
return ucanCtx, ok
}

View File

@@ -0,0 +1,146 @@
package exectx
import (
"errors"
"fmt"
"net/http"
"slices"
"github.com/INFURA/go-ethlibs/jsonrpc"
"github.com/ipfs/go-cid"
"github.com/ucan-wg/go-ucan/pkg/args"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/pkg/container"
"github.com/ucan-wg/go-ucan/pkg/meta"
"github.com/ucan-wg/go-ucan/pkg/policy"
"github.com/ucan-wg/go-ucan/token/delegation"
"github.com/ucan-wg/go-ucan/token/invocation"
bearer2 "github.com/INFURA/go-ucan-toolkit/server/bearer"
)
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 *bearer2.HttpBearer
jsonrpc *bearer2.JsonRpcBearer
}
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())
}
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
}
// 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 overwrite.
func (ctn UcanCtx) Meta() meta.ReadOnly {
return ctn.meta.ReadOnly()
}
// VerifyHttp verify the delegation's policies against arguments constructed from the HTTP request.
// 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 = bearer2.NewHttpBearer(ctn.policies, ctn.inv.Arguments(), req)
return ctn.http.Verify()
}
// VerifyJsonRpc verify the delegation's policies against arguments constructed from the JsonRpc request.
// 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) VerifyJsonRpc(req *jsonrpc.Request) error {
if ctn.jsonrpc != nil {
panic("only use once per request context")
}
ctn.jsonrpc = bearer2.NewJsonRpcBearer(ctn.policies, ctn.inv.Arguments(), req)
return ctn.jsonrpc.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.jsonrpc != nil {
jsonRpcArgs, err := ctn.jsonrpc.Args()
if err != nil {
return nil, err
}
newArgs.Include(jsonRpcArgs)
}
return newArgs, nil
})
}

View File

@@ -0,0 +1,162 @@
package exectx_test
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"testing"
"time"
"github.com/INFURA/go-ethlibs/jsonrpc"
"github.com/ipfs/go-cid"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/did/didtest"
"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"
"github.com/ucan-wg/go-ucan/token/invocation"
exectx2 "github.com/INFURA/go-ucan-toolkit/server/exectx"
)
func ExampleContext() {
// let's use some pre-made DID+privkey.
// use go-ucan/did to generate or parse them.
service := didtest.PersonaAlice
user := didtest.PersonaBob
// DELEGATION: the service gives some power to the user, in the form of a root UCAN token.
// The command defines the shape of the arguments on which the policies operate, it is specific to that service.
// Policies define what the user can do.
cmd := command.New("foo")
pol := policy.MustConstruct(
// some basic HTTP constraints
policy.Equal(".http.method", literal.String("GET")),
policy.Like(".http.path", "/foo/*"),
// some JsonRpc constraints
policy.Or(
policy.Like(".jsonrpc.method", "eth_*"),
policy.Equal(".jsonrpc.method", literal.String("debug_traceCall")),
),
)
dlg, _ := delegation.Root(service.DID(), user.DID(), cmd, pol,
delegation.WithExpirationIn(24*time.Hour),
)
dlgBytes, dlgCid, _ := dlg.ToSealed(service.PrivKey())
// INVOCATION: the user leverages the delegation (power) to make a request.
inv, _ := invocation.New(user.DID(), service.DID(), cmd, []cid.Cid{dlgCid},
invocation.WithExpirationIn(10*time.Minute),
invocation.WithArgument("myarg", "hello"), // we can specify invocation parameters
)
invBytes, invCid, _ := inv.ToSealed(user.PrivKey())
// PACKAGING: no obligation for the transport, but the user needs to give the service the invocation
// and all the proof delegations. We can use a container for that.
cont := container.NewWriter()
cont.AddSealed(dlgCid, dlgBytes)
cont.AddSealed(invCid, invBytes)
contBytes, _ := cont.ToCborBase64()
// MAKING A REQUEST: we pass the container in the Bearer HTTP header
jrpc := jsonrpc.NewRequest()
jrpc.Method = "eth_call"
jrpc.Params = jsonrpc.MustParams("0x599784", true)
jrpcBytes, _ := jrpc.MarshalJSON()
req, _ := http.NewRequest(http.MethodGet, "/foo/bar", bytes.NewReader(jrpcBytes))
req.Header.Set("Authorization", "Bearer "+string(contBytes))
// SERVER: Auth middleware
// - decode the container
// - create the context
authMw := func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Note: we obviously want something more robust, this is an example
// Note: if an error occur, we'll want to return an HTTP 401 Unauthorized
data := bytes.TrimPrefix([]byte(r.Header.Get("Authorization")), []byte("Bearer "))
cont, _ := container.FromCborBase64(data)
ucanCtx, _ := exectx2.FromContainer(cont)
// insert into the go context
req = req.WithContext(exectx2.AddUcanCtxToContext(req.Context(), ucanCtx))
next.ServeHTTP(w, req)
})
}
// SERVER: http checks
httpMw := func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ucanCtx, _ := exectx2.FromContext(req.Context())
err := ucanCtx.VerifyHttp(r)
if err != nil {
// This will print something like:
// `the following UCAN policy is not satisfied: ["==", ".http.path", "/foo"]`
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// SERVER: JsonRpc checks
jsonrpcMw := func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ucanCtx, _ := exectx2.FromContext(req.Context())
var jrpc jsonrpc.Request
_ = json.NewDecoder(r.Body).Decode(&jrpc)
err := ucanCtx.VerifyJsonRpc(&jrpc)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// SERVER: final handler
handler := func(w http.ResponseWriter, r *http.Request) {
ucanCtx, _ := exectx2.FromContext(req.Context())
if err := ucanCtx.ExecutionAllowed(); err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
_, _ = fmt.Fprintln(w, "Success!")
}
// Ready to go!
_ = http.ListenAndServe("", authMw(httpMw(jsonrpcMw(http.HandlerFunc(handler)))))
}
func TestGoCtx(t *testing.T) {
ctx := context.Background()
ucanCtx, ok := exectx2.FromContext(ctx)
require.False(t, ok)
require.Nil(t, ucanCtx)
expected := &exectx2.UcanCtx{}
ctx = exectx2.AddUcanCtxToContext(ctx, expected)
got, ok := exectx2.FromContext(ctx)
require.True(t, ok)
require.Equal(t, expected, got)
}