AdminUI: /uns area + line names bypass the UNS segment rule, blocking fleet-wide deploys #535

Closed
opened 2026-08-01 11:03:47 -04:00 by dohertj2 · 1 comment
Owner

The /uns area and line creation modals do not validate the UNS segment format. Only equipment does (EquipmentPage.razor:570, ^[a-z0-9-]{1,32}$ with a hint under the field).

Reproduced on docker-dev while authoring a UNS under SITE-A: an area named Packaging and a line named Line 1 both saved without complaint, and the next deployment failed with

HTTP 422
[UnsSegmentInvalid] UnsArea.Name 'Packaging' does not match [a-z0-9-]{1,32} or '_default';
[UnsSegmentInvalid] UnsLine.Name 'Line 1' does not match [a-z0-9-]{1,32} or '_default'

DraftValidator.ValidateUnsSegments checks all three levels (DraftValidator.cs:604-620); the AdminUI checks one.

Why this is worse than it looks: a deployment snapshots the whole database, so one badly-named area in one cluster blocks every cluster's deploy until someone finds it. The operator who typed the name may not be the one who discovers the fleet cannot deploy.

This is the same authoring-vs-gate gap #520 stage 7 just closed for NodeAcl rows — the fix there was to make the page apply the deploy gate's own rule. Here it is smaller: the regex and message already exist on the equipment form, so the area and line modals need the same [RegularExpression] plus the same hint text. Renaming an existing area/line is a separate path and should get it too.

Found incidentally; not fixed as part of stage 7 (out of scope). Recorded in docs/plans/2026-07-31-apikey-acl-enforcement-design.md under the stage-7 live gate.

The `/uns` **area** and **line** creation modals do not validate the UNS segment format. Only equipment does (`EquipmentPage.razor:570`, `^[a-z0-9-]{1,32}$` with a hint under the field). Reproduced on docker-dev while authoring a UNS under `SITE-A`: an area named **`Packaging`** and a line named **`Line 1`** both saved without complaint, and the next deployment failed with ``` HTTP 422 [UnsSegmentInvalid] UnsArea.Name 'Packaging' does not match [a-z0-9-]{1,32} or '_default'; [UnsSegmentInvalid] UnsLine.Name 'Line 1' does not match [a-z0-9-]{1,32} or '_default' ``` `DraftValidator.ValidateUnsSegments` checks all three levels (`DraftValidator.cs:604-620`); the AdminUI checks one. **Why this is worse than it looks:** a deployment snapshots the **whole database**, so one badly-named area in one cluster blocks **every** cluster's deploy until someone finds it. The operator who typed the name may not be the one who discovers the fleet cannot deploy. This is the same authoring-vs-gate gap #520 stage 7 just closed for `NodeAcl` rows — the fix there was to make the page apply the deploy gate's own rule. Here it is smaller: the regex and message already exist on the equipment form, so the area and line modals need the same `[RegularExpression]` plus the same hint text. Renaming an existing area/line is a separate path and should get it too. Found incidentally; not fixed as part of stage 7 (out of scope). Recorded in `docs/plans/2026-07-31-apikey-acl-enforcement-design.md` under the stage-7 live gate.
Author
Owner

Fixed. master 1a1dbc3f, pushed.

The rule now has one home

Configuration.Validation.UnsSegment — IsValid, AttributePattern, Message, Describe. DraftValidator no longer restates it, and the forms reference it instead of holding a literal. A second copy is how these drifted apart in the first place.

Enforced in the service, not just on the forms

  • CreateAreaAsync / UpdateAreaAsync, CreateLineAsync / UpdateLineAsync, CreateEquipmentAsync / UpdateEquipmentAsync reject an invalid name before touching the row.
  • ImportEquipmentAsync reports a bad name as an error, not a skip. A skip means "you already have this row", which would tell the operator the equipment exists when it does not. Good rows in the same batch still insert.

A form attribute could never have covered the CSV import, and with no bUnit in this repo a rule only a form enforces is also a rule no test can reach. The forms keep an attribute for the immediate message, plus a hint under the field so the mistake is prevented rather than reported.

One behaviour change beyond the fix

AttributePattern admits _default, which the equipment form's hand-written literal rejected. The form was stricter than the gate it was mirroring, so a _default row was uneditable through the AdminUI. Pinned by a test.

Verification

41 tests, falsified with three production mutations, each reddening exactly its own tests:

  • drop the CreateArea guard (the original bug) → the 7 CreateArea rejections
  • turn the import error back into a skip → only the import test
  • narrow AttributePattern to the bare pattern → only the attribute/IsValid agreement test

Every rejection is paired with a name that must still be accepted — a guard that refused everything would satisfy the rejection half alone.

Live-verified on docker-dev:

  • The area modal refuses Packaging in place, with the rule spelled out and the modal held open.
  • packaging-b saves — the positive control.
  • CSV import of two rows → Inserted: 1 · Skipped: 0 · Errors: 1, Row 'packer_003': equipment name 'Packer 3' does not match [a-z0-9-]{1,32} or '_default'. That is the leg that matters: it is the path no form attribute reaches.

Rig cleaned up; a further deployment sealed. Documented in docs/Uns.md §"Naming: area, line and equipment names are UNS path segments", including the id-vs-name distinction (UnsAreaId takes ^[A-Za-z0-9_-]+$ and never reaches a NodeId, so SITEA-area1 is a fine id for an area named packaging).

**Fixed.** `master` `1a1dbc3f`, pushed. ### The rule now has one home `Configuration.Validation.UnsSegment` — `IsValid`, `AttributePattern`, `Message`, `Describe`. `DraftValidator` no longer restates it, and the forms reference it instead of holding a literal. A second copy is how these drifted apart in the first place. ### Enforced in the service, not just on the forms - `CreateAreaAsync` / `UpdateAreaAsync`, `CreateLineAsync` / `UpdateLineAsync`, `CreateEquipmentAsync` / `UpdateEquipmentAsync` reject an invalid name before touching the row. - `ImportEquipmentAsync` reports a bad name as an **error, not a skip**. A skip means *"you already have this row"*, which would tell the operator the equipment exists when it does not. Good rows in the same batch still insert. A form attribute could never have covered the CSV import, and with no bUnit in this repo a rule only a form enforces is also a rule no test can reach. The forms keep an attribute for the immediate message, plus a hint under the field so the mistake is prevented rather than reported. ### One behaviour change beyond the fix `AttributePattern` admits `_default`, which the equipment form's hand-written literal rejected. The form was **stricter than the gate it was mirroring**, so a `_default` row was uneditable through the AdminUI. Pinned by a test. ### Verification 41 tests, falsified with three production mutations, each reddening exactly its own tests: - drop the `CreateArea` guard (the original bug) → the 7 `CreateArea` rejections - turn the import error back into a skip → only the import test - narrow `AttributePattern` to the bare pattern → only the attribute/`IsValid` agreement test Every rejection is paired with a name that must still be **accepted** — a guard that refused everything would satisfy the rejection half alone. **Live-verified on docker-dev:** - The area modal refuses `Packaging` in place, with the rule spelled out and the modal held open. - `packaging-b` saves — the positive control. - CSV import of two rows → **`Inserted: 1 · Skipped: 0 · Errors: 1`**, `Row 'packer_003': equipment name 'Packer 3' does not match [a-z0-9-]{1,32} or '_default'.` That is the leg that matters: it is the path no form attribute reaches. Rig cleaned up; a further deployment sealed. Documented in `docs/Uns.md` §"Naming: area, line and equipment names are UNS path segments", including the id-vs-name distinction (`UnsAreaId` takes `^[A-Za-z0-9_-]+$` and never reaches a NodeId, so `SITEA-area1` is a fine id for an area named `packaging`).
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#535