diff --git a/toolkit/server/exectx/ucanctx.go b/toolkit/server/exectx/ucanctx.go index e2a8999..6f87e55 100644 --- a/toolkit/server/exectx/ucanctx.go +++ b/toolkit/server/exectx/ucanctx.go @@ -8,6 +8,7 @@ import ( "github.com/INFURA/go-ethlibs/jsonrpc" "github.com/ipfs/go-cid" + "github.com/ipld/go-ipld-prime/datamodel" "github.com/ucan-wg/go-ucan/pkg/args" "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/pkg/container" @@ -34,6 +35,7 @@ type UcanCtx struct { // argument sources http *extargs.HttpExtArgs jsonrpc *extargs.JsonRpcExtArgs + infura *extargs.InfuraExtArgs } func FromContainer(cont container.Reader) (*UcanCtx, error) { @@ -99,6 +101,7 @@ func (ctn UcanCtx) Meta() 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 { @@ -110,6 +113,7 @@ func (ctn UcanCtx) VerifyHttp(req *http.Request) error { } // VerifyJsonRpc verify the delegation's policies against arguments constructed from the JsonRpc request. +// These arguments will be set in the `.jsonrpc` 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) VerifyJsonRpc(req *jsonrpc.Request) error { @@ -120,6 +124,18 @@ func (ctn UcanCtx) VerifyJsonRpc(req *jsonrpc.Request) error { return ctn.jsonrpc.Verify() } +// VerifyInfura verify the delegation's policies against arbitrary arguments provider through an IPLD MapAssembler. +// These arguments will be set in the `.inf` 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) VerifyInfura(assembler func(ma datamodel.MapAssembler)) error { + if ctn.infura != nil { + panic("only use once per request context") + } + ctn.infura = extargs.NewInfuraExtArgs(ctn.policies, assembler) + return ctn.infura.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 { @@ -140,6 +156,13 @@ func (ctn UcanCtx) ExecutionAllowed() error { } newArgs.Include(jsonRpcArgs) } + if ctn.infura != nil { + infuraArgs, err := ctn.infura.Args() + if err != nil { + return nil, err + } + newArgs.Include(infuraArgs) + } return newArgs, nil }) diff --git a/toolkit/server/extargs/http_test.go b/toolkit/server/extargs/http_test.go index 401bd3a..8ea454a 100644 --- a/toolkit/server/extargs/http_test.go +++ b/toolkit/server/extargs/http_test.go @@ -107,15 +107,15 @@ func TestHttp(t *testing.T) { // we don't test the args hash here emptyArgs := args.New().ReadOnly() - ctx := NewHttpExtArgs(pol, emptyArgs, r) + extArgs := NewHttpExtArgs(pol, emptyArgs, r) - _, err := ctx.Args() + _, err := extArgs.Args() require.NoError(t, err) if tc.expected { - require.NoError(t, ctx.Verify()) + require.NoError(t, extArgs.Verify()) } else { - require.Error(t, ctx.Verify()) + require.Error(t, extArgs.Verify()) } } @@ -173,12 +173,12 @@ func TestHttpHash(t *testing.T) { err := invArgs.Add(HttpArgsKey, tc.hash) require.NoError(t, err) - ctx := NewHttpExtArgs(pol, invArgs.ReadOnly(), req) + extArgs := NewHttpExtArgs(pol, invArgs.ReadOnly(), req) if tc.expected { - require.NoError(t, ctx.Verify()) + require.NoError(t, extArgs.Verify()) } else { - require.Error(t, ctx.Verify()) + require.Error(t, extArgs.Verify()) } }) } diff --git a/toolkit/server/extargs/infura.go b/toolkit/server/extargs/infura.go new file mode 100644 index 0000000..ff6c066 --- /dev/null +++ b/toolkit/server/extargs/infura.go @@ -0,0 +1,85 @@ +package extargs + +import ( + "fmt" + "sync" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/fluent/qp" + "github.com/ipld/go-ipld-prime/node/basicnode" + "github.com/ucan-wg/go-ucan/pkg/args" + "github.com/ucan-wg/go-ucan/pkg/policy" +) + +const InfuraArgsKey = "inf" + +type InfuraExtArgs struct { + pol policy.Policy + originalArgs args.ReadOnly + assembler func(ma datamodel.MapAssembler) + + once sync.Once + args *args.Args + argsIpld ipld.Node +} + +func NewInfuraExtArgs(pol policy.Policy, assembler func(ma datamodel.MapAssembler)) *InfuraExtArgs { + return &InfuraExtArgs{pol: pol, assembler: assembler} +} + +func (ia *InfuraExtArgs) Verify() error { + if err := ia.makeArgs(); err != nil { + return err + } + + // Note: InfuraExtArgs doesn't support verifying a hash computed client-side like the other + // external args, as the arguments are by nature dynamic. The client can't generate a meaningful hash. + + ok, leaf := ia.pol.PartialMatch(ia.argsIpld) + if !ok { + return fmt.Errorf("the following UCAN policy is not satisfied: %v", leaf.String()) + } + return nil +} + +func (ia *InfuraExtArgs) Args() (*args.Args, error) { + if err := ia.makeArgs(); err != nil { + return nil, err + } + return ia.args, nil +} + +func (ia *InfuraExtArgs) makeArgs() error { + var outerErr error + ia.once.Do(func() { + var err error + + ia.args, err = makeInfuraArgs(ia.assembler) + if err != nil { + outerErr = err + return + } + + ia.argsIpld, err = ia.args.ToIPLD() + if err != nil { + outerErr = err + return + } + }) + return outerErr +} + +func makeInfuraArgs(assembler func(ma datamodel.MapAssembler)) (*args.Args, error) { + n, err := qp.BuildMap(basicnode.Prototype.Any, -1, assembler) + if err != nil { + return nil, err + } + + res := args.New() + err = res.Add(InfuraArgsKey, n) + if err != nil { + return nil, err + } + return res, nil +} diff --git a/toolkit/server/extargs/infura_test.go b/toolkit/server/extargs/infura_test.go new file mode 100644 index 0000000..b8de085 --- /dev/null +++ b/toolkit/server/extargs/infura_test.go @@ -0,0 +1,114 @@ +package extargs + +import ( + "fmt" + "testing" + + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/fluent/qp" + "github.com/stretchr/testify/require" + "github.com/ucan-wg/go-ucan/pkg/policy" + "github.com/ucan-wg/go-ucan/pkg/policy/literal" +) + +func ExampleInfuraExtArgs() { + // Note: this is an example for how to build arguments, but you likely want to use InfuraExtArgs + // through UcanCtx. + + pol := policy.Policy{} // policies from the delegations + + // We will construct the following args: + // { + // "ntwk":"eth-mainnet", + // "quota":{ + // "ur":1234, + // "udc":1234, + // "arch":1234, + // "down":1234, + // "store":1234, + // "up":1234 + // } + // } + infArgs := NewInfuraExtArgs(pol, func(ma datamodel.MapAssembler) { + qp.MapEntry(ma, "ntwk", qp.String("eth-mainnet")) + qp.MapEntry(ma, "quota", qp.Map(6, func(ma datamodel.MapAssembler) { + qp.MapEntry(ma, "ur", qp.Int(1234)) + qp.MapEntry(ma, "udc", qp.Int(1234)) + qp.MapEntry(ma, "arch", qp.Int(1234)) + qp.MapEntry(ma, "down", qp.Int(1234)) + qp.MapEntry(ma, "store", qp.Int(1234)) + qp.MapEntry(ma, "up", qp.Int(1234)) + })) + }) + + err := infArgs.Verify() + fmt.Println(err) + + // Output: + // +} + +func TestInfura(t *testing.T) { + assembler := func(ma datamodel.MapAssembler) { + qp.MapEntry(ma, "ntwk", qp.String("eth-mainnet")) + qp.MapEntry(ma, "quota", qp.Map(6, func(ma datamodel.MapAssembler) { + qp.MapEntry(ma, "ur", qp.Int(1234)) + qp.MapEntry(ma, "udc", qp.Int(1234)) + qp.MapEntry(ma, "arch", qp.Int(1234)) + qp.MapEntry(ma, "down", qp.Int(1234)) + qp.MapEntry(ma, "store", qp.Int(1234)) + qp.MapEntry(ma, "up", qp.Int(1234)) + })) + } + + tests := []struct { + name string + pol policy.Policy + expected bool + }{ + { + name: "no policies", + pol: policy.Policy{}, + expected: true, + }, + { + name: "matching args", + pol: policy.MustConstruct( + policy.Equal(".inf.ntwk", literal.String("eth-mainnet")), + policy.LessThanOrEqual(".inf.quota.ur", literal.Int(1234)), + ), + expected: true, + }, + { + name: "wrong network", + pol: policy.MustConstruct( + policy.Equal(".inf.ntwk", literal.String("avalanche-fuji")), + policy.LessThanOrEqual(".inf.quota.ur", literal.Int(1234)), + ), + expected: false, + }, + { + name: "unrespected quota", + pol: policy.MustConstruct( + policy.Equal(".inf.ntwk", literal.String("eth-mainnet")), + policy.LessThanOrEqual(".inf.quota.ur", literal.Int(100)), + ), + expected: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + extArgs := NewInfuraExtArgs(tc.pol, assembler) + + _, err := extArgs.Args() + require.NoError(t, err) + + if tc.expected { + require.NoError(t, extArgs.Verify()) + } else { + require.Error(t, extArgs.Verify()) + } + }) + } +} diff --git a/toolkit/server/extargs/jsonrpc_test.go b/toolkit/server/extargs/jsonrpc_test.go index 1346f1d..80a444d 100644 --- a/toolkit/server/extargs/jsonrpc_test.go +++ b/toolkit/server/extargs/jsonrpc_test.go @@ -97,15 +97,15 @@ func TestJsonRpc(t *testing.T) { // we don't test the args hash here emptyArgs := args.New().ReadOnly() - ctx := NewJsonRpcExtArgs(tc.pol, emptyArgs, tc.req) + extArgs := NewJsonRpcExtArgs(tc.pol, emptyArgs, tc.req) - _, err := ctx.Args() + _, err := extArgs.Args() require.NoError(t, err) if tc.expected { - require.NoError(t, ctx.Verify()) + require.NoError(t, extArgs.Verify()) } else { - require.Error(t, ctx.Verify()) + require.Error(t, extArgs.Verify()) } }) } @@ -152,12 +152,12 @@ func TestJsonRpcHash(t *testing.T) { err := invArgs.Add(JsonRpcArgsKey, tc.hash) require.NoError(t, err) - ctx := NewJsonRpcExtArgs(pol, invArgs.ReadOnly(), req) + extArgs := NewJsonRpcExtArgs(pol, invArgs.ReadOnly(), req) if tc.expected { - require.NoError(t, ctx.Verify()) + require.NoError(t, extArgs.Verify()) } else { - require.Error(t, ctx.Verify()) + require.Error(t, extArgs.Verify()) } }) }