fix(archreview): gateway core P0 remediation (GWC-01/02/03, TST-02, TST-12)
Interlocking changes across the gateway server (shared GatewaySession.cs / SessionManager.cs), committed together: - GWC-01 (Critical): alarm monitor now attaches as an internal (non-counted) distributor subscriber instead of a second raw drain of the single worker event channel; WorkerClient._events -> SingleReader with a claimed-once guard so a future dual-consumer regression throws loudly. - GWC-02 (High): faulted sessions are swept in CloseExpiredLeasesAsync (IsFaultedReapable + FaultedReason); new FaultedGraceSeconds (default 0). - GWC-03 (High): configurable MaxSparseArrayLength (default 1_000_000) enforced before allocation. - TST-02 (High, security): StreamEvents attach now enforces the opening key id -> PermissionDenied on owner mismatch. - TST-12 (Medium): CLAUDE.md retention-defaults sentence corrected. Verified: NonWindows build clean; targeted tests 135/135 on macOS, plus WorkerClientTests 18/18 on the Windows host.
This commit is contained in:
@@ -48,6 +48,7 @@ public sealed class EventStreamService(
|
||||
/// </remarks>
|
||||
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||
StreamEventsRequest request,
|
||||
string? callerKeyId,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
if (!sessionManager.TryGetSession(request.SessionId, out GatewaySession? session) || session is null)
|
||||
@@ -57,6 +58,19 @@ public sealed class EventStreamService(
|
||||
$"Session {request.SessionId} was not found.");
|
||||
}
|
||||
|
||||
// Owner-scoped attach (TST-02, security control): a session's event stream may be
|
||||
// attached or reattached ONLY by the API key that opened the session. The detach-grace
|
||||
// and fan-out retention windows are on by default, so without this check any event-scoped
|
||||
// key that learns a session id could attach to another key's retained session and receive
|
||||
// its replayed and live data. Ordinal comparison; null owner (session opened with no auth)
|
||||
// matches only a null caller key.
|
||||
if (!string.Equals(session.OwnerKeyId, callerKeyId, StringComparison.Ordinal))
|
||||
{
|
||||
throw new SessionManagerException(
|
||||
SessionManagerErrorCode.PermissionDenied,
|
||||
$"Session {request.SessionId} is owned by a different API key; event-stream attach is owner-scoped.");
|
||||
}
|
||||
|
||||
// No `using` here — subscriber.Dispose() is called exactly once in the finally
|
||||
// block below, which also disposes the reader. A `using` declaration would add a
|
||||
// second Dispose on the same path and double-decrement the session subscriber count.
|
||||
|
||||
@@ -11,9 +11,16 @@ public interface IEventStreamService
|
||||
/// Streams events for the specified session to the caller.
|
||||
/// </summary>
|
||||
/// <param name="request">Request payload.</param>
|
||||
/// <param name="callerKeyId">
|
||||
/// The API key id of the calling client, used to enforce that only the key that opened a
|
||||
/// session may attach or reattach its event stream. <see langword="null"/> when the call
|
||||
/// is unauthenticated (e.g. auth disabled), which only matches a session opened with no
|
||||
/// owner key.
|
||||
/// </param>
|
||||
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
||||
/// <returns>The events emitted for the requested session.</returns>
|
||||
IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||
StreamEventsRequest request,
|
||||
string? callerKeyId,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ public sealed class MxAccessGatewayService(
|
||||
{
|
||||
requestValidator.ValidateStreamEvents(request);
|
||||
await foreach (MxEvent publicEvent in eventStreamService
|
||||
.StreamEventsAsync(request, context.CancellationToken)
|
||||
.StreamEventsAsync(request, identityAccessor.Current?.KeyId, context.CancellationToken)
|
||||
.WithCancellation(context.CancellationToken)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
@@ -931,6 +931,7 @@ public sealed class MxAccessGatewayService(
|
||||
SessionManagerErrorCode.SessionLimitExceeded => StatusCode.ResourceExhausted,
|
||||
SessionManagerErrorCode.OpenFailed => StatusCode.Unavailable,
|
||||
SessionManagerErrorCode.CloseFailed => StatusCode.Unavailable,
|
||||
SessionManagerErrorCode.PermissionDenied => StatusCode.PermissionDenied,
|
||||
_ => StatusCode.Unavailable,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user