Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoPipelineFolderPermission/Get-AzDoPipelineFolderPermission.ps1
|
<#
.SYNOPSIS Retrieves the current ACL state of an Azure DevOps pipeline folder. .DESCRIPTION Builds the 'Build' ACL token for the folder and compares its ACL against the desired permissions. A pipeline folder's token addresses the folder by path - '{projectId}/{folderPath}' - whereas a pipeline definition's token uses its numeric id. Omitting FolderPath targets the project build root, whose token is the project id alone. .PARAMETER ProjectName The name of the Azure DevOps project. .PARAMETER FolderPath The pipeline folder path. Omit to target the project build root. .PARAMETER isInherited Whether the ACL inherits permissions from its parent. .PARAMETER Permissions The desired access control entries. .PARAMETER LookupResult The lookup result supplied by the DSC base class. .PARAMETER Ensure The desired state, supplied by the DSC base class. .PARAMETER Force Forces the operation, supplied by the DSC base class. .EXAMPLE Get-AzDoPipelineFolderPermission -ProjectName 'Contoso' -FolderPath '\Platform' -isInherited $true #> Function Get-AzDoPipelineFolderPermission { [CmdletBinding()] [OutputType([System.Management.Automation.PSObject[]])] param ( [Parameter(Mandatory = $true)] [Alias('Name')] [System.String]$ProjectName, [Parameter()] [Alias('Path')] [System.String]$FolderPath, [Parameter(Mandatory = $true)] [System.Boolean]$isInherited, [Parameter()] [HashTable[]]$Permissions, [Parameter()] [HashTable]$LookupResult, [Parameter()] [Ensure]$Ensure, [Parameter()] [System.Management.Automation.SwitchParameter]$Force ) Write-Verbose "[Get-AzDoPipelineFolderPermission] Started." $SecurityNamespace = 'Build' $OrganizationName = Get-AzDoOrganizationName $getResult = @{ Ensure = [Ensure]::Absent propertiesChanged = @() status = $null reason = $null } $projectCache = Resolve-AzDoProject -ProjectName $ProjectName if (-not $projectCache) { $getResult.status = [DSCGetSummaryState]::Error $getResult.reason = "Project not found: $ProjectName" return $getResult } $normalizedPath = Format-AzDoPipelineFolderPath -Path $FolderPath $isRoot = ([String]::IsNullOrWhiteSpace($FolderPath) -or $normalizedPath -eq '\') if (-not $isRoot) { $folders = List-DevOpsPipelineFolders -Organization $OrganizationName -ProjectName $ProjectName -Path $normalizedPath $folder = $folders | Where-Object { (Format-AzDoPipelineFolderPath -Path $_.path) -eq $normalizedPath } | Select-Object -First 1 if ($null -eq $folder) { Write-Warning "[Get-AzDoPipelineFolderPermission] Pipeline folder '$normalizedPath' not found in project '$ProjectName'." $getResult.status = [DSCGetSummaryState]::NotFound $getResult.reason = "Pipeline folder not found: $normalizedPath" return $getResult } } $namespace = Get-CacheItem -Key $SecurityNamespace -Type 'SecurityNamespaces' if (-not $namespace) { Write-Error "[Get-AzDoPipelineFolderPermission] Security namespace '$SecurityNamespace' not found." $getResult.status = [DSCGetSummaryState]::Error return $getResult } $getResult.namespace = $namespace # The API token carries the folder path without its leading separator. $tokenFolderPath = $normalizedPath.TrimStart('\') $aclToken = if ($isRoot) { '{0}' -f $projectCache.id } else { '{0}/{1}' -f $projectCache.id, $tokenFolderPath } $DevOpsACLs = Get-DevOpsACL -OrganizationName $OrganizationName -SecurityDescriptorId $namespace.namespaceId -Token $aclToken if (-not $DevOpsACLs) { $DevOpsACLs = Get-DevOpsACL -OrganizationName $OrganizationName -SecurityDescriptorId $namespace.namespaceId } # Drop the ACLs this lookup cannot be interested in BEFORE formatting them, exactly as # Get-AzDoProjectPermission and Get-AzDoProcessPermission already do. Formatting resolves every # ACE through Find-Identity, which costs an API round trip for each descriptor that is not # already cached, so formatting a whole namespace only to keep one token is where the time goes. # The fallback above fires whenever the folder has no explicit ACL - the normal state # once permissions revert to inherited - so the full namespace is the common path, not a rare # one. The root case is an anchored exact token. The folder case re-uses the same # Format-AzDoPipelineFolderPath normalisation the parsed filter below applies, so both keep the # same set: the API spells the folder path several ways and a raw -eq would drop valid ACLs. $DevOpsACLs = @($DevOpsACLs | Where-Object { $rawToken = $_.token if ([String]::IsNullOrEmpty($rawToken)) { return $false } if ($isRoot) { return ($rawToken -eq $aclToken) } $prefix = '{0}/' -f $projectCache.id if (-not $rawToken.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) { return $false } (Format-AzDoPipelineFolderPath -Path $rawToken.Substring($prefix.Length)) -eq $normalizedPath }) $DifferenceACLs = $DevOpsACLs | ConvertTo-FormattedACL -SecurityNamespace $SecurityNamespace -OrganizationName $OrganizationName if ($isRoot) { # The build root is the token with no folder path and no definition id. $DifferenceACLs = $DifferenceACLs | Where-Object { ($_.Token.Type -eq 'Build') -and ($_.Token.ProjectId -eq $projectCache.id) -and (-not $_.Token.PipelineId) } } else { $DifferenceACLs = $DifferenceACLs | Where-Object { ($_.Token.Type -eq 'BuildFolder') -and ($_.Token.ProjectId -eq $projectCache.id) -and ((Format-AzDoPipelineFolderPath -Path $_.Token.FolderPath) -eq $normalizedPath) } } # The resource-side token keeps the leading separator on the folder path: New-ACLToken uses it # to tell a folder from a pipeline of the same name. $tokenName = if ($isRoot) { $ProjectName } else { '{0}/{1}' -f $ProjectName, $normalizedPath } $params = @{ Permissions = $Permissions SecurityNamespace = $SecurityNamespace isInherited = $isInherited OrganizationName = $OrganizationName TokenName = $tokenName } $ReferenceACLs = ConvertTo-ACL @params $compareResult = Test-ACLListforChanges -ReferenceACLs $ReferenceACLs -DifferenceACLs $DifferenceACLs $getResult.propertiesChanged = $compareResult.propertiesChanged $getResult.status = [DSCGetSummaryState]::"$($compareResult.status)" $getResult.reason = $compareResult.reason $getResult.ReferenceACLs = $ReferenceACLs $getResult.DifferenceACLs = $DifferenceACLs $getResult.aclToken = $aclToken return $getResult } |