Functions/AzureDevOps/Get-ADOPermission.ps1
function Get-ADOPermission { <# .Synopsis Gets Azure DevOps Permissions .Description Gets Azure DevOps security permissions. .Example Get-ADOPermission -Organization MyOrganization -Project MyProject -PersonalAccessToken $pat .Example Get-ADOProject -Organization MyOrganization -Project MyProject | # Get the project Get-ADOTeam | # get the teams within the project Get-ADOPermission -Dashboard # get the dashboard permissions of each team within the project. .Link https://docs.microsoft.com/en-us/rest/api/azure/devops/security/access%20control%20lists/query .Link https://docs.microsoft.com/en-us/rest/api/azure/devops/security/security%20namespaces/query .Link https://docs.microsoft.com/en-us/azure/devops/organizations/security/namespace-reference #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("Test-ForParameterSetAmbiguity", "", Justification="Ambiguity Desired.")] [OutputType('PSDevOps.SecurityNamespace', 'PSDevOps.AccessControlList')] param( # The Organization. [Parameter(Mandatory,ValueFromPipelineByPropertyName)] [Alias('Org')] [string] $Organization, # If set, will list the type of permisssions. [Parameter(ParameterSetName='securitynamespaces')] [Alias('SecurityNamespace', 'ListPermissionType','ListSecurityNamespace')] [switch] $PermissionType, # The Security Namespace ID. # For details about each namespace, see: # https://docs.microsoft.com/en-us/azure/devops/organizations/security/namespace-reference [Parameter(Mandatory,ValueFromPipelineByPropertyName, ParameterSetName='accesscontrollists/{NamespaceId}')] [string] $NamespaceID, # The Security Token. [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='accesscontrollists/{NamespaceId}')] [string] $SecurityToken, # The Project ID. # If this is provided without anything else, will get permissions for the projectID [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='Project')] [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='Analytics')] [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='EndpointID')] [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='AreaPath')] [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='Dashboard')] [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='Tagging')] [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='ManageTFVC')] [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='BuildDefinition')] [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='BuildPermission')] [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='IterationPath')] [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='RepositoryID')] [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='ProjectRepository')] [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='ProjectOverview')] [Alias('Project')] [string] $ProjectID, # If provided, will get permissions related to a given teamID. ( see Get-ADOTeam) [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='Dashboard')] [string] $TeamID, # If provided, will get permissions related to an Area Path. ( see Get-ADOAreaPath ) [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='AreaPath')] [string] $AreaPath, # If provided, will get permissions related to an Iteration Path. ( see Get-ADOIterationPath ) [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='IterationPath')] [string] $IterationPath, # If set, will get common permissions related to a project. # These are: # * Builds # * Boards # * Dashboards # * Git Repositories # * ServiceEndpoints # * Project Permissions # * Service Endpoints # * ServiceHooks [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='ProjectOverview')] [Alias('ProjectOverview')] [switch] $Overview, # If set, will get permissions for tagging related to the current project. [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='Tagging')] [switch] $Tagging, # If set, will get permissions for analytics related to the current project. [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='Analytics')] [switch] $Analytics, # If set, will get permissions for Team Foundation Version Control related to the current project. [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='ManageTFVC')] [switch] $ManageTFVC, # If set, will get permissions for Delivery Plans. [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='Plan')] [switch] $Plan, # If set, will get dashboard permissions related to the current project. [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='Dashboard')] [Alias('Dashboards')] [switch] $Dashboard, # If set, will get all service endpoints permissions. [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='ServiceEndpoint')] [Alias('ServiceEndpoints')] [switch] $ServiceEndpoint, # If set, will get endpoint permissions related to a particular endpoint. [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='EndpointID')] [string] $EndpointID, # The Build Definition ID [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='BuildDefinition')] [string] $DefinitionID, # The path to the build. [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='BuildDefinition')] [string] $BuildPath ='/', # If set, will get build and release permissions for a given project. [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='BuildPermission')] [switch] $BuildPermission, # If provided, will get build and release permissions for a given project's repositoryID [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='RepositoryID')] [string] $RepositoryID, # If provided, will get permissions for a given branch within a repository [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='RepositoryID')] [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='AllRepositories')] [string] $BranchName, # If set, will get permissions for repositories within a project [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='ProjectRepository')] [Alias('ProjectRepositories')] [switch] $ProjectRepository, # If set, will get permissions for repositories within a project [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='AllRepositories')] [Alias('AllRepositories')] [switch] $AllRepository, # The Descriptor [Parameter(ValueFromPipelineByPropertyName)] [string[]] $Descriptor, # If set and this is a hierarchical namespace, return child ACLs of the specified token. [Parameter(ValueFromPipelineByPropertyName)] [switch] $Recurse, # If set, populate the extended information properties for the access control entries in the returned lists. [Parameter(ValueFromPipelineByPropertyName)] [switch] $IncludeExtendedInfo, # If set, will expand the ACE dictionary returned [Alias('ACL')] [switch] $ExpandACL, # The server. By default https://dev.azure.com/. # To use against TFS, provide the tfs server URL (e.g. http://tfsserver:8080/tfs). [Parameter(ValueFromPipelineByPropertyName)] [uri] $Server = "https://dev.azure.com/", # The api version. By default, 5.1-preview. # If targeting TFS, this will need to change to match your server version. # See: https://docs.microsoft.com/en-us/azure/devops/integrate/concepts/rest-api-versioning?view=azure-devops [string] $ApiVersion = "5.1-preview") dynamicParam { . $GetInvokeParameters -DynamicParameter } begin { #region Copy Invoke-ADORestAPI parameters $invokeParams = . $getInvokeParameters $PSBoundParameters #endregion Copy Invoke-ADORestAPI parameters $q = [Collections.Queue]::new() $innerInvokeParams = @{} + $invokeParams $innerInvokeParams.Remove('AsJob') $innerInvokeParams.Remove('ExpandACL') } process { $ParameterSet = $psCmdlet.ParameterSetName if ($psCmdlet.ParameterSetName -notin 'securitynamespaces', 'accesscontrollists/{NamespaceId}') { $in = $_ $PSBoundParameters['InputObject'] = $in $PSBoundParameters['FriendlyName'] = $psCmdlet.ParameterSetName if ($ProjectID -and -not ($ProjectID -as [guid])) { $oldProgressPref = $ProgressPreference; $ProgressPreference = 'silentlycontinue' $projectID = Get-ADOProject -Organization $Organization -Project $projectID | Select-Object -ExpandProperty ProjectID $ProgressPreference = $oldProgressPref if (-not $ProjectID) { return } } $psBoundParameters["ParameterSet"] ='accesscontrollists/{NamespaceId}' switch -Regex ($psCmdlet.ParameterSetName) { Project { $null = $PSBoundParameters.Remove('ProjectID') $q.Enqueue(@{ NamespaceID = '52d39943-cb85-4d7f-8fa8-c6baac873819' SecurityToken = "`$PROJECT:vstfs:///Classification/TeamProject/$ProjectID" } + $PSBoundParameters) } 'AreaPath|IterationPath' { $gotPath = if ($psCmdlet.ParameterSetName -eq 'AreaPath') { Get-ADOAreaPath -Organization $Organization -Project $ProjectID -AreaPath $AreaPath } else { Get-ADOIterationPath -Organization $Organization -Project $ProjectID -IterationPath $iterationPath } if (-not $gotPath) { continue } $PathIdList = @( $gotPath.Identifier $parentUri = $gotPath._links.parent.href while ($parentUri) { $parentPath = Invoke-ADORestAPI -Uri $parentUri $parentPath.identifier $parentUri = $parentPath._links.parent.href } ) [Array]::Reverse($PathIdList) $null = $PSBoundParameters.Remove('ProjectID') $q.Enqueue(@{ NamespaceID = if ($psCmdlet.ParameterSetName -eq 'AreaPath') { '83e28ad4-2d72-4ceb-97b0-c7726d5502c3' } else { 'bf7bfa03-b2b7-47db-8113-fa2e002cc5b1' } SecurityToken = @(foreach($PathId in $PathIdList) { "vstfs:///Classification/Node/$PathId" }) -join ':' } + $PSBoundParameters) } Analytics { $null = $PSBoundParameters.Remove('ProjectID') $q.Enqueue(@{ NamespaceID = if ($ProjectID) { '58450c49-b02d-465a-ab12-59ae512d6531' } else { 'd34d3680-dfe5-4cc6-a949-7d9c68f73cba'} SecurityToken = "`$/$(if ($ProjectID) { $ProjectID } else { 'Shared' })" } + $PSBoundParameters) } Dashboard { $null = $PSBoundParameters.Remove('ProjectID') $q.Enqueue(@{ NamespaceID = '8adf73b7-389a-4276-b638-fe1653f7efc7' SecurityToken = "$/$(if ($ProjectID) { $ProjectID })/$(if ($teamID) { $teamid } else { [guid]::Empty } )" } + $PSBoundParameters) } ProjectOverview { $null = $psboundParameters.Remove('Recurse') $q.Enqueue(@{ NamespaceID = '52d39943-cb85-4d7f-8fa8-c6baac873819' # Project permissions SecurityToken = "`$PROJECT:vstfs:///Classification/TeamProject/$ProjectID" } + $PSBoundParameters) $q.Enqueue(@{ NamespaceID = '2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87' # Repositories SecurityToken = "repoV2/$projectId" Recurse = $true } + $PSBoundParameters) $q.Enqueue(@{ NamespaceID = '33344d9c-fc72-4d6f-aba5-fa317101a7e9' # Build definitions SecurityToken = "$ProjectID/" Recurse = $true } + $PSBoundParameters) $q.Enqueue(@{ NamespaceID = 'c788c23e-1b46-4162-8f5e-d7585343b5de' # Releases SecurityToken = "$ProjectID/" Recurse = $true } + $PSBoundParameters) $q.Enqueue(@{ NamespaceID = '8adf73b7-389a-4276-b638-fe1653f7efc7' # Dashboards SecurityToken = "`$/$ProjectID/" Recurse = $true } + $PSBoundParameters) $q.Enqueue(@{ NamespaceID = '49b48001-ca20-4adc-8111-5b60c903a50c' # Service Endpoints SecurityToken = "endpoints/$ProjectID" Recurse = $true } + $PSBoundParameters) $q.Enqueue(@{ NamespaceID = 'cb594ebe-87dd-4fc9-ac2c-6a10a4c92046' # Service Hooks SecurityToken = "PublisherSecurity/$ProjectID" Recurse = $true } + $PSBoundParameters) } Plan { $q.Enqueue(@{ NamespaceID = 'bed337f8-e5f3-4fb9-80da-81e17d06e7a8' SecurityToken = "Plan" } + $PSBoundParameters) } 'ServiceEndpoint|EndpointID' { if ($psCmdlet.ParameterSetName -eq 'ServiceEndpoint') { $PSBoundParameters['Recurse'] = $true } if ($EndpointID) { $q.Enqueue(@{ NamespaceID = '49b48001-ca20-4adc-8111-5b60c903a50c' SecurityToken = "endpoints/Collection/$(if ($EndpointID) {$EndpointID})" } + $PSBoundParameters) } $q.Enqueue(@{ NamespaceID = '49b48001-ca20-4adc-8111-5b60c903a50c' SecurityToken = "endpoints/$(if ($ProjectID) {"$ProjectID/"})$(if ($EndpointID) {$EndpointID})" } + $PSBoundParameters) } Tagging { $q.Enqueue(@{ NamespaceID = 'bb50f182-8e5e-40b8-bc21-e8752a1e7ae2' SecurityToken = "/$ProjectID" } + $PSBoundParameters) } ManageTFVC { $q.Enqueue(@{ NamespaceID = 'a39371cf-0841-4c16-bbd3-276e341bc052' SecurityToken = "/$ProjectID" } + $PSBoundParameters) } 'BuildDefinition|BuildPermission' { $q.Enqueue(@{ NamespaceID = 'a39371cf-0841-4c16-bbd3-276e341bc052' SecurityToken = "$ProjectID$(($buildpath -replace '\\','/').TrimEnd('/'))/$DefinitionID" } + $PSBoundParameters) $q.Enqueue(@{ NamespaceID = 'c788c23e-1b46-4162-8f5e-d7585343b5de' SecurityToken = "$ProjectID$(($buildpath -replace '\\','/').TrimEnd('/'))/$DefinitionID" } + $PSBoundParameters) } 'RepositoryID|AllRepositories|ProjectRepository' { $q.Enqueue(@{ NamespaceID = '2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87' SecurityToken = "repoV2$( if ($ProjectID) { '/' + $projectId} )$( if ($repositoryID) {'/' + $repositoryID} )$( if ($BranchName) { '/refs/heads/' + ([BitConverter]::ToString([Text.Encoding]::Unicode.GetBytes($BranchName)).Replace('-','').ToLower()) } )" } + $PSBoundParameters) } } return } $q.Enqueue(@{ParameterSet=$ParameterSet} + $PSBoundParameters) } end { $c, $t, $progId = 0, $q.Count, [Random]::new().Next() if ($Overview -and $inputObject) { $projectRepositories = $inputObject | Get-ADORepository $projectServiceEndpoints = $inputObject | Get-ADOServiceEndpoint $projectServiceHooks = $inputObject | Get-ADOServiceHook $projectBuildDefinitions = $inputObject | Get-ADOBuild -Definition $projectBoards = $inputObject | Get-ADOProject -Board } if ($ParameterSet -ne 'securitynamespaces') { $namespaceList = Get-ADOPermission @innerInvokeParams -Organization $Organization -PermissionType } while ($q.Count) { . $DQ $q # Pop one off the queue and declare all of it's variables (see /parts/DQ.ps1). $c++ $getProgressMessage = if ($friendlyName) { $friendlyName } else { $(@($ParameterSet -split '/' -notlike '{*}')[-1]) } $uri = # The URI is comprised of: @( "$server".TrimEnd('/') # the Server (minus any trailing slashes), $Organization # the Organization, '_apis' # the API Root ('_apis'), (. $ReplaceRouteParameter $ParameterSet) # and any parameterized URLs in this parameter set. ) -as [string[]] -ne '' -join '/' Write-Progress "Getting $getProgressMessage" $( if ($parameterSet -eq 'accesscontrollists/{NamespaceId}') { '' + $(foreach ($ns in $namespaceList) { if ($ns.NamespaceId -eq $NamespaceID) { $ns.Name ; break } }) + ' ' + $SecurityToken } else { "$uri" }) -Id $progId -PercentComplete ($c * 100/$t) $uri += '?' # The URI has a query string containing: $uri += @( if ($ParameterSet -eq 'accesscontrollists/{NamespaceId}') { if ($Recurse) { 'recurse=true' } if ($includeExtendedInfo) { 'includeExtendedInfo=true' } if ($SecurityToken) { "token=$SecurityToken"} if ($Descriptor) { "descriptors=$($Descriptor -join ',')"} } if ($Server -ne 'https://dev.azure.com/' -and -not $PSBoundParameters.ApiVersion) { $ApiVersion = '2.0' } if ($ApiVersion) { # the api-version "api-version=$apiVersion" } ) -join '&' # We want to decorate our return value. Handily enough, both URIs contain a distinct name in the last URL segment. $typename = @($parameterSet -split '/' -notlike '{*}')[-1].TrimEnd('s') # We just need to drop the 's' $typeNames = @( "$organization.$typename" "PSDevOps.$typename" ) $additionalProperties = @{Organization=$Organization;Server=$Server} if ($ParameterSet -eq 'accesscontrollists/{NamespaceId}') { $additionalProperties['NamespaceID'] = $NamespaceID $additionalProperties['NamespaceName'] = foreach ($ns in $namespaceList) { if ($ns.NamespaceId -eq $NamespaceID) { $ns.Name; break } } } if ($inputObject) { $additionalProperties['InputObject'] = $inputObject } $invokeParams.Uri = $uri $invokeParams.Property = $additionalProperties $invokeParams.PSTypeName = $typenames if ($ExpandACL) { $cachedIds = @{} Invoke-ADORestAPI @invokeParams | & { process { $inObj = $_ $aces = $inObj.acesDictionary $aceList = $inObj.acesDictionary.psobject.properties.name -join ',' if (-not $cachedIds[$aceList]) { $cachedIds[$aceList] = @($inObj | Get-ADOIdentity -Membership) } $expandedIdentities = $cachedIds[$aceList] $inObj.psobject.properties.Remove('acesDictionary') $namespace = foreach ($ns in $namespaceList) { if ($ns.NamespaceId -eq $inObj.namespaceID) { $ns; break } } $c = 0 foreach ($prop in $aces.psobject.properties) { $aclOut = [Ordered]@{} $resolvedId = $expandedIdentities[$c] $c++ $aclOut.IsReader = [bool]($prop.value.allow -band $namespace.readPermission) $aclOut.IsWriter = [bool]($prop.value.allow -band $namespace.writePermission) $aclOut.IsAdmin = [bool]($prop.value.allow -band $namespace.systemBitmask) $aclOut.Allow = $namespace.ConvertFromBitmask($prop.value.allow) $aclOut.Deny = $namespace.ConvertFromBitmask($prop.value.deny) $aclOut.Descriptor = $prop.Name foreach ($inProp in $inObj.psobject.properties) { $aclOut[$inProp.Name] = $inProp.Value } $aclOut.NamespaceName = $namespace.Name if ($Overview) { $aclOut.Target = switch ($namespace.Name) { Project { $inputObject } 'Git Repositories' { foreach ($repo in $projectRepositories) { if ($aclOut.Token -like "*/$($repo.id)*") { $repo;break } } } Boards { foreach ($board in $projectBoards) { if ($aclOut.Token -like "*$($board.id)*") { $board;break } } } "Build|ReleaseManagement" { foreach ($def in $projectBuildDefinitions) { if ($aclOut.Token -like "*/$($def.id)*") { $def;break } } } ServiceHooks { foreach ($svc in $projectServiceHooks) { if ($aclOut.Token -like "*/$($svc.id)*") { $svc;break } } } ServiceEndpoints { foreach ($svc in $projectServiceEndpoints) { if ($aclOut.Token -like "*/$($svc.id)*") { $svc;break } } } } } $resolvedIds = @() # Resolving group membership without resolving it recursively is less helpful, # but still helpful, so leave this alone for now $aclOut.Group = if ($resolvedId.members) { if ($resolvedID.customDisplayName) { $resolvedID.customDisplayName } elseif ($resolvedID.providerDisplayName) { $resolvedID.providerDisplayName } else { $resolvedID.descriptor } $memberAceList = $resolvedID.members -join ',' if (-not $cachedIds[$memberAceList]) { $cachedIds[$memberAceList] = @(Get-ADOIdentity -Organization $organization -Descriptors $resolvedID.members -Recurse -Membership) } $resolvedIds = $cachedIds[$memberAceList] } else { $resolvedIds =@($resolvedID) } foreach ($resolvedId in $resolvedIds) { $aclOut.IsGroup = $resolvedId.properties.SchemaClassName -eq 'Group' $aclOut.Identity = if ($resolvedID.customDisplayName) { $resolvedID.customDisplayName } elseif ($resolvedID.providerDisplayName) { $resolvedID.providerDisplayName } else { $resolvedID.descriptor } $out = [PSCustomObject]$aclOut $out.pstypenames.clear() $out.pstypenames.add("PSDevOps.AccessControlEntry") $out.pstypenames.add("$Organization.AccessControlEntry") $out } } } } } else { if ($psCmdlet.ParameterSetName -eq 'securitynamespaces' -and -not $invokeParams.AsJob) { if (-not $script:CachedSecurityNamespaces) { $script:CachedSecurityNamespaces = Invoke-ADORestAPI @invokeParams } $script:CachedSecurityNamespaces } else { Invoke-ADORestAPI @invokeParams -DecorateProperty @{ AcesDictionary = "$Organization.ACEDictionary", "PSDevOps.ACEDictionary" } } } } Write-Progress "Getting $($ParameterSet)" "$server $Organization $Project" -Id $progId -Completed } } |