Skip to content

Commit 12c47d0

Browse files
author
zer0stars
committed
refactor: remove chain/vehicle NFT config, use subject-based auth and identity API
- Drop CHAIN_ID and VEHICLE_NFT_ADDRESS from config, Helm values, and samples - Rename requireVehicleOptsByDID → requireSubjectOptsByDID; validate token first, then resolve DID via identity client - Rename identity GetVehicleDIDForDevice → GetLinkedDIDForDevice - Add identity package tests (aftermarket/synthetic/both-null) - Update resolver tests for subject-based flow and new error messages
1 parent 53a1eca commit 12c47d0

11 files changed

Lines changed: 234 additions & 166 deletions

File tree

charts/fetch-api/values-prod.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ env:
1313
EPHEMERAL_BUCKET: dimo-ingest-ephemeral-prod
1414
PARQUET_BUCKET: dimo-iceberg-prod
1515
VEHICLE_NFT_ADDRESS: '0xbA5738a18d83D41847dfFbDC6101d37C69c9B0cF'
16-
CHAIN_ID: 137
1716
IDENTITY_API_URL: https://identity-api.dimo.zone/query
1817
ingress:
1918
enabled: true

charts/fetch-api/values.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@ env:
3535
EPHEMERAL_BUCKET: dimo-ingest-ephemeral-dev
3636
PARQUET_BUCKET: dimo-iceberg-dev
3737
S3_AWS_REGION: us-east-2
38-
VEHICLE_NFT_ADDRESS: '0x45fbCD3ef7361d156e8b16F5538AE36DEdf61Da8'
39-
CHAIN_ID: 80002
4038
IDENTITY_API_URL: https://identity-api.dev.dimo.zone/query
4139
service:
4240
type: ClusterIP

internal/app/app.go

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ package app
33
import (
44
"fmt"
55
"net/http"
6-
"strconv"
7-
86
"github.com/99designs/gqlgen/graphql/handler"
97
"github.com/99designs/gqlgen/graphql/handler/extension"
108
"github.com/99designs/gqlgen/graphql/handler/transport"
@@ -38,11 +36,6 @@ type App struct {
3836

3937
// New creates a new application with GraphQL handler and middleware.
4038
func New(settings config.Settings) (*App, error) {
41-
chainID, err := strconv.ParseUint(settings.ChainID, 10, 64)
42-
if err != nil {
43-
return nil, fmt.Errorf("failed to parse chain ID: %w", err)
44-
}
45-
4639
chConn, err := chClientFromSettings(&settings)
4740
if err != nil {
4841
return nil, fmt.Errorf("failed to create ClickHouse connection: %w", err)
@@ -56,7 +49,7 @@ func New(settings config.Settings) (*App, error) {
5649
identityClient = identity.New(settings.IdentityAPIURL)
5750
}
5851

59-
gqlSrv := newGraphQLHandler(&settings, eventService, buckets, chainID, identityClient)
52+
gqlSrv := newGraphQLHandler(&settings, eventService, buckets, identityClient)
6053

6154
jwtMiddleware, err := auth.NewJWTMiddleware(settings.TokenExchangeIssuer, settings.TokenExchangeJWTKeySetURL)
6255
if err != nil {
@@ -98,12 +91,10 @@ func (a *App) Cleanup() {
9891
}
9992

10093
// newGraphQLHandler creates a configured gqlgen handler.Server.
101-
func newGraphQLHandler(settings *config.Settings, eventService *eventrepo.Service, buckets []string, chainID uint64, identityClient identity.Client) *handler.Server {
94+
func newGraphQLHandler(settings *config.Settings, eventService *eventrepo.Service, buckets []string, identityClient identity.Client) *handler.Server {
10295
resolver := &graph.Resolver{
10396
EventService: eventService,
10497
Buckets: buckets,
105-
VehicleAddr: settings.VehicleNFTAddress,
106-
ChainID: chainID,
10798
IdentityClient: identityClient,
10899
}
109100

internal/config/settings.go

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,17 @@ package config
33

44
import (
55
"github.com/DIMO-Network/clickhouse-infra/pkg/connect/config"
6-
"github.com/ethereum/go-ethereum/common"
76
)
87

98
// Settings contains the application config.
109
type Settings struct {
11-
Port int `yaml:"PORT"`
12-
MonPort int `yaml:"MON_PORT"`
13-
GRPCPort int `yaml:"GRPC_PORT"`
14-
EnablePprof bool `yaml:"ENABLE_PPROF"`
15-
MaxRequestDuration string `yaml:"MAX_REQUEST_DURATION"`
16-
TokenExchangeJWTKeySetURL string `yaml:"TOKEN_EXCHANGE_JWK_KEY_SET_URL"`
17-
TokenExchangeIssuer string `yaml:"TOKEN_EXCHANGE_ISSUER_URL"`
18-
VehicleNFTAddress common.Address `yaml:"VEHICLE_NFT_ADDRESS"`
19-
ChainID string `yaml:"CHAIN_ID"`
10+
Port int `yaml:"PORT"`
11+
MonPort int `yaml:"MON_PORT"`
12+
GRPCPort int `yaml:"GRPC_PORT"`
13+
EnablePprof bool `yaml:"ENABLE_PPROF"`
14+
MaxRequestDuration string `yaml:"MAX_REQUEST_DURATION"`
15+
TokenExchangeJWTKeySetURL string `yaml:"TOKEN_EXCHANGE_JWK_KEY_SET_URL"`
16+
TokenExchangeIssuer string `yaml:"TOKEN_EXCHANGE_ISSUER_URL"`
2017
CloudEventBucket string `yaml:"CLOUDEVENT_BUCKET"`
2118
EphemeralBucket string `yaml:"EPHEMERAL_BUCKET"`
2219
ParquetBucket string `yaml:"PARQUET_BUCKET"`

internal/graph/base.resolvers.go

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/graph/resolver.go

Lines changed: 40 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"github.com/DIMO-Network/fetch-api/pkg/eventrepo"
1212
"github.com/DIMO-Network/fetch-api/pkg/grpc"
1313
"github.com/DIMO-Network/token-exchange-api/pkg/tokenclaims"
14-
"github.com/ethereum/go-ethereum/common"
1514
)
1615

1716
// This file will not be regenerated automatically.
@@ -24,69 +23,65 @@ type ClaimsContextKey struct{}
2423
type Resolver struct {
2524
EventService *eventrepo.Service
2625
Buckets []string
27-
VehicleAddr common.Address
28-
ChainID uint64
2926
IdentityClient identity.Client
3027
}
3128

32-
// CheckVehicleRawDataByDID returns an error if the context does not have claims or the token
33-
// does not have GetRawData permission for the given vehicle DID, or if the DID is invalid.
34-
func CheckVehicleRawDataByDID(ctx context.Context, did string) error {
35-
if _, err := cloudevent.DecodeERC721DID(did); err != nil {
36-
return fmt.Errorf("invalid DID: %w", err)
29+
const (
30+
errNoTokenClaims = "unauthorized: no token claims"
31+
errNoPermission = "unauthorized: token does not have required permission for this operation"
32+
errNoAccessToSubject = "unauthorized: token does not have access to this subject"
33+
)
34+
35+
// requireSubjectOptsByDID validates raw-data access and returns search options for the DID.
36+
// requestedDID: the DID from the client (e.g. cloudEvents(did: "...")).
37+
// tokenSubjectDID: the DID the JWT grants access to (tok.Asset).
38+
func (r *queryResolver) requireSubjectOptsByDID(ctx context.Context, requestedDID string, filter *model.CloudEventFilter) (*grpc.SearchOptions, error) {
39+
token, err := requireRawDataToken(ctx)
40+
if err != nil {
41+
return nil, err
42+
}
43+
tokenSubjectDID := token.Asset // DID the JWT permits access to
44+
searchSubject, err := r.ensureRequestedDIDLinkedToPermissionedSubject(ctx, requestedDID, tokenSubjectDID)
45+
if err != nil {
46+
return nil, err
3747
}
38-
return checkRawDataPermissionsForVehicle(ctx, did)
48+
return filterToSearchOptions(filter, searchSubject), nil
3949
}
4050

41-
// checkRawDataPermissionsForVehicle verifies the context token has access for vehicleDID.
42-
// vehicleDID must already be validated.
43-
// Allowed: GetRawData, or both GetLocationHistory (all-time location) and GetNonLocationHistory.
44-
func checkRawDataPermissionsForVehicle(ctx context.Context, vehicleDID string) error {
51+
// requireRawDataToken returns the token if the context has claims and the token has raw-data permission.
52+
// Call this first so unauthenticated or insufficient-permission requests get a clear error before any DID resolution.
53+
func requireRawDataToken(ctx context.Context) (*tokenclaims.Token, error) {
4554
tok, _ := ctx.Value(ClaimsContextKey{}).(*tokenclaims.Token)
4655
if tok == nil {
47-
return fmt.Errorf("unauthorized: no token claims")
48-
}
49-
if tok.Asset != vehicleDID {
50-
return fmt.Errorf("unauthorized: token does not have access to this vehicle")
56+
return nil, fmt.Errorf("%s", errNoTokenClaims)
5157
}
5258
hasGetRawData := slices.Contains(tok.Permissions, tokenclaims.PermissionGetRawData)
5359
hasLocationHistory := slices.Contains(tok.Permissions, tokenclaims.PermissionGetLocationHistory)
5460
hasNonLocationHistory := slices.Contains(tok.Permissions, tokenclaims.PermissionGetNonLocationHistory)
5561
hasAllTimeData := hasLocationHistory && hasNonLocationHistory
5662
if !hasGetRawData && !hasAllTimeData {
57-
return fmt.Errorf("unauthorized: token does not have required permission for this vehicle operation")
63+
return nil, fmt.Errorf("%s", errNoPermission)
5864
}
59-
return nil
65+
return tok, nil
6066
}
6167

62-
// requireVehicleOptsByDID validates raw-data access and returns search options for the DID.
63-
//
64-
// If did belongs to the vehicle NFT contract (r.VehicleAddr), the token asset must match that
65-
// vehicle DID directly. Otherwise the DID is treated as a device DID: identity-api is called
66-
// to resolve the paired vehicle, and the token must have access to that vehicle. Either way,
67-
// the search subject is always the originally requested DID.
68-
func (r *queryResolver) requireVehicleOptsByDID(ctx context.Context, did string, filter *model.CloudEventFilter) (*grpc.SearchOptions, error) {
69-
decoded, err := cloudevent.DecodeERC721DID(did)
68+
// ensureRequestedDIDLinkedToPermissionedSubject verifies the client-requested DID is allowed by the token.
69+
// requestedDID: the DID from the query (e.g. cloudEvents(did: "...")).
70+
// 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+
requestedDIDParsed, err := cloudevent.DecodeERC721DID(requestedDID)
7073
if err != nil {
71-
return nil, fmt.Errorf("invalid DID: %w", err)
74+
return cloudevent.ERC721DID{}, fmt.Errorf("%s", errNoAccessToSubject)
7275
}
73-
74-
var vehicleDID string
75-
if decoded.ContractAddress == r.VehicleAddr {
76-
vehicleDID = did
77-
} else {
78-
if r.IdentityClient == nil {
79-
return nil, fmt.Errorf("device DID lookup not configured: no identity client")
80-
}
81-
vehicleDID, err = r.IdentityClient.GetVehicleDIDForDevice(ctx, did)
82-
if err != nil {
83-
return nil, fmt.Errorf("failed to resolve vehicle for device DID: %w", err)
84-
}
76+
if requestedDID == tokenSubjectDID {
77+
return requestedDIDParsed, nil
8578
}
86-
87-
if err := checkRawDataPermissionsForVehicle(ctx, vehicleDID); err != nil {
88-
return nil, err
79+
if r.IdentityClient == nil {
80+
return cloudevent.ERC721DID{}, fmt.Errorf("%s", errNoAccessToSubject)
8981
}
90-
91-
return filterToSearchOptions(filter, decoded), nil
82+
linkedDID, err := r.IdentityClient.GetLinkedDIDForDevice(ctx, requestedDIDParsed.String())
83+
if err != nil || linkedDID != tokenSubjectDID {
84+
return cloudevent.ERC721DID{}, fmt.Errorf("%s", errNoAccessToSubject)
85+
}
86+
return requestedDIDParsed, nil
9287
}

0 commit comments

Comments
 (0)