summaryrefslogtreecommitdiff
path: root/bitbucketlib.ps1
diff options
context:
space:
mode:
Diffstat (limited to 'bitbucketlib.ps1')
-rw-r--r--bitbucketlib.ps1/bitbucketlib.ps1864
-rw-r--r--bitbucketlib.ps1/description1
2 files changed, 865 insertions, 0 deletions
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
bgstack15