Refactor and enhance LDAP plugin configuration and validation
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Updated `LdapPluginOptions` to enforce TLS and client certificate requirements. - Added validation checks for TLS configuration in `LdapPluginOptionsTests`. - Improved error handling in `DirectoryServicesLdapConnectionFactory` for StartTLS negotiation. - Enhanced logging in `LdapCredentialStore` to include detailed audit properties for credential verification. - Introduced `StubStructuredRetriever` and `StubVectorRetriever` for testing in `ToolsetServiceCollectionExtensionsTests`. - Refactored `AdvisoryGuardrailPipelineTests` to improve test clarity and structure. - Added `FileSystemAdvisoryTaskQueueTests` for testing queue functionality. - Updated JSON test data for consistency with new requirements. - Modified `AdvisoryPipelineOrchestratorTests` to reflect changes in metadata keys.
This commit is contained in:
@@ -1,22 +1,52 @@
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.RateLimiting;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.AdvisoryAI.Caching;
|
||||
using StellaOps.AdvisoryAI.Hosting;
|
||||
using StellaOps.AdvisoryAI.Metrics;
|
||||
using StellaOps.AdvisoryAI.Orchestration;
|
||||
using StellaOps.AdvisoryAI.Queue;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Configuration.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
|
||||
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true)
|
||||
.AddEnvironmentVariables(prefix: "ADVISORYAI_");
|
||||
builder.Configuration
|
||||
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
|
||||
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true)
|
||||
.AddEnvironmentVariables(prefix: "ADVISORYAI_");
|
||||
|
||||
builder.Services.AddAdvisoryAiCore(builder.Configuration);
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
builder.Services.AddProblemDetails();
|
||||
builder.Services.AddRateLimiter(options =>
|
||||
{
|
||||
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
||||
options.AddPolicy("advisory-ai", context =>
|
||||
{
|
||||
var clientId = context.Request.Headers.TryGetValue("X-StellaOps-Client", out var value)
|
||||
? value.ToString()
|
||||
: "anonymous";
|
||||
|
||||
return RateLimitPartition.GetTokenBucketLimiter(
|
||||
clientId,
|
||||
_ => new TokenBucketRateLimiterOptions
|
||||
{
|
||||
TokenLimit = 30,
|
||||
TokensPerPeriod = 30,
|
||||
ReplenishmentPeriod = TimeSpan.FromMinutes(1),
|
||||
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
|
||||
QueueLimit = 0,
|
||||
AutoReplenishment = true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
@@ -26,35 +56,135 @@ app.UseExceptionHandler(static options => options.Run(async context =>
|
||||
await problem.ExecuteAsync(context);
|
||||
}));
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseRateLimiter();
|
||||
|
||||
app.MapGet("/health", () => Results.Ok(new { status = "ok" }));
|
||||
|
||||
app.MapPost("/v1/advisory-ai/pipeline/{taskType}", async (
|
||||
app.MapPost("/v1/advisory-ai/pipeline/{taskType}", HandleSinglePlan)
|
||||
.RequireRateLimiting("advisory-ai");
|
||||
|
||||
app.MapPost("/v1/advisory-ai/pipeline:batch", HandleBatchPlans)
|
||||
.RequireRateLimiting("advisory-ai");
|
||||
|
||||
app.Run();
|
||||
|
||||
static async Task<IResult> HandleSinglePlan(
|
||||
HttpContext httpContext,
|
||||
string taskType,
|
||||
PipelinePlanRequest request,
|
||||
IAdvisoryPipelineOrchestrator orchestrator,
|
||||
IAdvisoryPipelineQueuePublisher queue,
|
||||
AdvisoryAiMetrics metrics,
|
||||
ILoggerFactory loggerFactory,
|
||||
CancellationToken cancellationToken) =>
|
||||
IAdvisoryPlanCache planCache,
|
||||
IAdvisoryTaskQueue taskQueue,
|
||||
AdvisoryAiMetrics requestMetrics,
|
||||
AdvisoryPipelineMetrics pipelineMetrics,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!Enum.TryParse<AdvisoryTaskType>(taskType, ignoreCase: true, out var parsedType))
|
||||
{
|
||||
return Results.BadRequest(new { error = $"Unknown task type {taskType}." });
|
||||
return Results.BadRequest(new { error = $"Unknown task type '{taskType}'." });
|
||||
}
|
||||
|
||||
var httpRequest = request with { TaskType = parsedType };
|
||||
var orchestratorRequest = httpRequest.ToTaskRequest();
|
||||
if (!EnsureAuthorized(httpContext, parsedType))
|
||||
{
|
||||
return Results.StatusCode(StatusCodes.Status403Forbidden);
|
||||
}
|
||||
|
||||
var plan = await orchestrator.CreatePlanAsync(orchestratorRequest, cancellationToken).ConfigureAwait(false);
|
||||
metrics.RecordRequest(plan.Request.TaskType.ToString());
|
||||
if (string.IsNullOrWhiteSpace(request.AdvisoryKey))
|
||||
{
|
||||
return Results.BadRequest(new { error = "AdvisoryKey is required." });
|
||||
}
|
||||
|
||||
await queue.EnqueueAsync(new AdvisoryPipelineExecutionMessage(plan.CacheKey, plan.Request, plan.Metadata), cancellationToken).ConfigureAwait(false);
|
||||
metrics.RecordEnqueued(plan.Request.TaskType.ToString());
|
||||
var normalizedRequest = request with { TaskType = parsedType };
|
||||
var taskRequest = normalizedRequest.ToTaskRequest();
|
||||
var plan = await orchestrator.CreatePlanAsync(taskRequest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(AdvisoryPipelinePlanResponse.FromPlan(plan));
|
||||
});
|
||||
await planCache.SetAsync(plan.CacheKey, plan, cancellationToken).ConfigureAwait(false);
|
||||
await taskQueue.EnqueueAsync(new AdvisoryTaskQueueMessage(plan.CacheKey, plan.Request), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
app.Run();
|
||||
requestMetrics.RecordRequest(plan.Request.TaskType.ToString());
|
||||
requestMetrics.RecordEnqueued(plan.Request.TaskType.ToString());
|
||||
pipelineMetrics.RecordPlanQueued(plan.Request.TaskType);
|
||||
|
||||
var response = AdvisoryPipelinePlanResponse.FromPlan(plan);
|
||||
return Results.Ok(response);
|
||||
}
|
||||
|
||||
static async Task<IResult> HandleBatchPlans(
|
||||
HttpContext httpContext,
|
||||
BatchPipelinePlanRequest batchRequest,
|
||||
IAdvisoryPipelineOrchestrator orchestrator,
|
||||
IAdvisoryPlanCache planCache,
|
||||
IAdvisoryTaskQueue taskQueue,
|
||||
AdvisoryAiMetrics requestMetrics,
|
||||
AdvisoryPipelineMetrics pipelineMetrics,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (batchRequest.Requests.Count == 0)
|
||||
{
|
||||
return Results.BadRequest(new { error = "At least one request must be supplied." });
|
||||
}
|
||||
|
||||
var results = new List<AdvisoryPipelinePlanResponse>(batchRequest.Requests.Count);
|
||||
|
||||
foreach (var item in batchRequest.Requests)
|
||||
{
|
||||
var taskType = item.TaskType?.ToString() ?? "summary";
|
||||
if (!Enum.TryParse<AdvisoryTaskType>(taskType, ignoreCase: true, out var parsedType))
|
||||
{
|
||||
return Results.BadRequest(new { error = $"Unknown task type '{taskType}' in batch item." });
|
||||
}
|
||||
|
||||
if (!EnsureAuthorized(httpContext, parsedType))
|
||||
{
|
||||
return Results.StatusCode(StatusCodes.Status403Forbidden);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(item.AdvisoryKey))
|
||||
{
|
||||
return Results.BadRequest(new { error = "AdvisoryKey is required for every batch item." });
|
||||
}
|
||||
|
||||
var normalizedRequest = item with { TaskType = parsedType };
|
||||
var taskRequest = normalizedRequest.ToTaskRequest();
|
||||
var plan = await orchestrator.CreatePlanAsync(taskRequest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await planCache.SetAsync(plan.CacheKey, plan, cancellationToken).ConfigureAwait(false);
|
||||
await taskQueue.EnqueueAsync(new AdvisoryTaskQueueMessage(plan.CacheKey, plan.Request), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
requestMetrics.RecordRequest(plan.Request.TaskType.ToString());
|
||||
requestMetrics.RecordEnqueued(plan.Request.TaskType.ToString());
|
||||
pipelineMetrics.RecordPlanQueued(plan.Request.TaskType);
|
||||
|
||||
results.Add(AdvisoryPipelinePlanResponse.FromPlan(plan));
|
||||
}
|
||||
|
||||
return Results.Ok(results);
|
||||
}
|
||||
|
||||
static bool EnsureAuthorized(HttpContext context, AdvisoryTaskType taskType)
|
||||
{
|
||||
if (!context.Request.Headers.TryGetValue("X-StellaOps-Scopes", out var scopes))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var allowed = scopes
|
||||
.SelectMany(value => value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (allowed.Contains("advisory:run"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return allowed.Contains($"advisory:{taskType.ToString().ToLowerInvariant()}");
|
||||
}
|
||||
|
||||
internal sealed record PipelinePlanRequest(
|
||||
AdvisoryTaskType? TaskType,
|
||||
@@ -84,3 +214,8 @@ internal sealed record PipelinePlanRequest(
|
||||
ForceRefresh);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record BatchPipelinePlanRequest
|
||||
{
|
||||
public IReadOnlyList<PipelinePlanRequest> Requests { get; init; } = Array.Empty<PipelinePlanRequest>();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user