extargs: add ".inf" external args for arbitrary Infura args

This commit is contained in:
Michael Muré
2025-01-16 13:36:18 +01:00
committed by Michael Muré
parent 09c8815755
commit 3b6d70f47a
5 changed files with 236 additions and 14 deletions

View File

@@ -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
})

View File

@@ -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())
}
})
}

View File

@@ -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
}

View File

@@ -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:
// <nil>
}
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())
}
})
}
}

View File

@@ -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())
}
})
}