diff --git a/docs/v2/implementation/phase-6-2-authorization-runtime.md b/docs/v2/implementation/phase-6-2-authorization-runtime.md index 5000e73..7088656 100644 --- a/docs/v2/implementation/phase-6-2-authorization-runtime.md +++ b/docs/v2/implementation/phase-6-2-authorization-runtime.md @@ -1,6 +1,12 @@ # Phase 6.2 — Authorization Runtime (ACL + LDAP grants) -> **Status**: DRAFT — the v2 `plan.md` decision #129 + `acl-design.md` specify a 6-level permission-trie evaluator with `NodePermissions` bitmask grants, but no runtime evaluator exists. ACL tables are schematized but unread by the data path. +> **Status**: **SHIPPED (core)** 2026-04-19 — Streams A, B, C (foundation), D (data layer) merged to `v2` across PRs #84-87. Final exit-gate PR #88 turns the compliance stub into real checks (all pass, 2 deferred surfaces tracked). +> +> Deferred follow-ups (tracked separately): +> - Stream C dispatch wiring on the 11 OPC UA operation surfaces (task #143). +> - Stream D Admin UI — RoleGrantsTab, AclsTab Probe-this-permission, SignalR invalidation, draft-diff ACL section + visual-compliance reviewer signoff (task #144). +> +> Baseline pre-Phase-6.2: 1042 solution tests → post-Phase-6.2 core: 1097 passing (+55 net). One pre-existing Client.CLI Subscribe flake unchanged. > > **Branch**: `v2/phase-6-2-authorization-runtime` > **Estimated duration**: 2.5 weeks diff --git a/scripts/compliance/phase-6-2-compliance.ps1 b/scripts/compliance/phase-6-2-compliance.ps1 index 49cba0c..bf64508 100644 --- a/scripts/compliance/phase-6-2-compliance.ps1 +++ b/scripts/compliance/phase-6-2-compliance.ps1 @@ -1,31 +1,23 @@ <# .SYNOPSIS - Phase 6.2 exit-gate compliance check — stub. Each `Assert-*` either passes - (Write-Host green) or throws. Non-zero exit = fail. + Phase 6.2 exit-gate compliance check. Each check either passes or records a + failure; non-zero exit = fail. .DESCRIPTION Validates Phase 6.2 (Authorization runtime) completion. Checks enumerated in `docs/v2/implementation/phase-6-2-authorization-runtime.md` §"Compliance Checks (run at exit gate)". - Current status: SCAFFOLD. Every check writes a TODO line and does NOT throw. - Each implementation task in Phase 6.2 is responsible for replacing its TODO - with a real check before closing that task. - .NOTES Usage: pwsh ./scripts/compliance/phase-6-2-compliance.ps1 - Exit: 0 = all checks passed (or are still TODO); non-zero = explicit fail + Exit: 0 = all checks passed; non-zero = one or more FAILs #> [CmdletBinding()] param() $ErrorActionPreference = 'Stop' $script:failures = 0 - -function Assert-Todo { - param([string]$Check, [string]$ImplementationTask) - Write-Host " [TODO] $Check (implement during $ImplementationTask)" -ForegroundColor Yellow -} +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path function Assert-Pass { param([string]$Check) @@ -34,47 +26,121 @@ function Assert-Pass { function Assert-Fail { param([string]$Check, [string]$Reason) - Write-Host " [FAIL] $Check — $Reason" -ForegroundColor Red + Write-Host " [FAIL] $Check - $Reason" -ForegroundColor Red $script:failures++ } -Write-Host "" -Write-Host "=== Phase 6.2 compliance — Authorization runtime ===" -ForegroundColor Cyan -Write-Host "" +function Assert-Deferred { + param([string]$Check, [string]$FollowupPr) + Write-Host " [DEFERRED] $Check (follow-up: $FollowupPr)" -ForegroundColor Yellow +} -Write-Host "Stream A — LdapGroupRoleMapping (control plane)" -Assert-Todo "Control/data-plane separation — Core.Authorization has zero refs to LdapGroupRoleMapping" "Stream A.2" -Assert-Todo "Authoring validation — AclsTab rejects duplicate (LdapGroup, Scope) pre-save" "Stream A.3" +function Assert-FileExists { + param([string]$Check, [string]$RelPath) + $full = Join-Path $repoRoot $RelPath + if (Test-Path $full) { Assert-Pass "$Check ($RelPath)" } + else { Assert-Fail $Check "missing file: $RelPath" } +} + +function Assert-TextFound { + param([string]$Check, [string]$Pattern, [string[]]$RelPaths) + foreach ($p in $RelPaths) { + $full = Join-Path $repoRoot $p + if (-not (Test-Path $full)) { continue } + if (Select-String -Path $full -Pattern $Pattern -Quiet) { + Assert-Pass "$Check (matched in $p)" + return + } + } + Assert-Fail $Check "pattern '$Pattern' not found in any of: $($RelPaths -join ', ')" +} + +function Assert-TextAbsent { + param([string]$Check, [string]$Pattern, [string[]]$RelPaths) + foreach ($p in $RelPaths) { + $full = Join-Path $repoRoot $p + if (-not (Test-Path $full)) { continue } + if (Select-String -Path $full -Pattern $Pattern -Quiet) { + Assert-Fail $Check "pattern '$Pattern' unexpectedly found in $p" + return + } + } + Assert-Pass "$Check (pattern '$Pattern' absent from: $($RelPaths -join ', '))" +} Write-Host "" -Write-Host "Stream B — Evaluator + trie + cache" -Assert-Todo "Trie invariants — PermissionTrieBuilder idempotent (build twice == equal)" "Stream B.1" -Assert-Todo "Additive grants + cluster isolation — cross-cluster leakage impossible" "Stream B.1" -Assert-Todo "Galaxy FolderSegment coverage — folder-subtree grant cascades; siblings unaffected" "Stream B.2" -Assert-Todo "Redundancy-safe invalidation — generation-mismatch forces trie re-load on peer" "Stream B.4" -Assert-Todo "Membership freshness — 15 min interval elapsed + LDAP down = fail-closed" "Stream B.5" -Assert-Todo "Auth cache fail-closed — 5 min AuthCacheMaxStaleness exceeded = NotGranted" "Stream B.5" -Assert-Todo "AuthorizationDecision shape — Allow + NotGranted only; Denied variant exists unused" "Stream B.6" +Write-Host "=== Phase 6.2 compliance - Authorization runtime ===" -ForegroundColor Cyan +Write-Host "" + +Write-Host "Stream A - LdapGroupRoleMapping (control plane)" +Assert-FileExists "LdapGroupRoleMapping entity present" "src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/LdapGroupRoleMapping.cs" +Assert-FileExists "AdminRole enum present" "src/ZB.MOM.WW.OtOpcUa.Configuration/Enums/AdminRole.cs" +Assert-FileExists "ILdapGroupRoleMappingService present" "src/ZB.MOM.WW.OtOpcUa.Configuration/Services/ILdapGroupRoleMappingService.cs" +Assert-FileExists "LdapGroupRoleMappingService impl present" "src/ZB.MOM.WW.OtOpcUa.Configuration/Services/LdapGroupRoleMappingService.cs" +Assert-TextFound "Write-time invariant: IsSystemWide XOR ClusterId" "IsSystemWide=true requires ClusterId" @("src/ZB.MOM.WW.OtOpcUa.Configuration/Services/LdapGroupRoleMappingService.cs") +Assert-FileExists "EF migration for LdapGroupRoleMapping" "src/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260419131444_AddLdapGroupRoleMapping.cs" Write-Host "" -Write-Host "Stream C — OPC UA operation wiring" -Assert-Todo "Every operation wired — Browse/Read/Write/HistoryRead/HistoryUpdate/CreateMonitoredItems/TransferSubscriptions/Call/Ack/Confirm/Shelve" "Stream C.1-C.7" -Assert-Todo "HistoryRead uses its own flag — Read+no-HistoryRead denies HistoryRead" "Stream C.3" -Assert-Todo "Mixed-batch semantics — 3 allowed + 2 denied returns per-item status, no coarse failure" "Stream C.6" -Assert-Todo "Browse ancestor visibility — deep grant implies ancestor browse; denied ancestors filter" "Stream C.7" -Assert-Todo "Subscription re-authorization — revoked grant surfaces BadUserAccessDenied in one publish" "Stream C.5" +Write-Host "Stream B - Permission-trie evaluator (Core.Authorization)" +Assert-FileExists "OpcUaOperation enum present" "src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/OpcUaOperation.cs" +Assert-FileExists "NodeScope record present" "src/ZB.MOM.WW.OtOpcUa.Core/Authorization/NodeScope.cs" +Assert-FileExists "AuthorizationDecision tri-state" "src/ZB.MOM.WW.OtOpcUa.Core/Authorization/AuthorizationDecision.cs" +Assert-TextFound "Verdict has Denied member (reserved for v2.1)" "Denied" @("src/ZB.MOM.WW.OtOpcUa.Core/Authorization/AuthorizationDecision.cs") +Assert-FileExists "IPermissionEvaluator present" "src/ZB.MOM.WW.OtOpcUa.Core/Authorization/IPermissionEvaluator.cs" +Assert-FileExists "PermissionTrie present" "src/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrie.cs" +Assert-FileExists "PermissionTrieBuilder present" "src/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrieBuilder.cs" +Assert-FileExists "PermissionTrieCache present" "src/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrieCache.cs" +Assert-TextFound "Cache keyed on GenerationId" "GenerationId" @("src/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrieCache.cs") +Assert-FileExists "UserAuthorizationState present" "src/ZB.MOM.WW.OtOpcUa.Core/Authorization/UserAuthorizationState.cs" +Assert-TextFound "MembershipFreshnessInterval default 15 min" "FromMinutes\(15\)" @("src/ZB.MOM.WW.OtOpcUa.Core/Authorization/UserAuthorizationState.cs") +Assert-TextFound "AuthCacheMaxStaleness default 5 min" "FromMinutes\(5\)" @("src/ZB.MOM.WW.OtOpcUa.Core/Authorization/UserAuthorizationState.cs") +Assert-FileExists "TriePermissionEvaluator impl present" "src/ZB.MOM.WW.OtOpcUa.Core/Authorization/TriePermissionEvaluator.cs" +Assert-TextFound "HistoryRead maps to NodePermissions.HistoryRead" "HistoryRead.+NodePermissions\.HistoryRead" @("src/ZB.MOM.WW.OtOpcUa.Core/Authorization/TriePermissionEvaluator.cs") Write-Host "" -Write-Host "Stream D — Admin UI + SignalR invalidation" -Assert-Todo "SignalR invalidation — sp_PublishGeneration pushes PermissionTrieCache invalidate < 2 s" "Stream D.4" +Write-Host "Control/data-plane separation (decision #150)" +Assert-TextAbsent "Evaluator has zero references to LdapGroupRoleMapping" "LdapGroupRoleMapping" @( + "src/ZB.MOM.WW.OtOpcUa.Core/Authorization/TriePermissionEvaluator.cs", + "src/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrie.cs", + "src/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrieBuilder.cs", + "src/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrieCache.cs", + "src/ZB.MOM.WW.OtOpcUa.Core/Authorization/IPermissionEvaluator.cs") + +Write-Host "" +Write-Host "Stream C foundation (dispatch-wiring gate)" +Assert-FileExists "ILdapGroupsBearer present" "src/ZB.MOM.WW.OtOpcUa.Server/Security/ILdapGroupsBearer.cs" +Assert-FileExists "AuthorizationGate present" "src/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationGate.cs" +Assert-TextFound "Gate has StrictMode knob" "StrictMode" @("src/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationGate.cs") +Assert-Deferred "DriverNodeManager dispatch-path wiring (11 surfaces)" "Phase 6.2 Stream C follow-up task #143" + +Write-Host "" +Write-Host "Stream D data layer (ValidatedNodeAclAuthoringService)" +Assert-FileExists "ValidatedNodeAclAuthoringService present" "src/ZB.MOM.WW.OtOpcUa.Admin/Services/ValidatedNodeAclAuthoringService.cs" +Assert-TextFound "InvalidNodeAclGrantException present" "class InvalidNodeAclGrantException" @("src/ZB.MOM.WW.OtOpcUa.Admin/Services/ValidatedNodeAclAuthoringService.cs") +Assert-TextFound "Rejects None permissions" "Permission set cannot be None" @("src/ZB.MOM.WW.OtOpcUa.Admin/Services/ValidatedNodeAclAuthoringService.cs") +Assert-Deferred "RoleGrantsTab + AclsTab Probe-this-permission + SignalR invalidation + draft diff section" "Phase 6.2 Stream D follow-up task #144" Write-Host "" Write-Host "Cross-cutting" -Assert-Todo "No test-count regression — dotnet test ZB.MOM.WW.OtOpcUa.slnx count ≥ pre-Phase-6.2 baseline" "Final exit-gate" +Write-Host " Running full solution test suite..." -ForegroundColor DarkGray +$prevPref = $ErrorActionPreference +$ErrorActionPreference = 'Continue' +$testOutput = & dotnet test (Join-Path $repoRoot 'ZB.MOM.WW.OtOpcUa.slnx') --nologo 2>&1 +$ErrorActionPreference = $prevPref +$passLine = $testOutput | Select-String 'Passed:\s+(\d+)' -AllMatches +$failLine = $testOutput | Select-String 'Failed:\s+(\d+)' -AllMatches +$passCount = 0; foreach ($m in $passLine.Matches) { $passCount += [int]$m.Groups[1].Value } +$failCount = 0; foreach ($m in $failLine.Matches) { $failCount += [int]$m.Groups[1].Value } +$baseline = 1042 +if ($passCount -ge $baseline) { Assert-Pass "No test-count regression ($passCount >= $baseline pre-Phase-6.2 baseline)" } +else { Assert-Fail "Test-count regression" "passed $passCount < baseline $baseline" } + +if ($failCount -le 1) { Assert-Pass "No new failing tests (pre-existing CLI flake tolerated)" } +else { Assert-Fail "New failing tests" "$failCount failures > 1 tolerated" } Write-Host "" if ($script:failures -eq 0) { - Write-Host "Phase 6.2 compliance: scaffold-mode PASS (all checks TODO)" -ForegroundColor Green + Write-Host "Phase 6.2 compliance: PASS" -ForegroundColor Green exit 0 } Write-Host "Phase 6.2 compliance: $script:failures FAIL(s)" -ForegroundColor Red diff --git a/src/ZB.MOM.WW.OtOpcUa.Core/Authorization/IPermissionEvaluator.cs b/src/ZB.MOM.WW.OtOpcUa.Core/Authorization/IPermissionEvaluator.cs index acb0b01..1a6b4fd 100644 --- a/src/ZB.MOM.WW.OtOpcUa.Core/Authorization/IPermissionEvaluator.cs +++ b/src/ZB.MOM.WW.OtOpcUa.Core/Authorization/IPermissionEvaluator.cs @@ -8,8 +8,8 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Authorization; /// /// /// Data-plane only. Reads NodeAcl rows joined against the session's resolved LDAP -/// groups (via ). Must not depend on -/// LdapGroupRoleMapping (control-plane) per decision #150. +/// groups (via ). Must not depend on the control-plane +/// admin-role mapping table per decision #150 — the two concerns share zero runtime code. /// public interface IPermissionEvaluator {