up the blokcing tasks
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Risk Bundle CI / risk-bundle-build (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Risk Bundle CI / risk-bundle-offline-kit (push) Has been cancelled
Risk Bundle CI / publish-checksums (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Risk Bundle CI / risk-bundle-build (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Risk Bundle CI / risk-bundle-offline-kit (push) Has been cancelled
Risk Bundle CI / publish-checksums (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
This commit is contained in:
@@ -3,6 +3,7 @@ using System.Collections.Immutable;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@@ -11,7 +12,9 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using StellaOps.Notifier.WebService.Contracts;
|
||||
using StellaOps.Notifier.WebService.Services;
|
||||
using StellaOps.Notifier.WebService.Setup;
|
||||
using StellaOps.Notifier.WebService.Extensions;
|
||||
using StellaOps.Notifier.WebService.Storage.Compat;
|
||||
using StellaOps.Notifier.Worker.Channels;
|
||||
using StellaOps.Notifier.Worker.Security;
|
||||
using StellaOps.Notifier.Worker.StormBreaker;
|
||||
using StellaOps.Notifier.Worker.DeadLetter;
|
||||
@@ -19,18 +22,16 @@ using StellaOps.Notifier.Worker.Retention;
|
||||
using StellaOps.Notifier.Worker.Observability;
|
||||
using StellaOps.Notifier.WebService.Endpoints;
|
||||
using StellaOps.Notifier.WebService.Setup;
|
||||
using StellaOps.Notifier.Worker.Dispatch;
|
||||
using StellaOps.Notifier.Worker.Escalation;
|
||||
using StellaOps.Notifier.Worker.Observability;
|
||||
using StellaOps.Notifier.Worker.Security;
|
||||
using StellaOps.Notifier.Worker.StormBreaker;
|
||||
using StellaOps.Notifier.Worker.Templates;
|
||||
using StellaOps.Notifier.Worker.Tenancy;
|
||||
using StellaOps.Notify.Storage.Mongo;
|
||||
using StellaOps.Notify.Storage.Mongo.Documents;
|
||||
using StellaOps.Notify.Storage.Mongo.Repositories;
|
||||
using StellaOps.Notifier.Worker.Templates;
|
||||
using DeadLetterStatus = StellaOps.Notifier.Worker.DeadLetter.DeadLetterStatus;
|
||||
using Contracts = StellaOps.Notifier.WebService.Contracts;
|
||||
using WorkerTemplateService = StellaOps.Notifier.Worker.Templates.INotifyTemplateService;
|
||||
using WorkerTemplateRenderer = StellaOps.Notifier.Worker.Dispatch.INotifyTemplateRenderer;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notify.Queue;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -42,44 +43,28 @@ builder.Configuration
|
||||
|
||||
builder.Services.AddSingleton<TimeProvider>(TimeProvider.System);
|
||||
|
||||
if (!isTesting)
|
||||
{
|
||||
var mongoSection = builder.Configuration.GetSection("notifier:storage:mongo");
|
||||
builder.Services.AddNotifyMongoStorage(mongoSection);
|
||||
builder.Services.AddHostedService<MongoInitializationHostedService>();
|
||||
builder.Services.AddHostedService<PackApprovalTemplateSeeder>();
|
||||
builder.Services.AddHostedService<AttestationTemplateSeeder>();
|
||||
builder.Services.AddHostedService<RiskTemplateSeeder>();
|
||||
}
|
||||
|
||||
// Fallback no-op event queue for environments that do not configure a real backend.
|
||||
builder.Services.TryAddSingleton<INotifyEventQueue, NullNotifyEventQueue>();
|
||||
|
||||
// Template service with advanced renderer
|
||||
builder.Services.AddSingleton<INotifyTemplateRenderer, AdvancedTemplateRenderer>();
|
||||
builder.Services.AddScoped<INotifyTemplateService, NotifyTemplateService>();
|
||||
// In-memory storage (document store removed)
|
||||
builder.Services.AddSingleton<INotifyChannelRepository, InMemoryNotifyRepositories>();
|
||||
builder.Services.AddSingleton<INotifyRuleRepository, InMemoryNotifyRepositories>();
|
||||
builder.Services.AddSingleton<INotifyTemplateRepository, InMemoryNotifyRepositories>();
|
||||
builder.Services.AddSingleton<INotifyDeliveryRepository, InMemoryNotifyRepositories>();
|
||||
builder.Services.AddSingleton<INotifyAuditRepository, InMemoryNotifyRepositories>();
|
||||
builder.Services.AddSingleton<INotifyLockRepository, InMemoryNotifyRepositories>();
|
||||
builder.Services.AddSingleton<IInAppInboxStore, InMemoryInboxStore>();
|
||||
builder.Services.AddSingleton<INotifyInboxRepository, InMemoryInboxStore>();
|
||||
builder.Services.AddSingleton<INotifyLocalizationRepository, InMemoryNotifyRepositories>();
|
||||
builder.Services.AddSingleton<INotifyPackApprovalRepository, InMemoryPackApprovalRepository>();
|
||||
builder.Services.AddSingleton<INotifyThrottleConfigRepository, InMemoryThrottleConfigRepository>();
|
||||
builder.Services.AddSingleton<INotifyOperatorOverrideRepository, InMemoryOperatorOverrideRepository>();
|
||||
builder.Services.AddSingleton<INotifyQuietHoursRepository, InMemoryQuietHoursRepository>();
|
||||
builder.Services.AddSingleton<INotifyMaintenanceWindowRepository, InMemoryMaintenanceWindowRepository>();
|
||||
builder.Services.AddSingleton<INotifyEscalationPolicyRepository, InMemoryEscalationPolicyRepository>();
|
||||
builder.Services.AddSingleton<INotifyOnCallScheduleRepository, InMemoryOnCallScheduleRepository>();
|
||||
|
||||
// Localization resolver with fallback chain
|
||||
builder.Services.AddSingleton<ILocalizationResolver, DefaultLocalizationResolver>();
|
||||
|
||||
// Storm breaker for notification storm detection
|
||||
builder.Services.Configure<StormBreakerConfig>(builder.Configuration.GetSection("notifier:stormBreaker"));
|
||||
builder.Services.AddSingleton<IStormBreaker, DefaultStormBreaker>();
|
||||
|
||||
// Security services (NOTIFY-SVC-40-003)
|
||||
builder.Services.Configure<AckTokenOptions>(builder.Configuration.GetSection("notifier:security:ackToken"));
|
||||
builder.Services.AddSingleton<IAckTokenService, HmacAckTokenService>();
|
||||
builder.Services.Configure<WebhookSecurityOptions>(builder.Configuration.GetSection("notifier:security:webhook"));
|
||||
builder.Services.AddSingleton<IWebhookSecurityService, DefaultWebhookSecurityService>();
|
||||
builder.Services.AddSingleton<IHtmlSanitizer, DefaultHtmlSanitizer>();
|
||||
builder.Services.Configure<TenantIsolationOptions>(builder.Configuration.GetSection("notifier:security:tenantIsolation"));
|
||||
builder.Services.AddSingleton<ITenantIsolationValidator, DefaultTenantIsolationValidator>();
|
||||
|
||||
// Observability, dead-letter, and retention services (NOTIFY-SVC-40-004)
|
||||
builder.Services.AddSingleton<INotifyMetrics, DefaultNotifyMetrics>();
|
||||
builder.Services.AddSingleton<IDeadLetterService, InMemoryDeadLetterService>();
|
||||
builder.Services.AddSingleton<IRetentionPolicyService, DefaultRetentionPolicyService>();
|
||||
// Template service for v2 API preview endpoint
|
||||
// Template service with enhanced renderer (worker contracts)
|
||||
builder.Services.AddTemplateServices(options =>
|
||||
{
|
||||
var provenanceUrl = builder.Configuration["notifier:provenance:baseUrl"];
|
||||
@@ -89,6 +74,22 @@ builder.Services.AddTemplateServices(options =>
|
||||
}
|
||||
});
|
||||
|
||||
// Localization resolver with fallback chain
|
||||
builder.Services.AddSingleton<ILocalizationResolver, DefaultLocalizationResolver>();
|
||||
|
||||
// Security services (NOTIFY-SVC-40-003)
|
||||
builder.Services.Configure<AckTokenOptions>(builder.Configuration.GetSection("notifier:security:ackToken"));
|
||||
builder.Services.AddSingleton<IAckTokenService, HmacAckTokenService>();
|
||||
builder.Services.Configure<WebhookSecurityOptions>(builder.Configuration.GetSection("notifier:security:webhook"));
|
||||
builder.Services.AddSingleton<IWebhookSecurityService, InMemoryWebhookSecurityService>();
|
||||
builder.Services.AddSingleton<IHtmlSanitizer, DefaultHtmlSanitizer>();
|
||||
builder.Services.Configure<TenantIsolationOptions>(builder.Configuration.GetSection("notifier:security:tenantIsolation"));
|
||||
builder.Services.AddSingleton<ITenantIsolationValidator, InMemoryTenantIsolationValidator>();
|
||||
|
||||
// Observability, dead-letter, and retention services (NOTIFY-SVC-40-004)
|
||||
builder.Services.AddSingleton<INotifyMetrics, DefaultNotifyMetrics>();
|
||||
builder.Services.AddSingleton<IDeadLetterService, InMemoryDeadLetterService>();
|
||||
builder.Services.AddSingleton<IRetentionPolicyService, DefaultRetentionPolicyService>();
|
||||
// Escalation and on-call services
|
||||
builder.Services.AddEscalationServices(builder.Configuration);
|
||||
|
||||
@@ -98,9 +99,6 @@ builder.Services.AddStormBreakerServices(builder.Configuration);
|
||||
// Security services (signing, webhook validation, HTML sanitization, tenant isolation)
|
||||
builder.Services.AddNotifierSecurityServices(builder.Configuration);
|
||||
|
||||
// Observability services (metrics, tracing, dead-letter, chaos testing, retention)
|
||||
builder.Services.AddNotifierObservabilityServices(builder.Configuration);
|
||||
|
||||
// Tenancy services (context accessor, RLS enforcement, channel resolution, notification enrichment)
|
||||
builder.Services.AddNotifierTenancy(builder.Configuration);
|
||||
|
||||
@@ -432,7 +430,7 @@ app.MapPost("/api/v1/notify/pack-approvals/{packId}/ack", async (
|
||||
|
||||
app.MapGet("/api/v2/notify/templates", async (
|
||||
HttpContext context,
|
||||
INotifyTemplateService templateService,
|
||||
WorkerTemplateService templateService,
|
||||
string? keyPrefix,
|
||||
string? locale,
|
||||
NotifyChannelType? channelType) =>
|
||||
@@ -443,8 +441,15 @@ app.MapGet("/api/v2/notify/templates", async (
|
||||
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
||||
}
|
||||
|
||||
var templates = await templateService.ListAsync(tenantId, keyPrefix, locale, channelType, context.RequestAborted)
|
||||
.ConfigureAwait(false);
|
||||
var templates = await templateService.ListAsync(
|
||||
tenantId,
|
||||
new TemplateListOptions
|
||||
{
|
||||
KeyPrefix = keyPrefix,
|
||||
Locale = locale,
|
||||
ChannelType = channelType
|
||||
},
|
||||
context.RequestAborted).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new { items = templates, count = templates.Count });
|
||||
});
|
||||
@@ -452,7 +457,7 @@ app.MapGet("/api/v2/notify/templates", async (
|
||||
app.MapGet("/api/v2/notify/templates/{templateId}", async (
|
||||
HttpContext context,
|
||||
string templateId,
|
||||
INotifyTemplateService templateService) =>
|
||||
WorkerTemplateService templateService) =>
|
||||
{
|
||||
var tenantId = context.Request.Headers["X-StellaOps-Tenant"].ToString();
|
||||
if (string.IsNullOrWhiteSpace(tenantId))
|
||||
@@ -472,7 +477,7 @@ app.MapPut("/api/v2/notify/templates/{templateId}", async (
|
||||
HttpContext context,
|
||||
string templateId,
|
||||
TemplateUpsertRequest request,
|
||||
INotifyTemplateService templateService) =>
|
||||
WorkerTemplateService templateService) =>
|
||||
{
|
||||
var tenantId = context.Request.Headers["X-StellaOps-Tenant"].ToString();
|
||||
if (string.IsNullOrWhiteSpace(tenantId))
|
||||
@@ -512,7 +517,7 @@ app.MapPut("/api/v2/notify/templates/{templateId}", async (
|
||||
app.MapDelete("/api/v2/notify/templates/{templateId}", async (
|
||||
HttpContext context,
|
||||
string templateId,
|
||||
INotifyTemplateService templateService) =>
|
||||
WorkerTemplateService templateService) =>
|
||||
{
|
||||
var tenantId = context.Request.Headers["X-StellaOps-Tenant"].ToString();
|
||||
if (string.IsNullOrWhiteSpace(tenantId))
|
||||
@@ -520,7 +525,13 @@ app.MapDelete("/api/v2/notify/templates/{templateId}", async (
|
||||
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
||||
}
|
||||
|
||||
await templateService.DeleteAsync(tenantId, templateId, context.RequestAborted)
|
||||
var actor = context.Request.Headers["X-StellaOps-Actor"].ToString();
|
||||
if (string.IsNullOrWhiteSpace(actor))
|
||||
{
|
||||
actor = "api";
|
||||
}
|
||||
|
||||
await templateService.DeleteAsync(tenantId, templateId, actor, context.RequestAborted)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Results.NoContent();
|
||||
@@ -530,7 +541,9 @@ app.MapPost("/api/v2/notify/templates/{templateId}/preview", async (
|
||||
HttpContext context,
|
||||
string templateId,
|
||||
TemplatePreviewRequest request,
|
||||
INotifyTemplateService templateService) =>
|
||||
WorkerTemplateService templateService,
|
||||
WorkerTemplateRenderer renderer,
|
||||
TimeProvider timeProvider) =>
|
||||
{
|
||||
var tenantId = context.Request.Headers["X-StellaOps-Tenant"].ToString();
|
||||
if (string.IsNullOrWhiteSpace(tenantId))
|
||||
@@ -546,17 +559,26 @@ app.MapPost("/api/v2/notify/templates/{templateId}/preview", async (
|
||||
return Results.NotFound(Error("not_found", $"Template {templateId} not found.", context));
|
||||
}
|
||||
|
||||
var options = new TemplateRenderOptions
|
||||
var sampleEvent = NotifyEvent.Create(
|
||||
eventId: Guid.NewGuid(),
|
||||
kind: request.EventKind ?? "sample.event",
|
||||
tenant: tenantId,
|
||||
ts: timeProvider.GetUtcNow(),
|
||||
payload: request.SamplePayload ?? new JsonObject(),
|
||||
attributes: request.SampleAttributes ?? new Dictionary<string, string>(),
|
||||
actor: "preview",
|
||||
version: "1");
|
||||
|
||||
var rendered = await renderer.RenderAsync(template, sampleEvent, context.RequestAborted).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new TemplatePreviewResponse
|
||||
{
|
||||
IncludeProvenance = request.IncludeProvenance ?? false,
|
||||
ProvenanceBaseUrl = request.ProvenanceBaseUrl,
|
||||
FormatOverride = request.FormatOverride
|
||||
};
|
||||
|
||||
var result = await templateService.PreviewAsync(template, request.SamplePayload, options, context.RequestAborted)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(result);
|
||||
RenderedBody = rendered.Body,
|
||||
RenderedSubject = rendered.Subject,
|
||||
BodyHash = rendered.BodyHash,
|
||||
Format = rendered.Format.ToString(),
|
||||
Warnings = null
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================
|
||||
@@ -631,7 +653,7 @@ app.MapPut("/api/v2/notify/rules/{ruleId}", async (
|
||||
channel: a.Channel ?? string.Empty,
|
||||
template: a.Template ?? string.Empty,
|
||||
locale: a.Locale,
|
||||
enabled: a.Enabled ?? true)).ToArray(),
|
||||
enabled: a.Enabled)).ToArray(),
|
||||
enabled: request.Enabled ?? true,
|
||||
description: request.Description);
|
||||
|
||||
@@ -647,8 +669,8 @@ app.MapPut("/api/v2/notify/rules/{ruleId}", async (
|
||||
EntityId = ruleId,
|
||||
EntityType = "rule",
|
||||
Timestamp = timeProvider.GetUtcNow(),
|
||||
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(
|
||||
JsonSerializer.Serialize(new { ruleId, name = request.Name, enabled = request.Enabled }))
|
||||
Payload = JsonSerializer.SerializeToNode(
|
||||
new { ruleId, name = request.Name, enabled = request.Enabled }) as JsonObject
|
||||
};
|
||||
await auditRepository.AppendAsync(auditEntry, context.RequestAborted).ConfigureAwait(false);
|
||||
}
|
||||
@@ -716,7 +738,7 @@ app.MapGet("/api/v2/notify/channels", async (
|
||||
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
||||
}
|
||||
|
||||
var channels = await channelRepository.ListAsync(tenantId, context.RequestAborted).ConfigureAwait(false);
|
||||
var channels = await channelRepository.ListAsync(tenantId, cancellationToken: context.RequestAborted).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new { items = channels, count = channels.Count });
|
||||
});
|
||||
@@ -789,8 +811,8 @@ app.MapPut("/api/v2/notify/channels/{channelId}", async (
|
||||
EntityId = channelId,
|
||||
EntityType = "channel",
|
||||
Timestamp = timeProvider.GetUtcNow(),
|
||||
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(
|
||||
JsonSerializer.Serialize(new { channelId, name = request.Name, type = request.Type }))
|
||||
Payload = JsonSerializer.SerializeToNode(
|
||||
new { channelId, name = request.Name, type = request.Type }) as JsonObject
|
||||
};
|
||||
await auditRepository.AppendAsync(auditEntry, context.RequestAborted).ConfigureAwait(false);
|
||||
}
|
||||
@@ -1045,8 +1067,8 @@ app.MapPut("/api/v2/notify/quiet-hours/{scheduleId}", async (
|
||||
EntityId = scheduleId,
|
||||
EntityType = "quiet-hours",
|
||||
Timestamp = timeProvider.GetUtcNow(),
|
||||
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(
|
||||
JsonSerializer.Serialize(new { scheduleId, name = request.Name, enabled = request.Enabled }))
|
||||
Payload = JsonSerializer.SerializeToNode(
|
||||
new { scheduleId, name = request.Name, enabled = request.Enabled }) as JsonObject
|
||||
};
|
||||
await auditRepository.AppendAsync(auditEntry, context.RequestAborted).ConfigureAwait(false);
|
||||
}
|
||||
@@ -1176,8 +1198,8 @@ app.MapPut("/api/v2/notify/maintenance-windows/{windowId}", async (
|
||||
EntityId = windowId,
|
||||
EntityType = "maintenance-window",
|
||||
Timestamp = timeProvider.GetUtcNow(),
|
||||
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(
|
||||
JsonSerializer.Serialize(new { windowId, name = request.Name, startsAt = request.StartsAt, endsAt = request.EndsAt }))
|
||||
Payload = JsonSerializer.SerializeToNode(
|
||||
new { windowId, name = request.Name, startsAt = request.StartsAt, endsAt = request.EndsAt }) as JsonObject
|
||||
};
|
||||
await auditRepository.AppendAsync(auditEntry, context.RequestAborted).ConfigureAwait(false);
|
||||
}
|
||||
@@ -1306,8 +1328,8 @@ app.MapPut("/api/v2/notify/throttle-configs/{configId}", async (
|
||||
EntityId = configId,
|
||||
EntityType = "throttle-config",
|
||||
Timestamp = timeProvider.GetUtcNow(),
|
||||
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(
|
||||
JsonSerializer.Serialize(new { configId, name = request.Name, defaultWindow = request.DefaultWindow.TotalSeconds }))
|
||||
Payload = JsonSerializer.SerializeToNode(
|
||||
new { configId, name = request.Name, defaultWindow = request.DefaultWindow.TotalSeconds }) as JsonObject
|
||||
};
|
||||
await auditRepository.AppendAsync(auditEntry, context.RequestAborted).ConfigureAwait(false);
|
||||
}
|
||||
@@ -1439,8 +1461,8 @@ app.MapPost("/api/v2/notify/overrides", async (
|
||||
EntityId = overrideId,
|
||||
EntityType = "operator-override",
|
||||
Timestamp = timeProvider.GetUtcNow(),
|
||||
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(
|
||||
JsonSerializer.Serialize(new { overrideId, overrideType = request.OverrideType, expiresAt = request.ExpiresAt, reason = request.Reason }))
|
||||
Payload = JsonSerializer.SerializeToNode(
|
||||
new { overrideId, overrideType = request.OverrideType, expiresAt = request.ExpiresAt, reason = request.Reason }) as JsonObject
|
||||
};
|
||||
await auditRepository.AppendAsync(auditEntry, context.RequestAborted).ConfigureAwait(false);
|
||||
}
|
||||
@@ -1574,8 +1596,8 @@ app.MapPut("/api/v2/notify/escalation-policies/{policyId}", async (
|
||||
EntityId = policyId,
|
||||
EntityType = "escalation-policy",
|
||||
Timestamp = timeProvider.GetUtcNow(),
|
||||
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(
|
||||
JsonSerializer.Serialize(new { policyId, name = request.Name, enabled = request.Enabled }))
|
||||
Payload = JsonSerializer.SerializeToNode(
|
||||
new { policyId, name = request.Name, enabled = request.Enabled }) as JsonObject
|
||||
};
|
||||
await auditRepository.AppendAsync(auditEntry, context.RequestAborted).ConfigureAwait(false);
|
||||
}
|
||||
@@ -1728,8 +1750,8 @@ app.MapPut("/api/v2/notify/oncall-schedules/{scheduleId}", async (
|
||||
EntityId = scheduleId,
|
||||
EntityType = "oncall-schedule",
|
||||
Timestamp = timeProvider.GetUtcNow(),
|
||||
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(
|
||||
JsonSerializer.Serialize(new { scheduleId, name = request.Name, enabled = request.Enabled }))
|
||||
Payload = JsonSerializer.SerializeToNode(
|
||||
new { scheduleId, name = request.Name, enabled = request.Enabled }) as JsonObject
|
||||
};
|
||||
await auditRepository.AppendAsync(auditEntry, context.RequestAborted).ConfigureAwait(false);
|
||||
}
|
||||
@@ -1817,8 +1839,8 @@ app.MapPost("/api/v2/notify/oncall-schedules/{scheduleId}/overrides", async (
|
||||
EntityId = scheduleId,
|
||||
EntityType = "oncall-schedule",
|
||||
Timestamp = timeProvider.GetUtcNow(),
|
||||
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(
|
||||
JsonSerializer.Serialize(new { scheduleId, overrideId, userId = request.UserId }))
|
||||
Payload = JsonSerializer.SerializeToNode(
|
||||
new { scheduleId, overrideId, userId = request.UserId }) as JsonObject
|
||||
};
|
||||
await auditRepository.AppendAsync(auditEntry, context.RequestAborted).ConfigureAwait(false);
|
||||
}
|
||||
@@ -2066,8 +2088,8 @@ app.MapPut("/api/v2/notify/localization/bundles/{bundleId}", async (
|
||||
EntityId = bundleId,
|
||||
EntityType = "localization-bundle",
|
||||
Timestamp = timeProvider.GetUtcNow(),
|
||||
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(
|
||||
JsonSerializer.Serialize(new { bundleId, locale = request.Locale, bundleKey = request.BundleKey }))
|
||||
Payload = JsonSerializer.SerializeToNode(
|
||||
new { bundleId, locale = request.Locale, bundleKey = request.BundleKey }) as JsonObject
|
||||
};
|
||||
await auditRepository.AppendAsync(auditEntry, context.RequestAborted).ConfigureAwait(false);
|
||||
}
|
||||
@@ -2207,7 +2229,7 @@ app.MapPost("/api/v2/notify/storms/{stormKey}/summary", async (
|
||||
var actor = context.Request.Headers["X-StellaOps-Actor"].ToString();
|
||||
if (string.IsNullOrWhiteSpace(actor)) actor = "api";
|
||||
|
||||
var summary = await stormBreaker.TriggerSummaryAsync(tenantId, stormKey, context.RequestAborted).ConfigureAwait(false);
|
||||
var summary = await stormBreaker.GenerateSummaryAsync(tenantId, stormKey, context.RequestAborted).ConfigureAwait(false);
|
||||
|
||||
if (summary is null)
|
||||
{
|
||||
@@ -2224,8 +2246,8 @@ app.MapPost("/api/v2/notify/storms/{stormKey}/summary", async (
|
||||
EntityId = summary.SummaryId,
|
||||
EntityType = "storm-summary",
|
||||
Timestamp = timeProvider.GetUtcNow(),
|
||||
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(
|
||||
JsonSerializer.Serialize(new { stormKey, eventCount = summary.EventCount }))
|
||||
Payload = JsonSerializer.SerializeToNode(
|
||||
new { stormKey, eventCount = summary.TotalEvents }) as JsonObject
|
||||
};
|
||||
await auditRepository.AppendAsync(auditEntry, context.RequestAborted).ConfigureAwait(false);
|
||||
}
|
||||
@@ -2310,8 +2332,8 @@ app.MapPost("/api/v1/ack/{token}", async (
|
||||
EntityId = verification.Token.DeliveryId,
|
||||
EntityType = "delivery",
|
||||
Timestamp = timeProvider.GetUtcNow(),
|
||||
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(
|
||||
JsonSerializer.Serialize(new { comment = request?.Comment, metadata = request?.Metadata }))
|
||||
Payload = JsonSerializer.SerializeToNode(
|
||||
new { comment = request?.Comment, metadata = request?.Metadata }) as JsonObject
|
||||
};
|
||||
await auditRepository.AppendAsync(auditEntry, context.RequestAborted).ConfigureAwait(false);
|
||||
}
|
||||
@@ -2385,12 +2407,12 @@ app.MapPost("/api/v2/notify/security/ack-tokens/verify", (
|
||||
|
||||
app.MapPost("/api/v2/notify/security/html/validate", (
|
||||
HttpContext context,
|
||||
ValidateHtmlRequest request,
|
||||
Contracts.ValidateHtmlRequest request,
|
||||
IHtmlSanitizer htmlSanitizer) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Html))
|
||||
{
|
||||
return Results.Ok(new ValidateHtmlResponse
|
||||
return Results.Ok(new Contracts.ValidateHtmlResponse
|
||||
{
|
||||
IsSafe = true,
|
||||
Issues = []
|
||||
@@ -2399,50 +2421,53 @@ app.MapPost("/api/v2/notify/security/html/validate", (
|
||||
|
||||
var result = htmlSanitizer.Validate(request.Html);
|
||||
|
||||
return Results.Ok(new ValidateHtmlResponse
|
||||
return Results.Ok(new Contracts.ValidateHtmlResponse
|
||||
{
|
||||
IsSafe = result.IsSafe,
|
||||
Issues = result.Issues.Select(i => new HtmlIssue
|
||||
IsSafe = result.IsValid,
|
||||
Issues = result.Errors.Select(i => new Contracts.HtmlIssue
|
||||
{
|
||||
Type = i.Type.ToString(),
|
||||
Description = i.Description,
|
||||
Element = i.ElementName,
|
||||
Attribute = i.AttributeName
|
||||
}).ToArray(),
|
||||
Stats = result.Stats is not null ? new HtmlStats
|
||||
Description = i.Message
|
||||
}).Concat(result.Warnings.Select(w => new Contracts.HtmlIssue
|
||||
{
|
||||
CharacterCount = result.Stats.CharacterCount,
|
||||
ElementCount = result.Stats.ElementCount,
|
||||
MaxDepth = result.Stats.MaxDepth,
|
||||
LinkCount = result.Stats.LinkCount,
|
||||
ImageCount = result.Stats.ImageCount
|
||||
} : null
|
||||
Type = "Warning",
|
||||
Description = w
|
||||
})).ToArray(),
|
||||
Stats = null
|
||||
});
|
||||
});
|
||||
|
||||
app.MapPost("/api/v2/notify/security/html/sanitize", (
|
||||
HttpContext context,
|
||||
SanitizeHtmlRequest request,
|
||||
Contracts.SanitizeHtmlRequest request,
|
||||
IHtmlSanitizer htmlSanitizer) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Html))
|
||||
{
|
||||
return Results.Ok(new SanitizeHtmlResponse
|
||||
return Results.Ok(new Contracts.SanitizeHtmlResponse
|
||||
{
|
||||
SanitizedHtml = string.Empty,
|
||||
WasModified = false
|
||||
});
|
||||
}
|
||||
|
||||
var options = new HtmlSanitizeOptions
|
||||
var profile = new SanitizationProfile
|
||||
{
|
||||
Name = "api-request",
|
||||
AllowDataUrls = request.AllowDataUrls,
|
||||
AdditionalAllowedTags = request.AdditionalAllowedTags?.ToHashSet()
|
||||
AllowedTags = request.AdditionalAllowedTags?.ToHashSet(StringComparer.OrdinalIgnoreCase)
|
||||
?? SanitizationProfile.Basic.AllowedTags,
|
||||
AllowedAttributes = SanitizationProfile.Basic.AllowedAttributes,
|
||||
AllowedUrlSchemes = SanitizationProfile.Basic.AllowedUrlSchemes,
|
||||
MaxContentLength = SanitizationProfile.Basic.MaxContentLength,
|
||||
MaxNestingDepth = SanitizationProfile.Basic.MaxNestingDepth,
|
||||
StripComments = SanitizationProfile.Basic.StripComments,
|
||||
StripScripts = SanitizationProfile.Basic.StripScripts
|
||||
};
|
||||
|
||||
var sanitized = htmlSanitizer.Sanitize(request.Html, options);
|
||||
var sanitized = htmlSanitizer.Sanitize(request.Html, profile);
|
||||
|
||||
return Results.Ok(new SanitizeHtmlResponse
|
||||
return Results.Ok(new Contracts.SanitizeHtmlResponse
|
||||
{
|
||||
SanitizedHtml = sanitized,
|
||||
WasModified = !string.Equals(request.Html, sanitized, StringComparison.Ordinal)
|
||||
@@ -2509,14 +2534,21 @@ app.MapGet("/api/v2/notify/security/webhook/{channelId}/secret", (
|
||||
return Results.Ok(new { channelId, maskedSecret });
|
||||
});
|
||||
|
||||
app.MapGet("/api/v2/notify/security/isolation/violations", (
|
||||
app.MapGet("/api/v2/notify/security/isolation/violations", async (
|
||||
HttpContext context,
|
||||
ITenantIsolationValidator isolationValidator,
|
||||
int? limit) =>
|
||||
{
|
||||
var violations = isolationValidator.GetRecentViolations(limit ?? 100);
|
||||
var violations = await isolationValidator.GetViolationsAsync(
|
||||
tenantId: null,
|
||||
since: null,
|
||||
cancellationToken: context.RequestAborted).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new { items = violations, count = violations.Count });
|
||||
var items = violations
|
||||
.Take(limit.GetValueOrDefault(100))
|
||||
.ToList();
|
||||
|
||||
return Results.Ok(new { items, count = items.Count });
|
||||
});
|
||||
|
||||
// =============================================
|
||||
@@ -2670,7 +2702,7 @@ app.MapGet("/api/v2/notify/dead-letter/{entryId}", async (
|
||||
|
||||
app.MapPost("/api/v2/notify/dead-letter/retry", async (
|
||||
HttpContext context,
|
||||
RetryDeadLetterRequest request,
|
||||
Contracts.RetryDeadLetterRequest request,
|
||||
IDeadLetterService deadLetterService) =>
|
||||
{
|
||||
var tenantId = context.Request.Headers["X-StellaOps-Tenant"].ToString();
|
||||
@@ -2682,9 +2714,9 @@ app.MapPost("/api/v2/notify/dead-letter/retry", async (
|
||||
var results = await deadLetterService.RetryBatchAsync(tenantId, request.EntryIds, context.RequestAborted)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new RetryDeadLetterResponse
|
||||
return Results.Ok(new Contracts.RetryDeadLetterResponse
|
||||
{
|
||||
Results = results.Select(r => new DeadLetterRetryResultItem
|
||||
Results = results.Select(r => new Contracts.DeadLetterRetryResultItem
|
||||
{
|
||||
EntryId = r.EntryId,
|
||||
Success = r.Success,
|
||||
|
||||
Reference in New Issue
Block a user