Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoIterationPermission/Get-AzDoIterationPermission.ps1
|
Function Get-AzDoIterationPermission { [CmdletBinding()] [OutputType([System.Management.Automation.PSObject[]])] param ( [Parameter(Mandatory = $true)] [string]$ProjectName, [Parameter(Mandatory = $false)] [string]$IterationPath, [Parameter(Mandatory = $true)] [bool]$isInherited, [Parameter()] [HashTable[]]$Permissions, [Parameter()] [HashTable]$LookupResult, [Parameter()] [Ensure]$Ensure, [Parameter()] [System.Management.Automation.SwitchParameter] $Force ) Write-Verbose "[Get-AzDoIterationPermission] Started." # Define the Descriptor Type and Organization Name # https://learn.microsoft.com/en-us/azure/devops/organizations/security/namespace-reference?view=azure-devops $SecurityNamespace = 'Iteration' # Manages Iteration path object-level permissions. $OrganizationName = (Get-AzDoOrganizationName) Write-Verbose "[Get-AzDoIterationPermission] Security Namespace: $SecurityNamespace" Write-Verbose "[Get-AzDoIterationPermission] Organization Name: $OrganizationName" Write-Verbose "[Get-AzDoIterationPermission] Project Name: $ProjectName" if ([String]::IsNullOrEmpty($IterationPath)) { Write-Warning "[Get-AzDoIterationPermission] IterationPath not specified. Defaulting to top-level Project permissions" $IterationPath = $null } else { Write-Verbose "[Get-AzDoIterationPermission] IterationPath: $IterationPath" } # # Construct a hashtable detailing the group $results = @{ Ensure = [Ensure]::Absent propertiesChanged = @() project = $ProjectName IterationPath = $IterationPath status = $null reason = $null identifiers = $null } Write-Verbose "[Get-AzDoIterationPermission] Group result hashtable constructed." Write-Verbose "[Get-AzDoIterationPermission] Performing lookup of permissions for the IterationPath." # Define the ACL List $ACLList = [System.Collections.Generic.List[Hashtable]]::new() # Perform a Lookup within the Cache for the Project $projectCache = Get-CacheItem -Key $ProjectName -Type 'LiveProjects' # If not in cache, fall back to a live API lookup if (-not $projectCache) { Write-Verbose "[Get-AzDoIterationPermission] Project '$ProjectName' not in cache — falling back to live API lookup." $projectCache = Invoke-AzDevOpsApiRestMethod -Uri "https://dev.azure.com/$OrganizationName/_apis/projects/${ProjectName}?api-version=7.1-preview.4" -Method Get if ($projectCache) { Add-CacheItem -Key $ProjectName -Value $projectCache -Type 'LiveProjects' } } # Test if the Project was found if (-not $projectCache) { Write-Warning "[Get-AzDoIterationPermission] Project not found: $ProjectName" $results.status = [DSCGetSummaryState]::Error $results.reason = "Project not found: $ProjectName" return $results } # Test if the IterationPath was specified if ($IterationPath) { Write-Verbose "[Get-AzDoIterationPermission] IterationPath Name: $IterationPath is not null." # Format the IterationPath to retrieve all of the IterationPath nodes $FormattedIterationPaths = Format-AzDoIterationPath -Iteration $IterationPath -ProjectName $ProjectName | Get-AllAzDoClassificationNodePaths } else { Write-Verbose "[Get-AzDoIterationPermission] IterationPath Name: $IterationPath is null." # If IterationPath is not specified, get the top-level Iteration path $FormattedIterationPaths = @("\$ProjectName\Iteration") } # Perform a Lookup within the Cache for the IterationPath [Array]$IterationPaths = $FormattedIterationPaths | ForEach-Object { Write-Verbose "[Get-AzDoIterationPermission] IterationPath: $_" # Get the cached item for the IterationPath and add it to the list Get-CacheItem -Key $_ -Type 'LiveIterations' } # If iteration nodes not in cache, fall back to a live API lookup if ($IterationPaths.count -ne $FormattedIterationPaths.Count) { Write-Verbose "[Get-AzDoIterationPermission] Iteration path nodes not in cache — falling back to live API lookup." $liveNodes = List-DevOpsClassificationNodes -OrganizationName $OrganizationName -ProjectName $ProjectName foreach ($node in $liveNodes) { if ($node.structureType -eq 'iteration') { Format-ClassificationNode -Node $node -CacheType 'LiveIterations' } } [Array]$IterationPaths = $FormattedIterationPaths | ForEach-Object { Get-CacheItem -Key $_ -Type 'LiveIterations' } } # Ensure that the number of cached iteration path nodes is the same as the formatted nodes. if ($IterationPaths.count -ne $FormattedIterationPaths.Count) { Write-Warning "[Get-AzDoIterationPermission] The iteration path nodes do not match the formatted iteration path nodes." $results.status = [DSCGetSummaryState]::Error $results.reason = "The iteration path nodes do not match the formatted iteration path nodes." return $results } # Once the IterationPath is found, we can get the identifiers $identifierArr = $IterationPaths | ForEach-Object { $_.identifier } # Update the results. This is used to construct regex expressions. $results.identifiers = $identifierArr # # Perform Lookup of the Permissions $namespace = Get-CacheItem -Key $SecurityNamespace -Type 'SecurityNamespaces' Write-Verbose "[Get-AzDoIterationPermission] Retrieved namespace: $($namespace.namespaceId)" # Add to the ACL Lookup Params $results.namespace = $namespace # Token-scope the ACL fetch to this iteration path's classification-node token instead of scanning # the entire org-wide CSS namespace. Fall back to the full-namespace fetch if the scoped query # returns nothing, so behaviour is never worse than the previous full scan. $aclToken = ($identifierArr | ForEach-Object { 'vstfs:///Classification/Node/{0}' -f $_ }) -join ':' $ACLLookupParams = @{ OrganizationName = $OrganizationName SecurityDescriptorId = $namespace.namespaceId } if ($aclToken) { $ACLLookupParams.Token = $aclToken } # Get the ACL List and format the ACLS Write-Verbose "[Get-AzDoIterationPermission] ACL Lookup Params: $($ACLLookupParams | Out-String)" # Get the ACLs for the IterationPath. A $null result for a token-scoped query is a valid # "no ACL for this token yet" answer, not a signal to fall back to a full-namespace scan - # falling back reintroduces exactly the wrong-scope bug this token-scoping was added to fix. $DevOpsACLs = Get-DevOpsACL @ACLLookupParams # Convert the ACLs to a formatted ACL. Wrap in @() so $DifferenceACLs is always an array; # ConvertTo-FormattedACL returns a generic List that PowerShell unrolls to a bare hashtable # when there is only one entry, making [0] indexing in Test-ACLListforChanges return $null. $DifferenceACLs = @($DevOpsACLs | ConvertTo-FormattedACL -SecurityNamespace $SecurityNamespace -OrganizationName $OrganizationName) # Filter the ACLs for the IterationPath token (always apply to avoid matching unrelated ACLs). $DifferenceACLs = @($DifferenceACLs | Where-Object { $_.Token.Type -eq 'IterationPathPermission' } | Where-Object { if ($_.token.Identifiers.Count -ne $identifierArr.Count) { return $false } foreach ($item in $identifierArr) { if ($_.token.Identifiers.identifier -notcontains $item) { return $false } } return $true }) Write-Verbose "[Get-AzDoIterationPermission] ACL List retrieved and formatted." Write-Verbose "[Get-AzDoIterationPermission] Difference ACLs Count: $($DifferenceACLs.Count)" # # Convert the Permissions into an ACL Token $params = @{ Permissions = $Permissions SecurityNamespace = $SecurityNamespace isInherited = $isInherited OrganizationName = $OrganizationName TokenName = $(($identifierArr | ForEach-Object { "vstfs:///Classification/Node/{0}" -f $_ }) -join ':') } # Convert the Permissions to an ACL Token. Wrap in @() for the same single-item-unwrap reason as above. $ReferenceACLs = @(ConvertTo-ACL @params) # Compare the Reference ACLs to the Difference ACLs $compareResult = Test-ACLListforChanges -ReferenceACLs $ReferenceACLs -DifferenceACLs $DifferenceACLs $results.propertiesChanged = $compareResult.propertiesChanged $results.status = [DSCGetSummaryState]::"$($compareResult.status)" $results.reason = $compareResult.reason Write-Verbose "[Get-AzDoIterationPermission] ACL Token converted." Write-Verbose "[Get-AzDoIterationPermission] ACL Token Comparison Result: $($results.status)" # Export the ACL List to a file $results.ReferenceACLs = $ReferenceACLs $results.DifferenceACLs = $DifferenceACLs # Write Write-Verbose "[Get-AzDoIterationPermission] Result Status: $($results.status)" Write-Verbose "[Get-AzDoIterationPermission] Returning Group Result." # Return the Group Result return $results } |