diff --git a/Dan.Common/Models/CustomSubjectRequirement.cs b/Dan.Common/Models/CustomSubjectRequirement.cs
new file mode 100644
index 0000000..b08fbaf
--- /dev/null
+++ b/Dan.Common/Models/CustomSubjectRequirement.cs
@@ -0,0 +1,21 @@
+namespace Dan.Common.Models;
+
+///
+/// Requirement for subjects that differ from organisation number or Norwegian SSN
+///
+public class CustomSubjectRequirement : Requirement
+{
+ ///
+ /// Regex used to validate custom subject. Defaults to \w+ for any letters and digits
+ ///
+ [DataMember(Name = "subjectRegex")]
+ [Required]
+ public string SubjectRegex { get; set; } = @"\w+";
+
+ ///
+ /// Describes the regex in clear text
+ ///
+ [DataMember(Name = "subjectRegexDescription")]
+ [Required]
+ public string SubjectRegexDescription { get; set; } = "Any string";
+}
\ No newline at end of file
diff --git a/Dan.Core.UnitTest/AuthorizationRequestValidatorServiceTest.cs b/Dan.Core.UnitTest/Services/AuthorizationRequestValidatorServiceTest.cs
similarity index 95%
rename from Dan.Core.UnitTest/AuthorizationRequestValidatorServiceTest.cs
rename to Dan.Core.UnitTest/Services/AuthorizationRequestValidatorServiceTest.cs
index 6c758fa..df08997 100644
--- a/Dan.Core.UnitTest/AuthorizationRequestValidatorServiceTest.cs
+++ b/Dan.Core.UnitTest/Services/AuthorizationRequestValidatorServiceTest.cs
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
+using AwesomeAssertions;
using Dan.Common.Enums;
using Dan.Common.Models;
using Dan.Core.Exceptions;
@@ -9,7 +10,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
-namespace Dan.Core.UnitTest
+namespace Dan.Core.UnitTest.Services
{
[TestClass]
[ExcludeFromCodeCoverage]
@@ -140,7 +141,43 @@ await Assert.ThrowsAsync(async () =>
await arvs.Validate(GetAuthorizationRequest(subject: "123456789"));
});
}
-
+
+ [TestMethod]
+ [DataRow("03065001488")]
+ [DataRow("12345678")]
+ public async Task ValidateTest_CustomSubjectRequirement_ShouldSetParty(string subject)
+ {
+ // Setup
+ var ae = GetAvailableEvidenceCodes().Take(1).ToList();
+ ae.First().AuthorizationRequirements =
+ [
+ new CustomSubjectRequirement
+ {
+ SubjectRegex = @"^\d{1,8}$", // digits only 1-8 characters long,
+ SubjectRegexDescription = "Description",
+ RequirementType = "CustomSubjectRequirement"
+ }
+ ];
+
+ A.CallTo(() => _mockAvailableEvidenceCodesService.GetAvailableEvidenceCodes(A._))
+ .Returns(Task.FromResult(ae));
+
+ var arvs = new AuthorizationRequestValidatorService(
+ _loggerFactory,
+ _mockEntityRegistryService,
+ _mockAvailableEvidenceCodesService,
+ _mockRequirementValidatorService,
+ _mockRequestContextService);
+
+ var request = GetAuthorizationRequest(subject: subject);
+
+ // Act
+ var action = async () => await arvs.Validate(request);
+
+ // Assert
+ await action.Should().NotThrowAsync();
+ request.SubjectParty.Id.Should().Be(subject);
+ }
[TestMethod]
public async Task ValidateTestFailureWithInvalidRequestorOrgNo()
diff --git a/Dan.Core.UnitTest/RequirementValidationServiceTest.cs b/Dan.Core.UnitTest/Services/RequirementValidationServiceTest.cs
similarity index 88%
rename from Dan.Core.UnitTest/RequirementValidationServiceTest.cs
rename to Dan.Core.UnitTest/Services/RequirementValidationServiceTest.cs
index 7384a16..ce9dfb5 100644
--- a/Dan.Core.UnitTest/RequirementValidationServiceTest.cs
+++ b/Dan.Core.UnitTest/Services/RequirementValidationServiceTest.cs
@@ -1,4 +1,5 @@
-using Dan.Common.Enums;
+using AwesomeAssertions;
+using Dan.Common.Enums;
using Dan.Common.Models;
using Dan.Core.Exceptions;
using Dan.Core.Helpers;
@@ -10,7 +11,7 @@
using ConsentRequirement = Nadobe.Common.Models.ConsentRequirement;
-namespace Dan.Core.UnitTest
+namespace Dan.Core.UnitTest.Services
{
[TestClass]
public class RequirementValidationServiceTest
@@ -772,6 +773,140 @@ public async Task ReferenceTest_Failed_ConsentReferenceIsNull()
Assert.IsTrue(errorList.Count == 1);
Assert.IsTrue(errorList[0].Contains("The request requires a valid consent reference but none is provided"));
}
+
+ [TestMethod]
+ [DataRow("12345678", null)] // Custom subject
+ [DataRow("03065001488", PartyParser.NorwegianIcd)] // Already found subject in pre step
+ [DataRow("03065001488", PartyParser.SchemeIso6523ActorIdUpis)] // Already found subject in pre step
+ [DataRow("03065001488", PartyParser.SchemeNorwegianSsn)] // Already found subject in pre step
+ public async Task CustomSubjectTest_Success(string subject, string scheme)
+ {
+ // Arrange
+ var authRequest = GetAuthRequest(subject, "requestor");
+ authRequest.SubjectParty = new Party
+ {
+ Id = "12345678",
+ Scheme = scheme
+ };
+
+ var req = new Dictionary>
+ {
+ ["ec1"] =
+ [
+ new CustomSubjectRequirement()
+ {
+ SubjectRegex = @"^\d{1,8}$"
+ }
+ ]
+ };
+
+ var svc = new RequirementValidationService(_mockAltinnServiceOwnerApiService, _mockEntityRegistryService, _mockRequestContextService);
+
+ // Act
+ var errorList = await svc.ValidateRequirements(req, authRequest);
+
+ // Assert
+ errorList.Should().BeEmpty();
+ }
+
+ [TestMethod]
+ public async Task CustomSubjectTest_MismatchFormat_ShouldThrow()
+ {
+ // Arrange
+ var authRequest = GetAuthRequest("123456789", "requestor");
+ authRequest.SubjectParty = new Party
+ {
+ Id = "12345678",
+ Scheme = null
+ };
+
+ var req = new Dictionary>
+ {
+ ["ec1"] =
+ [
+ new CustomSubjectRequirement()
+ {
+ SubjectRegex = @"^\d{1,8}$",
+ SubjectRegexDescription = "Description"
+ }
+ ]
+ };
+
+ var svc = new RequirementValidationService(_mockAltinnServiceOwnerApiService, _mockEntityRegistryService, _mockRequestContextService);
+
+ // Act
+ var errorList = await svc.ValidateRequirements(req, authRequest);
+
+ // Assert
+ errorList.Should().HaveCount(1);
+ errorList.Should().Contain("ec1: Subject does not match custom subject format: Description");
+ }
+
+ [TestMethod]
+ public async Task CustomSubjectTest_MissingSubject_ShouldThrow()
+ {
+ // Arrange
+ var authRequest = GetAuthRequest("", "requestor");
+ authRequest.SubjectParty = new Party
+ {
+ Id = "",
+ Scheme = null
+ };
+
+ var req = new Dictionary>
+ {
+ ["ec1"] =
+ [
+ new CustomSubjectRequirement()
+ {
+ SubjectRegex = @"^\d{1,8}$",
+ SubjectRegexDescription = "Description"
+ }
+ ]
+ };
+
+ var svc = new RequirementValidationService(_mockAltinnServiceOwnerApiService, _mockEntityRegistryService, _mockRequestContextService);
+
+ // Act
+ var errorList = await svc.ValidateRequirements(req, authRequest);
+
+ // Assert
+ errorList.Should().HaveCount(1);
+ errorList.Should().Contain("ec1: Subject is missing");
+ }
+
+ [TestMethod]
+ public async Task CustomSubjectTest_MissingRegex_ShouldThrow()
+ {
+ // Arrange
+ var authRequest = GetAuthRequest("12345", "requestor");
+ authRequest.SubjectParty = new Party
+ {
+ Id = "12345",
+ Scheme = null
+ };
+
+ var req = new Dictionary>
+ {
+ ["ec1"] =
+ [
+ new CustomSubjectRequirement()
+ {
+ SubjectRegex = "",
+ SubjectRegexDescription = "Description"
+ }
+ ]
+ };
+
+ var svc = new RequirementValidationService(_mockAltinnServiceOwnerApiService, _mockEntityRegistryService, _mockRequestContextService);
+
+ // Act
+ var errorList = await svc.ValidateRequirements(req, authRequest);
+
+ // Assert
+ errorList.Should().HaveCount(1);
+ errorList.Should().Contain("ec1: Missing custom subject format regex");
+ }
private Requirement GetWhiteListRequirement(List owners, List subjects, List requestors)
{
diff --git a/Dan.Core/Services/AuthorizationRequestValidatorService.cs b/Dan.Core/Services/AuthorizationRequestValidatorService.cs
index d983307..8c443c9 100644
--- a/Dan.Core/Services/AuthorizationRequestValidatorService.cs
+++ b/Dan.Core/Services/AuthorizationRequestValidatorService.cs
@@ -1,6 +1,5 @@
using Dan.Common;
using Dan.Common.Enums;
-using Dan.Common.Interfaces;
using Dan.Common.Models;
using Dan.Core.Config;
using Dan.Core.Exceptions;
@@ -57,9 +56,22 @@ public async Task Validate(AuthorizationRequest? authorizationRequest)
_authRequest = authorizationRequest ?? throw new InvalidAuthorizationRequestException();
_registeredEvidenceCodes = await _availableEvidenceCodesService.GetAvailableEvidenceCodes();
_evidenceCodesFromRequest = _registeredEvidenceCodes.Where(r => _authRequest.EvidenceRequests.Any(x => x.EvidenceCodeName == r.EvidenceCodeName)).ToList();
+
+ var requirements = _evidenceCodesFromRequest.ToDictionary(es => es.EvidenceCodeName, es => es.AuthorizationRequirements);
+ if (authorizationRequest.FromEvidenceHarvester)
+ {
+ foreach (var requirement in requirements.Values)
+ {
+ requirement.RemoveAll(x => x.RequiredOnEvidenceHarvester == false);
+ }
+ }
+ var customSubject = requirements.Values.SelectMany(x => x).Any(req =>
+ req.RequirementType is not null &&
+ req.RequirementType.Equals("CustomSubjectRequirement", StringComparison.InvariantCultureIgnoreCase));
+
ValidateAndPopulateRequestor();
- ValidateAndPopulateSubject();
+ ValidateAndPopulateSubject(customSubject);
ValidateLegalBasisWellFormed();
ValidateEvidenceRequestWellFormed();
ValidateEvidenceCodesAreAvailableForServiceContext();
@@ -78,15 +90,6 @@ public async Task Validate(AuthorizationRequest? authorizationRequest)
ValidateLanguageCodes();
- var requirements = _evidenceCodesFromRequest.ToDictionary(es => es.EvidenceCodeName, es => es.AuthorizationRequirements);
- if (authorizationRequest.FromEvidenceHarvester)
- {
- foreach (var requirement in requirements.Values)
- {
- requirement.RemoveAll(x => x.RequiredOnEvidenceHarvester == false);
- }
- }
-
var authorizationErrors = await _requirementValidationService.ValidateRequirements(requirements, _authRequest);
if (authorizationErrors.Count > 0)
{
@@ -185,22 +188,33 @@ private void ValidateAndPopulateRequestor()
/// Uses PartyParser on the supplied subject, and populates SubjectParty with it. Overwrites Requestor with norwegian identifier if applicable, else set to null
///
///
- private void ValidateAndPopulateSubject()
+ private void ValidateAndPopulateSubject(bool customSubject)
{
if (_authRequest.Subject == null)
{
return;
}
-
- Party? party = PartyParser.GetPartyFromIdentifier(_authRequest.Subject, out string? error);
- if (party == null)
+
+ var party = PartyParser.GetPartyFromIdentifier(_authRequest.Subject, out var error);
+ if (party == null && !customSubject)
{
throw new InvalidSubjectException($"Invalid subject supplied: {error}");
}
- _authRequest.Subject = party.NorwegianOrganizationNumber ?? party.NorwegianSocialSecurityNumber;
- _authRequest.SubjectParty = party;
+ if (party != null)
+ {
+ _authRequest.Subject = party.NorwegianOrganizationNumber ?? party.NorwegianSocialSecurityNumber;
+ _authRequest.SubjectParty = party;
+ return;
+ }
+
+ // Custom Subject Validation will be done later in RequirementValidationService
+ var customParty = new Party
+ {
+ Id = _authRequest.Subject
+ };
+ _authRequest.SubjectParty = customParty;
}
private void ValidateLegalBasisWellFormed()
diff --git a/Dan.Core/Services/RequirementValidationService.cs b/Dan.Core/Services/RequirementValidationService.cs
index 54344c3..e7db1f9 100644
--- a/Dan.Core/Services/RequirementValidationService.cs
+++ b/Dan.Core/Services/RequirementValidationService.cs
@@ -7,7 +7,6 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
-using Dan.Common.Interfaces;
namespace Dan.Core.Services;
@@ -107,6 +106,7 @@ private async Task ValidateSingleRequirement(Requirement req, string evide
AccreditationPartyRequirement r => ValidateAccreditationPartyRequirement(r, _authRequest.Subject, _authRequest.Requestor, _owner, evidenceCodeName),
ReferenceRequirement r => ValidateReferenceRequirement(r, _authRequest, evidenceCodeName),
ProvideOwnTokenRequirement r => ValidateProvideOwnTokenRequirement(r, evidenceCodeName),
+ CustomSubjectRequirement r => ValidateCustomSubjectRequirement(r, _authRequest, evidenceCodeName),
_ => false
};
@@ -569,6 +569,37 @@ private bool ValidateProvideOwnTokenRequirement(Requirement req, string evidence
return false;
}
+ private bool ValidateCustomSubjectRequirement(CustomSubjectRequirement req, AuthorizationRequest authRequest, string evidenceCodeName)
+ {
+
+ if (authRequest.SubjectParty.Scheme is PartyParser.SchemeIso6523ActorIdUpis or
+ PartyParser.SchemeNorwegianSsn or
+ PartyParser.NorwegianIcd)
+ {
+ // Subject already parsed as Organisation number or norwegian ssn
+ return true;
+ }
+ if (string.IsNullOrWhiteSpace(authRequest.Subject))
+ {
+ AddError(req, $"Subject is missing", evidenceCodeName);
+ return false;
+ }
+
+ if (string.IsNullOrEmpty(req.SubjectRegex))
+ {
+ AddError(req, $"Missing custom subject format regex", evidenceCodeName);
+ return false;
+ }
+
+ var regexMatch = Regex.Match(authRequest.Subject, req.SubjectRegex, RegexOptions.None, TimeSpan.FromSeconds(1));
+ if (regexMatch.Success)
+ {
+ return true;
+ }
+ AddError(req, $"Subject does not match custom subject format: {req.SubjectRegexDescription}", evidenceCodeName);
+ return false;
+ }
+
private async Task GetPartyType(string? identifier)
{
if (identifier == null) return PartyTypeConstraint.Foreign;
diff --git a/Dan.PluginTest/Config/PluginConstants.cs b/Dan.PluginTest/Config/PluginConstants.cs
index 5abb96f..aa55968 100644
--- a/Dan.PluginTest/Config/PluginConstants.cs
+++ b/Dan.PluginTest/Config/PluginConstants.cs
@@ -14,4 +14,5 @@ public static class PluginConstants
public const string PluginForward = "PluginForward";
public const string PluginSettingsTest = "PluginSettingsTest";
public const string PluginGenericTest = "PluginGenericTest";
+ public const string PluginCustomSubjectTest = "PluginCustomSubjectTest";
}
\ No newline at end of file
diff --git a/Dan.PluginTest/Metadata.cs b/Dan.PluginTest/Metadata.cs
index d5b5afa..7acf233 100644
--- a/Dan.PluginTest/Metadata.cs
+++ b/Dan.PluginTest/Metadata.cs
@@ -109,6 +109,29 @@ public List GetEvidenceCodes()
}
]
},
+ new EvidenceCode
+ {
+ EvidenceCodeName = PluginConstants.PluginCustomSubjectTest,
+ EvidenceSource = PluginConstants.Source,
+ BelongsToServiceContexts = _serviceContexts,
+ Values =
+ [
+ new EvidenceValue
+ {
+ EvidenceValueName = "default",
+ ValueType = EvidenceValueType.JsonSchema,
+ JsonSchemaDefintion = EvidenceValue.SchemaFromObject()
+ }
+ ],
+ AuthorizationRequirements =
+ [
+ new CustomSubjectRequirement
+ {
+ SubjectRegex = @"^\d{1,8}$",
+ SubjectRegexDescription = "A number 1-8 characters long"
+ }
+ ]
+ },
];
}
}
\ No newline at end of file
diff --git a/Dan.PluginTest/Plugin.cs b/Dan.PluginTest/Plugin.cs
index 5f23a8c..787819c 100644
--- a/Dan.PluginTest/Plugin.cs
+++ b/Dan.PluginTest/Plugin.cs
@@ -119,6 +119,31 @@ public async Task PluginSettingsTest(
() => EvidenceValuesPluginSettings(evidenceHarvesterRequest));
}
+ [Function(PluginConstants.PluginCustomSubjectTest)]
+ public async Task PluginCustomSubjectTest(
+ [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequestData req,
+ FunctionContext context)
+ {
+ EvidenceHarvesterRequest? evidenceHarvesterRequest;
+ try
+ {
+ evidenceHarvesterRequest = await req.ReadFromJsonAsync();
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e,
+ "Exception while attempting to parse request into EvidenceHarvesterRequest: {exceptionType}: {exceptionMessage}",
+ e.GetType().Name, e.Message);
+ throw new EvidenceSourcePermanentClientException(PluginConstants.ErrorInvalidInput,
+ "Unable to parse request", e);
+ }
+
+ var unit = await ccrClientService.IsPublic("923609016", "local");
+
+ return await EvidenceSourceResponse.CreateResponse(req,
+ () => GetEvidenceValuesDatasetOne(evidenceHarvesterRequest));
+ }
+
[Function(PluginConstants.PluginGenericTest)]
public async Task PluginGenericTest(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequestData req,