<# .SYNOPSIS 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)". .NOTES Usage: pwsh ./scripts/compliance/phase-6-2-compliance.ps1 Exit: 0 = all checks passed; non-zero = one or more FAILs #> [CmdletBinding()] param() $ErrorActionPreference = 'Stop' $script:failures = 0 $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path function Assert-Pass { param([string]$Check) Write-Host " [PASS] $Check" -ForegroundColor Green } function Assert-Fail { param([string]$Check, [string]$Reason) Write-Host " [FAIL] $Check - $Reason" -ForegroundColor Red $script:failures++ } function Assert-Deferred { param([string]$Check, [string]$FollowupPr) Write-Host " [DEFERRED] $Check (follow-up: $FollowupPr)" -ForegroundColor Yellow } 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 "=== 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 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 "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" 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: PASS" -ForegroundColor Green exit 0 } Write-Host "Phase 6.2 compliance: $script:failures FAIL(s)" -ForegroundColor Red exit 1