Skip to content

Users/taaluru/6530 tenant organization connections #127

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/tools/private/*.ps1
tests/out*
coverage.xml
myenv.ps1
Expand Down
Binary file added ProjectStatisticsReport.xlsx
Binary file not shown.
Binary file added ServiceConnectionsReport.xlsx
Binary file not shown.
6 changes: 6 additions & 0 deletions TestPlanUsageReport.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"ProjectName","TestPlansEnabled","NumberOfTestPlans"
"uwv-fp4","False","0"
"plt-exp","False","0"
"tnt-cop","False","0"
"plt-sep","False","0"
"DEPRICATED-uwv-fp1","False","0"
8 changes: 4 additions & 4 deletions pipelines/azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ pool:

variables:
# Set to your variable group containing ADO_PAT
- group: 'ado-psrule-run'
- group: 'azdo-psrule-run'
# Set to your Azure DevOps organization
- name: devops_organization
value: 'cloudyspells'
value: 'tcsnlps'
# Set to your Azure DevOps project
- name: devops_project
value: 'psrule-fail-project'
value: 'ssc-set'

schedules:
- cron: "5 8 * * 0"
Expand Down Expand Up @@ -47,7 +47,7 @@ stages:
inputs:
targetType: 'inline'
script: |
Connect-AzDevOps -Organization $(devops_organization) -PAT "$(ADOPAT)"
Connect-AzDevOps -Organization $(AZDO-ORGANIZATION) -PAT "$(AZDO-PAT)"
Export-AzDevOpsRuleData `
-Project $(devops_project) `
-OutputPath .\Temp
Expand Down
64 changes: 42 additions & 22 deletions src/PSRule.Rules.AzureDevOps/Classes/AzureDevOpsConnection.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
#
# Path: src/PSRule.Rules.AzureDevOps/Functions/Connection.ps1
# This class contains methods to connect to Azure DevOps Rest API
# using a service principal, managed identity or personal access token (PAT).
# it provides an authentication header which is refreshed automatically when it expires.
# using a service principal, managed identity, personal access token (PAT),
# or Bearer token (OAuth 2.0 access token). It provides an authentication
# header which is refreshed automatically when it expires for supported auth types.
# --------------------------------------------------
#

class AzureDevOpsConnection {
[string]$Organization
Expand All @@ -18,7 +18,6 @@ class AzureDevOpsConnection {
[System.DateTime]$TokenExpires
[string]$AuthType
[string]$TokenType


# Constructor for Service Principal
AzureDevOpsConnection(
Expand All @@ -27,7 +26,6 @@ class AzureDevOpsConnection {
[string]$ClientSecret,
[string]$TenantId,
[string]$TokenType = 'FullAccess'

)
{
$this.Organization = $Organization
Expand All @@ -38,6 +36,7 @@ class AzureDevOpsConnection {
$this.Token = $null
$this.TokenExpires = [System.DateTime]::MinValue
$this.TokenType = $TokenType
$this.AuthType = 'ServicePrincipal'

# Get a token for the Azure DevOps REST API
$this.GetServicePrincipalToken()
Expand All @@ -51,17 +50,19 @@ class AzureDevOpsConnection {
{
$this.Organization = $Organization
# Get the Managed Identity token endpoint for the Azure DevOps REST API
if(-not $env:IDENTITY_ENDPOINT) {
if (-not $env:IDENTITY_ENDPOINT) {
$env:IDENTITY_ENDPOINT = "http://169.254.169.254/metadata/identity/oauth2/token"
}
if($env:ADO_MSI_CLIENT_ID) {
if ($env:ADO_MSI_CLIENT_ID) {
$this.TokenEndpoint = "$($env:IDENTITY_ENDPOINT)?resource=499b84ac-1321-427f-aa17-267ca6975798&api-version=2019-08-01&client_id=$($env:ADO_MSI_CLIENT_ID)"
} else {
}
else {
$this.TokenEndpoint = "$($env:IDENTITY_ENDPOINT)?resource=499b84ac-1321-427f-aa17-267ca6975798&api-version=2019-08-01"
}
$this.Token = $null
$this.TokenExpires = [System.DateTime]::MinValue
$this.TokenType = $TokenType
$this.AuthType = 'ManagedIdentity'

# Get a token for the Azure DevOps REST API
$this.GetManagedIdentityToken()
Expand All @@ -79,11 +80,32 @@ class AzureDevOpsConnection {
$this.Token = $null
$this.TokenExpires = [System.DateTime]::MaxValue
$this.TokenType = $TokenType
$this.AuthType = 'PAT'

# Get a token for the Azure DevOps REST API
$this.GetPATToken()
}

# Constructor for Bearer Token
AzureDevOpsConnection(
[string]$Organization,
[string]$AccessToken,
[string]$TokenType = 'FullAccess',
[switch]$Bearer
)
{
$this.Organization = $Organization
$this.Token = "Bearer $AccessToken"
$this.TokenExpires = [System.DateTime]::Now.AddHours(1) # Default 1-hour expiry
$this.TokenType = $TokenType
$this.AuthType = 'Bearer'

# Validate token format
if (-not $AccessToken -or $AccessToken -notmatch "^[A-Za-z0-9._-]+\.[A-Za-z0-9._-]+\.[A-Za-z0-9._-]+$") {
throw "Invalid Bearer token format. Ensure the token is a valid JWT."
}
}

# Get a token for the Azure DevOps REST API using a service principal
[void]GetServicePrincipalToken()
{
Expand All @@ -93,14 +115,9 @@ class AzureDevOpsConnection {
client_secret = $this.ClientSecret
scope = '499b84ac-1321-427f-aa17-267ca6975798/.default'
}
# URL encode the client secret and id
$secret = [System.Web.HttpUtility]::UrlEncode($this.ClientSecret)
$id = [System.Web.HttpUtility]::UrlEncode($this.ClientId)
#$body = "client_id=$($id)&client_secret=$($secret)&scope=499b84ac-1321-427f-aa17-267ca6975798/.default&grant_type=client_credentials"
$header = @{
'Content-Type' = 'application/x-www-form-urlencoded'
}
# POST as form url encoded body using the token endpoint
$response = Invoke-RestMethod -Uri $this.TokenEndpoint -Method Post -Body $body -ContentType 'application/x-www-form-urlencoded' -Headers $header
$this.Token = "Bearer $($response.access_token)"
$this.TokenExpires = [System.DateTime]::Now.AddSeconds($response.expires_in)
Expand All @@ -111,31 +128,30 @@ class AzureDevOpsConnection {
[void]GetManagedIdentityToken()
{
$header = @{}
If($env:IDENTITY_HEADER) {
$header = @{ 'X-IDENTITY-HEADER' = "$env:IDENTITY_HEADER" ; Metadata = 'true'}
} else {
If ($env:IDENTITY_HEADER) {
$header = @{ 'X-IDENTITY-HEADER' = "$env:IDENTITY_HEADER" ; Metadata = 'true' }
}
else {
$header = @{ Metadata = 'true' }
}
$response = Invoke-RestMethod -Uri $this.TokenEndpoint -Method Get -Headers $header
$this.Token = "Bearer $($response.access_token)"
# Get token expiration time from the expires_on property and convert it from unix to a DateTime object
$this.TokenExpires = (Get-Date 01.01.1970).AddSeconds($response.expires_on)
$this.AuthType = 'ManagedIdentity'
}

# Get a token for the Azure DevOps REST API using a personal access token (PAT)
[void]GetPATToken()
{
# base64 encode the PAT
$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes((":$($this.PAT)")))
$this.Token = 'Basic ' + $base64AuthInfo
$this.AuthType = 'PAT'
}

# Get the the up to date authentication header for the Azure DevOps REST API
# Get the up-to-date authentication header for the Azure DevOps REST API
[System.Collections.Hashtable]GetHeader()
{
# If the token is expired, get a new one
# If the token is expired, attempt to refresh (except for Bearer and PAT)
if ($this.TokenExpires -lt [System.DateTime]::Now) {
switch ($this.AuthType) {
'ServicePrincipal' {
Expand All @@ -145,13 +161,17 @@ class AzureDevOpsConnection {
$this.GetManagedIdentityToken()
}
'PAT' {
# PAT tokens don't expire
# PAT tokens don't expire in this context
}
'Bearer' {
throw "Bearer token has expired. Please provide a new token via Connect-AzDevOps."
}
}
}
$header = @{
Authorization = $this.Token
'Content-Type' = 'application/json'
}
return $header
}
}
}
111 changes: 69 additions & 42 deletions src/PSRule.Rules.AzureDevOps/Functions/Common.ps1
Original file line number Diff line number Diff line change
@@ -1,87 +1,114 @@
<#
 <#
.SYNOPSIS
Connect to Azure DevOps for a session using a Service Principal, Managed Identity or Personal Access Token (PAT)
Connects to an Azure DevOps organization for use with PSRule.Rules.AzureDevOps cmdlets.

.DESCRIPTION
Connect to Azure DevOps for a session using a Service Principal, Managed Identity or Personal Access Token (PAT)
The Connect-AzDevOps function establishes a connection to an Azure DevOps organization using one of several authentication methods: Personal Access Token (PAT), Service Principal, Managed Identity, or Bearer token. The connection details are stored in a script-level variable for use by other cmdlets in the PSRule.Rules.AzureDevOps module.

.PARAMETER Organization
Organization name for Azure DevOps
The name of the Azure DevOps organization to connect to.

.PARAMETER PAT
Personal Access Token (PAT) for Azure DevOps
A Personal Access Token (PAT) used to authenticate to Azure DevOps. Used with the 'Pat' parameter set.

.PARAMETER TenantId
The Microsoft Entra ID tenant ID for Service Principal authentication. Used with the 'ServicePrincipal' parameter set.

.PARAMETER ClientId
Client ID for Service Principal
The client ID of the Service Principal. Used with the 'ServicePrincipal' parameter set.

.PARAMETER ClientSecret
Client Secret for Service Principal

.PARAMETER TenantId
Tenant ID for Service Principal
The client secret of the Service Principal. Used with the 'ServicePrincipal' parameter set.

.PARAMETER AuthType
Authentication type for Azure DevOps (PAT, ServicePrincipal, ManagedIdentity)
.PARAMETER ManagedIdentity
Specifies that a Managed Identity should be used for authentication. Used with the 'ManagedIdentity' parameter set.

.PARAMETER TokenType
Token type for Azure DevOps (FullAccess, FineGrained, ReadOnly)
.PARAMETER AccessToken
A Bearer token (e.g., OAuth 2.0 access token from Microsoft Entra ID) used to authenticate to Azure DevOps. Used with the 'Bearer' parameter set.

.EXAMPLE
Connect-AzDevOps -Organization $Organization -PAT $PAT
Connect-AzDevOps -Organization "MyOrg" -PAT "abc123"
Connects to the "MyOrg" organization using a Personal Access Token.

.EXAMPLE
Connect-AzDevOps -Organization $Organization -ClientId $ClientId -ClientSecret $ClientSecret -TenantId $TenantId -AuthType ServicePrincipal
Connect-AzDevOps -Organization "MyOrg" -AccessToken "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik..."
Connects to the "MyOrg" organization using a Bearer token.

.EXAMPLE
Connect-AzDevOps -Organization $Organization -AuthType ManagedIdentity
Connect-AzDevOps -Organization "MyOrg" -TenantId "00000000-0000-0000-0000-000000000000" -ClientId "11111111-1111-1111-1111-111111111111" -ClientSecret "secret"
Connects to the "MyOrg" organization using a Service Principal.

.EXAMPLE
Connect-AzDevOps -Organization $Organization -PAT $PAT -AuthType PAT
Connect-AzDevOps -Organization "MyOrg" -ManagedIdentity
Connects to the "MyOrg" organization using a Managed Identity.

#>
Function Connect-AzDevOps {
.NOTES
- The Bearer token must have appropriate permissions for Azure DevOps APIs (e.g., read access to projects and settings).
- Bearer tokens typically expire after 1 hour; re-run Connect-AzDevOps with a new token if expired.
- The connection is stored in a script-level variable and persists for the session.

.LINK
https://docs.microsoft.com/en-us/rest/api/azure/devops/
#>
function Connect-AzDevOps {
[CmdletBinding()]
[OutputType([AzureDevOpsConnection])]
param (
[Parameter(Mandatory=$true)]
[Parameter(Mandatory = $true)]
[string]
$Organization,
[Parameter(ParameterSetName = 'PAT')]

[Parameter(Mandatory = $true, ParameterSetName = 'Pat')]
[string]
$PAT,
[Parameter(ParameterSetName = 'ServicePrincipal', Mandatory=$true)]

[Parameter(Mandatory = $true, ParameterSetName = 'ServicePrincipal')]
[string]
$TenantId,

[Parameter(Mandatory = $true, ParameterSetName = 'ServicePrincipal')]
[string]
$ClientId,
[Parameter(ParameterSetName = 'ServicePrincipal', Mandatory=$true)]

[Parameter(Mandatory = $true, ParameterSetName = 'ServicePrincipal')]
[string]
$ClientSecret,
[Parameter(ParameterSetName = 'ServicePrincipal', Mandatory=$true)]
[string]
$TenantId,
[Parameter()]
[ValidateSet('PAT', 'ServicePrincipal', 'ManagedIdentity')]
[string]
$AuthType = 'PAT',
[Parameter()]
[ValidateSet('FullAccess', 'FineGrained', 'ReadOnly')]

[Parameter(Mandatory = $true, ParameterSetName = 'ManagedIdentity')]
[switch]
$ManagedIdentity,

[Parameter(Mandatory = $true, ParameterSetName = 'Bearer')]
[string]
$TokenType = 'FullAccess'
$AccessToken
)
switch ($AuthType) {
'PAT' {
$connection = [AzureDevOpsConnection]::new($Organization, $PAT, $TokenType)

switch ($PSCmdlet.ParameterSetName) {
'Pat' {
$script:connection = [AzureDevOpsConnection]::new($Organization, $PAT)
}
'ServicePrincipal' {
$connection = [AzureDevOpsConnection]::new($Organization, $ClientId, $ClientSecret, $TenantId, $TokenType)
$script:connection = [AzureDevOpsConnection]::new($Organization, $ClientId, $ClientSecret, $TenantId)
}
'ManagedIdentity' {
$connection = [AzureDevOpsConnection]::new($Organization, $TokenType)
$script:connection = [AzureDevOpsConnection]::new($Organization)
}
'Bearer' {
$script:connection = [AzureDevOpsConnection]::new($Organization, $AccessToken, 'FullAccess', $true)
}
}
$script:connection = $connection

# Verify connection with a simple API call
try {
$uri = "https://dev.azure.com/$Organization/_apis/projects?api-version=7.0"
Invoke-RestMethod -Uri $uri -Method Get -Headers $script:connection.GetHeader() | Out-Null
Write-Verbose "Successfully connected to Azure DevOps organization: $Organization"
}
catch {
throw "Failed to connect to Azure DevOps: $($_.Exception.Message)"
}
}

# End of Function Connect-AzDevOps
Export-ModuleMember -Function Connect-AzDevOps

<#
.SYNOPSIS
Expand Down
Loading