feat: add customer authentication and scoped RBAC
This commit is contained in:
@@ -484,7 +484,11 @@ func matchesMonitorStatus(row VehicleRealtimeRow, requested string) bool {
|
||||
}
|
||||
|
||||
func (s *Service) MonitorSummary(ctx context.Context, query url.Values) (MonitorSummary, error) {
|
||||
vehicles, err := s.store.VehicleRealtime(ctx, normalizeMonitorQuery(query))
|
||||
scopedQuery, err := applyPrincipalVehicleScope(ctx, query)
|
||||
if err != nil {
|
||||
return MonitorSummary{}, err
|
||||
}
|
||||
vehicles, err := s.store.VehicleRealtime(ctx, normalizeMonitorQuery(scopedQuery))
|
||||
if err != nil {
|
||||
return MonitorSummary{}, err
|
||||
}
|
||||
@@ -538,7 +542,11 @@ func (s *Service) buildMonitorSummary(ctx context.Context, query url.Values, veh
|
||||
}
|
||||
|
||||
func (s *Service) MonitorWorkspace(ctx context.Context, query url.Values) (MonitorWorkspaceResponse, error) {
|
||||
vehicles, err := s.store.VehicleRealtime(ctx, normalizeMonitorQuery(query))
|
||||
scopedQuery, err := applyPrincipalVehicleScope(ctx, query)
|
||||
if err != nil {
|
||||
return MonitorWorkspaceResponse{}, err
|
||||
}
|
||||
vehicles, err := s.store.VehicleRealtime(ctx, normalizeMonitorQuery(scopedQuery))
|
||||
if err != nil {
|
||||
return MonitorWorkspaceResponse{}, err
|
||||
}
|
||||
@@ -566,7 +574,11 @@ func (s *Service) MonitorWorkspace(ctx context.Context, query url.Values) (Monit
|
||||
}
|
||||
|
||||
func (s *Service) MonitorMap(ctx context.Context, query url.Values) (MonitorMapResponse, error) {
|
||||
vehicles, err := s.store.VehicleRealtime(ctx, normalizeMonitorQuery(query))
|
||||
scopedQuery, err := applyPrincipalVehicleScope(ctx, query)
|
||||
if err != nil {
|
||||
return MonitorMapResponse{}, err
|
||||
}
|
||||
vehicles, err := s.store.VehicleRealtime(ctx, normalizeMonitorQuery(scopedQuery))
|
||||
if err != nil {
|
||||
return MonitorMapResponse{}, err
|
||||
}
|
||||
@@ -684,7 +696,11 @@ func buildMonitorMapResponse(vehicles Page[VehicleRealtimeRow], query url.Values
|
||||
}
|
||||
|
||||
func (s *Service) Vehicles(ctx context.Context, query url.Values) (Page[VehicleRow], error) {
|
||||
return s.store.Vehicles(ctx, query)
|
||||
scopedQuery, err := applyPrincipalVehicleScope(ctx, query)
|
||||
if err != nil {
|
||||
return Page[VehicleRow]{}, err
|
||||
}
|
||||
return s.store.Vehicles(ctx, scopedQuery)
|
||||
}
|
||||
|
||||
func (s *Service) ResolveVehicleIdentity(ctx context.Context, keyword string, protocol string) (VehicleIdentityResolution, error) {
|
||||
@@ -704,11 +720,19 @@ func (s *Service) ResolveVehicleIdentity(ctx context.Context, keyword string, pr
|
||||
}
|
||||
|
||||
func (s *Service) VehicleCoverage(ctx context.Context, query url.Values) (Page[VehicleCoverageRow], error) {
|
||||
return s.store.VehicleCoverage(ctx, query)
|
||||
scopedQuery, err := applyPrincipalVehicleScope(ctx, query)
|
||||
if err != nil {
|
||||
return Page[VehicleCoverageRow]{}, err
|
||||
}
|
||||
return s.store.VehicleCoverage(ctx, scopedQuery)
|
||||
}
|
||||
|
||||
func (s *Service) VehicleCoverageSummary(ctx context.Context, query url.Values) (VehicleCoverageSummary, error) {
|
||||
return s.store.VehicleCoverageSummary(ctx, query)
|
||||
scopedQuery, err := applyPrincipalVehicleScope(ctx, query)
|
||||
if err != nil {
|
||||
return VehicleCoverageSummary{}, err
|
||||
}
|
||||
return s.store.VehicleCoverageSummary(ctx, scopedQuery)
|
||||
}
|
||||
|
||||
func (s *Service) VehicleServiceSummary(ctx context.Context) (VehicleServiceSummary, error) {
|
||||
@@ -743,6 +767,15 @@ func (s *Service) VehicleRealtime(ctx context.Context, query url.Values) (Page[V
|
||||
func (s *Service) VehicleServiceOverview(ctx context.Context, keyword string, protocol string) (VehicleServiceOverview, error) {
|
||||
keyword = strings.TrimSpace(keyword)
|
||||
protocol = strings.TrimSpace(protocol)
|
||||
if principal, ok := PrincipalFromContext(ctx); ok && principal.UserType == "customer" {
|
||||
resolvedScopeVIN, err := s.resolveVehicleVIN(ctx, keyword, protocol)
|
||||
if err != nil {
|
||||
return VehicleServiceOverview{}, err
|
||||
}
|
||||
if err := authorizeVehicleVIN(ctx, resolvedScopeVIN); err != nil {
|
||||
return VehicleServiceOverview{}, err
|
||||
}
|
||||
}
|
||||
if batchStore, ok := s.store.(VehicleOverviewBatchStore); ok {
|
||||
page, err := batchStore.VehicleServiceOverviews(ctx, VehicleOverviewBatchQuery{
|
||||
Keywords: []string{keyword},
|
||||
@@ -827,6 +860,10 @@ func (s *Service) VehicleDetail(ctx context.Context, vin string, protocol string
|
||||
keyword := strings.TrimSpace(vin)
|
||||
protocol = strings.TrimSpace(protocol)
|
||||
vehicleQuery := url.Values{"keyword": {keyword}, "limit": {"10"}}
|
||||
vehicleQuery, err := applyPrincipalVehicleScope(ctx, vehicleQuery)
|
||||
if err != nil {
|
||||
return VehicleDetail{}, err
|
||||
}
|
||||
vehicles, err := s.store.Vehicles(ctx, vehicleQuery)
|
||||
if err != nil {
|
||||
return VehicleDetail{}, err
|
||||
@@ -843,6 +880,9 @@ func (s *Service) VehicleDetail(ctx context.Context, vin string, protocol string
|
||||
if queryVIN == "" {
|
||||
queryVIN = keyword
|
||||
}
|
||||
if err := authorizeVehicleVIN(ctx, queryVIN); err != nil {
|
||||
return VehicleDetail{}, err
|
||||
}
|
||||
if resolvedVIN == "" {
|
||||
qualityQuery := url.Values{"keyword": {keyword}, "limit": {"20"}}
|
||||
if protocol != "" {
|
||||
@@ -1495,6 +1535,9 @@ func (s *Service) LatestTelemetry(ctx context.Context, vehicleKey string) (Lates
|
||||
if err != nil {
|
||||
return LatestTelemetryResponse{}, err
|
||||
}
|
||||
if err := authorizeVehicleVIN(ctx, resolvedVIN); err != nil {
|
||||
return LatestTelemetryResponse{}, err
|
||||
}
|
||||
rawResult := make(chan latestTelemetryRawResult, len(canonicalVehicleProtocols))
|
||||
catalogResult := make(chan latestTelemetryCatalogResult, 1)
|
||||
for _, protocol := range canonicalVehicleProtocols {
|
||||
@@ -3616,7 +3659,10 @@ func cloneStringMap(values map[string]string) map[string]string {
|
||||
}
|
||||
|
||||
func (s *Service) resolveVehicleQuery(ctx context.Context, query url.Values) (url.Values, error) {
|
||||
resolved := cloneValues(query)
|
||||
resolved, err := applyPrincipalVehicleScope(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(resolved.Get("keywords")) != "" {
|
||||
return resolved, nil
|
||||
}
|
||||
@@ -3629,6 +3675,9 @@ func (s *Service) resolveVehicleQuery(ctx context.Context, query url.Values) (ur
|
||||
return nil, err
|
||||
}
|
||||
if resolvedVIN != "" {
|
||||
if principal, ok := PrincipalFromContext(ctx); ok && principal.UserType == "customer" && !principal.CanVIN(resolvedVIN) {
|
||||
return nil, clientError{Code: "VEHICLE_PERMISSION_DENIED", Message: "当前账号无权查看该车辆"}
|
||||
}
|
||||
resolved.Set("vin", resolvedVIN)
|
||||
}
|
||||
return resolved, nil
|
||||
@@ -3640,6 +3689,10 @@ func (s *Service) resolveVehicleVIN(ctx context.Context, keyword string, protoco
|
||||
return keyword, nil
|
||||
}
|
||||
vehicleQuery := url.Values{"keyword": {keyword}, "limit": {"10"}}
|
||||
vehicleQuery, err := applyPrincipalVehicleScope(ctx, vehicleQuery)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if protocol = strings.TrimSpace(protocol); protocol != "" {
|
||||
vehicleQuery.Set("protocol", protocol)
|
||||
}
|
||||
@@ -3654,6 +3707,44 @@ func (s *Service) resolveVehicleVIN(ctx context.Context, keyword string, protoco
|
||||
return identity.VIN, nil
|
||||
}
|
||||
|
||||
func applyPrincipalVehicleScope(ctx context.Context, query url.Values) (url.Values, error) {
|
||||
next := cloneValues(query)
|
||||
principal, ok := PrincipalFromContext(ctx)
|
||||
if !ok || principal.UserType != "customer" {
|
||||
return next, nil
|
||||
}
|
||||
allowed := make(map[string]bool, len(principal.VehicleVINs))
|
||||
for _, vin := range principal.VehicleVINs {
|
||||
allowed[strings.ToUpper(strings.TrimSpace(vin))] = true
|
||||
}
|
||||
for _, value := range []string{next.Get("vin"), next.Get("vins")} {
|
||||
for _, vin := range strings.Split(value, ",") {
|
||||
vin = strings.ToUpper(strings.TrimSpace(vin))
|
||||
if vin != "" && isLikelyVIN(vin) && !allowed[vin] {
|
||||
return nil, clientError{Code: "VEHICLE_PERMISSION_DENIED", Message: "当前账号无权查看该车辆"}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(principal.VehicleVINs) == 0 {
|
||||
next.Set("scopeVins", "__NO_VEHICLE_SCOPE__")
|
||||
} else {
|
||||
next.Set("scopeVins", strings.Join(principal.VehicleVINs, ","))
|
||||
}
|
||||
return next, nil
|
||||
}
|
||||
|
||||
func authorizeVehicleVIN(ctx context.Context, vin string) error {
|
||||
principal, ok := PrincipalFromContext(ctx)
|
||||
if !ok || principal.UserType != "customer" {
|
||||
return nil
|
||||
}
|
||||
vin = strings.ToUpper(strings.TrimSpace(vin))
|
||||
if vin == "" || !principal.CanVIN(vin) {
|
||||
return clientError{Code: "VEHICLE_PERMISSION_DENIED", Message: "当前账号无权查看该车辆"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cloneValues(values url.Values) url.Values {
|
||||
cloned := make(url.Values, len(values))
|
||||
for key, current := range values {
|
||||
|
||||
Reference in New Issue
Block a user