reorg packages, remove outdated server example
This commit is contained in:
committed by
Michael Muré
parent
4f4331b677
commit
4167bf44bd
16
toolkit/server/exectx/ctxvalue.go
Normal file
16
toolkit/server/exectx/ctxvalue.go
Normal 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
|
||||
}
|
||||
146
toolkit/server/exectx/ucanctx.go
Normal file
146
toolkit/server/exectx/ucanctx.go
Normal 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
|
||||
})
|
||||
}
|
||||
162
toolkit/server/exectx/ucanctx_test.go
Normal file
162
toolkit/server/exectx/ucanctx_test.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user