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));
|
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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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) =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
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": "",
|
"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": "",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user