From 65bde15ea86c5dc2c978e9cc466f9c903147cb29 Mon Sep 17 00:00:00 2001 From: Pedro Date: Mon, 7 Sep 2026 11:36:44 +0200 Subject: [PATCH] =?UTF-8?q?url=20y=20secret=20keys=20a=C3=B1adidos=20a=20l?= =?UTF-8?q?a=20OPE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Servicio de logs añadido --- .../ApiDenuncias/Services/GestionaService.cs | 29 +++++-- .../ApiOPE/Configuration/OpeOptions.cs | 2 + .../ApiOPE-PRE.postman_environment.json | 2 +- .../ApiOPE/Postman/INSTRUCCIONES-PRUEBA.txt | 4 +- Antifraude.Net/ApiOPE/Program.cs | 1 + .../Security/OpeAuthenticationMiddleware.cs | 49 ++++++++++-- .../ApiOPE/Services/OpeRequestFileLogger.cs | 76 +++++++++++++++++++ Antifraude.Net/ApiOPE/appsettings.json | 1 + .../Models/ThirdPartyAddressData.cs | 3 + 9 files changed, 152 insertions(+), 15 deletions(-) create mode 100644 Antifraude.Net/ApiOPE/Services/OpeRequestFileLogger.cs diff --git a/Antifraude.Net/ApiDenuncias/Services/GestionaService.cs b/Antifraude.Net/ApiDenuncias/Services/GestionaService.cs index 3c210fe..b9f4ca5 100644 --- a/Antifraude.Net/ApiDenuncias/Services/GestionaService.cs +++ b/Antifraude.Net/ApiDenuncias/Services/GestionaService.cs @@ -699,6 +699,20 @@ namespace ApiDenuncias.Services throw new ArgumentException("Primer apellido obligatorio.", nameof(thirdParty)); } + var notificationChannel = BuildNotificationChannel(thirdParty); + Dictionary? 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 { ["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, diff --git a/Antifraude.Net/ApiOPE/Configuration/OpeOptions.cs b/Antifraude.Net/ApiOPE/Configuration/OpeOptions.cs index 53b14ef..79e10fb 100644 --- a/Antifraude.Net/ApiOPE/Configuration/OpeOptions.cs +++ b/Antifraude.Net/ApiOPE/Configuration/OpeOptions.cs @@ -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; diff --git a/Antifraude.Net/ApiOPE/Postman/ApiOPE-PRE.postman_environment.json b/Antifraude.Net/ApiOPE/Postman/ApiOPE-PRE.postman_environment.json index fffa075..6e3f423 100644 --- a/Antifraude.Net/ApiOPE/Postman/ApiOPE-PRE.postman_environment.json +++ b/Antifraude.Net/ApiOPE/Postman/ApiOPE-PRE.postman_environment.json @@ -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 }, diff --git a/Antifraude.Net/ApiOPE/Postman/INSTRUCCIONES-PRUEBA.txt b/Antifraude.Net/ApiOPE/Postman/INSTRUCCIONES-PRUEBA.txt index 273f03d..e2cb2f3 100644 --- a/Antifraude.Net/ApiOPE/Postman/INSTRUCCIONES-PRUEBA.txt +++ b/Antifraude.Net/ApiOPE/Postman/INSTRUCCIONES-PRUEBA.txt @@ -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. diff --git a/Antifraude.Net/ApiOPE/Program.cs b/Antifraude.Net/ApiOPE/Program.cs index 92523ee..97a5f21 100644 --- a/Antifraude.Net/ApiOPE/Program.cs +++ b/Antifraude.Net/ApiOPE/Program.cs @@ -38,6 +38,7 @@ builder.Services builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddHttpClient((services, client) => { diff --git a/Antifraude.Net/ApiOPE/Security/OpeAuthenticationMiddleware.cs b/Antifraude.Net/ApiOPE/Security/OpeAuthenticationMiddleware.cs index 3b9502e..69d0360 100644 --- a/Antifraude.Net/ApiOPE/Security/OpeAuthenticationMiddleware.cs +++ b/Antifraude.Net/ApiOPE/Security/OpeAuthenticationMiddleware.cs @@ -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 options) { var authentication = context.GetEndpoint()?.Metadata.GetMetadata(); @@ -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; }); - await _next(context); + 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, diff --git a/Antifraude.Net/ApiOPE/Services/OpeRequestFileLogger.cs b/Antifraude.Net/ApiOPE/Services/OpeRequestFileLogger.cs new file mode 100644 index 0000000..ca64156 --- /dev/null +++ b/Antifraude.Net/ApiOPE/Services/OpeRequestFileLogger.cs @@ -0,0 +1,76 @@ +namespace ApiOPE.Services; + +public sealed class OpeRequestFileLogger +{ + private readonly string _path; + private readonly ILogger _logger; + private readonly SemaphoreSlim _writeLock = new(1, 1); + + public OpeRequestFileLogger( + IWebHostEnvironment environment, + IConfiguration configuration, + ILogger 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 ?? "-"}"); +} diff --git a/Antifraude.Net/ApiOPE/appsettings.json b/Antifraude.Net/ApiOPE/appsettings.json index 8094dcb..72cdf80 100644 --- a/Antifraude.Net/ApiOPE/appsettings.json +++ b/Antifraude.Net/ApiOPE/appsettings.json @@ -12,6 +12,7 @@ "ClientToken": "", "SecretKey": "", "PublicBaseUrl": "", + "RequestLogPath": "C:\\Users\\oaafadm\\Desktop\\ApiOPE-logs\\ope-requests.log", "TokenMaxAgeSeconds": 300, "ClockSkewSeconds": 60, "OrganizationId": "", diff --git a/Antifraude.Net/GestionaDenuncias.Shared/Models/ThirdPartyAddressData.cs b/Antifraude.Net/GestionaDenuncias.Shared/Models/ThirdPartyAddressData.cs index dce41ff..f61c064 100644 --- a/Antifraude.Net/GestionaDenuncias.Shared/Models/ThirdPartyAddressData.cs +++ b/Antifraude.Net/GestionaDenuncias.Shared/Models/ThirdPartyAddressData.cs @@ -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,