Cambios en la OPE para aceptar peticiones de gestiona
This commit is contained in:
@@ -55,6 +55,23 @@ public sealed class OpeProtocolTests
|
|||||||
Assert.Equal("53/2026", result.Request!.Data["FIELD_0"].Value);
|
Assert.Equal("53/2026", result.Request!.Data["FIELD_0"].Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RequestReader_AcceptsGestionaLinksNullProperty()
|
||||||
|
{
|
||||||
|
var context = CreateHttpContext(
|
||||||
|
OpeMediaTypes.GenericOperationRequest,
|
||||||
|
"""
|
||||||
|
{"data":{"FIELD_0":{"type":"STRING","value":"68/2026"}},"links":null}
|
||||||
|
""");
|
||||||
|
|
||||||
|
var result = await GenericOperationRequestReader.ReadAsync(
|
||||||
|
context.Request,
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.True(result.IsValid);
|
||||||
|
Assert.Equal("68/2026", result.Request!.Data["FIELD_0"].Value);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RequestReader_ReturnsFieldErrorsForUnexpectedInput()
|
public async Task RequestReader_ReturnsFieldErrorsForUnexpectedInput()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace ApiOPE.Contracts;
|
namespace ApiOPE.Contracts;
|
||||||
@@ -7,7 +8,10 @@ public sealed record OpeFieldValue(
|
|||||||
[property: JsonPropertyName("value")] string Value);
|
[property: JsonPropertyName("value")] string Value);
|
||||||
|
|
||||||
public sealed record OpeDataEnvelope(
|
public sealed record OpeDataEnvelope(
|
||||||
[property: JsonPropertyName("data")] IReadOnlyDictionary<string, OpeFieldValue> Data);
|
[property: JsonPropertyName("data")] IReadOnlyDictionary<string, OpeFieldValue> Data,
|
||||||
|
[property: JsonPropertyName("links")]
|
||||||
|
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
JsonElement? Links = null);
|
||||||
|
|
||||||
public sealed record OpeVersionResponse(
|
public sealed record OpeVersionResponse(
|
||||||
[property: JsonPropertyName("version")] string Version);
|
[property: JsonPropertyName("version")] string Version);
|
||||||
|
|||||||
@@ -10,6 +10,16 @@ namespace ApiOPE.Security;
|
|||||||
|
|
||||||
public sealed class OpeAuthenticationMiddleware
|
public sealed class OpeAuthenticationMiddleware
|
||||||
{
|
{
|
||||||
|
private const int MaxLoggedBodyLength = 64 * 1024;
|
||||||
|
|
||||||
|
private static readonly HashSet<string> SensitiveHeaders = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
TokenHeaderName,
|
||||||
|
"Authorization",
|
||||||
|
"Cookie",
|
||||||
|
"Set-Cookie"
|
||||||
|
};
|
||||||
|
|
||||||
public const string TokenHeaderName = "X-Rest-Basic-Token";
|
public const string TokenHeaderName = "X-Rest-Basic-Token";
|
||||||
public const string OrganizationIdHeaderName = "X-Organization-ID";
|
public const string OrganizationIdHeaderName = "X-Organization-ID";
|
||||||
public const string OrganizationDir3HeaderName = "X-Organization-DIR3";
|
public const string OrganizationDir3HeaderName = "X-Organization-DIR3";
|
||||||
@@ -39,16 +49,30 @@ public sealed class OpeAuthenticationMiddleware
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var requestHeaders = FormatHeaders(context.Request.Headers);
|
||||||
|
var requestBody = await ReadRequestBodyAsync(context.Request, context.RequestAborted);
|
||||||
|
|
||||||
if (!TryGetSingleHeader(context.Request.Headers, TokenHeaderName, out var token))
|
if (!TryGetSingleHeader(context.Request.Headers, TokenHeaderName, out var token))
|
||||||
{
|
{
|
||||||
await RejectAsync(context, "Falta la cabecera de autenticacion OPE.", requestFileLogger);
|
await RejectAsync(
|
||||||
|
context,
|
||||||
|
"Falta la cabecera de autenticacion OPE.",
|
||||||
|
requestFileLogger,
|
||||||
|
requestHeaders,
|
||||||
|
requestBody);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var validation = tokenValidator.Validate(token, authentication.Resource, authentication.VersionOptional);
|
var validation = tokenValidator.Validate(token, authentication.Resource, authentication.VersionOptional);
|
||||||
if (!validation.IsValid || validation.RequestContext is null)
|
if (!validation.IsValid || validation.RequestContext is null)
|
||||||
{
|
{
|
||||||
await RejectAsync(context, validation.FailureReason, requestFileLogger, token);
|
await RejectAsync(
|
||||||
|
context,
|
||||||
|
validation.FailureReason,
|
||||||
|
requestFileLogger,
|
||||||
|
requestHeaders,
|
||||||
|
requestBody,
|
||||||
|
token);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,12 +93,21 @@ public sealed class OpeAuthenticationMiddleware
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var originalResponseBody = context.Response.Body;
|
||||||
|
await using var capturedResponseBody = new MemoryStream();
|
||||||
|
context.Response.Body = capturedResponseBody;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _next(context);
|
await _next(context);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
var responseBody = await ReadStreamAsync(capturedResponseBody, context.RequestAborted);
|
||||||
|
capturedResponseBody.Position = 0;
|
||||||
|
await capturedResponseBody.CopyToAsync(originalResponseBody, context.RequestAborted);
|
||||||
|
context.Response.Body = originalResponseBody;
|
||||||
|
|
||||||
await requestFileLogger.WriteAsync(new OpeRequestLogEntry(
|
await requestFileLogger.WriteAsync(new OpeRequestLogEntry(
|
||||||
DateTimeOffset.UtcNow,
|
DateTimeOffset.UtcNow,
|
||||||
context.Request.Method,
|
context.Request.Method,
|
||||||
@@ -91,6 +124,10 @@ public sealed class OpeAuthenticationMiddleware
|
|||||||
ReadHeader(context.Request.Headers, "Content-Type"),
|
ReadHeader(context.Request.Headers, "Content-Type"),
|
||||||
ReadHeader(context.Request.Headers, "Accept"),
|
ReadHeader(context.Request.Headers, "Accept"),
|
||||||
ReadHeader(context.Request.Headers, "User-Agent"),
|
ReadHeader(context.Request.Headers, "User-Agent"),
|
||||||
|
requestHeaders,
|
||||||
|
requestBody,
|
||||||
|
FormatHeaders(context.Response.Headers),
|
||||||
|
responseBody,
|
||||||
null));
|
null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,6 +136,8 @@ public sealed class OpeAuthenticationMiddleware
|
|||||||
HttpContext context,
|
HttpContext context,
|
||||||
string reason,
|
string reason,
|
||||||
OpeRequestFileLogger requestFileLogger,
|
OpeRequestFileLogger requestFileLogger,
|
||||||
|
string requestHeaders,
|
||||||
|
string requestBody,
|
||||||
string? token = null,
|
string? token = null,
|
||||||
OpeRequestContext? requestContext = null)
|
OpeRequestContext? requestContext = null)
|
||||||
{
|
{
|
||||||
@@ -108,6 +147,23 @@ public sealed class OpeAuthenticationMiddleware
|
|||||||
context.Request.Path,
|
context.Request.Path,
|
||||||
reason);
|
reason);
|
||||||
|
|
||||||
|
var originalResponseBody = context.Response.Body;
|
||||||
|
await using var capturedResponseBody = new MemoryStream();
|
||||||
|
context.Response.Body = capturedResponseBody;
|
||||||
|
|
||||||
|
await OpeResponseWriter.WriteErrorAsync(
|
||||||
|
context.Response,
|
||||||
|
StatusCodes.Status401Unauthorized,
|
||||||
|
"Unauthorized",
|
||||||
|
OpeErrorCauses.WrongSignature,
|
||||||
|
null,
|
||||||
|
context.RequestAborted);
|
||||||
|
|
||||||
|
var responseBody = await ReadStreamAsync(capturedResponseBody, context.RequestAborted);
|
||||||
|
capturedResponseBody.Position = 0;
|
||||||
|
await capturedResponseBody.CopyToAsync(originalResponseBody, context.RequestAborted);
|
||||||
|
context.Response.Body = originalResponseBody;
|
||||||
|
|
||||||
await requestFileLogger.WriteAsync(new OpeRequestLogEntry(
|
await requestFileLogger.WriteAsync(new OpeRequestLogEntry(
|
||||||
DateTimeOffset.UtcNow,
|
DateTimeOffset.UtcNow,
|
||||||
context.Request.Method,
|
context.Request.Method,
|
||||||
@@ -124,15 +180,11 @@ public sealed class OpeAuthenticationMiddleware
|
|||||||
ReadHeader(context.Request.Headers, "Content-Type"),
|
ReadHeader(context.Request.Headers, "Content-Type"),
|
||||||
ReadHeader(context.Request.Headers, "Accept"),
|
ReadHeader(context.Request.Headers, "Accept"),
|
||||||
ReadHeader(context.Request.Headers, "User-Agent"),
|
ReadHeader(context.Request.Headers, "User-Agent"),
|
||||||
|
requestHeaders,
|
||||||
|
requestBody,
|
||||||
|
FormatHeaders(context.Response.Headers),
|
||||||
|
responseBody,
|
||||||
reason));
|
reason));
|
||||||
|
|
||||||
await OpeResponseWriter.WriteErrorAsync(
|
|
||||||
context.Response,
|
|
||||||
StatusCodes.Status401Unauthorized,
|
|
||||||
"Unauthorized",
|
|
||||||
OpeErrorCauses.WrongSignature,
|
|
||||||
null,
|
|
||||||
context.RequestAborted);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string Fingerprint(string value)
|
private static string Fingerprint(string value)
|
||||||
@@ -143,6 +195,49 @@ public sealed class OpeAuthenticationMiddleware
|
|||||||
? string.Join(",", values.ToArray()).Trim()
|
? string.Join(",", values.ToArray()).Trim()
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
private static string FormatHeaders(IHeaderDictionary headers)
|
||||||
|
=> string.Join(
|
||||||
|
"; ",
|
||||||
|
headers
|
||||||
|
.OrderBy(header => header.Key, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(header =>
|
||||||
|
$"{header.Key}={(SensitiveHeaders.Contains(header.Key) ? "[OCULTO]" : string.Join(",", header.Value.ToArray()))}"));
|
||||||
|
|
||||||
|
private static async Task<string> ReadRequestBodyAsync(
|
||||||
|
HttpRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
request.EnableBuffering();
|
||||||
|
request.Body.Position = 0;
|
||||||
|
|
||||||
|
using var reader = new StreamReader(
|
||||||
|
request.Body,
|
||||||
|
Encoding.UTF8,
|
||||||
|
detectEncodingFromByteOrderMarks: true,
|
||||||
|
leaveOpen: true);
|
||||||
|
var body = await reader.ReadToEndAsync(cancellationToken);
|
||||||
|
request.Body.Position = 0;
|
||||||
|
return Truncate(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string> ReadStreamAsync(
|
||||||
|
Stream stream,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
stream.Position = 0;
|
||||||
|
using var reader = new StreamReader(
|
||||||
|
stream,
|
||||||
|
Encoding.UTF8,
|
||||||
|
detectEncodingFromByteOrderMarks: true,
|
||||||
|
leaveOpen: true);
|
||||||
|
return Truncate(await reader.ReadToEndAsync(cancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Truncate(string value)
|
||||||
|
=> value.Length <= MaxLoggedBodyLength
|
||||||
|
? value
|
||||||
|
: value[..MaxLoggedBodyLength] + " [TRUNCADO]";
|
||||||
|
|
||||||
private static bool TryGetSingleHeader(
|
private static bool TryGetSingleHeader(
|
||||||
IHeaderDictionary headers,
|
IHeaderDictionary headers,
|
||||||
string name,
|
string name,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public sealed class OpeRequestFileLogger
|
|||||||
Directory.CreateDirectory(directory);
|
Directory.CreateDirectory(directory);
|
||||||
}
|
}
|
||||||
|
|
||||||
var line = entry.ToDisplayLine() + Environment.NewLine;
|
var line = entry.ToDisplayBlock() + Environment.NewLine;
|
||||||
await _writeLock.WaitAsync(cancellationToken);
|
await _writeLock.WaitAsync(cancellationToken);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -66,27 +66,32 @@ public sealed record OpeRequestLogEntry(
|
|||||||
string? ContentType,
|
string? ContentType,
|
||||||
string? Accept,
|
string? Accept,
|
||||||
string? UserAgent,
|
string? UserAgent,
|
||||||
|
string? RequestHeaders,
|
||||||
|
string? RequestBody,
|
||||||
|
string? ResponseHeaders,
|
||||||
|
string? ResponseBody,
|
||||||
string? FailureReason)
|
string? FailureReason)
|
||||||
{
|
{
|
||||||
public string ToDisplayLine()
|
public string ToDisplayBlock()
|
||||||
=> string.Join(
|
=> string.Join(
|
||||||
" | ",
|
Environment.NewLine,
|
||||||
TimestampUtc.ToLocalTime().ToString("dd/MM/yyyy HH:mm:ss zzz"),
|
new string('=', 100),
|
||||||
Method,
|
$"FECHA: {TimestampUtc.ToLocalTime():dd/MM/yyyy HH:mm:ss zzz}",
|
||||||
Path,
|
$"PETICION: {Method} {Path}",
|
||||||
$"HTTP {StatusCode}",
|
$"RESULTADO: HTTP {StatusCode} ({Result})",
|
||||||
Result,
|
$"IP: {Clean(RemoteIp)}",
|
||||||
$"ip={Clean(RemoteIp)}",
|
$"TRANSACCION: {Clean(TransactionId)}",
|
||||||
$"orgId={Clean(OrganizationId)}",
|
$"REQUEST ID: {Clean(RequestId)}",
|
||||||
$"dir3={Clean(OrganizationDir3)}",
|
$"CLIENTE (HUELLA): {Clean(ClientTokenFingerprint)}",
|
||||||
$"cif={Clean(OrganizationCif)}",
|
$"ORGANIZACION: id={Clean(OrganizationId)} | dir3={Clean(OrganizationDir3)} | cif={Clean(OrganizationCif)}",
|
||||||
$"contentType={Clean(ContentType)}",
|
$"CONTENT-TYPE: {Clean(ContentType)}",
|
||||||
$"accept={Clean(Accept)}",
|
$"ACCEPT: {Clean(Accept)}",
|
||||||
$"userAgent={Clean(UserAgent)}",
|
$"USER-AGENT: {Clean(UserAgent)}",
|
||||||
$"transaccion={TransactionId ?? "-"}",
|
$"MOTIVO: {Clean(FailureReason)}",
|
||||||
$"peticion={RequestId ?? "-"}",
|
$"CABECERAS PETICION: {Clean(RequestHeaders)}",
|
||||||
$"cliente={ClientTokenFingerprint ?? "-"}",
|
$"CUERPO PETICION: {Clean(RequestBody)}",
|
||||||
$"motivo={Clean(FailureReason)}");
|
$"CABECERAS RESPUESTA: {Clean(ResponseHeaders)}",
|
||||||
|
$"CUERPO RESPUESTA: {Clean(ResponseBody)}");
|
||||||
|
|
||||||
private static string Clean(string? value)
|
private static string Clean(string? value)
|
||||||
=> string.IsNullOrWhiteSpace(value)
|
=> string.IsNullOrWhiteSpace(value)
|
||||||
|
|||||||
Reference in New Issue
Block a user