Skip to content

Commit b6fb225

Browse files
LNK-4621: data acquisition query plan rest api not properly validating values (#1383)
* LNK-4621: enhanced qp validation * add/fix unit tests * fix integration tests * Update QueryPlanManager.cs * Sanitize inputs QPManager * Update QueryPlanManager.cs * Update QueryPlanValidator.cs * Update DataAcquisitionIntegrationTestFixture.cs
1 parent 7cc3141 commit b6fb225

11 files changed

Lines changed: 1800 additions & 97 deletions

File tree

Lines changed: 126 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
using DataAcquisition.Domain.Application.Models;
2-
using DataAcquisition.Domain.Application.Models.Exceptions;
32
using LantanaGroup.Link.DataAcquisition.Domain.Application.Models;
43
using LantanaGroup.Link.DataAcquisition.Domain.Application.Models.Exceptions;
4+
using LantanaGroup.Link.DataAcquisition.Domain.Application.Validators;
55
using LantanaGroup.Link.DataAcquisition.Domain.Infrastructure;
66
using LantanaGroup.Link.DataAcquisition.Domain.Infrastructure.Entities;
7-
using LantanaGroup.Link.DataAcquisition.Domain.Infrastructure.Interfaces;
8-
using LantanaGroup.Link.DataAcquisition.Domain.Infrastructure.Models.QueryConfig;
97
using LantanaGroup.Link.Shared.Application.Models;
108
using Microsoft.Extensions.Logging;
119

@@ -23,11 +21,60 @@ public class QueryPlanManager : IQueryPlanManager
2321
{
2422
private readonly IDatabase _database;
2523
private readonly ILogger<QueryPlanManager> _logger;
24+
private readonly IQueryPlanValidator _validator;
2625

27-
public QueryPlanManager(IDatabase database, ILogger<QueryPlanManager> logger)
26+
public QueryPlanManager(
27+
IDatabase database,
28+
ILogger<QueryPlanManager> logger,
29+
IQueryPlanValidator validator)
2830
{
2931
_database = database ?? throw new ArgumentNullException(nameof(database));
3032
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
33+
_validator = validator ?? throw new ArgumentNullException(nameof(validator));
34+
}
35+
36+
/// <summary>
37+
/// Returns a sanitized version of a value that is safe to include in log messages.
38+
/// Removes newline characters to help prevent log forging.
39+
/// </summary>
40+
/// <param name="value">The original value.</param>
41+
/// <returns>A logging-safe value.</returns>
42+
private static string? SanitizeForLog(string? value)
43+
{
44+
if (value == null)
45+
{
46+
return null;
47+
}
48+
49+
// Remove carriage return and line feed characters that can break log structure.
50+
return value.Replace("\r", string.Empty)
51+
.Replace("\n", string.Empty);
52+
}
53+
54+
/// <summary>
55+
/// Sanitizes log messages derived from user input to prevent log forging by removing line breaks.
56+
/// </summary>
57+
/// <param name="messages">The collection of messages to sanitize.</param>
58+
/// <returns>An enumerable of sanitized messages.</returns>
59+
private static IEnumerable<string> SanitizeLogMessages(IEnumerable<string> messages)
60+
{
61+
if (messages == null)
62+
{
63+
yield break;
64+
}
65+
66+
foreach (var message in messages)
67+
{
68+
if (message == null)
69+
{
70+
continue;
71+
}
72+
73+
// Replace carriage returns and newlines with spaces to keep each log entry on a single line.
74+
yield return message
75+
.Replace("\r", " ")
76+
.Replace("\n", " ");
77+
}
3178
}
3279

3380
public async Task<QueryPlanModel> AddAsync(CreateQueryPlanModel model, CancellationToken cancellationToken = default)
@@ -37,9 +84,27 @@ public async Task<QueryPlanModel> AddAsync(CreateQueryPlanModel model, Cancellat
3784
throw new ArgumentNullException(nameof(model), "CreateQueryPlanModel cannot be null.");
3885
}
3986

40-
//// Validate query order
41-
ValidateQueryOrder(model.InitialQueries, "InitialQueries");
42-
ValidateQueryOrder(model.SupplementalQueries, "SupplementalQueries");
87+
// Perform comprehensive validation
88+
var validationResult = _validator.ValidateQueryPlan(model.InitialQueries, model.SupplementalQueries);
89+
90+
var safeFacilityId = SanitizeForLog(model.FacilityId);
91+
92+
if (!validationResult.IsValid)
93+
{
94+
_logger.LogError("Query Plan validation failed for facility {FacilityId}: {Errors}",
95+
safeFacilityId,
96+
string.Join("; ", SanitizeLogMessages(validationResult.Errors)));
97+
98+
throw new BadRequestException($"Query Plan validation failed: {validationResult.GetErrorMessage()}");
99+
}
100+
101+
// Log warnings if any exist
102+
if (validationResult.Warnings.Any())
103+
{
104+
_logger.LogWarning("Query Plan validation warnings for facility {FacilityId}: {Warnings}",
105+
safeFacilityId,
106+
string.Join("; ", SanitizeLogMessages(validationResult.Warnings)));
107+
}
43108

44109
var date = DateTime.UtcNow;
45110

@@ -59,6 +124,10 @@ public async Task<QueryPlanModel> AddAsync(CreateQueryPlanModel model, Cancellat
59124
entity = await _database.QueryPlanRepository.AddAsync(entity);
60125
await _database.QueryPlanRepository.SaveChangesAsync();
61126

127+
_logger.LogInformation("Successfully created Query Plan for facility {FacilityId} with type {Type}",
128+
SanitizeForLog(model.FacilityId),
129+
model.Type);
130+
62131
return QueryPlanModel.FromDomain(entity);
63132
}
64133

@@ -69,82 +138,88 @@ public async Task<QueryPlanModel> UpdateAsync(UpdateQueryPlanModel model, Cancel
69138
throw new ArgumentNullException(nameof(model), "UpdateQueryPlanModel cannot be null.");
70139
}
71140

72-
// Validate query order
73-
ValidateQueryOrder(model.InitialQueries, "InitialQueries");
74-
ValidateQueryOrder(model.SupplementalQueries, "SupplementalQueries");
141+
// Perform comprehensive validation
142+
var validationResult = _validator.ValidateQueryPlan(model.InitialQueries, model.SupplementalQueries);
75143

76-
var existingQueryPlan = await _database.QueryPlanRepository.FirstOrDefaultAsync(q => q.FacilityId == model.FacilityId && q.Type == model.Type);
144+
if (!validationResult.IsValid)
145+
{
146+
_logger.LogError("Query Plan validation failed for facility {FacilityId}: {Errors}",
147+
SanitizeForLog(model.FacilityId),
148+
string.Join("; ", SanitizeLogMessages(validationResult.Errors)));
149+
150+
throw new BadRequestException($"Query Plan validation failed: {validationResult.GetErrorMessage()}");
151+
}
77152

78-
if (existingQueryPlan != null)
153+
// Log warnings if any exist
154+
if (validationResult.Warnings.Any())
79155
{
80-
existingQueryPlan.InitialQueries = model.InitialQueries;
81-
existingQueryPlan.SupplementalQueries = model.SupplementalQueries;
82-
existingQueryPlan.PlanName = model.PlanName;
83-
existingQueryPlan.EHRDescription = model.EHRDescription;
84-
existingQueryPlan.LookBack = model.LookBack;
85-
existingQueryPlan.ModifyDate = DateTime.UtcNow;
156+
_logger.LogWarning("Query Plan validation warnings for facility {FacilityId}: {Warnings}",
157+
SanitizeForLog(model.FacilityId),
158+
string.Join("; ", SanitizeLogMessages(validationResult.Warnings)));
159+
}
86160

87-
await _database.QueryPlanRepository.SaveChangesAsync();
161+
var existingQueryPlan = await _database.QueryPlanRepository.FirstOrDefaultAsync(
162+
q => q.FacilityId == model.FacilityId && q.Type == model.Type);
88163

89-
return QueryPlanModel.FromDomain(existingQueryPlan);
164+
if (existingQueryPlan == null)
165+
{
166+
throw new NotFoundException($"No Query Plan for FacilityId {model.FacilityId} and Type {model.Type} was found.");
90167
}
91168

92-
throw new NotFoundException($"No Query Plan for FacilityId {model.FacilityId} and Type {model.Type} was found.");
169+
existingQueryPlan.InitialQueries = model.InitialQueries;
170+
existingQueryPlan.SupplementalQueries = model.SupplementalQueries;
171+
existingQueryPlan.PlanName = model.PlanName;
172+
existingQueryPlan.EHRDescription = model.EHRDescription;
173+
existingQueryPlan.LookBack = model.LookBack;
174+
existingQueryPlan.ModifyDate = DateTime.UtcNow;
175+
176+
await _database.QueryPlanRepository.SaveChangesAsync();
177+
178+
_logger.LogInformation("Successfully updated Query Plan for facility {FacilityId} with type {Type}",
179+
SanitizeForLog(model.FacilityId),
180+
model.Type);
181+
182+
return QueryPlanModel.FromDomain(existingQueryPlan);
93183
}
94184

95185
public async Task DeleteAsync(string facilityId, Frequency type, CancellationToken cancellationToken = default)
96186
{
97-
var entity = await _database.QueryPlanRepository.SingleOrDefaultAsync(q => q.FacilityId == facilityId && q.Type == type);
187+
var entity = await _database.QueryPlanRepository.SingleOrDefaultAsync(
188+
q => q.FacilityId == facilityId && q.Type == type);
98189

99-
if (entity != null)
100-
{
101-
_database.QueryPlanRepository.Remove(entity);
102-
await _database.QueryPlanRepository.SaveChangesAsync();
103-
}
104-
else
190+
if (entity == null)
105191
{
106192
throw new NotFoundException($"No Query Plan for FacilityId {facilityId} and Type {type} was found.");
107193
}
194+
195+
_database.QueryPlanRepository.Remove(entity);
196+
await _database.QueryPlanRepository.SaveChangesAsync();
197+
198+
_logger.LogInformation("Successfully deleted Query Plan for facility {FacilityId} with type {Type}",
199+
SanitizeForLog(facilityId),
200+
type);
108201
}
109202

110203
public async Task DeleteAllQueryPlansAsync(string facilityId, CancellationToken cancellationToken = default)
111204
{
112-
// Get all query plans
113205
var allPlans = await _database.QueryPlanRepository.GetAllAsync(cancellationToken);
114-
115-
// Filter by facilityId
116206
var facilityPlans = allPlans.Where(q => q.FacilityId == facilityId).ToList();
117207

118-
// Remove each plan individually
119208
foreach (var plan in facilityPlans)
120209
{
121210
_database.QueryPlanRepository.Remove(plan);
122211
}
123212

124-
// Save changes once after all removals
125213
if (facilityPlans.Any())
126214
{
127215
await _database.QueryPlanRepository.SaveChangesAsync(cancellationToken);
216+
_logger.LogInformation("Successfully deleted {Count} Query Plans for facility {FacilityId}",
217+
facilityPlans.Count,
218+
SanitizeForLog(facilityId));
128219
}
129-
}
130-
131-
private void ValidateQueryOrder(Dictionary<string, IQueryConfig> queries, string querySetName)
132-
{
133-
if (queries == null) return;
134-
135-
bool seenReference = false;
136-
foreach (var kvp in queries.OrderBy(q => int.TryParse(q.Key, out var i) ? i : int.MaxValue))
220+
else
137221
{
138-
var config = kvp.Value;
139-
if (config is ReferenceQueryConfig)
140-
{
141-
seenReference = true;
142-
}
143-
else if (config is ParameterQueryConfig && seenReference)
144-
{
145-
throw new IncorrectQueryPlanOrderException(
146-
$"All ReferenceQueryConfig entries must appear after all ParameterQueryConfig entries in {querySetName}.");
147-
}
222+
_logger.LogInformation("No Query Plans found to delete for facility {FacilityId}", SanitizeForLog(facilityId));
148223
}
149224
}
150225
}

0 commit comments

Comments
 (0)