Skip to content

Commit fa37d9d

Browse files
devinroCopilot
andcommitted
symbols: add opt-in Managed Identity support for symbol server auth
Add ManagedIdentityEnabled setting (ProtoMember 26, default false) to SymbolFileSourceSettings. Refactor PDBDebugInfoProvider static credential chain into BuildAndSetCredentialChain(bool) + public ReinitializeCredentials(settings) to allow runtime reconfiguration after settings load (static ctor runs before settings are available). When ManagedIdentityEnabled=true, use ManagedIdentityCredential exclusively (no developer/browser creds) for headless Azure deployments. When false (default), use the existing full developer chain unchanged. Expose useManagedIdentity parameter on MCP server OpenTrace tool for headless/Azure callers like BigRedPerfAI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 576bc7c commit fa37d9d

3 files changed

Lines changed: 81 additions & 38 deletions

File tree

src/ProfileExplorer.McpServer/Program.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,11 @@ public static string OpenTrace(
170170
[Description("Optional additional symbol search path (e.g. 'd:\\temp\\landy' for custom kernel symbols)")]
171171
string? symbolPath = null,
172172
[Description("Optional additional binary search path (e.g. 'd:\\temp' for loose binaries like storport.sys). Required for assembly-level disassembly via GetFunctionAssembly when the binary isn't on the symbol server.")]
173-
string? binaryPath = null)
173+
string? binaryPath = null,
174+
[Description("Enable Azure Managed Identity for symbol server authentication. Use in headless/Azure environments where interactive browser auth is unavailable.")]
175+
bool useManagedIdentity = false)
174176
{
175-
DiagnosticLogger.LogInfo($"[MCP] OpenTrace called: profileFilePath={profileFilePath}, processNameOrId={processNameOrId}, symbolPath={symbolPath ?? "(none)"}, binaryPath={binaryPath ?? "(none)"}");
177+
DiagnosticLogger.LogInfo($"[MCP] OpenTrace called: profileFilePath={profileFilePath}, processNameOrId={processNameOrId}, symbolPath={symbolPath ?? "(none)"}, binaryPath={binaryPath ?? "(none)"}, useManagedIdentity={useManagedIdentity}");
176178

177179
if (!File.Exists(profileFilePath))
178180
return Error("OpenTrace", $"File not found: {profileFilePath}");
@@ -197,6 +199,7 @@ public static string OpenTrace(
197199
var options = new ProfileDataProviderOptions();
198200
var symbolSettings = new SymbolFileSourceSettings();
199201
symbolSettings.UseEnvironmentVarSymbolPaths = true;
202+
symbolSettings.ManagedIdentityEnabled = useManagedIdentity;
200203
if (!string.IsNullOrWhiteSpace(symbolPath))
201204
symbolSettings.InsertSymbolPath(symbolPath);
202205
if (!string.IsNullOrWhiteSpace(binaryPath))
@@ -206,7 +209,10 @@ public static string OpenTrace(
206209
}
207210
ProfileSession.SymbolSettings = symbolSettings;
208211

209-
DiagnosticLogger.LogInfo($"[MCP] SymbolSettings: UseEnvVar=true, CustomPath={symbolPath ?? "(none)"}, EnvVar={symbolSettings.EnvironmentVarSymbolPath ?? "(not set)"}");
212+
// Reinitialize credential chain to pick up ManagedIdentityEnabled flag.
213+
PDBDebugInfoProvider.ReinitializeCredentials(symbolSettings);
214+
215+
DiagnosticLogger.LogInfo($"[MCP] SymbolSettings: UseEnvVar=true, CustomPath={symbolPath ?? "(none)"}, EnvVar={symbolSettings.EnvironmentVarSymbolPath ?? "(not set)"}, ManagedIdentity={useManagedIdentity}");
210216
DiagnosticLogger.LogInfo($"[MCP] SymbolPaths: {string.Join("; ", symbolSettings.SymbolPaths)}");
211217
DiagnosticLogger.LogInfo($"[MCP] BinarySearchPaths: {(options.HasBinarySearchPaths ? string.Join("; ", options.BinarySearchPaths) : "(none)")}");
212218

src/ProfileExplorerCore/Binary/PDBDebugInfoProvider.cs

Lines changed: 70 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ public sealed class PDBDebugInfoProvider : IDebugInfoProvider {
3333

3434
private static ConcurrentDictionary<SymbolFileDescriptor, DebugFileSearchResult> resolvedSymbolsCache_ = new();
3535
private static readonly StringWriter authLogWriter_;
36-
private static readonly SymwebHandler authSymwebHandler_;
37-
private static readonly AzureDevOpsSourceHandler authAzDevOpsHandler_;
36+
private static SymwebHandler authSymwebHandler_;
37+
private static AzureDevOpsSourceHandler authAzDevOpsHandler_;
3838
private static readonly string authRecordPath_ = Path.Combine(Path.GetTempPath(), "ProfileExplorer", "auth_record.bin");
39+
private static readonly object credentialLock_ = new();
3940
private static object undecorateLock_ = new(); // Global lock for undname.
4041
private ConcurrentDictionary<long, SourceFileDebugInfo> sourceFileByRvaCache_ = new();
4142
private ConcurrentDictionary<string, SourceFileDebugInfo> sourceFileByNameCache_ = new();
@@ -84,53 +85,84 @@ public static void ClearResolvedCache() {
8485
public static string DiaRegistrationError => diaRegistrationError_;
8586

8687
static PDBDebugInfoProvider() {
88+
authLogWriter_ = new StringWriter();
89+
BuildAndSetCredentialChain(managedIdentityEnabled: false);
90+
}
91+
92+
/// <summary>
93+
/// Rebuilds the token credential chain based on the provided settings.
94+
/// This exists because the static constructor runs at class-load time, before
95+
/// application settings are available. Call this after settings are loaded to
96+
/// pick up the ManagedIdentityEnabled flag.
97+
/// Thread-safe — uses credentialLock_.
98+
/// </summary>
99+
public static void ReinitializeCredentials(SymbolFileSourceSettings settings) {
100+
BuildAndSetCredentialChain(settings?.ManagedIdentityEnabled ?? false);
101+
}
102+
103+
private static void BuildAndSetCredentialChain(bool managedIdentityEnabled) {
87104
// Create a single instance of the Symweb handler so that
88105
// when concurrent requests are made and login must be done,
89106
// the login page is displayed a single time, with other requests waiting for a token.
90107

91108
// DefaultAzureCredential is not allowed per SFI. Mimic behavior while continuing
92-
// to exclude the managed identity credential.
109+
// to exclude the managed identity credential unless explicitly opted in.
93110
// NOTE: ChainedTokenCredential only falls through on CredentialUnavailableException.
94111
// Other exceptions (like "needs re-authentication") stop the chain. We wrap each
95112
// credential to catch auth failures and convert them to CredentialUnavailableException
96113
// so the chain continues to InteractiveBrowserCredential which prompts the user.
97114

98-
// Enable token cache persistence for browser credential so tokens survive process restarts.
99-
// Additionally, load existing AuthenticationRecord for true silent auth across sessions.
100-
// This prevents re-authentication on every trace load if VS credential fails.
101-
var authRecord = LoadAuthenticationRecord();
102-
var browserCredentialOptions = new InteractiveBrowserCredentialOptions {
103-
TokenCachePersistenceOptions = new TokenCachePersistenceOptions {
104-
Name = "ProfileExplorer" // Unique cache name for this app
105-
},
106-
AuthenticationRecord = authRecord // Enable silent auth with cached identity
107-
};
108-
109-
TokenCredential browserCredential = new InteractiveBrowserCredential(browserCredentialOptions);
115+
List<TokenCredential> credentials;
110116

111-
// If no auth record exists yet, the user will be prompted on first symbol download.
112-
// After first successful auth, capture and save the AuthenticationRecord for future sessions.
113-
if (authRecord == null) {
114-
browserCredential = new CaptureAuthRecordCredential((InteractiveBrowserCredential)browserCredential);
117+
if (managedIdentityEnabled) {
118+
// In Azure/headless environments use Managed Identity exclusively.
119+
// Developer credentials (VS, CLI, browser) are unnecessary and can be
120+
// slow or misleading when running in a cloud context.
121+
Trace.WriteLine("[Auth] Building credential chain: ManagedIdentity-only");
122+
credentials = new List<TokenCredential>
123+
{
124+
WrapCredential(new ManagedIdentityCredential()),
125+
};
115126
}
127+
else {
128+
// Developer/interactive chain. Enable token cache persistence for browser
129+
// credential so tokens survive process restarts. Load any existing
130+
// AuthenticationRecord for true silent auth across sessions.
131+
var authRecord = LoadAuthenticationRecord();
132+
var browserCredentialOptions = new InteractiveBrowserCredentialOptions {
133+
TokenCachePersistenceOptions = new TokenCachePersistenceOptions {
134+
Name = "ProfileExplorer"
135+
},
136+
AuthenticationRecord = authRecord
137+
};
138+
139+
TokenCredential browserCredential = new InteractiveBrowserCredential(browserCredentialOptions);
140+
141+
// If no auth record exists yet, the user will be prompted on first symbol download.
142+
// After first successful auth, capture and save the record for future sessions.
143+
if (authRecord == null) {
144+
browserCredential = new CaptureAuthRecordCredential((InteractiveBrowserCredential)browserCredential);
145+
}
116146

117-
var credentials = new List<TokenCredential>
118-
{
119-
WrapCredential(new EnvironmentCredential()),
120-
WrapCredential(new WorkloadIdentityCredential()),
121-
WrapCredential(new SharedTokenCacheCredential()),
122-
WrapCredential(new VisualStudioCredential()),
123-
WrapCredential(new AzureCliCredential()),
124-
WrapCredential(new AzurePowerShellCredential()),
125-
WrapCredential(new AzureDeveloperCliCredential()),
126-
browserCredential // Don't wrap - final fallback should show errors
127-
};
147+
credentials = new List<TokenCredential>
148+
{
149+
WrapCredential(new EnvironmentCredential()),
150+
WrapCredential(new WorkloadIdentityCredential()),
151+
WrapCredential(new SharedTokenCacheCredential()),
152+
WrapCredential(new VisualStudioCredential()),
153+
WrapCredential(new AzureCliCredential()),
154+
WrapCredential(new AzurePowerShellCredential()),
155+
WrapCredential(new AzureDeveloperCliCredential()),
156+
browserCredential // Don't wrap - final fallback should show errors
157+
};
158+
}
128159

129160
var authCredential = new ChainedTokenCredential(credentials.ToArray());
130161

131-
authLogWriter_ = new StringWriter();
132-
authSymwebHandler_ = new SymwebHandler(authLogWriter_, authCredential);
133-
authAzDevOpsHandler_ = new AzureDevOpsSourceHandler(authLogWriter_, authCredential);
162+
lock (credentialLock_) {
163+
authSymwebHandler_ = new SymwebHandler(authLogWriter_, authCredential);
164+
authAzDevOpsHandler_ = new AzureDevOpsSourceHandler(authLogWriter_, authCredential);
165+
}
134166
}
135167

136168
/// <summary>
@@ -425,8 +457,11 @@ public static async Task<DebugFileSearchResult>
425457

426458
public static SymbolReaderAuthenticationHandler CreateAuthHandler(SymbolFileSourceSettings settings) {
427459
var authHandler = new SymbolReaderAuthenticationHandler();
428-
authHandler.AddHandler(authSymwebHandler_);
429-
authHandler.AddHandler(authAzDevOpsHandler_);
460+
461+
lock (credentialLock_) {
462+
authHandler.AddHandler(authSymwebHandler_);
463+
authHandler.AddHandler(authAzDevOpsHandler_);
464+
}
430465

431466
if (settings.AuthorizationTokenEnabled) {
432467
authHandler.AddHandler(new BasicAuthenticationHandler(settings, authLogWriter_));

src/ProfileExplorerCore/Settings/SymbolFileSourceSettings.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ public SymbolFileSourceSettings() {
8585
public int RejectedFilesCacheExpirationDays { get; set; }
8686
[ProtoMember(25)][OptionValue(true)]
8787
public bool AllowApproximateBinaryMatch { get; set; }
88+
[ProtoMember(26)][OptionValue(false)]
89+
public bool ManagedIdentityEnabled { get; set; }
8890
public bool HasAuthorizationToken => AuthorizationTokenEnabled && !string.IsNullOrEmpty(AuthorizationToken);
8991
public bool HasCompanyFilter => CompanyFilterEnabled;
9092

0 commit comments

Comments
 (0)