-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
BREAKING CHANGE - CORE 'Add Azure DevOps Managed Identity Support' (#2)
* Adding Initial Class * Creating ManagedIdentityToken class. Expanding ManagedIdentityClass Updated Invoke-AZDevOpsAPIRestMethod with ManagedIdentity Handeler * Refactoring 004.AzManagedIdentity into Functions Added APIRateLimit Enum * Renaming DataResources Updating AzureDevOpsDsc.Common.psd1 Initial ompleted Managed Identity Cmdlets. * Adding Initial Tests * Fixing Documentation Adding Unit Tests * Moving Tests Completed ManagedIdentityToken Test * Completed APIRateLimit Unit Testing * Bug Fixes on Azure VM to get Managed Identity Working * Updating Tests * Fixing Tests * Test Updates * Updating Paths * Update Paths * Bug fixes with build and testing process * Fixing Headers * Bug Fixes with the Token * Bug Fixes * More bug fixes * Fixing Bugs * Updated Documentation and ChangeLog to bring into line with guidelines. --------- Co-authored-by: Michael Zanatta <[email protected]>
- Loading branch information
1 parent
dbc4607
commit afecb3e
Showing
25 changed files
with
1,098 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
data AzManagedIdentityLocalizedData { | ||
|
||
ConvertFrom-StringData @' | ||
Global_AzureDevOps_Resource_Id=499b84ac-1321-427f-aa17-267ca6975798 | ||
Global_Url_Azure_Instance_Metadata_Url=http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource={0} | ||
Global_API_Azure_DevOps_Version=api-version=6.0 | ||
Global_Url_AZDO_Project=https://dev.azure.com/{0}/_apis/projects | ||
Error_ManagedIdentity_RestApiCallFailed=Error. Failed to call the Azure Instance Metadata Service. Please ensure that the Azure Instance Metadata Service is available. Error Details: {0} | ||
Error_Azure_Instance_Metadata_Service_Missing_Token=Error. Access token not returned from Azure Instance Metadata Service. Please ensure that the Azure Instance Metadata Service is available. | ||
Error_Azure_API_Call_Generic=Error. Failed to call the Azure DevOps API. Details: {0} | ||
Error_Azure_Get_AzManagedIdentity_Invalid_Caller=Error. Get-AzManagedIdentity can only be called from New-AzManagedIdentity or Update-AzManagedIdentity. | ||
'@ | ||
|
||
} | ||
|
||
New-Variable -Name AzManagedIdentityLocalizedData -Value $AzManagedIdentityLocalizedData -Option ReadOnly -Scope Global -Force |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
|
||
|
||
Class ManagedIdentityToken { | ||
|
||
[SecureString]$access_token | ||
[DateTime]$expires_on | ||
[Int]$expires_in | ||
[String]$resource | ||
[String]$token_type | ||
hidden [bool]$linux = $IsLinux | ||
|
||
# Constructor | ||
ManagedIdentityToken([PSCustomObject]$ManagedIdentityTokenObj) { | ||
|
||
# Validate that ManagedIdentityTokenObj is a HashTable and Contains the correct keys | ||
if (-not $this.isValid($ManagedIdentityTokenObj)) { throw "The ManagedIdentityTokenObj is not valid." } | ||
|
||
$epochStart = [datetime]::new(1970, 1, 1, 0, 0, 0, [DateTimeKind]::Utc) | ||
|
||
# Set the properties of the class | ||
$this.access_token = $ManagedIdentityTokenObj.access_token | ConvertTo-SecureString -AsPlainText -Force | ||
$this.expires_on = $epochStart.AddSeconds($ManagedIdentityTokenObj.expires_on) | ||
$this.expires_in = $ManagedIdentityTokenObj.expires_in | ||
$this.resource = $ManagedIdentityTokenObj.resource | ||
$this.token_type = $ManagedIdentityTokenObj.token_type | ||
|
||
} | ||
|
||
# Function to validate the ManagedIdentityTokenObj | ||
Hidden [Bool]isValid($ManagedIdentityTokenObj) { | ||
|
||
# Assuming these are the keys we expect in the hashtable | ||
$expectedKeys = @('access_token', 'expires_on', 'expires_in', 'resource', 'token_type') | ||
|
||
# Check if all expected keys exist in the hashtable | ||
foreach ($key in $expectedKeys) { | ||
if (-not $ManagedIdentityTokenObj."$key") { | ||
Write-Verbose "[ManagedIdentityToken] The hashtable does not contain the expected property: $key" | ||
return $false | ||
} | ||
} | ||
|
||
# If all checks pass, return true | ||
Write-Verbose "[ManagedIdentityToken] The hashtable is valid and contains all the expected keys." | ||
return $true | ||
} | ||
|
||
# Function to convert a SecureString to a String | ||
hidden [String]ConvertFromSecureString([SecureString]$SecureString) { | ||
# Convert a SecureString to a String | ||
$BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString) | ||
$String = ($this.linux) ? [System.Runtime.InteropServices.Marshal]::PtrToStringUni($BSTR) : | ||
[System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) | ||
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR) | ||
return $String | ||
} | ||
|
||
# Function to test the call stack | ||
hidden [Bool]TestCallStack([String]$name) { | ||
|
||
$CallStack = Get-PSCallStack | ||
|
||
# Check if any of the callers in the call stack is Invoke-DSCResource | ||
foreach ($stackFrame in $callStack) { | ||
if ($stackFrame.Command -eq $name) { | ||
Write-Verbose "[ManagedIdentityToken] The calling function is $name." | ||
return $true | ||
} | ||
} | ||
|
||
return $false | ||
|
||
} | ||
|
||
[Bool]isExpired() { | ||
# Remove 10 seconds from the expires_on time to account for clock skew. | ||
if ($this.expires_on.AddSeconds(-10) -lt (Get-Date)) { return $true } | ||
return $false | ||
} | ||
|
||
# Return the access token | ||
[String] Get() { | ||
|
||
# Prevent Execution and Writing to Files and Pipeline Variables. | ||
|
||
# Token can only be called within Invoke-AzDevOpsApiRestMethod. Test to see if the calling function is Invoke-AzDevOpsApiRestMethod | ||
if (-not($this.TestCallStack('Invoke-AzDevOpsApiRestMethod'))) { throw "[ManagedIdentityToken] The Get() method can only be called within Invoke-AzDevOpsApiRestMethod." } | ||
# Token cannot be returned within a Write-* function. Test to see if the calling function is Write-* | ||
if ($this.TestCallStack('Write-')) { throw "[ManagedIdentityToken] The Get() method cannot be called within a Write-* function." } | ||
# Token cannot be written to a file. Test to see if the calling function is Out-File | ||
if ($this.TestCallStack('Out-File')) { throw "[ManagedIdentityToken] The Get() method cannot be called within Out-File." } | ||
|
||
# Return the access token | ||
return ($this.ConvertFromSecureString($this.access_token)) | ||
|
||
} | ||
|
||
} | ||
|
||
# Function to create a new ManagedIdentityToken object | ||
Function global:New-ManagedIdentityToken ([hashtable]$ManagedIdentityTokenObj) { | ||
|
||
# Create and return a new ManagedIdentityToken object | ||
return [ManagedIdentityToken]::New($ManagedIdentityTokenObj) | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
Class APIRateLimit { | ||
|
||
[Int]$retryAfter = 0 | ||
[Int]$xRateLimitRemaining = 0 | ||
[Int]$xRateLimitReset = 0 | ||
|
||
# Constructor | ||
APIRateLimit([HashTable]$APIRateLimitObj) { | ||
|
||
# Validate that APIRateLimitObj is a HashTable and Contains the correct keys | ||
if (-not $this.isValid($APIRateLimitObj)) { throw "The APIRateLimitObj is not valid." } | ||
|
||
# Convert X-RateLimit-Reset from Unix Time to DateTime | ||
$epochStart = [datetime]::new(1970, 1, 1, 0, 0, 0, [DateTimeKind]::Utc) | ||
|
||
# Set the properties of the class | ||
$this.retryAfter = [Int]($APIRateLimitObj.'Retry-After') | ||
$this.XRateLimitRemaining = [Int]$APIRateLimitObj.'X-RateLimit-Remaining' | ||
$this.XRateLimitReset = [Int]($APIRateLimitObj.'X-RateLimit-Reset') | ||
|
||
} | ||
|
||
# Constructor with retryAfter Parameters | ||
APIRateLimit($retryAfter) { | ||
|
||
# Set the properties of the class | ||
$this.retryAfter = [int]$retryAfter | ||
|
||
} | ||
|
||
Hidden [Bool]isValid($APIRateLimitObj) { | ||
|
||
# Assuming these are the keys we expect in the hashtable | ||
$expectedKeys = @('Retry-After', 'X-RateLimit-Remaining', 'X-RateLimit-Reset') | ||
|
||
# Check if all expected keys exist in the hashtable | ||
foreach ($key in $expectedKeys) { | ||
if (-not $APIRateLimitObj.ContainsKey($key)) { | ||
Write-Error "[APIRateLimit] The hashtable does not contain the expected key: $key" | ||
return $false | ||
} | ||
} | ||
|
||
# If all checks pass, return true | ||
Write-Verbose "[APIRateLimit] The hashtable is valid and contains all the expected keys." | ||
return $true | ||
|
||
} | ||
|
||
|
||
} |
43 changes: 43 additions & 0 deletions
43
source/Examples/Resources/AzDevOpsProject/4-AddProject-UsingManaged-Identity.ps1
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
|
||
<# | ||
.DESCRIPTION | ||
This example shows how to ensure that the Azure DevOps project | ||
called 'Test Project' exists (or is added if it does not exist). | ||
This example uses Invoke-DSCResource to authenticate to Azure DevOps using a Managed Identity. | ||
#> | ||
|
||
Configuration Example | ||
{ | ||
param | ||
( | ||
[Parameter(Mandatory = $true)] | ||
[string] | ||
$ApiUri | ||
) | ||
|
||
Import-DscResource -ModuleName 'AzureDevOpsDsc' | ||
|
||
node localhost | ||
{ | ||
AzDevOpsProject 'AddProject' | ||
{ | ||
Ensure = 'Present' | ||
|
||
ApiUri = $ApiUri | ||
Pat = $Pat | ||
|
||
ProjectName = 'Test Project' | ||
ProjectDescription = 'A Test Project' | ||
|
||
SourceControlType = 'Git' | ||
} | ||
|
||
} | ||
} | ||
|
||
# Create a new Azure Managed Identity and store the token in a global variable | ||
New-AzManagedIdentity -OrganizationName "Contoso" | ||
|
||
# Using Invoke-DSCResource, invoke the 'Test' method of the 'AzDevOpsProject' resource. | ||
# The global variable will be used to authenticate to Azure DevOps. | ||
Invoke-DscResource -Name Example -Method Test -Verbose |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
65 changes: 65 additions & 0 deletions
65
source/Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Get-AzManagedIdentityToken.ps1
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
<# | ||
.SYNOPSIS | ||
Obtains a managed identity token from Azure AD. | ||
.DESCRIPTION | ||
The Get-AzManagedIdentityToken function is used to obtain an access token from Azure AD using a managed identity. It can only be called from the New-AzManagedIdentity or Update-AzManagedIdentity functions. | ||
.PARAMETER OrganizationName | ||
Specifies the name of the organization. | ||
.PARAMETER Verify | ||
Specifies whether to verify the connection. If this switch is not set, the function returns the managed identity token. If the switch is set, the function tests the connection and returns the access token. | ||
.EXAMPLE | ||
Get-AzManagedIdentityToken -OrganizationName "Contoso" -Verify | ||
Obtains the access token for the managed identity associated with the organization "Contoso" and verifies the connection. | ||
.NOTES | ||
This function does not require the Azure PowerShell module. | ||
#> | ||
|
||
Function Get-AzManagedIdentityToken { | ||
[CmdletBinding()] | ||
param ( | ||
# Organization Name | ||
[Parameter(Mandatory)] | ||
[String] | ||
$OrganizationName, | ||
|
||
# Verify the Connection | ||
[Parameter()] | ||
[Switch] | ||
$Verify | ||
) | ||
|
||
# Obtain the access token from Azure AD using the Managed Identity | ||
|
||
$ManagedIdentityParams = @{ | ||
# Define the Azure instance metadata endpoint to get the access token | ||
Uri = $AzManagedIdentityLocalizedData.Global_Url_Azure_Instance_Metadata_Url -f $AzManagedIdentityLocalizedData.Global_AzureDevOps_Resource_Id | ||
Method = 'Get' | ||
Headers = @{Metadata="true"} | ||
ContentType = 'Application/json' | ||
} | ||
|
||
# Invoke the RestAPI | ||
try { $response = Invoke-AzDevOpsApiRestMethod @ManagedIdentityParams } catch { Throw $_ } | ||
# Test the response | ||
if ($null -eq $response.access_token) { throw $AzManagedIdentityLocalizedData.Error_Azure_Instance_Metadata_Service_Missing_Token } | ||
|
||
# TypeCast the response to a ManagedIdentityToken object | ||
$ManagedIdentity = New-ManagedIdentityToken -ManagedIdentityTokenObj $response | ||
# Null the response | ||
$null = $response | ||
|
||
# Return the token if the verify switch is not set | ||
if (-not($verify)) { return $ManagedIdentity } | ||
|
||
# Test the Connection | ||
if (-not(Test-AzManagedIdentityToken $ManagedIdentity)) { throw $AzManagedIdentityLocalizedData.Error_Azure_API_Call_Generic } | ||
|
||
# Return the AccessToken | ||
return ($ManagedIdentity) | ||
|
||
} |
Oops, something went wrong.