Skip to content

Commit f72e699

Browse files
alanshawvolmedo
andauthored
feat: add access/grant capability definition (#51)
refs storacha/RFC#68 Since `access/authorize` is a capability that already exists and has semantics that vary slightly from what is proposed in the RFC I have named the capability `access/grant` which is roughly similar but different enough that we won't confuse the two. --------- Co-authored-by: Vicente Olmedo <vicente@storacha.network>
1 parent 91ae148 commit f72e699

File tree

6 files changed

+240
-3
lines changed

6 files changed

+240
-3
lines changed

capabilities/access/access.ipldsch

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,17 @@ type DelegateCaveats struct {
3636

3737
type DelegateOk struct {
3838
}
39+
40+
type GrantCaveats struct {
41+
att [CapabilityRequest]
42+
cause optional Link
43+
}
44+
45+
type GrantOk struct {
46+
delegations {String:Bytes}
47+
}
48+
49+
type GrantError struct {
50+
errorName String (rename "name")
51+
message String
52+
}

capabilities/access/grant.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package access
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/ipld/go-ipld-prime/datamodel"
7+
"github.com/storacha/go-libstoracha/capabilities/types"
8+
"github.com/storacha/go-ucanto/core/ipld"
9+
"github.com/storacha/go-ucanto/core/receipt"
10+
"github.com/storacha/go-ucanto/core/result/failure"
11+
"github.com/storacha/go-ucanto/core/schema"
12+
"github.com/storacha/go-ucanto/ucan"
13+
"github.com/storacha/go-ucanto/validator"
14+
)
15+
16+
const GrantAbility = "access/grant"
17+
18+
// GrantCaveats are the caveats required to perform an access/grant invocation.
19+
type GrantCaveats struct {
20+
// Att are the capabilities agent wishes to be granted.
21+
Att []CapabilityRequest
22+
// Cause is an OPTIONAL link to a UCAN that provides context for the grant
23+
// request. The linked UCAN MUST be included in the invocation.
24+
Cause ucan.Link
25+
}
26+
27+
func (gc GrantCaveats) ToIPLD() (datamodel.Node, error) {
28+
return ipld.WrapWithRecovery(&gc, GrantCaveatsType(), types.Converters...)
29+
}
30+
31+
var GrantCaveatsReader = schema.Struct[GrantCaveats](GrantCaveatsType(), nil, types.Converters...)
32+
33+
// GrantOk represents the successful response for a access/grant invocation.
34+
type GrantOk struct {
35+
Delegations DelegationsModel
36+
}
37+
38+
func (gok GrantOk) ToIPLD() (datamodel.Node, error) {
39+
return ipld.WrapWithRecovery(&gok, GrantOkType(), types.Converters...)
40+
}
41+
42+
var GrantOkReader = schema.Struct[GrantOk](GrantOkType(), nil, types.Converters...)
43+
44+
type GrantError struct {
45+
ErrorName string
46+
Message string
47+
}
48+
49+
func (ge GrantError) Name() string {
50+
return ge.ErrorName
51+
}
52+
53+
func (ge GrantError) Error() string {
54+
return ge.Message
55+
}
56+
57+
func (ge GrantError) ToIPLD() (datamodel.Node, error) {
58+
return ipld.WrapWithRecovery(&ge, GrantErrorType(), types.Converters...)
59+
}
60+
61+
type GrantReceipt receipt.Receipt[GrantOk, GrantError]
62+
type GrantReceiptReader receipt.ReceiptReader[GrantOk, GrantError]
63+
64+
func NewGrantReceiptReader() (GrantReceiptReader, error) {
65+
return receipt.NewReceiptReaderFromTypes[GrantOk, GrantError](GrantOkType(), GrantErrorType())
66+
}
67+
68+
// Grant is a capability that allows an agent to request capabilities from the
69+
// invocation executor.
70+
var Grant = validator.NewCapability(
71+
GrantAbility,
72+
schema.DIDString(),
73+
GrantCaveatsReader,
74+
GrantDerive,
75+
)
76+
77+
func GrantDerive(claimed, delegated ucan.Capability[GrantCaveats]) failure.Failure {
78+
return schema.NewSchemaError(fmt.Sprintf("%s cannot be delegated", GrantAbility))
79+
}

capabilities/access/grant_test.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package access_test
2+
3+
import (
4+
"io"
5+
"testing"
6+
7+
"github.com/storacha/go-libstoracha/capabilities/access"
8+
"github.com/storacha/go-libstoracha/testutil"
9+
"github.com/storacha/go-ucanto/core/delegation"
10+
"github.com/storacha/go-ucanto/core/invocation"
11+
"github.com/storacha/go-ucanto/core/message"
12+
"github.com/storacha/go-ucanto/core/receipt"
13+
"github.com/storacha/go-ucanto/core/receipt/ran"
14+
"github.com/storacha/go-ucanto/core/result"
15+
"github.com/storacha/go-ucanto/transport/car"
16+
"github.com/storacha/go-ucanto/ucan"
17+
"github.com/stretchr/testify/require"
18+
)
19+
20+
func TestGrant(t *testing.T) {
21+
grantCaveats := access.GrantCaveats{
22+
Att: []access.CapabilityRequest{{Can: "admin/party"}},
23+
Cause: testutil.RandomCID(t),
24+
}
25+
26+
inv, err := access.Grant.Invoke(
27+
testutil.Alice,
28+
testutil.Bob,
29+
testutil.Alice.DID().String(),
30+
grantCaveats,
31+
)
32+
require.NoError(t, err)
33+
34+
t.Run("round trip", func(t *testing.T) {
35+
d0, err := delegation.Delegate(
36+
testutil.Bob,
37+
testutil.Alice,
38+
[]ucan.Capability[ucan.NoCaveats]{
39+
ucan.NewCapability(grantCaveats.Att[0].Can, testutil.Bob.DID().String(), ucan.NoCaveats{}),
40+
},
41+
)
42+
require.NoError(t, err)
43+
44+
d0Bytes, err := io.ReadAll(d0.Archive())
45+
require.NoError(t, err)
46+
47+
grantOk := access.GrantOk{
48+
Delegations: access.DelegationsModel{
49+
Keys: []string{d0.Link().String()},
50+
Values: map[string][]byte{d0.Link().String(): d0Bytes},
51+
},
52+
}
53+
54+
r0, err := receipt.Issue(
55+
testutil.Bob,
56+
result.Ok[access.GrantOk, access.GrantError](grantOk),
57+
ran.FromInvocation(inv),
58+
)
59+
require.NoError(t, err)
60+
61+
// round trip the invocation and receipt in an agent message to ensure the
62+
// invocation can be encoded and the receipt decoded
63+
msg := roundTripAgentMessage(t, []invocation.Invocation{inv}, []receipt.AnyReceipt{r0})
64+
65+
rcptLink, ok := msg.Get(inv.Link())
66+
require.True(t, ok)
67+
68+
reader, err := access.NewGrantReceiptReader()
69+
require.NoError(t, err)
70+
71+
r1, err := reader.Read(rcptLink, msg.Blocks())
72+
require.NoError(t, err)
73+
74+
o, x := result.Unwrap(r1.Out())
75+
require.Empty(t, x)
76+
require.Len(t, o.Delegations.Keys, 1)
77+
require.Equal(t, d0.Link().String(), o.Delegations.Keys[0])
78+
79+
_, err = delegation.Extract(o.Delegations.Values[d0.Link().String()])
80+
require.NoError(t, err)
81+
})
82+
83+
t.Run("round trip with error", func(t *testing.T) {
84+
grantErr := access.GrantError{
85+
ErrorName: "Unauthorized",
86+
Message: "No, no you may not.",
87+
}
88+
89+
r0, err := receipt.Issue(
90+
testutil.Bob,
91+
result.Error[access.GrantOk](grantErr),
92+
ran.FromInvocation(inv),
93+
)
94+
require.NoError(t, err)
95+
96+
msg := roundTripAgentMessage(t, []invocation.Invocation{inv}, []receipt.AnyReceipt{r0})
97+
98+
rcptLink, ok := msg.Get(inv.Link())
99+
require.True(t, ok)
100+
101+
reader, err := access.NewGrantReceiptReader()
102+
require.NoError(t, err)
103+
104+
r1, err := reader.Read(rcptLink, msg.Blocks())
105+
require.NoError(t, err)
106+
107+
o, x := result.Unwrap(r1.Out())
108+
require.Empty(t, o)
109+
require.Equal(t, grantErr.ErrorName, x.ErrorName)
110+
require.Equal(t, grantErr.Name(), x.Name())
111+
require.Equal(t, grantErr.Message, x.Message)
112+
require.Equal(t, grantErr.Error(), x.Error())
113+
})
114+
}
115+
116+
func roundTripAgentMessage(t *testing.T, invs []invocation.Invocation, rcpts []receipt.AnyReceipt) message.AgentMessage {
117+
t.Helper()
118+
inMsg, err := message.Build(invs, rcpts)
119+
require.NoError(t, err)
120+
121+
outCodec := car.NewOutboundCodec()
122+
req, err := outCodec.Encode(inMsg)
123+
require.NoError(t, err)
124+
125+
inCodec, err := car.NewInboundCodec().Accept(req)
126+
require.NoError(t, err)
127+
128+
outMsg, err := inCodec.Decoder().Decode(req)
129+
require.NoError(t, err)
130+
131+
return outMsg
132+
}

capabilities/access/schema.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,15 @@ func DelegateCaveatsType() schema.Type {
5353
func DelegateOkType() schema.Type {
5454
return accessTS.TypeByName("DelegateOk")
5555
}
56+
57+
func GrantCaveatsType() schema.Type {
58+
return accessTS.TypeByName("GrantCaveats")
59+
}
60+
61+
func GrantOkType() schema.Type {
62+
return accessTS.TypeByName("GrantOk")
63+
}
64+
65+
func GrantErrorType() schema.Type {
66+
return accessTS.TypeByName("GrantError")
67+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ require (
1818
github.com/multiformats/go-multicodec v0.9.1
1919
github.com/multiformats/go-multihash v0.2.3
2020
github.com/multiformats/go-varint v0.0.7
21-
github.com/storacha/go-ucanto v0.5.0
21+
github.com/storacha/go-ucanto v0.6.5
2222
github.com/stretchr/testify v1.10.0
2323
github.com/whyrusleeping/cbor-gen v0.2.0
2424
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -507,8 +507,8 @@ github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t6
507507
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
508508
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
509509
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
510-
github.com/storacha/go-ucanto v0.5.0 h1:BCYfTOjJ7DxmoGwpZn4N1XITWj1BdKIbk5Ok7kMoQ6I=
511-
github.com/storacha/go-ucanto v0.5.0/go.mod h1:/I6qtE+oDHc+6lBc/glN+RFx0cbP/mDj4gihD7YezWc=
510+
github.com/storacha/go-ucanto v0.6.5 h1:mxy1UkJDqszAGe6SkoT0N2SG9YJ62YX7fzU1Pg9lxnA=
511+
github.com/storacha/go-ucanto v0.6.5/go.mod h1:O35Ze4x18EWtz3ftRXXd/mTZ+b8OQVjYYrnadJ/xNjg=
512512
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
513513
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
514514
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=

0 commit comments

Comments
 (0)