compose and authority fixes. finish sprints.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
""";
|
||||
|
||||
Reference in New Issue
Block a user