Skip to content
Draft
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
970f2b7
Auth on remove DummyGpCodeFunction
MWClayson-NHS Apr 22, 2026
3cc0350
rbac on visabiliuty of remove DummyGPCode Card
MWClayson-NHS Apr 22, 2026
68f260a
restrict dummy gp removal page
MWClayson-NHS Apr 22, 2026
d6e36df
fix DI issue
MWClayson-NHS Apr 23, 2026
658c2a9
Merge branch 'main' into feat/wireup-dummyGPCode
MWClayson-NHS Apr 23, 2026
45c74b6
add auth config to dev.tfvars
MWClayson-NHS Apr 23, 2026
46f70ff
add auth headers to removeDummyGPCode UI
MWClayson-NHS Apr 23, 2026
b88ca20
Merge branch 'main' into feat/wireup-dummyGPCode
MWClayson-NHS Apr 27, 2026
845ac84
small refactor of auditing
MWClayson-NHS Apr 27, 2026
34e2e78
add Audit log to dummy GP Code
MWClayson-NHS Apr 27, 2026
4bb3ce1
Bugs
MWClayson-NHS Apr 28, 2026
b434905
Test Updates and refactor auth extension
MWClayson-NHS Apr 29, 2026
995d690
fix web tests
MWClayson-NHS Apr 29, 2026
b6d091f
remove audit log for snow participant tests
MWClayson-NHS Apr 29, 2026
414af29
update e2e tests with correct env varibles
MWClayson-NHS Apr 29, 2026
2eeca2a
Sonar Qube
MWClayson-NHS Apr 30, 2026
0f352d5
Update application/CohortManager/src/Functions/Shared/Common/Extensio…
MWClayson-NHS Apr 30, 2026
8cad4b7
Update application/CohortManager/src/Functions/ParticipantManagementS…
MWClayson-NHS Apr 30, 2026
60ccf0a
Update application/CohortManager/src/Functions/ParticipantManagementS…
MWClayson-NHS Apr 30, 2026
7c08cdb
Update application/CohortManager/src/Functions/ParticipantManagementS…
MWClayson-NHS Apr 30, 2026
59f9b6c
Update application/CohortManager/src/Functions/ParticipantManagementS…
MWClayson-NHS Apr 30, 2026
c69cfc6
Update .github/workflows/ci-ui-tests.yaml
MWClayson-NHS Apr 30, 2026
8befaaf
Update application/CohortManager/src/Functions/Shared/Common/Authenti…
MWClayson-NHS Apr 30, 2026
b96e7e6
Keyed Service for Audit Logging
MWClayson-NHS Apr 30, 2026
368486d
Update Audit env varible
MWClayson-NHS Apr 30, 2026
629aa98
copilot comment
MWClayson-NHS Apr 30, 2026
def90f4
sonar comment
MWClayson-NHS Apr 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci-ui-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ jobs:
EXCEPTIONS_API_URL: ${{ vars.EXCEPTIONS_API_URL }}
COHORT_MANAGER_RBAC_CODE: ${{ vars.COHORT_MANAGER_RBAC_CODE }}
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
COHORT_MANAGER_REMOVE_DUMMY_GP_CODE_RBAC_CODE : ${{ vars.COHORT_MANAGER_REMOVE_DUMMY_GP_CODE_RBAC_CODE }}
Comment thread
MWClayson-NHS marked this conversation as resolved.
Outdated
REMOVE_DUMMY_GP_CODE_API_URL: ${{ vars.REMOVE_DUMMY_GP_CODE_API_URL }}
run: npm run test:e2e

- name: Upload test results
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
namespace NHS.CohortManager.AuditServices;

using System.Text.Json;
using Common;
using DataServices.Database;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Model;

public class AuditWriter
{
private const string AuditBlobContainer = "participant-audit";

private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true
};

private readonly DataServicesContext _dbContext;
private readonly IBlobStorageHelper _blobStorageHelper;
private readonly AuditConfig _config;
private readonly ILogger<AuditWriter> _logger;

public AuditWriter(DataServicesContext dbContext, ILogger<AuditWriter> logger)
public AuditWriter(
DataServicesContext dbContext,
IBlobStorageHelper blobStorageHelper,
IOptions<AuditConfig> config,
ILogger<AuditWriter> logger)
{
_dbContext = dbContext;
_blobStorageHelper = blobStorageHelper;
_config = config.Value;
_logger = logger;
}

Expand All @@ -43,6 +55,8 @@
return;
}

var rawDataRef = await WriteSnapshotToBlobAsync(audit);
Comment thread
MWClayson-NHS marked this conversation as resolved.

var auditLog = new ParticipantAuditLog
{
CorrelationId = audit.CorrelationId,
Expand All @@ -53,7 +67,7 @@
RecordSourceDesc = audit.RecordSourceDesc,
CreatedBy = audit.CreatedBy,
ScreeningId = audit.ScreeningId,
RawDataRef = audit.RawDataRef
RawDataRef = rawDataRef
};

try
Expand All @@ -80,4 +94,44 @@
audit.CorrelationId);
}
}

private async Task<string?> WriteSnapshotToBlobAsync(ParticipantAuditMessage message)
{
if (message.RequestSnapshot is null)
{
return null;
}

var blobPath = $"{message.CreatedDatetime:dd-MM-yyyy}/{message.CorrelationId}.json";
var payload = JsonSerializer.SerializeToUtf8Bytes(message.RequestSnapshot, JsonOptions);
var blobFile = new BlobFile(payload, blobPath);

try
{
var uri = await _blobStorageHelper.UploadFileToBlobStorageAndGetUri(
_config.AzureWebJobsStorage,
AuditBlobContainer,
blobFile,
overwrite: true);

if (uri is null)
{
_logger.LogError(
"Blob write returned null URI for CorrelationId {CorrelationId}.",
message.CorrelationId);
throw new InvalidOperationException(
$"Blob write returned null URI for CorrelationId {message.CorrelationId}.");
}

return uri;
}
catch (Exception ex)

Check warning on line 128 in application/CohortManager/src/Functions/AuditServices/AuditWriter/AuditWriter.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Either log this exception and handle it, or rethrow it with some contextual information.

See more on https://sonarcloud.io/project/issues?id=NHSDigital_dtos-cohort-manager&issues=AZ3ZPSMIjBcjKM-5sX96&open=AZ3ZPSMIjBcjKM-5sX96&pullRequest=1895
{
_logger.LogError(
ex,
"Failed to write audit snapshot to blob for CorrelationId {CorrelationId}.",
message.CorrelationId);
throw;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
.AddServiceBusClient(auditConfig.ServiceBusConnectionString)
.ConfigureServices(services =>
{
services.AddTransient<IBlobStorageHelper, BlobStorageHelper>();
services.AddDatabaseHealthCheck("AuditWriter");
})
.AddTelemetry()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,12 @@
services.AddTransient<IBlobStorageHelper, BlobStorageHelper>();
services.AddTransient<ICopyFailedBatchToBlob, CopyFailedBatchToBlob>();
services.AddScoped<IValidateDates, ValidateDates>();
services.AddTransient<IAuditLogClient, AuditLogClient>();
// Register health checks
services.AddBlobStorageHealthCheck("receiveCaasFile");
})
.AddTelemetry()
.AddHttpClient()
.AddServiceBusClient(config.ServiceBusConnectionString_client_internal)
.AddAuditLogging(config.ServiceBusConnectionString_client_internal)
.AddExceptionHandler()
.AddDatabaseConnection()
.Build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,19 @@ public class ManageServiceNowParticipantFunction
private readonly IExceptionHandler _exceptionHandler;
private readonly IDataServiceClient<ParticipantManagement> _participantManagementClient;
private readonly IQueueClient _queueClient;
private readonly IAuditLogClient _auditLogClient;

private static readonly Regex NonLetterRegex = new(@"[^\p{Lu}\p{Ll}\p{Lt}]", RegexOptions.Compiled, TimeSpan.FromSeconds(1));

public ManageServiceNowParticipantFunction(ILogger<ManageServiceNowParticipantFunction> logger, IOptions<ManageServiceNowParticipantConfig> config,
IHttpClientFunction httpClientFunction, IExceptionHandler handleException, IDataServiceClient<ParticipantManagement> participantManagementClient,
IQueueClient queueClient, IAuditLogClient auditLogClient)
IQueueClient queueClient)
{
_logger = logger;
_config = config.Value;
_httpClientFunction = httpClientFunction;
_exceptionHandler = handleException;
_participantManagementClient = participantManagementClient;
_queueClient = queueClient;
_auditLogClient = auditLogClient;
}

/// <summary>
Expand Down Expand Up @@ -86,15 +84,6 @@ public async Task Run([ServiceBusTrigger(topicName: "%ServiceNowParticipantManag
await HandleException(new Exception($"Failed to send participant from ServiceNow to topic: {_config.CohortDistributionTopic}"), serviceNowParticipant, ServiceNowMessageType.AddRequestInProgress);
}

await _auditLogClient.AddAsync(new ParticipantAuditMessage
{
NhsNumber = serviceNowParticipant.NhsNumber.ToString(),
Source = AuditSource.ManualAdd,
RecordSourceDesc = $"ServiceNow case: {serviceNowParticipant.ServiceNowCaseNumber}",
CreatedBy = nameof(ManageServiceNowParticipantFunction),
ScreeningId = (int)serviceNowParticipant.ScreeningId,
RequestSnapshot = serviceNowParticipant,
});
}
catch (Exception ex)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@

var host = new HostBuilder()
.AddConfiguration(out ManageServiceNowParticipantConfig config)
.AddConfiguration<AuditClientConfig>()
.AddDataServicesHandler()
.AddDataServicesHandler()
.AddDataService<ParticipantManagement>(config.ParticipantManagementURL)
.Build()
.ConfigureFunctionsWorkerDefaults()
Expand All @@ -18,11 +17,10 @@
// Register health checks
services.AddBasicHealthCheck("ManageServiceNowParticipant");
services.AddTransient<IBlobStorageHelper, BlobStorageHelper>();
services.AddTransient<IAuditLogClient, AuditLogClient>();
})
.AddServiceBusClient(config.ServiceBusConnectionString_client_internal)
.AddTelemetry()
.AddExceptionHandler()
.AddServiceBusClient(config.ServiceBusConnectionString_client_internal)
.AddHttpClient()
.Build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@

var host = new HostBuilder()
.AddConfiguration(out RemoveDummyGpCodeConfig config)
.ConfigureFunctionsWorkerDefaults()
.AddAuthentication()
.ConfigureServices(services =>
{
services.AddSingleton<ICreateResponse, CreateResponse>();
services.AddBasicHealthCheck("RemoveDummyGPCode");
})
.AddAuditLogging(config.ServiceBusConnectionString_client_internal)
.AddTelemetry()
.AddHttpClient()
.AddServiceBusClient(config.ServiceBusConnectionString_client_internal)
Comment thread
MWClayson-NHS marked this conversation as resolved.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace NHS.CohortManager.ParticipantManagementServices;
using System.Globalization;
using System.Net;
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
Comment thread
MWClayson-NHS marked this conversation as resolved.
Outdated
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
Expand All @@ -14,6 +15,7 @@ namespace NHS.CohortManager.ParticipantManagementServices;
using Microsoft.Extensions.Options;
using Model;
using Model.Constants;
using Model.Enums;
using NHS.CohortManager.ParticipantManagementServices.Models;

public class ReceiveRemoveDummyGpCodeFunction
Expand All @@ -23,6 +25,8 @@ public class ReceiveRemoveDummyGpCodeFunction
private readonly IHttpClientFunction _httpClientFunction;
private readonly IQueueClient _queueClient;
private readonly RemoveDummyGpCodeConfig _config;
private readonly IAuditLogClient _auditLogClient;
private readonly IFunctionContextAuthResolver _authResolver;

private static readonly Regex NonLetterRegex = new(@"[^\p{Lu}\p{Ll}\p{Lt}]", RegexOptions.Compiled, TimeSpan.FromSeconds(1));

Expand All @@ -31,19 +35,24 @@ public ReceiveRemoveDummyGpCodeFunction(
ICreateResponse createResponse,
IHttpClientFunction httpClientFunction,
IQueueClient queueClient,
IOptions<RemoveDummyGpCodeConfig> config)
IOptions<RemoveDummyGpCodeConfig> config,
IAuditLogClient auditLogClient,
IFunctionContextAuthResolver authResolver)
{
_logger = logger;
_createResponse = createResponse;
_httpClientFunction = httpClientFunction;
_queueClient = queueClient;
_config = config.Value;
_auditLogClient = auditLogClient;
_authResolver = authResolver;
}

/// <summary>
/// Validates and enqueues a dummy GP code removal request to the ServiceNow participant management topic.
/// </summary>
[Function("ReceiveRemoveDummyGPCodeFunction")]
[Authentication(Role.CohortManagerDummyGpRemoval)]
public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "RemoveDummyGPCode")] HttpRequestData req)
{
try
Expand All @@ -55,6 +64,29 @@ public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymou
return _createResponse.CreateHttpResponse(HttpStatusCode.BadRequest, req);
}


var user = _authResolver.GetCis2User(req.FunctionContext);

if(user == null && _authResolver.IsAuthenticationRequired(req.FunctionContext))
{
_logger.LogError("User information could not be retrieved from the function context");
return _createResponse.CreateHttpResponse(HttpStatusCode.InternalServerError, req);
Comment thread
MWClayson-NHS marked this conversation as resolved.
Outdated
}


await _auditLogClient.AddAsync(new ParticipantAuditMessage
{
NhsNumber = requestBody.NhsNumber,
Source = AuditSource.DummyGpRemoval,
RecordSourceDesc = "Dummy GP Code Removal Form",
CreatedBy = user != null ? $"{user.GivenName} {user.FamilyName}" : "Unknown User",
Comment thread
MWClayson-NHS marked this conversation as resolved.
Outdated
ScreeningId = 1,
Comment thread
MWClayson-NHS marked this conversation as resolved.
Outdated
RequestSnapshot = requestBody

});



var validationContext = new ValidationContext(requestBody);
var validationResult = new List<ValidationResult>();
var isRequestValid = Validator.TryValidateObject(requestBody, validationContext, validationResult, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
// Register health checks
services.AddBasicHealthCheck("ServiceNowMessageHandler");
})
.AddAuditLogging(config.ServiceBusConnectionString_client_internal)
.AddTelemetry()
.AddServiceBusClient(config.ServiceBusConnectionString_client_internal)
Comment thread
MWClayson-NHS marked this conversation as resolved.
.Build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,17 @@ public class ReceiveServiceNowMessageFunction
private readonly ServiceNowMessageHandlerConfig _config;
private readonly IDataServiceClient<ServicenowCase> _serviceNowCaseClient;
private readonly IServiceNowClient _serviceNowClient;
private readonly IAuditLogClient _auditLogClient;

public ReceiveServiceNowMessageFunction(ILogger<ReceiveServiceNowMessageFunction> logger, ICreateResponse createResponse,
IQueueClient queueClient, IOptions<ServiceNowMessageHandlerConfig> config, IDataServiceClient<ServicenowCase> serviceNowCaseClient,
IServiceNowClient serviceNowClient)
IServiceNowClient serviceNowClient, IAuditLogClient auditLogClient)
{
_logger = logger;
_createResponse = createResponse;
_queueClient = queueClient;
_config = config.Value;
_auditLogClient = auditLogClient;
_serviceNowCaseClient = serviceNowCaseClient;
_serviceNowClient = serviceNowClient;
}
Expand Down Expand Up @@ -59,6 +61,16 @@ public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymou
return _createResponse.CreateHttpResponse(HttpStatusCode.BadRequest, req);
}

await _auditLogClient.AddAsync(new ParticipantAuditMessage
{
NhsNumber = requestBody.VariableData.NhsNumber,
Source = AuditSource.ManualAdd,
RecordSourceDesc = $"ServiceNow case: {requestBody.ServiceNowCaseNumber}",
CreatedBy = nameof(ReceiveServiceNowMessageFunction),
ScreeningId = (int)ServiceProvider.BSS,
RequestSnapshot = requestBody
});

var validationContext = new ValidationContext(requestBody.VariableData);
var validationResult = new List<ValidationResult>();
bool isVariableDataValid = Validator.TryValidateObject(requestBody.VariableData, validationContext, validationResult, true);
Expand Down
Loading
Loading