Skip to content

Commit ce681c2

Browse files
elffjsclaude
andauthored
Allow any DID format when token asset matches query DID exactly (#69)
* Allow any DID format when token asset matches query DID exactly Previously, DecodeERC721DID was called before the direct-match check, so did:ethr tokens would fail immediately with "unauthorized" even when token.Asset and the query did were identical. Move the direct-match check first so it handles any DID format. Change filterToSearchOptions to accept a plain string subject so the ERC721DID assumption is removed end-to-end. The device-resolution path (aftermarket/synthetic device → vehicle DID) is unchanged. https://claude.ai/code/session_01R5aqTcL3rKEnPazSjxCCP2 * Add a note for later --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5a2365f commit ce681c2

3 files changed

Lines changed: 68 additions & 11 deletions

File tree

internal/graph/convert.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import (
99
"google.golang.org/protobuf/types/known/wrapperspb"
1010
)
1111

12-
// filterToSearchOptions converts GraphQL filter and tokenID to grpc.SearchOptions.
13-
func filterToSearchOptions(filter *model.CloudEventFilter, subject cloudevent.ERC721DID) *grpc.SearchOptions {
12+
// filterToSearchOptions converts GraphQL filter and subject DID to grpc.SearchOptions.
13+
func filterToSearchOptions(filter *model.CloudEventFilter, subject string) *grpc.SearchOptions {
1414
opts := &grpc.SearchOptions{
15-
Subject: &wrapperspb.StringValue{Value: subject.String()},
15+
Subject: &wrapperspb.StringValue{Value: subject},
1616
}
1717
if filter == nil {
1818
return opts

internal/graph/resolver.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type Resolver struct {
2626
IdentityClient identity.Client
2727
}
2828

29+
// TODO(elffjs): Shouldn't these be Errors?
2930
const (
3031
errNoTokenClaims = "unauthorized: no token claims"
3132
errNoPermission = "unauthorized: token does not have required permission for this operation"
@@ -68,20 +69,22 @@ func requireRawDataToken(ctx context.Context) (*tokenclaims.Token, error) {
6869
// ensureRequestedDIDLinkedToPermissionedSubject verifies the client-requested DID is allowed by the token.
6970
// requestedDID: the DID from the query (e.g. cloudEvents(did: "...")).
7071
// tokenSubjectDID: the DID the JWT grants access to (tok.Asset).
71-
func (r *queryResolver) ensureRequestedDIDLinkedToPermissionedSubject(ctx context.Context, requestedDID string, tokenSubjectDID string) (cloudevent.ERC721DID, error) {
72+
func (r *queryResolver) ensureRequestedDIDLinkedToPermissionedSubject(ctx context.Context, requestedDID string, tokenSubjectDID string) (string, error) {
73+
// Direct match works for any DID format (did:ethr, did:erc721, etc.).
74+
if requestedDID == tokenSubjectDID {
75+
return requestedDID, nil
76+
}
77+
// Device-resolution path requires an ERC721 DID.
7278
requestedDIDParsed, err := cloudevent.DecodeERC721DID(requestedDID)
7379
if err != nil {
74-
return cloudevent.ERC721DID{}, fmt.Errorf("%s", errNoAccessToSubject)
75-
}
76-
if requestedDID == tokenSubjectDID {
77-
return requestedDIDParsed, nil
80+
return "", fmt.Errorf("%s", errNoAccessToSubject)
7881
}
7982
if r.IdentityClient == nil {
80-
return cloudevent.ERC721DID{}, fmt.Errorf("%s", errNoAccessToSubject)
83+
return "", fmt.Errorf("%s", errNoAccessToSubject)
8184
}
8285
linkedDID, err := r.IdentityClient.GetLinkedDIDForDevice(ctx, requestedDIDParsed.String())
8386
if err != nil || linkedDID != tokenSubjectDID {
84-
return cloudevent.ERC721DID{}, fmt.Errorf("%s", errNoAccessToSubject)
87+
return "", fmt.Errorf("%s", errNoAccessToSubject)
8588
}
86-
return requestedDIDParsed, nil
89+
return requestedDIDParsed.String(), nil
8790
}

internal/graph/resolver_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,60 @@ func (s *stubIdentityClient) GetLinkedDIDForDevice(_ context.Context, _ string)
144144
return s.vehicleDID, s.err
145145
}
146146

147+
func TestRequireSubjectOptsByDID_EthrDID(t *testing.T) {
148+
const ethrDID = "did:ethr:137:0xabcdef1234567890abcdef1234567890abcdef12"
149+
const otherEthrDID = "did:ethr:137:0x1111111111111111111111111111111111111111"
150+
151+
t.Run("ethr token + same ethr query DID allowed", func(t *testing.T) {
152+
ctx := contextWithToken(ethrDID, tokenclaims.PermissionGetRawData)
153+
r := &Resolver{}
154+
q := &queryResolver{r}
155+
opts, err := q.requireSubjectOptsByDID(ctx, ethrDID, nil)
156+
require.NoError(t, err)
157+
require.NotNil(t, opts)
158+
require.NotNil(t, opts.Subject)
159+
assert.Equal(t, ethrDID, opts.Subject.Value)
160+
})
161+
162+
t.Run("ethr token + different ethr query DID denied", func(t *testing.T) {
163+
ctx := contextWithToken(ethrDID, tokenclaims.PermissionGetRawData)
164+
r := &Resolver{}
165+
q := &queryResolver{r}
166+
opts, err := q.requireSubjectOptsByDID(ctx, otherEthrDID, nil)
167+
require.Error(t, err)
168+
assert.Nil(t, opts)
169+
assert.Contains(t, err.Error(), "does not have access to this subject")
170+
})
171+
172+
t.Run("ethr token + erc721 query DID denied (device resolution returns vehicle DID, not ethr DID)", func(t *testing.T) {
173+
vehicleDID := cloudevent.ERC721DID{
174+
ChainID: 137,
175+
ContractAddress: common.HexToAddress("0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
176+
TokenID: big.NewInt(1),
177+
}.String()
178+
ctx := contextWithToken(ethrDID, tokenclaims.PermissionGetRawData)
179+
// Identity-api returns a vehicle DID, not the ethr DID — so linkedDID != tokenSubjectDID.
180+
r := &Resolver{
181+
IdentityClient: &stubIdentityClient{vehicleDID: vehicleDID},
182+
}
183+
q := &queryResolver{r}
184+
opts, err := q.requireSubjectOptsByDID(ctx, vehicleDID, nil)
185+
require.Error(t, err)
186+
assert.Nil(t, opts)
187+
assert.Contains(t, err.Error(), "does not have access to this subject")
188+
})
189+
190+
t.Run("ethr token without required permission denied", func(t *testing.T) {
191+
ctx := contextWithToken(ethrDID)
192+
r := &Resolver{}
193+
q := &queryResolver{r}
194+
opts, err := q.requireSubjectOptsByDID(ctx, ethrDID, nil)
195+
require.Error(t, err)
196+
assert.Nil(t, opts)
197+
assert.Contains(t, err.Error(), "required permission for this operation")
198+
})
199+
}
200+
147201
func TestRequireVehicleOptsByDID_DeviceDID(t *testing.T) {
148202
vehicleAddr := common.HexToAddress("0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
149203
vehicleDID := cloudevent.ERC721DID{

0 commit comments

Comments
 (0)