feat: add customer authentication and scoped RBAC

This commit is contained in:
lingniu
2026-07-16 13:58:28 +08:00
parent 6d6c9ce534
commit a1195fb97d
28 changed files with 1738 additions and 97 deletions

View File

@@ -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 {