Add monitoring metrics, numeric sorting, report-time display, vehicle number labels, and map layout fixes. Include accumulated mileage, hydrogen, OneOS authentication, open platform, ingestion, deployment, and UI updates. Exclude temporary files, generated reports, and credentials. Validate frontend 632 tests and production build, API packages, vehicle-map tests, and release installer tests. Gateway suite has one pre-existing date-sensitive identity throttling test failure reproduced on the parent commit.
481 lines
20 KiB
Go
481 lines
20 KiB
Go
package app
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"lingniu/vehicle-data-platform/apps/api/internal/config"
|
|
"lingniu/vehicle-data-platform/apps/api/internal/httpx"
|
|
"lingniu/vehicle-data-platform/apps/api/internal/platform"
|
|
)
|
|
|
|
type configuredPrincipal struct {
|
|
Token string `json:"token"`
|
|
Name string `json:"name"`
|
|
Role string `json:"role"`
|
|
}
|
|
|
|
type tokenPrincipal struct {
|
|
hash [sha256.Size]byte
|
|
principal platform.Principal
|
|
}
|
|
|
|
type apiAuthenticator struct {
|
|
mode string
|
|
tokens []tokenPrincipal
|
|
local *authStore
|
|
profile *oneOSProfileAuth
|
|
oneOS *oneOSIdentityAdapter
|
|
demo *demoAuthDirectory
|
|
localBackend bool
|
|
adapters []IdentityAdapter
|
|
}
|
|
|
|
func withAPIAuth(next http.Handler, cfg config.Config, databases ...*sql.DB) http.Handler {
|
|
var db *sql.DB
|
|
if len(databases) > 0 {
|
|
db = databases[0]
|
|
}
|
|
authenticator, err := newAPIAuthenticator(cfg, db)
|
|
if err != nil {
|
|
log.Printf("platform API authentication misconfigured: %v", err)
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
httpx.WriteError(w, http.StatusServiceUnavailable, "AUTH_CONFIG_INVALID", "平台鉴权配置无效", err.Error(), requestTraceID(r))
|
|
})
|
|
}
|
|
return authenticator.middleware(next)
|
|
}
|
|
|
|
func newAPIAuthenticator(cfg config.Config, db *sql.DB) (*apiAuthenticator, error) {
|
|
mode := strings.ToLower(strings.TrimSpace(cfg.AuthMode))
|
|
if mode == "" {
|
|
mode = "disabled"
|
|
}
|
|
if mode != "disabled" && mode != "enforce" {
|
|
return nil, fmt.Errorf("AUTH_MODE must be disabled or enforce")
|
|
}
|
|
local, err := newAuthStore(db, cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
legacyConfig := cfg
|
|
if cfg.OneOSAuthEnabled {
|
|
legacyConfig.OneOSSSOEnabled = false
|
|
}
|
|
oneOS, err := newOneOSIdentityAdapter(legacyConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if oneOS != nil && local == nil {
|
|
return nil, fmt.Errorf("ONEOS_SSO_ENABLED requires MYSQL_DSN and the platform identity schema")
|
|
}
|
|
authenticator := &apiAuthenticator{mode: mode, local: local, oneOS: oneOS, localBackend: cfg.OneOSLocalBackend}
|
|
if cfg.OneOSAuthEnabled {
|
|
localMock := (cfg.OneOSLocalOnly || cfg.OneOSDevIntegration) && strings.EqualFold(cfg.DataMode, "mock")
|
|
if mode != "enforce" || (local == nil && !localMock) {
|
|
return nil, fmt.Errorf("OneOS auth requires AUTH_MODE=enforce and identity DB or explicit isolated local mock")
|
|
}
|
|
if !validOneOSOrigin(cfg.OneOSPublicOrigin, cfg.OneOSLocalOnly || cfg.OneOSDevIntegration) {
|
|
return nil, fmt.Errorf("ONEOS_PUBLIC_ORIGIN must be a trusted HTTPS origin or explicit local loopback origin")
|
|
}
|
|
client, e := newOneOSProfileClient(cfg)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
authenticator.profile = &oneOSProfileAuth{localBackend: cfg.OneOSLocalBackend, vehicleQueryScope: cfg.OneOSVehicleQueryScope, devIntegration: cfg.OneOSDevIntegration, devDataProxy: cfg.OneOSDevDataProxyURL != "", client: client, store: local, origin: strings.TrimRight(cfg.OneOSPublicOrigin, "/"), issuer: cfg.OneOSProfileIssuer, tenant: cfg.OneOSProfileAllowedTenant, passwordEnabled: cfg.OneOSPasswordEnabled, scopeEnabled: cfg.OneOSVehicleScopePath != "" || cfg.OneOSVehicleQueryScope, localOnly: localMock, ssoEnabled: localMock && cfg.OneOSLoginTicketExchangePath != ""}
|
|
}
|
|
if mode == "disabled" && local == nil && strings.EqualFold(strings.TrimSpace(cfg.DataMode), "mock") {
|
|
authenticator.demo = newDemoAuthDirectory()
|
|
}
|
|
configured := []configuredPrincipal{}
|
|
if strings.TrimSpace(cfg.AuthTokensJSON) != "" {
|
|
if err := json.Unmarshal([]byte(cfg.AuthTokensJSON), &configured); err != nil {
|
|
return nil, fmt.Errorf("decode AUTH_TOKENS_JSON: %w", err)
|
|
}
|
|
}
|
|
if token := strings.TrimSpace(cfg.AuthToken); token != "" {
|
|
configured = append(configured, configuredPrincipal{Token: token, Name: "platform-admin", Role: "admin"})
|
|
}
|
|
seen := map[[sha256.Size]byte]bool{}
|
|
for _, item := range configured {
|
|
item.Token = strings.TrimSpace(item.Token)
|
|
item.Name = strings.TrimSpace(item.Name)
|
|
item.Role = strings.ToLower(strings.TrimSpace(item.Role))
|
|
if len(item.Token) < 16 {
|
|
return nil, fmt.Errorf("token for %q must contain at least 16 characters", item.Name)
|
|
}
|
|
if item.Name == "" || roleRank(item.Role) == 0 {
|
|
return nil, fmt.Errorf("token principal requires name and viewer/operator/admin role")
|
|
}
|
|
hash := sha256.Sum256([]byte(item.Token))
|
|
if seen[hash] {
|
|
return nil, fmt.Errorf("duplicate authentication token")
|
|
}
|
|
seen[hash] = true
|
|
principal := platform.Principal{Name: item.Name, Username: item.Name, Role: item.Role, UserType: item.Role, AuthProvider: "legacy-token", MenuKeys: append([]string(nil), adminMenus...)}
|
|
authenticator.tokens = append(authenticator.tokens, tokenPrincipal{hash: hash, principal: principal})
|
|
}
|
|
if mode == "enforce" && len(authenticator.tokens) == 0 && authenticator.local == nil && authenticator.profile == nil && len(authenticator.adapters) == 0 {
|
|
return nil, fmt.Errorf("enforce mode requires a local identity store, identity adapter, AUTH_TOKEN, or AUTH_TOKENS_JSON")
|
|
}
|
|
return authenticator, nil
|
|
}
|
|
|
|
func (a *apiAuthenticator) middleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/api/v2/auth/providers" {
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
if r.Method != http.MethodGet {
|
|
oneOSAuthError(w, r, 405, "METHOD_NOT_ALLOWED")
|
|
return
|
|
}
|
|
httpx.WriteOK(w, requestTraceID(r), map[string]any{"oneos": map[string]bool{"enabled": a.profile != nil, "devIntegration": a.profile != nil && a.profile.devIntegration, "realDataEnabled": a.profile != nil && (a.profile.devDataProxy || a.profile.localBackend), "passwordEnabled": a.profile != nil && a.profile.passwordEnabled, "authorizationReady": a.profile != nil && a.profile.scopeEnabled, "ssoEnabled": a.profile != nil && a.profile.ssoEnabled}, "local": map[string]bool{"enabled": a.local != nil}})
|
|
return
|
|
}
|
|
if a.profile != nil && a.profile.handle(w, r) {
|
|
return
|
|
}
|
|
if r.URL.Path == "/api/v2/auth/oneos/exchange" {
|
|
if r.Method != http.MethodPost {
|
|
httpx.WriteError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "OneOS 登录兑换接口仅支持 POST", "", requestTraceID(r))
|
|
return
|
|
}
|
|
if a.oneOS == nil || a.local == nil {
|
|
httpx.WriteError(w, http.StatusServiceUnavailable, "ONEOS_SSO_UNAVAILABLE", "OneOS 单点登录尚未启用", "", requestTraceID(r))
|
|
return
|
|
}
|
|
a.local.exchangeOneOSTicket(w, r, a.oneOS)
|
|
return
|
|
}
|
|
if r.URL.Path == "/api/v2/auth/login" {
|
|
if a.local == nil {
|
|
httpx.WriteError(w, http.StatusServiceUnavailable, "LOCAL_AUTH_UNAVAILABLE", "账号登录尚未启用", "", requestTraceID(r))
|
|
return
|
|
}
|
|
if r.Method != http.MethodPost {
|
|
httpx.WriteError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "登录接口仅支持 POST", "", requestTraceID(r))
|
|
return
|
|
}
|
|
a.local.login(w, r)
|
|
return
|
|
}
|
|
// This route resolves the caller's own provider; request parameters never select it.
|
|
if r.URL.Path == "/api/v2/auth/vehicle-scope" {
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
if r.Method != http.MethodGet {
|
|
oneOSAuthError(w, r, 405, "METHOD_NOT_ALLOWED")
|
|
return
|
|
}
|
|
if a.profile != nil && r.Header.Get("X-OneOS-Client-ID") != "" && a.profile.vehicleQueryScope {
|
|
token, clientID := bearerToken(r), r.Header.Get("X-OneOS-Client-ID")
|
|
current, err := a.profile.client.CurrentUser(r.Context(), token, clientID)
|
|
if err != nil || !a.profile.validProfile(current.Profile) {
|
|
oneOSAuthError(w, r, 401, "AUTH_REQUIRED")
|
|
return
|
|
}
|
|
p, _, err := a.profile.resolveVehicleQueryScope(r.Context(), token, clientID, current)
|
|
if err != nil {
|
|
httpx.WriteError(w, 503, "VEHICLE_SCOPE_UNAVAILABLE", "车辆权限暂时加载失败,请重试", "", requestTraceID(r))
|
|
return
|
|
}
|
|
httpx.WriteOK(w, requestTraceID(r), struct {
|
|
platform.Principal
|
|
AuthMode string `json:"authMode"`
|
|
}{p, a.mode})
|
|
return
|
|
}
|
|
p, ok := a.authenticate(r)
|
|
if !ok {
|
|
oneOSAuthError(w, r, 401, "AUTH_REQUIRED")
|
|
return
|
|
}
|
|
httpx.WriteOK(w, requestTraceID(r), struct {
|
|
platform.Principal
|
|
AuthMode string `json:"authMode"`
|
|
}{p, a.mode})
|
|
return
|
|
}
|
|
principal, ok := a.authenticate(r)
|
|
if !ok {
|
|
w.Header().Set("WWW-Authenticate", `Bearer realm="lingniu-vehicle-platform"`)
|
|
httpx.WriteError(w, http.StatusUnauthorized, "AUTH_REQUIRED", "需要有效的访问令牌", "", requestTraceID(r))
|
|
return
|
|
}
|
|
if a.profile != nil && r.Header.Get("X-OneOS-Client-ID") != "" {
|
|
limited := principal.AuthorizationStatus == "pending" || (a.profile.devIntegration && principal.AuthorizationStatus == "asset-catalog")
|
|
identityRoute := (r.Method == http.MethodGet && (r.URL.Path == "/api/v2/session" || r.URL.Path == "/api/v2/auth/oneos/me")) || (r.Method == http.MethodPost && r.URL.Path == "/api/v2/auth/logout")
|
|
catalogRoute := principal.AuthorizationStatus != "pending" && r.Method == http.MethodGet && r.URL.Path == "/api/v2/auth/oneos/vehicles" && (principal.AuthorizationStatus == "asset-catalog" || (a.profile.vehicleQueryScope || a.profile.devDataProxy && principal.Role == "admin"))
|
|
action := oneOSReadAction(r)
|
|
admin := oneOSHasAdminPermission(principal.PermissionKeys) && principal.Role == "admin"
|
|
if limited && admin && !identityRoute && !catalogRoute {
|
|
httpx.WriteError(w, http.StatusServiceUnavailable, "ONEOS_DATA_UNAVAILABLE", "管理员权限已开通,本地开发环境尚未连接中台业务数据源", "", requestTraceID(r))
|
|
return
|
|
}
|
|
if (limited && !identityRoute && !catalogRoute) || (!limited && !admin && !identityRoute && !catalogRoute && (action == "" || !oneOSPermissions(principal.PermissionKeys)[action])) {
|
|
oneOSAuthError(w, r, 403, "ONEOS_PERMISSION_DENIED")
|
|
return
|
|
}
|
|
if catalogRoute {
|
|
page, err := a.profile.client.VehicleAssetPage(r.Context(), bearerToken(r), r.Header.Get("X-OneOS-Client-ID"), r.URL.Query())
|
|
if err != nil {
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
if errors.Is(err, errOneOSAssetQuery) {
|
|
httpx.WriteError(w, http.StatusBadRequest, "ONEOS_CATALOG_QUERY_INVALID", "车辆目录查询参数无效,请检查筛选条件及分页范围", "", requestTraceID(r))
|
|
} else {
|
|
httpx.WriteError(w, http.StatusBadGateway, "ONEOS_CATALOG_UNAVAILABLE", "OneOS 车辆目录暂不可用,请稍后重试", "", requestTraceID(r))
|
|
}
|
|
return
|
|
}
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
httpx.WriteOK(w, requestTraceID(r), map[string]any{"records": page.Records, "total": page.Total, "current": page.Current, "size": page.Size, "telemetryAvailable": false, "historyAvailable": false, "source": "oneos-asset-catalog"})
|
|
return
|
|
}
|
|
}
|
|
if principal.UserType == "customer" && r.URL.Path != "/api/v2/session" && r.URL.Path != "/api/v2/auth/logout" && r.URL.Path != "/api/v2/auth/password" && r.URL.Path != "/api/v2/auth/oneos/me" {
|
|
menu := requiredMenu(r)
|
|
allowed := principal.CanMenu(menu)
|
|
if menu == "shared" {
|
|
allowed = principal.CanMenu("monitor") || principal.CanMenu("vehicles") || principal.CanMenu("tracks") || principal.CanMenu("statistics")
|
|
}
|
|
if menu == "" || !allowed {
|
|
httpx.WriteError(w, http.StatusForbidden, "MENU_PERMISSION_DENIED", "当前账号无权访问此功能", menu, requestTraceID(r))
|
|
return
|
|
}
|
|
}
|
|
if strings.HasPrefix(r.URL.Path, "/api/v2/admin/users") && !principal.CanMenu("users") {
|
|
httpx.WriteError(w, http.StatusForbidden, "MENU_PERMISSION_DENIED", "当前账号无权访问账号管理", "users", requestTraceID(r))
|
|
return
|
|
}
|
|
required := requiredRole(r)
|
|
if roleRank(principal.Role) < roleRank(required) {
|
|
httpx.WriteError(w, http.StatusForbidden, "PERMISSION_DENIED", "当前角色无权执行该操作", "需要 "+required+" 角色", requestTraceID(r))
|
|
return
|
|
}
|
|
if a.localBackend {
|
|
principal.DataSource = "production-local"
|
|
}
|
|
ctx := platform.WithPrincipal(r.Context(), principal)
|
|
if strings.HasPrefix(r.URL.Path, "/api/v2/customer-mileage") || strings.HasPrefix(r.URL.Path, "/api/v2/operation-compliance") {
|
|
snapshot := platform.CustomerAssignmentSnapshot{Tenant: principal.TenantRef}
|
|
if principal.AuthProvider != "oneos" && a.profile != nil {
|
|
snapshot.Tenant = a.profile.tenant
|
|
}
|
|
if principal.AuthProvider == "oneos" && (r.URL.Path == "/api/v2/customer-mileage" || strings.HasPrefix(r.URL.Path, "/api/v2/operation-compliance")) && r.Method == http.MethodGet && a.profile != nil && a.profile.vehicleQueryScope {
|
|
token, clientID := bearerToken(r), r.Header.Get("X-OneOS-Client-ID")
|
|
rows, err := a.profile.vehicleQuerySnapshot(ctx, token, clientID)
|
|
if err != nil {
|
|
httpx.WriteError(w, 503, "ONEOS_CATALOG_UNAVAILABLE", "OneOS 客户车辆归属暂不可用,请重试", "", requestTraceID(r))
|
|
return
|
|
}
|
|
snapshot.Live, snapshot.ObservedAt = true, time.Now()
|
|
key := sha256.Sum256([]byte(clientID + "\x00" + token))
|
|
a.profile.scopeMu.Lock()
|
|
if entry := a.profile.queryScopes[key]; entry != nil && !entry.expires.IsZero() {
|
|
snapshot.ObservedAt = entry.expires.Add(-oneOSQueryScopeTTL)
|
|
}
|
|
a.profile.scopeMu.Unlock()
|
|
for _, row := range rows {
|
|
snapshot.Rows = append(snapshot.Rows, platform.CustomerVehicleAssignment{VIN: row.VIN, Plate: row.PlateNumber, Customer: row.CustomerName, Department: row.BusinessDepartment, ResponsiblePerson: row.BusinessResponsiblePerson})
|
|
}
|
|
}
|
|
ctx = platform.WithCustomerAssignments(ctx, snapshot)
|
|
}
|
|
r = r.WithContext(ctx)
|
|
if r.URL.Path == "/api/v2/auth/oneos/me" {
|
|
if a.profile == nil || principal.AuthProvider != "oneos" || r.Method != http.MethodGet {
|
|
oneOSAuthError(w, r, 403, "ONEOS_REQUIRED")
|
|
return
|
|
}
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
httpx.WriteOK(w, requestTraceID(r), map[string]any{"username": principal.Username, "displayName": principal.Name, "tenantId": principal.TenantRef, "menuKeys": principal.MenuKeys, "authorizationStatus": principal.AuthorizationStatus})
|
|
return
|
|
}
|
|
if r.URL.Path == "/api/v2/session" {
|
|
principal.VehicleCount = len(principal.VehicleVINs)
|
|
httpx.WriteOK(w, requestTraceID(r), struct {
|
|
platform.Principal
|
|
AuthMode string `json:"authMode"`
|
|
}{principal, a.mode})
|
|
return
|
|
}
|
|
if r.URL.Path == "/api/v2/auth/logout" {
|
|
if r.Method != http.MethodPost {
|
|
httpx.WriteError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "退出接口仅支持 POST", "", requestTraceID(r))
|
|
return
|
|
}
|
|
if a.local != nil && (principal.AuthProvider == "local" || principal.AuthProvider == "oneos" || principal.AuthProvider == "oneos-profile") {
|
|
a.local.logout(r.Context(), bearerToken(r))
|
|
}
|
|
httpx.WriteOK(w, requestTraceID(r), map[string]bool{"loggedOut": true})
|
|
return
|
|
}
|
|
if r.URL.Path == "/api/v2/auth/password" {
|
|
if r.Method != http.MethodPut || a.local == nil {
|
|
httpx.WriteError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "密码修改接口仅支持 PUT", "", requestTraceID(r))
|
|
return
|
|
}
|
|
a.local.changePassword(w, r, principal)
|
|
return
|
|
}
|
|
if a.local != nil && a.local.handleAdmin(w, r, principal) {
|
|
return
|
|
}
|
|
if a.demo != nil && a.demo.handleAdmin(w, r, principal) {
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func (a *apiAuthenticator) authenticate(r *http.Request) (platform.Principal, bool) {
|
|
if a.mode == "disabled" {
|
|
return platform.Principal{Name: "local-developer", Username: "local-developer", Role: "admin", UserType: "admin", AuthProvider: "disabled", MenuKeys: append([]string(nil), adminMenus...)}, true
|
|
}
|
|
token := bearerToken(r)
|
|
if token == "" {
|
|
return platform.Principal{}, false
|
|
}
|
|
if a.profile != nil && r.Header.Get("X-OneOS-Client-ID") != "" {
|
|
return a.profile.authenticate(r, token)
|
|
}
|
|
if a.local != nil {
|
|
if principal, ok := a.local.authenticate(r.Context(), token); ok {
|
|
if principal.AuthProvider == "oneos-profile" || (a.profile != nil && principal.AuthProvider == "oneos") {
|
|
return platform.Principal{}, false
|
|
}
|
|
return principal, true
|
|
}
|
|
}
|
|
for _, adapter := range a.adapters {
|
|
principal, matched, err := adapter.AuthenticateBearer(r.Context(), token)
|
|
if err != nil {
|
|
log.Printf("identity adapter %s rejected credential: %v", adapter.Name(), err)
|
|
continue
|
|
}
|
|
if matched {
|
|
return principal, true
|
|
}
|
|
}
|
|
hash := sha256.Sum256([]byte(token))
|
|
var match platform.Principal
|
|
matched := 0
|
|
for _, item := range a.tokens {
|
|
equal := subtle.ConstantTimeCompare(hash[:], item.hash[:])
|
|
matched |= equal
|
|
if equal == 1 {
|
|
match = item.principal
|
|
}
|
|
}
|
|
return match, matched == 1
|
|
}
|
|
|
|
func bearerToken(r *http.Request) string {
|
|
header := strings.TrimSpace(r.Header.Get("Authorization"))
|
|
if len(header) < 8 || !strings.EqualFold(header[:7], "Bearer ") {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(header[7:])
|
|
}
|
|
|
|
func requiredMenu(r *http.Request) string {
|
|
path := r.URL.Path
|
|
switch {
|
|
case strings.HasPrefix(path, "/api/v2/operation-compliance"):
|
|
return "operation-compliance"
|
|
case strings.HasPrefix(path, "/api/v2/customer-mileage"):
|
|
return "customer-mileage"
|
|
case strings.HasPrefix(path, "/api/v2/monitor"), path == "/api/v2/alerts/events":
|
|
return "monitor"
|
|
case path == "/api/map/reverse-geocode", path == "/api/realtime/vehicles", path == "/api/realtime/locations", path == "/api/vehicle-service", path == "/api/vehicle-service/overview", path == "/api/vehicle-service/overviews", strings.HasSuffix(path, "/telemetry/latest"):
|
|
return "shared"
|
|
case path == "/api/v2/tracks":
|
|
return "tracks"
|
|
case path == "/api/mileage/daily", path == "/api/mileage/summary", path == "/api/v2/statistics/mileage", path == "/api/v2/statistics/vehicles", path == "/api/v2/statistics/vehicle-options", path == "/api/v2/statistics/vehicle-resolve":
|
|
return "statistics"
|
|
case path == "/api/vehicles", path == "/api/vehicles/resolve", path == "/api/vehicles/coverage", path == "/api/vehicles/coverage/summary", path == "/api/vehicles/business-filters":
|
|
return "shared"
|
|
case strings.HasPrefix(path, "/api/v2/vehicles/"):
|
|
return "vehicles"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func requiredRole(r *http.Request) string {
|
|
if r.URL.Path == "/api/v2/customer-mileage/rules" && r.Method != http.MethodGet {
|
|
return "admin"
|
|
}
|
|
if strings.HasPrefix(r.URL.Path, "/api/v2/open-platform/") {
|
|
return "admin"
|
|
}
|
|
if strings.HasPrefix(r.URL.Path, "/api/v2/reconciliation/") {
|
|
if r.Method == http.MethodPost && (strings.HasSuffix(r.URL.Path, "/archive") || strings.HasSuffix(r.URL.Path, "/restore") ||
|
|
strings.HasSuffix(r.URL.Path, "/batch-archive") || strings.HasSuffix(r.URL.Path, "/batch-restore")) {
|
|
return "admin"
|
|
}
|
|
if r.Method == http.MethodGet || r.Method == http.MethodHead || r.Method == http.MethodPost {
|
|
return "operator"
|
|
}
|
|
return "admin"
|
|
}
|
|
if strings.HasPrefix(r.URL.Path, "/api/v2/operations/") {
|
|
if r.Method == http.MethodGet || r.Method == http.MethodHead {
|
|
return "operator"
|
|
}
|
|
return "admin"
|
|
}
|
|
if (r.Method == http.MethodGet || r.Method == http.MethodHead) && strings.HasPrefix(r.URL.Path, "/api/v2/exports") {
|
|
return "viewer"
|
|
}
|
|
if r.Method == http.MethodGet || r.Method == http.MethodHead {
|
|
return "viewer"
|
|
}
|
|
path := r.URL.Path
|
|
if r.Method == http.MethodPost {
|
|
switch path {
|
|
case "/api/v2/auth/logout":
|
|
return "viewer"
|
|
case "/api/v2/statistics/vehicle-resolve", "/api/vehicle-service/overviews", "/api/history/raw-frames/query", "/api/mileage/daily", "/api/v2/statistics/mileage", "/api/v2/access/summary", "/api/v2/access/vehicles", "/api/v2/alerts/summary", "/api/v2/alerts/events", "/api/v2/exports":
|
|
return "viewer"
|
|
case "/api/v2/alerts/notifications/read":
|
|
return "operator"
|
|
}
|
|
if strings.HasPrefix(path, "/api/v2/alerts/events/") && strings.HasSuffix(path, "/actions") {
|
|
return "operator"
|
|
}
|
|
if path == "/api/v2/alerts/rules" {
|
|
return "admin"
|
|
}
|
|
if strings.HasPrefix(path, "/api/v2/alerts/rules/") && strings.HasSuffix(path, "/rollback") {
|
|
return "admin"
|
|
}
|
|
}
|
|
if r.Method == http.MethodPut && (path == "/api/v2/access/thresholds" || strings.HasPrefix(path, "/api/v2/alerts/rules/")) {
|
|
return "admin"
|
|
}
|
|
if r.Method == http.MethodPut && path == "/api/v2/auth/password" {
|
|
return "viewer"
|
|
}
|
|
return "admin"
|
|
}
|
|
|
|
func roleRank(role string) int {
|
|
switch strings.ToLower(role) {
|
|
case "viewer", "customer":
|
|
return 1
|
|
case "operator":
|
|
return 2
|
|
case "admin":
|
|
return 3
|
|
}
|
|
return 0
|
|
}
|