Skip to content

Commit 55b67bf

Browse files
authored
Merge pull request #1702 from bcgov/dev
Dev
2 parents e5775e9 + a78d935 commit 55b67bf

41 files changed

Lines changed: 1626 additions & 120 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/Collectors/BCAddressCollector.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Text.Json;
55
using System.Threading.Tasks;
66
using Unity.Flex.Worksheets.Values;
7+
using Unity.GrantManager.Integration.Geocoder;
78
using Unity.GrantManager.Integrations.Geocoder;
89

910
namespace Unity.Flex.Worksheets.Collectors

applications/Unity.GrantManager/modules/Unity.Notifications/src/Unity.Notifications.Application/Integrations/Ches/ChesClientService.cs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Net.Http;
99
using Volo.Abp.Caching;
1010
using Unity.GrantManager.Integrations;
11+
using Unity.GrantManager.Integrations.Css;
1112

1213
namespace Unity.Notifications.Integrations.Ches
1314
{
@@ -27,22 +28,18 @@ IOptions<ChesClientOptions> chesClientOptions
2728
string authToken = await GetAuthTokenAsync();
2829
string notificationsApiUrl = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.NOTIFICATION_API_BASE);
2930
var resource = $"{notificationsApiUrl}/email";
30-
3131
// Pass the object directly; ResilientHttpRequest will serialize it to JSON
3232
var response = await resilientHttpRequest.HttpAsync(
3333
HttpMethod.Post,
3434
resource,
3535
emailRequest,
3636
authToken
3737
);
38-
3938
return response;
4039
}
41-
4240
private async Task<string> GetAuthTokenAsync()
4341
{
4442
string notificationsAuthUrl = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.NOTIFICATION_AUTH);
45-
4643
ClientOptions clientOptions = new()
4744
{
4845
Url = notificationsAuthUrl,
@@ -55,4 +52,4 @@ private async Task<string> GetAuthTokenAsync()
5552
return await tokenService.GetAuthTokenAsync(clientOptions);
5653
}
5754
}
58-
}
55+
}

applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/CasTokenService.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
using Volo.Abp;
77
using Volo.Abp.Application.Services;
88
using Volo.Abp.Caching;
9-
using Volo.Abp.DependencyInjection;
9+
using Unity.GrantManager.Integrations.Css;
1010

11+
using Volo.Abp.DependencyInjection;
1112
namespace Unity.Payments.Integrations.Cas
1213
{
1314
[IntegrationService]
1415
[ExposeServices(typeof(CasTokenService), typeof(ICasTokenService))]
16+
1517
public class CasTokenService(
1618
IEndpointManagementAppService endpointManagementAppService,
1719
IOptions<CasClientOptions> casClientOptions,
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
using System.Threading.Tasks;
22
using Volo.Abp.Application.Services;
3-
43
namespace Unity.Payments.Integrations.Cas
54
{
65
public interface ICasTokenService : IApplicationService
76
{
87
Task<string> GetAuthTokenAsync();
98
}
10-
}
9+
}

applications/Unity.GrantManager/modules/Unity.Payments/src/Unity.Payments.Application/Integrations/Cas/SupplierService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,13 @@ public SupplierService (ILocalEventBus localEventBus,
4141
// Initialize the base API URL once during construction
4242
casBaseApiTask = InitializeBaseApiAsync(endpointManagementAppService);
4343
}
44+
4445
private static async Task<string> InitializeBaseApiAsync(IEndpointManagementAppService endpointManagementAppService)
4546
{
4647
var url = await endpointManagementAppService.GetUgmUrlByKeyNameAsync(DynamicUrlKeyNames.PAYMENT_API_BASE);
4748
return url ?? throw new UserFriendlyException("Payment API base URL is not configured.");
4849
}
4950

50-
5151
public virtual async Task UpdateApplicantSupplierInfo(string? supplierNumber, Guid applicantId)
5252
{
5353
Logger.LogInformation("SupplierService->UpdateApplicantSupplierInfo: {SupplierNumber}, {ApplicantId}", supplierNumber, applicantId);

applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/IResilientHttpRequest.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,15 @@ Task<HttpResponseMessage> HttpAsync(
2323
/// Set a base URL to be used for relative request paths.
2424
/// </summary>
2525
void SetBaseUrl(string baseUrl);
26+
27+
Task<HttpResponseMessage> HttpAsyncSecured(
28+
HttpMethod httpVerb,
29+
string resource,
30+
string certPath,
31+
string? certPassword = null,
32+
object? body = null,
33+
string? authToken = null,
34+
(string username, string password)? basicAuth = null,
35+
CancellationToken cancellationToken = default);
2636
}
2737
}

applications/Unity.GrantManager/modules/Unity.SharedKernel/Http/ResilientHttpRequest.cs

Lines changed: 91 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Linq;
55
using System.Net;
66
using System.Net.Http;
7+
using System.Security.Cryptography.X509Certificates;
78
using System.Text;
89
using System.Threading;
910
using System.Threading.Tasks;
@@ -18,22 +19,19 @@ public class ResilientHttpRequest(HttpClient httpClient) : IResilientHttpRequest
1819
private static int _maxRetryAttempts = 3;
1920
private static TimeSpan _pauseBetweenFailures = TimeSpan.FromSeconds(2);
2021
private static TimeSpan _httpRequestTimeout = TimeSpan.FromSeconds(60);
21-
2222
private const string AuthorizationHeader = "Authorization";
23-
2423
private static ResiliencePipeline<HttpResponseMessage> _pipeline = BuildPipeline();
25-
2624
private string? _baseUrl;
2725
private readonly HttpClient _httpClient = httpClient;
2826

29-
private static readonly HttpStatusCode[] RetryableStatusCodes =
30-
[
27+
private static readonly HttpStatusCode[] RetryableStatusCodes = new[]
28+
{
3129
HttpStatusCode.TooManyRequests,
3230
HttpStatusCode.InternalServerError,
3331
HttpStatusCode.BadGateway,
3432
HttpStatusCode.ServiceUnavailable,
3533
HttpStatusCode.GatewayTimeout
36-
];
34+
};
3735

3836
public static void SetPipelineOptions(
3937
int maxRetryAttempts,
@@ -43,7 +41,6 @@ public static void SetPipelineOptions(
4341
_maxRetryAttempts = maxRetryAttempts;
4442
_pauseBetweenFailures = pauseBetweenFailures;
4543
_httpRequestTimeout = httpRequestTimeout;
46-
4744
_pipeline = BuildPipeline(); // rebuild with new settings
4845
}
4946

@@ -70,12 +67,10 @@ public void SetBaseUrl(string baseUrl)
7067
{
7168
throw new ArgumentException("Base URL is not a valid absolute URI.", nameof(baseUrl));
7269
}
73-
7470
_baseUrl = baseUrl.TrimEnd('/');
7571
}
7672

77-
private static bool ShouldRetry(HttpStatusCode statusCode) =>
78-
RetryableStatusCodes.Contains(statusCode);
73+
private static bool ShouldRetry(HttpStatusCode statusCode) => RetryableStatusCodes.Contains(statusCode);
7974

8075
/// <summary>
8176
/// Send an HTTP request with resilience policies applied.
@@ -87,6 +82,57 @@ public async Task<HttpResponseMessage> HttpAsync(
8782
string? authToken = null,
8883
(string username, string password)? basicAuth = null,
8984
CancellationToken cancellationToken = default)
85+
{
86+
return await SendWithClientAsync(_httpClient, httpVerb, resource, body, authToken, basicAuth, cancellationToken);
87+
}
88+
89+
/// <summary>
90+
/// HTTP request with mutual TLS (client certificate).
91+
/// </summary>
92+
public Task<HttpResponseMessage> HttpAsyncSecured(
93+
HttpMethod httpVerb,
94+
string resource,
95+
string certPath,
96+
string? certPassword = null,
97+
object? body = null,
98+
string? authToken = null,
99+
(string username, string password)? basicAuth = null,
100+
CancellationToken cancellationToken = default)
101+
{
102+
var handler = new HttpClientHandler
103+
{
104+
ClientCertificateOptions = ClientCertificateOption.Manual
105+
};
106+
107+
X509Certificate2 clientCert = LoadCertificate(certPath, certPassword);
108+
handler.ClientCertificates.Add(clientCert);
109+
110+
using var securedHttpClient = new HttpClient(handler);
111+
return SendWithClientAsync(securedHttpClient, httpVerb, resource, body, authToken, basicAuth, cancellationToken);
112+
}
113+
114+
private static X509Certificate2 LoadCertificate(string certPath, string? certPassword = null)
115+
{
116+
if (string.IsNullOrWhiteSpace(certPassword))
117+
{
118+
// Load PEM or DER certificates
119+
return X509CertificateLoader.LoadCertificateFromFile(certPath);
120+
}
121+
else
122+
{
123+
// Load PFX/PKCS12 certificates with password
124+
return X509CertificateLoader.LoadPkcs12FromFile(certPath, certPassword);
125+
}
126+
}
127+
128+
private async Task<HttpResponseMessage> SendWithClientAsync(
129+
HttpClient client,
130+
HttpMethod httpVerb,
131+
string resource,
132+
object? body,
133+
string? authToken,
134+
(string username, string password)? basicAuth,
135+
CancellationToken cancellationToken)
90136
{
91137
// Determine final URL
92138
if (!Uri.TryCreate(resource, UriKind.Absolute, out Uri? fullUrl))
@@ -98,45 +144,47 @@ public async Task<HttpResponseMessage> HttpAsync(
98144
fullUrl = new Uri(new Uri(_baseUrl, UriKind.Absolute), resource);
99145
}
100146

101-
// Execute through resilience pipeline
102147
return await _pipeline.ExecuteAsync(async ct =>
103148
{
104-
using var requestMessage = new HttpRequestMessage(httpVerb, fullUrl);
149+
using var requestMessage = BuildRequestMessage(httpVerb, fullUrl, body, authToken, basicAuth);
150+
return await client.SendAsync(requestMessage, ct);
151+
}, cancellationToken);
152+
}
105153

106-
// Headers are per-request, not global
107-
requestMessage.Headers.Accept.Clear();
108-
requestMessage.Headers.ConnectionClose = true;
154+
private static HttpRequestMessage BuildRequestMessage(
155+
HttpMethod httpVerb,
156+
Uri fullUrl,
157+
object? body,
158+
string? authToken,
159+
(string username, string password)? basicAuth)
160+
{
161+
var requestMessage = new HttpRequestMessage(httpVerb, fullUrl);
162+
requestMessage.Headers.Accept.Clear();
163+
requestMessage.Headers.ConnectionClose = true;
109164

110-
if (!string.IsNullOrWhiteSpace(authToken))
111-
{
112-
requestMessage.Headers.Remove(AuthorizationHeader);
113-
requestMessage.Headers.Add(AuthorizationHeader, $"Bearer {authToken}");
114-
}
115-
else if (basicAuth.HasValue)
116-
{
117-
var credentials = Convert.ToBase64String(
118-
Encoding.ASCII.GetBytes($"{basicAuth.Value.username}:{basicAuth.Value.password}")
119-
);
120-
requestMessage.Headers.Remove(AuthorizationHeader);
121-
requestMessage.Headers.Add(AuthorizationHeader, $"Basic {credentials}");
122-
}
165+
if (!string.IsNullOrWhiteSpace(authToken))
166+
{
167+
requestMessage.Headers.Remove(AuthorizationHeader);
168+
requestMessage.Headers.Add(AuthorizationHeader, $"Bearer {authToken}");
169+
}
170+
else if (basicAuth.HasValue)
171+
{
172+
var credentials = Convert.ToBase64String(
173+
Encoding.ASCII.GetBytes($"{basicAuth.Value.username}:{basicAuth.Value.password}")
174+
);
175+
requestMessage.Headers.Remove(AuthorizationHeader);
176+
requestMessage.Headers.Add(AuthorizationHeader, $"Basic {credentials}");
177+
}
123178

124-
// Handle body if present
125-
if (body != null)
126-
{
127-
string bodyString = body is string s
128-
? s
129-
: JsonConvert.SerializeObject(body); // allow passing objects directly
130-
131-
requestMessage.Content = new StringContent(
132-
bodyString,
133-
Encoding.UTF8,
134-
"application/json"
135-
);
136-
}
179+
if (body != null)
180+
{
181+
string bodyString = body is string s ? s : JsonConvert.SerializeObject(body);
182+
requestMessage.Content = new StringContent(
183+
bodyString, Encoding.UTF8, "application/json"
184+
);
185+
}
137186

138-
return await _httpClient.SendAsync(requestMessage, ct);
139-
}, cancellationToken);
187+
return requestMessage;
140188
}
141189

142190
public static async Task<string> ContentToStringAsync(HttpContent httpContent)

applications/Unity.GrantManager/modules/Unity.SharedKernel/Integrations/ClientOptions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,7 @@ public class ClientOptions
66
public string ClientId { get; set; } = string.Empty;
77
public string ClientSecret { get; set; } = string.Empty;
88
public string ApiKey { get; set; } = string.Empty;
9+
public string CertificatePath { get; set; } = string.Empty;
10+
public string CertificatePassword { get; set; } = string.Empty;
911
}
1012
}

applications/Unity.GrantManager/modules/Unity.SharedKernel/Integrations/TokenService.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Volo.Abp;
1+
using Volo.Abp;
22
using System.Net;
33
using System.Threading.Tasks;
44
using System.Text.Json;
@@ -9,6 +9,7 @@
99
using System.Net.Http.Headers;
1010
using Microsoft.Extensions.Caching.Distributed;
1111
using Volo.Abp.Caching;
12+
using Unity.GrantManager.Integrations.Css;
1213

1314
namespace Unity.Modules.Shared.Integrations
1415
{

applications/Unity.GrantManager/modules/Unity.SharedKernel/Integrations/TokenValidationResponse.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
using System.Text.Json.Serialization;
1+
using System.Text.Json.Serialization;
22

3-
namespace Unity.Modules.Shared.Integrations
3+
namespace Unity.GrantManager.Integrations.Css
44
{
55
public class TokenValidationResponse
66
{
@@ -28,4 +28,4 @@ public class TokenValidationResponse
2828
[JsonPropertyName("error_decription")]
2929
public string? ErrorDetails { get; set; }
3030
}
31-
}
31+
}

0 commit comments

Comments
 (0)