url y secret keys añadidos a la OPE

Servicio de logs añadido
This commit is contained in:
2026-09-07 11:36:44 +02:00
parent 0e1f6522f2
commit 65bde15ea8
9 changed files with 152 additions and 15 deletions

View File

@@ -699,6 +699,20 @@ namespace ApiDenuncias.Services
throw new ArgumentException("Primer apellido obligatorio.", nameof(thirdParty)); 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?> var payload = new Dictionary<string, object?>
{ {
["nif_country"] = NormalizeCountryCode( ["nif_country"] = NormalizeCountryCode(
@@ -707,8 +721,13 @@ namespace ApiDenuncias.Services
: thirdParty.CountryCode), : thirdParty.CountryCode),
["nif"] = thirdParty.DocumentId.Trim(), ["nif"] = thirdParty.DocumentId.Trim(),
["type"] = thirdParty.IsLegalEntity ? "JURIDICAL" : "PHISIC", ["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); var nifType = GuessNifType(thirdParty.DocumentId, thirdParty.IsLegalEntity);
if (!string.IsNullOrWhiteSpace(nifType)) if (!string.IsNullOrWhiteSpace(nifType))
{ {
@@ -761,11 +780,6 @@ namespace ApiDenuncias.Services
.First(l => l.GetProperty("rel").GetString() == "self") .First(l => l.GetProperty("rel").GetString() == "self")
.GetProperty("href").GetString()!; .GetProperty("href").GetString()!;
if (thirdParty.Address?.HasAnyValue == true)
{
await TryEnsureThirdAddressAsync(selfHref, thirdParty.Address);
}
return (id, selfHref); return (id, selfHref);
} }
@@ -1628,7 +1642,8 @@ namespace ApiDenuncias.Services
["door"] = address.Door, ["door"] = address.Door,
["block"] = address.Block, ["block"] = address.Block,
["stair"] = address.Stair, ["stair"] = address.Stair,
["zipcode"] = address.ZipCode, ["extension"] = address.Extension,
["zip_code"] = address.ZipCode,
["country"] = NormalizeCountryCode(address.CountryCode), ["country"] = NormalizeCountryCode(address.CountryCode),
["province"] = provinceCode, ["province"] = provinceCode,
["type_of_road"] = string.IsNullOrWhiteSpace(address.RoadTypeCode) ? "CL" : address.RoadTypeCode, ["type_of_road"] = string.IsNullOrWhiteSpace(address.RoadTypeCode) ? "CL" : address.RoadTypeCode,

View File

@@ -14,6 +14,8 @@ public sealed class OpeOptions
public string PublicBaseUrl { get; set; } = string.Empty; public string PublicBaseUrl { get; set; } = string.Empty;
public string RequestLogPath { get; set; } = string.Empty;
public int TokenMaxAgeSeconds { get; set; } = 300; public int TokenMaxAgeSeconds { get; set; } = 300;
public int ClockSkewSeconds { get; set; } = 60; public int ClockSkewSeconds { get; set; } = 60;

View File

@@ -4,7 +4,7 @@
"values": [ "values": [
{ {
"key": "base_url", "key": "base_url",
"value": "http://158.158.42.110:7094", "value": "http://gesbuzpre.antifraudeandalucia.es:7094",
"type": "default", "type": "default",
"enabled": true "enabled": true
}, },

View File

@@ -14,7 +14,7 @@ Importacion
Variables que debe rellenar cada probador 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. - client_token: token de cliente configurado en ApiOPE.
- secret_key: secreto compartido usado para firmar el JWT HS256. - secret_key: secreto compartido usado para firmar el JWT HS256.
- organization_id: identificador de la organizacion remitente. - organization_id: identificador de la organizacion remitente.
@@ -40,4 +40,4 @@ Resultado esperado
- Los tests de Postman aparecen en verde. - Los tests de Postman aparecen en verde.
- La tercera respuesta contiene los campos disponibles del expediente solicitado. - 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.

View File

@@ -38,6 +38,7 @@ builder.Services
builder.Services.AddSingleton<OpeResponseSigner>(); builder.Services.AddSingleton<OpeResponseSigner>();
builder.Services.AddSingleton<OpeTokenValidator>(); builder.Services.AddSingleton<OpeTokenValidator>();
builder.Services.AddSingleton<OpeFieldMapper>(); builder.Services.AddSingleton<OpeFieldMapper>();
builder.Services.AddSingleton<OpeRequestFileLogger>();
builder.Services.AddHttpClient<InternalDenunciasClient>((services, client) => builder.Services.AddHttpClient<InternalDenunciasClient>((services, client) =>
{ {

View File

@@ -1,8 +1,11 @@
using System.Security.Claims; using System.Security.Claims;
using ApiOPE.Configuration; using ApiOPE.Configuration;
using ApiOPE.Contracts; using ApiOPE.Contracts;
using ApiOPE.Services;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using System.Security.Cryptography;
using System.Text;
namespace ApiOPE.Security; namespace ApiOPE.Security;
@@ -28,6 +31,7 @@ public sealed class OpeAuthenticationMiddleware
HttpContext context, HttpContext context,
OpeTokenValidator tokenValidator, OpeTokenValidator tokenValidator,
OpeResponseSigner responseSigner, OpeResponseSigner responseSigner,
OpeRequestFileLogger requestFileLogger,
IOptions<OpeOptions> options) IOptions<OpeOptions> options)
{ {
var authentication = context.GetEndpoint()?.Metadata.GetMetadata<OpeAuthenticatedAttribute>(); var authentication = context.GetEndpoint()?.Metadata.GetMetadata<OpeAuthenticatedAttribute>();
@@ -39,20 +43,20 @@ public sealed class OpeAuthenticationMiddleware
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."); await RejectAsync(context, "Falta la cabecera de autenticacion OPE.", requestFileLogger);
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); await RejectAsync(context, validation.FailureReason, requestFileLogger, token);
return; return;
} }
if (!ValidateOrganizationHeaders(context.Request.Headers, options.Value, out var organizationFailure)) if (!ValidateOrganizationHeaders(context.Request.Headers, options.Value, out var organizationFailure))
{ {
await RejectAsync(context, organizationFailure); await RejectAsync(context, organizationFailure, requestFileLogger, token, validation.RequestContext);
return; return;
} }
@@ -73,10 +77,31 @@ public sealed class OpeAuthenticationMiddleware
return Task.CompletedTask; 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( _logger.LogWarning(
"Peticion OPE rechazada en {Method} {Path}. Motivo={Reason}", "Peticion OPE rechazada en {Method} {Path}. Motivo={Reason}",
@@ -84,6 +109,17 @@ public sealed class OpeAuthenticationMiddleware
context.Request.Path, context.Request.Path,
reason); 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( await OpeResponseWriter.WriteErrorAsync(
context.Response, context.Response,
StatusCodes.Status401Unauthorized, StatusCodes.Status401Unauthorized,
@@ -93,6 +129,9 @@ public sealed class OpeAuthenticationMiddleware
context.RequestAborted); context.RequestAborted);
} }
private static string Fingerprint(string value)
=> Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value)))[..12];
private static bool ValidateOrganizationHeaders( private static bool ValidateOrganizationHeaders(
IHeaderDictionary headers, IHeaderDictionary headers,
OpeOptions options, OpeOptions options,

View 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 ?? "-"}");
}

View File

@@ -12,6 +12,7 @@
"ClientToken": "", "ClientToken": "",
"SecretKey": "", "SecretKey": "",
"PublicBaseUrl": "", "PublicBaseUrl": "",
"RequestLogPath": "C:\\Users\\oaafadm\\Desktop\\ApiOPE-logs\\ope-requests.log",
"TokenMaxAgeSeconds": 300, "TokenMaxAgeSeconds": 300,
"ClockSkewSeconds": 60, "ClockSkewSeconds": 60,
"OrganizationId": "", "OrganizationId": "",

View File

@@ -8,6 +8,7 @@ public sealed class ThirdPartyAddressData
public string? Door { get; set; } public string? Door { get; set; }
public string? Block { get; set; } public string? Block { get; set; }
public string? Stair { get; set; } public string? Stair { get; set; }
public string? Extension { get; set; }
public string? Municipality { get; set; } public string? Municipality { get; set; }
public string? Province { get; set; } public string? Province { get; set; }
public string? ZipCode { get; set; } public string? ZipCode { get; set; }
@@ -17,6 +18,7 @@ public sealed class ThirdPartyAddressData
public bool HasAnyValue => public bool HasAnyValue =>
!string.IsNullOrWhiteSpace(Street) || !string.IsNullOrWhiteSpace(Street) ||
!string.IsNullOrWhiteSpace(Number) || !string.IsNullOrWhiteSpace(Number) ||
!string.IsNullOrWhiteSpace(Extension) ||
!string.IsNullOrWhiteSpace(Municipality) || !string.IsNullOrWhiteSpace(Municipality) ||
!string.IsNullOrWhiteSpace(Province) || !string.IsNullOrWhiteSpace(Province) ||
!string.IsNullOrWhiteSpace(ZipCode); !string.IsNullOrWhiteSpace(ZipCode);
@@ -32,6 +34,7 @@ public sealed class ThirdPartyAddressData
Door = denuncia.DireccionPuerta, Door = denuncia.DireccionPuerta,
Block = denuncia.DireccionBloque, Block = denuncia.DireccionBloque,
Stair = denuncia.DireccionEscalera, Stair = denuncia.DireccionEscalera,
Extension = denuncia.DireccionExtra,
Municipality = denuncia.Municipio, Municipality = denuncia.Municipio,
Province = denuncia.Provincia, Province = denuncia.Provincia,
ZipCode = denuncia.CodigoPostal, ZipCode = denuncia.CodigoPostal,