Files
mic-check/docs/api/07_audit_and_webhooks.md
2026-06-30 11:44:10 -07:00

7.2 KiB
Executable File

Step 7: Audit Logging & Webhooks

Goal

Implement the audit trail system (recording all changes to flags, environments, segments, etc.) and the webhook system (notifying external systems of flag state changes).


Audit Logging

What Gets Audited

Every create, update, and delete operation on:

  • Features and FeatureStates
  • Environments
  • Segments
  • Identities (admin operations)
  • API Keys (create/revoke)
  • User permissions
  • Organization settings
  • Project settings

AuditLog Domain Model

// MicCheck.Api/Audit/AuditLog.cs (already defined in Step 2)
// Key fields:
//   ResourceType: "Feature", "FeatureState", "Environment", etc.
//   ResourceId: string representation of the entity's PK
//   Action: "Created", "Updated", "Deleted"
//   Changes: JSON diff of before/after values (for Updates)
//   ActorUserId: null for API key operations without user context

Audit Service

Lives in MicCheck.Api/Audit/. Injects MicCheckDbContext directly.

// MicCheck.Api/Audit/AuditService.cs
public class AuditService
{
    private readonly MicCheckDbContext _db;

    public Task RecordAsync(
        string resourceType,
        string resourceId,
        string action,
        int organizationId,
        int? projectId = null,
        int? environmentId = null,
        int? actorUserId = null,
        object? before = null,
        object? after = null,
        CancellationToken ct = default);
}

Change diffing:

  • Serialize before and after objects to JSON
  • Store both in Changes as { "before": {...}, "after": {...} }
  • Use System.Text.Json for serialization

Integration with Services

Audit logging is called directly from other service methods:

// In FeatureService.CreateAsync:
await _auditService.RecordAsync(
    resourceType: "Feature",
    resourceId: feature.Id.ToString(),
    action: "Created",
    organizationId: project.OrganizationId,
    projectId: feature.ProjectId,
    after: feature);

Audit Log Queries

AuditService exposes a query method returning domain objects; the controller maps to response DTOs.

Support filtering by:

  • Date range (from, to)
  • Resource type
  • Action type
  • Project
  • Environment
  • Actor user

Always ordered by CreatedAt DESC. Paginated (default 20, max 100).

// MicCheck.Api/Audit/AuditController.cs
[HttpGet]
public async Task<ActionResult<PaginatedResponse<AuditLogResponse>>> List(
    int id, [FromQuery] AuditLogFilter filter, CancellationToken ct)
{
    var (logs, total) = await _auditService.ListAsync(id, filter, ct);
    return Ok(new PaginatedResponse<AuditLogResponse>(
        total, null, null,
        logs.Select(AuditLogResponse.From).ToList()));
}

Webhooks

Webhook Triggers

Webhooks fire on:

  • FLAG_UPDATED — any FeatureState change
  • FLAG_DELETED — feature deleted
  • AUDIT_LOG_CREATED — any audit log entry (organization webhooks)

Webhook Payload

{
  "data": {
    "changed_by": "user@example.com",
    "timestamp": "2026-04-06T12:00:00Z",
    "new_state": {
      "id": 42,
      "enabled": true,
      "feature_state_value": null,
      "feature": { "id": 1, "name": "dark_mode" },
      "environment": { "id": 5, "name": "Production" }
    },
    "previous_state": { ... }
  },
  "event_type": "FLAG_UPDATED"
}

Webhook Event & Dispatcher

Live in MicCheck.Api/Webhooks/. Dispatcher injects MicCheckDbContext directly.

// MicCheck.Api/Webhooks/WebhookEvent.cs
public class WebhookEvent
{
    public required string EventType { get; init; }
    public required object Data { get; init; }
    public int? EnvironmentId { get; init; }
    public int OrganizationId { get; init; }
}

// MicCheck.Api/Webhooks/WebhookDispatcher.cs
public class WebhookDispatcher
{
    private readonly MicCheckDbContext _db;
    private readonly IHttpClientFactory _httpClientFactory;

    // Finds all active webhooks for the environment (and org-level)
    // Serializes payload to JSON
    // Signs payload with HMAC-SHA256 using webhook secret
    // POSTs to webhook URL with timeout of 10 seconds
    // Stores delivery result in WebhookDeliveryLog
    public Task DispatchAsync(WebhookEvent webhookEvent, CancellationToken ct = default);
}

Webhook Signing

// HMAC-SHA256 signature in header:
// X-Flagsmith-Signature: sha256=<hex_digest>

private static string ComputeSignature(string secret, string payload)
{
    var keyBytes = Encoding.UTF8.GetBytes(secret);
    var payloadBytes = Encoding.UTF8.GetBytes(payload);
    var hashBytes = HMACSHA256.HashData(keyBytes, payloadBytes);
    return "sha256=" + Convert.ToHexString(hashBytes).ToLowerInvariant();
}

Webhook Queue (Async Dispatch)

Lives in MicCheck.Api/Webhooks/. Uses System.Threading.Channels so dispatch does not block the API response.

// MicCheck.Api/Webhooks/WebhookQueue.cs
public class WebhookQueue
{
    private readonly Channel<WebhookEvent> _channel = Channel.CreateUnbounded<WebhookEvent>();

    public ValueTask EnqueueAsync(WebhookEvent webhookEvent) =>
        _channel.Writer.WriteAsync(webhookEvent);

    public IAsyncEnumerable<WebhookEvent> ReadAllAsync(CancellationToken ct) =>
        _channel.Reader.ReadAllAsync(ct);
}

Retry Background Service

Lives in MicCheck.Infrastructure/Webhooks/. Polls WebhookDeliveryLog and retries failures.

// MicCheck.Infrastructure/Webhooks/WebhookRetryBackgroundService.cs
public class WebhookRetryBackgroundService : BackgroundService
{
    // Polls WebhookDeliveryLog for failed deliveries within retry window
    // Up to 3 attempts: immediate, +5 min, +30 min
    // Reattempts via WebhookDispatcher
}

Retry Strategy

  • Up to 3 delivery attempts (immediate, +5 min, +30 min)
  • Failed deliveries are logged but do not block the API response

Webhook Admin Endpoints

Controllers map domain objects to/from DTOs in the action method, calling WebhookService for all data access.

GET    /api/v1/environments/{apiKey}/webhooks/
POST   /api/v1/environments/{apiKey}/webhooks/
PUT    /api/v1/environments/{apiKey}/webhooks/{id}/
DELETE /api/v1/environments/{apiKey}/webhooks/{id}/
GET    /api/v1/environments/{apiKey}/webhooks/{id}/deliveries/

GET    /api/v1/organisations/{id}/webhooks/
POST   /api/v1/organisations/{id}/webhooks/
PUT    /api/v1/organisations/{id}/webhooks/{id}/
DELETE /api/v1/organisations/{id}/webhooks/{id}/

Acceptance Criteria

  • Every create/update/delete in the Admin API produces an AuditLog record
  • Audit logs correctly capture before/after state for updates
  • GET /api/v1/organisations/{id}/audit-logs/ returns paginated, filtered results
  • Audit controllers map AuditLog domain objects to AuditLogResponse DTOs in the action
  • Webhooks fire within 1 second of a flag state change
  • HMAC-SHA256 signature is correct and verifiable by receivers
  • Failed webhook deliveries are logged with status code and response body
  • Retry logic attempts delivery 3 times with backoff
  • Webhook delivery does not block the API response (fire-and-forget via WebhookQueue)
  • Unit tests cover: signature computation, payload serialization, retry scheduling
  • Unit tests cover: audit diff generation for create/update/delete scenarios