Add data-path ACL design (acl-design.md, closes corrections B1) + dev-environment inventory and setup plan (dev-environment.md), and remove consumer cutover from OtOpcUa v2 scope.

ACL design defines NodePermissions bitmask flags covering Browse / Read / Subscribe / HistoryRead / WriteOperate / WriteTune / WriteConfigure / AlarmRead / AlarmAcknowledge / AlarmConfirm / AlarmShelve / MethodCall plus common bundles (ReadOnly / Operator / Engineer / Admin); 6-level scope hierarchy (Cluster / Namespace / UnsArea / UnsLine / Equipment / Tag) with default-deny + additive grants and Browse-implication on ancestors; per-LDAP-group grants in a new generation-versioned NodeAcl table edited via the same draft → diff → publish → rollback boundary as every other content table; per-session permission-trie evaluator with O(depth × group-count) cost cached for the lifetime of the session and rebuilt on generation-apply or LDAP group cache expiry; cluster-create workflow seeds a default ACL set matching the v1 LmxOpcUa LDAP-role-to-permission map for v1 → v2 consumer migration parity; Admin UI ACL tab with two views (by LDAP group, by scope), bulk-grant flow, and permission simulator that lets operators preview "as user X" effective permissions across the cluster's UNS tree before publishing; explicit Deny deferred to v2.1 since verbose grants suffice at v2.0 fleet sizes; only denied OPC UA operations are audit-logged (not allowed ones — would dwarf the audit log). Schema doc gains the NodeAcl table with cross-cluster invariant enforcement and same-generation FK validation; admin-ui.md gains the ACLs tab; phase-1 doc gains Task E.9 wiring this through Stream E plus a NodeAcl entry in Task B.1's DbContext list.

Dev-environment doc inventories every external resource the v2 build needs across two tiers per decision #99 — inner-loop (in-process simulators on developer machines: SQL Server local or container, GLAuth at C:\publish\glauth\, local dev Galaxy) and integration (one dedicated Windows host with Docker Desktop on WSL2 backend so TwinCAT XAR VM can run in Hyper-V alongside containerized oitc/modbus-server, plus WSL2-hosted Snap7 and ab_server, plus OPC Foundation reference server, plus FOCAS TestStub and FaultShim) — with concrete container images, ports, default dev credentials (clearly marked dev-only since production uses Integrated Security / gMSA per decision #46), bootstrap order for both tiers, network topology diagram, test data seed locations, and operational risks (TwinCAT trial expiry automation, Docker pricing, integration host SPOF mitigation, per-developer GLAuth config sync, Aveva license scoping that keeps Galaxy tests on developer machines and off the shared host).

Removes consumer cutover (ScadaBridge / Ignition / System Platform IO) from OtOpcUa v2 scope per decision #136 — owned by a separate integration / operations team, tracked in 3-year-plan handoff §"Rollout Posture" and corrections §C5; OtOpcUa team's scope ends at Phase 5. Updates implementation/overview.md phase index to drop the "6+" row and add an explicit "OUT of v2 scope" callout; updates phase-1 and phase-2 docs to reframe cutover as integration-team-owned rather than future-phase numbered.

Decisions #129–137 added: ACL model (#129), NodeAcl generation-versioned (#130), v1-compatibility seed (#131), denied-only audit logging (#132), two-tier dev environment (#133), Docker WSL2 backend for TwinCAT VM coexistence (#134), TwinCAT VM centrally managed / Galaxy on dev machines only (#135), cutover out of v2 scope (#136), dev credentials documented openly (#137).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-04-17 11:58:33 -04:00
parent 2a6c9828e4
commit 4903a19ec9
8 changed files with 735 additions and 7 deletions

View File

@@ -410,6 +410,47 @@ All five are exposed as **OPC UA properties** on the equipment node so external
`EquipmentClassRef` is a nullable string hook; v2.0 ships with no validation. When the central `schemas` repo lands, this becomes a foreign key into the schemas-repo equipment-class catalog, validated at draft-publish time.
### `NodeAcl`
```sql
CREATE TABLE dbo.NodeAcl (
NodeAclRowId uniqueidentifier NOT NULL PRIMARY KEY DEFAULT NEWSEQUENTIALID(),
GenerationId bigint NOT NULL FOREIGN KEY REFERENCES dbo.ConfigGeneration(GenerationId),
NodeAclId nvarchar(64) NOT NULL, -- stable logical ID across generations
ClusterId nvarchar(64) NOT NULL FOREIGN KEY REFERENCES dbo.ServerCluster(ClusterId),
LdapGroup nvarchar(256) NOT NULL,
ScopeKind nvarchar(16) NOT NULL CHECK (ScopeKind IN ('Cluster', 'Namespace', 'UnsArea', 'UnsLine', 'Equipment', 'Tag')),
ScopeId nvarchar(64) NULL, -- NULL when ScopeKind='Cluster'; logical ID otherwise
PermissionFlags int NOT NULL, -- bitmask of NodePermissions enum
Notes nvarchar(512) NULL
);
CREATE INDEX IX_NodeAcl_Generation_Cluster
ON dbo.NodeAcl (GenerationId, ClusterId);
CREATE INDEX IX_NodeAcl_Generation_Group
ON dbo.NodeAcl (GenerationId, LdapGroup);
CREATE INDEX IX_NodeAcl_Generation_Scope
ON dbo.NodeAcl (GenerationId, ScopeKind, ScopeId) WHERE ScopeId IS NOT NULL;
CREATE UNIQUE INDEX UX_NodeAcl_Generation_LogicalId
ON dbo.NodeAcl (GenerationId, NodeAclId);
-- Within a generation, a (Group, Scope) pair has at most one row
CREATE UNIQUE INDEX UX_NodeAcl_Generation_GroupScope
ON dbo.NodeAcl (GenerationId, ClusterId, LdapGroup, ScopeKind, ScopeId);
```
`NodeAcl` is **generation-versioned** (decision #130). ACL changes go through draft → diff → publish → rollback like every other content table. Cross-generation invariant: `NodeAclId` once published with `(LdapGroup, ScopeKind, ScopeId)` cannot have any of those columns change in a future generation; rename an LDAP group by disabling the old grant and creating a new one.
`PermissionFlags` is a bitmask of the `NodePermissions` enum defined in `acl-design.md` (Browse, Read, Subscribe, HistoryRead, WriteOperate, WriteTune, WriteConfigure, AlarmRead, AlarmAcknowledge, AlarmConfirm, AlarmShelve, MethodCall). Common bundles (`ReadOnly`, `Operator`, `Engineer`, `Admin`) expand to specific flag combinations at evaluation time.
Validation in `sp_ValidateDraft`:
- `ScopeId` must resolve in the same generation when `ScopeKind ≠ 'Cluster'`
- Resolved scope must belong to the same `ClusterId` as the ACL row (cross-cluster bindings rejected, same pattern as decision #122)
- `PermissionFlags` must contain only bits defined in `NodePermissions`
- `LdapGroup` non-empty, ≤256 chars, allowlisted character set (no LDAP-DN-breaking chars)
- Cross-generation identity stability per the invariant above
Full evaluation algorithm + Admin UI design + v1-compatibility seed in `acl-design.md`.
### `ExternalIdReservation`
```sql