Security & resource hardening: eliminate CPU/disk attack surface
Addresses production CPU spike incident. Key changes: - Guard OTel exporter behind OTEL_EXPORTER_OTLP_ENDPOINT env var; filter tracing to /api paths only — unconditional export was primary suspect - Remove /healthz endpoint entirely (unauthenticated, hit DB on every call) - Replace KnownUserMiddleware with POST /api/users/me called once on login from TokenSync — eliminates unconditional DB write on every request - Add DB indexes: (BudgetId, IsDeleted) on Incomes/Outgos, OwnerUserId on Budgets, SharedWithUserId and (IsPending, SharedWithEmail) on BudgetShares - Move UseRateLimiter() before UseStaticFiles() so all requests are throttled - Replace full-array reorder with move-by-position (id + newIndex) — bounded input, fewer DB writes, better API design - Lock ForwardedHeaders to 172.20.0.0/16 subnet; fixes KnownNetworks deprecation warning (0 warnings in build now) - Add AsNoTracking() to all read-only queries in Summary/Incomes/OutgosController - FrequencyCalculator returns 0 for unknown enum values instead of throwing - Thread.Sleep → await Task.Delay in OIDC startup loop - AllowedHosts locked to budget.stwaddle.com Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+271
@@ -0,0 +1,271 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Budget.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Budget.Infrastructure.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260507024323_AddHardeningIndexes")]
|
||||
partial class AddHardeningIndexes
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.7")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Budget.Core.Models.Budget", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("OwnerUserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<uint>("xmin")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("xid")
|
||||
.HasColumnName("xmin");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OwnerUserId");
|
||||
|
||||
b.ToTable("Budgets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Budget.Core.Models.BudgetShare", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("BudgetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsPending")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("Permission")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("SharedWithEmail")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("SharedWithUserId")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SharedWithUserId");
|
||||
|
||||
b.HasIndex("BudgetId", "SharedWithEmail")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("IsPending", "SharedWithEmail");
|
||||
|
||||
b.ToTable("BudgetShares");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Budget.Core.Models.Income", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("numeric(18,2)");
|
||||
|
||||
b.Property<Guid>("BudgetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Frequency")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BudgetId", "IsDeleted");
|
||||
|
||||
b.ToTable("Incomes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Budget.Core.Models.KnownUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<DateTimeOffset>("LastSeenAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("KnownUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Budget.Core.Models.Outgo", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("numeric(18,2)");
|
||||
|
||||
b.Property<Guid>("BudgetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTimeOffset?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Frequency")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
|
||||
b.Property<string>("PaymentSource")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BudgetId", "IsDeleted");
|
||||
|
||||
b.ToTable("Outgos");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Budget.Core.Models.BudgetShare", b =>
|
||||
{
|
||||
b.HasOne("Budget.Core.Models.Budget", "Budget")
|
||||
.WithMany("Shares")
|
||||
.HasForeignKey("BudgetId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Budget");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Budget.Core.Models.Income", b =>
|
||||
{
|
||||
b.HasOne("Budget.Core.Models.Budget", "Budget")
|
||||
.WithMany("Incomes")
|
||||
.HasForeignKey("BudgetId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Budget");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Budget.Core.Models.Outgo", b =>
|
||||
{
|
||||
b.HasOne("Budget.Core.Models.Budget", "Budget")
|
||||
.WithMany("Outgos")
|
||||
.HasForeignKey("BudgetId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Budget");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Budget.Core.Models.Budget", b =>
|
||||
{
|
||||
b.Navigation("Incomes");
|
||||
|
||||
b.Navigation("Outgos");
|
||||
|
||||
b.Navigation("Shares");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Budget.Infrastructure.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddHardeningIndexes : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Outgos_BudgetId",
|
||||
table: "Outgos");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Incomes_BudgetId",
|
||||
table: "Incomes");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Outgos_BudgetId_IsDeleted",
|
||||
table: "Outgos",
|
||||
columns: new[] { "BudgetId", "IsDeleted" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Incomes_BudgetId_IsDeleted",
|
||||
table: "Incomes",
|
||||
columns: new[] { "BudgetId", "IsDeleted" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BudgetShares_IsPending_SharedWithEmail",
|
||||
table: "BudgetShares",
|
||||
columns: new[] { "IsPending", "SharedWithEmail" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BudgetShares_SharedWithUserId",
|
||||
table: "BudgetShares",
|
||||
column: "SharedWithUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Budgets_OwnerUserId",
|
||||
table: "Budgets",
|
||||
column: "OwnerUserId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Outgos_BudgetId_IsDeleted",
|
||||
table: "Outgos");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Incomes_BudgetId_IsDeleted",
|
||||
table: "Incomes");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_BudgetShares_IsPending_SharedWithEmail",
|
||||
table: "BudgetShares");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_BudgetShares_SharedWithUserId",
|
||||
table: "BudgetShares");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Budgets_OwnerUserId",
|
||||
table: "Budgets");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Outgos_BudgetId",
|
||||
table: "Outgos",
|
||||
column: "BudgetId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Incomes_BudgetId",
|
||||
table: "Incomes",
|
||||
column: "BudgetId");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,8 @@ namespace Budget.Infrastructure.Data.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OwnerUserId");
|
||||
|
||||
b.ToTable("Budgets");
|
||||
});
|
||||
|
||||
@@ -96,9 +98,13 @@ namespace Budget.Infrastructure.Data.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SharedWithUserId");
|
||||
|
||||
b.HasIndex("BudgetId", "SharedWithEmail")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("IsPending", "SharedWithEmail");
|
||||
|
||||
b.ToTable("BudgetShares");
|
||||
});
|
||||
|
||||
@@ -134,7 +140,7 @@ namespace Budget.Infrastructure.Data.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BudgetId");
|
||||
b.HasIndex("BudgetId", "IsDeleted");
|
||||
|
||||
b.ToTable("Incomes");
|
||||
});
|
||||
@@ -210,7 +216,7 @@ namespace Budget.Infrastructure.Data.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BudgetId");
|
||||
b.HasIndex("BudgetId", "IsDeleted");
|
||||
|
||||
b.ToTable("Outgos");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user