feat(auth): enforce vehicle grant time boundaries

This commit is contained in:
lingniu
2026-07-16 15:44:29 +08:00
parent a541d10c7b
commit 4688abadef
11 changed files with 580 additions and 35 deletions

View File

@@ -920,6 +920,16 @@ func (s *Service) VehicleDetail(ctx context.Context, vin string, protocol string
rawQuery.Protocol = protocol
qualityQuery.Set("protocol", protocol)
}
historyQuery, err = applyPrincipalHistoryTimeScope(ctx, queryVIN, historyQuery)
if err != nil {
return VehicleDetail{}, err
}
rawQuery.DateFrom = historyQuery.Get("dateFrom")
rawQuery.DateTo = historyQuery.Get("dateTo")
mileageQuery, err = applyPrincipalVehicleScope(ctx, mileageQuery)
if err != nil {
return VehicleDetail{}, err
}
realtimeSummary, err := s.store.VehicleRealtime(ctx, url.Values{"vin": {queryVIN}, "limit": {"1"}})
if err != nil {
return VehicleDetail{}, err
@@ -1273,20 +1283,21 @@ func coverageStatus(sourceCount int, onlineSourceCount int) string {
}
func (s *Service) enrichVehicleSourceStatus(ctx context.Context, vin string, statuses []VehicleSourceStatus) []VehicleSourceStatus {
scopedWindow, scopeErr := applyPrincipalHistoryTimeScope(ctx, vin, url.Values{})
for index := range statuses {
protocol := statuses[index].Protocol
if protocol == "" {
continue
}
if !statuses[index].HasHistory {
query := url.Values{"vin": {vin}, "protocol": {protocol}, "limit": {"1"}}
if !statuses[index].HasHistory && scopeErr == nil {
query := url.Values{"vin": {vin}, "protocol": {protocol}, "dateFrom": {scopedWindow.Get("dateFrom")}, "limit": {"1"}}
if page, err := s.store.HistoryLocationsFromTDengine(ctx, query); err == nil && len(page.Items) > 0 {
statuses[index].HasHistory = true
statuses[index].LastSeen = latestString(statuses[index].LastSeen, firstNonEmpty(page.Items[0].ServerTime, page.Items[0].DeviceTime))
}
}
if !statuses[index].HasRaw {
page, err := s.store.RawFrames(ctx, RawFrameQuery{VIN: vin, Protocol: protocol, Limit: 1})
if !statuses[index].HasRaw && scopeErr == nil {
page, err := s.store.RawFrames(ctx, RawFrameQuery{VIN: vin, Protocol: protocol, DateFrom: scopedWindow.Get("dateFrom"), Limit: 1})
if err == nil && len(page.Items) > 0 {
statuses[index].HasRaw = true
statuses[index].LastSeen = latestString(statuses[index].LastSeen, firstNonEmpty(page.Items[0].ServerTime, page.Items[0].DeviceTime))
@@ -1347,6 +1358,10 @@ func (s *Service) HistoryLocations(ctx context.Context, query url.Values) (Page[
if err != nil {
return Page[HistoryLocationRow]{}, err
}
resolvedQuery, err = applyPrincipalHistoryTimeScope(ctx, resolvedQuery.Get("vin"), resolvedQuery)
if err != nil {
return Page[HistoryLocationRow]{}, err
}
return s.store.HistoryLocationsFromTDengine(ctx, resolvedQuery)
}
@@ -1369,6 +1384,10 @@ func (s *Service) TrackPlayback(ctx context.Context, query url.Values) (TrackPla
if err != nil {
return TrackPlaybackResponse{}, err
}
resolvedQuery, err = applyPrincipalHistoryTimeScope(ctx, resolvedQuery.Get("vin"), resolvedQuery)
if err != nil {
return TrackPlaybackResponse{}, err
}
resolvedQuery.Set("limit", "5000")
resolvedQuery.Set("offset", "0")
page, err := s.store.HistoryLocationsFromTDengine(ctx, resolvedQuery)
@@ -1401,7 +1420,7 @@ func (s *Service) TrackPlayback(ctx context.Context, query url.Values) (TrackPla
Quality: quality,
AsOf: time.Now().UTC().Format(time.RFC3339),
}
result.Coverage = trackCoverage(query, page.Total, len(rawPoints), len(points), 0, result.Truncated, false)
result.Coverage = trackCoverage(resolvedQuery, page.Total, len(rawPoints), len(points), 0, result.Truncated, false)
applyTrackPrimarySourceCoverage(&result.Coverage, selectedProtocol, alternateSourcePoints)
if len(points) == 0 {
return result, nil
@@ -1440,7 +1459,7 @@ func (s *Service) TrackPlayback(ctx context.Context, query url.Values) (TrackPla
for index := range result.Stops {
result.Stops[index].SampledIndex = nearestSampledIndex((result.Stops[index].startIndex+result.Stops[index].endIndex)/2, sampledIndexes)
}
result.Coverage = trackCoverage(query, page.Total, len(rawPoints), len(points), len(result.Points), result.Truncated, result.Sampled)
result.Coverage = trackCoverage(resolvedQuery, page.Total, len(rawPoints), len(points), len(result.Points), result.Truncated, result.Sampled)
applyTrackPrimarySourceCoverage(&result.Coverage, selectedProtocol, alternateSourcePoints)
result.Coverage.ActualStart = result.Summary.StartTime
result.Coverage.ActualEnd = result.Summary.EndTime
@@ -1538,11 +1557,15 @@ func (s *Service) LatestTelemetry(ctx context.Context, vehicleKey string) (Lates
if err := authorizeVehicleVIN(ctx, resolvedVIN); err != nil {
return LatestTelemetryResponse{}, err
}
scopedWindow, err := applyPrincipalHistoryTimeScope(ctx, resolvedVIN, url.Values{})
if err != nil {
return LatestTelemetryResponse{}, err
}
rawResult := make(chan latestTelemetryRawResult, len(canonicalVehicleProtocols))
catalogResult := make(chan latestTelemetryCatalogResult, 1)
for _, protocol := range canonicalVehicleProtocols {
go func(protocol string) {
page, queryErr := s.store.RawFrames(ctx, RawFrameQuery{VIN: resolvedVIN, Protocol: protocol, IncludeFields: true, Limit: 5, SkipCount: true})
page, queryErr := s.store.RawFrames(ctx, RawFrameQuery{VIN: resolvedVIN, Protocol: protocol, DateFrom: scopedWindow.Get("dateFrom"), DateTo: scopedWindow.Get("dateTo"), IncludeFields: true, Limit: 5, SkipCount: true})
rawResult <- latestTelemetryRawResult{page: page, err: queryErr}
}(protocol)
}
@@ -1981,7 +2004,17 @@ func (s *Service) HistorySeries(ctx context.Context, query url.Values) (HistoryS
if resolveErr != nil {
return HistorySeriesResponse{}, resolveErr
}
buckets, aggregateErr := store.HistoryLocationSeries(ctx, HistoryLocationSeriesQuery{VIN: vin, Protocol: protocol, DateFrom: dateFrom, DateTo: dateTo, GrainSeconds: grainSeconds})
if err := authorizeVehicleVIN(ctx, vin); err != nil {
return HistorySeriesResponse{}, err
}
scopedWindow, scopeErr := applyPrincipalHistoryTimeScope(ctx, vin, url.Values{
"dateFrom": {dateFrom},
"dateTo": {dateTo},
})
if scopeErr != nil {
return HistorySeriesResponse{}, scopeErr
}
buckets, aggregateErr := store.HistoryLocationSeries(ctx, HistoryLocationSeriesQuery{VIN: vin, Protocol: protocol, DateFrom: scopedWindow.Get("dateFrom"), DateTo: scopedWindow.Get("dateTo"), GrainSeconds: grainSeconds})
if aggregateErr != nil {
return HistorySeriesResponse{}, aggregateErr
}
@@ -2132,6 +2165,10 @@ func (s *Service) historyDataForVehicle(ctx context.Context, category, keyword s
if err != nil {
return nil, 0, err
}
resolvedQuery, err = applyPrincipalHistoryTimeScope(ctx, resolvedQuery.Get("vin"), resolvedQuery)
if err != nil {
return nil, 0, err
}
page, err := s.store.HistoryLocationsFromTDengine(ctx, resolvedQuery)
if err != nil {
return nil, 0, err
@@ -2150,7 +2187,7 @@ func (s *Service) historyDataForVehicle(ctx context.Context, category, keyword s
if err != nil {
return nil, 0, err
}
page, err := s.store.RawFrames(ctx, RawFrameQuery{VIN: resolvedVIN, Protocol: protocol, DateFrom: query.Get("dateFrom"), DateTo: query.Get("dateTo"), IncludeFields: true, Limit: fetchLimit})
page, err := s.RawFrames(ctx, RawFrameQuery{VIN: resolvedVIN, Protocol: protocol, DateFrom: query.Get("dateFrom"), DateTo: query.Get("dateTo"), IncludeFields: true, Limit: fetchLimit})
if err != nil {
return nil, 0, err
}
@@ -2170,6 +2207,9 @@ func (s *Service) historyDataForVehicle(ctx context.Context, category, keyword s
if err != nil {
return nil, 0, err
}
if err := requirePrincipalHistoricalGrantScope(ctx, resolvedQuery); err != nil {
return nil, 0, err
}
page, err := s.store.DailyMileage(ctx, resolvedQuery)
if err != nil {
return nil, 0, err
@@ -3195,6 +3235,18 @@ func (s *Service) RawFrames(ctx context.Context, query RawFrameQuery) (Page[RawF
if resolvedVIN != "" {
query.VIN = resolvedVIN
}
if err := authorizeVehicleVIN(ctx, query.VIN); err != nil {
return Page[RawFrameRow]{}, err
}
scoped, err := applyPrincipalHistoryTimeScope(ctx, query.VIN, url.Values{
"dateFrom": {query.DateFrom},
"dateTo": {query.DateTo},
})
if err != nil {
return Page[RawFrameRow]{}, err
}
query.DateFrom = scoped.Get("dateFrom")
query.DateTo = scoped.Get("dateTo")
return s.store.RawFrames(ctx, query)
}
@@ -3211,6 +3263,9 @@ func (s *Service) MileageSummary(ctx context.Context, query url.Values) (Mileage
if err != nil {
return MileageSummary{}, err
}
if err := requirePrincipalHistoricalGrantScope(ctx, resolvedQuery); err != nil {
return MileageSummary{}, err
}
return s.store.MileageSummary(ctx, resolvedQuery)
}
@@ -3219,6 +3274,9 @@ func (s *Service) DailyMileage(ctx context.Context, query url.Values) (Page[Dail
if err != nil {
return Page[DailyMileageRow]{}, err
}
if err := requirePrincipalHistoricalGrantScope(ctx, resolvedQuery); err != nil {
return Page[DailyMileageRow]{}, err
}
resolvedQuery, err = normalizeMileageVINSelection(resolvedQuery)
if err != nil {
return Page[DailyMileageRow]{}, err
@@ -3235,6 +3293,9 @@ func (s *Service) MileageStatistics(ctx context.Context, query url.Values) (Mile
if err != nil {
return MileageStatistics{}, err
}
if err := requirePrincipalHistoricalGrantScope(ctx, resolvedQuery); err != nil {
return MileageStatistics{}, err
}
resolvedQuery, err = normalizeMileageVINSelection(resolvedQuery)
if err != nil {
return MileageStatistics{}, err
@@ -3730,9 +3791,99 @@ func applyPrincipalVehicleScope(ctx context.Context, query url.Values) (url.Valu
} else {
next.Set("scopeVins", strings.Join(principal.VehicleVINs, ","))
}
validFromByVIN := make(map[string]string, len(principal.VehicleGrants))
shanghai := time.FixedZone("Asia/Shanghai", 8*60*60)
for _, grant := range principal.VehicleGrants {
vin := strings.ToUpper(strings.TrimSpace(grant.VIN))
if vin == "" || grant.ValidFrom.IsZero() {
continue
}
validFrom := grant.ValidFrom.In(shanghai)
firstFullDay := time.Date(validFrom.Year(), validFrom.Month(), validFrom.Day(), 0, 0, 0, 0, shanghai)
if !validFrom.Equal(firstFullDay) {
firstFullDay = firstFullDay.AddDate(0, 0, 1)
}
validFromByVIN[vin] = firstFullDay.Format("2006-01-02")
}
encoded, err := json.Marshal(validFromByVIN)
if err != nil {
return nil, err
}
next.Set("scopeVehicleFrom", string(encoded))
return next, nil
}
func applyPrincipalHistoryTimeScope(ctx context.Context, vin string, query url.Values) (url.Values, error) {
next := cloneValues(query)
principal, ok := PrincipalFromContext(ctx)
if !ok || principal.UserType != "customer" {
return next, nil
}
grant, ok := principal.VehicleGrant(vin)
if !ok || grant.ValidFrom.IsZero() {
return nil, clientError{Code: "HISTORY_SCOPE_UNAVAILABLE", Message: "当前车辆缺少可验证的历史授权起始时间"}
}
cutoff := grant.ValidFrom
var requestedStart time.Time
if value := strings.TrimSpace(next.Get("dateTo")); value != "" {
end, parsed := parseTrackRequestTime(value)
if !parsed {
return nil, clientError{Code: "TRACK_TIME_INVALID", Message: "轨迹时间格式无效"}
}
if !end.After(cutoff) {
return nil, clientError{Code: "HISTORY_BEFORE_AUTHORIZATION", Message: "所选时间范围早于该车辆的授权开始时间"}
}
}
if value := strings.TrimSpace(next.Get("dateFrom")); value != "" {
start, parsed := parseTrackRequestTime(value)
if !parsed {
return nil, clientError{Code: "TRACK_TIME_INVALID", Message: "轨迹时间格式无效"}
}
requestedStart = start
}
if grant.ValidTo != nil && !grant.ValidTo.IsZero() {
if !requestedStart.IsZero() && !requestedStart.Before(*grant.ValidTo) {
return nil, clientError{Code: "HISTORY_AFTER_AUTHORIZATION", Message: "所选时间范围晚于该车辆的授权结束时间"}
}
if value := strings.TrimSpace(next.Get("dateTo")); value == "" {
next.Set("dateTo", grant.ValidTo.Format(time.RFC3339))
} else if end, _ := parseTrackRequestTime(value); end.After(*grant.ValidTo) {
next.Set("dateTo", grant.ValidTo.Format(time.RFC3339))
}
}
if !requestedStart.IsZero() && !requestedStart.Before(cutoff) {
return next, nil
}
next.Set("dateFrom", cutoff.Format(time.RFC3339))
return next, nil
}
func requirePrincipalHistoricalGrantScope(ctx context.Context, query url.Values) error {
principal, ok := PrincipalFromContext(ctx)
if !ok || principal.UserType != "customer" {
return nil
}
requested := make([]string, 0)
for _, raw := range []string{query.Get("vin"), query.Get("vins")} {
for _, vin := range strings.Split(raw, ",") {
vin = strings.ToUpper(strings.TrimSpace(vin))
if vin != "" {
requested = append(requested, vin)
}
}
}
if len(requested) == 0 {
requested = principal.VehicleVINs
}
for _, vin := range requested {
grant, exists := principal.VehicleGrant(vin)
if !exists || grant.ValidFrom.IsZero() {
return clientError{Code: "HISTORY_SCOPE_UNAVAILABLE", Message: "当前车辆缺少可验证的历史授权起始时间"}
}
}
return nil
}
func authorizeVehicleVIN(ctx context.Context, vin string) error {
principal, ok := PrincipalFromContext(ctx)
if !ok || principal.UserType != "customer" {