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 Support for Symweb Authentication to PerfView #2039

Merged
merged 3 commits into from
May 30, 2024
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
3 changes: 3 additions & 0 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
<MicroBuildCoreVersion>0.3.1</MicroBuildCoreVersion>
<MicrosoftIdentityClientVersion>4.60.1</MicrosoftIdentityClientVersion>
<MicrosoftIdentityClientExtensionsMsalVersion>4.60.1</MicrosoftIdentityClientExtensionsMsalVersion>
<MicrosoftIdentityModelAbstractionsVersion>7.1.2</MicrosoftIdentityModelAbstractionsVersion>
<MicrosoftIdentityModelTokensVersion>7.1.2</MicrosoftIdentityModelTokensVersion>
<MicrosoftIdentityModelJsonWebTokensVersion>7.1.2</MicrosoftIdentityModelJsonWebTokensVersion>
<MicrosoftSourceLinkGitHubVersion>8.0.0</MicrosoftSourceLinkGitHubVersion>
Expand All @@ -58,6 +59,8 @@
<SystemReflectionTypeExtensionsVersion>4.7.0</SystemReflectionTypeExtensionsVersion>
<SystemRuntimeCompilerServicesUnsafeVersion>6.0.0</SystemRuntimeCompilerServicesUnsafeVersion>
<SystemRuntimeInteropServicesRuntimeInformationVersion></SystemRuntimeInteropServicesRuntimeInformationVersion>
<SystemSecurityCryptographyAlgorithmsVersion>4.3.1</SystemSecurityCryptographyAlgorithmsVersion>
<SystemSecurityCryptographyProtectedDataVersion>4.7.0</SystemSecurityCryptographyProtectedDataVersion>
<SystemTextEncodingsWebVersion>8.0.0</SystemTextEncodingsWebVersion>
<SystemTextJsonVersion>8.0.1</SystemTextJsonVersion>
<SystemThreadingTasksExtensionsVersion>4.5.4</SystemThreadingTasksExtensionsVersion>
Expand Down
3 changes: 3 additions & 0 deletions src/PerfView/Authentication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ public static void Configure(this SymbolReaderHttpHandler handler, Authenticatio

handler.ClearHandlers();

// Always add Symweb authentication.
handler.AddSymwebAuthentication(log);
brianrob marked this conversation as resolved.
Show resolved Hide resolved

// The order isn't critical, but we chose to put GCM last
// because the user might want to use GCM for GitHub and
// Developer identity for Azure DevOps. If GCM were first,
Expand Down
17 changes: 16 additions & 1 deletion src/PerfView/PerfView.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
<NeutralLanguage>en</NeutralLanguage>

<GeneratePackageOnBuild>true</GeneratePackageOnBuild>

<NuspecFile>PerfView.nuspec</NuspecFile>
<GenerateNuspecDependsOn>$(GenerateNuspecDependsOn);SetNuspecProperties</GenerateNuspecDependsOn>
</PropertyGroup>
Expand Down Expand Up @@ -87,11 +86,13 @@
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent.SupportFiles" Version="$(MicrosoftDiagnosticsTracingTraceEventSupportFilesVersion)" PrivateAssets="all" />
<PackageReference Include="Microsoft.Identity.Client" Version="$(MicrosoftIdentityClientVersion)" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="$(MicrosoftIdentityClientExtensionsMsalVersion)" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.IdentityModel.Abstractions" Version="$(MicrosoftIdentityModelAbstractionsVersion)" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="$(MicrosoftIdentityModelTokensVersion)" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="$(MicrosoftIdentityModelJsonWebTokensVersion)" GeneratePathProperty="true" />
<PackageReference Include="PerfView.SupportFiles" Version="$(PerfViewSupportFilesVersion)" PrivateAssets="all" />
<PackageReference Include="System.Buffers" Version="$(SystemBuffersVersion)" GeneratePathProperty="true" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="$(SystemDiagnosticsDiagnosticSourceVersion)" GeneratePathProperty="true" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="$(SystemSecurityCryptographyProtectedDataVersion)" GeneratePathProperty="true" />
<PackageReference Include="System.Memory" Version="$(SystemMemoryVersion)" GeneratePathProperty="true" />
<PackageReference Include="System.Numerics.Vectors" Version="$(SystemNumericsVectorsVersion)" GeneratePathProperty="true" />
<PackageReference Include="System.Text.Encodings.Web" Version="$(SystemTextEncodingsWebVersion)" GeneratePathProperty="true" />
Expand Down Expand Up @@ -510,6 +511,13 @@
<Link>Microsoft.Identity.Client.Extensions.Msal.dll</Link>
<Visible>False</Visible>
</EmbeddedResource>
<EmbeddedResource Include="$(PkgMicrosoft_IdentityModel_Abstractions)\lib\net461\Microsoft.IdentityModel.Abstractions.dll">
brianrob marked this conversation as resolved.
Show resolved Hide resolved
<Type>Non-Resx</Type>
<WithCulture>false</WithCulture>
<LogicalName>.\Microsoft.IdentityModel.Abstractions.dll</LogicalName>
<Link>Microsoft.IdentityModel.Abstractions.dll</Link>
<Visible>False</Visible>
</EmbeddedResource>
<EmbeddedResource Include="$(PkgMicrosoft_IdentityModel_JsonWebTokens)\lib\net461\Microsoft.IdentityModel.JsonWebTokens.dll">
<Type>Non-Resx</Type>
<WithCulture>false</WithCulture>
Expand Down Expand Up @@ -552,6 +560,13 @@
<Link>System.Numerics.Vectors.dll</Link>
<Visible>False</Visible>
</EmbeddedResource>
<EmbeddedResource Include="$(PkgSystem_Security_Cryptography_ProtectedData)\lib\net461\System.Security.Cryptography.ProtectedData.dll">
<Type>Non-Resx</Type>
<WithCulture>false</WithCulture>
<LogicalName>.\System.Security.Cryptography.ProtectedData.dll</LogicalName>
<Link>System.Security.Cryptography.ProtectedData.dll</Link>
<Visible>False</Visible>
</EmbeddedResource>
<EmbeddedResource Include="$(PkgSystem_Text_Encodings_Web)\lib\net462\System.Text.Encodings.Web.dll">
<Type>Non-Resx</Type>
<WithCulture>false</WithCulture>
Expand Down
173 changes: 173 additions & 0 deletions src/PerfView/SymbolReaderHttpHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,26 @@ public SymbolReaderHttpHandler AddAzureDevOpsAuthentication(TextWriter log, bool
return AddHandler(new AzureDevOpsHandler(log, new DefaultAzureCredential(options)));
}

/// <summary>
/// Add a handler for Symweb authentication using local credentials.
/// It will try to use cached credentials from Visual Studio, VS Code,
/// Azure Powershell and Azure CLI.
/// </summary>
/// <param name="log">A logger.</param>
/// <param name="silent">If no local credentials can be found, then a browser window will
/// be opened to prompt the user. Set this to true to if you don't want that.</param>
/// <returns>This instance for fluent chaining.</returns>
public SymbolReaderHttpHandler AddSymwebAuthentication(TextWriter log, bool silent = false)
{
DefaultAzureCredentialOptions options = new DefaultAzureCredentialOptions
{
ExcludeInteractiveBrowserCredential = silent,
ExcludeManagedIdentityCredential = true // This is not designed to be used in a service.
};

return AddHandler(new SymwebHandler(log, new DefaultAzureCredential(options)));
}

/// <summary>
/// Add a handler for GitHub device flow authentication.
/// </summary>
Expand Down Expand Up @@ -1572,6 +1592,101 @@ private static bool TryGetNextChallengeParameter(ref ReadOnlySpan<char> paramete
}
}

/// <summary>
/// A handler that adds authorization for Symweb.
/// </summary>
internal sealed class SymwebHandler : SymbolReaderAuthHandlerBase
{
/// <summary>
/// The value of <see cref="Symweb.Scope"/> stored in a single element
/// array suitable for passing to
/// <see cref="TokenCredential.GetTokenAsync(TokenRequestContext, CancellationToken)"/>.
/// </summary>
private static readonly string[] s_scopes = new[] { Symweb.Scope };

/// <summary>
/// Prefix to put in front of logging messages.
/// </summary>
private const string LogPrefix = "SymwebAuth: ";

/// <summary>
/// A provider of access tokens.
/// </summary>
private readonly TokenCredential _tokenCredential;

/// <summary>
/// Protect <see cref="_tokenCredential"/> against concurrent access.
/// </summary>
private readonly SemaphoreSlim _tokenCredentialGate = new SemaphoreSlim(initialCount: 1);

/// <summary>
/// An HTTP client used to discover the authority (login endpoint and tenant) for Symweb.
/// </summary>
private readonly HttpClient _httpClient = new HttpClient(new HttpClientHandler() { CheckCertificateRevocationList = true });

/// <summary>
/// Construct a new <see cref="SymwebHandler"/> instance.
/// </summary>
/// <param name="tokenCredential">A provider of access tokens.</param>
public SymwebHandler(TextWriter log, TokenCredential tokenCredential) : base(log, LogPrefix)
{
_tokenCredential = tokenCredential ?? throw new ArgumentNullException(nameof(tokenCredential));
}

/// <summary>
/// Try to find the authority endpoint for Symweb
/// given a full URI.
/// </summary>
/// <param name="requestUri">The request URI.</param>
/// <param name="authority">The authority, if found.</param>
/// <returns>True if <paramref name="requestUri"/> represents a path to a
/// resource in Symweb.</returns>
protected override bool TryGetAuthority(Uri requestUri, out Uri authority) => Symweb.TryGetAuthority(requestUri, out authority);

/// <summary>
/// Get a token to access Symweb.
/// </summary>
/// <param name="context">The request context.</param>
/// <param name="next">The next handler.</param>
/// <param name="authority">The Symweb instance.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>An access token, or null if one could not be obtained.</returns>
protected override async Task<AuthToken?> GetAuthTokenAsync(RequestContext context, SymbolReaderHandlerDelegate next, Uri authority, CancellationToken cancellationToken)
{
// Get a new access token from the credential provider.
WriteLog("Asking for authorization to access {0}", authority);
AuthToken token = await GetTokenAsync(cancellationToken).ConfigureAwait(false);

return token;
}

/// <summary>
/// Get a new access token for Symweb from the <see cref="TokenCredential"/>.
/// </summary>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>The access token.</returns>
private async Task<AuthToken> GetTokenAsync(CancellationToken cancellationToken)
{
await _tokenCredentialGate.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
// Use the token credential provider to acquire a new token.
TokenRequestContext requestContext = new TokenRequestContext(s_scopes);
AccessToken accessToken = await _tokenCredential.GetTokenAsync(requestContext, cancellationToken).ConfigureAwait(false);
return AuthToken.FromAzureCoreAccessToken(accessToken);
}
catch (Exception ex)
{
WriteStatusLog("Exception getting token. {0}", ex);
throw;
}
finally
{
_tokenCredentialGate.Release();
}
}
}

/// <summary>
/// A handler that handles GitHub device flow authorization.
/// </summary>
Expand Down Expand Up @@ -1861,6 +1976,64 @@ private sealed class AccessTokenResponse
}
}

/// <summary>
/// Contains constants, static properties and helper methods pertinent to Symweb.
/// </summary>
internal static class Symweb
{
/// <summary>
/// The OAuth scope to use when requesting tokens for Symweb.
/// </summary>
public const string Scope = "af9e1c69-e5e9-4331-8cc5-cdf93d57bafa/.default";

/// <summary>
/// The url host for Symweb.
/// </summary>
public const string SymwebHost = "symweb.azurefd.net";

/// <summary>
/// Try to find the authority endpoint for Symweb given a full URI.
/// </summary>
/// <param name="requestUri">The request URI.</param>
/// <param name="authority">The authority, if found.</param>
/// <returns>True if <paramref name="requestUri"/> represents a path to a
/// resource in Symweb.</returns>
public static bool TryGetAuthority(Uri requestUri, out Uri authority)
{
if (!requestUri.IsAbsoluteUri)
{
authority = null;
return false;
}

UriBuilder builder = null;
string host = requestUri.DnsSafeHost;
if (host.Equals(SymwebHost, StringComparison.OrdinalIgnoreCase))
{
builder = new UriBuilder
{
Host = SymwebHost
};
}

if (builder is null)
{
// Not a Symweb URI.
authority = null;
return false;
}

builder.Scheme = requestUri.Scheme;
if (!requestUri.IsDefaultPort)
{
builder.Port = requestUri.Port;
}

authority = builder.Uri;
return true;
}
}

/// <summary>
/// Contains constants, static properties and helper methods pertinent to Azure DevOps.
/// </summary>
Expand Down
4 changes: 1 addition & 3 deletions src/TraceEvent/Symbols/SymbolPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,7 @@ public static string MicrosoftSymbolServerPath
if (s_MicrosoftSymbolServerPath == null)
{
s_MicrosoftSymbolServerPath = s_MicrosoftSymbolServerPath +
";" + @"SRV*https://msdl.microsoft.com/download/symbols" + // Operatig system Symbols
";" + @"SRV*https://nuget.smbsrc.net" + // Nuget symbols
";" + @"SRV*https://referencesource.microsoft.com/symbols"; // .NET Runtime desktop symbols
";" + @"SRV*https://msdl.microsoft.com/download/symbols";
}
return s_MicrosoftSymbolServerPath;
}
Expand Down
6 changes: 3 additions & 3 deletions src/TraceEvent/Symbols/SymbolReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1072,7 +1072,7 @@ internal bool GetPhysicalFileFromServer(string serverPath, string pdbIndexPath,
{
m_log.WriteLine("FindSymbolFilePath: In task, sending HTTP request {0}", fullUri);

var responseTask = HttpClient.GetAsync(fullUri);
var responseTask = HttpClient.GetAsync(fullUri, HttpCompletionOption.ResponseHeadersRead);
brianrob marked this conversation as resolved.
Show resolved Hide resolved
responseTask.Wait();
var response = responseTask.Result.EnsureSuccessStatusCode();

Expand Down Expand Up @@ -1140,8 +1140,8 @@ internal bool GetPhysicalFileFromServer(string serverPath, string pdbIndexPath,
}
});

// Wait 25 seconds allowing for interruptions.
var limit = 250;
// Wait 60 seconds allowing for interruptions.
var limit = 600;

for (int i = 0; i < limit; i++)
{
Expand Down