url y secret keys añadidos a la OPE
Servicio de logs añadido
This commit is contained in:
@@ -699,6 +699,20 @@ namespace ApiDenuncias.Services
|
||||
throw new ArgumentException("Primer apellido obligatorio.", nameof(thirdParty));
|
||||
}
|
||||
|
||||
var notificationChannel = BuildNotificationChannel(thirdParty);
|
||||
Dictionary<string, object?>? addressPayload = null;
|
||||
if (thirdParty.Address?.HasAnyValue == true)
|
||||
{
|
||||
addressPayload = await BuildAddressPayloadAsync(thirdParty.Address);
|
||||
}
|
||||
|
||||
if (string.Equals(notificationChannel, "PAPER", StringComparison.Ordinal) &&
|
||||
addressPayload is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"No se puede crear el tercero con notificacion postal porque la denuncia no contiene una direccion postal valida.");
|
||||
}
|
||||
|
||||
var payload = new Dictionary<string, object?>
|
||||
{
|
||||
["nif_country"] = NormalizeCountryCode(
|
||||
@@ -707,8 +721,13 @@ namespace ApiDenuncias.Services
|
||||
: thirdParty.CountryCode),
|
||||
["nif"] = thirdParty.DocumentId.Trim(),
|
||||
["type"] = thirdParty.IsLegalEntity ? "JURIDICAL" : "PHISIC",
|
||||
["notification_channel"] = BuildNotificationChannel(thirdParty)
|
||||
["notification_channel"] = notificationChannel
|
||||
};
|
||||
|
||||
if (addressPayload is not null)
|
||||
{
|
||||
payload["addresses"] = new[] { addressPayload };
|
||||
}
|
||||
var nifType = GuessNifType(thirdParty.DocumentId, thirdParty.IsLegalEntity);
|
||||
if (!string.IsNullOrWhiteSpace(nifType))
|
||||
{
|
||||
@@ -761,11 +780,6 @@ namespace ApiDenuncias.Services
|
||||
.First(l => l.GetProperty("rel").GetString() == "self")
|
||||
.GetProperty("href").GetString()!;
|
||||
|
||||
if (thirdParty.Address?.HasAnyValue == true)
|
||||
{
|
||||
await TryEnsureThirdAddressAsync(selfHref, thirdParty.Address);
|
||||
}
|
||||
|
||||
return (id, selfHref);
|
||||
}
|
||||
|
||||
@@ -1628,7 +1642,8 @@ namespace ApiDenuncias.Services
|
||||
["door"] = address.Door,
|
||||
["block"] = address.Block,
|
||||
["stair"] = address.Stair,
|
||||
["zipcode"] = address.ZipCode,
|
||||
["extension"] = address.Extension,
|
||||
["zip_code"] = address.ZipCode,
|
||||
["country"] = NormalizeCountryCode(address.CountryCode),
|
||||
["province"] = provinceCode,
|
||||
["type_of_road"] = string.IsNullOrWhiteSpace(address.RoadTypeCode) ? "CL" : address.RoadTypeCode,
|
||||
|
||||
@@ -14,6 +14,8 @@ public sealed class OpeOptions
|
||||
|
||||
public string PublicBaseUrl { get; set; } = string.Empty;
|
||||
|
||||
public string RequestLogPath { get; set; } = string.Empty;
|
||||
|
||||
public int TokenMaxAgeSeconds { get; set; } = 300;
|
||||
|
||||
public int ClockSkewSeconds { get; set; } = 60;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"values": [
|
||||
{
|
||||
"key": "base_url",
|
||||
"value": "http://158.158.42.110:7094",
|
||||
"value": "http://gesbuzpre.antifraudeandalucia.es:7094",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@ Importacion
|
||||
|
||||
Variables que debe rellenar cada probador
|
||||
-----------------------------------------
|
||||
- base_url: direccion publicada de ApiOPE. El paquete propone http://158.158.42.110:7094.
|
||||
- base_url: direccion publicada de ApiOPE. El paquete propone http://gesbuzpre.antifraudeandalucia.es:7094.
|
||||
- client_token: token de cliente configurado en ApiOPE.
|
||||
- secret_key: secreto compartido usado para firmar el JWT HS256.
|
||||
- organization_id: identificador de la organizacion remitente.
|
||||
@@ -40,4 +40,4 @@ Resultado esperado
|
||||
- Los tests de Postman aparecen en verde.
|
||||
- La tercera respuesta contiene los campos disponibles del expediente solicitado.
|
||||
|
||||
Si la peticion no llega al servidor, comprobar que el equipo tiene conectividad con 158.158.42.110 y acceso al puerto 7094.
|
||||
Si la peticion no llega al servidor, comprobar que el equipo resuelve gesbuzpre.antifraudeandalucia.es y tiene acceso al puerto 7094.
|
||||
|
||||
@@ -38,6 +38,7 @@ builder.Services
|
||||
builder.Services.AddSingleton<OpeResponseSigner>();
|
||||
builder.Services.AddSingleton<OpeTokenValidator>();
|
||||
builder.Services.AddSingleton<OpeFieldMapper>();
|
||||
builder.Services.AddSingleton<OpeRequestFileLogger>();
|
||||
|
||||
builder.Services.AddHttpClient<InternalDenunciasClient>((services, client) =>
|
||||
{
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
using System.Security.Claims;
|
||||
using ApiOPE.Configuration;
|
||||
using ApiOPE.Contracts;
|
||||
using ApiOPE.Services;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace ApiOPE.Security;
|
||||
|
||||
@@ -28,6 +31,7 @@ public sealed class OpeAuthenticationMiddleware
|
||||
HttpContext context,
|
||||
OpeTokenValidator tokenValidator,
|
||||
OpeResponseSigner responseSigner,
|
||||
OpeRequestFileLogger requestFileLogger,
|
||||
IOptions<OpeOptions> options)
|
||||
{
|
||||
var authentication = context.GetEndpoint()?.Metadata.GetMetadata<OpeAuthenticatedAttribute>();
|
||||
@@ -39,20 +43,20 @@ public sealed class OpeAuthenticationMiddleware
|
||||
|
||||
if (!TryGetSingleHeader(context.Request.Headers, TokenHeaderName, out var token))
|
||||
{
|
||||
await RejectAsync(context, "Falta la cabecera de autenticacion OPE.");
|
||||
await RejectAsync(context, "Falta la cabecera de autenticacion OPE.", requestFileLogger);
|
||||
return;
|
||||
}
|
||||
|
||||
var validation = tokenValidator.Validate(token, authentication.Resource, authentication.VersionOptional);
|
||||
if (!validation.IsValid || validation.RequestContext is null)
|
||||
{
|
||||
await RejectAsync(context, validation.FailureReason);
|
||||
await RejectAsync(context, validation.FailureReason, requestFileLogger, token);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ValidateOrganizationHeaders(context.Request.Headers, options.Value, out var organizationFailure))
|
||||
{
|
||||
await RejectAsync(context, organizationFailure);
|
||||
await RejectAsync(context, organizationFailure, requestFileLogger, token, validation.RequestContext);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -73,10 +77,31 @@ public sealed class OpeAuthenticationMiddleware
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await requestFileLogger.WriteAsync(new OpeRequestLogEntry(
|
||||
DateTimeOffset.UtcNow,
|
||||
context.Request.Method,
|
||||
context.Request.Path,
|
||||
context.Response.StatusCode,
|
||||
context.Response.StatusCode is >= 200 and < 300 ? "accepted" : "response_error",
|
||||
validation.RequestContext.TransactionId.ToString(),
|
||||
context.TraceIdentifier,
|
||||
Fingerprint(validation.RequestContext.ClientToken),
|
||||
null));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RejectAsync(HttpContext context, string reason)
|
||||
private async Task RejectAsync(
|
||||
HttpContext context,
|
||||
string reason,
|
||||
OpeRequestFileLogger requestFileLogger,
|
||||
string? token = null,
|
||||
OpeRequestContext? requestContext = null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Peticion OPE rechazada en {Method} {Path}. Motivo={Reason}",
|
||||
@@ -84,6 +109,17 @@ public sealed class OpeAuthenticationMiddleware
|
||||
context.Request.Path,
|
||||
reason);
|
||||
|
||||
await requestFileLogger.WriteAsync(new OpeRequestLogEntry(
|
||||
DateTimeOffset.UtcNow,
|
||||
context.Request.Method,
|
||||
context.Request.Path,
|
||||
StatusCodes.Status401Unauthorized,
|
||||
"rejected",
|
||||
requestContext?.TransactionId.ToString(),
|
||||
context.TraceIdentifier,
|
||||
string.IsNullOrWhiteSpace(token) ? null : Fingerprint(token),
|
||||
reason));
|
||||
|
||||
await OpeResponseWriter.WriteErrorAsync(
|
||||
context.Response,
|
||||
StatusCodes.Status401Unauthorized,
|
||||
@@ -93,6 +129,9 @@ public sealed class OpeAuthenticationMiddleware
|
||||
context.RequestAborted);
|
||||
}
|
||||
|
||||
private static string Fingerprint(string value)
|
||||
=> Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value)))[..12];
|
||||
|
||||
private static bool ValidateOrganizationHeaders(
|
||||
IHeaderDictionary headers,
|
||||
OpeOptions options,
|
||||
|
||||
76
Antifraude.Net/ApiOPE/Services/OpeRequestFileLogger.cs
Normal file
76
Antifraude.Net/ApiOPE/Services/OpeRequestFileLogger.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
namespace ApiOPE.Services;
|
||||
|
||||
public sealed class OpeRequestFileLogger
|
||||
{
|
||||
private readonly string _path;
|
||||
private readonly ILogger<OpeRequestFileLogger> _logger;
|
||||
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
||||
|
||||
public OpeRequestFileLogger(
|
||||
IWebHostEnvironment environment,
|
||||
IConfiguration configuration,
|
||||
ILogger<OpeRequestFileLogger> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
var configuredPath = configuration["Ope:RequestLogPath"];
|
||||
_path = string.IsNullOrWhiteSpace(configuredPath)
|
||||
? Path.Combine(environment.ContentRootPath, "logs", "ope-requests.jsonl")
|
||||
: configuredPath;
|
||||
}
|
||||
|
||||
public async Task WriteAsync(OpeRequestLogEntry entry, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var directory = Path.GetDirectoryName(_path);
|
||||
if (!string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
var line = entry.ToDisplayLine() + Environment.NewLine;
|
||||
await _writeLock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
await File.AppendAllTextAsync(_path, line, cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_writeLock.Release();
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// El registro de diagnóstico nunca debe impedir una llamada OPE.
|
||||
_logger.LogWarning(
|
||||
exception,
|
||||
"No se pudo escribir el registro de peticiones OPE en {Path}.",
|
||||
_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record OpeRequestLogEntry(
|
||||
DateTimeOffset TimestampUtc,
|
||||
string Method,
|
||||
string Path,
|
||||
int StatusCode,
|
||||
string Result,
|
||||
string? TransactionId,
|
||||
string? RequestId,
|
||||
string? ClientTokenFingerprint,
|
||||
string? FailureReason)
|
||||
{
|
||||
public string ToDisplayLine()
|
||||
=> string.Join(
|
||||
" | ",
|
||||
TimestampUtc.ToLocalTime().ToString("dd/MM/yyyy HH:mm:ss zzz"),
|
||||
Method,
|
||||
Path,
|
||||
$"HTTP {StatusCode}",
|
||||
Result,
|
||||
$"transaccion={TransactionId ?? "-"}",
|
||||
$"peticion={RequestId ?? "-"}",
|
||||
$"cliente={ClientTokenFingerprint ?? "-"}",
|
||||
$"motivo={FailureReason ?? "-"}");
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
"ClientToken": "",
|
||||
"SecretKey": "",
|
||||
"PublicBaseUrl": "",
|
||||
"RequestLogPath": "C:\\Users\\oaafadm\\Desktop\\ApiOPE-logs\\ope-requests.log",
|
||||
"TokenMaxAgeSeconds": 300,
|
||||
"ClockSkewSeconds": 60,
|
||||
"OrganizationId": "",
|
||||
|
||||
@@ -8,6 +8,7 @@ public sealed class ThirdPartyAddressData
|
||||
public string? Door { get; set; }
|
||||
public string? Block { get; set; }
|
||||
public string? Stair { get; set; }
|
||||
public string? Extension { get; set; }
|
||||
public string? Municipality { get; set; }
|
||||
public string? Province { get; set; }
|
||||
public string? ZipCode { get; set; }
|
||||
@@ -17,6 +18,7 @@ public sealed class ThirdPartyAddressData
|
||||
public bool HasAnyValue =>
|
||||
!string.IsNullOrWhiteSpace(Street) ||
|
||||
!string.IsNullOrWhiteSpace(Number) ||
|
||||
!string.IsNullOrWhiteSpace(Extension) ||
|
||||
!string.IsNullOrWhiteSpace(Municipality) ||
|
||||
!string.IsNullOrWhiteSpace(Province) ||
|
||||
!string.IsNullOrWhiteSpace(ZipCode);
|
||||
@@ -32,6 +34,7 @@ public sealed class ThirdPartyAddressData
|
||||
Door = denuncia.DireccionPuerta,
|
||||
Block = denuncia.DireccionBloque,
|
||||
Stair = denuncia.DireccionEscalera,
|
||||
Extension = denuncia.DireccionExtra,
|
||||
Municipality = denuncia.Municipio,
|
||||
Province = denuncia.Provincia,
|
||||
ZipCode = denuncia.CodigoPostal,
|
||||
|
||||
Reference in New Issue
Block a user