From 524e930b8a97e548adb54e3bf1b68fdce27e3a17 Mon Sep 17 00:00:00 2001 From: B Stack Date: Mon, 8 Jun 2020 08:30:32 -0400 Subject: add bitbucketlib.ps1 --- bitbucketlib.ps1/bitbucketlib.ps1 | 864 ++++++++++++++++++++++++++++++++++++++ bitbucketlib.ps1/description | 1 + 2 files changed, 865 insertions(+) create mode 100644 bitbucketlib.ps1/bitbucketlib.ps1 create mode 100644 bitbucketlib.ps1/description (limited to 'bitbucketlib.ps1') diff --git a/bitbucketlib.ps1/bitbucketlib.ps1 b/bitbucketlib.ps1/bitbucketlib.ps1 new file mode 100644 index 0000000..f5e3397 --- /dev/null +++ b/bitbucketlib.ps1/bitbucketlib.ps1 @@ -0,0 +1,864 @@ +# Startdate: 2020-05-18 +# Title: Library for Bitbucket functions + +# References: +# https://developer.atlassian.com/bitbucket/api/2/reference/meta/authentication +# https://developer.atlassian.com/server/bitbucket/how-tos/command-line-rest/ +# https://github.com/AtlassianPS/BitbucketPS +# https://stackoverflow.com/questions/9825060/powershell-filtering-for-unique-values/9825218#9825218 + +# Useful starting values in procedural function at end. + +# call: $CloudHeaders = Get-Bitbucket-Auth-Headers +Function Get-Bitbucket-Auth-Headers { + <# + .Synopsis Either username and password, or credential are required. + #> + param( + [Parameter(Mandatory = $false)][string]$Username, + [Parameter(Mandatory = $false)][string]$Password, + [Parameter(Mandatory = $false)][System.Management.Automation.PSCredential]$Credential + ) + + . '\\rdputils1\e$\scripts\Functions\credlib.ps1' + $internal_continue=$True + $Headers = @{} + + if ($Username -ne $null -And $Password -ne $Null -And $Username -ne "" -And $Password -ne "") { + $secret = $password | ConvertTo-SecureString -AsPlainText -Force + $Credential = New-Object -TypeName "System.Management.Automation.PSCredential" -ArgumentList ${username}, ${secret} + } elseif ($Credential -ne $null) { + # pass through safely + } else { + if ($Username.tolower() -eq "prompt") { + $Credential = Get-Credential + } else { + Write-Error "Provide -Username and -Password, or -Credential." + $internal_continue = $False + break + } + } + + if ($internal_continue) { + $Headers += Get-Basic-Base64-Auth-Headers -Credential $credential + $Headers + } +} + +Function _Iterate-Values-From-BB-API { + [CmdletBinding()] + param( + [Parameter(Mandatory = $False)][Hashtable]$AuthHeaders = ${global:CloudHeaders}, + [Parameter(Mandatory = $True )][string]$StartUri, + [Parameter(Mandatory = $False)][string]$Location = "cloud", # "cloud" or "local" which sets page defaults + [Parameter(Mandatory = $False)][string]$Descriptor = "item", + [Parameter(Mandatory = $False)][boolean]$SkipCount = $False + ) + # StartUri could be any of these, for example: + # https://api.bitbucket.org/2.0/repositories/${org} + # https://api.bitbucket.org/2.0/workspaces/${org}/projects + # https://git.example.com/rest/api/1.0/repos + # https://git.example.com/rest/api/1.0/projects + # https://api.bitbucket.org/2.0/teams/${org}/permissions/repositories?q=permission%3D%22write%22 + Begin { + $done = $False + $output = @() + $size = -1 + $count = 0 + $Baseuri = $StartUri + $keepcounting = $false + + # All this just to get object count for the local Bitbucket, because its API does not list total object count on each return page. + if (!($SkipCount)) { + if ($Location -like "*local*" ) { + $pageSize = 250 + $CountUri = "$($Baseuri)?limit=$($pageSize)" + $localcount = 0 + While (!$done) { + Write-Progress -Activity "Counting objects" -Status "$($localcount)" + $interim = Invoke-RestMethod $CountUri -Method "GET" -Headers $AuthHeaders + if ( !($interim.isLastPage) ) { + $localcount += $pageSize + $CountUri = "$($Baseuri)?limit=$($pageSize)&start=$($localcount)" + } else { + $done = $True + $localcount += $interim.size + } + } + $done = $False + $size = $localcount + } Elseif ($Location -like "*cloud*" ) { + $CountUri = "$($Baseuri)" + $cloudcount = 0 + While (!$done) { + Write-Progress -Activity "Counting $($Descriptor)s" -Status "$($cloudcount)" + $interim = Invoke-RestMethod $CountUri -Method "GET" -Headers $AuthHeaders + # short circuit if size is provided + If ($interim.size -ne $null) { + $cloudcount = $interim.size + $done = $True + } Elseif ($interim.next -eq $null) { + $cloudcount += $interim.pagelen + $done = $True + } Else { + $cloudcount += $interim.pagelen + $Counturi = $interim.next + } + } + $done = $False + $size = $cloudcount + } + } Else { + # skip the count! + $size = 10 + } + } + + Process { + Write-Verbose "Will look for $($size) $($descriptor)s" + $Uri = $StartUri + While (!$done) { + $interim = Invoke-RestMethod -Uri $Uri -Method "GET" -Headers $AuthHeaders + if (!($SkipCount) -And $size -eq -1) { # only run once because it will always be the same + if ($interim.size -ne $null) { $size = $interim.size } + Else { $keepcounting = $True; $size += $interim.values.count } + } + if ($keepcounting) { $size += $interim.values.count } + $interim.values | % { + $output += $_ ; + $count += 1 ; + [int]$percent = ($count/$size)*100 + $percent = (@($percent, 100) | Measure -Minimum).Minimum + $percent = (@($percent, 0) | Measure -Maximum).Maximum + Write-Progress -Activity "Listing $($descriptor)" -Status "$count/$size" -PercentComplete $percent + } + # Bitbucket Cloud uses property "next" but on-prem server uses "nextPageStart" + If ( $interim.next -ne $Null ) { + $Uri = $interim.next + } Elseif ( $interim.nextPageStart -ne $Null -And (!($interim.isLastPage)) ) { + $Uri = "$($Baseuri)?start=$($interim.nextPageStart)" + } else { $done = $True } + } + } + + End { $output } +} + +Function List-All-Cloud-Projects { + [CmdletBinding()] + param( + [Parameter(Mandatory = $True )][Hashtable]$CloudHeaders, + [Parameter(Mandatory = $False)][string]$Server = "api.bitbucket.org", + [Parameter(Mandatory = $False)][string]$Instance = "example" + ) + _Iterate-Values-From-BB-API -AuthHeaders $CloudHeaders -StartUri "https://${Server}/2.0/workspaces/${Instance}/projects" -Descriptor "project" +} + +Function List-All-Cloud-Repos { + [CmdletBinding()] + param( + [Parameter(Mandatory = $True )][Hashtable]$CloudHeaders, + [Parameter(Mandatory = $False)][string]$Server = "api.bitbucket.org", + [Parameter(Mandatory = $False)][string]$Instance = "example" + ) + _Iterate-Values-From-BB-API -AuthHeaders $CloudHeaders -StartUri "https://${Server}/2.0/repositories/${Instance}" -Descriptor "repo" +} + +Function List-All-Local-Projects { + [CmdletBinding()] + param( + [Parameter(Mandatory = $True )][Hashtable]$LocalHeaders, + [Parameter(Mandatory = $False)][string]$Server = "gittest.example.com" + ) + _Iterate-Values-From-BB-API -AuthHeaders $LocalHeaders -StartUri "https://${Server}/rest/api/1.0/projects" -Descriptor "project" -Location "local" +} + +Function List-All-Local-Repos { + [CmdletBinding()] + param( + [Parameter(Mandatory = $True )][Hashtable]$LocalHeaders, + [Parameter(Mandatory = $False)][string]$Server = "gittest.example.com", + [Parameter(Mandatory = $False)][string]$Instance = "example" + ) + _Iterate-Values-From-BB-API -AuthHeaders $LocalHeaders -StartUri "https://${Server}/rest/api/1.0/repos" -Descriptor "repo" -Location "local" +} + +# idea: $foo = List-All-Cloud-Repos -CloudHeaders $CloudHeaders +# $out1 = $foo | Transform-Cloud-Repos-To-Useful +# MapFile is CSV with columns Old,New to transform project names +# Good mapfile is "U:\2020\05\project-cloud-to-onprem.csv" +Function Transform-Cloud-Repos-To-Useful { + <# .Synopsis add relevant columns #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $False,ValueFromPipeLine = $true,ValueFromPipelineByPropertyName = $true)]$repo, + [Parameter(Mandatory = $True)]$MapFile + ) + Begin { + $output = @() ; + If (!(Test-Path $MapFile)) { + Write-Error "MapFile must be a valid CSV file with columns named Old and New." + break + } + $newDict = @{} ; Import-Csv $MapFile | % { $newDict[$_.Old]=$_.New } + } + + Process { + # a shallow copy is good enough for this object type. We do not have links to other objects in here, that we will care about or change or use. + # https://stackoverflow.com/questions/9581568/how-to-create-new-clone-instance-of-psobject-object/13275775#13275775 + $newitem = $_.PsObject.Copy() + $newitem | Add-Member -NotePropertyName projectkey -NotePropertyValue $_.project.key + $newitem | Add-Member -NotePropertyName projectname -NotePropertyValue $_.project.name + $newitem | Add-Member -NotePropertyName projecttype -NotePropertyValue $_.project.type + $newitem | Add-Member -NotePropertyName NewProjectKey ` + -NotePropertyValue $newDict[$newitem.projectkey] + $output += $newitem + } + End { $output ; } +} + +# Useful for pulling columns from cloud repo definitions. Use after Transform-Repos-To-Useful +Function Select-Useful-Cloud-Repo-Columns { + [CmdletBinding()] + param( + [Parameter(Mandatory = $False,ValueFromPipeLine = $true,ValueFromPipelineByPropertyName = $true)]$repo + ) + + Process { $repo | Select-Object updated_on,size,slug,description, ` + projectkey,projectname,NewProjectKey,fork_policy,uuid,full_name, ` + name,language,created_on,has_issues } + +} + +Function Select-Useful-Local-Repo-Columns { + [CmdletBinding()] + param( + [Parameter(Mandatory = $False,ValueFromPipeLine = $true,ValueFromPipelineByPropertyName = $true)]$repo + ) + + Process { $repo | Select-Object slug,name,forkable, + @{L='ProjectKey';E={($_.Project.key)};}, + @{L='ProjectName';E={($_.project.name)};}, + @{L='fullName';E={("$($_.project.key)/$($_.slug)")};} + } +} + +# low-level function for making a single repo with values +Function Act-Repo { + [CmdletBinding()] + param( + [Parameter(Mandatory = $False)][string]$Server = "gittest.example.com", + [Parameter(Mandatory = $True )][string]$Name, + [Parameter(Mandatory = $True )][string]$ProjectSlug, + [Parameter(Mandatory = $True )][string]$Action, # either Create or Delete + [Parameter(Mandatory = $False)][Hashtable]$LocalHeaders = ${global:LocalHeaders} + ) + if (!($Action -in @("Create","Delete"))) { + Throw "Action must be either Create or Delete" + $null + } + if ($Action -eq "Create") { + $data = @{ + name = $Name + scmID = "git" + } | ConvertTo-Json -Depth 3 + $Uri = "https://$($Server)/rest/api/1.0/projects/$($ProjectSlug)/repos" + $Method = "POST" + } elseif ($Action -eq "Delete") { + $Uri = "https://$($Server)/rest/api/1.0/projects/$($ProjectSlug)/repos/$($Name)" + $Method = "DELETE" + } + try { + Write-Host "Trying to $Action repo $ProjectSlug/$Name" + $RestParams = @{ + Uri = $Uri + Method = $Method + Headers = $LocalHeaders + ContentType = "application/json" + } + if ($data -ne $Null) { $RestParams += @{ Body = $Data } } + $response = Invoke-RestMethod @RestParams + } + <# catch [System.InvalidOperationException] { # we will get an invalid operation if the project already exists! Write-Warning "Repo $($ProjectSlug)/$($Name) already exists. Resuming..." Continue } #> + catch { + Write-Error $PSItem + } + $response +} + +# Goal: create repositories based on CSV fields +# Act-All-Repos -MapFile "U:\2020\05\repos-testfull.csv" -Action "Create" +Function Act-All-Repos { + [CmdletBinding()] + param( + [Parameter(Mandatory = $False)][string]$Server = "gittest.example.com", + [Parameter(Mandatory = $True )][string]$MapFile, + [Parameter(Mandatory = $True )][string]$Action, # either Create or Delete + [Parameter(Mandatory = $False)][Hashtable]$LocalHeaders = ${global:LocalHeaders} + ) + + # DELETE is not implemented yet. + Begin { + If (!(Test-Path $MapFile)) { + Write-Error "MapFile must be a valid CSV file with columns named slug,NewProjectKey." + break + } + if (!($Action -in @("Create","Delete"))) { + Throw "Action must be either Create or Delete" + } + $map = Import-Csv $MapFile + } + + Process { + # for every item in $map + $map | % { + Write-Host "Act-Repo -Name $($_.slug) -ProjectSlug $($_.NewProjectKey) -Action $Action -LocalHeaders LocalHeaders" + Write-Progress "Trying to $Action repo $($_.NewProjectKey)/$($_.slug)" + Act-Repo -Name "$($_.slug)" -ProjectSlug "$($_.NewProjectKey)" -Action $Action -LocalHeaders $LocalHeaders + } + } + End { + } +} + +Function Act-Group { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory = $False)][string]$Server = "gittest.example.com", + [Parameter(Mandatory = $True )][string]$Name, + [Parameter(Mandatory = $True )][string]$Action, # either Create or Delete + [Parameter(Mandatory = $False)][Hashtable]$LocalHeaders = ${global:LocalHeaders} + ) + + if (!($Action -in @("Create","Delete"))) { + Throw "Action must be either Create or Delete" + } + + $Uri = "https://${Server}/rest/api/1.0/admin/groups?name=${Name}" + $Method = "POST" ; if ($Action -eq "Delete") { $Method = "DELETE" } + Try { + If ($PsCmdlet.ShouldProcess("$($Action) group $($Name)")) { + $response = Invoke-RestMethod -Uri $Uri -Method $Method -Headers $LocalHeaders -ContentType "application/json" -Verbose:$False + } + } Catch { + if ($_.Exception.Response.StatusCode.value__ -eq 409) { + # just be silent when trying to create an extant group. 409: conflict. + #Write-Warning "Group ""${name}"" already exists, continuing." + } else { + Write-Error $_ + } + } + # Bitbucket will return .name and .deletable if the object was successfully created. +} + +Function Act-User-In-Group { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory = $False)][string]$Server = "gittest.example.com", + [Parameter(Mandatory = $False)][Hashtable]$AuthHeaders = ${global:CloudHeaders}, + [Parameter(Mandatory = $True )][string]$GroupName, + [Parameter(Mandatory = $True )][string]$UserName, # safer to pass in uuid when location=cloud + [Parameter(Mandatory = $False)][string]$Instance = "example", + [Parameter(Mandatory = $True )][string]$Action, # either Add or Remove + [Parameter(Mandatory = $False)][string]$Location = "local" # "cloud" or "local" + ) + Begin { + If (!($Action -in @("Add","Remove"))) { + Write-Error "Action must be in set @(""Add"",""Remove""). Aborted." + break + } + If (!($Location -in @("local","cloud"))) { + Write-Error "Location must be in set @(""local"",""cloud""). Aborted." + break + } + } + Process { + If ($Action -eq "Add") { + If ($Location -eq "local") { + $data = @{ + user = $UserName + groups = @( $GroupName ) + } | ConvertTo-Json -Depth 5 + $Restparams = @{ + Uri = "https://${Server}/rest/api/1.0/admin/users/add-groups" + Method = "POST" + } + } ElseIf ($Location -eq "cloud") { + # Reference: https://confluence.atlassian.com/bitbucket/groups-endpoint-296093143.html + # WORKS: Invoke-RestMethod -ContentType "application/json" -Uri "https://api.bitbucket.org/1.0/groups/example/devops/members/exampleteam/" -Body "{}" -method "PUT" -Headers $CloudHeaders + $data = "{}" + $Restparams = @{ + Uri = "https://${Server}/1.0/groups/${Instance}/${GroupName}/members/${UserName}/" + Method = "PUT" + } + } + } Elseif ($Action -eq "Remove") { + If ($Location -eq "local") { + $data = @{ + context = $UserName + itemName = $GroupName + } | ConvertTo-Json -Depth 5 + $Restparams = @{ + Uri = "https://${Server}/rest/api/1.0/admin/users/remove-group" + Method = "POST" + } + } ElseIf ($Location -eq "cloud") { + $data = "{}" + $Restparams = @{ + Uri = "https://${Server}/1.0/groups/${Instance}/${GroupName}/members/${UserName}/" + Method = "DELETE" + } + } + } + + # Generic headers, and the invocation + $Restparams += @{ ContentType = "application/json" } + $Restparams += @{ Body = $data } + $Restparams += @{ Headers = $AuthHeaders } + #$Restparams + #$Restparams.body + #$response = Invoke-RestMethod -Method "POST" "https://${Server}/rest/api/1.0/admin/users/add-groups" -Body $data -ContentType "application/json" -Headers $AuthHeaders + Try { + #Write-Verbose "$($Action) user $($Username) to group $($GroupName)" + #$Restparams.Uri + # WORKHERE + If ($PsCmdlet.ShouldProcess("$($Action) user $($UserName) to group $($GroupName)")) { + $response = Invoke-RestMethod @Restparams -Verbose:$False + } + } + Catch { + If ($Action -eq "Remove" -And $_.Exception.Response.StatusCode.value__ -eq 404) { + # just be silent when trying to delete a non-existent group. + #Write-Warning "Group ""${name}"" already exists, continuing." + } ElseIf ($Action -eq "Add" -And $_.Exception.Response.StatusCode.value__ -eq 409) { + # be silent when trying to add a user to a group he is already in + } Else { + Write-Error $_ + Write-Error $data + } + } + } +} + +Function WritePermissionsCsv_Message { + Write-Error "WritePermissionsCsv must be a valid CSV file with columns named Repo, Name. Probably use useful-write-permissions.csv from Build-Useful-Write-Permissions-List." +} + +Function ReposCsv_Message { + Write-Error "ReposCsv must be a valid CSV with columns slug, NewProjectKey. Probably use bbcloudrepos.csv for real migration, or gittest-importtest-repos.csv." +} + +# This is the individual step for making a $REPONAME-write group +Function Act-RepoPermission-Group { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory = $False)][string]$Server = "gittest.example.com", + [Parameter(Mandatory = $True )][string]$Name, + [Parameter(Mandatory = $True )][string]$ProjectSlug, + [Parameter(Mandatory = $True )][string]$Action, # either Grant or Revoke + [Parameter(Mandatory = $False)][Hashtable]$LocalHeaders = ${global:LocalHeaders}, + [Parameter(Mandatory = $False)][boolean]$Populate = $false, # if True, then add users based on PermissionsCsv + [Parameter(Mandatory = $False)][string]$WritePermissionsCsv # + ) + # assumptions: repo already exists. + + Begin { + If ($Populate) { + Try { + If (!(Test-Path $WritePermissionsCsv)) { WritePermissionsCsv_Message ; break ; } + $WritePermissions = Import-Csv $WritePermissionsCsv + $ColumnNames = $WritePermissions[0].PsObject.Properties.Name + If (!("Repo" -in $ColumnNames) -Or !("Name" -In $ColumnNames)) { WritePermissionsCsv_Message ; break ; } + } + Catch { WritePermissionsCsv_Message ; break ; } + } + If (!($Action -in @("Grant","Revoke"))) { + Throw "Action must be either Grant or Revoke" + } + } + + Process { + $Uri = "https://$($Server)/rest/api/1.0/projects/$($ProjectSlug)/repos/$($Name)/permissions/groups" + $WriteGroup = "$($Name)-write" + $WritePermissionName = "REPO_WRITE" + + If ($Action -eq "Grant") { + # flow: create group and grant it permission + #If ($PsCmdlet.ShouldProcess("Create group $($WriteGroup)")) { Act-Group -Action "Create" -Name "$($WriteGroup)" -LocalHeaders $LocalHeaders -Server $Server ; } + Act-Group -Action "Create" -Name "$($WriteGroup)" -LocalHeaders $LocalHeaders -Server $Server + $Restparams = @{ + Uri = "$($Uri)?permission=$($WritePermissionName)&name=$($WriteGroup)" + Method = "PUT" + Headers = $LocalHeaders + ContentType = "application/json" + } + #$RestParams.Uri + If ($PsCmdlet.ShouldProcess("$($Action) group $($WriteGroup) write perms to repo $($Name)")) { + Invoke-RestMethod @RestParams -Verbose:$False + } + # And now do the populate step. + If ($Populate -And $PsCmdlet.ShouldProcess("Populate group $($WriteGroup)")) { + # For every permission entry where Repo column = $Name parameter for this function, + $countUsersAdded = 0 + $WritePermissions | ? { $_.Repo -eq "$($Name)" } | % { + # add user to the group + #Write-Host "Please add user $($_.name) to $WriteGroup" + If ( "$($_.name)" -ne "") { + Try { + Act-User-In-Group -AuthHeaders $LocalHeaders -Server $Server -GroupName $WriteGroup -UserName $_.name -Action "Add" -Location "local" -WhatIf:$False -Confirm:$False + $countUsersAdded += 1 + } + Catch { + Write-Error $PSItem + $countUsersAdded -= 1 + } + } Else { # samaccountname is blank + Write-Warning "Unable to add user $_" + } + } + Write-Verbose "Added $countUsersAdded to group ""$WriteGroup""" + } + } elseif ($Action -eq "Revoke") { + # flow: remove permission to group and delete it + $Restparams = @{ + Uri = "$($Uri)?name=$($WriteGroup)" + Method = "DELETE" + Headers = $LocalHeaders + ContentType = "application/json" + } + If ($PsCmdlet.ShouldProcess("$($Action) group $($WriteGroup) write perms to repo $($Name)")) { Invoke-RestMethod @RestParams -Verbose:$False ; } + Act-Group -Action "Delete" -Name "$($WriteGroup)" -LocalHeaders $LocalHeaders -Server $Server + } + } +} + +# goal: given the input file $WritePermissionsCsv, call Act-RepoPermission-Group for each repo. +Function Act-All-RepoPermission-Groups { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory = $False)][string]$Server = "gittest.example.com", + [Parameter(Mandatory = $True )][string]$Action, # either Grant or Revoke + [Parameter(Mandatory = $False)][Hashtable]$LocalHeaders = ${global:LocalHeaders}, + [Parameter(Mandatory = $False)][boolean]$Populate = $false, # if True, then add users based on PermissionsCsv + [Parameter(Mandatory = $False)][string]$WritePermissionsCsv, + [Parameter(Mandatory = $True )][string]$ReposCsv # columns slug, NewProjectKey, probably gittest-importest-repos.csv or bbcloudrepos.csv + ) + Begin { + Try { + If (!(Test-Path $WritePermissionsCsv)) { WritePermissionsCsv_Message ; break ; } + $WritePermissions = Import-Csv $WritePermissionsCsv + $ColumnNames = $WritePermissions[0].PsObject.Properties.Name + If (!("Repo" -in $ColumnNames) -Or !("Name" -In $ColumnNames)) { WritePermissionsCsv_Message ; break ; } + } + Catch { WritePermissionsCsv_Message ; break ; } + + Try { + If (!(Test-Path $ReposCsv)) { ReposCsv_Message ; break ; } + $Repos = Import-Csv $ReposCsv + $ColumnNames = $Repos[0].PsObject.Properties.Name + If (!("slug" -in $ColumnNames) -Or !("NewProjectKey" -In $ColumnNames)) { ReposCsv_Message ; break ; } + } + Catch { ReposCsv_Message ; break ; } + + If (!($Action -in @("Grant","Revoke"))) { + Throw "Action must be either Grant or Revoke" + } + $count = 0 + $size = ($Repos | Group-Object { $_.Repo }).count + } + + Process { + $Repos | % { + # for each repository listing + Try { + Act-RepoPermission-Group -Name $_.slug -ProjectSlug $_.NewProjectKey -Action $Action -LocalHeaders $Localheaders -Server $Server -Populate $Populate -WritePermissionsCsv $WritePermissionsCsv + $count += 1 ; + [int]$percent = ($count/$size)*100 + $percent = (@($percent, 100) | Measure -Minimum).Minimum + $percent = (@($percent, 0) | Measure -Maximum).Maximum + Write-Progress -Activity "$($Action) for repo" -Status "$count/$size" -PercentComplete $percent + + } + Catch { + $RepoCount -= 1 + } + } + } +} + +# List users who have write permission on this one repo. +Function List-Write-Users-For-Cloud-Repo { + [CmdletBinding()] + param( + [Parameter(Mandatory = $True )][Hashtable]$CloudHeaders, + [Parameter(Mandatory = $False)][string]$Server = "api.bitbucket.org", + [Parameter(Mandatory = $False)][string]$Instance = "example", + [Parameter(Mandatory = $True )][string]$Name, + [Parameter(Mandatory = $False)][bool]$SkipCount = $False, + [Parameter(Mandatory = $False)][string]$Property = "uuid" + ) + $interim = _Iterate-Values-From-BB-API -StartUri "https://${Server}/2.0/teams/${Instance}/permissions/repositories/${Name}?q=permission%3D%22write%22" -Location "cloud" -SkipCount $SkipCount -Descriptor "for repo ""$Name"" write permission" + $output = @() + $interim | % { + <# # In case I want to have a multi-field object in the array. $output += @{ username = $_.user.display_name repo = $_.repository.full_name }#> + #$output += $_.user.display_name + $output += $_.user.$($Property) + } + $output +} + +# Return array of @{ Repo = "example/foobar", User = "bgstack15"} objects +# Usage: +# $cloudRepos = List-All-Cloud-Repos -CloudHeaders $CloudHeaders -Verbose +# $allWritePermissions = $cloudRepos | List-Write-Users-For-All-Cloud-Repos -CloudHeaders $CloudHeaders +# $allWritePermissions | Export-Csv U:\2002\05\all-write-permissions.csv -NoTypeInformation +# Next steps: +# use a function that makes ${Repo}-write group and adds the users +Function List-Write-Users-For-All-Cloud-Repos { + [CmdletBinding()] + param( + [Parameter(Mandatory = $True )][Hashtable]$CloudHeaders, + [Parameter(Mandatory = $False)][string]$Server = "api.bitbucket.org", + [Parameter(Mandatory = $False)][string]$Instance = "example", + [Parameter(Mandatory = $False,ValueFromPipeLine = $true,ValueFromPipelineByPropertyName = $true)]$Repos + ) + Begin { + # list all repos + #$Repos.count + $repoCount = 0 + + #if ($Repos -eq $Null) { $Repos = List-All-Cloud-Repos -CloudHeaders $CloudHeaders } + } + Process { + # iterate through all repos + #$repoCount = 0 + $_ | % { + $repoCount += 1 + #$_.name + $thisRepo = $_.name + $interim = List-Write-Users-For-Cloud-Repo -CloudHeaders $CloudHeaders -Server $Server -Instance $Instance -Name $_.name -SkipCount $True + Write-Host "Working on repo #$repoCount ""$thisRepo""" + #$interim[3] + $new = @() + ForEach ($item in $interim) { + #[psCustomObject]$newitem = $item.PsObject.Copy() # shallow copy is good enough + $newItem = New-Object -TypeName psobject + $newItem | Add-Member -MemberType NoteProperty -Name "Repo" -Value $thisRepo + $newItem | Add-Member -MemberType NoteProperty -Name "User" -Value $item + $new += $newitem + } + $new + } + } +} + +# take a list of user ids from pipeline and convert to usable property +# I don't know where I was going with this. +Function Convert-User-To-Property-STUB { + [CmdletBinding()] + param( + [Parameter(Mandatory = $True )][Hashtable]$CloudHeaders, + [Parameter(Mandatory = $False)][string]$Server = "api.bitbucket.org", + [Parameter(Mandatory = $False)][string]$Instance = "example", + [Parameter(Mandatory = $False,ValueFromPipeLine = $true,ValueFromPipelineByPropertyName = $true)]$UserIds + ) + Begin { + $userCount = 0 + Write-Error "STUB! I cannot remember where I was going with this but it looks cool." + } + + Process { + $_ | %{ + $userCount += 1 + + } + } + +} + +Function List-Groups { + [CmdletBinding()] + param( + [Parameter(Mandatory = $False)][string]$Server = "gittest.example.com", + [Parameter(Mandatory = $False)][Hashtable]$AuthHeaders = ${global:CloudHeaders}, + [Parameter(Mandatory = $False)][string]$Instance = "example", + [Parameter(Mandatory = $False)][string]$Location = "local", # "cloud" or "local" + [Parameter(Mandatory = $False)][boolean]$SkipCount = $False + ) + Begin { + If (!($Location -in @("local","cloud"))) { + Write-Error "Location must be in set @(""local"",""cloud""). Aborted." + break + } + } + Process { + If ($Location -eq "cloud") { + # we can cheat here because I know there are only 18 groups defined in bitbucket cloud, so no concerns about pagination. Plus this api endpoint is very poorly documented because it's a deprecated one despite the fact the 2.0 groups endpoint does not exist; so pagination methodology is undetermined. + $Uri = "https://${Server}/1.0/groups/${Instance}/" + Invoke-RestMethod -ContentType "application/json" -Uri $Uri -method "GET" -Headers $AuthHeaders + } ElseIf ($Location -eq "local") { + $Uri = "https://${Server}/rest/api/1.0/admin/groups" + _Iterate-Values-From-BB-API -AuthHeaders $AuthHeaders -StartUri $Uri -Location $Location -Descriptor $Group -SkipCount $SkipCount + + } + } +} + +# Usage: +# $cloudgroups = List-Groups -Location "cloud" -Server "api.bitbucket.org" +# $cloudmemberships = $cloudgroups | List-Memberships-From-Cloud-Groups +# $cloudmemberships | Export-Csv -NoTypeInformation "U:\2020\05\cloud-memberships.csv" +Function List-Memberships-From-Cloud-Groups { + # $cloudgroups = List-Groups -Location "cloud" -Server "api.bitbucket.org" + # $cloudmemberships = $cloudgroups | Select-Object name,members,slug | Select-Object -Property slug,name -ExpandProperty members | Select-Object -Property display_name,account_id,uuid,nickname,@{l="groupname";e={$_.name}},@{l="groupslug";e={$_.slug}} + [CmdletBinding()] + Param( + [Parameter(Mandatory = $False,ValueFromPipeLine = $true,ValueFromPipelineByPropertyName = $true)]$groups + ) + Begin { + } + Process { + $_ | Select-Object name,members,slug | Select-Object -Property name,slug -ExpandProperty members -Erroraction "silentlycontinue" | Select-Object -Property display_name,account_id,uuid,nickname,@{l="groupname";e={$_.name}},@{l="groupslug";e={$_.slug}} + } +} + +# goal: take $cloudmemberships (with attributes uuid, groupname) and take $Action on them +Function Act-All-Cloud-Memberships-From-List { + [CmdletBinding()] + param( + [Parameter(Mandatory = $False)][string]$Server = "api.bitbucket.org", + [Parameter(Mandatory = $False)][Hashtable]$AuthHeaders = ${global:CloudHeaders}, + [Parameter(Mandatory = $False)][string]$Instance = "example", + [Parameter(Mandatory = $True )][string]$Action, # either Add or Remove + [Parameter(Mandatory = $False)][string]$Location = "cloud", # "cloud" or "local" + [Parameter(Mandatory = $False,ValueFromPipeLine = $true)]$MembershipsPipeLine, + [Parameter(Mandatory = $False)][string]$MembershipsCsv, + [Parameter(Mandatory = $False)][PSObject]$Memberships, + [Parameter(Mandatory = $False)][string]$GroupMatchProperty = "groupname" + # WORKHERE + ) + Begin { + If (!($Action -in @("Add","Remove"))) { + Write-Error "Action must be in set @(""Add"",""Remove""). Aborted." + break + } + If (!($Location -in @("local","cloud"))) { + Write-Error "Location must be in set @(""local"",""cloud""). Aborted." + break + } + If (!($GroupMatchProperty -in @("groupname","groupslug"))) { + Write-Error "GroupMatchProperty must be in set @(""groupname"",""groupslug""). Aborted." + break + } + } + Process { + If ($MembershipsPipeLine -eq $Null) { + If ($Memberships -eq $Null) { + If ("$($MembershipsCsv)" -eq "" -Or !(Test-Path $MembershipsCsv)) { + Write-Error "Either pipe in or provide -Memberships with uuid,display_name,groupname values or provide MembershipsCsv file with such-named columns. Aborted." + break + } + Write-Warning "using csv" + $Memberships = Import-Csv $MembershipsCsv + } Else { + Write-Warning "using memberships var directly" + } + } Else { + #Write-Warning "using pipeline" + $Memberships = $MembershipsPipeLine + } + $Memberships | ? { $_.groupname -ne "Administrators" } | % { + Write-Verbose "$($Action) user $($_.display_name) ( uuid $($_.uuid) ) to group $($_.$($GroupMatchProperty))" + Act-User-In-Group -Server $Server -AuthHeaders $AuthHeaders -GroupName "$($_.$($GroupMatchProperty))" -UserName $_.uuid -Instance $Instance -Action $Action -Location $Location + } + } +} + +# Goal: take Repo,User list and the display_name,email list and output a repo,samaccountname,user,email table. +# The NamesToEmailsCsv is a manually curated list that is boiled down from all the Users listed from the WritePermissionsCsv ( | sort | uniq ) with email address for each user. +# Usage: +# $list = Build-Useful-Write-Permissions-List -WritePermissionsCsv "U:\2020\05\all-write-permissions.csv" -NamesToEmailsCsv "U:\2020\05\bitbucket-emails.csv" +# $list | Export-Csv -NoTypeInformation "U:\2020\05\useful-write-permissions.csv" +Function Build-Useful-Write-Permissions-List { + [CmdletBinding()] + param( + [Parameter(Mandatory = $True )][string]$WritePermissionsCsv, + [Parameter(Mandatory = $True )][string]$NamesToEmailsCsv, + [Parameter(Mandatory = $False)][string]$DefaultDomain = "example.com" + ) + Begin { + If (!(Test-Path $WritePermissionsCsv)) { + Write-Error "WritePermissionsCsv must be a valid CSV file with columns named Repo, User." + break + } + If (!(Test-Path $NamesToEmailsCsv)) { + Write-Error "NamesToEmailsCsv must be a valid CSV file with columns named display_name,email." + break + } + $Output = @() + $count = 0 + $WritePermissions = Import-Csv $WritePermissionsCsv + $NamesToEmails = Import-Csv $NamesToEmailsCsv + $size = $WritePermissions.count + } + + Process { + ForEach ($wpitem in $WritePermissions) { + if (!($wpitem.User -In $NamesToEmails.display_name)) { + # just be silent + #Write-Error "Cannot find $($wpitem.User) in NamesToEmails" + } Else { + $count += 1 ; + [int]$percent = ($count/$size)*100 + $percent = (@($percent, 100) | Measure -Minimum).Minimum + $percent = (@($percent, 0) | Measure -Maximum).Maximum + Write-Progress -Activity "Listing perm" -Status "$count/$size" -PercentComplete $percent + + $NamesToEmails | % { + $NameToEmailitem = $_ + if ($NameToEmailItem.display_name -eq $wpitem.User) { + If ("$($NameToEmailItem.email)" -ne "") { + $thisADUser = Get-ADUser -Filter "mail -eq '$($NameToEmailItem.email)'" -Properties mail + if (!($thisADUser -ne $null)) { + # if it is still empty, treat the stored email as a upn + $thisADUser = Get-ADUser -Filter "userprincipalname -eq '$($NameToEmailItem.email)'" -Properties mail + } + if (!($thisADUser -ne $null)) { + # if still empty, try it as sammaccountname@domain + $matchString = $($NameToEmailItem.email.split("@")[0]) + $thisADUser = Get-ADUser -Filter "samaccountname -eq '$matchString'" -Properties mail + } + if (!($thisADUser -ne $null)) { + Write-Error "Unable to match $($NameToEmailItem.email) to an AD user!" + } else { + $newItem = New-Object -TypeName psobject + $newItem | Add-Member -MemberType NoteProperty -Name "Repo" -Value $wpitem.Repo + $newItem | Add-Member -MemberType NoteProperty -Name "Displayname" -Value $wpitem.User + $newItem | Add-Member -MemberType NoteProperty -Name "Mail" -Value $thisADUser.mail + $newItem | Add-Member -MemberType NoteProperty -Name "Name" -Value $thisADUser.samaccountname + $Output += $newItem + Write-Verbose $newItem + } + } Else { # so if there is no email address listed for this user + # well apparently it lists the non-email-mapped ones a lot, so just be quiet. + #Write-Error "No email for user $($NameToEmailItem.display_name)" + } + } + } + } + } + } + End { $Output ; } +} + +# Procedural section +if ($LoadVars) { + . '\\rdputils1\e$\scripts\Functions\credlib.ps1' + $cloudcred = Get-Shared-Credential -User "exampleteam" -PasswordCategory exampleteam + $CloudHeaders = Get-Bitbucket-Auth-Headers -Credential $cloudcred + $bbprodcred = Get-Shared-Credential -User "serviceaccount"-PasswordCategory "serviceaccount" + $bbprodHeaders = Get-Bitbucket-Auth-Headers -Credential $bbprodcred + $localcred = $bbprodcred + $localHeaders = Get-Bitbucket-Auth-headers -Credential $localcred + Remove-Variable LoadVars +} diff --git a/bitbucketlib.ps1/description b/bitbucketlib.ps1/description new file mode 100644 index 0000000..77ac9c1 --- /dev/null +++ b/bitbucketlib.ps1/description @@ -0,0 +1 @@ +A small powershell library for interacting with Bitbucket Cloud and Server API -- cgit