feat(security): audit batch C — data posture, retention, DP keys (#22)
CI / backend (push) Successful in 52s
CI / frontend (push) Successful in 11s
Deploy Staging / deploy (push) Successful in 24s
Security / secrets (push) Successful in 4s
Security / dependencies (push) Successful in 52s
CI / backend (pull_request) Successful in 49s
CI / frontend (pull_request) Successful in 10s
Security / secrets (pull_request) Successful in 3s
Security / dependencies (pull_request) Successful in 55s
CI / backend (push) Successful in 52s
CI / frontend (push) Successful in 11s
Deploy Staging / deploy (push) Successful in 24s
Security / secrets (push) Successful in 4s
Security / dependencies (push) Successful in 52s
CI / backend (pull_request) Successful in 49s
CI / frontend (pull_request) Successful in 10s
Security / secrets (pull_request) Successful in 3s
Security / dependencies (pull_request) Successful in 55s
This commit was merged in pull request #22.
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
using InboxIntel.Infrastructure.Configuration;
|
||||
using InboxIntel.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace InboxIntel.Infrastructure.Retention;
|
||||
|
||||
/// <summary>
|
||||
/// AUDIT H-3 (retention): purges locally stored email data past the configured age so the
|
||||
/// local mailbox copy is not kept forever by default-of-omission. This deletes ONLY the
|
||||
/// local Postgres rows — the user's actual Gmail is never touched. Both knobs default to 0
|
||||
/// (disabled) so existing deployments are unchanged until the operator opts in.
|
||||
/// </summary>
|
||||
public class RetentionService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly DataRetentionOptions _options;
|
||||
private readonly ILogger<RetentionService> _logger;
|
||||
|
||||
public RetentionService(AppDbContext db, IOptions<DataRetentionOptions> options, ILogger<RetentionService> logger)
|
||||
{
|
||||
_db = db;
|
||||
_options = options.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>Runs the configured purges. Returns the number of emails removed.</summary>
|
||||
public async Task<int> PurgeAsync(CancellationToken ct = default)
|
||||
{
|
||||
var removed = 0;
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
if (_options.PurgeTrashedAfterDays > 0)
|
||||
{
|
||||
var cutoff = now.AddDays(-_options.PurgeTrashedAfterDays);
|
||||
removed += await PurgeWhereAsync(e => e.IsTrashed && e.SentAtUtc < cutoff, ct);
|
||||
}
|
||||
|
||||
if (_options.PurgeAllAfterDays > 0)
|
||||
{
|
||||
var cutoff = now.AddDays(-_options.PurgeAllAfterDays);
|
||||
removed += await PurgeWhereAsync(e => e.SentAtUtc < cutoff, ct);
|
||||
}
|
||||
|
||||
if (removed > 0)
|
||||
_logger.LogInformation("Retention purge removed {Count} locally stored emails", removed);
|
||||
return removed;
|
||||
}
|
||||
|
||||
private async Task<int> PurgeWhereAsync(
|
||||
System.Linq.Expressions.Expression<Func<Domain.Entities.Email, bool>> predicate,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// RemoveRange (not ExecuteDelete) so dependent rows (labels/attachments) cascade via
|
||||
// the model on every provider, and the InMemory test provider works identically.
|
||||
var doomed = await _db.Emails.Where(predicate).ToListAsync(ct);
|
||||
if (doomed.Count == 0) return 0;
|
||||
_db.Emails.RemoveRange(doomed);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return doomed.Count;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user