Compare commits

..

10 Commits

Author SHA1 Message Date
754ef88644 Bump version to 2.0.2-beta in ReC.API.csproj
Updated project, assembly, and file versions from 2.0.1-beta/2.0.1.0 to 2.0.2-beta/2.0.2.0 to reflect new release. No other changes were made.
2026-03-16 13:46:10 +01:00
95ece6fdcf Remove unused imports from ResultController.cs
Removed unnecessary using statements for ReC.API.Extensions and ReC.API.Models to clean up the code and improve maintainability. No functional changes were made.
2026-03-16 13:44:38 +01:00
87194df697 Refactor and expand REST action authentication support
Refactored authentication logic for REST actions to use a structured switch block, adding explicit support for ApiKey, BearerToken, JwtBearer, OAuth2, BasicAuth, and NTLM authentication types. Introduced dedicated HttpClient/Handler for NTLM with proper disposal. Improved extensibility, clarity, and resource management. Added IOptions and Options usage for configuration.
2026-03-16 13:43:27 +01:00
b38d53248c Force HTTP/1.1 for NTLM when UseHttp1ForNtlm is enabled
Added logic to set HTTP/1.1 and exact version policy for NTLM
authentication requests when the UseHttp1ForNtlm option is true.
This ensures compatibility with NTLM endpoints requiring HTTP/1.1.
2026-03-16 13:41:04 +01:00
56b604bd35 Add UseHttp1ForNtlm option to RecAction config
Added the boolean UseHttp1ForNtlm property to the RecAction section in appsettings.json and RecActionOptions class. This option defaults to false and allows control over using HTTP/1 for NTLM authentication.
2026-03-16 13:38:45 +01:00
f67579dba9 Move AddedWho config to RecActionOptions and refactor usage
Centralize AddedWho under RecAction in appsettings.json and add it to RecActionOptions. Update InvokeRecActionViewCommandHandler to use IOptions<RecActionOptions> for strongly-typed configuration access, replacing previous config-based retrieval. Removes global AddedWho property for improved maintainability.
2026-03-16 13:32:34 +01:00
636397efb8 Remove MaxConcurrentInvocations from RecAction config
Removed the MaxConcurrentInvocations property from the RecActionOptions class and deleted the corresponding setting from the RecAction section in appsettings.json. This makes RecActionOptions an empty class.
2026-03-16 13:28:50 +01:00
ef4d0767e9 Remove FakeProfileId config and related extension method
Removed the GetFakeProfileId extension method and its class from ConfigExtensions.cs, along with the "FakeProfileId" entry from appsettings.json. Also fixed a trailing comma in appsettings.json to ensure valid JSON syntax.
2026-03-16 13:26:08 +01:00
f15725ade2 Support NTLM password substitution in DEBUG mode
In DEBUG builds, replace "%NTLM_PW%" in NTLM auth password with a value from config. This enables dynamic credential testing without hardcoding passwords.
2026-03-16 13:18:24 +01:00
382eef0089 Enable User Secrets in ReC.API.csproj for dev config
Added <UserSecretsId> to the project file to support ASP.NET Core User Secrets, allowing secure storage of sensitive development configuration data.
2026-03-16 12:28:58 +01:00
6 changed files with 119 additions and 96 deletions

View File

@@ -1,7 +1,5 @@
using MediatR;
using Microsoft.AspNetCore.Mvc;
using ReC.API.Extensions;
using ReC.API.Models;
using ReC.Application.Common.Procedures.DeleteProcedure;
using ReC.Application.Common.Procedures.InsertProcedure;
using ReC.Application.Common.Procedures.UpdateProcedure;

View File

@@ -1,6 +0,0 @@
namespace ReC.API.Extensions;
public static class ConfigurationExtensions
{
public static int GetFakeProfileId(this IConfiguration config) => config.GetValue("FakeProfileId", 2);
}

View File

@@ -10,13 +10,14 @@
<Product>ReC.API</Product>
<PackageIcon>Assets\icon.ico</PackageIcon>
<PackageTags>digital data rest-caller rec api</PackageTags>
<Version>2.0.1-beta</Version>
<AssemblyVersion>2.0.1.0</AssemblyVersion>
<FileVersion>2.0.1.0</FileVersion>
<Version>2.0.2-beta</Version>
<AssemblyVersion>2.0.2.0</AssemblyVersion>
<FileVersion>2.0.2.0</FileVersion>
<InformationalVersion>2.0.1-beta</InformationalVersion>
<Copyright>Copyright © 2025 Digital Data GmbH. All rights reserved.</Copyright>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
<UserSecretsId>cf893b96-c71a-4a96-a6a7-40004249e1a3</UserSecretsId>
</PropertyGroup>
<ItemGroup>

View File

@@ -6,13 +6,12 @@
"AllowedHosts": "*",
"LuckyPennySoftwareLicenseKey": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ikx1Y2t5UGVubnlTb2Z0d2FyZUxpY2Vuc2VLZXkvYmJiMTNhY2I1OTkwNGQ4OWI0Y2IxYzg1ZjA4OGNjZjkiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2x1Y2t5cGVubnlzb2Z0d2FyZS5jb20iLCJhdWQiOiJMdWNreVBlbm55U29mdHdhcmUiLCJleHAiOiIxNzg0ODUxMjAwIiwiaWF0IjoiMTc1MzM2MjQ5MSIsImFjY291bnRfaWQiOiIwMTk4M2M1OWU0YjM3MjhlYmZkMzEwM2MyYTQ4NmU4NSIsImN1c3RvbWVyX2lkIjoiY3RtXzAxazB5NmV3MmQ4YTk4Mzg3aDJnbTRuOWswIiwic3ViX2lkIjoiLSIsImVkaXRpb24iOiIwIiwidHlwZSI6IjIifQ.ZqsFG7kv_-xGfxS6ACk3i0iuNiVUXX2AvPI8iAcZ6-z2170lGv__aO32tWpQccD9LCv5931lBNLWSblKS0MT3gOt-5he2TEftwiSQGFwoIBgtOHWsNRMinUrg2trceSp3IhyS3UaMwnxZDrCvx4-0O-kpOzVpizeHUAZNr5U7oSCWO34bpKdae6grtM5e3f93Z1vs7BW_iPgItd-aLvPwApbaG9VhmBTKlQ7b4Jh64y7UXJ9mKP7Qb_Oa97oEg0oY5DPHOWTZWeE1EzORgVr2qkK2DELSHuZ_EIUhODojkClPNAKtvEl_qEjpq0HZCIvGwfCCRlKlSkQqIeZdFkiXg",
"RecAction": {
"MaxConcurrentInvocations": 5
"AddedWho": "ReC.API",
"UseHttp1ForNtlm": false
},
// Bad request SqlException numbers numbers can be updated at runtime; no restart required.
"SqlException": {
// https://learn.microsoft.com/en-us/dotnet/api/system.data.sqlclient.sqlexception.number
"BadRequestSqlExceptionNumbers": [ 515, 547, 2601, 2627, 50000 ]
},
"AddedWho": "ReC.API",
"FakeProfileId": 2
}
}

View File

@@ -2,5 +2,7 @@
public class RecActionOptions
{
public int MaxConcurrentInvocations { get; set; } = 5;
}
public string AddedWho { get; set; } = null!;
public bool UseHttp1ForNtlm { get; set; } = false;
}

View File

@@ -1,9 +1,11 @@
using MediatR;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using ReC.Application.Common;
using ReC.Application.Common.Constants;
using ReC.Application.Common.Dto;
using ReC.Application.Common.Exceptions;
using ReC.Application.Common.Options;
using ReC.Application.Common.Procedures.InsertProcedure;
using ReC.Application.Results.Commands;
using ReC.Domain.Constants;
@@ -20,17 +22,18 @@ public record InvokeRecActionViewCommand : IRequest<bool>
}
public class InvokeRecActionViewCommandHandler(
IOptions<RecActionOptions> options,
ISender sender,
IHttpClientFactory clientFactory,
IConfiguration? config = null
) : IRequestHandler<InvokeRecActionViewCommand, bool>
{
private readonly RecActionOptions _options = options.Value;
public async Task<bool> Handle(InvokeRecActionViewCommand request, CancellationToken cancel)
{
var action = request.Action;
using var http = clientFactory.CreateClient(Http.ClientName);
if (action.RestType is not RestType restType)
throw new DataIntegrityException(
$"Rec action could not be invoked because the RestType value is null. " +
@@ -47,92 +50,118 @@ public class InvokeRecActionViewCommandHandler(
foreach (var header in action.Headers)
httpReq.Headers.Add(header.Key, header.Value);
switch (action.EndpointAuthType)
{
case EndpointAuthType.NoAuth:
break;
HttpClient? ntlmClient = null;
HttpClientHandler? ntlmHandler = null;
case EndpointAuthType.ApiKey:
if (action.EndpointAuthApiKey is string apiKey && action.EndpointAuthApiValue is string apiValue)
{
switch (action.EndpointAuthApiKeyAddTo)
try
{
switch (action.EndpointAuthType)
{
case EndpointAuthType.NoAuth:
break;
case EndpointAuthType.ApiKey:
if (action.EndpointAuthApiKey is string apiKey && action.EndpointAuthApiValue is string apiValue)
{
case ApiKeyLocation.Header:
httpReq.Headers.Add(apiKey, apiValue);
break;
case ApiKeyLocation.Query:
var uriBuilder = new UriBuilder(httpReq.RequestUri!);
var query = System.Web.HttpUtility.ParseQueryString(uriBuilder.Query);
query[apiKey] = apiValue;
uriBuilder.Query = query.ToString();
httpReq.RequestUri = uriBuilder.Uri;
break;
default:
throw new DataIntegrityException(
$"The API key location '{action.EndpointAuthApiKeyAddTo}' is not supported. " +
$"ProfileId: {action.ProfileId}, " +
$"Id: {action.Id}"
);
switch (action.EndpointAuthApiKeyAddTo)
{
case ApiKeyLocation.Header:
httpReq.Headers.Add(apiKey, apiValue);
break;
case ApiKeyLocation.Query:
var uriBuilder = new UriBuilder(httpReq.RequestUri!);
var query = System.Web.HttpUtility.ParseQueryString(uriBuilder.Query);
query[apiKey] = apiValue;
uriBuilder.Query = query.ToString();
httpReq.RequestUri = uriBuilder.Uri;
break;
default:
throw new DataIntegrityException(
$"The API key location '{action.EndpointAuthApiKeyAddTo}' is not supported. " +
$"ProfileId: {action.ProfileId}, " +
$"Id: {action.Id}"
);
}
}
}
break;
break;
case EndpointAuthType.BearerToken:
case EndpointAuthType.JwtBearer:
case EndpointAuthType.OAuth2:
if (action.EndpointAuthToken is string authToken)
httpReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
break;
case EndpointAuthType.BearerToken:
case EndpointAuthType.JwtBearer:
case EndpointAuthType.OAuth2:
if (action.EndpointAuthToken is string authToken)
httpReq.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
break;
case EndpointAuthType.BasicAuth:
if (action.EndpointAuthUsername is string authUsername && action.EndpointAuthPassword is string authPassword)
{
var basicAuth = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{authUsername}:{authPassword}"));
httpReq.Headers.Authorization = new AuthenticationHeaderValue("Basic", basicAuth);
}
break;
case EndpointAuthType.BasicAuth:
if (action.EndpointAuthUsername is string authUsername && action.EndpointAuthPassword is string authPassword)
{
var basicAuth = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{authUsername}:{authPassword}"));
httpReq.Headers.Authorization = new AuthenticationHeaderValue("Basic", basicAuth);
}
break;
case EndpointAuthType.NtlmAuth:
if (!string.IsNullOrWhiteSpace(action.EndpointAuthUsername))
{
var credentials = new NetworkCredential(
action.EndpointAuthUsername,
action.EndpointAuthPassword,
action.EndpointAuthDomain);
var credentialCache = new CredentialCache { { httpReq.RequestUri!, "NTLM", credentials } };
httpReq.Options.Set(new HttpRequestOptionsKey<CredentialCache>("Credentials"), credentialCache);
}
break;
case EndpointAuthType.NtlmAuth:
if (!string.IsNullOrWhiteSpace(action.EndpointAuthUsername))
{
if (_options.UseHttp1ForNtlm)
{
httpReq.Version = HttpVersion.Version11;
httpReq.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
}
var endpointAuthPassword = action.EndpointAuthPassword
#if DEBUG
?.Replace("%NTLM_PW%", config?.GetValue<string>("%NTLM_PW%"))
#endif
;
var credentials = new NetworkCredential(
action.EndpointAuthUsername,
endpointAuthPassword,
action.EndpointAuthDomain);
var credentialCache = new CredentialCache { { httpReq.RequestUri!, "NTLM", credentials } };
ntlmHandler = new HttpClientHandler
{
Credentials = credentialCache,
UseDefaultCredentials = false
};
ntlmClient = new HttpClient(ntlmHandler);
}
break;
case EndpointAuthType.DigestAuth:
case EndpointAuthType.OAuth1:
case EndpointAuthType.AwsSignature:
// These authentication methods require more complex implementations,
// often involving multi-step handshakes or specialized libraries.
// They are left as placeholders for future implementation.
default:
throw new NotImplementedException(
$"The authentication type '{action.EndpointAuthType}' is not supported yet. " +
$"ProfileId: {action.ProfileId}, " +
$"Id: {action.Id}"
);
case EndpointAuthType.DigestAuth:
case EndpointAuthType.OAuth1:
case EndpointAuthType.AwsSignature:
// These authentication methods require more complex implementations,
// often involving multi-step handshakes or specialized libraries.
// They are left as placeholders for future implementation.
default:
throw new NotImplementedException(
$"The authentication type '{action.EndpointAuthType}' is not supported yet. " +
$"ProfileId: {action.ProfileId}, " +
$"Id: {action.Id}"
);
}
using var http = ntlmClient ?? clientFactory.CreateClient(Http.ClientName);
using var response = await http.SendAsync(httpReq, cancel);
var resBody = await response.Content.ReadAsStringAsync(cancel);
var resHeaders = response.Headers.ToDictionary();
var statusCode = (short)response.StatusCode;
await sender.ExecuteInsertProcedure(new InsertResultProcedure()
{
StatusId = statusCode,
ActionId = action.Id,
Header = JsonSerializer.Serialize(resHeaders, options: new() { WriteIndented = false }),
Body = resBody
}, _options.AddedWho, cancel);
return response.IsSuccessStatusCode;
}
using var response = await http.SendAsync(httpReq, cancel);
var resBody = await response.Content.ReadAsStringAsync(cancel);
var resHeaders = response.Headers.ToDictionary();
var statusCode = (short)response.StatusCode;
await sender.ExecuteInsertProcedure(new InsertResultProcedure()
finally
{
StatusId = statusCode,
ActionId = action.Id,
Header = JsonSerializer.Serialize(resHeaders, options: new() { WriteIndented = false }),
Body = resBody
}, config?["AddedWho"], cancel);
return response.IsSuccessStatusCode;
ntlmHandler?.Dispose();
}
}
private static HttpRequestMessage CreateHttpRequestMessage(RestType restType, string? endpointUri)