Files
scadalink-design/deprecated/lmxproxy/src-reference/ZB.MOM.WW.LmxProxy.Host/Security/ApiKeyInterceptor.cs
Joseph Doherty 9dccf8e72f deprecate(lmxproxy): move all LmxProxy code, tests, and docs to deprecated/
LmxProxy is no longer needed. Moved the entire lmxproxy/ workspace, DCL
adapter files, and related docs to deprecated/. Removed LmxProxy registration
from DataConnectionFactory, project reference from DCL, protocol option from
UI, and cleaned up all requirement docs.
2026-04-08 15:56:23 -04:00

169 lines
7.3 KiB
C#

using System;
using System.Linq;
using System.Threading.Tasks;
using Grpc.Core;
using Grpc.Core.Interceptors;
using Serilog;
namespace ZB.MOM.WW.LmxProxy.Host.Security
{
/// <summary>
/// gRPC interceptor for API key authentication.
/// Validates API keys for incoming requests and enforces role-based access control.
/// </summary>
public class ApiKeyInterceptor : Interceptor
{
private static readonly ILogger Logger = Log.ForContext<ApiKeyInterceptor>();
/// <summary>
/// List of gRPC method names that require write access.
/// </summary>
private static readonly string[] WriteMethodNames =
{
"Write",
"WriteBatch",
"WriteBatchAndWait"
};
private readonly ApiKeyService _apiKeyService;
/// <summary>
/// Initializes a new instance of the <see cref="ApiKeyInterceptor" /> class.
/// </summary>
/// <param name="apiKeyService">The API key service used for validation.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="apiKeyService" /> is null.</exception>
public ApiKeyInterceptor(ApiKeyService apiKeyService)
{
_apiKeyService = apiKeyService ?? throw new ArgumentNullException(nameof(apiKeyService));
}
/// <summary>
/// Handles unary gRPC calls, validating API key and enforcing permissions.
/// </summary>
/// <typeparam name="TRequest">The request type.</typeparam>
/// <typeparam name="TResponse">The response type.</typeparam>
/// <param name="request">The request message.</param>
/// <param name="context">The server call context.</param>
/// <param name="continuation">The continuation delegate.</param>
/// <returns>The response message.</returns>
/// <exception cref="RpcException">Thrown if authentication or authorization fails.</exception>
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
string apiKey = GetApiKeyFromContext(context);
string methodName = GetMethodName(context.Method);
if (string.IsNullOrEmpty(apiKey))
{
Logger.Warning("Missing API key for method {Method} from {Peer}",
context.Method, context.Peer);
throw new RpcException(new Status(StatusCode.Unauthenticated, "API key is required"));
}
ApiKey? key = _apiKeyService.ValidateApiKey(apiKey);
if (key == null)
{
Logger.Warning("Invalid API key for method {Method} from {Peer}",
context.Method, context.Peer);
throw new RpcException(new Status(StatusCode.Unauthenticated, "Invalid API key"));
}
// Check if method requires write access
if (IsWriteMethod(methodName) && key.Role != ApiKeyRole.ReadWrite)
{
Logger.Warning("Insufficient permissions for method {Method} with API key {Description}",
context.Method, key.Description);
throw new RpcException(new Status(StatusCode.PermissionDenied,
"API key does not have write permissions"));
}
// Add API key info to context items for use in service methods
context.UserState["ApiKey"] = key;
Logger.Debug("Authorized method {Method} for API key {Description}",
context.Method, key.Description);
return await continuation(request, context);
}
/// <summary>
/// Handles server streaming gRPC calls, validating API key and enforcing permissions.
/// </summary>
/// <typeparam name="TRequest">The request type.</typeparam>
/// <typeparam name="TResponse">The response type.</typeparam>
/// <param name="request">The request message.</param>
/// <param name="responseStream">The response stream writer.</param>
/// <param name="context">The server call context.</param>
/// <param name="continuation">The continuation delegate.</param>
/// <returns>A task representing the asynchronous operation.</returns>
/// <exception cref="RpcException">Thrown if authentication fails.</exception>
public override async Task ServerStreamingServerHandler<TRequest, TResponse>(
TRequest request,
IServerStreamWriter<TResponse> responseStream,
ServerCallContext context,
ServerStreamingServerMethod<TRequest, TResponse> continuation)
{
string apiKey = GetApiKeyFromContext(context);
if (string.IsNullOrEmpty(apiKey))
{
Logger.Warning("Missing API key for streaming method {Method} from {Peer}",
context.Method, context.Peer);
throw new RpcException(new Status(StatusCode.Unauthenticated, "API key is required"));
}
ApiKey? key = _apiKeyService.ValidateApiKey(apiKey);
if (key == null)
{
Logger.Warning("Invalid API key for streaming method {Method} from {Peer}",
context.Method, context.Peer);
throw new RpcException(new Status(StatusCode.Unauthenticated, "Invalid API key"));
}
// Add API key info to context items
context.UserState["ApiKey"] = key;
Logger.Debug("Authorized streaming method {Method} for API key {Description}",
context.Method, key.Description);
await continuation(request, responseStream, context);
}
/// <summary>
/// Extracts the API key from the gRPC request headers.
/// </summary>
/// <param name="context">The server call context.</param>
/// <returns>The API key value, or an empty string if not found.</returns>
private static string GetApiKeyFromContext(ServerCallContext context)
{
// Check for API key in metadata (headers)
Metadata.Entry? entry = context.RequestHeaders.FirstOrDefault(e =>
e.Key.Equals("x-api-key", StringComparison.OrdinalIgnoreCase));
return entry?.Value ?? string.Empty;
}
/// <summary>
/// Gets the method name from the full gRPC method string.
/// </summary>
/// <param name="method">The full method string (e.g., /package.Service/Method).</param>
/// <returns>The method name.</returns>
private static string GetMethodName(string method)
{
// Method format is /package.Service/Method
int lastSlash = method.LastIndexOf('/');
return lastSlash >= 0 ? method.Substring(lastSlash + 1) : method;
}
/// <summary>
/// Determines whether the specified method name requires write access.
/// </summary>
/// <param name="methodName">The method name.</param>
/// <returns><c>true</c> if the method requires write access; otherwise, <c>false</c>.</returns>
private static bool IsWriteMethod(string methodName) =>
WriteMethodNames.Contains(methodName, StringComparer.OrdinalIgnoreCase);
}
}