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

@@ -0,0 +1,75 @@
# Authentication and customer authorization
## Boundary
The platform owns authorization even when authentication is delegated later. An upstream identity system may prove who the user is, but it cannot bypass the local menu and vehicle Scope checks.
Current providers:
- `local`: username/password login and opaque server session.
- `legacy-token`: existing static operational Bearer tokens, retained for emergency access and migration.
- `disabled`: local development only.
The API exposes `IdentityAdapter` as the future signed-token or introspection boundary. Yudao Cloud / RuoYi Sys integration must validate a short-lived signed credential and map `(auth_provider, external_subject)` to `platform_user`. Never trust unsigned identity headers from a reverse proxy.
## Roles and menus
- `admin` receives all platform menus and can manage customer accounts.
- `customer` receives only explicitly granted menu keys from the fixed customer catalog: `monitor`, `vehicles`, `tracks`, `statistics`.
- The server rejects every unspecified customer API route. The web navigation and route guard are usability layers, not the authorization boundary.
Shared vehicle APIs used by more than one customer menu are allowed when the account owns any relevant menu, but every data query still receives the same VIN Scope.
## Vehicle Scope
`platform_user_vehicle` is the authoritative local grant table. Customer principals carry a bounded list of active VIN grants. Service queries inject it as `scopeVins` before SQL construction, and the MySQL builders apply the list to vehicle, realtime, monitor, track resolution and mileage queries.
Explicit VINs outside the grant set return `403 VEHICLE_PERMISSION_DENIED`. A missing vehicle grant is fail-closed and produces an empty collection. Plate-number resolution is performed inside the same VIN Scope.
## Sessions and password policy
- Passwords are stored with bcrypt cost 12.
- Password length is 10-128 characters and must contain at least three of: lowercase, uppercase, number, symbol.
- Five consecutive failures lock the account for 15 minutes.
- Login creates a cryptographically random opaque token; only SHA-256 is stored in MySQL.
- Default session lifetime is 12 hours (`AUTH_SESSION_TTL_HOURS`).
- The browser stores the token in `sessionStorage`, not persistent storage.
- Disabling an account or resetting its password revokes all active sessions immediately.
- Successful/failed logins and account permission changes are written to `platform_auth_audit`.
Session principals are cached in the API for at most 30 seconds to keep realtime polling inexpensive. Account disable and password reset revoke the database session immediately; permission updates invalidate local cache entries and become visible across multiple API instances within the cache window.
## Bootstrap
The first local administrator is created only when no administrator exists:
```text
BOOTSTRAP_ADMIN_USERNAME=admin
BOOTSTRAP_ADMIN_PASSWORD=<strong one-time bootstrap secret>
AUTH_SESSION_TTL_HOURS=12
```
The bootstrap password does not overwrite an existing administrator. Keep the environment file root-owned and mode `0600`. After an administrator changes credentials through the account password dialog, remove the bootstrap password from the environment.
## APIs
```text
POST /api/v2/auth/login
POST /api/v2/auth/logout
PUT /api/v2/auth/password
GET /api/v2/session
GET /api/v2/admin/users
POST /api/v2/admin/users
PUT /api/v2/admin/users/{id}
```
Customer creation/update accepts display name, optional external customer/tenant references, account status, menu keys and VINs. Username is immutable after creation. Password reset and account disable invalidate previous sessions.
## External identity migration
1. Add a concrete `IdentityAdapter` for the upstream issuer using JWKS signature validation or server-side token introspection.
2. Match the validated issuer subject to `platform_user.auth_provider` and `platform_user.external_subject`.
3. Keep menu and VIN grants in the platform initially, or sync them into the same local projection with source/version metadata.
4. Run local and external providers in parallel during migration; do not remove the operational token until external login and rollback are proven.
5. Preserve `subjectId`, `customerRef`, `tenantRef`, `menuKeys`, `vehicleCount` and `authProvider` in the session contract so the web application does not depend on a Yudao/RuoYi-specific payload.

View File

@@ -85,6 +85,9 @@ EXPORT_DIR=/opt/lingniu-vehicle-platform/data/exports
AUTH_MODE=enforce
# JSON array with viewer/operator/admin principals. Keep this file mode 0600.
AUTH_TOKENS_JSON=[{"token":"<at-least-16-random-characters>","name":"ecs-admin","role":"admin"}]
BOOTSTRAP_ADMIN_USERNAME=admin
BOOTSTRAP_ADMIN_PASSWORD=<strong-first-admin-password>
AUTH_SESSION_TTL_HOURS=12
ONEOS_MYSQL_DSN=vehicle_scope_reader:***@tcp(rm-bp179zbv481rnw3e2.mysql.rds.aliyuncs.com:3306)/ln_asset_management?parseTime=true&loc=Asia%2FShanghai
ONEOS_SCOPE_SYNC_TIMEOUT_SEC=60
ONEOS_SCOPE_MAX_REJECTED=100
@@ -165,7 +168,8 @@ test -n "$MYSQL_DSN"
/opt/lingniu-vehicle-platform/releases/$PLATFORM_RELEASE/deploy/migrations/009_alert_stream_checkpoint.sql \
/opt/lingniu-vehicle-platform/releases/$PLATFORM_RELEASE/deploy/migrations/010_alert_stream_metric_mapping.sql \
/opt/lingniu-vehicle-platform/releases/$PLATFORM_RELEASE/deploy/migrations/011_alert_stream_invalid_evidence.sql \
/opt/lingniu-vehicle-platform/releases/$PLATFORM_RELEASE/deploy/migrations/012_business_scope_projection.sql
/opt/lingniu-vehicle-platform/releases/$PLATFORM_RELEASE/deploy/migrations/012_business_scope_projection.sql \
/opt/lingniu-vehicle-platform/releases/$PLATFORM_RELEASE/deploy/migrations/013_platform_identity_access.sql
```
The API guards the access-threshold tables for compatibility, while alert APIs deliberately require the alert migrations to exist. Run every numbered migration explicitly before switching traffic so DDL permission and index creation failures are caught early. The migration journal records filename and SHA-256 and refuses a changed file; duplicate forward `ADD COLUMN` and `CREATE INDEX` statements are tolerated only when resuming partially executed MySQL DDL. Full-line SQL comments are removed before statement splitting, so punctuation in a comment cannot become executable SQL. Migration `008` adds forward-compatible access evidence columns and an index to the gateway-owned realtime snapshot table without changing its `(protocol, vin)` primary key. Migration `009` creates the per-group/topic/partition event-time checkpoint used to make MySQL effects authoritative before Kafka offsets are committed.