- Añade el endpoint gestiona-fields por número de expediente de Gestiona.
- Mantiene disponible la consulta existente por número de denuncia. - Resuelve la asociación mediante el histórico de subidas para conservar el cifrado de complaints. - Elimina el reintento de llamadas a GlobalLeaks sin DPoP. - Evita solicitar renovación 2FA ante errores de permisos o validación DPoP. - Limita la renovación a señales reales de sesión inválida o expirada. - Añade una traza específica para respuestas 412 de GlobalLeaks. - Mejora los mensajes de error diferenciando permisos, autenticación y DPoP.
This commit is contained in:
@@ -82,13 +82,42 @@ public sealed class DenunciasController : ControllerBase
|
|||||||
return NotFound(new ApiError("No se ha encontrado la denuncia solicitada."));
|
return NotFound(new ApiError("No se ha encontrado la denuncia solicitada."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return BuildGestionaFieldsResponse(denuncia);
|
||||||
|
}
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpGet("{numeroExpediente:int}/{anioExpediente:int}/gestiona-fields")]
|
||||||
|
public async Task<ActionResult<GestionaExternalFieldsResponse>> GetGestionaFieldsByExpediente(
|
||||||
|
int numeroExpediente,
|
||||||
|
int anioExpediente,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (numeroExpediente <= 0 || anioExpediente <= 0)
|
||||||
|
{
|
||||||
|
return BadRequest(new ApiError("Debes indicar un numero de expediente de Gestiona valido."));
|
||||||
|
}
|
||||||
|
|
||||||
|
var expedienteGestiona = $"{numeroExpediente.ToString(CultureInfo.InvariantCulture)}/{anioExpediente.ToString(CultureInfo.InvariantCulture)}";
|
||||||
|
var denuncia = await _filteredDenunciaStore.GetDenunciaByGestionaFileCodeAsync(
|
||||||
|
expedienteGestiona,
|
||||||
|
cancellationToken);
|
||||||
|
if (denuncia is null)
|
||||||
|
{
|
||||||
|
return NotFound(new ApiError("No se ha encontrado una denuncia asociada al expediente de Gestiona solicitado."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return BuildGestionaFieldsResponse(denuncia);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ActionResult<GestionaExternalFieldsResponse> BuildGestionaFieldsResponse(DenunciasGestiona denuncia)
|
||||||
|
{
|
||||||
var response = ToGestionaComplaintFields(denuncia);
|
var response = ToGestionaComplaintFields(denuncia);
|
||||||
var missingFields = GetMissingGestionaFields(response);
|
var missingFields = GetMissingGestionaFields(response);
|
||||||
if (missingFields.Count > 0)
|
if (missingFields.Count > 0)
|
||||||
{
|
{
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Campos Gestiona denuncia {DenunciaId}: campos vacios={MissingFields}; rawReportLength={RawReportLength}; formFieldsCount={FormFieldsCount}.",
|
"Campos Gestiona denuncia {DenunciaId}: campos vacios={MissingFields}; rawReportLength={RawReportLength}; formFieldsCount={FormFieldsCount}.",
|
||||||
denunciaId,
|
denuncia.Id_Denuncia,
|
||||||
string.Join(", ", missingFields),
|
string.Join(", ", missingFields),
|
||||||
denuncia.TextoOriginalReport?.Length ?? 0,
|
denuncia.TextoOriginalReport?.Length ?? 0,
|
||||||
denuncia.GetCamposFormulario().Count);
|
denuncia.GetCamposFormulario().Count);
|
||||||
|
|||||||
@@ -386,16 +386,6 @@ public sealed class InboxController : ControllerBase
|
|||||||
ex.StatusCode,
|
ex.StatusCode,
|
||||||
ex.Message);
|
ex.Message);
|
||||||
|
|
||||||
if (operation.Contains("bandeja", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return new ObjectResult(new ApiError(
|
|
||||||
"GlobalLeaks ha rechazado la sesion al cargar la bandeja. Renueva el 2FA y vuelve a intentarlo.",
|
|
||||||
SessionExpired: true))
|
|
||||||
{
|
|
||||||
StatusCode = StatusCodes.Status401Unauthorized
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsSessionAuthorizationProblem(ex.Message))
|
if (IsSessionAuthorizationProblem(ex.Message))
|
||||||
{
|
{
|
||||||
return new ObjectResult(new ApiError(
|
return new ObjectResult(new ApiError(
|
||||||
@@ -406,8 +396,30 @@ public sealed class InboxController : ControllerBase
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (IsDpopValidationProblem(ex.Message))
|
||||||
|
{
|
||||||
return new ObjectResult(new ApiError(
|
return new ObjectResult(new ApiError(
|
||||||
"No tienes permiso en GlobalLeaks para acceder a esa denuncia. Puede ser una denuncia anterior a la creacion de tu usuario; consulta con el administrador del buzon."))
|
"GlobalLeaks no ha podido validar la proteccion de la sesion (DPoP). La sesion no se ha marcado como caducada. Vuelve a intentarlo y, si se repite, avisa al equipo tecnico."))
|
||||||
|
{
|
||||||
|
StatusCode = StatusCodes.Status502BadGateway
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsMissingAuthenticationHeadersProblem(ex.Message))
|
||||||
|
{
|
||||||
|
return new ObjectResult(new ApiError(
|
||||||
|
"No se ha podido completar la autenticacion de la llamada a GlobalLeaks. La sesion no se ha marcado como caducada. Vuelve a intentarlo y, si se repite, avisa al equipo tecnico."))
|
||||||
|
{
|
||||||
|
StatusCode = StatusCodes.Status502BadGateway
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var permissionMessage = operation.Contains("bandeja", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? "GlobalLeaks no ha autorizado la carga de la bandeja para este usuario. La sesion sigue activa; comprueba los permisos del usuario en el buzon."
|
||||||
|
: "No tienes permiso en GlobalLeaks para acceder a esa denuncia. Puede ser una denuncia anterior a la creacion de tu usuario; consulta con el administrador del buzon.";
|
||||||
|
|
||||||
|
return new ObjectResult(new ApiError(
|
||||||
|
permissionMessage))
|
||||||
{
|
{
|
||||||
StatusCode = StatusCodes.Status403Forbidden
|
StatusCode = StatusCodes.Status403Forbidden
|
||||||
};
|
};
|
||||||
@@ -430,10 +442,19 @@ public sealed class InboxController : ControllerBase
|
|||||||
|
|
||||||
private static bool IsSessionAuthorizationProblem(string? message)
|
private static bool IsSessionAuthorizationProblem(string? message)
|
||||||
=> !string.IsNullOrWhiteSpace(message) &&
|
=> !string.IsNullOrWhiteSpace(message) &&
|
||||||
(message.Contains("Invalid DPoP proof", StringComparison.OrdinalIgnoreCase) ||
|
(message.Contains("NotAuthenticated", StringComparison.OrdinalIgnoreCase) ||
|
||||||
message.Contains("No token and no session", StringComparison.OrdinalIgnoreCase) ||
|
message.Contains("Invalid token", StringComparison.OrdinalIgnoreCase) ||
|
||||||
message.Contains("Invalid session", StringComparison.OrdinalIgnoreCase) ||
|
message.Contains("Invalid session", StringComparison.OrdinalIgnoreCase) ||
|
||||||
message.Contains("session", StringComparison.OrdinalIgnoreCase) && message.Contains("expired", StringComparison.OrdinalIgnoreCase));
|
message.Contains("session", StringComparison.OrdinalIgnoreCase) && message.Contains("expired", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
message.Contains("token", StringComparison.OrdinalIgnoreCase) && message.Contains("expired", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
private static bool IsDpopValidationProblem(string? message)
|
||||||
|
=> !string.IsNullOrWhiteSpace(message) &&
|
||||||
|
message.Contains("Invalid DPoP proof", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private static bool IsMissingAuthenticationHeadersProblem(string? message)
|
||||||
|
=> !string.IsNullOrWhiteSpace(message) &&
|
||||||
|
message.Contains("No token and no session", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
private async Task<GlobalLeaksStoredSession?> RequireActiveSessionAsync(string username, CancellationToken cancellationToken)
|
private async Task<GlobalLeaksStoredSession?> RequireActiveSessionAsync(string username, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -198,6 +198,14 @@ public sealed class EncryptedDenunciaStore : IDenunciaStore, IFilteredDenunciaSt
|
|||||||
return denuncia is null ? null : await UnprotectComplaintAsync(denuncia, [], cancellationToken);
|
return denuncia is null ? null : await UnprotectComplaintAsync(denuncia, [], cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<DenunciasGestiona?> GetDenunciaByGestionaFileCodeAsync(
|
||||||
|
string gestionaFileCode,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var denuncia = await _inner.GetDenunciaByGestionaFileCodeAsync(gestionaFileCode, cancellationToken);
|
||||||
|
return denuncia is null ? null : await UnprotectComplaintAsync(denuncia, [], cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task UpsertDenunciaAsync(DenunciasGestiona denuncia, CancellationToken cancellationToken = default)
|
public async Task UpsertDenunciaAsync(DenunciasGestiona denuncia, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var key = await _envelopeKeyProvider.GetCurrentKeyAsync(cancellationToken);
|
var key = await _envelopeKeyProvider.GetCurrentKeyAsync(cancellationToken);
|
||||||
|
|||||||
@@ -667,6 +667,10 @@ public sealed class GlobalLeaksClient
|
|||||||
var response = await _httpClient.SendAsync(request, completionOption, cancellationToken);
|
var response = await _httpClient.SendAsync(request, completionOption, cancellationToken);
|
||||||
if ((int)response.StatusCode == 412)
|
if ((int)response.StatusCode == 412)
|
||||||
{
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"GlobalLeaks ha indicado sesion expirada (412) en {Method} {Path}.",
|
||||||
|
request.Method.Method,
|
||||||
|
request.RequestUri?.OriginalString);
|
||||||
response.Dispose();
|
response.Dispose();
|
||||||
throw new GlobalLeaksSessionExpiredException();
|
throw new GlobalLeaksSessionExpiredException();
|
||||||
}
|
}
|
||||||
@@ -704,23 +708,9 @@ public sealed class GlobalLeaksClient
|
|||||||
return await SendGlRequestAsync(requestWithoutDpop, cancellationToken, completionOption);
|
return await SendGlRequestAsync(requestWithoutDpop, cancellationToken, completionOption);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var requestWithDpop = CreateAuthenticatedRequest(method, path, sessionId, dpopPrivateKey);
|
using var requestWithDpop = CreateAuthenticatedRequest(method, path, sessionId, dpopPrivateKey);
|
||||||
return await SendGlRequestAsync(requestWithDpop, cancellationToken, completionOption);
|
return await SendGlRequestAsync(requestWithDpop, cancellationToken, completionOption);
|
||||||
}
|
}
|
||||||
catch (GlobalLeaksValidationException ex) when (ex.StatusCode is StatusCodes.Status401Unauthorized or StatusCodes.Status403Forbidden)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(
|
|
||||||
ex,
|
|
||||||
"GlobalLeaks ha rechazado {Method} {Path} con DPoP. Se reintentara una vez solo con X-Session para compatibilidad.",
|
|
||||||
method.Method,
|
|
||||||
path);
|
|
||||||
|
|
||||||
using var fallbackRequest = CreateAuthenticatedRequest(method, path, sessionId, dpopPrivateKey: null);
|
|
||||||
return await SendGlRequestAsync(fallbackRequest, cancellationToken, completionOption);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static HttpRequestMessage CreateRequest(HttpMethod method, string path)
|
private static HttpRequestMessage CreateRequest(HttpMethod method, string path)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ namespace ApiDenuncias.Services;
|
|||||||
|
|
||||||
public interface IFilteredDenunciaStore
|
public interface IFilteredDenunciaStore
|
||||||
{
|
{
|
||||||
|
Task<DenunciasGestiona?> GetDenunciaByGestionaFileCodeAsync(
|
||||||
|
string gestionaFileCode,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
Task<List<DenunciasGestiona>> GetDenunciasByIdsAsync(
|
Task<List<DenunciasGestiona>> GetDenunciasByIdsAsync(
|
||||||
IReadOnlyCollection<int> denunciaIds,
|
IReadOnlyCollection<int> denunciaIds,
|
||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
|
|||||||
@@ -551,6 +551,38 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<DenunciasGestiona?> GetDenunciaByGestionaFileCodeAsync(
|
||||||
|
string gestionaFileCode,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await EnsureSchemaReadyAsync(cancellationToken);
|
||||||
|
|
||||||
|
var sql = $"""
|
||||||
|
SELECT
|
||||||
|
{ComplaintSelectColumns}
|
||||||
|
FROM complaints
|
||||||
|
WHERE external_report_id = (
|
||||||
|
SELECT history.external_report_id
|
||||||
|
FROM gestiona_upload_history history
|
||||||
|
WHERE history.gestiona_file_code = @gestionaFileCode
|
||||||
|
ORDER BY history.uploaded_at_utc DESC, history.id DESC
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
OR gestiona_file_code = @gestionaFileCode
|
||||||
|
ORDER BY COALESCE(gestiona_uploaded_at_utc, report_date_utc) DESC, external_report_id DESC
|
||||||
|
LIMIT 1;
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var connection = await OpenConnectionAsync(cancellationToken);
|
||||||
|
await using var command = new MySqlCommand(sql, connection);
|
||||||
|
command.Parameters.AddWithValue("@gestionaFileCode", gestionaFileCode.Trim());
|
||||||
|
|
||||||
|
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||||
|
return await reader.ReadAsync(cancellationToken)
|
||||||
|
? MapComplaint(reader)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task AddGestionaUploadHistoryAsync(
|
public async Task AddGestionaUploadHistoryAsync(
|
||||||
GestionaUploadHistoryCreateRequest request,
|
GestionaUploadHistoryCreateRequest request,
|
||||||
string username,
|
string username,
|
||||||
|
|||||||
Reference in New Issue
Block a user