5ade3f4f48
Tests-003: temp auth-DB directories leaked under %TEMP%. Added the TempDatabaseDirectory IDisposable helper (clears the Sqlite connection pool, then recursively deletes); SqliteAuthStoreTests and ApiKeyAdminCliRunnerTests now dispose every directory they create. Tests-004: added end-to-end coverage composing the real authorization interceptor in front of the real MxAccessGatewayService, plus scope-resolver tests confirming an unmapped request type fails closed to the admin scope. Tests-005: added coverage for a worker faulting mid-command — a pipe disconnect and a worker fault while an InvokeAsync is in flight both fail the pending invoke. No product change needed. Tests-006 (re-triaged): the flaky ReadLoop_WhenClientFaults_KillsOwnedWorkerProcess is a test race, not a product bug — the kill runs synchronously inside SetFaulted. Rewrote it to await FakeWorkerProcess exit deterministically, and replaced fixed Task.Delay timing in the late-reply and heartbeat tests with FIFO ordering and an injected ManualTimeProvider. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
541 lines
22 KiB
C#
541 lines
22 KiB
C#
using System.Runtime.CompilerServices;
|
|
using Grpc.Core;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
using MxGateway.Contracts;
|
|
using MxGateway.Contracts.Proto;
|
|
using MxGateway.Server.Configuration;
|
|
using MxGateway.Server.Grpc;
|
|
using MxGateway.Server.Metrics;
|
|
using MxGateway.Server.Security.Authentication;
|
|
using MxGateway.Server.Security.Authorization;
|
|
using MxGateway.Server.Sessions;
|
|
|
|
namespace MxGateway.Tests.Security.Authorization;
|
|
|
|
public sealed class GatewayGrpcAuthorizationInterceptorTests
|
|
{
|
|
/// <summary>Verifies that missing API key returns unauthenticated status.</summary>
|
|
[Fact]
|
|
public async Task UnaryServerHandler_MissingApiKey_ReturnsUnauthenticated()
|
|
{
|
|
GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
|
|
new FakeApiKeyVerifier(ApiKeyVerificationResult.Fail(
|
|
ApiKeyVerificationFailure.MissingOrMalformedCredentials)),
|
|
new GatewayRequestIdentityAccessor());
|
|
|
|
RpcException exception = await Assert.ThrowsAsync<RpcException>(
|
|
() => interceptor.UnaryServerHandler(
|
|
new OpenSessionRequest(),
|
|
new TestServerCallContext([]),
|
|
(_, _) => Task.FromResult(new OpenSessionReply())));
|
|
|
|
Assert.Equal(StatusCode.Unauthenticated, exception.StatusCode);
|
|
Assert.DoesNotContain("secret", exception.Status.Detail, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
/// <summary>Verifies that invalid API key error does not expose raw credentials.</summary>
|
|
[Fact]
|
|
public async Task UnaryServerHandler_InvalidApiKey_DoesNotExposeRawCredentialInStatus()
|
|
{
|
|
GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
|
|
new FakeApiKeyVerifier(ApiKeyVerificationResult.Fail(ApiKeyVerificationFailure.SecretMismatch)),
|
|
new GatewayRequestIdentityAccessor());
|
|
|
|
RpcException exception = await Assert.ThrowsAsync<RpcException>(
|
|
() => interceptor.UnaryServerHandler(
|
|
new OpenSessionRequest(),
|
|
ContextWithAuthorization("Bearer mxgw_operator01_super-secret"),
|
|
(_, _) => Task.FromResult(new OpenSessionReply())));
|
|
|
|
Assert.Equal(StatusCode.Unauthenticated, exception.StatusCode);
|
|
Assert.DoesNotContain("super-secret", exception.Status.Detail, StringComparison.Ordinal);
|
|
}
|
|
|
|
/// <summary>Verifies that valid key without required scope returns permission denied.</summary>
|
|
[Fact]
|
|
public async Task UnaryServerHandler_ValidApiKeyMissingScope_ReturnsPermissionDenied()
|
|
{
|
|
GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
|
|
new FakeApiKeyVerifier(SuccessWithScopes(GatewayScopes.EventsRead)),
|
|
new GatewayRequestIdentityAccessor());
|
|
|
|
RpcException exception = await Assert.ThrowsAsync<RpcException>(
|
|
() => interceptor.UnaryServerHandler(
|
|
new OpenSessionRequest(),
|
|
ContextWithAuthorization("Bearer mxgw_operator01_secret"),
|
|
(_, _) => Task.FromResult(new OpenSessionReply())));
|
|
|
|
Assert.Equal(StatusCode.PermissionDenied, exception.StatusCode);
|
|
Assert.Contains(GatewayScopes.SessionOpen, exception.Status.Detail, StringComparison.Ordinal);
|
|
}
|
|
|
|
/// <summary>Verifies that valid key with scope sets request identity for the handler.</summary>
|
|
[Fact]
|
|
public async Task UnaryServerHandler_ValidApiKeyWithScope_SetsRequestIdentity()
|
|
{
|
|
GatewayRequestIdentityAccessor identityAccessor = new();
|
|
ApiKeyIdentity? identitySeenByHandler = null;
|
|
GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
|
|
new FakeApiKeyVerifier(SuccessWithScopes(GatewayScopes.SessionOpen)),
|
|
identityAccessor);
|
|
|
|
OpenSessionReply reply = await interceptor.UnaryServerHandler(
|
|
new OpenSessionRequest(),
|
|
ContextWithAuthorization("Bearer mxgw_operator01_secret"),
|
|
(_, _) =>
|
|
{
|
|
identitySeenByHandler = identityAccessor.Current;
|
|
|
|
return Task.FromResult(new OpenSessionReply { SessionId = "session-1" });
|
|
});
|
|
|
|
Assert.Equal("session-1", reply.SessionId);
|
|
Assert.NotNull(identitySeenByHandler);
|
|
Assert.Equal("operator01", identitySeenByHandler.KeyId);
|
|
Assert.Null(identityAccessor.Current);
|
|
}
|
|
|
|
/// <summary>Verifies that server stream handler requires proper scope.</summary>
|
|
[Fact]
|
|
public async Task ServerStreamingServerHandler_ValidApiKeyMissingScope_ReturnsPermissionDenied()
|
|
{
|
|
GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
|
|
new FakeApiKeyVerifier(SuccessWithScopes(GatewayScopes.SessionOpen)),
|
|
new GatewayRequestIdentityAccessor());
|
|
|
|
RpcException exception = await Assert.ThrowsAsync<RpcException>(
|
|
() => interceptor.ServerStreamingServerHandler(
|
|
new StreamEventsRequest(),
|
|
new TestServerStreamWriter<MxEvent>(),
|
|
ContextWithAuthorization("Bearer mxgw_operator01_secret"),
|
|
(_, _, _) => Task.CompletedTask));
|
|
|
|
Assert.Equal(StatusCode.PermissionDenied, exception.StatusCode);
|
|
Assert.Contains(GatewayScopes.EventsRead, exception.Status.Detail, StringComparison.Ordinal);
|
|
}
|
|
|
|
/// <summary>Verifies that server stream handler allows streams with proper scope.</summary>
|
|
[Fact]
|
|
public async Task ServerStreamingServerHandler_ValidApiKeyWithScope_AllowsStream()
|
|
{
|
|
GatewayRequestIdentityAccessor identityAccessor = new();
|
|
GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
|
|
new FakeApiKeyVerifier(SuccessWithScopes(GatewayScopes.EventsRead)),
|
|
identityAccessor);
|
|
TestServerStreamWriter<MxEvent> streamWriter = new();
|
|
|
|
await interceptor.ServerStreamingServerHandler(
|
|
new StreamEventsRequest(),
|
|
streamWriter,
|
|
ContextWithAuthorization("Bearer mxgw_operator01_secret"),
|
|
async (_, writer, _) =>
|
|
{
|
|
Assert.Equal("operator01", identityAccessor.Current?.KeyId);
|
|
await writer.WriteAsync(new MxEvent { SessionId = "session-1" });
|
|
});
|
|
|
|
MxEvent eventMessage = Assert.Single(streamWriter.Messages);
|
|
Assert.Equal("session-1", eventMessage.SessionId);
|
|
Assert.Null(identityAccessor.Current);
|
|
}
|
|
|
|
/// <summary>Verifies that disabled authentication skips API key verification.</summary>
|
|
[Fact]
|
|
public async Task UnaryServerHandler_AuthenticationDisabled_SkipsApiKeyVerification()
|
|
{
|
|
GatewayRequestIdentityAccessor identityAccessor = new();
|
|
FakeApiKeyVerifier verifier = new(ApiKeyVerificationResult.Fail(
|
|
ApiKeyVerificationFailure.MissingOrMalformedCredentials));
|
|
GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
|
|
verifier,
|
|
identityAccessor,
|
|
AuthenticationMode.Disabled);
|
|
|
|
OpenSessionReply reply = await interceptor.UnaryServerHandler(
|
|
new OpenSessionRequest(),
|
|
new TestServerCallContext([]),
|
|
(_, _) => Task.FromResult(new OpenSessionReply { SessionId = "session-1" }));
|
|
|
|
Assert.Equal("session-1", reply.SessionId);
|
|
Assert.False(verifier.WasCalled);
|
|
Assert.Null(identityAccessor.Current);
|
|
}
|
|
|
|
/// <summary>
|
|
/// End-to-end composition test: runs an <c>OpenSession</c> call through the real
|
|
/// interceptor in front of the real <see cref="MxAccessGatewayService"/> with a key
|
|
/// that lacks the <c>session:open</c> scope, and asserts the interceptor denies the
|
|
/// call with <see cref="StatusCode.PermissionDenied"/> before the service runs.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task InterceptorComposedWithService_OpenSessionMissingScope_DeniesBeforeServiceRuns()
|
|
{
|
|
GatewayRequestIdentityAccessor identityAccessor = new();
|
|
RecordingSessionManager sessionManager = new();
|
|
GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
|
|
new FakeApiKeyVerifier(SuccessWithScopes(GatewayScopes.EventsRead)),
|
|
identityAccessor);
|
|
MxAccessGatewayService service = CreateService(sessionManager, identityAccessor);
|
|
|
|
RpcException exception = await Assert.ThrowsAsync<RpcException>(
|
|
() => interceptor.UnaryServerHandler(
|
|
new OpenSessionRequest { ClientSessionName = "operator-session" },
|
|
ContextWithAuthorization("Bearer mxgw_operator01_secret"),
|
|
(request, context) => service.OpenSession(request, context)));
|
|
|
|
Assert.Equal(StatusCode.PermissionDenied, exception.StatusCode);
|
|
Assert.Contains(GatewayScopes.SessionOpen, exception.Status.Detail, StringComparison.Ordinal);
|
|
Assert.Equal(0, sessionManager.OpenSessionCount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// End-to-end composition test: runs an <c>OpenSession</c> call through the real
|
|
/// interceptor in front of the real <see cref="MxAccessGatewayService"/> with a key
|
|
/// that holds <c>session:open</c>, and asserts the service runs and observes the
|
|
/// interceptor-supplied identity.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task InterceptorComposedWithService_OpenSessionWithScope_RunsServiceWithIdentity()
|
|
{
|
|
GatewayRequestIdentityAccessor identityAccessor = new();
|
|
RecordingSessionManager sessionManager = new();
|
|
GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
|
|
new FakeApiKeyVerifier(SuccessWithScopes(GatewayScopes.SessionOpen)),
|
|
identityAccessor);
|
|
MxAccessGatewayService service = CreateService(sessionManager, identityAccessor);
|
|
|
|
OpenSessionReply reply = await interceptor.UnaryServerHandler(
|
|
new OpenSessionRequest { ClientSessionName = "operator-session" },
|
|
ContextWithAuthorization("Bearer mxgw_operator01_secret"),
|
|
(request, context) => service.OpenSession(request, context));
|
|
|
|
Assert.Equal("session-1", reply.SessionId);
|
|
Assert.Equal(1, sessionManager.OpenSessionCount);
|
|
Assert.Equal("Operator Key", sessionManager.LastClientIdentity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// End-to-end composition test: an <c>Invoke</c> call through the real interceptor in
|
|
/// front of the real service with a key holding only <c>invoke:read</c> is denied
|
|
/// because the wrapped command is a write, confirming command-scope mapping is
|
|
/// enforced through the full composition.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task InterceptorComposedWithService_InvokeWriteCommandWithReadScope_DeniesBeforeServiceRuns()
|
|
{
|
|
GatewayRequestIdentityAccessor identityAccessor = new();
|
|
RecordingSessionManager sessionManager = new();
|
|
GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
|
|
new FakeApiKeyVerifier(SuccessWithScopes(GatewayScopes.InvokeRead)),
|
|
identityAccessor);
|
|
MxAccessGatewayService service = CreateService(sessionManager, identityAccessor);
|
|
MxCommandRequest request = new()
|
|
{
|
|
SessionId = "session-1",
|
|
Command = new MxCommand
|
|
{
|
|
Kind = MxCommandKind.Write,
|
|
Write = new WriteCommand { ServerHandle = 1, ItemHandle = 2 },
|
|
},
|
|
};
|
|
|
|
RpcException exception = await Assert.ThrowsAsync<RpcException>(
|
|
() => interceptor.UnaryServerHandler(
|
|
request,
|
|
ContextWithAuthorization("Bearer mxgw_operator01_secret"),
|
|
(req, context) => service.Invoke(req, context)));
|
|
|
|
Assert.Equal(StatusCode.PermissionDenied, exception.StatusCode);
|
|
Assert.Contains(GatewayScopes.InvokeWrite, exception.Status.Detail, StringComparison.Ordinal);
|
|
Assert.Equal(0, sessionManager.InvokeCount);
|
|
}
|
|
|
|
private static MxAccessGatewayService CreateService(
|
|
ISessionManager sessionManager,
|
|
IGatewayRequestIdentityAccessor identityAccessor)
|
|
{
|
|
return new MxAccessGatewayService(
|
|
sessionManager,
|
|
identityAccessor,
|
|
new AllowAllConstraintEnforcer(),
|
|
new MxAccessGrpcRequestValidator(),
|
|
new MxAccessGrpcMapper(),
|
|
new NoOpEventStreamService(),
|
|
new GatewayMetrics(),
|
|
NullLogger<MxAccessGatewayService>.Instance);
|
|
}
|
|
|
|
private static GatewayGrpcAuthorizationInterceptor CreateInterceptor(
|
|
IApiKeyVerifier apiKeyVerifier,
|
|
IGatewayRequestIdentityAccessor identityAccessor,
|
|
AuthenticationMode authenticationMode = AuthenticationMode.ApiKey)
|
|
{
|
|
return new GatewayGrpcAuthorizationInterceptor(
|
|
apiKeyVerifier,
|
|
new GatewayGrpcScopeResolver(),
|
|
identityAccessor,
|
|
Options.Create(new GatewayOptions
|
|
{
|
|
Authentication = new AuthenticationOptions
|
|
{
|
|
Mode = authenticationMode
|
|
}
|
|
}));
|
|
}
|
|
|
|
private static ApiKeyVerificationResult SuccessWithScopes(params string[] scopes)
|
|
{
|
|
return ApiKeyVerificationResult.Success(new ApiKeyIdentity(
|
|
KeyId: "operator01",
|
|
KeyPrefix: "mxgw_operator01",
|
|
DisplayName: "Operator Key",
|
|
Scopes: new HashSet<string>(scopes, StringComparer.Ordinal)));
|
|
}
|
|
|
|
private static TestServerCallContext ContextWithAuthorization(string authorizationHeader)
|
|
{
|
|
return new TestServerCallContext([new Metadata.Entry("authorization", authorizationHeader)]);
|
|
}
|
|
|
|
/// <summary>Records whether the gateway service ran past the interceptor for composition tests.</summary>
|
|
private sealed class RecordingSessionManager : ISessionManager
|
|
{
|
|
/// <summary>Gets the number of times OpenSessionAsync was invoked.</summary>
|
|
public int OpenSessionCount { get; private set; }
|
|
|
|
/// <summary>Gets the number of times InvokeAsync was invoked.</summary>
|
|
public int InvokeCount { get; private set; }
|
|
|
|
/// <summary>Gets the last client identity passed to OpenSessionAsync.</summary>
|
|
public string? LastClientIdentity { get; private set; }
|
|
|
|
/// <inheritdoc />
|
|
public Task<GatewaySession> OpenSessionAsync(
|
|
SessionOpenRequest request,
|
|
string? clientIdentity,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
OpenSessionCount++;
|
|
LastClientIdentity = clientIdentity;
|
|
|
|
GatewaySession session = new(
|
|
"session-1",
|
|
GatewayContractInfo.DefaultBackendName,
|
|
"pipe",
|
|
"nonce",
|
|
clientIdentity ?? "client",
|
|
"client-session",
|
|
"client-correlation",
|
|
TimeSpan.FromSeconds(7),
|
|
TimeSpan.FromSeconds(30),
|
|
TimeSpan.FromSeconds(10),
|
|
DateTimeOffset.UtcNow);
|
|
|
|
return Task.FromResult(session);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public bool TryGetSession(string sessionId, out GatewaySession session)
|
|
{
|
|
session = null!;
|
|
return false;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<WorkerCommandReply> InvokeAsync(
|
|
string sessionId,
|
|
WorkerCommand command,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
InvokeCount++;
|
|
return Task.FromResult(new WorkerCommandReply());
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IAsyncEnumerable<WorkerEvent> ReadEventsAsync(
|
|
string sessionId,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
return AsyncEnumerable.Empty<WorkerEvent>();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<SessionCloseResult> CloseSessionAsync(
|
|
string sessionId,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult(new SessionCloseResult(sessionId, SessionState.Closed, AlreadyClosed: false));
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<int> CloseExpiredLeasesAsync(
|
|
DateTimeOffset now,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
return Task.FromResult(0);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task ShutdownAsync(CancellationToken cancellationToken)
|
|
{
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
/// <summary>Event stream service that yields nothing; alarm/event RPCs are not under test here.</summary>
|
|
private sealed class NoOpEventStreamService : IEventStreamService
|
|
{
|
|
/// <inheritdoc />
|
|
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
|
StreamEventsRequest request,
|
|
[EnumeratorCancellation] CancellationToken cancellationToken)
|
|
{
|
|
await Task.CompletedTask;
|
|
yield break;
|
|
}
|
|
}
|
|
|
|
/// <summary>Constraint enforcer that permits every operation for composition tests.</summary>
|
|
private sealed class AllowAllConstraintEnforcer : IConstraintEnforcer
|
|
{
|
|
/// <inheritdoc />
|
|
public Task<ConstraintFailure?> CheckReadTagAsync(
|
|
ApiKeyIdentity? identity,
|
|
string tagAddress,
|
|
CancellationToken cancellationToken) => Task.FromResult<ConstraintFailure?>(null);
|
|
|
|
/// <inheritdoc />
|
|
public Task<ConstraintFailure?> CheckReadHandleAsync(
|
|
ApiKeyIdentity? identity,
|
|
GatewaySession session,
|
|
int serverHandle,
|
|
int itemHandle,
|
|
CancellationToken cancellationToken) => Task.FromResult<ConstraintFailure?>(null);
|
|
|
|
/// <inheritdoc />
|
|
public Task<ConstraintFailure?> CheckWriteHandleAsync(
|
|
ApiKeyIdentity? identity,
|
|
GatewaySession session,
|
|
int serverHandle,
|
|
int itemHandle,
|
|
CancellationToken cancellationToken) => Task.FromResult<ConstraintFailure?>(null);
|
|
|
|
/// <inheritdoc />
|
|
public Task RecordDenialAsync(
|
|
ApiKeyIdentity? identity,
|
|
string commandKind,
|
|
string target,
|
|
ConstraintFailure failure,
|
|
CancellationToken cancellationToken) => Task.CompletedTask;
|
|
}
|
|
|
|
private sealed class FakeApiKeyVerifier(ApiKeyVerificationResult result) : IApiKeyVerifier
|
|
{
|
|
/// <summary>Gets whether the verifier was called.</summary>
|
|
public bool WasCalled { get; private set; }
|
|
|
|
/// <summary>Gets the last authorization header seen by the verifier.</summary>
|
|
public string? LastAuthorizationHeader { get; private set; }
|
|
|
|
/// <summary>Verifies the authorization header against stored result.</summary>
|
|
/// <param name="authorizationHeader">The authorization header to verify.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>Configured verification result.</returns>
|
|
public Task<ApiKeyVerificationResult> VerifyAsync(
|
|
string? authorizationHeader,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
WasCalled = true;
|
|
LastAuthorizationHeader = authorizationHeader;
|
|
|
|
return Task.FromResult(result);
|
|
}
|
|
}
|
|
|
|
private sealed class TestServerStreamWriter<T> : IServerStreamWriter<T>
|
|
{
|
|
/// <summary>Gets messages written to the stream.</summary>
|
|
public List<T> Messages { get; } = [];
|
|
|
|
/// <summary>Gets or sets write options for the stream.</summary>
|
|
public WriteOptions? WriteOptions { get; set; }
|
|
|
|
/// <summary>Writes a message to the stream.</summary>
|
|
/// <param name="message">The message to write.</param>
|
|
/// <returns>Task representing the write operation.</returns>
|
|
public Task WriteAsync(T message)
|
|
{
|
|
Messages.Add(message);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
private sealed class TestServerCallContext(
|
|
Metadata requestHeaders,
|
|
CancellationToken cancellationToken = default) : ServerCallContext
|
|
{
|
|
private readonly Metadata responseTrailers = [];
|
|
private readonly Dictionary<object, object> userState = [];
|
|
private Status status;
|
|
private WriteOptions? writeOptions;
|
|
|
|
/// <inheritdoc />
|
|
protected override string MethodCore => "/mxaccess_gateway.v1.MxAccessGateway/Test";
|
|
|
|
/// <inheritdoc />
|
|
protected override string HostCore => "localhost";
|
|
|
|
/// <inheritdoc />
|
|
protected override string PeerCore => "ipv4:127.0.0.1:5000";
|
|
|
|
/// <inheritdoc />
|
|
protected override DateTime DeadlineCore => DateTime.UtcNow.AddMinutes(1);
|
|
|
|
/// <inheritdoc />
|
|
protected override Metadata RequestHeadersCore => requestHeaders;
|
|
|
|
/// <inheritdoc />
|
|
protected override CancellationToken CancellationTokenCore => cancellationToken;
|
|
|
|
/// <inheritdoc />
|
|
protected override Metadata ResponseTrailersCore => responseTrailers;
|
|
|
|
/// <inheritdoc />
|
|
protected override Status StatusCore
|
|
{
|
|
get => status;
|
|
set => status = value;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override WriteOptions? WriteOptionsCore
|
|
{
|
|
get => writeOptions;
|
|
set => writeOptions = value;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override AuthContext AuthContextCore { get; } = new(
|
|
string.Empty,
|
|
new Dictionary<string, List<AuthProperty>>(StringComparer.Ordinal));
|
|
|
|
/// <inheritdoc />
|
|
protected override IDictionary<object, object> UserStateCore => userState;
|
|
|
|
/// <inheritdoc />
|
|
protected override Task WriteResponseHeadersAsyncCore(Metadata responseHeaders)
|
|
{
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override ContextPropagationToken CreatePropagationTokenCore(
|
|
ContextPropagationOptions? options)
|
|
{
|
|
throw new NotSupportedException();
|
|
}
|
|
}
|
|
}
|