Files
git.stella-ops.org/src/BinaryIndex/StellaOps.Symbols.Server/Program.cs

344 lines
11 KiB
C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.HttpResults;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.ServerIntegration;
using StellaOps.Auth.ServerIntegration.Tenancy;
using StellaOps.Symbols.Core.Abstractions;
using StellaOps.Symbols.Core.Models;
using StellaOps.Symbols.Infrastructure;
using StellaOps.Symbols.Infrastructure.Hashing;
using StellaOps.Symbols.Marketplace.Scoring;
using StellaOps.Symbols.Server.Contracts;
using StellaOps.Symbols.Server.Endpoints;
using StellaOps.Symbols.Server.Security;
using StellaOps.Router.AspNet;
var builder = WebApplication.CreateBuilder(args);
// Authentication and Authorization
builder.Services.AddStellaOpsResourceServerAuthentication(
builder.Configuration,
configure: options =>
{
options.RequiredScopes.Clear();
});
builder.Services.AddStellaOpsTenantServices();
builder.Services.AddAuthorization(options =>
{
options.AddStellaOpsScopePolicy(SymbolsPolicies.Read, StellaOpsScopes.SymbolsRead);
options.AddStellaOpsScopePolicy(SymbolsPolicies.Write, StellaOpsScopes.SymbolsWrite);
});
// Symbols services (in-memory for development)
builder.Services.AddSymbolsInMemory();
// Marketplace services
builder.Services.AddSingleton<ISymbolSourceTrustScorer, DefaultSymbolSourceTrustScorer>();
builder.Services.AddSingleton<StellaOps.Symbols.Marketplace.Repositories.ISymbolSourceReadRepository, StellaOps.Symbols.Server.Endpoints.InMemorySymbolSourceReadRepository>();
builder.Services.AddSingleton<StellaOps.Symbols.Marketplace.Repositories.IMarketplaceCatalogRepository, StellaOps.Symbols.Server.Endpoints.InMemoryMarketplaceCatalogRepository>();
builder.Services.AddOpenApi();
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
// Stella Router integration
var routerEnabled = builder.Services.AddRouterMicroservice(
builder.Configuration,
serviceName: "symbols",
version: System.Reflection.CustomAttributeExtensions.GetCustomAttribute<System.Reflection.AssemblyInformationalVersionAttribute>(System.Reflection.Assembly.GetExecutingAssembly())?.InformationalVersion ?? "1.0.0",
routerOptionsSection: "Router");
builder.TryAddStellaOpsLocalBinding("symbols");
var app = builder.Build();
app.LogStellaOpsLocalHostname("symbols");
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseStellaOpsCors();
app.UseAuthentication();
app.UseAuthorization();
app.UseStellaOpsTenantMiddleware();
app.TryUseStellaRouter(routerEnabled);
// Health endpoint (anonymous)
app.MapGet("/health", () =>
{
return TypedResults.Ok(new SymbolsHealthResponse(
Status: "healthy",
Version: "1.0.0",
Timestamp: DateTimeOffset.UtcNow,
Metrics: null));
})
.AllowAnonymous()
.WithName("GetHealth")
.WithSummary("Health check endpoint");
// Upload symbol manifest
app.MapPost("/v1/symbols/manifests", async Task<Results<Created<UploadSymbolManifestResponse>, ProblemHttpResult>> (
HttpContext httpContext,
UploadSymbolManifestRequest request,
ISymbolRepository repository,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
var symbols = request.Symbols.Select(s => new SymbolEntry
{
Address = s.Address,
Size = s.Size,
MangledName = s.MangledName,
DemangledName = s.DemangledName,
Type = s.Type,
Binding = s.Binding,
SourceFile = s.SourceFile,
SourceLine = s.SourceLine,
ContentHash = s.ContentHash
}).ToList();
var sourceMappings = request.SourceMappings?.Select(m => new SourceMapping
{
CompiledPath = m.CompiledPath,
SourcePath = m.SourcePath,
ContentHash = m.ContentHash
}).ToList();
var manifestId = ComputeManifestId(request.DebugId, tenantId, symbols);
var manifest = new SymbolManifest
{
ManifestId = manifestId,
DebugId = request.DebugId,
CodeId = request.CodeId,
BinaryName = request.BinaryName,
Platform = request.Platform,
Format = request.Format,
Symbols = symbols,
SourceMappings = sourceMappings,
TenantId = tenantId,
CreatedAt = DateTimeOffset.UtcNow
};
await repository.StoreManifestAsync(manifest, cancellationToken).ConfigureAwait(false);
var response = new UploadSymbolManifestResponse(
ManifestId: manifestId,
DebugId: request.DebugId,
BinaryName: request.BinaryName,
BlobUri: manifest.BlobUri,
SymbolCount: symbols.Count,
CreatedAt: manifest.CreatedAt);
return TypedResults.Created($"/v1/symbols/manifests/{manifestId}", response);
})
.RequireAuthorization(SymbolsPolicies.Write)
.WithName("UploadSymbolManifest")
.WithSummary("Upload a symbol manifest")
.Produces(StatusCodes.Status201Created)
.ProducesProblem(StatusCodes.Status400BadRequest);
// Get manifest by ID
app.MapGet("/v1/symbols/manifests/{manifestId}", async Task<Results<Ok<SymbolManifestDetailResponse>, NotFound, ProblemHttpResult>> (
string manifestId,
ISymbolRepository repository,
CancellationToken cancellationToken) =>
{
var manifest = await repository.GetManifestAsync(manifestId, cancellationToken).ConfigureAwait(false);
if (manifest is null)
{
return TypedResults.NotFound();
}
var response = MapToDetailResponse(manifest);
return TypedResults.Ok(response);
})
.RequireAuthorization(SymbolsPolicies.Read)
.WithName("GetSymbolManifest")
.WithSummary("Get symbol manifest by ID")
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.ProducesProblem(StatusCodes.Status400BadRequest);
// Query manifests
app.MapGet("/v1/symbols/manifests", async Task<Results<Ok<SymbolManifestListResponse>, ProblemHttpResult>> (
HttpContext httpContext,
ISymbolRepository repository,
string? debugId,
string? codeId,
string? binaryName,
string? platform,
int? limit,
int? offset,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
var query = new SymbolQuery
{
TenantId = tenantId,
DebugId = debugId,
CodeId = codeId,
BinaryName = binaryName,
Platform = platform,
Limit = limit ?? 50,
Offset = offset ?? 0
};
var result = await repository.QueryManifestsAsync(query, cancellationToken).ConfigureAwait(false);
var summaries = result.Manifests.Select(m => new SymbolManifestSummary(
ManifestId: m.ManifestId,
DebugId: m.DebugId,
CodeId: m.CodeId,
BinaryName: m.BinaryName,
Platform: m.Platform,
Format: m.Format,
SymbolCount: m.Symbols.Count,
HasDsse: !string.IsNullOrEmpty(m.DsseDigest),
CreatedAt: m.CreatedAt)).ToList();
return TypedResults.Ok(new SymbolManifestListResponse(
Manifests: summaries,
TotalCount: result.TotalCount,
Offset: result.Offset,
Limit: result.Limit));
})
.RequireAuthorization(SymbolsPolicies.Read)
.WithName("QuerySymbolManifests")
.WithSummary("Query symbol manifests")
.Produces(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest);
// Resolve symbols
app.MapPost("/v1/symbols/resolve", async Task<Results<Ok<ResolveSymbolsResponse>, ProblemHttpResult>> (
HttpContext httpContext,
ResolveSymbolsRequest request,
ISymbolResolver resolver,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
var resolutions = await resolver.ResolveBatchAsync(
request.DebugId,
request.Addresses,
tenantId,
cancellationToken).ConfigureAwait(false);
var dtos = resolutions.Select(r => new SymbolResolutionDto(
Address: r.Address,
Found: r.Found,
MangledName: r.Symbol?.MangledName,
DemangledName: r.Symbol?.DemangledName,
Offset: r.Offset,
SourceFile: r.Symbol?.SourceFile,
SourceLine: r.Symbol?.SourceLine,
Confidence: r.Confidence)).ToList();
return TypedResults.Ok(new ResolveSymbolsResponse(
DebugId: request.DebugId,
Resolutions: dtos));
})
.RequireAuthorization(SymbolsPolicies.Read)
.WithName("ResolveSymbols")
.WithSummary("Resolve symbol addresses")
.Produces(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest);
// Symbol source and marketplace endpoints
app.MapSymbolSourceEndpoints();
// Get manifests by debug ID
app.MapGet("/v1/symbols/by-debug-id/{debugId}", async Task<Results<Ok<SymbolManifestListResponse>, ProblemHttpResult>> (
HttpContext httpContext,
string debugId,
ISymbolRepository repository,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
var manifests = await repository.GetManifestsByDebugIdAsync(debugId, tenantId, cancellationToken)
.ConfigureAwait(false);
var summaries = manifests.Select(m => new SymbolManifestSummary(
ManifestId: m.ManifestId,
DebugId: m.DebugId,
CodeId: m.CodeId,
BinaryName: m.BinaryName,
Platform: m.Platform,
Format: m.Format,
SymbolCount: m.Symbols.Count,
HasDsse: !string.IsNullOrEmpty(m.DsseDigest),
CreatedAt: m.CreatedAt)).ToList();
return TypedResults.Ok(new SymbolManifestListResponse(
Manifests: summaries,
TotalCount: summaries.Count,
Offset: 0,
Limit: summaries.Count));
})
.RequireAuthorization(SymbolsPolicies.Read)
.WithName("GetManifestsByDebugId")
.WithSummary("Get manifests by debug ID")
.Produces(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest);
app.TryRefreshStellaRouterEndpoints(routerEnabled);
app.Run();
static bool TryGetTenant(HttpContext httpContext, out ProblemHttpResult? problem, out string tenantId)
{
tenantId = string.Empty;
if (!httpContext.Request.Headers.TryGetValue("X-Stella-Tenant", out var tenantValues) ||
string.IsNullOrWhiteSpace(tenantValues))
{
problem = TypedResults.Problem(statusCode: StatusCodes.Status400BadRequest, title: "missing_tenant");
return false;
}
tenantId = tenantValues.ToString();
problem = null;
return true;
}
static string ComputeManifestId(string debugId, string tenantId, IReadOnlyList<SymbolEntry> symbols)
{
return SymbolHashing.ComputeManifestId(debugId, tenantId, symbols);
}
static SymbolManifestDetailResponse MapToDetailResponse(SymbolManifest manifest)
{
return new SymbolManifestDetailResponse(
ManifestId: manifest.ManifestId,
DebugId: manifest.DebugId,
CodeId: manifest.CodeId,
BinaryName: manifest.BinaryName,
Platform: manifest.Platform,
Format: manifest.Format,
TenantId: manifest.TenantId,
BlobUri: manifest.BlobUri,
DsseDigest: manifest.DsseDigest,
RekorLogIndex: manifest.RekorLogIndex,
SymbolCount: manifest.Symbols.Count,
Symbols: manifest.Symbols.Select(s => new SymbolEntryDto(
s.Address, s.Size, s.MangledName, s.DemangledName,
s.Type, s.Binding, s.SourceFile, s.SourceLine, s.ContentHash)).ToList(),
SourceMappings: manifest.SourceMappings?.Select(m => new SourceMappingDto(
m.CompiledPath, m.SourcePath, m.ContentHash)).ToList(),
CreatedAt: manifest.CreatedAt);
}