diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index bcf48b7..ccdbe32 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -27,11 +27,21 @@ runs: shell: pwsh run: ./build.ps1 -ResolveDependency -Task noop + # Replace dot in semVer with dash, for Sampler validation in pre-release + - name: Format semVer for Sampler + id: formatSemVer + shell: pwsh + run: | + $SemVer = '${{ steps.gitversion.outputs.semVer }}' + # Replace last dot with dash for Sampler to accept it as pre-release, does not allow dots in pre-release name + $SemVer = $SemVer -replace '^([\d\.]+\-\w+)\.(\d+)$','$1-$2' + Add-Content -Path $env:GITHUB_OUTPUT -Value "formattedSemVer=$SemVer" + - name: Build module shell: pwsh run: ./build.ps1 -tasks pack env: - ModuleVersion: ${{ env.gitVersion.NuGetVersionV2 }} + ModuleVersion: ${{ steps.formatSemVer.outputs.formattedSemVer }} - name: Publish build artifacts uses: actions/upload-artifact@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 675aea4..9ac3ac3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ The format is based on and uses the types of changes according to [Keep a Change ### Added - Adds related links links to blog posts for Get-AzToken and the parameters -WorkloadIdentity & -ExternalToken +- Added `-TimeoutSeconds` parameter for Managed Identity authentication and non-interactive authentication +- Added Managed Identity authentication as first option of non-interactive login ## [2.2.10] - 2024-05-22 diff --git a/GitVersion.yml b/GitVersion.yml index 4497177..38e6df5 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -1,29 +1,116 @@ -strategies: - - Mainline -next-version: 0.0.1 -major-version-bump-message: '(breaking\schange|breaking|major)\b' -minor-version-bump-message: '(adds?|features?|minor)\b' -patch-version-bump-message: '\s?(fix|patch)' +assembly-versioning-scheme: MajorMinorPatch +assembly-file-versioning-scheme: MajorMinorPatch +tag-prefix: '[vV]?' +version-in-branch-pattern: (?[vV]?\d+(\.\d+)?(\.\d+)?).* +major-version-bump-message: '\+semver:\s?(breaking|major)' +minor-version-bump-message: '\+semver:\s?(feature|minor)' +patch-version-bump-message: '\+semver:\s?(fix|patch)' no-bump-message: '\+semver:\s?(none|skip)' -assembly-informational-format: 'MajorMinorPatch' +tag-pre-release-weight: 60000 +commit-date-format: yyyy-MM-dd +merge-message-formats: {} +update-build-number: true +semantic-version-format: Strict +strategies: +- Fallback +- ConfiguredNextVersion +- MergeMessage +- TaggedCommit +- TrackReleaseBranches +- VersionInBranchName branches: main: - label: preview - regex: ^main$ + label: 'preview' increment: Patch - pull-request: - label: PR - feature: - label: useBranchName - increment: Minor - regex: f(eature(s)?)?[\/-] - source-branches: ['main'] - hotfix: - label: fix + prevent-increment: + of-merged-branch: true + track-merge-target: false + track-merge-message: true + regex: ^master$|^main$ + source-branches: [] + is-source-branch-for: [] + tracks-release-branches: false + is-release-branch: false + is-main-branch: true + pre-release-weight: 55000 + release: + mode: ManualDeployment + label: beta increment: Patch - regex: (hot)?fix(es)?[\/-] - source-branches: ['main'] - + prevent-increment: + of-merged-branch: true + when-branch-merged: false + when-current-commit-tagged: false + track-merge-target: false + track-merge-message: true + regex: ^releases?[/-](?.+) + source-branches: + - main + is-source-branch-for: [] + tracks-release-branches: false + is-release-branch: true + is-main-branch: false + pre-release-weight: 30000 + feature: + mode: ManualDeployment + label: '{BranchName}' + increment: Inherit + prevent-increment: + when-current-commit-tagged: false + track-merge-message: true + regex: ^features?[/-](?.+) + source-branches: + - main + - release + is-source-branch-for: [] + is-main-branch: false + pre-release-weight: 30000 + pull-request: + mode: ContinuousDelivery + label: PullRequest + increment: Inherit + prevent-increment: + of-merged-branch: true + when-current-commit-tagged: false + label-number-pattern: '[/-](?\d+)' + track-merge-message: true + regex: ^(pull|pull\-requests|pr)[/-] + source-branches: + - main + - release + - feature + is-source-branch-for: [] + pre-release-weight: 30000 + unknown: + mode: ManualDeployment + label: '{BranchName}' + increment: Inherit + prevent-increment: + when-current-commit-tagged: false + track-merge-message: false + regex: (?.+) + source-branches: + - main + - release + - feature + - pull-request + is-source-branch-for: [] + is-main-branch: false ignore: sha: [] -merge-message-formats: {} \ No newline at end of file +mode: ContinuousDelivery +label: '{BranchName}' +increment: Inherit +prevent-increment: + of-merged-branch: false + when-branch-merged: false + when-current-commit-tagged: true +track-merge-target: false +track-merge-message: true +commit-message-incrementing: Enabled +regex: '' +source-branches: [] +is-source-branch-for: [] +tracks-release-branches: false +is-release-branch: false +is-main-branch: false \ No newline at end of file diff --git a/docs/help/Get-AzToken.md b/docs/help/Get-AzToken.md index 439f736..80217d1 100644 --- a/docs/help/Get-AzToken.md +++ b/docs/help/Get-AzToken.md @@ -15,8 +15,8 @@ Gets a new Azure access token. ### NonInteractive (Default) ``` -Get-AzToken [[-Resource] ] [[-Scope] ] [-TenantId ] [-Claim ] [-Force] - [] +Get-AzToken [[-Resource] ] [[-Scope] ] [-TenantId ] [-Claim ] + [-TimeoutSeconds ] [-Force] [] ``` ### Cache @@ -43,7 +43,8 @@ Get-AzToken [[-Resource] ] [[-Scope] ] [-TenantId ] [- ### ManagedIdentity ``` Get-AzToken [[-Resource] ] [[-Scope] ] [-TenantId ] [-Claim ] - [-ClientId ] [-ManagedIdentity] [-Force] [] + [-ClientId ] [-TimeoutSeconds ] [-ManagedIdentity] [-Force] + [] ``` ### WorkloadIdentity @@ -409,7 +410,7 @@ The number of seconds to wait until the login times out. ```yaml Type: Int32 -Parameter Sets: Interactive, DeviceCode +Parameter Sets: NonInteractive, Interactive, DeviceCode, ManagedIdentity Aliases: Required: False diff --git a/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.ManagedIdentity.cs b/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.ManagedIdentity.cs index 27e7304..22343cf 100644 --- a/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.ManagedIdentity.cs +++ b/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.ManagedIdentity.cs @@ -8,8 +8,8 @@ internal static partial class TokenManager /// /// Gets token as a managed identity. /// - internal static AzToken GetTokenManagedIdentity(string resource, string[] scopes, string? claims, string? clientId, string? tenantId, CancellationToken cancellationToken) => - taskFactory.Run(() => GetTokenManagedIdentityAsync(resource, scopes, claims, clientId, tenantId, cancellationToken)); + internal static AzToken GetTokenManagedIdentity(string resource, string[] scopes, string? claims, string? clientId, string? tenantId, int timeoutSeconds, CancellationToken cancellationToken) => + taskFactory.Run(() => GetTokenManagedIdentityAsync(resource, scopes, claims, clientId, tenantId, timeoutSeconds, cancellationToken)); /// /// Gets token as a managed identity. @@ -20,6 +20,7 @@ internal static async Task GetTokenManagedIdentityAsync( string? claims, string? clientId, string? tenantId, + int timeoutSeconds, CancellationToken cancellationToken) { var fullScopes = scopes.Select(s => $"{resource.TrimEnd('/')}/{s}").ToArray(); @@ -28,7 +29,14 @@ internal static async Task GetTokenManagedIdentityAsync( // Re-use the previous managed identity credential if client id didn't change if (credential is not ManagedIdentityCredential || previousClientId != clientId) { - credential = new ManagedIdentityCredential(clientId); + credential = new ManagedIdentityCredential(clientId, options: new TokenCredentialOptions{ + Retry = { + NetworkTimeout = TimeSpan.FromSeconds(timeoutSeconds), + MaxRetries = 0, + Delay = TimeSpan.Zero, + MaxDelay = TimeSpan.Zero + } + }); } previousClientId = clientId; diff --git a/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.NonInteractive.cs b/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.NonInteractive.cs index eaf57ee..3825f4c 100644 --- a/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.NonInteractive.cs +++ b/source/AzAuth.Core/TokenManagerAuthMethods/TokenManager.NonInteractive.cs @@ -1,5 +1,8 @@ using Azure.Core; +using Azure.Core.Diagnostics; +using Azure.Core.Pipeline; using Azure.Identity; +using System.Diagnostics.Tracing; using System.Text.RegularExpressions; namespace PipeHow.AzAuth; @@ -9,8 +12,8 @@ internal static partial class TokenManager /// /// Gets token noninteractively. /// - internal static AzToken GetTokenNonInteractive(string resource, string[] scopes, string? claims, string? tenantId, CancellationToken cancellationToken) => - taskFactory.Run(() => GetTokenNonInteractiveAsync(resource, scopes, claims, tenantId, cancellationToken)); + internal static AzToken GetTokenNonInteractive(string resource, string[] scopes, string? claims, string? tenantId, int? timeoutSeconds, int managedIdentityTimeoutSeconds, CancellationToken cancellationToken) => + taskFactory.Run(() => GetTokenNonInteractiveAsync(resource, scopes, claims, tenantId, timeoutSeconds, managedIdentityTimeoutSeconds, cancellationToken)); /// /// Gets token noninteractively. @@ -20,19 +23,42 @@ internal static async Task GetTokenNonInteractiveAsync( string[] scopes, string? claims, string? tenantId, + int? timeoutSeconds, + int managedIdentityTimeoutSeconds, CancellationToken cancellationToken) { var fullScopes = scopes.Select(s => $"{resource.TrimEnd('/')}/{s}").ToArray(); + // If timeoutSeconds is not null, create a new TokenCredentialOptions with the specified timeout + // Otherwise, set to null to use default timeout + TokenCredentialOptions? genericTimeoutOptions = timeoutSeconds.HasValue ? new TokenCredentialOptions + { + Retry = { + NetworkTimeout = TimeSpan.FromSeconds(timeoutSeconds.Value), + MaxRetries = 0, + Delay = TimeSpan.Zero, + MaxDelay = TimeSpan.Zero + } + } : null; + // Create our own credential chain because we want to change the order var sources = new List() { - new EnvironmentCredential(), - new AzurePowerShellCredential(), - new AzureCliCredential(), - new VisualStudioCodeCredential(), - new VisualStudioCredential(), - new SharedTokenCacheCredential() + // ManagedIdentityCredential with custom timeout + new ManagedIdentityCredential(options: new TokenCredentialOptions{ + Retry = { + NetworkTimeout = TimeSpan.FromSeconds(managedIdentityTimeoutSeconds), + MaxRetries = 0, + Delay = TimeSpan.Zero, + MaxDelay = TimeSpan.Zero + } + }), + new EnvironmentCredential(genericTimeoutOptions), + new AzurePowerShellCredential(genericTimeoutOptions as AzurePowerShellCredentialOptions), + new AzureCliCredential(genericTimeoutOptions as AzureCliCredentialOptions), + new VisualStudioCodeCredential(genericTimeoutOptions as VisualStudioCodeCredentialOptions), + new VisualStudioCredential(genericTimeoutOptions as VisualStudioCredentialOptions), + new SharedTokenCacheCredential(genericTimeoutOptions as SharedTokenCacheCredentialOptions) }; // If user authenticated interactively in the same session and tenant didn't change, add it as the first option to find tokens from diff --git a/source/AzAuth.PS/Cmdlets/GetAzToken.cs b/source/AzAuth.PS/Cmdlets/GetAzToken.cs index eb80785..af6e945 100644 --- a/source/AzAuth.PS/Cmdlets/GetAzToken.cs +++ b/source/AzAuth.PS/Cmdlets/GetAzToken.cs @@ -79,7 +79,9 @@ public class GetAzToken : PSLoggerCmdletBase [ArgumentCompleter(typeof(ExistingAccounts))] public string Username { get; set; } + [Parameter(ParameterSetName = "NonInteractive")] [Parameter(ParameterSetName = "Interactive")] + [Parameter(ParameterSetName = "ManagedIdentity")] [Parameter(ParameterSetName = "DeviceCode")] [ValidateRange(1, int.MaxValue)] public int TimeoutSeconds { get; set; } = 120; @@ -146,7 +148,17 @@ protected override void EndProcessing() if (ParameterSetName == "NonInteractive") { + // If user didn't specify a timeout, default to 1 second for managed identity + int managedIdentityTimeoutSeconds = 1; + int? noninteractiveTimeoutSeconds = null; + if (MyInvocation.BoundParameters.ContainsKey("TimeoutSeconds")) + { + managedIdentityTimeoutSeconds = TimeoutSeconds; + noninteractiveTimeoutSeconds = TimeoutSeconds; + } + WriteVerbose(@"Looking for a token from the following sources: +Managed Identity (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.managedidentitycredential) Environment variables (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.environmentcredential) Azure PowerShell (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.azurepowershellcredential) Azure CLI (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.azureclicredential) @@ -154,7 +166,7 @@ Visual Studio Code (https://learn.microsoft.com/en-us/dotnet/api/azure.identity. Visual Studio (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.visualstudiocredential) Shared token cache (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.sharedtokencachecredential) "); - WriteObject(TokenManager.GetTokenNonInteractive(Resource, Scope, Claim, TenantId, stopProcessing.Token)); + WriteObject(TokenManager.GetTokenNonInteractive(Resource, Scope, Claim, TenantId, noninteractiveTimeoutSeconds, managedIdentityTimeoutSeconds, stopProcessing.Token)); } else if (ParameterSetName == "Cache") { @@ -190,8 +202,13 @@ Shared token cache (https://learn.microsoft.com/en-us/dotnet/api/azure.identity. } else if (ManagedIdentity.IsPresent) { + // If user didn't specify a timeout, default to 1 second for managed identity + if (!MyInvocation.BoundParameters.ContainsKey("TimeoutSeconds")) + { + TimeoutSeconds = 1; + } WriteVerbose("Getting token using a managed identity (https://learn.microsoft.com/en-us/dotnet/api/azure.identity.managedidentitycredential)."); - WriteObject(TokenManager.GetTokenManagedIdentity(Resource, Scope, Claim, ClientId, TenantId, stopProcessing.Token)); + WriteObject(TokenManager.GetTokenManagedIdentity(Resource, Scope, Claim, ClientId, TenantId, TimeoutSeconds, stopProcessing.Token)); } else if (WorkloadIdentity.IsPresent) { diff --git a/tests/Get-AzToken.Tests.ps1 b/tests/Get-AzToken.Tests.ps1 index 7475a88..ffc17ba 100644 --- a/tests/Get-AzToken.Tests.ps1 +++ b/tests/Get-AzToken.Tests.ps1 @@ -94,7 +94,9 @@ BeforeDiscovery { Name = 'TimeoutSeconds' Type = 'int' ParameterSets = @( + @{ Name = 'NonInteractive'; Mandatory = $false } @{ Name = 'Interactive'; Mandatory = $false } + @{ Name = 'ManagedIdentity'; Mandatory = $false } @{ Name = 'DeviceCode'; Mandatory = $false } ) }