Backup-Permission.ps1
|
Function Backup-Permission { [CmdletBinding(DefaultParameterSetName="AccessToken")] param ( # Personal Access Token with DevOps Access [Parameter(ParameterSetName="PAT",Mandatory=$true)] [String]$PAT, # AccessToken with DevOps Access [Parameter(ParameterSetName="AccessToken",Mandatory=$true)] [String]$AccessToken, # Azure DevOps organization name [Parameter(Mandatory=$true)] [String]$OrganizationName, # Azure DevOps Api version [Parameter()] [String]$ApiVersion = "7.1-preview.1", # Output localization [Parameter(Mandatory=$true)] [String]$OutputPath, # SecurityNamespaces [String[]]$SecurityNamespacesNames = @("Project", "Git Repositories", "Build") ) #Helper Functions Function Get-BitDecoder { [CmdletBinding()] Param([int]$Bits) $BitsTable = @{ 0x01 = 1 0x02 = 2 0x04 = 4 0x08 = 8 0x10 = 16 0x20 = 32 0x40 = 64 0x80 = 128 0x100 = 256 0x200 = 512 0x400 = 1024 0x800 = 2048 0x1000 = 4096 0x2000 = 8192 0x4000 = 16384 0x8000 = 32768 0x10000 = 65536 0x20000 = 131072 0x40000 = 262144 0x80000 = 524288 0x100000 = 1048576 0x200000 = 2097152 0x400000 = 4194304 0x800000 = 8388608 0x1000000 = 16777216 0x2000000 = 33554432 0x4000000 = 67108864 0x8000000 = 134217728 } foreach($BitsKey in $BitsTable.Keys | Sort-Object){ if($Bits -band $BitsKey){ $BitsTable[$BitsKey] } } } Function ConvertFrom-Base64 { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [String]$Base64, [Parameter()] [ValidateSet("Default","ASCII","BigEndianUnicode","Unicode","UTF32","UTF7","UTF8")] [String]$Encoding = "Default" ) Write-Verbose "Original string: $Base64" $ModulusValue = $Base64.length % 4 Switch ($ModulusValue) { '0' {$Base64String = $Base64} '1' {$Base64String = $Base64.Substring(0,$Base64.Length - 1)} '2' {$Base64String = $Base64 + ('=' * (4 - $ModulusValue))} '3' {$Base64String = $Base64 + ('=' * (4 - $ModulusValue))} } Write-Verbose "Base64 string: $Base64String" Return [System.Text.Encoding]::$Encoding.GetString([System.Convert]::FromBase64String($Base64String)) } Function Get-DescriptorMembers { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] $Descriptors, [Parameter(Mandatory = $true)] $Descriptor, [String]$Path ) if($Descriptor -is [String]) { Write-Verbose "As String" $DescriptorDetails = $Descriptors[$Descriptor] } else { Write-Verbose "As Object" $DescriptorDetails = $Descriptor } Write-Verbose "$($DescriptorDetails.principalName) ($($DescriptorDetails.subjectKind))" Write-Verbose "$($DescriptorDetails.Descriptor)" $Members = @() if($DescriptorDetails.subjectKind -eq "group") { Foreach($Member in $DescriptorDetails.Members) { $Members += $Member | Select-Object *, @{l="Path";e={"$Path\\$($DescriptorDetails.principalName)"}} if($Member.subjectKind -eq "group") { Get-DescriptorMembers -Descriptors $Descriptors -Descriptor $Member.Descriptor -Path "$Path\\$($DescriptorDetails.principalName)" } } } Return $Members } #Headers $Header = [Ordered]@{ "Content-Type" = "application/json" "Accept" = "application/json" } if($PAT) { $Header["Authorization"] = "Basic " + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$($PAT)")) } if($AccessToken) { $Header["Authorization"] = "Bearer " + $AccessToken } #API Uri $UriBase = "https://dev.azure.com/$($OrganizationName)/" $UriBaseVSSPS = "https://vssps.dev.azure.com/$($OrganizationName)/" #Export path $OrganizationPath = Join-Path -Path $OutputPath -ChildPath $OrganizationName if(!(Test-Path -Path $OrganizationPath)) { New-Item -Path $OutputPath -Name $OrganizationName -ItemType Directory | Out-Null } #Dictionaries of most user entities: Projects, Repositories, Pipelines Write-Output "Get helper data for Projects, Repositories and Pipelines..." $Dictionaries = @() $Uri = $UriBase + "_apis/projects?api-version=$ApiVersion" $Response = Invoke-RestMethod -Uri $Uri -Method Get -Headers $Header $Projects = $Response.value $Dictionaries += $Projects | Select-Object Id, Name, @{l="Type";e={"Project"}} Foreach($Project in $Projects) { #Repositories $Uri = $UriBase + "$($Project.Name)/_apis/git/repositories/$($RepositoryId)?api-version=$ApiVersion" $Response = Invoke-RestMethod -Uri $Uri -Method Get -Headers $Header $Repositories = $Response.value $Dictionaries += $Repositories | Select-Object Id, Name, @{l="Type";e={"Repository"}},@{l="ProjectName";e={$Project.Name}} #Pipelines $Uri = $UriBase + "$($Project.Name)/_apis/pipelines?api-version=$ApiVersion" $Response = Invoke-RestMethod -Uri $Uri -Method Get -Headers $Header $Pipelines = $Response.value $Dictionaries += $Pipelines | Select-Object @{l="Id";e={[String]$_.Id}}, Name, @{l="Type";e={"Pipeline"}},@{l="ProjectName";e={$Project.Name}} } Write-Output "Projects: $(($Dictionaries | where-Object Type -eq "Project" | Measure-Object).Count)" Write-Output "Repositories: $(($Dictionaries | where-Object Type -eq "Repository" | Measure-Object).Count)" Write-Output "Pipeline: $(($Dictionaries | where-Object Type -eq "Pipeline" | Measure-Object).Count)" $DictionariesHT = $Dictionaries | Group-Object -Property Id -AsHashTable #Get Groups List $Uri = $UriBaseVSSPS + "_apis/graph/groups?api-version=$ApiVersion" $Response = Invoke-RestMethod -Uri $Uri -Method Get -Headers $Header $Groups = $Response.value Write-Output "Groups: $($Groups.Count)" #Get Users List $Uri = $UriBaseVSSPS + "_apis/graph/users?api-version=$ApiVersion" $Response = Invoke-RestMethod -Uri $Uri -Method Get -Headers $Header $Users = $Response.value Write-Output "Users: $($Users.Count)" #Get Serviceprincipals List $Uri = $UriBaseVSSPS + "_apis/graph/serviceprincipals?api-version=$ApiVersion" $Response = Invoke-RestMethod -Uri $Uri -Method Get -Headers $Header $Serviceprincipals = $Response.value Write-Output "Serviceprincipals: $($Serviceprincipals.Count)" #Get all descriptors Write-Output "Processing descriptors..." $Descriptors = @{} foreach($Item in $Groups) { $Descriptors[$Item.descriptor] = [PSCustomObject]@{ subjectKind = $Item.subjectKind description = $Item.description domain = $Item.domain principalName = $Item.principalName mailAddress = $Item.mailAddress origin = $Item.origin originId = $Item.originId displayName = $Item.displayName descriptor = $Item.descriptor sid = ConvertFrom-Base64 -Base64 $Item.descriptor.split(".")[1] memberof = @() members = @() } } foreach($Item in $Users) { $Descriptors[$Item.descriptor] = [PSCustomObject]@{ subjectKind = $Item.subjectKind domain = $Item.domain principalName = $Item.principalName mailAddress = $Item.mailAddress origin = $Item.origin originId = $Item.originId displayName = $Item.displayName descriptor = $Item.descriptor sid = ConvertFrom-Base64 -Base64 $Item.descriptor.split(".")[1] memberof = @() } } foreach($Item in $Serviceprincipals) { $Descriptors[$Item.descriptor] = [PSCustomObject]@{ subjectKind = $Item.subjectKind applicationId = $Item.applicationId domain = $Item.domain principalName = $Item.principalName mailAddress = $Item.mailAddress origin = $Item.origin originId = $Item.originId displayName = $Item.displayName descriptor = $Item.descriptor sid = ConvertFrom-Base64 -Base64 $Item.descriptor.split(".")[1] memberof = @() } } #Get object Memberships foreach($Item in $Groups) { Write-Verbose "Processing $($Item.subjectKind) descriptor: $($Item.principalName)" #Get item mamberships $Uri = $UriBaseVSSPS + "_apis/graph/Memberships/$($Item.descriptor)?api-version=$ApiVersion" $Response = Invoke-RestMethod -Uri $Uri -Method Get -Headers $Header $Memberships = $Response.value Foreach($Membership in $Memberships) { $MemberOf = $Groups | Where-Object descriptor -eq $Membership.containerDescriptor $Descriptors[$Item.descriptor].memberof += $MemberOf | Select-Object subjectKind,principalName,displayName,originId,descriptor $Descriptors[$MemberOf.descriptor].members += $Item | Select-Object subjectKind,principalName,displayName,originId,descriptor } } foreach($Item in $Users) { Write-Verbose "Processing $($Item.subjectKind) descriptor: $($Item.principalName)" #Get item mamberships $Uri = $UriBaseVSSPS + "_apis/graph/Memberships/$($Item.descriptor)?api-version=$ApiVersion" $Response = Invoke-RestMethod -Uri $Uri -Method Get -Headers $Header $Memberships = $Response.value Foreach($Membership in $Memberships) { $MemberOf = $Groups | Where-Object descriptor -eq $Membership.containerDescriptor $Descriptors[$Item.descriptor].memberof += $MemberOf | Select-Object subjectKind,principalName,displayName,originId,descriptor $Descriptors[$MemberOf.descriptor].members += $Item | Select-Object subjectKind,principalName,displayName,originId,descriptor } } foreach($Item in $Serviceprincipals) { Write-Verbose "Processing $($Item.subjectKind) descriptor: $($Item.principalName)" #Get item mamberships $Uri = $UriBaseVSSPS + "_apis/graph/Memberships/$($Item.descriptor)?api-version=$ApiVersion" $Response = Invoke-RestMethod -Uri $Uri -Method Get -Headers $Header $Memberships = $Response.value Foreach($Membership in $Memberships) { $MemberOf = $Groups | Where-Object descriptor -eq $Membership.containerDescriptor $Descriptors[$Item.descriptor].memberof += $MemberOf | Select-Object subjectKind,principalName,displayName,originId,descriptor $Descriptors[$MemberOf.descriptor].members += $Item | Select-Object subjectKind,principalName,displayName,originId,descriptor } } $DescriptorsList = $Descriptors.GetEnumerator() | Select-Object -ExpandProperty Value $FileName = "Descriptors.json" $OutFile = Join-Path -Path $OrganizationPath -ChildPath $FileName $DescriptorsList | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutFile #Get SecurityNamespaces $Uri = $UriBase + "_apis/securitynamespaces?api-version=$ApiVersion" $Response = Invoke-RestMethod -Uri $Uri -Method Get -Headers $Header $SecurityNamespaces = $Response.value | Where-Object Name -in $SecurityNamespacesNames | Select-Object namespaceId,name,actions $ReportSecurityNamespaces = @() Write-Output "Processing permissions..." Foreach($SecurityNamespace in $SecurityNamespaces) { $ReportSecurityNamespace = [PSCustomObject]@{ SecurityNamespace = $SecurityNamespace.name SecurityNamespaceId = $SecurityNamespace.namespaceId AccessControlLists = @() } #Get AccessControlLists for SecurityNamespace $Uri = $UriBase + "_apis/accesscontrollists/$($SecurityNamespace.namespaceId)?api-version=$ApiVersion" $Response = Invoke-RestMethod -Uri $Uri -Method Get -Headers $Header $AccessControlLists = $Response.value Write-Output "SecurityNamespace: $($SecurityNamespace.name), ACLs: $($AccessControlLists.Count)" $Actions = @{} $Securitynamespace.actions | Select-Object Bit,Name,DisplayName | ForEach-Object { $Actions[[int]$_.bit] = $_ } Foreach($AccessControlList in $AccessControlLists) { Write-Verbose "Processing $($SecurityNamespace.name) AccessControlList: $($AccessControlList.token)" $TokenDetails = $AccessControlList.token -split "/" $ReportAccessControlList = [PSCustomObject]@{ Token = $AccessControlList.token TokenDetails = $TokenDetails | ForEach-Object { $DictionariesHT[$_] } | Where-Object Id InheritPermissions = $AccessControlList.inheritPermissions Identities = @() } $Identities = $AccessControlList.acesDictionary | Get-Member -Name Microsoft.TeamFoundation* | Select-Object -ExpandProperty Name Foreach($Identity in $Identities) { $ReportIdentity = [Ordered]@{ Identity = $Identity } $SID = ($Identity -split ";")[1] if($SID -match "S-1-9-") { $Descriptor = $DescriptorsList | Where-Object sid -eq $SID if($null -eq $Descriptor) { switch($SID) { { $_ -match "S-1-9-(?:\d+-){1,14}\d+-0-0-0-0-1$" } { $Descriptor = $DescriptorsList | Where-Object { $_.principalName -match "\[$OrganizationName\]\\" -and $_.sid -match "S-1-9-(?:\d+-){1,14}\d+-0-0-0-0-1$" } } #Project Collection Administrators { $_ -match "S-1-9-(?:\d+-){1,14}\d+-0-0-0-1-2$" } { $Descriptor = $DescriptorsList | Where-Object { $_.principalName -match "\[$OrganizationName\]\\" -and $_.sid -match "S-1-9-(?:\d+-){1,14}\d+-0-0-0-1-2$" } } #Project Collection Build Administrators { $_ -match "S-1-9-(?:\d+-){1,14}\d+-0-0-0-1-1$" } { $Descriptor = $DescriptorsList | Where-Object { $_.principalName -match "\[$OrganizationName\]\\" -and $_.sid -match "S-1-9-(?:\d+-){1,14}\d+-0-0-0-1-1$" } } #Project Collection Build Service Accounts { $_ -match "S-1-9-(?:\d+-){1,14}\d+-0-0-0-5-1$" } { $Descriptor = $DescriptorsList | Where-Object { $_.principalName -match "\[$OrganizationName\]\\" -and $_.sid -match "S-1-9-(?:\d+-){1,14}\d+-0-0-0-5-1$" } } #Project Collection Proxy Service Accounts { $_ -match "S-1-9-(?:\d+-){1,14}\d+-0-0-0-0-2$" } { $Descriptor = $DescriptorsList | Where-Object { $_.principalName -match "\[$OrganizationName\]\\" -and $_.sid -match "S-1-9-(?:\d+-){1,14}\d+-0-0-0-0-2$" } } #Project Collection Service Accounts { $_ -match "S-1-9-(?:\d+-){1,14}\d+-0-0-0-4-1$" } { $Descriptor = $DescriptorsList | Where-Object { $_.principalName -match "\[$OrganizationName\]\\" -and $_.sid -match "S-1-9-(?:\d+-){1,14}\d+-0-0-0-4-1$" } } #Project Collection Test Service Accounts { $_ -match "S-1-9-(?:\d+-){1,14}\d+-0-0-0-0-3$" } { $Descriptor = $DescriptorsList | Where-Object { $_.principalName -match "\[$OrganizationName\]\\" -and $_.sid -match "S-1-9-(?:\d+-){1,14}\d+-0-0-0-0-3$" } } #Project Collection Valid Users { $_ -match "S-1-9-(?:\d+-){1,14}\d+-0-0-0-0-14$" } { $Descriptor = $DescriptorsList | Where-Object { $_.principalName -match "\[$OrganizationName\]\\" -and $_.sid -match "S-1-9-(?:\d+-){1,14}\d+-0-0-0-0-14$" } } #Project-Scoped Users { $_ -match "S-1-9-(?:\d+-){1,14}\d+-0-0-0-0-5$" } { $Descriptor = $DescriptorsList | Where-Object { $_.principalName -match "\[$OrganizationName\]\\" -and $_.sid -match "S-1-9-(?:\d+-){1,14}\d+-0-0-0-0-5$" } } #Security Service Group } } $ReportIdentity["SID"] = $SID if($null -ne $Descriptor) { $ReportIdentity["Descriptor"] = $Descriptor.descriptor $ReportIdentity["PrincipalName"] = $Descriptor.principalName $ReportIdentity["DescriptorMembers"] = Get-DescriptorMembers -Descriptors $Descriptors -Descriptor $Descriptor.Descriptor } } else { $SID = $null #Get identity details if not in Descriptors $Uri = $UriBaseVSSPS + "_apis/identities/?descriptors=$($Identity)&queryMembership=None&api-version=$ApiVersion" $Response = Invoke-RestMethod -Uri $Uri -Method Get -Headers $Header $IdentityDetails = $Response.value $ReportIdentity["Descriptor"] = $IdentityDetails.subjectDescriptor $ReportIdentity["PrincipalName"] = $IdentityDetails.customDisplayName } #Decode permissions $BitsAllow = $AccessControlList.acesDictionary.$Identity.Allow $BitsDeny = $AccessControlList.acesDictionary.$Identity.Deny $Allow = Get-BitDecoder $BitsAllow $Deny = Get-BitDecoder $BitsDeny $ReportIdentity["BitsAllow"] = $BitsAllow $ReportIdentity["BitsDeny"] = $BitsDeny $ReportIdentity["Allow"] = $Allow | ForEach-Object { $Actions[$_] } $ReportIdentity["Deny"] = $Deny | ForEach-Object { $Actions[$_] } $ReportAccessControlList.Identities += [PSCustomObject]$ReportIdentity } $ReportSecurityNamespace.AccessControlLists += $ReportAccessControlList } $ReportSecurityNamespaces += $ReportSecurityNamespace } $FileName = "AccessControlLists.json" $OutFile = Join-Path -Path $OrganizationPath -ChildPath $FileName $ReportSecurityNamespaces | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutFile } |