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.
169 lines
7.3 KiB
C#
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);
|
|
}
|
|
}
|