Skip to content
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

Add short lived memory cache for DefaultAzureCredential (#15053) #15105

Merged
Merged
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
50 changes: 50 additions & 0 deletions Arcade.sln
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Tar", "src
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.DotNet.Internal.SymbolHelper", "src\Microsoft.DotNet.Internal.SymbolHelper\Microsoft.DotNet.Internal.SymbolHelper.csproj", "{17C9E506-7A74-46B7-A345-ABB360E7390E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.ArcadeAzureIntegration", "src\Microsoft.DotNet.ArcadeAzureIntegration\Microsoft.DotNet.ArcadeAzureIntegration.csproj", "{CA159C84-CD7D-4364-9121-3842F97D4B60}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -979,6 +981,54 @@ Global
{17C9E506-7A74-46B7-A345-ABB360E7390E}.Release|x64.Build.0 = Release|Any CPU
{17C9E506-7A74-46B7-A345-ABB360E7390E}.Release|x86.ActiveCfg = Release|Any CPU
{17C9E506-7A74-46B7-A345-ABB360E7390E}.Release|x86.Build.0 = Release|Any CPU
{19A9472B-3984-4FF1-B0BF-28A50E69A240}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{19A9472B-3984-4FF1-B0BF-28A50E69A240}.Debug|Any CPU.Build.0 = Debug|Any CPU
{19A9472B-3984-4FF1-B0BF-28A50E69A240}.Debug|x64.ActiveCfg = Debug|Any CPU
{19A9472B-3984-4FF1-B0BF-28A50E69A240}.Debug|x64.Build.0 = Debug|Any CPU
{19A9472B-3984-4FF1-B0BF-28A50E69A240}.Debug|x86.ActiveCfg = Debug|Any CPU
{19A9472B-3984-4FF1-B0BF-28A50E69A240}.Debug|x86.Build.0 = Debug|Any CPU
{19A9472B-3984-4FF1-B0BF-28A50E69A240}.Release|Any CPU.ActiveCfg = Release|Any CPU
{19A9472B-3984-4FF1-B0BF-28A50E69A240}.Release|Any CPU.Build.0 = Release|Any CPU
{19A9472B-3984-4FF1-B0BF-28A50E69A240}.Release|x64.ActiveCfg = Release|Any CPU
{19A9472B-3984-4FF1-B0BF-28A50E69A240}.Release|x64.Build.0 = Release|Any CPU
{19A9472B-3984-4FF1-B0BF-28A50E69A240}.Release|x86.ActiveCfg = Release|Any CPU
{19A9472B-3984-4FF1-B0BF-28A50E69A240}.Release|x86.Build.0 = Release|Any CPU
{6BA81447-C61D-4F91-BF0F-5B17AF4CFFAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6BA81447-C61D-4F91-BF0F-5B17AF4CFFAC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6BA81447-C61D-4F91-BF0F-5B17AF4CFFAC}.Debug|x64.ActiveCfg = Debug|Any CPU
{6BA81447-C61D-4F91-BF0F-5B17AF4CFFAC}.Debug|x64.Build.0 = Debug|Any CPU
{6BA81447-C61D-4F91-BF0F-5B17AF4CFFAC}.Debug|x86.ActiveCfg = Debug|Any CPU
{6BA81447-C61D-4F91-BF0F-5B17AF4CFFAC}.Debug|x86.Build.0 = Debug|Any CPU
{6BA81447-C61D-4F91-BF0F-5B17AF4CFFAC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6BA81447-C61D-4F91-BF0F-5B17AF4CFFAC}.Release|Any CPU.Build.0 = Release|Any CPU
{6BA81447-C61D-4F91-BF0F-5B17AF4CFFAC}.Release|x64.ActiveCfg = Release|Any CPU
{6BA81447-C61D-4F91-BF0F-5B17AF4CFFAC}.Release|x64.Build.0 = Release|Any CPU
{6BA81447-C61D-4F91-BF0F-5B17AF4CFFAC}.Release|x86.ActiveCfg = Release|Any CPU
{6BA81447-C61D-4F91-BF0F-5B17AF4CFFAC}.Release|x86.Build.0 = Release|Any CPU
{D2B5D28E-CC80-4468-A037-05C2385FA4C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D2B5D28E-CC80-4468-A037-05C2385FA4C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D2B5D28E-CC80-4468-A037-05C2385FA4C4}.Debug|x64.ActiveCfg = Debug|Any CPU
{D2B5D28E-CC80-4468-A037-05C2385FA4C4}.Debug|x64.Build.0 = Debug|Any CPU
{D2B5D28E-CC80-4468-A037-05C2385FA4C4}.Debug|x86.ActiveCfg = Debug|Any CPU
{D2B5D28E-CC80-4468-A037-05C2385FA4C4}.Debug|x86.Build.0 = Debug|Any CPU
{D2B5D28E-CC80-4468-A037-05C2385FA4C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D2B5D28E-CC80-4468-A037-05C2385FA4C4}.Release|Any CPU.Build.0 = Release|Any CPU
{D2B5D28E-CC80-4468-A037-05C2385FA4C4}.Release|x64.ActiveCfg = Release|Any CPU
{D2B5D28E-CC80-4468-A037-05C2385FA4C4}.Release|x64.Build.0 = Release|Any CPU
{D2B5D28E-CC80-4468-A037-05C2385FA4C4}.Release|x86.ActiveCfg = Release|Any CPU
{D2B5D28E-CC80-4468-A037-05C2385FA4C4}.Release|x86.Build.0 = Release|Any CPU
{CA159C84-CD7D-4364-9121-3842F97D4B60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CA159C84-CD7D-4364-9121-3842F97D4B60}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CA159C84-CD7D-4364-9121-3842F97D4B60}.Debug|x64.ActiveCfg = Debug|Any CPU
{CA159C84-CD7D-4364-9121-3842F97D4B60}.Debug|x64.Build.0 = Debug|Any CPU
{CA159C84-CD7D-4364-9121-3842F97D4B60}.Debug|x86.ActiveCfg = Debug|Any CPU
{CA159C84-CD7D-4364-9121-3842F97D4B60}.Debug|x86.Build.0 = Debug|Any CPU
{CA159C84-CD7D-4364-9121-3842F97D4B60}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CA159C84-CD7D-4364-9121-3842F97D4B60}.Release|Any CPU.Build.0 = Release|Any CPU
{CA159C84-CD7D-4364-9121-3842F97D4B60}.Release|x64.ActiveCfg = Release|Any CPU
{CA159C84-CD7D-4364-9121-3842F97D4B60}.Release|x64.Build.0 = Release|Any CPU
{CA159C84-CD7D-4364-9121-3842F97D4B60}.Release|x86.ActiveCfg = Release|Any CPU
{CA159C84-CD7D-4364-9121-3842F97D4B60}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
6 changes: 3 additions & 3 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,9 @@
<!-- xliff-tasks -->
<MicrosoftDotNetXliffTasksVersion>1.0.0-beta.23475.1</MicrosoftDotNetXliffTasksVersion>
<!-- external -->
<AzureCoreVersion>1.34.0</AzureCoreVersion>
<AzureIdentityVersion>1.11.4</AzureIdentityVersion>
<AzureStorageBlobsVersion>12.16.0</AzureStorageBlobsVersion>
<AzureCoreVersion>1.42.0</AzureCoreVersion>
<AzureIdentityVersion>1.12.0</AzureIdentityVersion>
<AzureStorageBlobsVersion>12.19.1</AzureStorageBlobsVersion>
<CommandLineParserVersion>2.5.0</CommandLineParserVersion>
<CoverletCollectorVersion>1.0.1</CoverletCollectorVersion>
<FluentAssertionsVersion>5.10.3</FluentAssertionsVersion>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#if !NET472_OR_GREATER

#nullable enable

using System;
using System.IO;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Azure.Core;
using Azure.Identity;

namespace Microsoft.DotNet.ArcadeAzureIntegration;


// This class is an workaround for disable az cli auto update mechanism which cause timeout when waiting for
// console input in case of new version of az available
// - this wrppper will run "az config set auto-upgrade.enable=no" only once before first call to az for acquiring the token
public class AzureCliCredentialWithAzNoUpdateWrapper : TokenCredential
{
private readonly AzureCliCredential _azureCliCredential;

public AzureCliCredentialWithAzNoUpdateWrapper(AzureCliCredential azureCliCredential)
{
_azureCliCredential = azureCliCredential;
}

public static string? EnvProgramFilesX86 => GetNonEmptyStringOrNull(Environment.GetEnvironmentVariable("ProgramFiles(x86)"));
public static string? EnvProgramFiles => GetNonEmptyStringOrNull(Environment.GetEnvironmentVariable("ProgramFiles"));
public static string? EnvPath => GetNonEmptyStringOrNull(Environment.GetEnvironmentVariable("PATH"));

private static readonly string DefaultPathWindows = $"{EnvProgramFilesX86}\\Microsoft SDKs\\Azure\\CLI2\\wbin;{EnvProgramFiles}\\Microsoft SDKs\\Azure\\CLI2\\wbin";
private static readonly string DefaultWorkingDirWindows = Environment.GetFolderPath(Environment.SpecialFolder.System);
private const string DefaultPathNonWindows = "/usr/bin:/usr/local/bin";
private const string DefaultWorkingDirNonWindows = "/bin/";
private static readonly string DefaultPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? DefaultPathWindows : DefaultPathNonWindows;
private static readonly string DefaultWorkingDir = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? DefaultWorkingDirWindows : DefaultWorkingDirNonWindows;

private static string? GetNonEmptyStringOrNull(string? str)
{
return !string.IsNullOrEmpty(str) ? str : null;
}

private static SemaphoreSlim _azCliInitSemaphore = new SemaphoreSlim(1, 1);
private static bool _azCliInitialized = false;

private async Task SetUpAzAsync()
{
await _azCliInitSemaphore.WaitAsync();
try
{
if (_azCliInitialized) return;

string fileName;
string argument;
string command = $"az config set auto-upgrade.enable=no";

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
fileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "cmd.exe");
argument = $"/d /c \"{command}\"";
}
else
{
fileName = "/bin/sh";
argument = $"-c \"{command}\"";
}

string path = !string.IsNullOrEmpty(EnvPath) ? EnvPath : DefaultPath;
var processInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = argument,
UseShellExecute = false,
ErrorDialog = false,
CreateNoWindow = true,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
WorkingDirectory = DefaultWorkingDir,
Environment = { { "PATH", path } }
};

using Process? process = Process.Start(processInfo);
if (process == null)
{
throw new InvalidOperationException("Failed to start process to disable auto update of Azure CLI");
}

using var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(30));
await process.WaitForExitAsync(tokenSource.Token);

if (!process.HasExited)
{
// try clean up the process if it is still running after timeout
try { process.Kill(); } catch { /* ignore this excpetion */}
throw new InvalidOperationException("Could not finish az config command to disable auto update on time");
}

process.StandardInput.Close();
process.Close();
}
catch (Exception e)
{
// silent catch with direct console output as this is not a critical error
Console.WriteLine($"Warning - Disable auto update of Azure CLI failed: {e.Message}");
}
finally
{
_azCliInitialized = true;
_azCliInitSemaphore.Release();
}
}

public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken = default)
{
if (!_azCliInitialized)
{
SetUpAzAsync().Wait();
}
return _azureCliCredential.GetToken(requestContext, cancellationToken);
}

public override async ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken = default)
{
if (!_azCliInitialized)
{
await SetUpAzAsync();
}
return await _azureCliCredential.GetTokenAsync(requestContext, cancellationToken);
}
}

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#if !NET472_OR_GREATER

#nullable enable

using System;
using System.Collections.Generic;
using System.IO;
using Azure.Core;
using Azure.Identity;

namespace Microsoft.DotNet.ArcadeAzureIntegration;


// This implementation of TokenCredential will try to cover all common ways of
// authentication to Azure services used in Arcade tooling
public class DefaultIdentityTokenCredential : ChainedTokenCredential
{
public DefaultIdentityTokenCredential()
: this(new DefaultIdentityTokenCredentialOptions())
{
}

public DefaultIdentityTokenCredential(DefaultIdentityTokenCredentialOptions options)
: base(CreateAvailableTokenCredentials(options))
{
}

private static TokenCredential[] CreateAvailableTokenCredentials(DefaultIdentityTokenCredentialOptions options)
{
List<TokenCredential> tokenCredentials = [];

// Add Managed Identity credential if the client id is provided
if (!string.IsNullOrEmpty(options.ManagedIdentityClientId))
{
tokenCredentials.Add(
new ManagedIdentityCredential(options.ManagedIdentityClientId)
);
}

// Add work load identity credential if the environment variables are set
var workloadIdentityCredential = GetWorkloadIdentityCredentialForAzurePipelineTask();
if (workloadIdentityCredential != null)
{
tokenCredentials.Add(workloadIdentityCredential);
}

// Add Azure Pipelines credential if the environment variables are set
var azurePipelinesCredential = GetAzurePipelinesCredentialForAzurePipelineTask();
if (azurePipelinesCredential != null)
{
tokenCredentials.Add(azurePipelinesCredential);
}

if (!options.ExcludeAzureCliCredential)
{
// Add Azure CLI credential as the last resort
// az command to disable auto update of the Azure CLI to avoid timeout waiting for
// console input will be called before first use of AzureCliCredential
tokenCredentials.Add(
new AzureCliCredentialWithAzNoUpdateWrapper(
new AzureCliCredential(
new AzureCliCredentialOptions
{
ProcessTimeout = TimeSpan.FromSeconds(30)
}
)
)
);
}

if (tokenCredentials.Count == 0)
{
throw new InvalidOperationException("No valid credential class detected and configured for authentication to Azure services.");
}

return tokenCredentials.ToArray();
}

private static object _workloadTokenFileLock = new object();
private static string? _workloadTokenFile = null;
private static string? _workloadToken = null;

// Create WorkloadIdentityCredential if the environment variables set by AzurePipeline are provided
private static WorkloadIdentityCredential? GetWorkloadIdentityCredentialForAzurePipelineTask()
{
string? servicePrincipalId = Environment.GetEnvironmentVariable("servicePrincipalId");
string? idToken = Environment.GetEnvironmentVariable("idToken");
string? tenantId = Environment.GetEnvironmentVariable("tenantId");

if (!string.IsNullOrEmpty(idToken) &&
!string.IsNullOrEmpty(tenantId) &&
!string.IsNullOrEmpty(servicePrincipalId))
{
lock (_workloadTokenFileLock)
{
if (idToken != _workloadToken)
{
// create token file
var tokenFileName = Path.GetTempFileName();
File.WriteAllText(tokenFileName, idToken);
_workloadTokenFile = tokenFileName;
_workloadToken = idToken;
}
return new WorkloadIdentityCredential(new WorkloadIdentityCredentialOptions
{
ClientId = servicePrincipalId,
TokenFilePath = _workloadTokenFile,
TenantId = tenantId,
});
}
}
return null;
}

// Create AzurePipelinesCredential if the environment variables set by AzureCli task and SYSTEM_ACCESSTOKEN are provided
private static AzurePipelinesCredential? GetAzurePipelinesCredentialForAzurePipelineTask()
{
string? systemAccessToken = Environment.GetEnvironmentVariable("SYSTEM_ACCESSTOKEN");
string? clientId = Environment.GetEnvironmentVariable("AZURESUBSCRIPTION_CLIENT_ID");
string? tenantId = Environment.GetEnvironmentVariable("AZURESUBSCRIPTION_TENANT_ID");
string? serviceConnectionId = Environment.GetEnvironmentVariable("AZURESUBSCRIPTION_SERVICE_CONNECTION_ID");

if (!string.IsNullOrEmpty(systemAccessToken) &&
!string.IsNullOrEmpty(clientId) &&
!string.IsNullOrEmpty(tenantId) &&
!string.IsNullOrEmpty(serviceConnectionId))
{
return new AzurePipelinesCredential(tenantId, clientId, serviceConnectionId, systemAccessToken);
}
return null;
}
}

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#if !NET472_OR_GREATER

#nullable enable

namespace Microsoft.DotNet.ArcadeAzureIntegration;


public class DefaultIdentityTokenCredentialOptions
{
public string? ManagedIdentityClientId { get; set; } = null;
public bool ExcludeAzureCliCredential { get; set; }
}

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>$(NetCurrent);$(NetFrameworkToolCurrent)</TargetFrameworks>
<ExcludeFromSourceBuild>true</ExcludeFromSourceBuild>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Identity" Version="$(AzureIdentityVersion)" />
</ItemGroup>

</Project>
Loading
Loading