feat(platform): consolidate production vehicle data workflows

This commit is contained in:
lingniu
2026-07-15 23:26:29 +08:00
parent 6cddc0a43d
commit 3fabcf181a
59 changed files with 6849 additions and 3532 deletions

View File

@@ -0,0 +1,194 @@
# 车辆数据中台客户 Scope API 覆盖矩阵
更新时间2026-07-14
状态:安全审计与实施设计;尚未开放客户主体。
## 1. 当前结论
车辆中台当前的 Bearer Token 只表达 `viewer/operator/admin` 功能角色,不表达客户或车辆数据范围。
因此,即使 OneOS 已能登录客户,也不能直接把客户 token 映射成 `viewer`:这会让客户读取全量车辆、轨迹、
告警、统计及其他人的导出文件。
2026-07-14 已只读核验 ECS 当前运行状态:
- `AUTH_MODE=enforce`
- 使用 `AUTH_TOKENS_JSON`,未使用单一 `AUTH_TOKEN`
- `/opt/lingniu-vehicle-platform/env/platform.env` 权限为 `0600 root:root`
- `lingniu-vehicle-platform` 服务已启用且正在运行。
该配置能阻止匿名访问普通平台 API但不能提供客户级隔离。
## 2. 独立发现:高德服务端接口绕过平台鉴权
`app.NewServer` 当前先把平台 API 包在 `withAPIAuth` 中,之后又在外层注册
`withAMapReverseGeocodeAPI`。所以 `/api/map/reverse-geocode` 在进入鉴权中间件前就会被处理;只要配置了
`AMAP_API_KEY`,匿名请求即可消耗服务端高德额度。
整改要求:
- 将逆地理编码路由放回鉴权之后;
- 对客户和内部主体分别限流,并限制经纬度格式和并发;
- 不记录完整 access token
- `app-config.js` 可继续公开,但只能暴露高德 Web JS 公钥和代理路径,不暴露服务端 API Key
- 高德安全代理因浏览器 SDK 需要可访问,应在边缘层配置域名来源、速率和响应缓存,不能把它视为客户身份接口。
## 3. 主体与权限模型
`Principal` 至少扩展为:
```text
name
role # 仅内部功能角色
subject_type # employee | service | customer
subject_id # OneOS sys_user.user_id
customer_id # 仅 customer 必填
tenant_id
identity_version
capabilities # vehicle:read, history:read, export:create ...
```
约束:
- 内部静态 token 只能生成 `employee/service` 主体;
- 客户主体只能来自 OneOS 签名、短时且 `aud=vehicle-data-platform` 的声明;
- 浏览器 Query、Body、Cookie 或普通转发头中的 `customerId` 均无效;
- 客户权限不能通过 `roleRank(viewer)` 复用内部 viewer 权限,需要独立 capability 判断;
- 未知 `subject_type`、缺失 customer ID、签名异常或身份版本失效全部拒绝。
## 4. QueryScope 机制
所有 Store 查询显式接收 `QueryScope`,避免新增方法时忘记从 `context.Context` 读取 Scope
```text
mode: unrestricted | customer_current | customer_period
customer_id
active_version
valid_from / valid_to # 历史持有期查询
```
`unrestricted` 只能由已验证的内部 employee/service 主体创建。空客户 Scope 表示“零车辆”,绝不能解释为不限制。
MySQL 当前数据查询应 JOIN
```text
business_scope_state(id=1)
business_customer_vehicle_scope(
source_version = active_version,
customer_id = principal.customer_id,
vin = 业务表.vin
)
```
同时要求:
- `last_success_at` 距当前时间不超过 10 分钟;
- active version 存在且快照计数完整;
- 快照缺失或过期返回 `503 BUSINESS_SCOPE_UNAVAILABLE`,而不是空列表或全量数据;
- 快照新鲜但该客户确实没有车辆时返回空列表;
- 单车越权统一返回 404避免泄露 VIN 是否存在。
TDengine 无法直接 JOIN MySQL Scope单车查询先在 MySQL 做授权判断;多车查询从同一 active version 读取授权
VIN 集合,设置数量上限后再生成参数化过滤。不能先从 TDengine 取全量再由前端过滤。
## 5. API 覆盖矩阵
### 5.1 客户首期允许,但必须按当前 Scope 过滤
| API 组 | 代表路由 | 必须实施的控制 |
| --- | --- | --- |
| 全局监控 | `/api/v2/monitor/summary``/map` | 车辆、点位、聚合、告警数和所有汇总均从同一客户 Scope 计算;禁止混用全局 DashboardSummary |
| 车辆列表与解析 | `/api/vehicles``/resolve``/detail` | VIN、车牌、终端号解析必须先限定 Scope越权关键字表现为不存在 |
| 最新状态 | `/api/realtime/vehicles``/locations` | 列表、total、分页和筛选均在 SQL 层过滤 |
| 车辆只读档案 | `GET /api/v2/vehicles/{vin}/profile` | 先校验 VIN只返回门户需要的非敏感字段 |
| 最新遥测 | `/api/v2/vehicles/{vin}/telemetry/latest` | 路径 VIN 校验 Scope原始协议帧字段按白名单输出 |
| 轨迹 | `/api/v2/tracks` | 车牌/VIN/终端解析、轨迹点、停留点和统计都使用同一授权 VIN |
| 历史查询 | `/api/history/locations``/api/v2/history/query``/series` | 首期只允许当前车辆且 eventAt 不早于 `scope_start_at`;不能读取交付前数据 |
| 里程 | `/api/mileage/summary``/daily``/api/v2/statistics/mileage` | 列表与汇总按 VIN 和授权时间过滤total 不能泄露全局数量 |
| 在线统计 | `/api/statistics/online-summary``/online-vehicles` | 分母、在线数、离线数、趋势和明细均限定客户车辆 |
| 告警只读 | `/api/v2/alerts/summary``/events``/events/{id}` | 列表 JOIN event.vin Scope详情按 ID+Scope 查询,越权返回 404 |
| 指标元数据 | `/api/v2/metrics``/api/v2/history/metrics` | 可返回全局字段定义,但过滤内部专用/敏感指标 |
| 逆地理编码 | `/api/map/reverse-geocode` | 必须鉴权和限流;不依赖客户参数,不返回其他车辆信息 |
### 5.2 客户首期明确禁止
| API 组 | 路由 | 原因 |
| --- | --- | --- |
| 平台总览 | `/api/dashboard/summary` | 包含全局车辆、帧量、Kafka/服务健康信息 |
| 接入诊断 | `/api/vehicles/coverage*``/api/vehicle-service*` | 包含协议覆盖、终端、原始帧和数据质量诊断 |
| 原始帧 | `/api/history/raw-frames*` | 包含协议证据和内部字段;首期使用白名单遥测/历史 API 代替 |
| 质量中心 | `/api/quality/*`、旧 `/api/alert-events/*` 别名 | 平台级质量问题,不是客户业务告警 |
| 运维 | `/api/ops/*` | 数据源、容量、Kafka、Redis 和服务健康仅内部可见 |
| 接入管理 | `/api/v2/access/*` | 身份绑定、阈值和未解析终端均为内部运维能力 |
| 车辆档案写入 | `PUT /api/v2/vehicles/{vin}/profile``/vehicle-profiles/sync` | 只允许内部管理员/同步服务 |
| 告警规则/处置 | `/api/v2/alerts/rules*``events/{id}/actions` | 首期客户只读,不能修改平台规则或全局事件状态 |
| 平台通知 | `/api/v2/alerts/notifications*` | 当前通知没有客户 owner读取与已读操作均可能跨客户 |
禁止应在服务端 capability 层返回 403不能只隐藏前端菜单。
### 5.3 导出必须单独重构后才能开放
当前导出实现有四个 P0 问题:
1. `CreateHistoryExport` 没有 `context`,创建时无法固化主体;
2. `HistoryExportJob` 没有 owner/customer/scope version
3. `ListHistoryExports` 返回全局任务;
4. `HistoryExportFile(id)` 只校验 ID 和完成状态,知道 ID 即可下载。
整改要求:
- `Create/List/Download` 全部接收 context 和 Principal
- Job 固化 `owner_subject_id``customer_id``scope_version`、已解析 VIN、时间范围和审计 request ID
- 创建任务时校验每个 VIN异步执行前再次校验
- 列表只返回当前 owner下载同时校验 owner 和当前授权,越权返回 404
- 客户停用、车辆授权撤销或 Scope 失效时停止未完成任务并拒绝下载;
- 文件名和 CSV 元数据不得包含其他客户信息;文件按 owner 隔离目录并设置 `0640`
- 定期清理过期文件与 jobs 记录。
在上述改造完成前,客户主体不授予 `export:create``export:download`
## 6. 当前 Scope 与历史 Scope 的边界
已实现的 `business_customer_vehicle_scope` 只保存当前有效车辆。它足以保护实时监控,但不能完整实现“客户还车后
仍可查看持有期间历史”。
首期安全策略:
- 只允许查询当前仍在 Scope 中的车辆;
- 历史起点不得早于当前记录的 `scope_start_at`
- 车辆还车并从当前 Scope 移除后,历史访问也随即关闭。
若产品确认需要持有期历史,再新增版本化区间投影:
```text
customer_id, vin, contract_id, valid_from, valid_to, source_version
```
历史查询必须满足 VIN 和 `valid_from <= event_at < valid_to`。不能仅凭“客户曾经拥有过这辆车”开放该 VIN 的全部历史。
## 7. 必须覆盖的越权测试
准备客户 A、客户 B、内部 viewer、内部 operator、内部 admin 和 service 六类主体,至少覆盖:
1. A 在列表、关键字解析、路径 VIN、POST body、分页游标中请求 B 的车辆;
2. A 直接访问 B 的告警事件 ID、导出 ID 和下载 URL
3. A 使用车牌或终端号绕过 VIN 校验;
4. A 的空 Scope 不返回全量Scope 缺失/过期返回 503
5. 快照切换期间所有查询只使用一个 active version不混合新旧结果
6. total、summary、地图聚合数量和在线率不泄露全局或 B 的数量;
7. A 只能读取交付后的历史点,不能读取交付前记录;
8. 车辆从 A 还车并交付给 B 后A 的实时访问立即 404B 只能读取自身授权时间段;
9. 客户主体访问运维、接入、质量、写档案、规则和处置接口均为 403
10. 匿名逆地理编码失败;认证请求达到限额后返回 429
11. 客户 token 的 customer ID 不能被 Query/Header/Body 覆盖;
12. 服务重启后导出 owner、Scope 和下载授权仍保持,不能退化成全局任务。
## 8. 推荐实施顺序
1. 扩展 Principal 和 OneOS JWT 校验,但暂不开放客户路由;
2. 引入强类型 QueryScope先改车辆解析与单车详情
3. 改所有列表、汇总、地图、统计和 TDengine 查询;
4. 改告警详情 IDOR、逆地理编码鉴权限流
5. 重构导出 owner 与异步 Scope
6. 完成双客户自动化测试和生产影子查询;
7. 最后为测试客户启用 capabilities默认保持客户入口关闭。