Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filter SO dialog search using fnr/dnr #333

Merged
merged 16 commits into from
Jan 22, 2024
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System.Globalization;
using System.Text.RegularExpressions;

namespace Digdir.Domain.Dialogporten.Application.Common.Numbers;

internal static partial class EndUserIdentifier
{
private static readonly int[] NorwegianIdentifierNumberWeights1 = [3, 7, 6, 1, 8, 9, 4, 5, 2, 1];
private static readonly int[] NorwegianIdentifierNumberWeights2 = [5, 4, 3, 2, 7, 6, 5, 4, 3, 2, 1];

[GeneratedRegex(@"urn:altinn:([\w-]{5,20}):([\w-]{4,20})::([\w-]{5,36})", RegexOptions.None, matchTimeoutMilliseconds: 100)]
private static partial Regex IdRegex();

public static bool IsValid(string identifier)
{
var match = IdRegex().Match(identifier);

var namespacePart = match.Groups[1].Value;
var type = match.Groups[2].Value;
var value = match.Groups[3].Value;

return namespacePart switch
{
"person" => ValidatePerson(type, value),
"systemuser" => ValidateSystemUser(type, value),
_ => false
};
}

private static bool ValidatePerson(string type, string value)
{
return type switch
{
"identifier-no" => ValidateNorwegianIdentifier(value),
_ => false,
};
}

private static bool ValidateSystemUser(string type, string value)
{
return type == "uuid" && Guid.TryParse(value, out _);
}

private static bool ValidateNorwegianIdentifier(ReadOnlySpan<char> norwegianIdentifier)
{
return norwegianIdentifier.Length == 11
&& Mod11.TryCalculateControlDigit(norwegianIdentifier[..9], NorwegianIdentifierNumberWeights1, out var control1)
&& Mod11.TryCalculateControlDigit(norwegianIdentifier[..10], NorwegianIdentifierNumberWeights2, out var control2)
&& control1 == int.Parse(norwegianIdentifier[9..10], CultureInfo.InvariantCulture)
&& control2 == int.Parse(norwegianIdentifier[10..11], CultureInfo.InvariantCulture);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ public Task<DialogDetailsAuthorizationResult> GetDialogDetailsAuthorization(
public Task<DialogSearchAuthorizationResult> GetAuthorizedResourcesForSearch(
List<string> constraintParties,
List<string> constraintServiceResources,
string? endUserId = null,
CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public async Task<GetDialogActivityResult> Handle(GetDialogActivityQuery request

var authorizationResult = await _altinnAuthorization.GetDialogDetailsAuthorization(
dialog,
cancellationToken);
cancellationToken: cancellationToken);

// If we cannot read the dialog at all, we don't allow access to any of the activity history
if (!authorizationResult.HasReadAccessToMainResource())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ public async Task<SearchDialogResult> Handle(SearchDialogQuery request, Cancella
var authorizedResources = await _altinnAuthorization.GetAuthorizedResourcesForSearch(
request.Party ?? new List<string>(),
request.ServiceResource ?? new List<string>(),
cancellationToken);
cancellationToken: cancellationToken);

if (authorizedResources.HasNoAuthorizations)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public sealed class SearchDialogDto
public string Org { get; set; } = null!;
public string ServiceResource { get; set; } = null!;
public string Party { get; set; } = null!;
public string? EndUserId { get; set; } = null!;
public int? Progress { get; set; }
public string? ExtendedStatus { get; set; }
public DateTimeOffset CreatedAt { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
using Digdir.Domain.Dialogporten.Application.Common;
using Digdir.Domain.Dialogporten.Application.Common.Extensions;
using Digdir.Domain.Dialogporten.Application.Common.Extensions.Enumerables;
using Digdir.Domain.Dialogporten.Application.Common.Numbers;
using Digdir.Domain.Dialogporten.Application.Common.Pagination;
using Digdir.Domain.Dialogporten.Application.Common.Pagination.OrderOption;
using Digdir.Domain.Dialogporten.Application.Common.ReturnTypes;
using Digdir.Domain.Dialogporten.Application.Externals;
using Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization;
using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities;
using Digdir.Domain.Dialogporten.Domain.Localizations;
using MediatR;
Expand All @@ -29,6 +31,11 @@ public sealed class SearchDialogQuery : SortablePaginationParameter<SearchDialog
/// </summary>
public List<string>? Party { get; init; }

/// <summary>
/// Filter by end user id
/// </summary>
public string? EndUserId { get; init; }

/// <summary>
/// Filter by one or more extended statuses
/// </summary>
Expand Down Expand Up @@ -116,25 +123,31 @@ internal sealed class SearchDialogQueryHandler : IRequestHandler<SearchDialogQue
private readonly IDialogDbContext _db;
private readonly IMapper _mapper;
private readonly IUserService _userService;
private readonly IAltinnAuthorization _altinnAuthorization;

public SearchDialogQueryHandler(
IDialogDbContext db,
IMapper mapper,
IUserService userService)
IUserService userService,
IAltinnAuthorization altinnAuthorization)
{
_db = db ?? throw new ArgumentNullException(nameof(db));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
_userService = userService ?? throw new ArgumentNullException(nameof(userService));
_altinnAuthorization = altinnAuthorization;
}

public async Task<SearchDialogResult> Handle(SearchDialogQuery request, CancellationToken cancellationToken)
{
var resourceIds = await _userService.GetCurrentUserResourceIds(cancellationToken);
var searchExpression = Expressions.LocalizedSearchExpression(request.Search, request.SearchCultureCode);
return await _db.Dialogs
.WhereIf(!request.ServiceResource.IsNullOrEmpty(), x => request.ServiceResource!.Contains(x.ServiceResource))

var query = _db.Dialogs
.WhereIf(!request.ServiceResource.IsNullOrEmpty(),
x => request.ServiceResource!.Contains(x.ServiceResource))
.WhereIf(!request.Party.IsNullOrEmpty(), x => request.Party!.Contains(x.Party))
.WhereIf(!request.ExtendedStatus.IsNullOrEmpty(), x => x.ExtendedStatus != null && request.ExtendedStatus!.Contains(x.ExtendedStatus))
.WhereIf(!request.ExtendedStatus.IsNullOrEmpty(),
x => x.ExtendedStatus != null && request.ExtendedStatus!.Contains(x.ExtendedStatus))
.WhereIf(!string.IsNullOrWhiteSpace(request.ExternalReference),
x => x.ExternalReference != null && request.ExternalReference == x.ExternalReference)
.WhereIf(!request.Status.IsNullOrEmpty(), x => request.Status!.Contains(x.StatusId))
Expand All @@ -150,7 +163,19 @@ public async Task<SearchDialogResult> Handle(SearchDialogQuery request, Cancella
x.Content.Any(x => x.Value.Localizations.AsQueryable().Any(searchExpression)) ||
x.SearchTags.Any(x => EF.Functions.ILike(x.Value, request.Search!))
)
.Where(x => resourceIds.Contains(x.ServiceResource))
.Where(x => resourceIds.Contains(x.ServiceResource));

if (request.EndUserId is not null)
{
var authorizedResources = await _altinnAuthorization.GetAuthorizedResourcesForSearch(
request.Party ?? new List<string>(),
request.ServiceResource ?? new List<string>(),
request.EndUserId,
cancellationToken);
query = query.WhereUserIsAuthorizedFor(authorizedResources);
}

return await query
.ProjectTo<SearchDialogDto>(_mapper.ConfigurationProvider)
.ToPaginatedListAsync(request, cancellationToken: cancellationToken);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Digdir.Domain.Dialogporten.Application.Common.Pagination;
using Digdir.Domain.Dialogporten.Application.Common.Extensions.Enumerables;
using Digdir.Domain.Dialogporten.Application.Common.Numbers;
using Digdir.Domain.Dialogporten.Application.Common.Pagination;
using Digdir.Domain.Dialogporten.Domain.Localizations;
using FluentValidation;

Expand All @@ -17,6 +19,13 @@ public SearchDialogQueryValidator()
.Must(x => x is null || Localization.IsValidCultureCode(x))
.WithMessage("'{PropertyName}' must be a valid culture code.");

RuleFor(x => x)
.Must(x => EndUserIdentifier.IsValid(x.EndUserId!))
.WithMessage($"'{nameof(SearchDialogQuery.EndUserId)}' must be a valid end user identifier. It should match the format 'urn:altinn:person:identifier-no::{{norwegian f-nr/d-nr}} or 'urn:altinn:systemuser:{{uuid}}\"")
.Must(x => !x.ServiceResource.IsNullOrEmpty() || !x.Party.IsNullOrEmpty())
.WithMessage($"Either '{nameof(SearchDialogQuery.ServiceResource)}' or '{nameof(SearchDialogQuery.Party)}' must be specified if '{nameof(SearchDialogQuery.EndUserId)}' is provided.")
.When(x => x.EndUserId is not null);

RuleFor(x => x.ServiceResource!.Count)
.LessThanOrEqualTo(20)
.When(x => x.ServiceResource is not null);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Net;
using System.Security.Claims;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
Expand All @@ -14,6 +15,8 @@ namespace Digdir.Domain.Dialogporten.Infrastructure.Altinn.Authorization;

internal sealed class AltinnAuthorizationClient : IAltinnAuthorization
{
private const string AttributePidClaim = "urn:altinn:ssn";

private readonly HttpClient _httpClient;
private readonly IUser _user;
private readonly IDialogDbContext _db;
Expand All @@ -35,7 +38,7 @@ public async Task<DialogDetailsAuthorizationResult> GetDialogDetailsAuthorizatio
CancellationToken cancellationToken = default) =>
await PerformDialogDetailsAuthorization(new DialogDetailsAuthorizationRequest
{
ClaimsPrincipal = _user.GetPrincipal(),
Claims = _user.GetPrincipal().Claims.ToList(),
ServiceResource = dialogEntity.ServiceResource,
DialogId = dialogEntity.Id,
Party = dialogEntity.Party,
Expand All @@ -45,13 +48,17 @@ await PerformDialogDetailsAuthorization(new DialogDetailsAuthorizationRequest
public async Task<DialogSearchAuthorizationResult> GetAuthorizedResourcesForSearch(
List<string> constraintParties,
List<string> serviceResources,
CancellationToken cancellationToken = default) =>
await PerformNonScalableDialogSearchAuthorization(new DialogSearchAuthorizationRequest
string? endUserId,
CancellationToken cancellationToken = default)
{
var claims = GetOrCreateClaimsBasedOnEndUserId(endUserId);
return await PerformNonScalableDialogSearchAuthorization(new DialogSearchAuthorizationRequest
{
ClaimsPrincipal = _user.GetPrincipal(),
Claims = claims,
ConstraintParties = constraintParties,
ConstraintServiceResources = serviceResources
}, cancellationToken);
}

private async Task<DialogSearchAuthorizationResult> PerformNonScalableDialogSearchAuthorization(DialogSearchAuthorizationRequest request, CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -97,6 +104,23 @@ private async Task<DialogDetailsAuthorizationResult> PerformDialogDetailsAuthori
return DecisionRequestHelper.CreateDialogDetailsResponse(request.AltinnActions, xamlJsonResponse);
}

private List<Claim> GetOrCreateClaimsBasedOnEndUserId(string? endUserId)
{
List<Claim> claims = new();
if (endUserId is not null)
{
claims.Add(new Claim(AttributePidClaim, ExtractEndUserIdNumber(endUserId)!));
knuhau marked this conversation as resolved.
Show resolved Hide resolved
}
else
{
claims.AddRange(_user.GetPrincipal().Claims);
}
return claims;
}

private static string ExtractEndUserIdNumber(string endUserId) =>
endUserId.Split("::").LastOrDefault() ?? string.Empty;

private static readonly JsonSerializerOptions _serializerOptions = new()
{
PropertyNameCaseInsensitive = true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ internal static class DecisionRequestHelper

public static XacmlJsonRequestRoot CreateDialogDetailsRequest(DialogDetailsAuthorizationRequest request)
{
var accessSubject = CreateAccessSubjectCategory(request.ClaimsPrincipal.Claims);
var accessSubject = CreateAccessSubjectCategory(request.Claims);
var actions = CreateActionCategories(request.AltinnActions, out var actionIdByName);
var resources = CreateResourceCategories(request.ServiceResource, request.DialogId, request.Party, request.AltinnActions, out var resourceIdByName);

Expand Down Expand Up @@ -223,7 +223,7 @@ public static XacmlJsonRequestRoot CreateDialogSearchRequest(DialogSearchAuthori
new (Constants.ReadAction, Constants.MainResource)
};

var accessSubject = CreateAccessSubjectCategory(request.ClaimsPrincipal.Claims);
var accessSubject = CreateAccessSubjectCategory(request.Claims);
var actions = CreateActionCategories(requestActions, out _);
var resources = CreateResourceCategoriesForSearch(request.ConstraintServiceResources, request.ConstraintParties);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Digdir.Domain.Dialogporten.Infrastructure.Altinn.Authorization;

public sealed class DialogDetailsAuthorizationRequest
{
public required ClaimsPrincipal ClaimsPrincipal { get; init; }
public required List<Claim> Claims { get; init; }
public required string ServiceResource { get; init; }
public required Guid DialogId { get; init; }
public required string Party { get; init; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace Digdir.Domain.Dialogporten.Infrastructure.Altinn.Authorization;

public sealed class DialogSearchAuthorizationRequest
{
public required ClaimsPrincipal ClaimsPrincipal { get; init; }
public required List<Claim> Claims { get; init; }
public List<string> ConstraintParties { get; set; } = new();
public List<string> ConstraintServiceResources { get; set; } = new();
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public Task<DialogDetailsAuthorizationResult> GetDialogDetailsAuthorization(Dial
// Just allow everything
Task.FromResult(new DialogDetailsAuthorizationResult { AuthorizedAltinnActions = dialogEntity.GetAltinnActions() });

public async Task<DialogSearchAuthorizationResult> GetAuthorizedResourcesForSearch(List<string> constraintParties, List<string> serviceResources,
public async Task<DialogSearchAuthorizationResult> GetAuthorizedResourcesForSearch(List<string> constraintParties, List<string> serviceResources, string? endUserId,
CancellationToken cancellationToken = default)
{
// Allow all resources for all parties
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ private static DialogDetailsAuthorizationRequest CreateDialogDetailsAuthorizatio
allClaims.AddRange(principalClaims);
return new DialogDetailsAuthorizationRequest
{
ClaimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(allClaims, "test")),
Claims = allClaims,
ServiceResource = "urn:altinn:resource:some-service",
DialogId = Guid.NewGuid(),

Expand Down