344 lines
11 KiB
C#
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);
|
|
}
|
|
|