Skip to content

Commit 3b51bfb

Browse files
committed
Add identity-api client and expand fetch permission rules
- Add identity.Client for device DID -> vehicle DID resolution (GraphQL) - Wire IdentityAPIURL in config, app (optional; device DIDs require it) - Allow fetch access with GetRawData OR (GetLocationHistory AND GetNonLocationHistory) - Helm: IDENTITY_API_URL in values.yaml (dev) and values-prod.yaml (prod) - Sample config: IDENTITY_API_URL in settings.sample.yaml and sample.env - Lint: handle httpResp.Body.Close() error in identity client
1 parent bca7b9d commit 3b51bfb

9 files changed

Lines changed: 322 additions & 68 deletions

File tree

charts/fetch-api/values-prod.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ env:
1414
PARQUET_BUCKET: dimo-iceberg-prod
1515
VEHICLE_NFT_ADDRESS: '0xbA5738a18d83D41847dfFbDC6101d37C69c9B0cF'
1616
CHAIN_ID: 137
17+
IDENTITY_API_URL: https://identity-api.dimo.zone/query
1718
ingress:
1819
enabled: true
1920
className: nginx

charts/fetch-api/values.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ env:
3737
S3_AWS_REGION: us-east-2
3838
VEHICLE_NFT_ADDRESS: '0x45fbCD3ef7361d156e8b16F5538AE36DEdf61Da8'
3939
CHAIN_ID: 80002
40+
IDENTITY_API_URL: https://identity-api.dev.dimo.zone/query
4041
service:
4142
type: ClusterIP
4243
ports:

internal/app/app.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/DIMO-Network/fetch-api/internal/config"
1313
"github.com/DIMO-Network/fetch-api/internal/fetch/rpc"
1414
"github.com/DIMO-Network/fetch-api/internal/graph"
15+
"github.com/DIMO-Network/fetch-api/internal/identity"
1516
"github.com/DIMO-Network/fetch-api/internal/limits"
1617
"github.com/DIMO-Network/fetch-api/pkg/eventrepo"
1718
fetchgrpc "github.com/DIMO-Network/fetch-api/pkg/grpc"
@@ -50,7 +51,12 @@ func New(settings config.Settings) (*App, error) {
5051
buckets := []string{settings.CloudEventBucket, settings.EphemeralBucket}
5152
eventService := eventrepo.New(chConn, s3Client, settings.ParquetBucket)
5253

53-
gqlSrv := newGraphQLHandler(&settings, eventService, buckets, chainID)
54+
var identityClient identity.Client
55+
if settings.IdentityAPIURL != "" {
56+
identityClient = identity.New(settings.IdentityAPIURL)
57+
}
58+
59+
gqlSrv := newGraphQLHandler(&settings, eventService, buckets, chainID, identityClient)
5460

5561
jwtMiddleware, err := auth.NewJWTMiddleware(settings.TokenExchangeIssuer, settings.TokenExchangeJWTKeySetURL)
5662
if err != nil {
@@ -92,12 +98,13 @@ func (a *App) Cleanup() {
9298
}
9399

94100
// newGraphQLHandler creates a configured gqlgen handler.Server.
95-
func newGraphQLHandler(settings *config.Settings, eventService *eventrepo.Service, buckets []string, chainID uint64) *handler.Server {
101+
func newGraphQLHandler(settings *config.Settings, eventService *eventrepo.Service, buckets []string, chainID uint64, identityClient identity.Client) *handler.Server {
96102
resolver := &graph.Resolver{
97-
EventService: eventService,
98-
Buckets: buckets,
99-
VehicleAddr: settings.VehicleNFTAddress,
100-
ChainID: chainID,
103+
EventService: eventService,
104+
Buckets: buckets,
105+
VehicleAddr: settings.VehicleNFTAddress,
106+
ChainID: chainID,
107+
IdentityClient: identityClient,
101108
}
102109

103110
cfg := graph.Config{Resolvers: resolver}

internal/config/settings.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ type Settings struct {
2424
S3AWSAccessKeyID string `yaml:"S3_AWS_ACCESS_KEY_ID"`
2525
S3AWSSecretAccessKey string `yaml:"S3_AWS_SECRET_ACCESS_KEY"`
2626
Clickhouse config.Settings `yaml:",inline"`
27+
IdentityAPIURL string `yaml:"IDENTITY_API_URL"`
2728
}

internal/graph/resolver.go

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/DIMO-Network/cloudevent"
99
"github.com/DIMO-Network/fetch-api/internal/graph/model"
10+
"github.com/DIMO-Network/fetch-api/internal/identity"
1011
"github.com/DIMO-Network/fetch-api/pkg/eventrepo"
1112
"github.com/DIMO-Network/fetch-api/pkg/grpc"
1213
"github.com/DIMO-Network/token-exchange-api/pkg/tokenclaims"
@@ -15,17 +16,17 @@ import (
1516

1617
// This file will not be regenerated automatically.
1718
//
18-
// It serves as dependency injection for your app, add any dependencies you require
19-
// here.
19+
// It serves as dependency injection for your app, add any dependencies you require here.
2020

2121
// ClaimsContextKey is the context key for token claims (set by JWT middleware).
2222
type ClaimsContextKey struct{}
2323

2424
type Resolver struct {
25-
EventService *eventrepo.Service
26-
Buckets []string
27-
VehicleAddr common.Address
28-
ChainID uint64
25+
EventService *eventrepo.Service
26+
Buckets []string
27+
VehicleAddr common.Address
28+
ChainID uint64
29+
IdentityClient identity.Client
2930
}
3031

3132
// CheckVehicleRawDataByDID returns an error if the context does not have claims or the token
@@ -34,27 +35,58 @@ func CheckVehicleRawDataByDID(ctx context.Context, did string) error {
3435
if _, err := cloudevent.DecodeERC721DID(did); err != nil {
3536
return fmt.Errorf("invalid DID: %w", err)
3637
}
38+
return checkRawDataPermissionsForVehicle(ctx, did)
39+
}
40+
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 {
3745
tok, _ := ctx.Value(ClaimsContextKey{}).(*tokenclaims.Token)
3846
if tok == nil {
3947
return fmt.Errorf("unauthorized: no token claims")
4048
}
41-
if tok.Asset != did {
49+
if tok.Asset != vehicleDID {
4250
return fmt.Errorf("unauthorized: token does not have access to this vehicle")
4351
}
44-
if !slices.Contains(tok.Permissions, tokenclaims.PermissionGetRawData) {
45-
return fmt.Errorf("unauthorized: missing GetRawData privilege")
52+
hasGetRawData := slices.Contains(tok.Permissions, tokenclaims.PermissionGetRawData)
53+
hasLocationHistory := slices.Contains(tok.Permissions, tokenclaims.PermissionGetLocationHistory)
54+
hasNonLocationHistory := slices.Contains(tok.Permissions, tokenclaims.PermissionGetNonLocationHistory)
55+
hasAllTimeData := hasLocationHistory && hasNonLocationHistory
56+
if !hasGetRawData && !hasAllTimeData {
57+
return fmt.Errorf("unauthorized: token does not have access to this vehicle")
4658
}
4759
return nil
4860
}
4961

50-
// requireVehicleOptsByDID validates access by DID and returns search options for the vehicle.
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.
5168
func (r *queryResolver) requireVehicleOptsByDID(ctx context.Context, did string, filter *model.CloudEventFilter) (*grpc.SearchOptions, error) {
52-
if err := CheckVehicleRawDataByDID(ctx, did); err != nil {
53-
return nil, err
54-
}
55-
subject, err := cloudevent.DecodeERC721DID(did)
69+
decoded, err := cloudevent.DecodeERC721DID(did)
5670
if err != nil {
5771
return nil, fmt.Errorf("invalid DID: %w", err)
5872
}
59-
return filterToSearchOptions(filter, subject), nil
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+
}
85+
}
86+
87+
if err := checkRawDataPermissionsForVehicle(ctx, vehicleDID); err != nil {
88+
return nil, err
89+
}
90+
91+
return filterToSearchOptions(filter, decoded), nil
6092
}

0 commit comments

Comments
 (0)