docs(audit): Reservations.md — accuracy pass

STALE-STATUS / CODE-REALITY fixes:
- Table row ReleasedAt/ReleasedBy: "FleetAdmin" → "Administrator" (AdminRole
  enum renamed in CanonicalizeAdminRoles migration).  ReleasedBy now documents
  that it is the LDAP operator name passed as explicit @ReleasedBy param — not
  SUSER_SNAME() — per migration 20260522000001_AddReleasedByToReleaseExternalIdReservation.
- §4 Release: "FleetAdmin" → "Administrator"; added @ReleasedBy required param
  requirement matching the updated stored-proc signature; replaced "SUSER_SNAME()"
  attribution claim with the correct explicit-param description.
- §The Admin page: replaced entirely.  Actual Reservations.razor uses bare
  [Authorize] (not [Authorize(Policy="FleetAdmin")] and not "CanPublish").
  The page is a read-only flat list (no Active/Released split, no Release row
  action, no Release dialog).  Redirected release-flow readers to
  docs/v2/admin-ui.md §"Release an external-ID reservation".

Evidence:
  src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Reservations.razor:2
  src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/AdminRole.cs:36
  src/Server/ZB.MOM.WW.OtOpcUa.Security/ServiceCollectionExtensions.cs:130
  src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260522000001_AddReleasedByToReleaseExternalIdReservation.cs
This commit is contained in:
Joseph Doherty
2026-06-03 16:22:08 -04:00
parent 60d2fdf25c
commit da074adce9
+17 -16
View File
@@ -52,7 +52,7 @@ is refreshed, and they are eventually *released* — but never silently deleted.
| `ClusterId` | The first cluster to publish the reservation. |
| `FirstPublishedAt` / `FirstPublishedBy` | When and by whom the claim was first made. |
| `LastPublishedAt` | Refreshed on every subsequent publish that re-asserts the same `(Kind, Value, EquipmentUuid)`. |
| `ReleasedAt` / `ReleasedBy` / `ReleaseReason` | Non-null once a FleetAdmin explicitly releases the claim. A row with `ReleasedAt IS NULL` is *active*. |
| `ReleasedAt` / `ReleasedBy` / `ReleaseReason` | Non-null once an Administrator explicitly releases the claim. `ReleasedBy` is the LDAP operator name (passed explicitly as `@ReleasedBy`; not `SUSER_SNAME()`). A row with `ReleasedAt IS NULL` is *active*. |
There is no foreign key from `EquipmentUuid` / `ClusterId` to their tables — by
design, so a reservation survives the deletion or disabling of the equipment
@@ -99,14 +99,16 @@ being disabled, the generation being superseded, or a rollback.
### 4. Release
Reusing an identifier for a **different** piece of equipment requires a
FleetAdmin to explicitly release the existing claim. Release runs
Reusing an identifier for a **different** piece of equipment requires an
Administrator to explicitly release the existing claim. Release runs
`sp_ReleaseExternalIdReservation`, which:
- Requires a non-empty **reason** — a hard audit invariant; the procedure
raises an error without one.
- Stamps `ReleasedAt`, `ReleasedBy` (`SUSER_SNAME()`), and `ReleaseReason`
rather than deleting the row, so the history is preserved.
- Requires a non-empty **`@ReleasedBy`** — the LDAP operator name supplied
by the caller; the procedure raises an error without it.
- Stamps `ReleasedAt`, `ReleasedBy` (the supplied operator name), and
`ReleaseReason` rather than deleting the row, so the history is preserved.
- Once released, the `(Kind, Value)` pair is free — a different
`EquipmentUuid` can claim it on a future publish.
@@ -116,20 +118,19 @@ permanent for the life of the asset.
## The Admin page
`/reservations` (Admin UI) is the operator surface. It is **FleetAdmin-only**
(the `CanPublish` policy).
`/reservations` (Admin UI) is the operator surface. It requires authentication
(`[Authorize]`) but is not restricted to a specific Admin UI role — any signed-in
user can view it.
- **Active** table — every reservation with `ReleasedAt IS NULL`: kind, value,
owning `EquipmentUuid`, cluster, and the first/last publish stamps. Each row
has a **Release** action.
- **Released** table — the 100 most recently released reservations, with the
releasing user and reason.
- **Release dialog** — opened from an active row; it requires a reason before
the Release button will submit, mirroring the procedure's audit invariant.
The page is a **read-only flat list** of all `ExternalIdReservation` rows,
ordered by Kind then Value. It shows Kind, Value, owning `EquipmentUuid`, and
Cluster. There is no Active/Released split, no Release action, and no Release
dialog on this page.
You cannot *create* a reservation from this page — reservations only ever come
into existence as a side-effect of publishing a generation. The page is for
inspection and for the release flow.
into existence as a side-effect of publishing a generation. The release flow
is described in `docs/v2/admin-ui.md` § "Release an external-ID reservation"
and runs via `sp_ReleaseExternalIdReservation`.
## Related