Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 21 additions & 0 deletions Dan.Common/Models/CustomSubjectRequirement.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace Dan.Common.Models;

/// <summary>
/// Requirement for subjects that differ from organisation number or Norwegian SSN
/// </summary>
public class CustomSubjectRequirement : Requirement
{
/// <summary>
/// Regex used to validate custom subject. Defaults to \w+ for any letters and digits
/// </summary>
[DataMember(Name = "subjectRegex")]
[Required]
public string SubjectRegex { get; set; } = @"\w+";

/// <summary>
/// Describes the regex in clear text
/// </summary>
[DataMember(Name = "subjectRegexDescription")]
[Required]
public string SubjectRegexDescription { get; set; } = "Any string";
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using AwesomeAssertions;
using Dan.Common.Enums;
using Dan.Common.Models;
using Dan.Core.Exceptions;
Expand All @@ -9,7 +10,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace Dan.Core.UnitTest
namespace Dan.Core.UnitTest.Services
{
[TestClass]
[ExcludeFromCodeCoverage]
Expand Down Expand Up @@ -140,7 +141,43 @@ await Assert.ThrowsAsync<InvalidSubjectException>(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<bool>._))
.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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -10,7 +11,7 @@

using ConsentRequirement = Nadobe.Common.Models.ConsentRequirement;

namespace Dan.Core.UnitTest
namespace Dan.Core.UnitTest.Services
{
[TestClass]
public class RequirementValidationServiceTest
Expand Down Expand Up @@ -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<string, List<Requirement>>
{
["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<string, List<Requirement>>
{
["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<string, List<Requirement>>
{
["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<string, List<Requirement>>
{
["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<string> owners, List<string> subjects, List<string> requestors)
{
Expand Down
48 changes: 31 additions & 17 deletions Dan.Core/Services/AuthorizationRequestValidatorService.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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)
{
Expand Down Expand Up @@ -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
/// </summary>
/// <exception cref="InvalidSubjectException"></exception>
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;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private void ValidateLegalBasisWellFormed()
Expand Down
33 changes: 32 additions & 1 deletion Dan.Core/Services/RequirementValidationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
using Dan.Common.Interfaces;

namespace Dan.Core.Services;

Expand Down Expand Up @@ -107,6 +106,7 @@ private async Task<bool> 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
};

Expand Down Expand Up @@ -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;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private async Task<PartyTypeConstraint> GetPartyType(string? identifier)
{
if (identifier == null) return PartyTypeConstraint.Foreign;
Expand Down
1 change: 1 addition & 0 deletions Dan.PluginTest/Config/PluginConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Loading
Loading