consolidation of some of the modules, localization fixes, product advisories work, qa work

This commit is contained in:
master
2026-03-05 03:54:22 +02:00
parent 7bafcc3eef
commit 8e1cb9448d
3878 changed files with 72600 additions and 46861 deletions

View File

@@ -0,0 +1,191 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.ServerIntegration;
using StellaOps.Auth.ServerIntegration.Tenancy;
using StellaOps.Cryptography.Audit;
using StellaOps.Router.AspNet;
using StellaOps.TimelineIndexer.Core.Abstractions;
using StellaOps.TimelineIndexer.Core.Models;
using StellaOps.TimelineIndexer.Infrastructure.DependencyInjection;
using StellaOps.Localization;
using StellaOps.TimelineIndexer.WebService;
using static StellaOps.Localization.T;
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("appsettings.Development.json", optional: true, reloadOnChange: true);
builder.Configuration.AddEnvironmentVariables();
builder.Configuration.AddEnvironmentVariables(prefix: "TIMELINE_");
builder.Services.AddTimelineIndexerPostgres(builder.Configuration);
builder.Services.AddSingleton<IAuthEventSink, TimelineAuthorizationAuditSink>();
builder.Services.AddStellaOpsResourceServerAuthentication(
builder.Configuration,
configure: options =>
{
options.RequiredScopes.Clear();
});
builder.Services.AddAuthorization(options =>
{
options.AddObservabilityResourcePolicies();
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddRequirements(new StellaOpsScopeRequirement(new[] { StellaOpsScopes.TimelineRead }))
.Build();
options.FallbackPolicy = options.DefaultPolicy;
});
builder.Services.AddOpenApi();
builder.Services.AddStellaOpsTenantServices();
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
builder.Services.AddStellaOpsLocalization(builder.Configuration);
builder.Services.AddTranslationBundle(System.Reflection.Assembly.GetExecutingAssembly());
// Stella Router integration
var routerEnabled = builder.Services.AddRouterMicroservice(
builder.Configuration,
serviceName: "timelineindexer",
version: System.Reflection.CustomAttributeExtensions.GetCustomAttribute<System.Reflection.AssemblyInformationalVersionAttribute>(System.Reflection.Assembly.GetExecutingAssembly())?.InformationalVersion ?? "1.0.0",
routerOptionsSection: "Router");
builder.TryAddStellaOpsLocalBinding("timelineindexer");
var app = builder.Build();
app.LogStellaOpsLocalHostname("timelineindexer");
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseStellaOpsCors();
app.UseStellaOpsLocalization();
app.UseAuthentication();
app.UseAuthorization();
app.UseStellaOpsTenantMiddleware();
app.TryUseStellaRouter(routerEnabled);
await app.LoadTranslationsAsync();
MapTimelineEndpoints(app.MapGroup("/api/v1").RequireTenant(), routeNamePrefix: "timeline_api_v1");
MapTimelineEndpoints(app.MapGroup(string.Empty).RequireTenant(), routeNamePrefix: "timeline");
// Refresh Router endpoint cache
app.TryRefreshStellaRouterEndpoints(routerEnabled);
await app.RunAsync().ConfigureAwait(false);
static string GetTenantId(HttpContext ctx)
{
// Temporary: allow explicit header override; fallback to claim if present.
if (ctx.Request.Headers.TryGetValue("X-Tenant", out var header) && !string.IsNullOrWhiteSpace(header))
{
return header!;
}
var tenant = ctx.User.FindFirst("tenant")?.Value;
if (!string.IsNullOrWhiteSpace(tenant))
{
return tenant!;
}
throw new InvalidOperationException("Tenant not provided");
}
static void MapTimelineEndpoints(RouteGroupBuilder routes, string routeNamePrefix)
{
routes.MapGet("/timeline", async (
HttpContext ctx,
ITimelineQueryService service,
[FromQuery] string? eventType,
[FromQuery] string? source,
[FromQuery] string? correlationId,
[FromQuery] string? traceId,
[FromQuery] string? severity,
[FromQuery] DateTimeOffset? since,
[FromQuery] long? after,
[FromQuery] int? limit,
CancellationToken cancellationToken) =>
{
var tenantId = GetTenantId(ctx);
var options = new TimelineQueryOptions
{
EventType = eventType,
Source = source,
CorrelationId = correlationId,
TraceId = traceId,
Severity = severity,
Since = since,
AfterEventSeq = after,
Limit = limit ?? 100
};
var items = await service.QueryAsync(tenantId, options, cancellationToken).ConfigureAwait(false);
return Results.Ok(items);
})
.WithName($"{routeNamePrefix}_query")
.WithSummary("List timeline events")
.WithDescription(_t("timelineindexer.timeline.query_description"))
.WithTags("timeline")
.Produces<IReadOnlyList<TimelineEventView>>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.RequireAuthorization(StellaOpsResourceServerPolicies.TimelineRead);
routes.MapGet("/timeline/{eventId}", async (
HttpContext ctx,
ITimelineQueryService service,
string eventId,
CancellationToken cancellationToken) =>
{
var tenantId = GetTenantId(ctx);
var item = await service.GetAsync(tenantId, eventId, cancellationToken).ConfigureAwait(false);
return item is null ? Results.NotFound() : Results.Ok(item);
})
.WithName($"{routeNamePrefix}_get_by_id")
.WithSummary("Get timeline event")
.WithDescription(_t("timelineindexer.timeline.get_by_id_description"))
.WithTags("timeline")
.Produces<TimelineEventView>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.RequireAuthorization(StellaOpsResourceServerPolicies.TimelineRead);
routes.MapGet("/timeline/{eventId}/evidence", async (
HttpContext ctx,
ITimelineQueryService service,
string eventId,
CancellationToken cancellationToken) =>
{
var tenantId = GetTenantId(ctx);
var evidence = await service.GetEvidenceAsync(tenantId, eventId, cancellationToken).ConfigureAwait(false);
return evidence is null ? Results.NotFound() : Results.Ok(evidence);
})
.WithName($"{routeNamePrefix}_get_evidence")
.WithSummary("Get event evidence")
.WithDescription(_t("timelineindexer.timeline.get_evidence_description"))
.WithTags("timeline")
.Produces<TimelineEvidenceView>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.RequireAuthorization(StellaOpsResourceServerPolicies.TimelineRead);
routes.MapPost("/timeline/events", () =>
Results.Accepted("/timeline/events", new TimelineIngestAcceptedResponse("indexed")))
.WithName($"{routeNamePrefix}_ingest_event")
.WithSummary("Ingest timeline event")
.WithDescription(_t("timelineindexer.timeline.ingest_description"))
.WithTags("timeline")
.Produces<TimelineIngestAcceptedResponse>(StatusCodes.Status202Accepted)
.Produces(StatusCodes.Status401Unauthorized)
.RequireAuthorization(StellaOpsResourceServerPolicies.TimelineWrite);
}
public sealed record TimelineIngestAcceptedResponse(string Status);

View File

@@ -0,0 +1,27 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:10231",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"STELLAOPS_WEBSERVICES_CORS": "true",
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:10230;http://localhost:10231",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"STELLAOPS_WEBSERVICES_CORS": "true",
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
}
}
}
}

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" ?>
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\__Libraries\StellaOps.TimelineIndexer.Core\StellaOps.TimelineIndexer.Core.csproj" />
<ProjectReference Include="..\__Libraries\StellaOps.TimelineIndexer.Infrastructure\StellaOps.TimelineIndexer.Infrastructure.csproj" />
<ProjectReference Include="..\..\Authority\StellaOps.Authority\StellaOps.Auth.ServerIntegration\StellaOps.Auth.ServerIntegration.csproj" />
<ProjectReference Include="..\..\Authority\StellaOps.Authority\StellaOps.Auth.Abstractions\StellaOps.Auth.Abstractions.csproj" />
<ProjectReference Include="..\..\Router/__Libraries/StellaOps.Router.AspNet\StellaOps.Router.AspNet.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Localization\StellaOps.Localization.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Translations\*.json" />
</ItemGroup>
<Target Name="PackRouterTransportPlugins" AfterTargets="Publish">
<PropertyGroup>
<RouterTransportPluginProject Condition="'$(RouterTransportPluginProject)' == ''">$(MSBuildThisFileDirectory)..\..\Router\__Libraries\StellaOps.Router.Transport.Messaging\StellaOps.Router.Transport.Messaging.csproj</RouterTransportPluginProject>
<MessagingTransportPluginProject Condition="'$(MessagingTransportPluginProject)' == ''">$(MSBuildThisFileDirectory)..\..\Router\__Libraries\StellaOps.Messaging.Transport.Valkey\StellaOps.Messaging.Transport.Valkey.csproj</MessagingTransportPluginProject>
<RouterTransportPluginSourceDir Condition="'$(RouterTransportPluginSourceDir)' == ''">$(MSBuildThisFileDirectory)..\..\Router\__Libraries\StellaOps.Router.Transport.Messaging\bin\$(Configuration)\$(TargetFramework)</RouterTransportPluginSourceDir>
<MessagingTransportPluginSourceDir Condition="'$(MessagingTransportPluginSourceDir)' == ''">$(MSBuildThisFileDirectory)..\..\Router\__Libraries\StellaOps.Messaging.Transport.Valkey\bin\$(Configuration)\$(TargetFramework)</MessagingTransportPluginSourceDir>
</PropertyGroup>
<MSBuild
Projects="$(RouterTransportPluginProject);$(MessagingTransportPluginProject)"
Targets="Restore;Build"
Properties="Configuration=$(Configuration);TargetFramework=$(TargetFramework);CopyLocalLockFileAssemblies=true"
BuildInParallel="false" />
<ItemGroup>
<_RouterTransportPlugins Include="$(RouterTransportPluginSourceDir)\StellaOps*.dll" />
<_RouterTransportPluginMetadata Include="$(RouterTransportPluginSourceDir)\*.deps.json" />
<_MessagingTransportPlugins Include="$(MessagingTransportPluginSourceDir)\StellaOps*.dll" />
<_MessagingTransportPlugins Include="$(MessagingTransportPluginSourceDir)\StackExchange.Redis.dll" Condition="Exists('$(MessagingTransportPluginSourceDir)\StackExchange.Redis.dll')" />
<_MessagingTransportPlugins Include="$(MessagingTransportPluginSourceDir)\Pipelines.Sockets.Unofficial.dll" Condition="Exists('$(MessagingTransportPluginSourceDir)\Pipelines.Sockets.Unofficial.dll')" />
<_MessagingTransportPlugins Include="$(MessagingTransportPluginSourceDir)\System.IO.Hashing.dll" Condition="Exists('$(MessagingTransportPluginSourceDir)\System.IO.Hashing.dll')" />
<_MessagingTransportPluginMetadata Include="$(MessagingTransportPluginSourceDir)\*.deps.json" />
</ItemGroup>
<MakeDir Directories="$(PublishDir)plugins/router/transports" />
<MakeDir Directories="$(PublishDir)plugins/messaging" />
<Copy
SourceFiles="@(_RouterTransportPlugins)"
DestinationFolder="$(PublishDir)plugins/router/transports"
SkipUnchangedFiles="true" />
<Copy
SourceFiles="@(_RouterTransportPluginMetadata)"
DestinationFolder="$(PublishDir)plugins/router/transports"
SkipUnchangedFiles="true" />
<Copy
SourceFiles="@(_MessagingTransportPlugins)"
DestinationFolder="$(PublishDir)plugins/messaging"
SkipUnchangedFiles="true" />
<Copy
SourceFiles="@(_MessagingTransportPluginMetadata)"
DestinationFolder="$(PublishDir)plugins/messaging"
SkipUnchangedFiles="true" />
</Target>
<PropertyGroup Label="StellaOpsReleaseVersion">
<Version>1.0.0-alpha1</Version>
<InformationalVersion>1.0.0-alpha1</InformationalVersion>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,6 @@
@StellaOps.TimelineIndexer.WebService_HostAddress = http://localhost:5194
GET {{StellaOps.TimelineIndexer.WebService_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@@ -0,0 +1,9 @@
# StellaOps.TimelineIndexer.WebService Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_solid_review.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| RVM-04 | DONE | Adopted `AddRouterMicroservice()` (plugin-driven transport registration), added `/api/v1/timeline*` alias routes, fixed plugin publish packaging with clean-container restore/build, and validated compose-default Valkey messaging registration + gateway OpenAPI endpoint/schema visibility. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/TimelineIndexer/StellaOps.TimelineIndexer/StellaOps.TimelineIndexer.WebService/StellaOps.TimelineIndexer.WebService.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |

View File

@@ -0,0 +1,26 @@
using Microsoft.Extensions.Logging;
using StellaOps.Cryptography.Audit;
using System.Linq;
namespace StellaOps.TimelineIndexer.WebService;
/// <summary>
/// Logs authorization outcomes for timeline read/write operations.
/// </summary>
public sealed class TimelineAuthorizationAuditSink(ILogger<TimelineAuthorizationAuditSink> logger) : IAuthEventSink
{
public ValueTask WriteAsync(AuthEventRecord record, CancellationToken cancellationToken)
{
logger.LogInformation(
"Auth {Outcome} for {EventType} tenant={Tenant} scopes={Scopes} subject={Subject} correlation={Correlation}",
record.Outcome,
record.EventType,
record.Tenant.Value ?? "<none>",
record.Scopes.Any() ? string.Join(" ", record.Scopes) : "<none>",
record.Subject?.SubjectId.Value ?? "<unknown>",
record.CorrelationId ?? "<none>");
return ValueTask.CompletedTask;
}
}

View File

@@ -0,0 +1,8 @@
{
"_meta": { "locale": "en-US", "namespace": "timelineindexer", "version": "1.0" },
"timelineindexer.timeline.query_description": "Returns timeline events filtered by tenant and optional query parameters.",
"timelineindexer.timeline.get_by_id_description": "Returns a single timeline event by event identifier for the current tenant.",
"timelineindexer.timeline.get_evidence_description": "Returns evidence linkage for a timeline event, including bundle and attestation references.",
"timelineindexer.timeline.ingest_description": "Queues an event ingestion request for asynchronous timeline indexing."
}

View File

@@ -0,0 +1,61 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Information"
}
},
"Authority": {
"ResourceServer": {
"Authority": "https://authority.localtest.me",
"Audiences": [
"api://timeline-indexer"
],
"RequiredTenants": [
"tenant-default"
]
}
},
"Postgres": {
"Timeline": {
"ConnectionString": "Host=localhost;Database=timeline;Username=timeline;Password=timeline;",
"SchemaName": "timeline",
"CommandTimeoutSeconds": 30
}
},
"TimelineIndexer": {
"Router": {
"Enabled": false,
"Region": "local",
"DefaultTimeoutSeconds": 30,
"HeartbeatIntervalSeconds": 10,
"TransportPlugins": {
"Directory": "plugins/router/transports",
"SearchPattern": "StellaOps.Router.Transport.*.dll"
},
"Gateways": [
{
"Host": "router.stella-ops.local",
"Port": 9100,
"TransportType": "Messaging"
}
],
"Messaging": {
"Transport": "valkey",
"PluginDirectory": "plugins/messaging",
"SearchPattern": "StellaOps.Messaging.Transport.*.dll",
"RequestQueueTemplate": "router:requests:{service}",
"ResponseQueueName": "router:responses",
"ConsumerGroup": "timelineindexer",
"RequestTimeout": "30s",
"LeaseDuration": "5m",
"BatchSize": 10,
"HeartbeatInterval": "10s",
"valkey": {
"ConnectionString": "cache.stella-ops.local:6379"
}
}
}
},
"AllowedHosts": "*"
}

View File

@@ -0,0 +1,61 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Authority": {
"ResourceServer": {
"Authority": "https://authority.localtest.me",
"Audiences": [
"api://timeline-indexer"
],
"RequiredTenants": [
"tenant-default"
]
}
},
"Postgres": {
"Timeline": {
"ConnectionString": "Host=localhost;Database=timeline;Username=timeline;Password=timeline;",
"SchemaName": "timeline",
"CommandTimeoutSeconds": 30
}
},
"TimelineIndexer": {
"Router": {
"Enabled": false,
"Region": "local",
"DefaultTimeoutSeconds": 30,
"HeartbeatIntervalSeconds": 10,
"TransportPlugins": {
"Directory": "plugins/router/transports",
"SearchPattern": "StellaOps.Router.Transport.*.dll"
},
"Gateways": [
{
"Host": "router.stella-ops.local",
"Port": 9100,
"TransportType": "Messaging"
}
],
"Messaging": {
"Transport": "valkey",
"PluginDirectory": "plugins/messaging",
"SearchPattern": "StellaOps.Messaging.Transport.*.dll",
"RequestQueueTemplate": "router:requests:{service}",
"ResponseQueueName": "router:responses",
"ConsumerGroup": "timelineindexer",
"RequestTimeout": "30s",
"LeaseDuration": "5m",
"BatchSize": 10,
"HeartbeatInterval": "10s",
"valkey": {
"ConnectionString": "cache.stella-ops.local:6379"
}
}
}
},
"AllowedHosts": "*"
}