search and ai stabilization work, localization stablized.
This commit is contained in:
@@ -3,10 +3,12 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Auth.ServerIntegration.Tenancy;
|
||||
using StellaOps.Localization;
|
||||
using StellaOps.Graph.Api.Contracts;
|
||||
using StellaOps.Graph.Api.Security;
|
||||
using StellaOps.Graph.Api.Services;
|
||||
using StellaOps.Router.AspNet;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -59,6 +61,34 @@ builder.Services.AddAuthorization(options =>
|
||||
builder.Services.AddStellaOpsTenantServices();
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
|
||||
builder.Services.AddStellaOpsLocalization(builder.Configuration, options =>
|
||||
{
|
||||
options.DefaultLocale = string.IsNullOrWhiteSpace(options.DefaultLocale) ? "en-US" : options.DefaultLocale;
|
||||
if (options.SupportedLocales.Count == 0)
|
||||
{
|
||||
options.SupportedLocales.Add("en-US");
|
||||
}
|
||||
|
||||
if (!options.SupportedLocales.Contains("de-DE", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
options.SupportedLocales.Add("de-DE");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.RemoteBundleUrl))
|
||||
{
|
||||
var platformUrl = builder.Configuration["STELLAOPS_PLATFORM_URL"] ?? builder.Configuration["Platform:BaseUrl"];
|
||||
if (!string.IsNullOrWhiteSpace(platformUrl))
|
||||
{
|
||||
options.RemoteBundleUrl = platformUrl;
|
||||
}
|
||||
}
|
||||
|
||||
options.EnableRemoteBundles =
|
||||
options.EnableRemoteBundles || !string.IsNullOrWhiteSpace(options.RemoteBundleUrl);
|
||||
});
|
||||
builder.Services.AddTranslationBundle(System.Reflection.Assembly.GetExecutingAssembly());
|
||||
builder.Services.AddRemoteTranslationBundles();
|
||||
|
||||
// Stella Router integration
|
||||
var routerEnabled = builder.Services.AddRouterMicroservice(
|
||||
builder.Configuration,
|
||||
@@ -71,12 +101,15 @@ var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("graph");
|
||||
|
||||
app.UseStellaOpsCors();
|
||||
app.UseStellaOpsLocalization();
|
||||
app.UseRouting();
|
||||
app.TryUseStellaRouter(routerEnabled);
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.UseStellaOpsTenantMiddleware();
|
||||
|
||||
await app.LoadTranslationsAsync();
|
||||
|
||||
app.MapPost("/graph/search", async (HttpContext context, GraphSearchRequest request, IGraphSearchService service, CancellationToken ct) =>
|
||||
{
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
@@ -318,7 +351,11 @@ app.MapGet("/graph/export/{jobId}", async (string jobId, HttpContext context, IG
|
||||
if (job is null || !string.Equals(job.Tenant, auth.TenantId, StringComparison.Ordinal))
|
||||
{
|
||||
LogAudit(context, "/graph/export/download", StatusCodes.Status404NotFound, sw.ElapsedMilliseconds);
|
||||
return Results.NotFound(new ErrorResponse { Error = "GRAPH_EXPORT_NOT_FOUND", Message = "Export job not found" });
|
||||
return Results.NotFound(new ErrorResponse
|
||||
{
|
||||
Error = "GRAPH_EXPORT_NOT_FOUND",
|
||||
Message = _t("graph.error.export_not_found")
|
||||
});
|
||||
}
|
||||
|
||||
context.Response.Headers.ContentLength = job.Payload.Length;
|
||||
@@ -372,7 +409,11 @@ app.MapGet("/graph/edges/{edgeId}/metadata", async (string edgeId, HttpContext c
|
||||
if (result is null)
|
||||
{
|
||||
LogAudit(context, "/graph/edges/metadata", StatusCodes.Status404NotFound, sw.ElapsedMilliseconds);
|
||||
return Results.NotFound(new ErrorResponse { Error = "EDGE_NOT_FOUND", Message = $"Edge '{edgeId}' not found" });
|
||||
return Results.NotFound(new ErrorResponse
|
||||
{
|
||||
Error = "EDGE_NOT_FOUND",
|
||||
Message = _tn("graph.error.edge_not_found", ("edgeId", edgeId))
|
||||
});
|
||||
}
|
||||
|
||||
LogAudit(context, "/graph/edges/metadata", StatusCodes.Status200OK, sw.ElapsedMilliseconds);
|
||||
@@ -419,7 +460,11 @@ app.MapGet("/graph/edges/by-reason/{reason}", async (string reason, int? limit,
|
||||
if (!Enum.TryParse<EdgeReason>(reason, ignoreCase: true, out var edgeReason))
|
||||
{
|
||||
LogAudit(context, "/graph/edges/by-reason", StatusCodes.Status400BadRequest, sw.ElapsedMilliseconds);
|
||||
return Results.BadRequest(new ErrorResponse { Error = "INVALID_REASON", Message = $"Unknown edge reason: {reason}" });
|
||||
return Results.BadRequest(new ErrorResponse
|
||||
{
|
||||
Error = "INVALID_REASON",
|
||||
Message = _tn("graph.error.invalid_reason", ("reason", reason))
|
||||
});
|
||||
}
|
||||
|
||||
var response = await service.QueryByReasonAsync(auth.TenantId!, edgeReason, limit ?? 100, cursor, ct);
|
||||
@@ -452,7 +497,7 @@ app.MapGet("/graph/edges/by-evidence", async (string evidenceType, string eviden
|
||||
app.MapGet("/healthz", () => Results.Ok(new { status = "ok" }));
|
||||
|
||||
app.TryRefreshStellaRouterEndpoints(routerEnabled);
|
||||
app.Run();
|
||||
await app.RunAsync().ConfigureAwait(false);
|
||||
|
||||
static async Task WriteError(HttpContext ctx, int status, string code, string message, CancellationToken ct)
|
||||
{
|
||||
@@ -487,7 +532,12 @@ static async Task<(bool Allowed, string? TenantId)> AuthorizeTenantRequestAsync(
|
||||
var authResult = await context.AuthenticateAsync(GraphHeaderAuthenticationHandler.SchemeName);
|
||||
if (!authResult.Succeeded || authResult.Principal?.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
await WriteError(context, StatusCodes.Status401Unauthorized, "GRAPH_UNAUTHORIZED", "Missing Authorization header", ct);
|
||||
await WriteError(
|
||||
context,
|
||||
StatusCodes.Status401Unauthorized,
|
||||
"GRAPH_UNAUTHORIZED",
|
||||
_t("graph.error.unauthorized_missing_auth"),
|
||||
ct);
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
@@ -495,7 +545,12 @@ static async Task<(bool Allowed, string? TenantId)> AuthorizeTenantRequestAsync(
|
||||
|
||||
if (!RateLimit(context, route))
|
||||
{
|
||||
await WriteError(context, StatusCodes.Status429TooManyRequests, "GRAPH_RATE_LIMITED", "Too many requests", ct);
|
||||
await WriteError(
|
||||
context,
|
||||
StatusCodes.Status429TooManyRequests,
|
||||
"GRAPH_RATE_LIMITED",
|
||||
_t("graph.error.rate_limited"),
|
||||
ct);
|
||||
LogAudit(context, route, StatusCodes.Status429TooManyRequests, elapsedMs);
|
||||
return (false, null);
|
||||
}
|
||||
@@ -514,8 +569,8 @@ static async Task<(bool Allowed, string? TenantId)> AuthorizeTenantRequestAsync(
|
||||
static string TranslateTenantResolutionError(string? tenantError)
|
||||
{
|
||||
return string.Equals(tenantError, "tenant_conflict", StringComparison.Ordinal)
|
||||
? "Conflicting tenant context"
|
||||
: $"Missing {StellaOpsHttpHeaderNames.Tenant} header";
|
||||
? _t("graph.error.tenant_conflict")
|
||||
: _tn("graph.error.tenant_missing_header", ("header", StellaOpsHttpHeaderNames.Tenant));
|
||||
}
|
||||
|
||||
static bool RateLimit(HttpContext ctx, string route)
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.ServerIntegration/StellaOps.Auth.ServerIntegration.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Localization/StellaOps.Localization.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Translations\*.json" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="StellaOpsReleaseVersion">
|
||||
<Version>1.0.0-alpha1</Version>
|
||||
|
||||
@@ -10,3 +10,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| AUDIT-0350-A | TODO | Pending approval (non-test project; revalidated 2026-01-07). |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
| SPRINT-20260222-058-GRAPH-TEN | DONE | `docs/implplan/SPRINT_20260222_058_Graph_tenant_resolution_and_auth_alignment.md`: migrated Graph endpoint tenant/scope checks to shared resolver + policy-driven authorization (tenant-aware limiter/audit included). |
|
||||
| SPRINT-20260224-002-LOC-101 | DONE | `SPRINT_20260224_002_Platform_translation_rollout_phase3_phase4.md`: adopted StellaOps localization runtime bundle loading in Graph API and localized selected edge/export validation messages (`en-US`/`de-DE`). |
|
||||
|
||||
11
src/Graph/StellaOps.Graph.Api/Translations/de-DE.graph.json
Normal file
11
src/Graph/StellaOps.Graph.Api/Translations/de-DE.graph.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"_meta": { "locale": "de-DE", "namespace": "graph", "version": "1.0" },
|
||||
|
||||
"graph.error.export_not_found": "Export-Auftrag wurde nicht gefunden",
|
||||
"graph.error.edge_not_found": "Kante '{edgeId}' wurde nicht gefunden",
|
||||
"graph.error.invalid_reason": "Unbekannter Kanten-Grund: {reason}",
|
||||
"graph.error.unauthorized_missing_auth": "Authorization-Header fehlt",
|
||||
"graph.error.rate_limited": "Zu viele Anfragen",
|
||||
"graph.error.tenant_conflict": "Widerspruechlicher Tenant-Kontext",
|
||||
"graph.error.tenant_missing_header": "Header {header} fehlt"
|
||||
}
|
||||
11
src/Graph/StellaOps.Graph.Api/Translations/en-US.graph.json
Normal file
11
src/Graph/StellaOps.Graph.Api/Translations/en-US.graph.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"_meta": { "locale": "en-US", "namespace": "graph", "version": "1.0" },
|
||||
|
||||
"graph.error.export_not_found": "Export job not found",
|
||||
"graph.error.edge_not_found": "Edge '{edgeId}' not found",
|
||||
"graph.error.invalid_reason": "Unknown edge reason: {reason}",
|
||||
"graph.error.unauthorized_missing_auth": "Missing Authorization header",
|
||||
"graph.error.rate_limited": "Too many requests",
|
||||
"graph.error.tenant_conflict": "Conflicting tenant context",
|
||||
"graph.error.tenant_missing_header": "Missing {header} header"
|
||||
}
|
||||
@@ -51,7 +51,7 @@ public sealed class EdgeMetadataEndpointsAuthorizationTests : IClassFixture<WebA
|
||||
var payload = await response.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
Assert.Contains("GRAPH_VALIDATION_FAILED", payload, StringComparison.Ordinal);
|
||||
Assert.Contains("tenant", payload, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -146,6 +146,26 @@ public sealed class EdgeMetadataEndpointsAuthorizationTests : IClassFixture<WebA
|
||||
Assert.Contains("EDGE_NOT_FOUND", payload, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Intent", "Safety")]
|
||||
public async Task EdgeMetadataGet_WithGermanLocale_UnknownEdge_ReturnsLocalizedMessage()
|
||||
{
|
||||
using var client = _factory.CreateClient();
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, "/graph/edges/ge:acme:missing/metadata");
|
||||
request.Headers.TryAddWithoutValidation("Authorization", "Bearer qa-token");
|
||||
request.Headers.TryAddWithoutValidation("X-Stella-Tenant", "acme");
|
||||
request.Headers.TryAddWithoutValidation("X-Stella-Scopes", "graph:read");
|
||||
request.Headers.TryAddWithoutValidation("X-Locale", "de-DE");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
var payload = await response.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
Assert.Contains("EDGE_NOT_FOUND", payload, StringComparison.Ordinal);
|
||||
Assert.Contains("wurde nicht gefunden", payload, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Intent", "Safety")]
|
||||
|
||||
@@ -93,7 +93,7 @@ public sealed class ExportEndpointsAuthorizationTests : IClassFixture<WebApplica
|
||||
var payload = await response.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
Assert.Contains("GRAPH_VALIDATION_FAILED", payload, StringComparison.Ordinal);
|
||||
Assert.Contains("tenant", payload, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static async Task<(string JobId, string DownloadUrl)> CreateExportJobAsync(HttpClient client, string tenant)
|
||||
|
||||
@@ -66,7 +66,7 @@ public sealed class GraphTenantAuthorizationAlignmentTests : IClassFixture<WebAp
|
||||
var payload = await response.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
Assert.Contains("GRAPH_VALIDATION_FAILED", payload, StringComparison.Ordinal);
|
||||
Assert.Contains("tenant", payload, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -14,3 +14,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| QA-GRAPH-RECHECK-005 | DONE | SPRINT_20260210_005: query/overlay API integration tests added to validate runtime data and explain-trace behavior. |
|
||||
| QA-GRAPH-RECHECK-006 | DONE | SPRINT_20260210_005: known-edge metadata positive-path integration test added to catch empty-runtime-data regressions. |
|
||||
| SPRINT-20260222-058-GRAPH-TEN-05 | DONE | `docs/implplan/SPRINT_20260222_058_Graph_tenant_resolution_and_auth_alignment.md`: added focused Graph tenant/auth alignment tests and executed Graph API test project evidence run (`73 passed`). |
|
||||
| SPRINT-20260224-002-LOC-101-T | DONE | `SPRINT_20260224_002_Platform_translation_rollout_phase3_phase4.md`: added focused Graph API locale-aware unknown-edge test and validated German localized error text. |
|
||||
|
||||
Reference in New Issue
Block a user