235 lines
6.9 KiB
Go
235 lines
6.9 KiB
Go
package exectx_test
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"code.sonr.org/go/did-it/didtest"
|
|
"github.com/ipfs/go-cid"
|
|
"github.com/ipld/go-ipld-prime/datamodel"
|
|
"github.com/ipld/go-ipld-prime/fluent/qp"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"code.sonr.org/go/ucan/pkg/command"
|
|
"code.sonr.org/go/ucan/pkg/container"
|
|
"code.sonr.org/go/ucan/pkg/policy"
|
|
"code.sonr.org/go/ucan/pkg/policy/literal"
|
|
"code.sonr.org/go/ucan/token/delegation"
|
|
"code.sonr.org/go/ucan/token/invocation"
|
|
"code.sonr.org/go/ucan/toolkit/server/exectx"
|
|
)
|
|
|
|
const (
|
|
network = "eth-mainnet"
|
|
)
|
|
|
|
func TestUcanCtxFullFlow(t *testing.T) {
|
|
// 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 custom constraints
|
|
// Network
|
|
policy.Equal(".custom.ntwk", literal.String(network)),
|
|
// Quota
|
|
policy.LessThanOrEqual(".custom.quota.ur", literal.Int(1234)),
|
|
)
|
|
|
|
dlg, err := delegation.Root(service.DID(), user.DID(), cmd, pol,
|
|
delegation.WithExpirationIn(24*time.Hour),
|
|
)
|
|
require.NoError(t, err)
|
|
dlgBytes, dlgCid, err := dlg.ToSealed(service.PrivKey())
|
|
require.NoError(t, err)
|
|
|
|
// INVOCATION: the user leverages the delegation (power) to make a request.
|
|
|
|
inv, err := invocation.New(user.DID(), cmd, service.DID(), []cid.Cid{dlgCid},
|
|
invocation.WithExpirationIn(10*time.Minute),
|
|
invocation.WithArgument("myarg", "hello"), // we can specify invocation parameters
|
|
)
|
|
require.NoError(t, err)
|
|
invBytes, _, err := inv.ToSealed(user.PrivKey())
|
|
require.NoError(t, err)
|
|
|
|
// 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(dlgBytes)
|
|
cont.AddSealed(invBytes)
|
|
contBytes, err := cont.ToBase64StdPadding()
|
|
require.NoError(t, err)
|
|
|
|
// MAKING A REQUEST: we pass the container in the Bearer HTTP header
|
|
|
|
req, err := http.NewRequest(http.MethodGet, "/foo/bar", nil)
|
|
require.NoError(t, err)
|
|
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 := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
|
|
cont, err := container.FromString(data)
|
|
require.NoError(t, err)
|
|
ucanCtx, err := exectx.FromContainer(cont)
|
|
require.NoError(t, err)
|
|
|
|
// insert into the go context
|
|
r = r.WithContext(exectx.AddUcanCtxToContext(r.Context(), ucanCtx))
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// SERVER: http checks
|
|
|
|
httpMw := func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
ucanCtx, ok := exectx.FromContext(r.Context())
|
|
require.True(t, ok)
|
|
|
|
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: custom args checks
|
|
|
|
customArgsMw := func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
ucanCtx, ok := exectx.FromContext(r.Context())
|
|
require.True(t, ok)
|
|
err := ucanCtx.VerifyCustom("custom", func(ma datamodel.MapAssembler) {
|
|
qp.MapEntry(ma, "ntwk", qp.String(network))
|
|
qp.MapEntry(ma, "quota", qp.Map(1, func(ma datamodel.MapAssembler) {
|
|
qp.MapEntry(ma, "ur", qp.Int(1234))
|
|
}))
|
|
})
|
|
require.NoError(t, err)
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// SERVER: final handler
|
|
|
|
handler := func(w http.ResponseWriter, r *http.Request) {
|
|
ucanCtx, ok := exectx.FromContext(r.Context())
|
|
require.True(t, ok)
|
|
|
|
if err := ucanCtx.ExecutionAllowed(); err != nil {
|
|
http.Error(w, err.Error(), http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
sut := authMw(httpMw(customArgsMw(http.HandlerFunc(handler))))
|
|
|
|
rec := httptest.NewRecorder()
|
|
sut.ServeHTTP(rec, req)
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
}
|
|
|
|
func TestGoCtx(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
ucanCtx, ok := exectx.FromContext(ctx)
|
|
require.False(t, ok)
|
|
require.Nil(t, ucanCtx)
|
|
|
|
expected := &exectx.UcanCtx{}
|
|
|
|
ctx = exectx.AddUcanCtxToContext(ctx, expected)
|
|
|
|
got, ok := exectx.FromContext(ctx)
|
|
require.True(t, ok)
|
|
require.Equal(t, expected, got)
|
|
}
|
|
|
|
func TestUcanCtx(t *testing.T) {
|
|
const service = didtest.PersonaAlice
|
|
const client1 = didtest.PersonaBob
|
|
const client2 = didtest.PersonaCarol
|
|
const cmd = "/foo/bar"
|
|
|
|
cont := container.NewWriter()
|
|
|
|
pol1 := policy.MustConstruct(
|
|
policy.Equal(".http.scheme", literal.String("https")),
|
|
)
|
|
dlg1, err := delegation.Root(service.DID(), client1.DID(), cmd, pol1,
|
|
delegation.WithMeta("foo", "bar"),
|
|
)
|
|
require.NoError(t, err)
|
|
dlg1Byte, dlg1Cid, err := dlg1.ToSealed(service.PrivKey())
|
|
require.NoError(t, err)
|
|
cont.AddSealed(dlg1Byte)
|
|
|
|
pol2 := policy.MustConstruct(
|
|
policy.Equal(".http.method", literal.String("GET")),
|
|
)
|
|
dlg2, err := delegation.New(client1.DID(), client2.DID(), cmd, pol2, service.DID(),
|
|
delegation.WithMeta("foo", "foo"), // attempt to replace
|
|
)
|
|
require.NoError(t, err)
|
|
dlg2Byte, dlg2Cid, err := dlg2.ToSealed(client1.PrivKey())
|
|
require.NoError(t, err)
|
|
cont.AddSealed(dlg2Byte)
|
|
|
|
inv, err := invocation.New(client2.DID(), cmd, service.DID(), []cid.Cid{dlg2Cid, dlg1Cid})
|
|
require.NoError(t, err)
|
|
invBytes, _, err := inv.ToSealed(client2.PrivKey())
|
|
require.NoError(t, err)
|
|
cont.AddSealed(invBytes)
|
|
|
|
ctx, err := exectx.FromContainer(cont.ToReader())
|
|
require.NoError(t, err)
|
|
|
|
require.NotNil(t, ctx.Invocation())
|
|
require.Equal(t, cmd, ctx.Command().String())
|
|
require.Equal(t, 1, ctx.Meta().Len())
|
|
require.Equal(t, "bar", must(ctx.Meta().GetString("foo")))
|
|
require.Equal(t, service.DID(), ctx.GetRootDelegation().Issuer())
|
|
require.Equal(t, append(pol1, pol2...), ctx.Policies())
|
|
|
|
require.ErrorContains(t, ctx.ExecutionAllowed(), `the following UCAN policy is not satisfied: ["==", ".http.method", "GET"]`)
|
|
|
|
r := httptest.NewRequest(http.MethodGet, "https://foo/bar", nil)
|
|
require.NoError(t, ctx.VerifyHttp(r))
|
|
require.NoError(t, ctx.ExecutionAllowed())
|
|
}
|
|
|
|
func must[T any](e T, err error) T {
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return e
|
|
}
|