docs(guide): incident-management per-institution ownership mode (#221)
Document the MSP_MANAGED/CUSTOMER_MANAGED incident ownership mode across the
Claude/Internal/Architect/Testing/Customer tabs: the write matrix, the
/institution-keys/{key}/incident-mode endpoint + MSP toggle, controller-layer
assertWritable enforcement, IncidentDTO.institution, V90 migration, and the
known UI follow-up (customer nav still hidden).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,24 @@ audience: dev
|
||||
|
||||
`PARKED` is a first-class status: while any incident for a device is `PARKED`, `IncidentAutoCreationService` suppresses new auto-incidents for that device (#219).
|
||||
|
||||
## Authorization — per-institution ownership mode (#221)
|
||||
|
||||
Incident writes are authorized in two layers:
|
||||
|
||||
1. **Coarse role gate** — every mutating endpoint carries `@PreAuthorize("hasAnyRole('MSP_ADMIN','BCOS_ADMIN','CUSTOMER')")`.
|
||||
2. **Per-institution mode** — the controller then calls `IncidentService.assertWritable(incident)` (or `assertCreatable(atmId)` for manual create), which resolves the incident's institution name → `institution_keys.incident_management_mode` and applies:
|
||||
|
||||
| Mode | Writers |
|
||||
|------|---------|
|
||||
| `MSP_MANAGED` (default) | BCOS_ADMIN, MSP_ADMIN |
|
||||
| `CUSTOMER_MANAGED` | BCOS_ADMIN (override), that institution's CUSTOMER users |
|
||||
|
||||
Design notes:
|
||||
- The guard is invoked **only from the HTTP controllers** (`IncidentController`, `IncidentManagementController`) — deliberately **not** from `saveIncident`/`createIncident`, which the Kafka/journal auto-creation path (`IncidentAutoCreationService`, `AriaThreatEventConsumer`) also calls with no user role in context.
|
||||
- The mode column lives in incident's **own** `institution_keys` copy (incident-domain setting → stays inside the incident service; the canonical institution record in hiveops-devices is untouched). The MSP admin UI writes it via `PUT /api/institution-keys/{key}/incident-mode` (upsert, since incident's copy can lag devices').
|
||||
- `IncidentDTO.institution` (institution name) is surfaced so the SPA can gate action controls and show a Read-only badge; `institutionKeyAPI.getAccessible()` returns `incidentManagementMode` for the client's institution→mode map.
|
||||
- Migration: `V90__institution_incident_mode.sql` (`ALTER TABLE institution_keys ADD COLUMN incident_management_mode VARCHAR(20) NOT NULL DEFAULT 'MSP_MANAGED'`).
|
||||
|
||||
## Kafka — incident lifecycle (producer side)
|
||||
|
||||
`IncidentLifecycleProducer` publishes on create and update (config `IncidentLifecycleKafkaConfig`, 3 partitions each):
|
||||
|
||||
@@ -28,11 +28,27 @@ audience: dev
|
||||
| DELETE | `/api/incident-management/{id}/link/{linkedId}` | MSP_ADMIN/BCOS_ADMIN |
|
||||
| GET | `/api/incident-management/{id}/links` · `/audit-trail` · `/stats/summary` | any auth |
|
||||
| GET | `/api/incident-management/technicians/available` | any auth |
|
||||
| PUT | `/api/institution-keys/{key}/incident-mode` (set ownership mode, #221) | MSP_ADMIN/BCOS_ADMIN |
|
||||
|
||||
† Mutating rows list the `@PreAuthorize` gate. Since #221 that gate also admits `CUSTOMER`, and the real write decision is the per-institution **ownership mode** — see *Roles & per-institution ownership mode* below.
|
||||
|
||||
Bulk close body: `{ "incidentIds": ["1","2"], "newStatus": "CLOSED" }` (ids are strings).
|
||||
|
||||
## Roles
|
||||
- Every mutation: `@PreAuthorize("hasAnyRole('MSP_ADMIN','BCOS_ADMIN')")`. Reads: authenticated only.
|
||||
## Roles & per-institution ownership mode (#221)
|
||||
Reads: authenticated only. Mutations are gated in **two layers**:
|
||||
1. `@PreAuthorize("hasAnyRole('MSP_ADMIN','BCOS_ADMIN','CUSTOMER')")` on every mutating endpoint.
|
||||
2. Service-layer `IncidentService.assertWritable(incident)` / `assertCreatable(atmId)` then enforces the
|
||||
institution's **incident ownership mode** (`institution_keys.incident_management_mode`):
|
||||
|
||||
| Mode | BCOS_ADMIN | MSP_ADMIN | CUSTOMER (own institution) |
|
||||
|------|-----------|-----------|----------------------------|
|
||||
| `MSP_MANAGED` (default) | ✅ | ✅ | ❌ read-only |
|
||||
| `CUSTOMER_MANAGED` | ✅ (override) | ❌ read-only | ✅ |
|
||||
|
||||
- Mode is resolved from the incident's institution **name** (`incident.getAtm().getInstitution()`), looked up in incident's own `institution_keys` copy; unknown institution ⇒ `MSP_MANAGED` (unchanged behaviour).
|
||||
- Set it: `PUT /api/institution-keys/{key}/incident-mode` body `{"incidentManagementMode":"MSP_MANAGED"|"CUSTOMER_MANAGED"}` (MSP_ADMIN/BCOS_ADMIN; **upserts** incident's institution_keys row). Surfaced in the **MSP app** → Institution Keys → "Incident management".
|
||||
- `IncidentDTO.institution` carries the institution name so the frontend can gate controls (hidden actions + a **Read-only** badge in the detail panel).
|
||||
- The guard lives at the **controller layer only** — NOT in `saveIncident`/`createIncident`, which Kafka/journal auto-creation also uses, so auto-created incidents are unaffected.
|
||||
|
||||
## Tables (`hiveiq_incident`)
|
||||
- `incidents`, `incident_events`, `incident_notes`, `workflow_transitions`, `atms`, `technicians`, `helpdesk_persons`, `device_summary` (Kafka mirror), `incident_embeddings`.
|
||||
@@ -50,6 +66,7 @@ Bulk close body: `{ "incidentIds": ["1","2"], "newStatus": "CLOSED" }` (ids are
|
||||
- Transition requires a valid `workflow_transitions` edge; if `requiresAssignment`, assignee must be set first (else 400).
|
||||
- Closing blocked by `assertNoUnfixedChildren()` if linked children unfixed.
|
||||
- **Possible Fraud note-before-close is FRONTEND-only** (`IncidentList.svelte` fraud-close modal) — NOT enforced by the API.
|
||||
- **MSP_ADMIN gets 403 on a `CUSTOMER_MANAGED` institution's incidents — this is intended** (#221), not a bug. Check the institution's `incident_management_mode`; only BCOS_ADMIN overrides it. Conversely a CUSTOMER *can* now write their own incidents when their institution is `CUSTOMER_MANAGED`.
|
||||
- Agents never call this service; they go via **hiveops-agent-proxy** → `/api/internal/**` (`X-Internal-Secret`).
|
||||
- `device_summary` is a read-only Kafka mirror from hiveops-devices; no reconciler → stale = fix upstream.
|
||||
- `DB_URL` default points at a stale `hiveops_incident` DB — prod overrides to `hiveiq_incident`.
|
||||
@@ -57,7 +74,8 @@ Bulk close body: `{ "incidentIds": ["1","2"], "newStatus": "CLOSED" }` (ids are
|
||||
## Do NOT
|
||||
- Do NOT add device/institution writes here — device data is owned by hiveops-devices.
|
||||
- Do NOT assume the fraud-note rule protects direct API closes — it doesn't.
|
||||
- Do NOT expect UPDATE/transition without an admin JWT — reads succeed, writes 403.
|
||||
- Do NOT assume "MSP_ADMIN can always write" — since #221, a `CUSTOMER_MANAGED` institution makes MSP_ADMIN read-only (only BCOS_ADMIN overrides); and CUSTOMER can write when their institution is customer-managed.
|
||||
- Do NOT move the write-mode guard into `saveIncident`/`createIncident` — it would 403 Kafka/journal auto-creation (no user role in context). Keep it at the controller layer.
|
||||
- Do NOT split `DB_URL` into host/port/name.
|
||||
- Do NOT create incidents for a parked device expecting auto-flow — parking silences it.
|
||||
- Do NOT invent transition endpoints per-status (start/resolve/close); all go through one `POST /{id}/transition` with `newStatus`.
|
||||
|
||||
@@ -6,11 +6,11 @@ order: 20
|
||||
audience: internal
|
||||
---
|
||||
|
||||
> Ops/support/admin view for the Incident Management screen (`IncidentList.svelte`). Served by **hiveops-incident** (port 8080, NGINX `/api/incident/`). All facts below grounded in source as of 2026-07-01.
|
||||
> Ops/support/admin view for the Incident Management screen (`IncidentList.svelte`). Served by **hiveops-incident** (port 8080, NGINX `/api/incident/`). All facts below grounded in source as of 2026-07-03.
|
||||
|
||||
## Who can do what
|
||||
|
||||
Read/list is open to any authenticated user. **Every mutating action requires `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')`** (`@PreAuthorize` on `IncidentManagementController`):
|
||||
Read/list is open to any authenticated user. Every mutating action is gated by `@PreAuthorize` **and** by the institution's **incident ownership mode** (#221 — see below):
|
||||
|
||||
| Action | Endpoint | Role gate |
|
||||
|--------|----------|-----------|
|
||||
@@ -21,7 +21,24 @@ Read/list is open to any authenticated user. **Every mutating action requires `h
|
||||
| Link / unlink / group | `POST /api/incident-management/{id}/link` · `DELETE .../link/{linkedId}` · `POST .../group` | admin |
|
||||
| Create incident (manual) | `POST /api/incident-management` or `POST /api/incidents` | admin |
|
||||
|
||||
If an admin reports "buttons do nothing / 403 on save," first confirm their JWT actually carries `MSP_ADMIN` or `BCOS_ADMIN` — a plain customer role can load the list but cannot transition or assign.
|
||||
If an admin reports "buttons do nothing / 403 on save," first confirm their JWT carries `MSP_ADMIN`/`BCOS_ADMIN` — **and** check the institution's ownership mode (below), because a `CUSTOMER_MANAGED` institution deliberately makes MSP admins read-only.
|
||||
|
||||
## Per-institution incident ownership mode (#221)
|
||||
|
||||
Each institution has an **incident ownership mode** (`institution_keys.incident_management_mode`) deciding who may work its incidents:
|
||||
|
||||
| Mode | BCOS_ADMIN | MSP_ADMIN | Customer (own institution) |
|
||||
|------|-----------|-----------|----------------------------|
|
||||
| **MSP-managed** (`MSP_MANAGED`, default) | ✅ | ✅ | read-only |
|
||||
| **Customer-managed** (`CUSTOMER_MANAGED`) | ✅ (override) | **read-only** | ✅ |
|
||||
|
||||
Use *Customer-managed* when an institution runs its own fleet/help-desk and just needs HiveIQ as a view (the AOCU case). BCOS_ADMIN always retains write as a platform override.
|
||||
|
||||
- **Set it:** in the **MSP app** → *Institution Keys* → edit an institution → **"Incident management"** dropdown (a mode badge also shows in the list). API: `PUT /api/institution-keys/{key}/incident-mode` `{"incidentManagementMode":"…"}`.
|
||||
- **In the incident UI**, non-writers see a **Read-only** badge in the detail panel and the status/assign/notes/bulk/add controls are hidden; the backend also returns **403** on direct API calls (`assertWritable`).
|
||||
- Reads are never affected — everyone in scope still sees the incidents.
|
||||
|
||||
> "MSP admin says a whole institution went read-only" → that institution is `CUSTOMER_MANAGED`. Intended. Change it back via the MSP toggle, or act as BCOS_ADMIN.
|
||||
|
||||
## First things to check when it misbehaves
|
||||
|
||||
|
||||
@@ -33,6 +33,9 @@ Your working list of everything that needs attention across your fleet. Use this
|
||||
1. Tick the checkboxes on the incidents you want.
|
||||
2. **Close** them in bulk, or **🔗 Link** two or more together (they must belong to the same device).
|
||||
|
||||
## Who manages your incidents
|
||||
Each institution is set to either **MSP-managed** (your managed-service provider works the incidents on your behalf) or **Customer-managed** (your own team owns them). If your institution is managed by your provider, incidents appear **read-only** to you — you can still view everything, but changes are made by your provider. Your BCOS administrator sets this per institution.
|
||||
|
||||
## What you're looking at
|
||||
- **ATM** — the device the incident belongs to.
|
||||
- **Type** — what kind of problem it is (e.g. Card Reader Fail, Cassette Low).
|
||||
|
||||
@@ -34,4 +34,18 @@ Incidents are auto-raised from the simulator devices — **C1-ATM-001…**, **C1
|
||||
| 9 | As `msp_a` scan the ATM column; then log in as `bcos_a` and compare | `msp_a` sees only **C1 and C2** devices (no C3); `bcos_a` sees **C1, C2 and C3** |
|
||||
| 10 | Log in as `customer_a` and look at the left sidebar | There is **no Incident Mgmt menu** — read-only customers cannot open this list |
|
||||
|
||||
## Per-institution ownership mode (#221)
|
||||
|
||||
Each institution can be **MSP-managed** (default: MSP works incidents, customer read-only) or **Customer-managed** (the institution's own customer users work incidents, MSP read-only; BCOS_ADMIN always overrides). Set the mode in the **MSP app** → *Institution Keys* → **"Incident management"**.
|
||||
|
||||
| # | Do this | ✅ Pass if |
|
||||
|---|---------|-----------|
|
||||
| 11 | In the **MSP app**, open *Institution Keys* | Each row shows an **Incidents** badge — *MSP-managed* or *Customer-managed* |
|
||||
| 12 | Edit an institution (e.g. **C1BD**), set **Incident management → Customer-managed**, save | Saves without error; the row badge flips to **Customer-managed** |
|
||||
| 13 | As `msp_a`, open a **C1BD** incident (now customer-managed) | Detail panel shows a **Read-only** badge; the status/assign/notes/bulk/**+ Add** controls are **hidden**. A **C2BD** incident still shows all controls |
|
||||
| 14 | As `msp_a`, try to change a C1BD incident via API `POST /api/incident-management/{id}/notes` | Returns **403** (MSP is read-only for a customer-managed institution) |
|
||||
| 15 | Set C1BD back to **MSP-managed** in the MSP app | `msp_a` regains full controls on C1BD incidents |
|
||||
|
||||
> **Known limitation (follow-up):** customer-role users still have no Incident Mgmt menu (`App.svelte` hides it for all customers), so in Customer-managed mode a customer can currently only work incidents via the API, not the UI. Exposing the list to customer-managed customers is tracked as follow-up to #221.
|
||||
|
||||
**Report a fail with:** the row #, which login you used, and what you actually saw.
|
||||
|
||||
Reference in New Issue
Block a user