Security: controllers and stack traces in logs
All checks were successful
Build Docker Images / test (map[name:Morska plugin_project:DiunaBI.Plugins.Morska]) (push) Successful in 1m32s
Build Docker Images / test (map[name:PedrolloPL plugin_project:DiunaBI.Plugins.PedrolloPL]) (push) Successful in 1m29s
Build Docker Images / build-and-push (map[image_suffix:morska name:Morska plugin_project:DiunaBI.Plugins.Morska]) (push) Successful in 1m47s
Build Docker Images / build-and-push (map[image_suffix:pedrollopl name:PedrolloPL plugin_project:DiunaBI.Plugins.PedrolloPL]) (push) Successful in 1m42s

This commit is contained in:
2025-12-05 21:37:15 +01:00
parent 46805fb196
commit 71c293320b
4 changed files with 93 additions and 47 deletions

View File

@@ -0,0 +1,63 @@
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace DiunaBI.API.Attributes;
/// <summary>
/// Authorization attribute that validates API key from X-API-Key header.
/// Uses constant-time comparison to prevent timing attacks.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class ApiKeyAuthAttribute : Attribute, IAuthorizationFilter
{
private const string ApiKeyHeaderName = "X-API-Key";
public void OnAuthorization(AuthorizationFilterContext context)
{
var configuration = context.HttpContext.RequestServices.GetRequiredService<IConfiguration>();
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<ApiKeyAuthAttribute>>();
// Get expected API key from configuration
var expectedApiKey = configuration["apiKey"];
if (string.IsNullOrEmpty(expectedApiKey))
{
logger.LogError("API key not configured in appsettings");
context.Result = new StatusCodeResult(StatusCodes.Status500InternalServerError);
return;
}
// Get API key from header
if (!context.HttpContext.Request.Headers.TryGetValue(ApiKeyHeaderName, out var extractedApiKey))
{
logger.LogWarning("API key missing from request header");
context.Result = new UnauthorizedObjectResult(new { error = "API key is required" });
return;
}
// Constant-time comparison to prevent timing attacks
if (!IsApiKeyValid(extractedApiKey!, expectedApiKey))
{
logger.LogWarning("Invalid API key provided from {RemoteIp}", context.HttpContext.Connection.RemoteIpAddress);
context.Result = new UnauthorizedObjectResult(new { error = "Invalid API key" });
return;
}
// API key is valid - allow the request to proceed
}
/// <summary>
/// Constant-time string comparison to prevent timing attacks.
/// </summary>
private static bool IsApiKeyValid(string providedKey, string expectedKey)
{
if (providedKey == null || expectedKey == null)
return false;
var providedBytes = Encoding.UTF8.GetBytes(providedKey);
var expectedBytes = Encoding.UTF8.GetBytes(expectedKey);
return CryptographicOperations.FixedTimeEquals(providedBytes, expectedBytes);
}
}

View File

@@ -87,7 +87,7 @@ public class DataInboxController : Controller
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, "DataInbox: Insert error for source {Source}, name {Name}", dataInbox.Source, dataInbox.Name); _logger.LogError(e, "DataInbox: Insert error for source {Source}, name {Name}", dataInbox.Source, dataInbox.Name);
return BadRequest(e.ToString()); return BadRequest("An error occurred processing your request");
} }
} }
@@ -137,7 +137,7 @@ public class DataInboxController : Controller
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, "GetAll: Error retrieving data inbox items"); _logger.LogError(e, "GetAll: Error retrieving data inbox items");
return BadRequest(e.ToString()); return BadRequest("An error occurred processing your request");
} }
} }
@@ -172,7 +172,7 @@ public class DataInboxController : Controller
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, "Get: Error retrieving data inbox item {Id}", id); _logger.LogError(e, "Get: Error retrieving data inbox item {Id}", id);
return BadRequest(e.ToString()); return BadRequest("An error occurred processing your request");
} }
} }

View File

@@ -1,3 +1,4 @@
using DiunaBI.API.Attributes;
using DiunaBI.Application.DTOModels.Common; using DiunaBI.Application.DTOModels.Common;
using DiunaBI.Domain.Entities; using DiunaBI.Domain.Entities;
using DiunaBI.Infrastructure.Data; using DiunaBI.Infrastructure.Data;
@@ -82,7 +83,7 @@ public class JobsController : Controller
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "GetAll: Error retrieving jobs"); _logger.LogError(ex, "GetAll: Error retrieving jobs");
return BadRequest(ex.ToString()); return BadRequest("An error occurred while retrieving jobs");
} }
} }
@@ -108,21 +109,15 @@ public class JobsController : Controller
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Get: Error retrieving job {JobId}", id); _logger.LogError(ex, "Get: Error retrieving job {JobId}", id);
return BadRequest(ex.ToString()); return BadRequest("An error occurred processing your request");
} }
} }
[HttpPost] [HttpPost]
[Route("schedule/{apiKey}")] [Route("schedule")]
[AllowAnonymous] [ApiKeyAuth]
public async Task<IActionResult> ScheduleJobs(string apiKey, [FromQuery] string? nameFilter = null) public async Task<IActionResult> ScheduleJobs([FromQuery] string? nameFilter = null)
{ {
if (apiKey != _configuration["apiKey"])
{
_logger.LogWarning("ScheduleJobs: Unauthorized request with apiKey {ApiKey}", apiKey);
return Unauthorized();
}
try try
{ {
var jobsCreated = await _jobScheduler.ScheduleAllJobsAsync(nameFilter); var jobsCreated = await _jobScheduler.ScheduleAllJobsAsync(nameFilter);
@@ -139,21 +134,15 @@ public class JobsController : Controller
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "ScheduleJobs: Error scheduling jobs"); _logger.LogError(ex, "ScheduleJobs: Error scheduling jobs");
return BadRequest(ex.ToString()); return BadRequest("An error occurred processing your request");
} }
} }
[HttpPost] [HttpPost]
[Route("schedule/imports/{apiKey}")] [Route("schedule/imports")]
[AllowAnonymous] [ApiKeyAuth]
public async Task<IActionResult> ScheduleImportJobs(string apiKey, [FromQuery] string? nameFilter = null) public async Task<IActionResult> ScheduleImportJobs([FromQuery] string? nameFilter = null)
{ {
if (apiKey != _configuration["apiKey"])
{
_logger.LogWarning("ScheduleImportJobs: Unauthorized request with apiKey {ApiKey}", apiKey);
return Unauthorized();
}
try try
{ {
var jobsCreated = await _jobScheduler.ScheduleImportJobsAsync(nameFilter); var jobsCreated = await _jobScheduler.ScheduleImportJobsAsync(nameFilter);
@@ -170,21 +159,15 @@ public class JobsController : Controller
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "ScheduleImportJobs: Error scheduling import jobs"); _logger.LogError(ex, "ScheduleImportJobs: Error scheduling import jobs");
return BadRequest(ex.ToString()); return BadRequest("An error occurred processing your request");
} }
} }
[HttpPost] [HttpPost]
[Route("schedule/processes/{apiKey}")] [Route("schedule/processes")]
[AllowAnonymous] [ApiKeyAuth]
public async Task<IActionResult> ScheduleProcessJobs(string apiKey) public async Task<IActionResult> ScheduleProcessJobs()
{ {
if (apiKey != _configuration["apiKey"])
{
_logger.LogWarning("ScheduleProcessJobs: Unauthorized request with apiKey {ApiKey}", apiKey);
return Unauthorized();
}
try try
{ {
var jobsCreated = await _jobScheduler.ScheduleProcessJobsAsync(); var jobsCreated = await _jobScheduler.ScheduleProcessJobsAsync();
@@ -201,7 +184,7 @@ public class JobsController : Controller
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "ScheduleProcessJobs: Error scheduling process jobs"); _logger.LogError(ex, "ScheduleProcessJobs: Error scheduling process jobs");
return BadRequest(ex.ToString()); return BadRequest("An error occurred processing your request");
} }
} }
@@ -243,7 +226,7 @@ public class JobsController : Controller
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "RetryJob: Error retrying job {JobId}", id); _logger.LogError(ex, "RetryJob: Error retrying job {JobId}", id);
return BadRequest(ex.ToString()); return BadRequest("An error occurred processing your request");
} }
} }
@@ -290,7 +273,7 @@ public class JobsController : Controller
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "CancelJob: Error cancelling job {JobId}", id); _logger.LogError(ex, "CancelJob: Error cancelling job {JobId}", id);
return BadRequest(ex.ToString()); return BadRequest("An error occurred processing your request");
} }
} }
@@ -317,7 +300,7 @@ public class JobsController : Controller
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "GetStats: Error retrieving job statistics"); _logger.LogError(ex, "GetStats: Error retrieving job statistics");
return BadRequest(ex.ToString()); return BadRequest("An error occurred processing your request");
} }
} }
@@ -429,7 +412,7 @@ public class JobsController : Controller
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "CreateJobForLayer: Error creating job for layer {LayerId}", layerId); _logger.LogError(ex, "CreateJobForLayer: Error creating job for layer {LayerId}", layerId);
return BadRequest(ex.ToString()); return BadRequest("An error occurred processing your request");
} }
} }
} }

View File

@@ -99,7 +99,7 @@ public class LayersController : Controller
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, "GetAll: Error retrieving layers"); _logger.LogError(e, "GetAll: Error retrieving layers");
return BadRequest(e.ToString()); return BadRequest("An error occurred processing your request");
} }
} }
[HttpGet] [HttpGet]
@@ -119,7 +119,7 @@ public class LayersController : Controller
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, "Get: Error retrieving layer {LayerId}", id); _logger.LogError(e, "Get: Error retrieving layer {LayerId}", id);
return BadRequest(e.ToString()); return BadRequest("An error occurred processing your request");
} }
} }
[HttpGet] [HttpGet]
@@ -396,7 +396,7 @@ public class LayersController : Controller
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, "AutoImport: Process error"); _logger.LogError(e, "AutoImport: Process error");
return BadRequest(e.ToString()); return BadRequest("An error occurred processing your request");
} }
} }
@@ -808,7 +808,7 @@ public class LayersController : Controller
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, "CreateRecord: Error creating record in layer {LayerId}", layerId); _logger.LogError(e, "CreateRecord: Error creating record in layer {LayerId}", layerId);
return BadRequest(e.ToString()); return BadRequest("An error occurred processing your request");
} }
} }
@@ -889,7 +889,7 @@ public class LayersController : Controller
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, "UpdateRecord: Error updating record {RecordId} in layer {LayerId}", recordId, layerId); _logger.LogError(e, "UpdateRecord: Error updating record {RecordId} in layer {LayerId}", recordId, layerId);
return BadRequest(e.ToString()); return BadRequest("An error occurred processing your request");
} }
} }
@@ -944,7 +944,7 @@ public class LayersController : Controller
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, "DeleteRecord: Error deleting record {RecordId} from layer {LayerId}", recordId, layerId); _logger.LogError(e, "DeleteRecord: Error deleting record {RecordId} from layer {LayerId}", recordId, layerId);
return BadRequest(e.ToString()); return BadRequest("An error occurred processing your request");
} }
} }
@@ -983,7 +983,7 @@ public class LayersController : Controller
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, "GetRecordHistory: Error retrieving history for record {RecordId}", recordId); _logger.LogError(e, "GetRecordHistory: Error retrieving history for record {RecordId}", recordId);
return BadRequest(e.ToString()); return BadRequest("An error occurred processing your request");
} }
} }
@@ -1033,7 +1033,7 @@ public class LayersController : Controller
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, "GetDeletedRecords: Error retrieving deleted records for layer {LayerId}", layerId); _logger.LogError(e, "GetDeletedRecords: Error retrieving deleted records for layer {LayerId}", layerId);
return BadRequest(e.ToString()); return BadRequest("An error occurred processing your request");
} }
} }