Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ The Large Language Model (LLM) used with this MCP server is **entirely customer-
- **Data accuracy depends on source traces.** The MCP server returns data as-is from the database. Analysis quality depends on the completeness and correctness of imported trace data.
- **No built-in authentication.** The default configuration uses anonymous read access. Customers must implement appropriate authentication and network security for their deployment.
- **AI analysis is non-deterministic.** Different AI models and prompts will produce different analysis results for the same trace data. Results should be verified by qualified engineers.
- **Read-only access.** The MCP server provides read and execute permissions only. It cannot modify trace data.
- **Read-only access.** Tables and views allow reads only. Procedure execution is limited to the four read-only keyword searches; trace deletion is not exposed by DAB.
- **View-based analysis thresholds are fixed.** Analytical views (e.g., N+1 pattern detection at >100 DB calls, slow SQL at >5 seconds) use hardcoded thresholds that may not suit all scenarios.

### Disclaimers
Expand Down Expand Up @@ -273,6 +273,8 @@ All server behavior is defined declaratively in `dab-config.json`. Key settings:
- **GraphQL introspection:** Enabled
- **REST request body:** Strict validation

The supplied configuration does not expose `DeleteTrace` and grants no table mutations on REST, GraphQL or MCP. Disabling MCP DML tools alone is not an authorization boundary for the other protocols. Keep deletion out of this public analysis API; [TraceParserWeb](../TraceParserWeb/README.md#authenticated-trace-deletion) provides a separate server-side path for signed-in users.

## Third-Party Dependencies

This project uses the following third-party components:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,6 @@
"actions": [
{
"action": "read"
},
{
"action": "delete"
}
]
}
Expand Down Expand Up @@ -88,9 +85,6 @@
"actions": [
{
"action": "read"
},
{
"action": "delete"
}
]
}
Expand Down Expand Up @@ -145,9 +139,6 @@
"actions": [
{
"action": "read"
},
{
"action": "delete"
}
]
}
Expand Down Expand Up @@ -665,44 +656,6 @@
]
}
]
},
"DeleteTrace": {
"description": "Delete a trace and all related data (TraceLines, UserSessions, etc.) by TraceId.",
"source": {
"object": "dbo.sp_DeleteTrace",
"type": "stored-procedure",
"parameters": [
{
"name": "TraceId",
"required": false,
"default": "0"
}
]
},
"graphql": {
"enabled": true,
"operation": "mutation",
"type": {
"singular": "DeleteTrace",
"plural": "DeleteTraces"
}
},
"rest": {
"enabled": true,
"methods": [
"post"
]
},
"permissions": [
{
"role": "anonymous",
"actions": [
{
"action": "execute"
}
]
}
]
}
}
}
29 changes: 29 additions & 0 deletions Agents/Implementation Agents/Trace Parser/TraceParserWeb/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,32 @@ For local dev, add to `appsettings.Development.json` or user secrets:
}
}
```

## Authenticated trace deletion

Deletion is performed inside the Blazor server, not through the public DAB API. Immediately before accessing SQL, the service requires a signed-in user whose tenant claim matches the configured `AzureAd:TenantId`. **Every signed-in user in that tenant, including admitted guests, can delete any trace.** This is not an administrator-only or per-trace ownership policy.

Deletion is disabled until `TraceAdministration:SqlConnectionString` is configured. On App Service the setting name is `TraceAdministration__SqlConnectionString`. Keep its value in protected server configuration (or a Key Vault reference), never in source, browser fields, logs, or saved agent profiles. The connection validates the SQL server certificate and requires encryption.

Provision a dedicated database principal; do not reuse the SQL administrator, DAB, or importer credential. Prefer a managed identity where SQL Entra authentication is already configured. Alternatively, an operator can create a contained SQL user with a generated password and store that password securely. Grant only:

```sql
GRANT EXECUTE ON OBJECT::dbo.sp_DeleteTrace TO [TraceParserWebDeletion];
GRANT SELECT ON OBJECT::dbo.Traces TO [TraceParserWebDeletion];
```

The principal must not belong to `db_owner`, `db_datawriter`, or other broad roles. The procedure relies on the normal same-owner SQL ownership chain; do not grant table-delete permissions to compensate for a broken chain. No SQL schema or procedure replacement is required by this change.

The server repeats the existing procedure until a separate parameterized query confirms the trace is absent. This supports both the original procedure and incremental versions returning `HasMore`; one successful batch is not reported as a completed deletion. Errors and cancellation are surfaced, and the operation has a five-minute budget. A failed or timed-out operation can leave a partially deleted trace; refresh and retry. Do not delete traces while they are being imported.

Deploy the updated **read-only** `TraceParserMCP/dab-config.json` as part of this update. Merely hiding the Delete button does not remove direct API access. Existing deployments must remove the `DeleteTrace` entity and table-delete grants, not just update the web application. Anonymous analysis remains unchanged; protect sensitive traces with appropriate network and read-access controls.

`deploy.ps1` does not provision deletion credentials. Configure the dedicated principal and server setting separately, then restart the web app. If deletion is unavailable, retain the read-only DAB configuration rather than restoring public mutations.

### Deletion regression checks

The dependency-free console harness checks denied anonymous/wrong-tenant calls, ordinary tenant-user access, disabled configuration, incremental completion, failure/cancellation propagation, and read-only DAB permissions. It uses synthetic identities and a fake store, with no database or network calls:

```powershell
dotnet run --project .\tests\TraceParserWeb.RegressionTests -c Release
```
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
@using TraceParserWeb.Services
@attribute [Authorize]
@inject TraceService TraceSvc
@inject TraceDeletionService DeletionSvc
@inject ILogger<TraceList> Logger
@implements IDisposable

Expand All @@ -20,6 +21,10 @@
<div>
<h2 class="traces-title">Imported Traces</h2>
<p class="traces-subtitle">Manage imported ETL trace sessions.</p>
@if (!DeletionSvc.IsConfigured)
{
<p class="traces-subtitle">Deletion is unavailable until the deployment owner configures authenticated trace deletion.</p>
}
</div>
</div>

Expand Down Expand Up @@ -107,7 +112,7 @@
}
else
{
<button class="btn-delete" @onclick="@(() => RequestDelete(trace.TraceId))" title="Delete trace">
<button class="btn-delete" @onclick="@(() => RequestDelete(trace.TraceId))" title="Delete trace" disabled="@(!DeletionSvc.IsConfigured)">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" width="16" height="16">
<path stroke-linecap="round" stroke-linejoin="round"
Expand Down Expand Up @@ -260,7 +265,7 @@

try
{
await TraceSvc.DeleteTraceAsync(traceId);
await DeletionSvc.DeleteTraceAsync(traceId);
_statusMessage = $"Trace {traceId} deleted successfully.";
_statusIsError = false;
_traces.RemoveAll(t => t.TraceId == traceId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@
builder.Services.Configure<EtlImportOptions>(builder.Configuration.GetSection("EtlImport"));
builder.Services.AddScoped<EtlUploadService>();
builder.Services.AddScoped<TraceService>();
builder.Services.AddScoped<TraceDeletionService>();
builder.Services.Configure<TraceAdministrationOptions>(builder.Configuration.GetSection("TraceAdministration"));
builder.Services.AddScoped<ITraceDeletionStore, SqlTraceDeletionStore>();
builder.Services.AddHttpClient("dab", client =>
{
var dabUrl = builder.Configuration["EtlImport:DabBaseUrl"] ?? "";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System.Data;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Options;

namespace TraceParserWeb.Services;

public class TraceAdministrationOptions
{
public string SqlConnectionString { get; set; } = "";
}

public interface ITraceDeletionStore
{
Task<bool> DeleteBatchAsync(int traceId, CancellationToken ct);
}

public class SqlTraceDeletionStore(IOptions<TraceAdministrationOptions> options) : ITraceDeletionStore
{
public async Task<bool> DeleteBatchAsync(int traceId, CancellationToken ct)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(traceId);
if (string.IsNullOrWhiteSpace(options.Value.SqlConnectionString))
throw new InvalidOperationException("Authenticated trace deletion has not been configured.");

var connectionString = new SqlConnectionStringBuilder(options.Value.SqlConnectionString)
{
Encrypt = true,
TrustServerCertificate = false,
PersistSecurityInfo = false
};
await using var connection = new SqlConnection(connectionString.ConnectionString);
await connection.OpenAsync(ct);
await using var command = connection.CreateCommand();
command.CommandType = CommandType.StoredProcedure;
command.CommandText = "dbo.sp_DeleteTrace";
command.CommandTimeout = 120;
command.Parameters.Add("@TraceId", SqlDbType.Int).Value = traceId;
await command.ExecuteNonQueryAsync(ct);

// The installed procedure may return after one batch. Confirm absence
// independently, also supporting older procedures with no result set.
command.CommandType = CommandType.Text;
command.CommandText = "SELECT CAST(CASE WHEN EXISTS (SELECT 1 FROM dbo.Traces WHERE TraceId = @TraceId) THEN 1 ELSE 0 END AS bit)";
var remaining = await command.ExecuteScalarAsync(ct);
return remaining is bool exists
? exists
: throw new InvalidOperationException("SQL did not confirm trace deletion.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Options;

namespace TraceParserWeb.Services;

public class TraceDeletionService(
ITraceDeletionStore store,
AuthenticationStateProvider authenticationState,
IOptions<TraceAdministrationOptions> options,
IConfiguration configuration,
ILogger<TraceDeletionService> logger)
{
public bool IsConfigured => !string.IsNullOrWhiteSpace(options.Value.SqlConnectionString);

public async Task DeleteTraceAsync(int traceId, CancellationToken ct = default)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(traceId);
ct.ThrowIfCancellationRequested();

var user = (await authenticationState.GetAuthenticationStateAsync()).User;
var tenantClaim = user.FindFirst("tid")?.Value
?? user.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid")?.Value;
if (user.Identity?.IsAuthenticated != true
|| !Guid.TryParse(configuration["AzureAd:TenantId"], out var configuredTenant)
|| configuredTenant == Guid.Empty
|| !Guid.TryParse(tenantClaim, out var userTenant)
|| userTenant != configuredTenant)
{
logger.LogWarning("Denied trace deletion for a user outside the configured tenant");
throw new UnauthorizedAccessException("Sign in to the configured tenant to delete traces.");
}
if (!IsConfigured)
throw new InvalidOperationException("Authenticated trace deletion has not been configured.");

using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromMinutes(5));
try
{
while (true)
{
cts.Token.ThrowIfCancellationRequested();
if (!await store.DeleteBatchAsync(traceId, cts.Token))
break;
}
}
catch (OperationCanceledException) when (!ct.IsCancellationRequested && cts.IsCancellationRequested)
{
throw new TimeoutException("Deletion timed out and may be partial. Refresh the trace list and retry.");
}

logger.LogInformation("Deleted trace {TraceId} for a signed-in user in the configured tenant", traceId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public class TraceStats
public int TotalDatabaseCalls { get; set; }
}

public class TraceService(IHttpClientFactory httpFactory, ILogger<TraceService> logger)
public class TraceService(IHttpClientFactory httpFactory)
{
public async Task<List<TraceDto>> GetTracesAsync(CancellationToken ct = default)
{
Expand Down Expand Up @@ -138,26 +138,4 @@ public async Task<ImportStage> GetImportStageAsync(int traceId, CancellationToke
return ImportStage.Parsing;
}
}

public async Task DeleteTraceAsync(int traceId, CancellationToken ct = default)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromMinutes(5));

var http = httpFactory.CreateClient("dab");
// DAB requires SP parameters in JSON body (not query string).
// Use explicit options to preserve PascalCase — DAB rejects camelCase field names.
var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = null };
var content = JsonContent.Create(new { TraceId = traceId }, options: jsonOptions);
var response = await http.PostAsync("/api/DeleteTrace", content, cts.Token);

if (!response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync(cts.Token);
logger.LogError("Delete trace {TraceId} failed: {Status} {Body}", traceId, response.StatusCode, body);
throw new InvalidOperationException($"Failed to delete trace {traceId}: {response.StatusCode}");
}

logger.LogInformation("Deleted trace {TraceId}", traceId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<PackageReference Include="Azure.Storage.Blobs" Version="12.24.1" />
<PackageReference Include="Markdig" Version="0.44.0" />
<PackageReference Include="Microsoft.Agents.CopilotStudio.Client" Version="1.3.176" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.4" />
<PackageReference Include="Microsoft.Extensions.AI" Version="10.1.1" />
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.79.2" />
<PackageReference Include="Microsoft.Identity.Web" Version="4.2.0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"ContainerName": "etl-uploads",
"DabBaseUrl": "<YOUR_DAB_APP_SERVICE_URL>"
},
"TraceAdministration": {
"SqlConnectionString": ""
},
"AllowedHosts": "*"
}


Loading