Public/New-NSPConditionalAccessDeploymentRun.ps1
|
function New-NSPConditionalAccessDeploymentRun { <# .SYNOPSIS Creates a run journal from a plan's approved items. Performs none of them. .DESCRIPTION The journal (.nsp-conditionalaccess\runs\<TenantId>\<RunId>.json, Git-ignored) holds, in plan order, every Approved item with its stages. Each stage is saved as it moves Pending -> Running -> Succeeded / Failed, so a run survives a closed window or a reboot. Invoke-NSPConditionalAccessDeploymentRun performs it. Refuses a plan with no approved items. Items that depend on a prerequisite (DependsOn) must have every prerequisite approved in the same plan. .EXAMPLE $run = New-NSPConditionalAccessDeploymentRun -Plan $plan.Path #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)][string]$Plan, [string]$TenantProfile, [string]$ConfigRoot ) $content = Get-Content -LiteralPath $Plan -Raw | ConvertFrom-Json if (-not $content.PlanId) { throw "$Plan is not a deployment plan." } $approved = @($content.Items | Where-Object Decision -eq 'Approved') if (-not $approved.Count) { throw 'The plan has no approved items. Use Set-NSPConditionalAccessPlanDecision first.' } $approvedIds = @($approved | ForEach-Object ItemId) foreach ($item in $approved) { $missing = @($item.DependsOn | Where-Object { $_ -and $_ -notin $approvedIds }) if ($missing.Count) { throw "$($item.ItemId) depends on $($missing -join ', '), which is not approved." } } if (-not $TenantProfile) { $TenantProfile = Join-Path (Get-NSPConditionalAccessLocalPath -Kind TenantProfiles -ConfigRoot $ConfigRoot) "$($content.TenantId).json" } if (-not (Test-Path -LiteralPath $TenantProfile)) { throw "Tenant profile not found: $TenantProfile" } $versions = @{} foreach ($definition in Get-NSPConditionalAccessDefinition) { $versions[$definition['Id']] = $definition['Version'] } $runId = (Get-Date).ToUniversalTime().ToString('yyyyMMdd-HHmmss') + '-' + ([guid]::NewGuid().ToString('N').Substring(0, 6)) $run = [pscustomobject][ordered]@{ SchemaVersion = 1 RunId = $runId PlanId = $content.PlanId PlanPath = (Resolve-Path -LiteralPath $Plan).Path ProfilePath = (Resolve-Path -LiteralPath $TenantProfile).Path TenantId = $content.TenantId TenantDomain = $content.TenantDomain CreatedAtUtc = (Get-Date).ToUniversalTime().ToString('o') Status = 'Pending' Items = @($approved | ForEach-Object { $item = $_ [pscustomobject][ordered]@{ ItemId = $item.ItemId Kind = $item.Kind Action = $item.Action DefinitionId = $item.DefinitionId DefinitionVersion = if ($item.DefinitionId) { $versions[$item.DefinitionId] } else { $null } DisplayName = $item.DisplayName DesiredBody = $item.DesiredBody DesiredHash = $item.DesiredHash LiveObjectId = $item.LiveObjectId LiveHash = $item.LiveHash LiveState = $item.LiveState ObjectId = $item.LiveObjectId Result = 'Pending' Notes = @() Stages = @(Get-NSPConditionalAccessRunStage -Action $item.Action | ForEach-Object { [pscustomobject]@{ Name = $_; Status = 'Pending'; Message = $null; StartedAtUtc = $null; FinishedAtUtc = $null } }) } }) Path = $null } $run.Path = Join-Path (Join-Path (Get-NSPConditionalAccessLocalPath -Kind Runs -ConfigRoot $ConfigRoot) $content.TenantId) "$runId.json" if ($PSCmdlet.ShouldProcess($run.Path, "Create run journal for $($approved.Count) approved item(s)")) { Save-NSPConditionalAccessJson -InputObject $run -Path $run.Path } $run } |