fix(backend): key IdempotencyStore on caller + idem key (RB-18)
IdempotencyStore keyed a replayed submission on the raw Idempotency-Key
header alone. Two different callers who send the same header value
shared one cache slot: the second caller received the first caller's
cached reference instead of running its own submission.
Program.cs now composes the key as "{SubjectId}:{idemKey}" in the
Submit helper, so the cache is scoped per caller. Add a test that
proves a caller cannot replay another caller's idempotency key and
receive their cached result.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -987,12 +987,14 @@ void LogBrief(HttpContext ctx, string action, (BriefStore.Outcome outcome, Brief
|
||||
// generated reference and the caller's correlation id (the observability seam — a
|
||||
// real system ships this to structured logging / an audit store). A repeated
|
||||
// Idempotency-Key short-circuits to the first call's result — see IdempotencyStore
|
||||
// — so a retried submit dedupes instead of minting a second reference.
|
||||
// — so a retried submit dedupes instead of minting a second reference. The key is
|
||||
// scoped to the caller (RB-18/BIO-018): two callers who happen to send the same
|
||||
// client-chosen header value do not share a cached result.
|
||||
IResult Submit(HttpContext ctx, string kind, string? reject, IReadOnlyList<DocumentRefDto>? documents = null)
|
||||
{
|
||||
var cid = ctx.Items.TryGetValue("CorrelationId", out var v) ? (string)v! : "none";
|
||||
var idemKey = ctx.Request.Headers.TryGetValue("Idempotency-Key", out var k) && !string.IsNullOrEmpty(k)
|
||||
? k.ToString()
|
||||
? $"{ctx.Caller().SubjectId}:{k}"
|
||||
: null;
|
||||
|
||||
if (idemKey is not null && IdempotencyStore.TryGet(idemKey, out var cached))
|
||||
|
||||
Reference in New Issue
Block a user