compose and authority fixes. finish sprints.

This commit is contained in:
master
2026-02-17 21:59:47 +02:00
parent fb46a927ad
commit 49cdebe2f1
187 changed files with 23189 additions and 1439 deletions

View File

@@ -26,6 +26,7 @@ public static class PlatformEndpoints
MapPreferencesEndpoints(platform);
MapSearchEndpoints(app, platform);
MapMetadataEndpoints(platform);
MapLegacyQuotaCompatibilityEndpoints(app);
return app;
}
@@ -472,6 +473,402 @@ public static class PlatformEndpoints
}).RequireAuthorization(PlatformPolicies.MetadataRead);
}
private static void MapLegacyQuotaCompatibilityEndpoints(IEndpointRouteBuilder app)
{
var quotas = app.MapGroup("/api/v1/authority/quotas")
.WithTags("Platform Quotas Compatibility");
quotas.MapGet(string.Empty, async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
PlatformQuotaService service,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var summary = await service.GetSummaryAsync(requestContext!, cancellationToken).ConfigureAwait(false);
return Results.Ok(BuildLegacyEntitlement(summary.Value, requestContext!));
}).RequireAuthorization(PlatformPolicies.QuotaRead);
quotas.MapGet("/consumption", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
PlatformQuotaService service,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var summary = await service.GetSummaryAsync(requestContext!, cancellationToken).ConfigureAwait(false);
return Results.Ok(BuildLegacyConsumption(summary.Value));
}).RequireAuthorization(PlatformPolicies.QuotaRead);
quotas.MapGet("/dashboard", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
PlatformQuotaService service,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var summary = await service.GetSummaryAsync(requestContext!, cancellationToken).ConfigureAwait(false);
return Results.Ok(new
{
entitlement = BuildLegacyEntitlement(summary.Value, requestContext!),
consumption = BuildLegacyConsumption(summary.Value),
tenantCount = 1,
activeAlerts = 0,
recentViolations = 0
});
}).RequireAuthorization(PlatformPolicies.QuotaRead);
quotas.MapGet("/history", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
PlatformQuotaService service,
[FromQuery] string? categories,
[FromQuery] string? aggregation,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var summary = await service.GetSummaryAsync(requestContext!, cancellationToken).ConfigureAwait(false);
var now = DateTimeOffset.UtcNow;
var selected = string.IsNullOrWhiteSpace(categories)
? null
: categories.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var points = BuildLegacyConsumption(summary.Value)
.Where(item => selected is null || selected.Contains(item.Category, StringComparer.OrdinalIgnoreCase))
.Select(item => new
{
timestamp = now.ToString("o"),
category = item.Category,
value = item.Current,
percentage = item.Percentage
})
.ToArray();
return Results.Ok(new
{
period = new
{
start = now.AddDays(-30).ToString("o"),
end = now.ToString("o")
},
points,
aggregation = string.IsNullOrWhiteSpace(aggregation) ? "daily" : aggregation
});
}).RequireAuthorization(PlatformPolicies.QuotaRead);
quotas.MapGet("/tenants", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
PlatformQuotaService service,
[FromQuery] int limit,
[FromQuery] int offset,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var summary = await service.GetSummaryAsync(requestContext!, cancellationToken).ConfigureAwait(false);
var consumption = BuildLegacyConsumption(summary.Value);
var now = DateTimeOffset.UtcNow;
var item = new
{
tenantId = requestContext!.TenantId,
tenantName = "Default Tenant",
planName = "Local Development",
quotas = new
{
license = GetLegacyQuota(consumption, "license"),
jobs = GetLegacyQuota(consumption, "jobs"),
api = GetLegacyQuota(consumption, "api"),
storage = GetLegacyQuota(consumption, "storage")
},
trend = "stable",
trendPercentage = 0,
lastActivity = now.ToString("o")
};
var items = new[] { item }
.Skip(Math.Max(0, offset))
.Take(limit > 0 ? limit : 50)
.ToArray();
return Results.Ok(new { items, total = 1 });
}).RequireAuthorization(PlatformPolicies.QuotaRead);
quotas.MapGet("/tenants/{tenantId}", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
PlatformQuotaService service,
string tenantId,
CancellationToken cancellationToken) =>
{
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
{
return failure!;
}
var result = await service.GetTenantAsync(tenantId, cancellationToken).ConfigureAwait(false);
var consumption = BuildLegacyConsumption(result.Value);
return Results.Ok(new
{
tenantId,
tenantName = "Default Tenant",
planName = "Local Development",
licensePeriod = new
{
start = DateTimeOffset.UtcNow.AddDays(-30).ToString("o"),
end = DateTimeOffset.UtcNow.AddDays(30).ToString("o")
},
quotaDetails = new
{
artifacts = BuildLegacyLimit(consumption, "license", 100000),
users = BuildLegacyLimit(consumption, "license", 25),
scansPerDay = BuildLegacyLimit(consumption, "jobs", 1000),
storageMb = BuildLegacyLimit(consumption, "storage", 5000),
concurrentJobs = BuildLegacyLimit(consumption, "jobs", 20)
},
usageByResourceType = new[]
{
new { type = "api", percentage = GetLegacyQuota(consumption, "api").Percentage },
new { type = "jobs", percentage = GetLegacyQuota(consumption, "jobs").Percentage },
new { type = "storage", percentage = GetLegacyQuota(consumption, "storage").Percentage }
},
forecast = BuildLegacyForecast("api")
});
}).RequireAuthorization(PlatformPolicies.QuotaRead);
quotas.MapGet("/forecast", async Task<IResult> (
HttpContext context,
PlatformRequestContextResolver resolver,
[FromQuery] string? category) =>
{
if (!TryResolveContext(context, resolver, out _, out var failure))
{
return failure!;
}
var categories = string.IsNullOrWhiteSpace(category)
? new[] { "license", "jobs", "api", "storage" }
: new[] { category.Trim().ToLowerInvariant() };
var forecasts = categories.Select(BuildLegacyForecast).ToArray();
return Results.Ok(forecasts);
}).RequireAuthorization(PlatformPolicies.QuotaRead);
quotas.MapGet("/alerts", (HttpContext context, PlatformRequestContextResolver resolver) =>
{
if (!TryResolveContext(context, resolver, out _, out var failure))
{
return Task.FromResult(failure!);
}
return Task.FromResult<IResult>(Results.Ok(new
{
thresholds = new[]
{
new { category = "license", enabled = true, warningThreshold = 75, criticalThreshold = 90 },
new { category = "jobs", enabled = true, warningThreshold = 75, criticalThreshold = 90 },
new { category = "api", enabled = true, warningThreshold = 80, criticalThreshold = 95 },
new { category = "storage", enabled = true, warningThreshold = 80, criticalThreshold = 95 }
},
channels = Array.Empty<object>(),
escalationMinutes = 30
}));
}).RequireAuthorization(PlatformPolicies.QuotaRead);
quotas.MapPost("/alerts", (HttpContext context, PlatformRequestContextResolver resolver, [FromBody] object config) =>
{
if (!TryResolveContext(context, resolver, out _, out var failure))
{
return Task.FromResult(failure!);
}
return Task.FromResult<IResult>(Results.Ok(config));
}).RequireAuthorization(PlatformPolicies.QuotaAdmin);
var rateLimits = app.MapGroup("/api/v1/gateway/rate-limits")
.WithTags("Platform Gateway Compatibility");
rateLimits.MapGet(string.Empty, (HttpContext context, PlatformRequestContextResolver resolver) =>
{
if (!TryResolveContext(context, resolver, out _, out var failure))
{
return Task.FromResult(failure!);
}
return Task.FromResult<IResult>(Results.Ok(new[]
{
new
{
endpoint = "/api/v1/release-orchestrator/dashboard",
method = "GET",
limit = 600,
remaining = 599,
resetAt = DateTimeOffset.UtcNow.AddMinutes(1).ToString("o"),
burstLimit = 120,
burstRemaining = 119
}
}));
}).RequireAuthorization(PlatformPolicies.QuotaRead);
rateLimits.MapGet("/violations", (HttpContext context, PlatformRequestContextResolver resolver) =>
{
if (!TryResolveContext(context, resolver, out _, out var failure))
{
return Task.FromResult(failure!);
}
var now = DateTimeOffset.UtcNow;
return Task.FromResult<IResult>(Results.Ok(new
{
items = Array.Empty<object>(),
total = 0,
period = new
{
start = now.AddDays(-1).ToString("o"),
end = now.ToString("o")
}
}));
}).RequireAuthorization(PlatformPolicies.QuotaRead);
}
private static LegacyQuotaItem[] BuildLegacyConsumption(IReadOnlyList<PlatformQuotaUsage> usage)
{
var now = DateTimeOffset.UtcNow.ToString("o");
var map = usage
.ToDictionary(item => ToLegacyCategory(item.QuotaId), item => item, StringComparer.OrdinalIgnoreCase);
return new[]
{
BuildLegacyConsumptionItem("license", map.GetValueOrDefault("license"), 100m, 27m, now),
BuildLegacyConsumptionItem("jobs", map.GetValueOrDefault("jobs"), 1000m, 120m, now),
BuildLegacyConsumptionItem("api", map.GetValueOrDefault("api"), 100000m, 23000m, now),
BuildLegacyConsumptionItem("storage", map.GetValueOrDefault("storage"), 5000m, 2400m, now)
};
}
private static object BuildLegacyEntitlement(IReadOnlyList<PlatformQuotaUsage> usage, PlatformRequestContext context)
{
return new
{
planId = $"local-{context.TenantId}",
planName = "Local Development",
features = new[] { "control-plane", "policy", "security", "operations" },
limits = new
{
artifacts = 100000,
users = 25,
scansPerDay = 1000,
storageMb = 5000,
concurrentJobs = 20,
apiRequestsPerMinute = 600
},
validFrom = DateTimeOffset.UtcNow.AddDays(-30).ToString("o"),
validTo = DateTimeOffset.UtcNow.AddDays(30).ToString("o")
};
}
private static LegacyQuotaItem BuildLegacyConsumptionItem(string category, PlatformQuotaUsage? usage, decimal fallbackLimit, decimal fallbackUsed, string now)
{
var limit = usage?.Limit ?? fallbackLimit;
var current = usage?.Used ?? fallbackUsed;
var percentage = limit <= 0 ? 0 : Math.Round((current / limit) * 100m, 1);
return new LegacyQuotaItem(
category,
current,
limit,
percentage,
GetLegacyStatus(percentage),
"stable",
0,
now);
}
private static LegacyQuotaItem GetLegacyQuota(LegacyQuotaItem[] items, string category)
{
return items.First(item => string.Equals(item.Category, category, StringComparison.OrdinalIgnoreCase));
}
private static object BuildLegacyLimit(LegacyQuotaItem[] items, string category, decimal hardLimit)
{
var quota = GetLegacyQuota(items, category);
var current = quota.Current;
var limit = Math.Max(hardLimit, quota.Limit);
var percentage = limit <= 0 ? 0 : Math.Round((current / limit) * 100m, 1);
return new
{
current,
limit,
percentage
};
}
private static object BuildLegacyForecast(string category)
{
return new
{
category,
exhaustionDays = 45,
confidence = 0.82,
trendSlope = 0.04,
recommendation = "Current usage is stable. Keep existing quota policy.",
severity = "info"
};
}
private static string GetLegacyStatus(decimal percentage)
{
return percentage switch
{
>= 100m => "exceeded",
>= 90m => "critical",
>= 75m => "warning",
_ => "healthy"
};
}
private static string ToLegacyCategory(string quotaId)
{
if (quotaId.Contains("gateway", StringComparison.OrdinalIgnoreCase))
{
return "api";
}
if (quotaId.Contains("jobs", StringComparison.OrdinalIgnoreCase))
{
return "jobs";
}
if (quotaId.Contains("storage", StringComparison.OrdinalIgnoreCase))
{
return "storage";
}
return "license";
}
private static bool TryResolveContext(
HttpContext context,
PlatformRequestContextResolver resolver,
@@ -488,6 +885,16 @@ public static class PlatformEndpoints
return false;
}
private sealed record LegacyQuotaItem(
string Category,
decimal Current,
decimal Limit,
decimal Percentage,
string Status,
string Trend,
decimal TrendPercentage,
string LastUpdated);
private sealed record SearchQuery(
[FromQuery(Name = "q")] string? Query,
string? Sources,

View File

@@ -73,26 +73,34 @@ builder.Services.AddStellaOpsResourceServerAuthentication(
resourceOptions.RequireHttpsMetadata = bootstrapOptions.Authority.RequireHttpsMetadata;
resourceOptions.MetadataAddress = bootstrapOptions.Authority.MetadataAddress;
// Read collections directly from IConfiguration to work around
// .NET Configuration.Bind() not populating IList<string> in nested init objects.
var authoritySection = builder.Configuration.GetSection("Platform:Authority");
var audiences = authoritySection.GetSection("Audiences").Get<string[]>() ?? [];
resourceOptions.Audiences.Clear();
foreach (var audience in bootstrapOptions.Authority.Audiences)
foreach (var audience in audiences)
{
resourceOptions.Audiences.Add(audience);
}
var requiredScopes = authoritySection.GetSection("RequiredScopes").Get<string[]>() ?? [];
resourceOptions.RequiredScopes.Clear();
foreach (var scope in bootstrapOptions.Authority.RequiredScopes)
foreach (var scope in requiredScopes)
{
resourceOptions.RequiredScopes.Add(scope);
}
var requiredTenants = authoritySection.GetSection("RequiredTenants").Get<string[]>() ?? [];
resourceOptions.RequiredTenants.Clear();
foreach (var tenant in bootstrapOptions.Authority.RequiredTenants)
foreach (var tenant in requiredTenants)
{
resourceOptions.RequiredTenants.Add(tenant);
}
var bypassNetworks = authoritySection.GetSection("BypassNetworks").Get<string[]>() ?? [];
resourceOptions.BypassNetworks.Clear();
foreach (var network in bootstrapOptions.Authority.BypassNetworks)
foreach (var network in bypassNetworks)
{
resourceOptions.BypassNetworks.Add(network);
}

View File

@@ -86,8 +86,8 @@ public sealed class PlatformAnalyticsQueryExecutor : IPlatformAnalyticsQueryExec
SUM(total_vulns) - SUM(vex_mitigated) AS net_exposure,
SUM(kev_vulns) AS kev_vulns
FROM analytics.daily_vulnerability_counts
WHERE snapshot_date >= CURRENT_DATE - make_interval(days => @days)
AND (@environment IS NULL OR environment = @environment)
WHERE snapshot_date >= CURRENT_DATE - (@days::int * INTERVAL '1 day')
AND (@environment::text IS NULL OR environment = @environment::text)
GROUP BY snapshot_date, environment
ORDER BY environment, snapshot_date;
""";
@@ -134,8 +134,8 @@ public sealed class PlatformAnalyticsQueryExecutor : IPlatformAnalyticsQueryExec
SUM(total_components) AS total_components,
SUM(unique_suppliers) AS unique_suppliers
FROM analytics.daily_component_counts
WHERE snapshot_date >= CURRENT_DATE - make_interval(days => @days)
AND (@environment IS NULL OR environment = @environment)
WHERE snapshot_date >= CURRENT_DATE - (@days::int * INTERVAL '1 day')
AND (@environment::text IS NULL OR environment = @environment::text)
GROUP BY snapshot_date, environment
ORDER BY environment, snapshot_date;
""";