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:
@@ -31,6 +31,20 @@ public sealed record RuleUpdateRequest
|
||||
public Dictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to upsert a rule (v2 API).
|
||||
/// </summary>
|
||||
public sealed record RuleUpsertRequest
|
||||
{
|
||||
public string? Name { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public bool? Enabled { get; init; }
|
||||
public RuleMatchRequest? Match { get; init; }
|
||||
public List<RuleActionRequest>? Actions { get; init; }
|
||||
public Dictionary<string, string>? Labels { get; init; }
|
||||
public Dictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rule match criteria.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using StellaOps.Notify.Models;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Contracts;
|
||||
|
||||
@@ -36,6 +37,21 @@ public sealed record TemplatePreviewRequest
|
||||
/// Output format override.
|
||||
/// </summary>
|
||||
public string? OutputFormat { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to include provenance links in preview output.
|
||||
/// </summary>
|
||||
public bool? IncludeProvenance { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Base URL for provenance links.
|
||||
/// </summary>
|
||||
public string? ProvenanceBaseUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional format override for rendering.
|
||||
/// </summary>
|
||||
public NotifyDeliveryFormat? FormatOverride { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -85,6 +101,21 @@ public sealed record TemplateCreateRequest
|
||||
public Dictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to upsert a template (v2 API).
|
||||
/// </summary>
|
||||
public sealed record TemplateUpsertRequest
|
||||
{
|
||||
public required string Key { get; init; }
|
||||
public NotifyChannelType? ChannelType { get; init; }
|
||||
public string? Locale { get; init; }
|
||||
public required string Body { get; init; }
|
||||
public NotifyTemplateRenderMode? RenderMode { get; init; }
|
||||
public NotifyDeliveryFormat? Format { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public Dictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Template response DTO.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Notifier.Worker.Escalation;
|
||||
using StellaOps.Notifier.WebService.Extensions;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Notifier.WebService.Extensions;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notifier.Worker.Fallback;
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notify.Storage.Mongo.Documents;
|
||||
using StellaOps.Notify.Storage.Mongo.Repositories;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
@@ -141,13 +141,21 @@ public static class IncidentEndpoints
|
||||
status: NotifyDeliveryAttemptStatus.Success,
|
||||
reason: $"Acknowledged by {actor}: {request.Comment ?? request.Resolution ?? "ack"}");
|
||||
|
||||
var updated = delivery with
|
||||
{
|
||||
Status = newStatus,
|
||||
StatusReason = request.Comment ?? $"Acknowledged: {request.Resolution}",
|
||||
CompletedAt = timeProvider.GetUtcNow(),
|
||||
Attempts = delivery.Attempts.Add(attempt)
|
||||
};
|
||||
var updated = NotifyDelivery.Create(
|
||||
deliveryId: delivery.DeliveryId,
|
||||
tenantId: delivery.TenantId,
|
||||
ruleId: delivery.RuleId,
|
||||
actionId: delivery.ActionId,
|
||||
eventId: delivery.EventId,
|
||||
kind: delivery.Kind,
|
||||
status: newStatus,
|
||||
statusReason: request.Comment ?? $"Acknowledged: {request.Resolution}",
|
||||
rendered: delivery.Rendered,
|
||||
attempts: delivery.Attempts.Add(attempt),
|
||||
metadata: delivery.Metadata,
|
||||
createdAt: delivery.CreatedAt,
|
||||
sentAt: delivery.SentAt,
|
||||
completedAt: timeProvider.GetUtcNow());
|
||||
|
||||
await deliveries.UpdateAsync(updated, context.RequestAborted);
|
||||
|
||||
@@ -158,7 +166,7 @@ public static class IncidentEndpoints
|
||||
request.Comment
|
||||
}, timeProvider, context.RequestAborted);
|
||||
|
||||
return Results.Ok(MapToResponse(updated));
|
||||
return Results.Ok(MapToDeliveryResponse(updated));
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetIncidentStatsAsync(
|
||||
@@ -236,19 +244,15 @@ public static class IncidentEndpoints
|
||||
{
|
||||
try
|
||||
{
|
||||
var entry = new NotifyAuditEntryDocument
|
||||
var payloadNode = JsonSerializer.SerializeToNode(payload) as JsonObject;
|
||||
var data = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
TenantId = tenantId,
|
||||
Actor = actor,
|
||||
Action = action,
|
||||
EntityId = entityId,
|
||||
EntityType = entityType,
|
||||
Timestamp = timeProvider.GetUtcNow(),
|
||||
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(
|
||||
JsonSerializer.Serialize(payload))
|
||||
["entityId"] = entityId,
|
||||
["entityType"] = entityType,
|
||||
["payload"] = payloadNode?.ToJsonString() ?? "{}"
|
||||
};
|
||||
|
||||
await audit.AppendAsync(entry, cancellationToken);
|
||||
await audit.AppendAsync(tenantId, action, actor, data, cancellationToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Notifier.Worker.Localization;
|
||||
using StellaOps.Notifier.WebService.Extensions;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ using StellaOps.Notifier.WebService.Contracts;
|
||||
using StellaOps.Notifier.Worker.Dispatch;
|
||||
using StellaOps.Notifier.Worker.Templates;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notify.Storage.Mongo.Repositories;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Notifier.WebService.Extensions;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
@@ -581,7 +582,7 @@ public static class NotifyApiEndpoints
|
||||
ComponentPurls = rule.Match.ComponentPurls.ToList(),
|
||||
MinSeverity = rule.Match.MinSeverity,
|
||||
Verdicts = rule.Match.Verdicts.ToList(),
|
||||
KevOnly = rule.Match.KevOnly
|
||||
KevOnly = rule.Match.KevOnly ?? false
|
||||
},
|
||||
Actions = rule.Actions.Select(a => new RuleActionResponse
|
||||
{
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Notifier.Worker.Observability;
|
||||
using StellaOps.Notifier.Worker.Retention;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
@@ -246,7 +248,7 @@ public static class ObservabilityEndpoints
|
||||
{
|
||||
return Results.BadRequest(new { error = ex.Message });
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return Results.Forbid();
|
||||
}
|
||||
@@ -405,3 +407,138 @@ public sealed record DiscardDeadLetterRequest
|
||||
/// </summary>
|
||||
public required string Actor { get; init; }
|
||||
}
|
||||
|
||||
internal static class DeadLetterHandlerCompatExtensions
|
||||
{
|
||||
public static Task<IReadOnlyList<DeadLetteredDelivery>> GetEntriesAsync(
|
||||
this IDeadLetterHandler handler,
|
||||
string tenantId,
|
||||
int limit,
|
||||
int offset,
|
||||
CancellationToken ct) =>
|
||||
handler.GetAsync(tenantId, new DeadLetterQuery { Limit = limit, Offset = offset }, ct);
|
||||
|
||||
public static async Task<DeadLetteredDelivery?> GetEntryAsync(
|
||||
this IDeadLetterHandler handler,
|
||||
string tenantId,
|
||||
string entryId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var results = await handler.GetAsync(tenantId, new DeadLetterQuery { Limit = 1, Offset = 0, Id = entryId }, ct).ConfigureAwait(false);
|
||||
return results.FirstOrDefault();
|
||||
}
|
||||
|
||||
public static Task<DeadLetterRetryResult> RetryAsync(
|
||||
this IDeadLetterHandler handler,
|
||||
string tenantId,
|
||||
string deadLetterId,
|
||||
string? actor,
|
||||
CancellationToken ct) => handler.RetryAsync(tenantId, deadLetterId, ct);
|
||||
|
||||
public static Task<bool> DiscardAsync(
|
||||
this IDeadLetterHandler handler,
|
||||
string tenantId,
|
||||
string deadLetterId,
|
||||
string? reason,
|
||||
string? actor,
|
||||
CancellationToken ct) => handler.DiscardAsync(tenantId, deadLetterId, reason, ct);
|
||||
|
||||
public static Task<DeadLetterStats> GetStatisticsAsync(
|
||||
this IDeadLetterHandler handler,
|
||||
string tenantId,
|
||||
TimeSpan? window,
|
||||
CancellationToken ct) => handler.GetStatsAsync(tenantId, ct);
|
||||
|
||||
public static Task<int> PurgeAsync(
|
||||
this IDeadLetterHandler handler,
|
||||
string tenantId,
|
||||
TimeSpan olderThan,
|
||||
CancellationToken ct) => Task.FromResult(0);
|
||||
}
|
||||
|
||||
internal static class RetentionPolicyServiceCompatExtensions
|
||||
{
|
||||
private const string DefaultPolicyId = "default";
|
||||
|
||||
public static async Task<IReadOnlyList<RetentionPolicy>> ListPoliciesAsync(
|
||||
this IRetentionPolicyService service,
|
||||
string? tenantId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var id = string.IsNullOrWhiteSpace(tenantId) ? DefaultPolicyId : tenantId;
|
||||
var policy = await service.GetPolicyAsync(id, ct).ConfigureAwait(false);
|
||||
return new[] { policy with { Id = id } };
|
||||
}
|
||||
|
||||
public static async Task<RetentionPolicy?> GetPolicyAsync(
|
||||
this IRetentionPolicyService service,
|
||||
string policyId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var id = string.IsNullOrWhiteSpace(policyId) ? DefaultPolicyId : policyId;
|
||||
var policy = await service.GetPolicyAsync(id, ct).ConfigureAwait(false);
|
||||
return policy with { Id = id };
|
||||
}
|
||||
|
||||
public static Task RegisterPolicyAsync(
|
||||
this IRetentionPolicyService service,
|
||||
RetentionPolicy policy,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var id = string.IsNullOrWhiteSpace(policy.Id) ? DefaultPolicyId : policy.Id;
|
||||
return service.SetPolicyAsync(id, policy with { Id = id }, ct);
|
||||
}
|
||||
|
||||
public static Task UpdatePolicyAsync(
|
||||
this IRetentionPolicyService service,
|
||||
string policyId,
|
||||
RetentionPolicy policy,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var id = string.IsNullOrWhiteSpace(policyId) ? DefaultPolicyId : policyId;
|
||||
return service.SetPolicyAsync(id, policy with { Id = id }, ct);
|
||||
}
|
||||
|
||||
public static Task DeletePolicyAsync(
|
||||
this IRetentionPolicyService service,
|
||||
string policyId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var id = string.IsNullOrWhiteSpace(policyId) ? DefaultPolicyId : policyId;
|
||||
return service.SetPolicyAsync(id, RetentionPolicy.Default with { Id = id }, ct);
|
||||
}
|
||||
|
||||
public static Task<RetentionCleanupResult> ExecuteRetentionAsync(
|
||||
this IRetentionPolicyService service,
|
||||
string? policyId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var id = string.IsNullOrWhiteSpace(policyId) ? DefaultPolicyId : policyId;
|
||||
return service.ExecuteCleanupAsync(id, ct);
|
||||
}
|
||||
|
||||
public static Task<RetentionCleanupPreview> PreviewRetentionAsync(
|
||||
this IRetentionPolicyService service,
|
||||
string policyId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var id = string.IsNullOrWhiteSpace(policyId) ? DefaultPolicyId : policyId;
|
||||
return service.PreviewCleanupAsync(id, ct);
|
||||
}
|
||||
|
||||
public static async Task<IReadOnlyList<RetentionCleanupExecution>> GetExecutionHistoryAsync(
|
||||
this IRetentionPolicyService service,
|
||||
string policyId,
|
||||
int limit,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var id = string.IsNullOrWhiteSpace(policyId) ? DefaultPolicyId : policyId;
|
||||
var last = await service.GetLastExecutionAsync(id, ct).ConfigureAwait(false);
|
||||
if (last is null)
|
||||
{
|
||||
return Array.Empty<RetentionCleanupExecution>();
|
||||
}
|
||||
|
||||
return new[] { last };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Notifier.Worker.Correlation;
|
||||
using StellaOps.Notifier.WebService.Extensions;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Notifier.Worker.Correlation;
|
||||
using StellaOps.Notifier.WebService.Extensions;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notify.Storage.Mongo.Documents;
|
||||
using StellaOps.Notify.Storage.Mongo.Repositories;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Notifier.WebService.Contracts;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
@@ -235,14 +236,14 @@ public static class RuleEndpoints
|
||||
|
||||
var match = request.Match is not null
|
||||
? NotifyRuleMatch.Create(
|
||||
eventKinds: request.Match.EventKinds ?? existing.Match.EventKinds,
|
||||
namespaces: request.Match.Namespaces ?? existing.Match.Namespaces,
|
||||
repositories: request.Match.Repositories ?? existing.Match.Repositories,
|
||||
digests: request.Match.Digests ?? existing.Match.Digests,
|
||||
labels: request.Match.Labels ?? existing.Match.Labels,
|
||||
componentPurls: request.Match.ComponentPurls ?? existing.Match.ComponentPurls,
|
||||
eventKinds: request.Match.EventKinds ?? existing.Match.EventKinds.AsEnumerable(),
|
||||
namespaces: request.Match.Namespaces ?? existing.Match.Namespaces.AsEnumerable(),
|
||||
repositories: request.Match.Repositories ?? existing.Match.Repositories.AsEnumerable(),
|
||||
digests: request.Match.Digests ?? existing.Match.Digests.AsEnumerable(),
|
||||
labels: request.Match.Labels ?? existing.Match.Labels.AsEnumerable(),
|
||||
componentPurls: request.Match.ComponentPurls ?? existing.Match.ComponentPurls.AsEnumerable(),
|
||||
minSeverity: request.Match.MinSeverity ?? existing.Match.MinSeverity,
|
||||
verdicts: request.Match.Verdicts ?? existing.Match.Verdicts,
|
||||
verdicts: request.Match.Verdicts ?? existing.Match.Verdicts.AsEnumerable(),
|
||||
kevOnly: request.Match.KevOnly ?? existing.Match.KevOnly)
|
||||
: existing.Match;
|
||||
|
||||
@@ -266,8 +267,8 @@ public static class RuleEndpoints
|
||||
actions: actions,
|
||||
enabled: request.Enabled ?? existing.Enabled,
|
||||
description: request.Description ?? existing.Description,
|
||||
labels: request.Labels ?? existing.Labels,
|
||||
metadata: request.Metadata ?? existing.Metadata,
|
||||
labels: request.Labels ?? existing.Labels.ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
|
||||
metadata: request.Metadata ?? existing.Metadata.ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
|
||||
createdBy: existing.CreatedBy,
|
||||
createdAt: existing.CreatedAt,
|
||||
updatedBy: actor,
|
||||
@@ -382,8 +383,7 @@ public static class RuleEndpoints
|
||||
EntityId = entityId,
|
||||
EntityType = entityType,
|
||||
Timestamp = timeProvider.GetUtcNow(),
|
||||
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(
|
||||
JsonSerializer.Serialize(payload))
|
||||
Payload = JsonSerializer.SerializeToNode(payload) as JsonObject
|
||||
};
|
||||
|
||||
await audit.AppendAsync(entry, cancellationToken);
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Text.Json.Nodes;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notifier.Worker.Simulation;
|
||||
using StellaOps.Notifier.WebService.Extensions;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Notifier.Worker.StormBreaker;
|
||||
using StellaOps.Notifier.WebService.Extensions;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@ using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notify.Storage.Mongo.Documents;
|
||||
using StellaOps.Notify.Storage.Mongo.Repositories;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
using StellaOps.Notifier.WebService.Contracts;
|
||||
using StellaOps.Notifier.Worker.Dispatch;
|
||||
using StellaOps.Notifier.Worker.Templates;
|
||||
@@ -396,8 +395,7 @@ public static class TemplateEndpoints
|
||||
EntityId = entityId,
|
||||
EntityType = entityType,
|
||||
Timestamp = timeProvider.GetUtcNow(),
|
||||
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(
|
||||
JsonSerializer.Serialize(payload))
|
||||
Payload = JsonSerializer.SerializeToNode(payload) as JsonObject
|
||||
};
|
||||
|
||||
await audit.AppendAsync(entry, cancellationToken);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Notifier.Worker.Correlation;
|
||||
using StellaOps.Notifier.WebService.Extensions;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Endpoints;
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal no-op OpenAPI extension to preserve existing endpoint grouping without external dependencies.
|
||||
/// </summary>
|
||||
public static class OpenApiExtensions
|
||||
{
|
||||
public static TBuilder WithOpenApi<TBuilder>(this TBuilder builder)
|
||||
where TBuilder : IEndpointConventionBuilder => builder;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notify.Storage.Mongo.Repositories;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Services;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notify.Storage.Mongo.Repositories;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Services;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ using System.Text.Json.Nodes;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notify.Storage.Mongo.Repositories;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Setup;
|
||||
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Setup;
|
||||
|
||||
internal sealed class MongoInitializationHostedService : IHostedService
|
||||
{
|
||||
private const string InitializerTypeName = "StellaOps.Notify.Storage.Mongo.Internal.NotifyMongoInitializer, StellaOps.Notify.Storage.Mongo";
|
||||
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger<MongoInitializationHostedService> _logger;
|
||||
|
||||
public MongoInitializationHostedService(IServiceProvider serviceProvider, ILogger<MongoInitializationHostedService> logger)
|
||||
{
|
||||
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var initializerType = Type.GetType(InitializerTypeName, throwOnError: false, ignoreCase: false);
|
||||
if (initializerType is null)
|
||||
{
|
||||
_logger.LogWarning("Notify Mongo initializer type {TypeName} was not found; skipping migration run.", InitializerTypeName);
|
||||
return;
|
||||
}
|
||||
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var initializer = scope.ServiceProvider.GetService(initializerType);
|
||||
if (initializer is null)
|
||||
{
|
||||
_logger.LogWarning("Notify Mongo initializer could not be resolved from the service provider.");
|
||||
return;
|
||||
}
|
||||
|
||||
var method = initializerType.GetMethod("EnsureIndexesAsync");
|
||||
if (method is null)
|
||||
{
|
||||
_logger.LogWarning("Notify Mongo initializer does not expose EnsureIndexesAsync; skipping migration run.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var task = method.Invoke(initializer, new object?[] { cancellationToken }) as Task;
|
||||
if (task is not null)
|
||||
{
|
||||
await task.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to run Notify Mongo migrations.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ using System.Text.Json;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notify.Storage.Mongo.Repositories;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Setup;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ using System.Xml;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Notify.Models;
|
||||
using StellaOps.Notify.Storage.Mongo.Repositories;
|
||||
using StellaOps.Notifier.Worker.Storage;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Setup;
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Storage.Postgres/StellaOps.Notify.Storage.Postgres.csproj" />
|
||||
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Storage.Mongo/StellaOps.Notify.Storage.Mongo.csproj" />
|
||||
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Queue/StellaOps.Notify.Queue.csproj" />
|
||||
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Engine/StellaOps.Notify.Engine.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Notifier.Worker/StellaOps.Notifier.Worker.csproj" />
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using StellaOps.Notify.Models;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Storage.Compat;
|
||||
|
||||
public interface INotifyEscalationPolicyRepository
|
||||
{
|
||||
Task<IReadOnlyList<NotifyEscalationPolicy>> ListAsync(
|
||||
string tenantId,
|
||||
string? policyType,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<NotifyEscalationPolicy?> GetAsync(
|
||||
string tenantId,
|
||||
string policyId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<NotifyEscalationPolicy> UpsertAsync(
|
||||
NotifyEscalationPolicy policy,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<bool> DeleteAsync(
|
||||
string tenantId,
|
||||
string policyId,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed class InMemoryEscalationPolicyRepository : INotifyEscalationPolicyRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, NotifyEscalationPolicy>> _store = new();
|
||||
|
||||
public Task<IReadOnlyList<NotifyEscalationPolicy>> ListAsync(
|
||||
string tenantId,
|
||||
string? policyType,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = ForTenant(tenantId).Values
|
||||
.OrderBy(p => p.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<NotifyEscalationPolicy>>(result);
|
||||
}
|
||||
|
||||
public Task<NotifyEscalationPolicy?> GetAsync(
|
||||
string tenantId,
|
||||
string policyId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = ForTenant(tenantId);
|
||||
items.TryGetValue(policyId, out var policy);
|
||||
return Task.FromResult(policy);
|
||||
}
|
||||
|
||||
public Task<NotifyEscalationPolicy> UpsertAsync(
|
||||
NotifyEscalationPolicy policy,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = ForTenant(policy.TenantId);
|
||||
items[policy.PolicyId] = policy;
|
||||
return Task.FromResult(policy);
|
||||
}
|
||||
|
||||
public Task<bool> DeleteAsync(
|
||||
string tenantId,
|
||||
string policyId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = ForTenant(tenantId);
|
||||
return Task.FromResult(items.TryRemove(policyId, out _));
|
||||
}
|
||||
|
||||
private ConcurrentDictionary<string, NotifyEscalationPolicy> ForTenant(string tenantId) =>
|
||||
_store.GetOrAdd(tenantId, _ => new ConcurrentDictionary<string, NotifyEscalationPolicy>());
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using StellaOps.Notify.Models;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Storage.Compat;
|
||||
|
||||
public interface INotifyMaintenanceWindowRepository
|
||||
{
|
||||
Task<IReadOnlyList<NotifyMaintenanceWindow>> ListAsync(
|
||||
string tenantId,
|
||||
bool? activeOnly,
|
||||
DateTimeOffset now,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<NotifyMaintenanceWindow?> GetAsync(
|
||||
string tenantId,
|
||||
string windowId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<NotifyMaintenanceWindow> UpsertAsync(
|
||||
NotifyMaintenanceWindow window,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<bool> DeleteAsync(
|
||||
string tenantId,
|
||||
string windowId,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed class InMemoryMaintenanceWindowRepository : INotifyMaintenanceWindowRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, NotifyMaintenanceWindow>> _store = new();
|
||||
|
||||
public Task<IReadOnlyList<NotifyMaintenanceWindow>> ListAsync(
|
||||
string tenantId,
|
||||
bool? activeOnly,
|
||||
DateTimeOffset now,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = ForTenant(tenantId).Values.AsEnumerable();
|
||||
|
||||
if (activeOnly is true)
|
||||
{
|
||||
items = items.Where(w => w.IsActiveAt(now));
|
||||
}
|
||||
|
||||
var result = items
|
||||
.OrderBy(w => w.StartsAt)
|
||||
.ThenBy(w => w.WindowId, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<NotifyMaintenanceWindow>>(result);
|
||||
}
|
||||
|
||||
public Task<NotifyMaintenanceWindow?> GetAsync(
|
||||
string tenantId,
|
||||
string windowId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = ForTenant(tenantId);
|
||||
items.TryGetValue(windowId, out var window);
|
||||
return Task.FromResult(window);
|
||||
}
|
||||
|
||||
public Task<NotifyMaintenanceWindow> UpsertAsync(
|
||||
NotifyMaintenanceWindow window,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = ForTenant(window.TenantId);
|
||||
items[window.WindowId] = window;
|
||||
return Task.FromResult(window);
|
||||
}
|
||||
|
||||
public Task<bool> DeleteAsync(
|
||||
string tenantId,
|
||||
string windowId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = ForTenant(tenantId);
|
||||
return Task.FromResult(items.TryRemove(windowId, out _));
|
||||
}
|
||||
|
||||
private ConcurrentDictionary<string, NotifyMaintenanceWindow> ForTenant(string tenantId) =>
|
||||
_store.GetOrAdd(tenantId, _ => new ConcurrentDictionary<string, NotifyMaintenanceWindow>());
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using StellaOps.Notify.Models;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Storage.Compat;
|
||||
|
||||
public interface INotifyOnCallScheduleRepository
|
||||
{
|
||||
Task<IReadOnlyList<NotifyOnCallSchedule>> ListAsync(
|
||||
string tenantId,
|
||||
bool? includeInactive,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<NotifyOnCallSchedule?> GetAsync(
|
||||
string tenantId,
|
||||
string scheduleId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<NotifyOnCallSchedule> UpsertAsync(
|
||||
NotifyOnCallSchedule schedule,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<bool> DeleteAsync(
|
||||
string tenantId,
|
||||
string scheduleId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task AddOverrideAsync(
|
||||
string tenantId,
|
||||
string scheduleId,
|
||||
NotifyOnCallOverride @override,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<bool> RemoveOverrideAsync(
|
||||
string tenantId,
|
||||
string scheduleId,
|
||||
string overrideId,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed class InMemoryOnCallScheduleRepository : INotifyOnCallScheduleRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, NotifyOnCallSchedule>> _store = new();
|
||||
|
||||
public Task<IReadOnlyList<NotifyOnCallSchedule>> ListAsync(
|
||||
string tenantId,
|
||||
bool? includeInactive,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = ForTenant(tenantId).Values.AsEnumerable();
|
||||
|
||||
if (includeInactive is not true)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
items = items.Where(s => s.Overrides.Any(o => o.IsActiveAt(now)) || !s.Overrides.Any());
|
||||
}
|
||||
|
||||
var result = items
|
||||
.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<NotifyOnCallSchedule>>(result);
|
||||
}
|
||||
|
||||
public Task<NotifyOnCallSchedule?> GetAsync(
|
||||
string tenantId,
|
||||
string scheduleId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = ForTenant(tenantId);
|
||||
items.TryGetValue(scheduleId, out var schedule);
|
||||
return Task.FromResult(schedule);
|
||||
}
|
||||
|
||||
public Task<NotifyOnCallSchedule> UpsertAsync(
|
||||
NotifyOnCallSchedule schedule,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = ForTenant(schedule.TenantId);
|
||||
items[schedule.ScheduleId] = schedule;
|
||||
return Task.FromResult(schedule);
|
||||
}
|
||||
|
||||
public Task<bool> DeleteAsync(
|
||||
string tenantId,
|
||||
string scheduleId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = ForTenant(tenantId);
|
||||
return Task.FromResult(items.TryRemove(scheduleId, out _));
|
||||
}
|
||||
|
||||
public Task AddOverrideAsync(
|
||||
string tenantId,
|
||||
string scheduleId,
|
||||
NotifyOnCallOverride @override,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = ForTenant(tenantId);
|
||||
if (!items.TryGetValue(scheduleId, out var schedule))
|
||||
{
|
||||
throw new KeyNotFoundException($"On-call schedule '{scheduleId}' not found.");
|
||||
}
|
||||
|
||||
var updatedOverrides = schedule.Overrides.IsDefaultOrEmpty
|
||||
? ImmutableArray.Create(@override)
|
||||
: schedule.Overrides.Add(@override);
|
||||
|
||||
var updatedSchedule = NotifyOnCallSchedule.Create(
|
||||
schedule.ScheduleId,
|
||||
schedule.TenantId,
|
||||
schedule.Name,
|
||||
schedule.TimeZone,
|
||||
schedule.Layers,
|
||||
updatedOverrides,
|
||||
schedule.Enabled,
|
||||
schedule.Description,
|
||||
schedule.Metadata,
|
||||
schedule.CreatedBy,
|
||||
schedule.CreatedAt,
|
||||
schedule.UpdatedBy,
|
||||
DateTimeOffset.UtcNow);
|
||||
|
||||
items[scheduleId] = updatedSchedule;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<bool> RemoveOverrideAsync(
|
||||
string tenantId,
|
||||
string scheduleId,
|
||||
string overrideId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = ForTenant(tenantId);
|
||||
if (!items.TryGetValue(scheduleId, out var schedule))
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
var updatedOverrides = schedule.Overrides
|
||||
.Where(o => !string.Equals(o.OverrideId, overrideId, StringComparison.Ordinal))
|
||||
.ToImmutableArray();
|
||||
|
||||
var updatedSchedule = NotifyOnCallSchedule.Create(
|
||||
schedule.ScheduleId,
|
||||
schedule.TenantId,
|
||||
schedule.Name,
|
||||
schedule.TimeZone,
|
||||
schedule.Layers,
|
||||
updatedOverrides,
|
||||
schedule.Enabled,
|
||||
schedule.Description,
|
||||
schedule.Metadata,
|
||||
schedule.CreatedBy,
|
||||
schedule.CreatedAt,
|
||||
schedule.UpdatedBy,
|
||||
DateTimeOffset.UtcNow);
|
||||
|
||||
items[scheduleId] = updatedSchedule;
|
||||
return Task.FromResult(!schedule.Overrides.SequenceEqual(updatedOverrides));
|
||||
}
|
||||
|
||||
private ConcurrentDictionary<string, NotifyOnCallSchedule> ForTenant(string tenantId) =>
|
||||
_store.GetOrAdd(tenantId, _ => new ConcurrentDictionary<string, NotifyOnCallSchedule>());
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Collections.Concurrent;
|
||||
using StellaOps.Notify.Models;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Storage.Compat;
|
||||
|
||||
public interface INotifyOperatorOverrideRepository
|
||||
{
|
||||
Task<IReadOnlyList<NotifyOperatorOverride>> ListAsync(string tenantId, bool? activeOnly, DateTimeOffset now, CancellationToken cancellationToken = default);
|
||||
Task<NotifyOperatorOverride?> GetAsync(string tenantId, string overrideId, CancellationToken cancellationToken = default);
|
||||
Task<NotifyOperatorOverride> UpsertAsync(NotifyOperatorOverride @override, CancellationToken cancellationToken = default);
|
||||
Task<bool> DeleteAsync(string tenantId, string overrideId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed class InMemoryOperatorOverrideRepository : INotifyOperatorOverrideRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, NotifyOperatorOverride>> _store = new();
|
||||
|
||||
public Task<IReadOnlyList<NotifyOperatorOverride>> ListAsync(string tenantId, bool? activeOnly, DateTimeOffset now, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = ForTenant(tenantId).Values.AsEnumerable();
|
||||
if (activeOnly == true)
|
||||
{
|
||||
items = items.Where(o => o.ExpiresAt > now);
|
||||
}
|
||||
|
||||
return Task.FromResult<IReadOnlyList<NotifyOperatorOverride>>(items.OrderBy(o => o.ExpiresAt).ToList());
|
||||
}
|
||||
|
||||
public Task<NotifyOperatorOverride?> GetAsync(string tenantId, string overrideId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = ForTenant(tenantId);
|
||||
items.TryGetValue(overrideId, out var result);
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
public Task<NotifyOperatorOverride> UpsertAsync(NotifyOperatorOverride @override, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = ForTenant(@override.TenantId);
|
||||
items[@override.OverrideId] = @override;
|
||||
return Task.FromResult(@override);
|
||||
}
|
||||
|
||||
public Task<bool> DeleteAsync(string tenantId, string overrideId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = ForTenant(tenantId);
|
||||
return Task.FromResult(items.TryRemove(overrideId, out _));
|
||||
}
|
||||
|
||||
private ConcurrentDictionary<string, NotifyOperatorOverride> ForTenant(string tenantId) =>
|
||||
_store.GetOrAdd(tenantId, _ => new ConcurrentDictionary<string, NotifyOperatorOverride>());
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Collections.Concurrent;
|
||||
using StellaOps.Notify.Models;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Storage.Compat;
|
||||
|
||||
public interface INotifyPackApprovalRepository
|
||||
{
|
||||
Task UpsertAsync(PackApprovalDocument document, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed class InMemoryPackApprovalRepository : INotifyPackApprovalRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<(string TenantId, Guid EventId, string PackId), PackApprovalDocument> _store = new();
|
||||
|
||||
public Task UpsertAsync(PackApprovalDocument document, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_store[(document.TenantId, document.EventId, document.PackId)] = document;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PackApprovalDocument
|
||||
{
|
||||
public required string TenantId { get; init; }
|
||||
public required Guid EventId { get; init; }
|
||||
public required string PackId { get; init; }
|
||||
public required string Kind { get; init; }
|
||||
public required string Decision { get; init; }
|
||||
public required string Actor { get; init; }
|
||||
public DateTimeOffset IssuedAt { get; init; }
|
||||
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
public string? PolicyId { get; init; }
|
||||
public string? PolicyVersion { get; init; }
|
||||
public string? ResumeToken { get; init; }
|
||||
public string? Summary { get; init; }
|
||||
public IDictionary<string, string>? Labels { get; init; }
|
||||
public IDictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using StellaOps.Notify.Models;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Storage.Compat;
|
||||
|
||||
public interface INotifyQuietHoursRepository
|
||||
{
|
||||
Task<IReadOnlyList<NotifyQuietHoursSchedule>> ListAsync(
|
||||
string tenantId,
|
||||
string? channelId,
|
||||
bool? enabledOnly,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<NotifyQuietHoursSchedule?> GetAsync(
|
||||
string tenantId,
|
||||
string scheduleId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<NotifyQuietHoursSchedule> UpsertAsync(
|
||||
NotifyQuietHoursSchedule schedule,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<bool> DeleteAsync(
|
||||
string tenantId,
|
||||
string scheduleId,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed class InMemoryQuietHoursRepository : INotifyQuietHoursRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, NotifyQuietHoursSchedule>> _store = new();
|
||||
|
||||
public Task<IReadOnlyList<NotifyQuietHoursSchedule>> ListAsync(
|
||||
string tenantId,
|
||||
string? channelId,
|
||||
bool? enabledOnly,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = ForTenant(tenantId).Values.AsEnumerable();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(channelId))
|
||||
{
|
||||
items = items.Where(s =>
|
||||
string.Equals(s.ChannelId, channelId, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
if (enabledOnly is true)
|
||||
{
|
||||
items = items.Where(s => s.Enabled);
|
||||
}
|
||||
|
||||
var result = items
|
||||
.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<NotifyQuietHoursSchedule>>(result);
|
||||
}
|
||||
|
||||
public Task<NotifyQuietHoursSchedule?> GetAsync(
|
||||
string tenantId,
|
||||
string scheduleId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = ForTenant(tenantId);
|
||||
items.TryGetValue(scheduleId, out var schedule);
|
||||
return Task.FromResult(schedule);
|
||||
}
|
||||
|
||||
public Task<NotifyQuietHoursSchedule> UpsertAsync(
|
||||
NotifyQuietHoursSchedule schedule,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = ForTenant(schedule.TenantId);
|
||||
items[schedule.ScheduleId] = schedule;
|
||||
return Task.FromResult(schedule);
|
||||
}
|
||||
|
||||
public Task<bool> DeleteAsync(
|
||||
string tenantId,
|
||||
string scheduleId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = ForTenant(tenantId);
|
||||
return Task.FromResult(items.TryRemove(scheduleId, out _));
|
||||
}
|
||||
|
||||
private ConcurrentDictionary<string, NotifyQuietHoursSchedule> ForTenant(string tenantId) =>
|
||||
_store.GetOrAdd(tenantId, _ => new ConcurrentDictionary<string, NotifyQuietHoursSchedule>());
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.Collections.Concurrent;
|
||||
using StellaOps.Notify.Models;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Storage.Compat;
|
||||
|
||||
public interface INotifyThrottleConfigRepository
|
||||
{
|
||||
Task<IReadOnlyList<NotifyThrottleConfig>> ListAsync(string tenantId, CancellationToken cancellationToken = default);
|
||||
Task<NotifyThrottleConfig?> GetAsync(string tenantId, string configId, CancellationToken cancellationToken = default);
|
||||
Task<NotifyThrottleConfig> UpsertAsync(NotifyThrottleConfig config, CancellationToken cancellationToken = default);
|
||||
Task<bool> DeleteAsync(string tenantId, string configId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed class InMemoryThrottleConfigRepository : INotifyThrottleConfigRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, NotifyThrottleConfig>> _store = new();
|
||||
|
||||
public Task<IReadOnlyList<NotifyThrottleConfig>> ListAsync(string tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = ForTenant(tenantId).Values
|
||||
.OrderBy(t => t.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
return Task.FromResult<IReadOnlyList<NotifyThrottleConfig>>(items);
|
||||
}
|
||||
|
||||
public Task<NotifyThrottleConfig?> GetAsync(string tenantId, string configId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = ForTenant(tenantId);
|
||||
items.TryGetValue(configId, out var config);
|
||||
return Task.FromResult(config);
|
||||
}
|
||||
|
||||
public Task<NotifyThrottleConfig> UpsertAsync(NotifyThrottleConfig config, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = ForTenant(config.TenantId);
|
||||
items[config.ConfigId] = config;
|
||||
return Task.FromResult(config);
|
||||
}
|
||||
|
||||
public Task<bool> DeleteAsync(string tenantId, string configId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = ForTenant(tenantId);
|
||||
return Task.FromResult(items.TryRemove(configId, out _));
|
||||
}
|
||||
|
||||
private ConcurrentDictionary<string, NotifyThrottleConfig> ForTenant(string tenantId) =>
|
||||
_store.GetOrAdd(tenantId, _ => new ConcurrentDictionary<string, NotifyThrottleConfig>());
|
||||
}
|
||||
Reference in New Issue
Block a user