docs(guide): scrub decommissioned QDS_ADMIN role from KB content

Follow-up to hiveops-incident#253 (QDS_ADMIN removed from all 5 backends'
SecurityConfig/InstitutionContext, and from the hiveops-auth Role enum so no
JWT can carry it). The guide KB still quoted QDS_ADMIN in role/allow-list
descriptions, which no longer matched the code.

Removed QDS_ADMIN from 26 references across 23 content files; surviving roles
(USER, CUSTOMER, ADMIN, MSP_ADMIN, BCOS_ADMIN, AGENT, SYSTEM) left unchanged.
No frontmatter touched -> zero module-coverage impact. Doc-accuracy only.

Closes #10

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 15:41:36 -04:00
parent 1d88370c71
commit 3ace2e06d2
23 changed files with 26 additions and 26 deletions
@@ -38,7 +38,7 @@ audience: dev
## Roles / scoping
- `CUSTOMER` / `MSP_ADMIN` → scoped to granted institution keys (downstream `InstitutionContext.resolveScope()`).
- `BCOS_ADMIN` / `ADMIN` / `QDS_ADMIN` / `USER` → unrestricted.
- `BCOS_ADMIN` / `ADMIN` / `USER` → unrestricted.
## Config
- `INCIDENT_SERVICE_URL` default `http://hiveops-incident-backend:8080`.
@@ -25,7 +25,7 @@ Browser → GET /api/analytics/incident-trends (hiveops-analytics)
- The endpoint has **no `@PreAuthorize`** — any authenticated JWT can call it. There is no MSP_ADMIN/BCOS_ADMIN gate on Incident Trends.
- **Data is institution-scoped downstream.** `hiveops-incident` calls `InstitutionContext.resolveScope()` on the forwarded token:
- `ROLE_CUSTOMER` / `ROLE_MSP_ADMIN` → scoped to their granted institution keys (sees only their ATMs).
- `BCOS_ADMIN` / `ADMIN` / `QDS_ADMIN` / `USER` → unrestricted (all institutions).
- `BCOS_ADMIN` / `ADMIN` / `USER` → unrestricted (all institutions).
- So a support user seeing "less data than expected" is usually a **scoped role**, not a bug.
## First things to check when it misbehaves
@@ -34,7 +34,7 @@ Terse agent reference for `dashboard.cash-stats`. Read view over cassette bill c
- No topic is consumed/produced by the dashboard itself.
## Roles
- Read endpoints: `USER, CUSTOMER, ADMIN, MSP_ADMIN, QDS_ADMIN, BCOS_ADMIN` (incident `/api/**`).
- Read endpoints: `USER, CUSTOMER, ADMIN, MSP_ADMIN, BCOS_ADMIN` (incident `/api/**`).
- Scope: `InstitutionContext.resolveScope()` → `CUSTOMER`/`MSP_ADMIN` scoped to JWT institution keys; others unrestricted.
- Journal admin: `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')`.
@@ -20,8 +20,8 @@ The page makes exactly two calls on load and every 60 s:
## Roles required
- Both endpoints fall under incident's `/api/**` rule → **any authenticated app user**: `USER, CUSTOMER, ADMIN, MSP_ADMIN, QDS_ADMIN, BCOS_ADMIN`. (`/api/journal-events` POST also allows `AGENT`, but that's ingest, not this page.)
- **Institution scoping is automatic** (`InstitutionContext.resolveScope`): `CUSTOMER`/`MSP_ADMIN` see only their granted institution keys; `BCOS_ADMIN/ADMIN/QDS_ADMIN/USER` see the whole fleet. If a customer says "I only see some ATMs," that's correct scoping, not a bug.
- Both endpoints fall under incident's `/api/**` rule → **any authenticated app user**: `USER, CUSTOMER, ADMIN, MSP_ADMIN, BCOS_ADMIN`. (`/api/journal-events` POST also allows `AGENT`, but that's ingest, not this page.)
- **Institution scoping is automatic** (`InstitutionContext.resolveScope`): `CUSTOMER`/`MSP_ADMIN` see only their granted institution keys; `BCOS_ADMIN/ADMIN/USER` see the whole fleet. If a customer says "I only see some ATMs," that's correct scoping, not a bug.
- **Admin backfill/sync is `MSP_ADMIN` or `BCOS_ADMIN` only** (journal `AdminController`).
## First things to check when it misbehaves
@@ -31,7 +31,7 @@ audience: dev
## Roles / scoping (`InstitutionContext.resolveScope()`)
- `null` (unrestricted): `BCOS_ADMIN`, `ADMIN`, `QDS_ADMIN`, `USER` → `institutionKey` param honored (blank = all).
- `null` (unrestricted): `BCOS_ADMIN`, `ADMIN`, `USER` → `institutionKey` param honored (blank = all).
- scoped list: `CUSTOMER`, `MSP_ADMIN` → forced to `scopedKeys.get(0)`; **param ignored**.
## Gotchas
@@ -28,7 +28,7 @@ audience: internal
Scope comes from `InstitutionContext.resolveScope()` (institution keys are carried in the JWT):
- **BCOS_ADMIN / ADMIN / QDS_ADMIN / USER** → unrestricted. The `institutionKey` query param is honored (blank = all institutions).
- **BCOS_ADMIN / ADMIN / USER** → unrestricted. The `institutionKey` query param is honored (blank = all institutions).
- **CUSTOMER / MSP_ADMIN** → scoped. The backend **ignores the query param** and forces the log to the caller's **first** scoped institution key only.
## First things to check when it misbehaves
@@ -39,7 +39,7 @@ Constants in `config/DeviceEventKafkaConfig`: `TOPIC`, `CONSUMER_GROUP`.
## Roles
- Viewing tracker: **any valid JWT**. Read endpoints have **no `@PreAuthorize`**; scoped by `InstitutionContext.resolveScope()`:
- CUSTOMER / MSP_ADMIN → scoped to JWT-granted institution keys.
- BCOS_ADMIN / ADMIN / QDS_ADMIN / USER → `null` = all institutions.
- BCOS_ADMIN / ADMIN / USER → `null` = all institutions.
- Mutations (create/assign/transition/bulk): `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')`.
## Gotchas
@@ -23,7 +23,7 @@ Rows are joined **in JS** on the ATM business key: `incident.atmName` ↔ `devic
- **Viewing the tracker** needs only a **valid JWT** — the two read endpoints (`/api/device-summary`, `/api/incidents/paginated`) carry **no `@PreAuthorize`**. Access is scoped by `InstitutionContext.resolveScope()`:
- `ROLE_CUSTOMER` / `ROLE_MSP_ADMIN` → **scoped** to their granted institution keys (list comes from the JWT credentials).
- `BCOS_ADMIN` / `ADMIN` / `QDS_ADMIN` / `USER` → `null` scope = **sees all institutions**.
- `BCOS_ADMIN` / `ADMIN` / `USER` → `null` scope = **sees all institutions**.
- **Acting on an incident** (the "View Incidents" drill-down leads to create/assign/transition) requires `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')` — enforced on `POST /api/incidents`, `POST /api/incident-management/{id}/assign`, `/transition`, `/bulk/update`.
## First things to check when it misbehaves
@@ -25,7 +25,7 @@ Agents never call incident directly — see [platform.service-ownership].
2. `JournalEventController.getEventsPaginated` → `JournalEventService.getEventsPaginated` builds a JPA `Specification` (`JournalEventSpecification.withFilters`) filtering on `atm.id`, `eventType IN (...)`, and `eventTime >= now-hoursBack`.
3. `journalEventRepository.findAll(spec, pageable)` → mapped to `JournalEventDTO`, returned as a Spring `Page` (`{content, page:{totalElements,totalPages,number}}`).
Aggregate reads (`/activity`, `/fleet/cassette-inventory`) additionally resolve institution scope via `InstitutionContext.resolveScope()` (null = unrestricted for `USER`/`ADMIN`/`QDS_ADMIN`/`BCOS_ADMIN`; scoped list for `CUSTOMER`/`MSP_ADMIN`). The per-ATM read endpoints do **not** apply that scope (they filter by `atmId` only).
Aggregate reads (`/activity`, `/fleet/cassette-inventory`) additionally resolve institution scope via `InstitutionContext.resolveScope()` (null = unrestricted for `USER`/`ADMIN`/`BCOS_ADMIN`; scoped list for `CUSTOMER`/`MSP_ADMIN`). The per-ATM read endpoints do **not** apply that scope (they filter by `atmId` only).
## Data flow — write/ingest path
@@ -32,7 +32,7 @@ Act-without-guessing reference. Everything below is confirmed in code.
- Paginated query params: `page`, `size`, `sort=eventTime,desc`, `hoursBack`, `eventTypes` (repeated key, e.g. `eventTypes=CARD_READER_FAIL&eventTypes=...`).
## Roles
- Read `/api/journal-events/**`: `hasAnyRole('USER','CUSTOMER','ADMIN','MSP_ADMIN','QDS_ADMIN','BCOS_ADMIN')`. NO `@PreAuthorize`; NOT admin-only.
- Read `/api/journal-events/**`: `hasAnyRole('USER','CUSTOMER','ADMIN','MSP_ADMIN','BCOS_ADMIN')`. NO `@PreAuthorize`; NOT admin-only.
- Ingest internal: `hasRole('INTERNAL')` (header `X-Internal-Secret` = env `INTERNAL_SERVICE_SECRET`).
- Agent path `POST /api/journal-events` also allows `ROLE_AGENT`.
@@ -10,7 +10,7 @@ Ops/support view of the **Event Journal** screen (frontend component `JournalEve
## Who can see it
- Read endpoints under `/api/journal-events/**` fall through to the `/api/**` rule in incident's `SecurityConfig` → `hasAnyRole('USER','CUSTOMER','ADMIN','MSP_ADMIN','QDS_ADMIN','BCOS_ADMIN')`.
- Read endpoints under `/api/journal-events/**` fall through to the `/api/**` rule in incident's `SecurityConfig` → `hasAnyRole('USER','CUSTOMER','ADMIN','MSP_ADMIN','BCOS_ADMIN')`.
- **Not admin-gated.** There is no `@PreAuthorize` on `JournalEventController` — any authenticated browser (`USER`) or JWT user can read journal events. This is intentional (it is a diagnostic view), unlike `/api/incident-management/**` which is `MSP_ADMIN`/`BCOS_ADMIN` only.
- `USER` = HiveIQ Browser session; `CUSTOMER`/`MSP_ADMIN` = customer JWT.
@@ -48,7 +48,7 @@ audience: dev
| — | none | **institution-key CRUD emits NO Kafka event** |
## Roles
- Read (`getAll`/`accessible`/`config-*`): no `@PreAuthorize`; scoped by `InstitutionContext.resolveScope()`. `null` (BCOS_ADMIN/ADMIN/QDS_ADMIN/USER) = all; list (CUSTOMER/MSP_ADMIN) = JWT keys.
- Read (`getAll`/`accessible`/`config-*`): no `@PreAuthorize`; scoped by `InstitutionContext.resolveScope()`. `null` (BCOS_ADMIN/ADMIN/USER) = all; list (CUSTOMER/MSP_ADMIN) = JWT keys.
- Write (create/update/delete/save-agent-props): `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')` — plain `ADMIN` is **excluded**.
## Gotchas
@@ -15,10 +15,10 @@ Owning service is **hiveops-devices** (NOT hiveops-msp — the MSP SPA has no ba
## Roles required
- **Read** (list/search): any valid JWT. `GET` endpoints have **no `@PreAuthorize`**; results are scoped by `InstitutionContext.resolveScope()`.
- BCOS_ADMIN / ADMIN / QDS_ADMIN / USER → unrestricted (sees all).
- BCOS_ADMIN / ADMIN / USER → unrestricted (sees all).
- CUSTOMER / MSP_ADMIN → only their JWT-granted keys.
- **Create / Edit / Delete**: `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')` only.
- Note: a plain `ADMIN` (or QDS_ADMIN/USER) can *read all* but **cannot mutate** — the write `@PreAuthorize` does not include `ADMIN`. If a "why can't I add a key?" ticket comes in, check the caller's role first.
- Note: a plain `ADMIN` (or USER) can *read all* but **cannot mutate** — the write `@PreAuthorize` does not include `ADMIN`. If a "why can't I add a key?" ticket comes in, check the caller's role first.
## First things to check when it misbehaves
1. **New device won't come online / not visible.** The device's `deviceType` must be in `supported_device_types` for its key, and the key must exist. Registration (agent-proxy → devices `POST /api/internal/atms/register`) returns **403 `{linked:false, error:...}`** when the device type isn't supported. Add the missing type on the Edit panel.
@@ -22,7 +22,7 @@ audience: dev
Each service's `JwtAuthenticationFilter` sets a single authority `ROLE_<role>`.
## Roles
Admin roles are **`MSP_ADMIN`, `BCOS_ADMIN`** (and `QDS_ADMIN`). Most `@PreAuthorize` uses
Admin roles are **`MSP_ADMIN`, `BCOS_ADMIN`**. Most `@PreAuthorize` uses
`hasAnyRole('MSP_ADMIN','BCOS_ADMIN')`. Audience gating (e.g. the guide's internal tabs) keys off
these admin roles.
@@ -44,7 +44,7 @@ Admin/write (not on grid load, `@PreAuthorize hasAnyRole('MSP_ADMIN','BCOS_ADMIN
## Scoping (JWT)
- `InstitutionContext.resolveScope()`: `null` for BCOS_ADMIN/ADMIN/QDS_ADMIN (all ATMs); institution-key list for MSP_ADMIN; single key for CUSTOMER.
- `InstitutionContext.resolveScope()`: `null` for BCOS_ADMIN/ADMIN (all ATMs); institution-key list for MSP_ADMIN; single key for CUSTOMER.
- Admin (`null`) grid = recon positions **merged with all `device_summary` ids**. Scoped users = only ATMs that already have a recon position.
- All endpoints need a valid JWT (`anyRequest().authenticated()`; only `/actuator/health*` public).
@@ -26,7 +26,7 @@ audience: internal
| Role | Scope | What appears in the grid |
|---|---|---|
| BCOS_ADMIN / ADMIN / QDS_ADMIN | `null` (unrestricted) | All recon ATMs **plus** every ATM in `device_summary` (merged in so the whole fleet shows even before first replenishment) |
| BCOS_ADMIN / ADMIN | `null` (unrestricted) | All recon ATMs **plus** every ATM in `device_summary` (merged in so the whole fleet shows even before first replenishment) |
| MSP_ADMIN | list of granted institution keys | Only ATMs that already have a recon position in those institutions |
| CUSTOMER (BANK) | single institution key | Only that institution's ATMs that already have a recon position |
@@ -37,7 +37,7 @@ audience: dev
## Scope (from JWT, `InstitutionContext`)
- BCOS_ADMIN / ADMIN / QDS_ADMIN → scope `null` → reads/writes `institution IS NULL` rows; `all` = every `device_summary` row.
- BCOS_ADMIN / ADMIN → scope `null` → reads/writes `institution IS NULL` rows; `all` = every `device_summary` row.
- CUSTOMER → single institution key.
- MSP_ADMIN → list of granted institution keys.
- GET `selected`: `null`→`findByInstitutionIsNull()`; scoped→`findByInstitutionIn(scope)` (union).
@@ -33,7 +33,7 @@ audience: internal
Scope is resolved from the JWT by `InstitutionContext`:
- **BCOS_ADMIN / ADMIN / QDS_ADMIN** → *unrestricted* (scope = null). They read/write the **global** selection rows (`institution IS NULL`) and see **all** ATMs from `device_summary`.
- **BCOS_ADMIN / ADMIN** → *unrestricted* (scope = null). They read/write the **global** selection rows (`institution IS NULL`) and see **all** ATMs from `device_summary`.
- **CUSTOMER (BANK)** → scoped to their single institution key.
- **MSP_ADMIN** → scoped to their granted institution keys (a list).
@@ -22,7 +22,7 @@ No Kafka. No producers, consumers, or `@KafkaListener` in the codebase.
## API surface
Server port **8098** (`application.properties`). All routes require a valid JWT (`hasAnyRole(USER, CUSTOMER, ADMIN, MSP_ADMIN, QDS_ADMIN, BCOS_ADMIN)`) except `/actuator/health` and `/error`.
Server port **8098** (`application.properties`). All routes require a valid JWT (`hasAnyRole(USER, CUSTOMER, ADMIN, MSP_ADMIN, BCOS_ADMIN)`) except `/actuator/health` and `/error`.
| Method | Path | Purpose |
|--------|------|---------|
@@ -42,7 +42,7 @@ The three proxy controllers accept any HTTP method (`@RequestMapping` without a
## Gotchas
- **`services.ai.url` points at incident, not the AI service.** Default is `http://hiveiq-incident:8081`; incident owns the `/api/adoons/**` endpoints (per the comment in `AdoonsProxyController`). Don't assume Adoons traffic hits hiveops-ai directly.
- **Role check is broad.** `SecurityConfig` gates `/api/**` with `hasAnyRole("USER","CUSTOMER","ADMIN","MSP_ADMIN","QDS_ADMIN","BCOS_ADMIN")` — not the `MSP_ADMIN`/`BCOS_ADMIN`-only pattern used elsewhere. Authorization beyond role is delegated to the upstream services.
- **Role check is broad.** `SecurityConfig` gates `/api/**` with `hasAnyRole("USER","CUSTOMER","ADMIN","MSP_ADMIN","BCOS_ADMIN")` — not the `MSP_ADMIN`/`BCOS_ADMIN`-only pattern used elsewhere. Authorization beyond role is delegated to the upstream services.
- **Only `Authorization` and `Content-Type` headers are forwarded.** `ProxyService` rebuilds a fresh `HttpHeaders` — other inbound headers (custom `X-*`, cookies) are dropped. The `transfer-encoding` response header is stripped from replies.
- **CORS is disabled at the app** (`.cors(AbstractHttpConfigurer::disable)`); nginx is the CORS authority. Allowed origins config key (`cors.allowed-origins`, default `http://localhost:5178`) exists but is not wired into the disabled security chain.
- **JWT secret is required** — `jwt.secret=${JWT_SECRET}` has no default; the service will fail to start without it.
@@ -42,7 +42,7 @@ Request body for analyze: `{transactionId, rawLines, atmId, atmVendor}`. Save PA
## Roles
- Proxy `/api/**`: `hasAnyRole('USER','CUSTOMER','ADMIN','MSP_ADMIN','QDS_ADMIN','BCOS_ADMIN')`.
- Proxy `/api/**`: `hasAnyRole('USER','CUSTOMER','ADMIN','MSP_ADMIN','BCOS_ADMIN')`.
- Journal endpoints: `@PreAuthorize("isAuthenticated()")`; institution isolation via `InstitutionContext.resolveScope()` in service layer.
- incident→ai hop: `X-Internal-Secret`, not user JWT.
@@ -10,7 +10,7 @@ Ops/support view of the Customer Journey drawer (the per-transaction replay in t
## Who can open it
- Any authenticated Transactions user. The proxy (`hiveops-transactions` `SecurityConfig`) gates `/api/**` with `hasAnyRole('USER','CUSTOMER','ADMIN','MSP_ADMIN','QDS_ADMIN','BCOS_ADMIN')` — **not** the `MSP_ADMIN`/`BCOS_ADMIN`-only pattern used elsewhere.
- Any authenticated Transactions user. The proxy (`hiveops-transactions` `SecurityConfig`) gates `/api/**` with `hasAnyRole('USER','CUSTOMER','ADMIN','MSP_ADMIN','BCOS_ADMIN')` — **not** the `MSP_ADMIN`/`BCOS_ADMIN`-only pattern used elsewhere.
- Journal endpoints behind it require only `isAuthenticated()`.
- **Institution scoping is enforced downstream** in journal: cheque lookups (`ChequeImageService.listByTxnId`) and the Adoons-save PATCH filter by the caller's institution scope. A user only sees journeys/cheques for their own institution(s).
@@ -66,4 +66,4 @@ Transactions has **no database**; devices/ATM identity lives elsewhere. See [pla
- Admin endpoints: `@PreAuthorize("hasAnyRole('MSP_ADMIN','BCOS_ADMIN')")`.
- `GET /api/journal/files`: `@PreAuthorize("isAuthenticated()")`, scoped by `InstitutionContext.resolveScope()` in the default branch.
- The transactions proxy gates `/api/**` broadly (`USER,CUSTOMER,ADMIN,MSP_ADMIN,QDS_ADMIN,BCOS_ADMIN`); the real authorization is journal's `@PreAuthorize`. See [platform.service-ownership].
- The transactions proxy gates `/api/**` broadly (`USER,CUSTOMER,ADMIN,MSP_ADMIN,BCOS_ADMIN`); the real authorization is journal's `@PreAuthorize`. See [platform.service-ownership].
@@ -15,7 +15,7 @@ Ops / support / admin view of **Journal Reparse** (Transactions SPA → Settings
| View parse status / file list | Refresh, tab switch, ATM filter | `GET /api/journal/files` | `isAuthenticated()` — any logged-in user |
| Trigger bulk reparse | **Reparse All** button | `POST /api/journal/admin/reparse-all` | **`hasAnyRole('MSP_ADMIN','BCOS_ADMIN')`** |
So a plain user can *see* status but only **MSP_ADMIN / BCOS_ADMIN** can trigger a reparse. If a non-admin clicks Reparse All they get a 403 from the journal service (surfaced as the red error alert). The transactions proxy in front is deliberately loose (`hasAnyRole(USER,CUSTOMER,ADMIN,MSP_ADMIN,QDS_ADMIN,BCOS_ADMIN)` on `/api/**`); the real authorization is enforced downstream by journal's `AdminController`.
So a plain user can *see* status but only **MSP_ADMIN / BCOS_ADMIN** can trigger a reparse. If a non-admin clicks Reparse All they get a 403 from the journal service (surfaced as the red error alert). The transactions proxy in front is deliberately loose (`hasAnyRole(USER,CUSTOMER,ADMIN,MSP_ADMIN,BCOS_ADMIN)` on `/api/**`); the real authorization is enforced downstream by journal's `AdminController`.
## What "Reparse All" actually does