internal/functions/Get-PolicySetVersionedDetails.ps1
|
function Get-PolicySetVersionedDetails { <# .SYNOPSIS Resolves a Policy Set assignment's definitionVersion to a concrete published version and returns the member details and required roleDefinitionIds for that version. .DESCRIPTION EPAC reads deployed Policy Set definitions at their latest version. An assignment which pins definitionVersion runs a different version, whose member list and hard-coded member effects can differ. Calculating the Managed Identity roles from the latest version therefore grants the wrong roles: roles can be missing for members which only deploy in the pinned version. This resolves the pinned version from the Policy Set's published version history and recomputes both the member details and the role union against it. Resolution follows the wildcard forms EPAC's assignment schema allows, 'XX.*.*' and 'XX.XX.*', and also accepts an exact version. Stable versions are preferred; a pre-release version is only selected when no stable version matches the requested pattern. Any failure to resolve - no version history, an unmatched pattern or a REST error - returns $null so the caller falls back to the latest definition and its unfiltered roles. .PARAMETER PolicySetId Resource id of the Policy Set definition. .PARAMETER DefinitionVersion The version or version wildcard pinned by the assignment. .PARAMETER PacEnvironment The pacEnvironment, used for the policySetDefinitionVersions API version. .PARAMETER PolicyDetails Policy details hashtable keyed by Policy definition id, as produced by Convert-PolicyResourcesToDetails. .PARAMETER PolicyRoleIds Hashtable of definition id to roleDefinitionIds, used to build the member role union. .PARAMETER Cache Hashtable used to memoize version history lookups across assignments in one build. .OUTPUTS Hashtable with policySetDetails, policyRoleDefinitionIds and resolvedVersion, or $null. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $PolicySetId, [Parameter(Mandatory = $true)] [string] $DefinitionVersion, [Parameter(Mandatory = $true)] $PacEnvironment, [Parameter(Mandatory = $true)] [hashtable] $PolicyDetails, [Parameter(Mandatory = $true)] $PolicyRoleIds, [Parameter(Mandatory = $false)] [hashtable] $Cache = $null ) #region retrieve version history $versions = $null if ($null -ne $Cache -and $Cache.ContainsKey($PolicySetId)) { $versions = $Cache.$PolicySetId } else { $apiVersion = $PacEnvironment.apiVersions.policySetDefinitionVersions if (-not $apiVersion) { $apiVersion = $PacEnvironment.apiVersions.policySetDefinitions } $versions = Get-AzPolicySetDefinitionVersionsRestMethod -PolicySetId $PolicySetId -ApiVersion $apiVersion if ($null -ne $Cache) { $Cache[$PolicySetId] = $versions } } if ($null -eq $versions -or @($versions).Count -eq 0) { return $null } #endregion retrieve version history #region resolve the requested version # A pattern may carry a pre-release suffix, for example '1.*.*-preview', which opts the # assignment in to pre-release versions of the matching line. $pattern = $DefinitionVersion $preReleaseRequested = $false $patternDashIndex = $pattern.IndexOf("-") if ($patternDashIndex -ge 0) { $pattern = $pattern.Substring(0, $patternDashIndex) $preReleaseRequested = $true } $patternSegments = $pattern.Split(".") $stableMatches = [System.Collections.ArrayList]::new() $preReleaseMatches = [System.Collections.ArrayList]::new() foreach ($candidate in $versions) { $name = $candidate.name if (-not $name) { continue } # Split '1.3.0-preview' into the numeric core '1.3.0' and the pre-release label 'preview'. $core = $name $isPreRelease = $false $dashIndex = $name.IndexOf("-") if ($dashIndex -ge 0) { $core = $name.Substring(0, $dashIndex) $isPreRelease = $true } $coreSegments = $core.Split(".") if ($coreSegments.Count -ne $patternSegments.Count) { continue } $isMatch = $true for ($i = 0; $i -lt $patternSegments.Count; $i++) { if ($patternSegments[$i] -eq "*") { continue } if ($patternSegments[$i] -ne $coreSegments[$i]) { $isMatch = $false break } } if (-not $isMatch) { continue } $sortableVersion = $null if (-not [System.Version]::TryParse($core, [ref] $sortableVersion)) { continue } $entry = @{ definition = $candidate version = $sortableVersion name = $name isPreRelease = $isPreRelease } if ($isPreRelease) { $null = $preReleaseMatches.Add($entry) } else { $null = $stableMatches.Add($entry) } } # A pre-release is only used when the requested pattern matches nothing stable, unless the # pattern explicitly asked for pre-releases. $candidates = $stableMatches if ($preReleaseRequested) { $candidates = [System.Collections.ArrayList]::new() $candidates.AddRange($stableMatches) $candidates.AddRange($preReleaseMatches) } if ($candidates.Count -eq 0) { $candidates = $preReleaseMatches } if ($candidates.Count -eq 0) { Write-Verbose "No published version of '$PolicySetId' matches definitionVersion '$DefinitionVersion'" return $null } # Ties between a stable and a pre-release of the same core version favour the pre-release only # when the pattern asked for one. $resolved = $candidates | Sort-Object -Property @{ Expression = { $_.version } }, @{ Expression = { $_.isPreRelease -eq $preReleaseRequested } } -Descending | Select-Object -First 1 #endregion resolve the requested version #region calculate details and roles for the resolved version $versionedDetails = @{} Convert-PolicySetToDetails ` -PolicySetId $PolicySetId ` -PolicySetDefinition $resolved.definition ` -PolicySetDetails $versionedDetails ` -PolicyDetails $PolicyDetails $policySetDetails = $versionedDetails.$PolicySetId if ($null -eq $policySetDetails) { return $null } # Union the roles of the members present in this version, matching how Build-DeploymentPlans # calculates the role union for the latest version. $roleIds = [ordered]@{} $properties = Get-PolicyResourceProperties -PolicyResource $resolved.definition foreach ($policyInPolicySet in $properties.policyDefinitions) { $policyId = $policyInPolicySet.policyDefinitionId if ($PolicyRoleIds.ContainsKey($policyId)) { foreach ($roleDefinitionId in $PolicyRoleIds.$policyId) { $roleIds[$roleDefinitionId] = "added" } } } return @{ policySetDetails = $policySetDetails policyRoleDefinitionIds = @($roleIds.Keys) resolvedVersion = $resolved.name } #endregion calculate details and roles for the resolved version } |