193 lines
11 KiB
Markdown
193 lines
11 KiB
Markdown
# 车辆数据中台客户 Scope API 覆盖矩阵
|
||
|
||
更新时间:2026-07-16
|
||
|
||
状态:本地客户主体与车辆/时间 Scope 已实施;OneOS 联邦身份仍按后续接口契约接入。
|
||
|
||
## 1. 当前结论
|
||
|
||
车辆中台已支持本地 `admin/customer` 账号、菜单权限、车辆 VIN Scope 和授权有效时间。认证中间件把
|
||
`subjectId/userType/customerRef/tenantRef/authProvider` 以及当前车辆授权注入 `Principal`;车辆、监控、轨迹、
|
||
历史、里程和导出入口均在服务端校验主体与时间边界。客户不能通过 Query、Header 或 Body 自行覆盖 customer ID。
|
||
|
||
OneOS 身份尚不能直接映射为本地 `viewer`。后续只有在 OneOS 提供签名、受众、身份版本和客户关联等稳定接口后,
|
||
才能映射为同一强类型 Principal;在此之前继续使用车辆中台本地客户账号,不读取或修改 OneOS 业务逻辑。
|
||
|
||
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` 服务已启用且正在运行。
|
||
|
||
该配置负责内部静态 token;客户级隔离由平台账号、会话、菜单和车辆授权表共同提供。
|
||
|
||
## 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 导出 owner、异步 Scope 与审计(已实施)
|
||
|
||
release `export-owner-scope-20260716165737` 已完成:
|
||
|
||
- `Create/List/Download` 全部接收 context 和 Principal;
|
||
- Job 固化 owner ID、账号、角色/用户类型、认证来源、客户/租户、已解析 VIN 和逐车时间范围;
|
||
- 创建时同步校验每个 VIN 和历史授权时间;异步开始、每 5,000 行及下载前重新校验客户账号与车辆授权;
|
||
- 非管理员列表只返回当前 owner;越权或猜测下载 ID 返回 404;管理员保留审计视角;
|
||
- 客户停用、车辆授权撤销或授权区间变化会停止未完成任务并拒绝历史文件下载;
|
||
- CSV 文件使用 `0640`,任务索引原子持久化;CSV 元数据包含创建账号、角色、客户、车辆 Scope、查询条件与生成时间;
|
||
- Web 使用带 Bearer Token 的 Blob 下载,不再通过无 Authorization 的直接链接;
|
||
- 服务重启后 owner、Scope 和完成文件仍可复核,执行中任务安全失败并要求重新创建。
|
||
|
||
自动化已覆盖两个互斥客户、管理员审计、IDOR、撤权停止和持久化元数据。生产真实导出在重启前后文件
|
||
SHA-256 一致,伪造 ID 返回 404,且无 `.part` 文件残留。
|
||
|
||
## 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 的实时访问立即 404,B 只能读取自身授权时间段;
|
||
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. 双客户导出自动化与生产管理员实车导出:已完成;完整客户 API 矩阵继续补齐;
|
||
7. 本地测试客户可按菜单和车辆授权启用;OneOS 联邦客户入口保持关闭,等待正式身份接口。
|