- 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 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 missingFields = GetMissingGestionaFields(response);
|
||||
if (missingFields.Count > 0)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Campos Gestiona denuncia {DenunciaId}: campos vacios={MissingFields}; rawReportLength={RawReportLength}; formFieldsCount={FormFieldsCount}.",
|
||||
denunciaId,
|
||||
denuncia.Id_Denuncia,
|
||||
string.Join(", ", missingFields),
|
||||
denuncia.TextoOriginalReport?.Length ?? 0,
|
||||
denuncia.GetCamposFormulario().Count);
|
||||
|
||||
@@ -386,16 +386,6 @@ public sealed class InboxController : ControllerBase
|
||||
ex.StatusCode,
|
||||
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))
|
||||
{
|
||||
return new ObjectResult(new ApiError(
|
||||
@@ -406,8 +396,30 @@ public sealed class InboxController : ControllerBase
|
||||
};
|
||||
}
|
||||
|
||||
if (IsDpopValidationProblem(ex.Message))
|
||||
{
|
||||
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
|
||||
};
|
||||
@@ -430,10 +442,19 @@ public sealed class InboxController : ControllerBase
|
||||
|
||||
private static bool IsSessionAuthorizationProblem(string? message)
|
||||
=> !string.IsNullOrWhiteSpace(message) &&
|
||||
(message.Contains("Invalid DPoP proof", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.Contains("No token and no session", StringComparison.OrdinalIgnoreCase) ||
|
||||
(message.Contains("NotAuthenticated", StringComparison.OrdinalIgnoreCase) ||
|
||||
message.Contains("Invalid token", 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)
|
||||
{
|
||||
|
||||
@@ -198,6 +198,14 @@ public sealed class EncryptedDenunciaStore : IDenunciaStore, IFilteredDenunciaSt
|
||||
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)
|
||||
{
|
||||
var key = await _envelopeKeyProvider.GetCurrentKeyAsync(cancellationToken);
|
||||
|
||||
@@ -667,6 +667,10 @@ public sealed class GlobalLeaksClient
|
||||
var response = await _httpClient.SendAsync(request, completionOption, cancellationToken);
|
||||
if ((int)response.StatusCode == 412)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"GlobalLeaks ha indicado sesion expirada (412) en {Method} {Path}.",
|
||||
request.Method.Method,
|
||||
request.RequestUri?.OriginalString);
|
||||
response.Dispose();
|
||||
throw new GlobalLeaksSessionExpiredException();
|
||||
}
|
||||
@@ -704,23 +708,9 @@ public sealed class GlobalLeaksClient
|
||||
return await SendGlRequestAsync(requestWithoutDpop, cancellationToken, completionOption);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var requestWithDpop = CreateAuthenticatedRequest(method, path, sessionId, dpopPrivateKey);
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,10 @@ namespace ApiDenuncias.Services;
|
||||
|
||||
public interface IFilteredDenunciaStore
|
||||
{
|
||||
Task<DenunciasGestiona?> GetDenunciaByGestionaFileCodeAsync(
|
||||
string gestionaFileCode,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<List<DenunciasGestiona>> GetDenunciasByIdsAsync(
|
||||
IReadOnlyCollection<int> denunciaIds,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
@@ -551,6 +551,38 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
||||
: 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(
|
||||
GestionaUploadHistoryCreateRequest request,
|
||||
string username,
|
||||
|
||||
Reference in New Issue
Block a user