wip: doctor/cli/docs/api to vector db consolidation; api hardening for descriptions, tenant, and scopes; migrations and conversions of all DALs to EF v10
This commit is contained in:
@@ -124,7 +124,7 @@ public sealed class IdentityHeaderPolicyMiddlewareTests
|
||||
#region Header Overwriting (Not Set-If-Missing)
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_OverwritesSpoofedTenantWithClaimValue()
|
||||
public async Task InvokeAsync_RejectsSpoofedTenantHeaderWhenOverrideDisabled()
|
||||
{
|
||||
var middleware = CreateMiddleware();
|
||||
var claims = new[]
|
||||
@@ -133,15 +133,15 @@ public sealed class IdentityHeaderPolicyMiddlewareTests
|
||||
new Claim(StellaOpsClaimTypes.Subject, "real-subject")
|
||||
};
|
||||
var context = CreateHttpContext("/api/scan", claims);
|
||||
context.Response.Body = new MemoryStream();
|
||||
|
||||
// Client attempts to spoof tenant
|
||||
context.Request.Headers["X-StellaOps-Tenant"] = "spoofed-tenant";
|
||||
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
Assert.True(_nextCalled);
|
||||
// Header should contain claim value, not spoofed value
|
||||
Assert.Equal("real-tenant", context.Request.Headers["X-StellaOps-Tenant"].ToString());
|
||||
Assert.False(_nextCalled);
|
||||
Assert.Equal(StatusCodes.Status403Forbidden, context.Response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -225,6 +225,7 @@ public sealed class IdentityHeaderPolicyMiddlewareTests
|
||||
|
||||
Assert.True(_nextCalled);
|
||||
Assert.Equal("tenant-abc", context.Request.Headers["X-StellaOps-Tenant"].ToString());
|
||||
Assert.Equal("tenant-abc", context.Request.Headers["X-Tenant-Id"].ToString());
|
||||
Assert.Equal("tenant-abc", context.Items[GatewayContextKeys.TenantId]);
|
||||
}
|
||||
|
||||
@@ -243,6 +244,25 @@ public sealed class IdentityHeaderPolicyMiddlewareTests
|
||||
|
||||
Assert.True(_nextCalled);
|
||||
Assert.Equal("legacy-tenant-456", context.Request.Headers["X-StellaOps-Tenant"].ToString());
|
||||
Assert.Equal("legacy-tenant-456", context.Request.Headers["X-Tenant-Id"].ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_AuthenticatedRequestWithoutTenantClaim_DoesNotWriteTenantHeaders()
|
||||
{
|
||||
var middleware = CreateMiddleware();
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(StellaOpsClaimTypes.Subject, "user")
|
||||
};
|
||||
var context = CreateHttpContext("/api/scan", claims);
|
||||
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
Assert.True(_nextCalled);
|
||||
Assert.DoesNotContain("X-StellaOps-Tenant", context.Request.Headers.Keys);
|
||||
Assert.DoesNotContain("X-Stella-Tenant", context.Request.Headers.Keys);
|
||||
Assert.DoesNotContain("X-Tenant-Id", context.Request.Headers.Keys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -308,6 +328,109 @@ public sealed class IdentityHeaderPolicyMiddlewareTests
|
||||
|
||||
#endregion
|
||||
|
||||
#region Tenant Override
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_OverrideEnabledAndAllowed_UsesRequestedTenant()
|
||||
{
|
||||
_options.EnableTenantOverride = true;
|
||||
var middleware = CreateMiddleware();
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(StellaOpsClaimTypes.Subject, "user"),
|
||||
new Claim(StellaOpsClaimTypes.Tenant, "tenant-a"),
|
||||
new Claim(StellaOpsClaimTypes.AllowedTenants, "tenant-a tenant-b")
|
||||
};
|
||||
var context = CreateHttpContext("/api/platform", claims);
|
||||
|
||||
context.Request.Headers["X-StellaOps-Tenant"] = "TENANT-B";
|
||||
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
Assert.True(_nextCalled);
|
||||
Assert.Equal("tenant-b", context.Request.Headers["X-StellaOps-Tenant"].ToString());
|
||||
Assert.Equal("tenant-b", context.Request.Headers["X-Tenant-Id"].ToString());
|
||||
Assert.Equal("tenant-b", context.Items[GatewayContextKeys.TenantId]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_OverrideEnabledButNotAllowed_ReturnsForbidden()
|
||||
{
|
||||
_options.EnableTenantOverride = true;
|
||||
var middleware = CreateMiddleware();
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(StellaOpsClaimTypes.Subject, "user"),
|
||||
new Claim(StellaOpsClaimTypes.Tenant, "tenant-a"),
|
||||
new Claim(StellaOpsClaimTypes.AllowedTenants, "tenant-a tenant-c")
|
||||
};
|
||||
var context = CreateHttpContext("/api/platform", claims);
|
||||
context.Response.Body = new MemoryStream();
|
||||
context.Request.Headers["X-StellaOps-Tenant"] = "tenant-b";
|
||||
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
Assert.False(_nextCalled);
|
||||
Assert.Equal(StatusCodes.Status403Forbidden, context.Response.StatusCode);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Auth Header Passthrough
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_PreservesAuthorizationHeadersForApprovedConfiguredPrefix()
|
||||
{
|
||||
_options.JwtPassthroughPrefixes = ["/connect"];
|
||||
_options.ApprovedAuthPassthroughPrefixes = ["/connect", "/console"];
|
||||
var middleware = CreateMiddleware();
|
||||
var context = CreateHttpContext("/connect/token");
|
||||
context.Request.Headers.Authorization = "Bearer token-value";
|
||||
context.Request.Headers["DPoP"] = "proof-value";
|
||||
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
Assert.True(_nextCalled);
|
||||
Assert.Equal("Bearer token-value", context.Request.Headers.Authorization.ToString());
|
||||
Assert.Equal("proof-value", context.Request.Headers["DPoP"].ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_StripsAuthorizationHeadersWhenConfiguredPrefixIsNotApproved()
|
||||
{
|
||||
_options.JwtPassthroughPrefixes = ["/api/v1/authority"];
|
||||
_options.ApprovedAuthPassthroughPrefixes = ["/connect"];
|
||||
var middleware = CreateMiddleware();
|
||||
var context = CreateHttpContext("/api/v1/authority/clients");
|
||||
context.Request.Headers.Authorization = "Bearer token-value";
|
||||
context.Request.Headers["DPoP"] = "proof-value";
|
||||
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
Assert.True(_nextCalled);
|
||||
Assert.False(context.Request.Headers.ContainsKey("Authorization"));
|
||||
Assert.False(context.Request.Headers.ContainsKey("DPoP"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_StripsAuthorizationHeadersWhenPrefixIsNotConfigured()
|
||||
{
|
||||
_options.JwtPassthroughPrefixes = [];
|
||||
_options.ApprovedAuthPassthroughPrefixes = ["/connect"];
|
||||
var middleware = CreateMiddleware();
|
||||
var context = CreateHttpContext("/connect/token");
|
||||
context.Request.Headers.Authorization = "Bearer token-value";
|
||||
context.Request.Headers["DPoP"] = "proof-value";
|
||||
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
Assert.True(_nextCalled);
|
||||
Assert.False(context.Request.Headers.ContainsKey("Authorization"));
|
||||
Assert.False(context.Request.Headers.ContainsKey("DPoP"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Legacy Header Compatibility
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -464,7 +464,7 @@ public sealed class OpenApiDocumentGeneratorTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateDocument_WithoutExplicitDescription_UsesSummaryAsDescriptionFallback()
|
||||
public void GenerateDocument_WithoutExplicitDescription_LeavesDescriptionUnset()
|
||||
{
|
||||
var endpoint = new EndpointDescriptor
|
||||
{
|
||||
@@ -485,7 +485,37 @@ public sealed class OpenApiDocumentGeneratorTests
|
||||
var operation = document["paths"]!["/api/v1/timeline"]!["get"]!.AsObject();
|
||||
|
||||
operation["summary"]!.GetValue<string>().Should().Be("GET /api/v1/timeline");
|
||||
operation["description"]!.GetValue<string>().Should().Be("GET /api/v1/timeline");
|
||||
operation["description"].Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateDocument_WithSchemaDescription_PreservesEndpointDescription()
|
||||
{
|
||||
var endpoint = new EndpointDescriptor
|
||||
{
|
||||
ServiceName = "timelineindexer",
|
||||
Version = "1.0.0",
|
||||
Method = "GET",
|
||||
Path = "/api/v1/timeline",
|
||||
SchemaInfo = new EndpointSchemaInfo
|
||||
{
|
||||
Summary = "Get timeline",
|
||||
Description = "Return the timeline entries in reverse chronological order."
|
||||
}
|
||||
};
|
||||
|
||||
var routingState = new Mock<IGlobalRoutingState>();
|
||||
routingState.Setup(state => state.GetAllConnections()).Returns([CreateConnection("timelineindexer", endpoint)]);
|
||||
|
||||
var generator = new OpenApiDocumentGenerator(
|
||||
routingState.Object,
|
||||
Options.Create(new OpenApiAggregationOptions()));
|
||||
|
||||
var document = JsonNode.Parse(generator.GenerateDocument())!.AsObject();
|
||||
var operation = document["paths"]!["/api/v1/timeline"]!["get"]!.AsObject();
|
||||
|
||||
operation["summary"]!.GetValue<string>().Should().Be("Get timeline");
|
||||
operation["description"]!.GetValue<string>().Should().Be("Return the timeline entries in reverse chronological order.");
|
||||
}
|
||||
|
||||
private static ConnectionState CreateConnection(
|
||||
|
||||
Reference in New Issue
Block a user