AzureDevOpsDscv3.psm1
|
enum Ensure { Present Absent } enum SourceControl { Git Tfvc } enum RequiredAction { None Get New Set Remove Test Error } <# .SYNOPSIS Manages Azure DevOps projects for an organization. .PARAMETER ProjectName Azure DevOps project name. .PARAMETER Description Optional project description. .PARAMETER SourceControlType Source control type for the project (Git or Tfvc). .PARAMETER Organization Azure DevOps organization name. .PARAMETER Ensure Desired state: Present or Absent. .PARAMETER pat Personal access token used for authentication. .PARAMETER templateTypeId Process template type ID used when creating a project. .PARAMETER apiVersion Azure DevOps REST API version. #> [DscResource()] class ProjectResource { [DscProperty(Key)] [string]$ProjectName [DscProperty()] [string]$Description [DscProperty()] [string]$SourceControlType = [SourceControl]::Git [DscProperty(Mandatory)] [string]$Organization [DscProperty()] [Ensure]$Ensure = [Ensure]::Present [DscProperty(Mandatory)] [Alias('Token','PersonalAccessToken')] [System.String] $pat [DscProperty()] [string]$templateTypeId = "adcc42ab-9882-485e-a3ed-7678f01f66bc" [DscProperty()] [string]$apiVersion = "7.1" hidden [string] GetTokenValue() { if ($this.pat -is [hashtable]) { $keyNames = @("value", "secureString", "Token", "PersonalAccessToken", "pat", "_value") foreach ($key in $keyNames) { if ($this.pat.ContainsKey($key)) { return $this.pat[$key].ToString() } } $firstKey = $this.pat.Keys | Select-Object -First 1 if ($firstKey) { return $this.pat[$firstKey].ToString() } } return $this.pat.ToString() } hidden [object] CallApi([string]$Method, [string]$UriSuffix, [string]$Body) { [string]$StringToken = $this.GetTokenValue() $Base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$StringToken")) $BaseUrl = "https://dev.azure.com/$($this.Organization)/_apis" $Uri = $BaseUrl + "/projects" + $UriSuffix + "?api-version=$($this.apiVersion)" $Headers = @{ Authorization = "Basic $Base64AuthInfo" "Content-Type" = "application/json" } if ($Method -eq "PATCH") { $Headers["Content-Type"] = "application/json-patch+json" } if ($Method -eq "PATCH") { $Headers["Content-Type"] = "application/json-patch+json" } $Params = @{ Uri = $Uri Method = $Method Headers = $Headers ErrorAction = "Stop" } if ($Body) { $Params.Body = $Body } try { return Invoke-RestMethod @Params } catch { $errorMsg = $_ Write-Error "ProjectResource.CallApi error: $errorMsg" if ($_.Exception.Response) { $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream()) $reader.BaseStream.Position = 0 $reader.DiscardBufferedData() $responseBody = $reader.ReadToEnd() Write-Error "ProjectResource API Response Body: $responseBody" } throw } } [bool] Test() { try { $EncodedName = [uri]::EscapeDataString($this.ProjectName) $Project = $this.CallApi("GET", "/$EncodedName", $null) $Exists = $null -ne $Project if ($this.Ensure -eq [Ensure]::Present) { return $Exists } else { return !$Exists } } catch { return $this.Ensure -eq [Ensure]::Absent } } [void] Set() { if ($this.Ensure -eq [Ensure]::Present) { # Check if project already exists try { $EncodedName = [uri]::EscapeDataString($this.ProjectName) $Project = $this.CallApi("GET", "/$EncodedName", $null) if ($Project) { return } } catch { # Project doesn't exist, continue with creation } $Payload = @{ name = $this.ProjectName capabilities = @{ versioncontrol = @{ sourceControlType = $this.SourceControlType } processTemplate = @{ templateTypeId = $this.templateTypeId } } } if (![string]::IsNullOrEmpty($this.Description)) { $Payload.description = $this.Description } $JsonPayload = $Payload | ConvertTo-Json -Depth 10 $this.CallApi("POST", "", $JsonPayload) } else { Write-Host "DEBUG: Deleting project: $($this.ProjectName)" $EncodedName = [uri]::EscapeDataString($this.ProjectName) try { $Project = $this.CallApi("GET", "/$EncodedName", $null) if ($Project) { $ProjectId = $Project.id $this.CallApi("DELETE", "/$ProjectId", $null) } } catch { # Project not found; nothing to delete } } } [ProjectResource] Get() { try { $EncodedName = [uri]::EscapeDataString($this.ProjectName) $Project = $this.CallApi("GET", "/$EncodedName", $null) return [ProjectResource]@{ ProjectName = $Project.name Description = $Project.description SourceControlType = $Project.capabilities.versioncontrol.sourceControlType Ensure = [Ensure]::Present } } catch { return [ProjectResource]@{ ProjectName = $this.ProjectName Ensure = [Ensure]::Absent } } } } <# .SYNOPSIS Manages Azure DevOps user entitlements for an organization. .PARAMETER UserPrincipalName User principal name (email) to manage entitlements for. .PARAMETER Organization Azure DevOps organization name. .PARAMETER AccessLevel Access level: Stakeholder, Basic, or BasicPlusTestPlans. .PARAMETER Ensure Desired state: Present or Absent. .PARAMETER pat Personal access token used for authentication. .PARAMETER apiVersion Azure DevOps REST API version. #> [DscResource()] class OrganizationUserResource { [DscProperty(Key)] [string]$UserPrincipalName [DscProperty(Mandatory)] [string]$Organization [DscProperty()] [string]$AccessLevel = "Stakeholder" [DscProperty()] [Ensure]$Ensure = [Ensure]::Present [DscProperty(Mandatory)] [Alias('Token','PersonalAccessToken')] [string]$pat [DscProperty()] [string]$apiVersion = "7.1-preview.1" hidden [string] GetTokenValue() { if ($this.pat -is [hashtable]) { $keyNames = @("value", "secureString", "Token", "PersonalAccessToken", "pat", "_value") foreach ($key in $keyNames) { if ($this.pat.ContainsKey($key)) { return $this.pat[$key].ToString() } } $firstKey = $this.pat.Keys | Select-Object -First 1 if ($firstKey) { return $this.pat[$firstKey].ToString() } } return $this.pat.ToString() } hidden [string] GetOrganizationValue() { if ($this.Organization -is [hashtable]) { $firstValue = $this.Organization.Values | Select-Object -First 1 if ($firstValue) { return $firstValue.ToString() } } return $this.Organization } hidden [object] CallApi([string]$Method, [string]$UriSuffix, [string]$Body) { [string]$StringToken = $this.GetTokenValue() [string]$OrgName = $this.GetOrganizationValue() if ([string]::IsNullOrWhiteSpace($StringToken)) { throw "Token is null or empty" } if ([string]::IsNullOrWhiteSpace($OrgName)) { throw "Organization is null or empty" } $Base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$StringToken")) $BaseUrl = "https://vsaex.dev.azure.com/$OrgName/_apis" $Uri = $BaseUrl + $UriSuffix + "?api-version=$($this.apiVersion)" $Headers = @{ Authorization = "Basic $Base64AuthInfo" "Content-Type" = "application/json" } if ($Method -eq "PATCH") { $Headers["Content-Type"] = "application/json-patch+json" } $Params = @{ Uri = $Uri Method = $Method Headers = $Headers ErrorAction = "Stop" } if ($Body) { $Params.Body = $Body } try { return Invoke-RestMethod @Params } catch { $errorMsg = $_ Write-Error "OrganizationUserResource.CallApi error: $errorMsg" if ($_.Exception.Response) { $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream()) $reader.BaseStream.Position = 0 $reader.DiscardBufferedData() $responseBody = $reader.ReadToEnd() Write-Error "OrganizationUserResource API Response Body: $responseBody" } throw } } hidden [string] GetAccountLicenseType() { $result = switch ($this.AccessLevel) { "Stakeholder" { "stakeholder" } "Basic" { "express" } "BasicPlusTestPlans" { "advanced" } default { "stakeholder" } } return $result } [bool] Test() { try { Write-Verbose "Test() - Checking user: $($this.UserPrincipalName)" $users = $this.CallApi("GET", "/userentitlements", $null) if ($null -eq $users -or $null -eq $users.value) { Write-Verbose "Test() - No users returned from API" return $this.Ensure -eq [Ensure]::Absent } $existingUser = $users.value | Where-Object { $_.user.principalName -eq $this.UserPrincipalName } if ($this.Ensure -eq [Ensure]::Present) { if ($existingUser) { $currentLicense = $existingUser.accessLevel.accountLicenseType $desiredLicense = $this.GetAccountLicenseType() $match = $currentLicense -eq $desiredLicense Write-Verbose "Test() - User exists with license: $currentLicense, desired: $desiredLicense, match: $match" return $match } Write-Verbose "Test() - User does not exist" return $false } else { $exists = $null -ne $existingUser Write-Verbose "Test() - Ensure=Absent, user exists: $exists" return !$exists } } catch { Write-Error "Test() - Error: $_" return $this.Ensure -eq [Ensure]::Absent } } [void] Set() { try { if ($this.Ensure -eq [Ensure]::Present) { # Check if user already exists and matches desired license $users = $this.CallApi("GET", "/userentitlements", $null) $existingUser = $users.value | Where-Object { $_.user.principalName -eq $this.UserPrincipalName } if ($null -eq $existingUser) { $Payload = @{ accessLevel = @{ accountLicenseType = $this.GetAccountLicenseType() licensingSource = "account" } user = @{ principalName = $this.UserPrincipalName subjectKind = "user" } projectEntitlements = @() } $JsonPayload = $Payload | ConvertTo-Json -Depth 10 $response = $this.CallApi("POST", "/userentitlements", $JsonPayload) } else { $userId = $existingUser.id $Payload = @( @{ op = "replace" path = "/accessLevel" value = @{ accountLicenseType = $this.GetAccountLicenseType() licensingSource = "account" } } ) $JsonPayload = ConvertTo-Json -Depth 10 -InputObject $Payload $response = $this.CallApi("PATCH", "/userentitlements/$userId", $JsonPayload) } } else { $users = $this.CallApi("GET", "/userentitlements", $null) $existingUser = $users.value | Where-Object { $_.user.principalName -eq $this.UserPrincipalName } if ($existingUser) { $userId = $existingUser.id $this.CallApi("DELETE", "/userentitlements/$userId", $null) } } } catch { Write-Error "Set() - Error: $_" throw } } [OrganizationUserResource] Get() { try { Write-Verbose "Get() - Retrieving user: $($this.UserPrincipalName)" $users = $this.CallApi("GET", "/userentitlements", $null) if ($null -eq $users) { Write-Verbose "Get() - No users found" $result = [OrganizationUserResource]::new() $result.UserPrincipalName = $this.UserPrincipalName $result.Organization = $this.GetOrganizationValue() $result.Ensure = [Ensure]::Absent $result.pat = $this.pat $result.apiVersion = $this.apiVersion return $result } $existingUser = $users.value | Where-Object { $_.user.principalName -eq $this.UserPrincipalName } if ($existingUser) { Write-Verbose "Get() - User found" $licenseType = $existingUser.accessLevel.accountLicenseType $userAccessLevel = switch ($licenseType) { "stakeholder" { "Stakeholder" } "express" { "Basic" } "advanced" { "BasicPlusTestPlans" } default { "Stakeholder" } } $result = [OrganizationUserResource]::new() $result.UserPrincipalName = $existingUser.user.principalName $result.Organization = $this.GetOrganizationValue() $result.AccessLevel = $userAccessLevel $result.Ensure = [Ensure]::Present $result.pat = $this.pat $result.apiVersion = $this.apiVersion return $result } else { Write-Verbose "Get() - User not found" $result = [OrganizationUserResource]::new() $result.UserPrincipalName = $this.UserPrincipalName $result.Organization = $this.GetOrganizationValue() $result.AccessLevel = $this.AccessLevel $result.Ensure = [Ensure]::Absent $result.pat = $this.pat $result.apiVersion = $this.apiVersion return $result } } catch { Write-Error "Get() - Error: $_" $result = [OrganizationUserResource]::new() $result.UserPrincipalName = $this.UserPrincipalName $result.Organization = $this.GetOrganizationValue() $result.Ensure = [Ensure]::Absent $result.pat = $this.pat $result.apiVersion = $this.apiVersion return $result } } } <# .SYNOPSIS Manages Azure DevOps group entitlements for an organization. .PARAMETER GroupOriginId Origin ID of the group in Azure DevOps. .PARAMETER GroupDisplayName Optional display name for the group. .PARAMETER Organization Azure DevOps organization name. .PARAMETER AccessLevel Access level: Stakeholder, Basic, or BasicPlusTestPlans. .PARAMETER Ensure Desired state: Present or Absent. .PARAMETER pat Personal access token used for authentication. .PARAMETER apiVersion Azure DevOps REST API version. #> [DscResource()] class OrganizationGroupResource { [DscProperty(Key)] [string]$GroupOriginId [DscProperty()] [string]$GroupDisplayName [DscProperty(Mandatory)] [string]$Organization [DscProperty()] [string]$AccessLevel = "Stakeholder" [DscProperty()] [Ensure]$Ensure = [Ensure]::Present [DscProperty(Mandatory)] [Alias('Token','PersonalAccessToken')] [string]$pat [DscProperty()] [string]$apiVersion = "7.1-preview.1" hidden [string] GetTokenValue() { if ($this.pat -is [hashtable]) { $keyNames = @("value", "secureString", "Token", "PersonalAccessToken", "pat", "_value") foreach ($key in $keyNames) { if ($this.pat.ContainsKey($key)) { return $this.pat[$key].ToString() } } $firstKey = $this.pat.Keys | Select-Object -First 1 if ($firstKey) { return $this.pat[$firstKey].ToString() } } return $this.pat.ToString() } hidden [string] GetOrganizationValue() { if ($this.Organization -is [hashtable]) { $firstValue = $this.Organization.Values | Select-Object -First 1 if ($firstValue) { return $firstValue.ToString() } } return $this.Organization } hidden [object] CallApi([string]$Method, [string]$UriSuffix, [string]$Body) { [string]$StringToken = $this.GetTokenValue() [string]$OrgName = $this.GetOrganizationValue() if ([string]::IsNullOrWhiteSpace($StringToken)) { throw "Token is null or empty" } if ([string]::IsNullOrWhiteSpace($OrgName)) { throw "Organization is null or empty" } $Base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$StringToken")) $BaseUrl = "https://vsaex.dev.azure.com/$OrgName/_apis" $Uri = $BaseUrl + $UriSuffix + "?api-version=$($this.apiVersion)" $Headers = @{ Authorization = "Basic $Base64AuthInfo" "Content-Type" = "application/json" } if ($Method -eq "PATCH") { $Headers["Content-Type"] = "application/json-patch+json" } $Params = @{ Uri = $Uri Method = $Method Headers = $Headers ErrorAction = "Stop" } if ($Body) { $Params.Body = $Body } try { return Invoke-RestMethod @Params } catch { $errorMsg = $_ Write-Error "OrganizationGroupResource.CallApi error: $errorMsg" if ($_.Exception.Response) { $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream()) $reader.BaseStream.Position = 0 $reader.DiscardBufferedData() $responseBody = $reader.ReadToEnd() Write-Error "OrganizationGroupResource API Response Body: $responseBody" } throw } } hidden [string] GetAccountLicenseType() { $result = switch ($this.AccessLevel) { "Stakeholder" { "stakeholder" } "Basic" { "express" } "BasicPlusTestPlans" { "advanced" } default { "stakeholder" } } return $result } [bool] Test() { try { Write-Verbose "Test() - Checking group entitlement: $($this.GroupOriginId)" $groups = $this.CallApi("GET", "/groupentitlements", $null) if ($null -eq $groups -or $null -eq $groups.value) { Write-Verbose "Test() - No group entitlements returned from API" return $this.Ensure -eq [Ensure]::Absent } $existingGroup = $groups.value | Where-Object { $_.group.originId -eq $this.GroupOriginId } if ($this.Ensure -eq [Ensure]::Present) { if ($existingGroup) { $currentLicense = $existingGroup.licenseRule.accountLicenseType $desiredLicense = $this.GetAccountLicenseType() $match = $currentLicense -eq $desiredLicense Write-Verbose "Test() - Group exists with license: $currentLicense, desired: $desiredLicense, match: $match" return $match } Write-Verbose "Test() - Group entitlement does not exist" return $false } else { $exists = $null -ne $existingGroup Write-Verbose "Test() - Ensure=Absent, group exists: $exists" return !$exists } } catch { Write-Error "Test() - Error: $_" return $this.Ensure -eq [Ensure]::Absent } } [void] Set() { try { if ($this.Ensure -eq [Ensure]::Present) { $groups = $this.CallApi("GET", "/groupentitlements", $null) $existingGroup = $groups.value | Where-Object { $_.group.originId -eq $this.GroupOriginId } if ($null -eq $existingGroup) { $Payload = @{ group = @{ originId = $this.GroupOriginId subjectKind = "group" } licenseRule = @{ accountLicenseType = $this.GetAccountLicenseType() licensingSource = "account" } } if (![string]::IsNullOrWhiteSpace($this.GroupDisplayName)) { $Payload.group.displayName = $this.GroupDisplayName } $JsonPayload = $Payload | ConvertTo-Json -Depth 10 $response = $this.CallApi("POST", "/groupentitlements", $JsonPayload) } else { $groupId = $existingGroup.id $Payload = @( @{ op = "replace" path = "/licenseRule/accountLicenseType" value = $this.GetAccountLicenseType() } @{ op = "replace" path = "/licenseRule/licensingSource" value = "account" } ) $JsonPayload = ConvertTo-Json -Depth 10 -InputObject $Payload $response = $this.CallApi("PATCH", "/groupentitlements/$groupId", $JsonPayload) } } else { $groups = $this.CallApi("GET", "/groupentitlements", $null) $existingGroup = $groups.value | Where-Object { $_.group.originId -eq $this.GroupOriginId } if ($existingGroup) { $groupId = $existingGroup.id $this.CallApi("DELETE", "/groupentitlements/$groupId", $null) } } } catch { Write-Error "Set() - Error: $_" throw } } [OrganizationGroupResource] Get() { try { Write-Verbose "Get() - Retrieving group entitlement: $($this.GroupOriginId)" $groups = $this.CallApi("GET", "/groupentitlements", $null) if ($null -eq $groups) { Write-Verbose "Get() - No group entitlements found" $result = [OrganizationGroupResource]::new() $result.GroupOriginId = $this.GroupOriginId $result.Organization = $this.GetOrganizationValue() $result.Ensure = [Ensure]::Absent $result.pat = $this.pat $result.apiVersion = $this.apiVersion return $result } $existingGroup = $groups.value | Where-Object { $_.group.originId -eq $this.GroupOriginId } if ($existingGroup) { Write-Verbose "Get() - Group entitlement found" $licenseType = $existingGroup.licenseRule.accountLicenseType $groupAccessLevel = switch ($licenseType) { "stakeholder" { "Stakeholder" } "express" { "Basic" } "advanced" { "BasicPlusTestPlans" } default { "Stakeholder" } } $result = [OrganizationGroupResource]::new() $result.GroupOriginId = $existingGroup.group.originId $result.GroupDisplayName = $existingGroup.group.displayName $result.Organization = $this.GetOrganizationValue() $result.AccessLevel = $groupAccessLevel $result.Ensure = [Ensure]::Present $result.pat = $this.pat $result.apiVersion = $this.apiVersion return $result } else { Write-Verbose "Get() - Group entitlement not found" $result = [OrganizationGroupResource]::new() $result.GroupOriginId = $this.GroupOriginId $result.GroupDisplayName = $this.GroupDisplayName $result.Organization = $this.GetOrganizationValue() $result.AccessLevel = $this.AccessLevel $result.Ensure = [Ensure]::Absent $result.pat = $this.pat $result.apiVersion = $this.apiVersion return $result } } catch { Write-Error "Get() - Error: $_" $result = [OrganizationGroupResource]::new() $result.GroupOriginId = $this.GroupOriginId $result.GroupDisplayName = $this.GroupDisplayName $result.Organization = $this.GetOrganizationValue() $result.Ensure = [Ensure]::Absent $result.pat = $this.pat $result.apiVersion = $this.apiVersion return $result } } } |