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
}