Skip to content

Commit

Permalink
Add user type validation middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
elsand committed Apr 15, 2024
1 parent 6583f54 commit e2a3517
Show file tree
Hide file tree
Showing 10 changed files with 73 additions and 62 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ namespace Digdir.Domain.Dialogporten.Application.Common;

public interface IUserNameRegistry
{
bool TryGetCurrentUserExternalId([NotNullWhen(true)] out string? userExternalId);
Task<UserInformation?> GetUserInformation(CancellationToken cancellationToken);
string GetCurrentUserExternalId();
Task<UserInformation> GetCurrentUserInformation(CancellationToken cancellationToken);
}

public record UserInformation(string UserPid, string? UserName);
Expand All @@ -27,22 +27,18 @@ public UserNameRegistry(IUser user, INameRegistry nameRegistry, IOrganizationReg
_nameRegistry = nameRegistry ?? throw new ArgumentNullException(nameof(nameRegistry));
_organizationRegistry = organizationRegistry ?? throw new ArgumentNullException(nameof(organizationRegistry));
}

public bool TryGetCurrentUserExternalId([NotNullWhen(true)] out string? userExternalId)
public string GetCurrentUserExternalId()
{
if (_user.TryGetPid(out userExternalId)) return true;
if (_user.TryGetLegacySystemUserId(out userExternalId)) return true;
if (_user.TryGetOrgNumber(out userExternalId)) return true;
return false;
if (_user.TryGetPid(out var userId)) return userId;
if (_user.TryGetLegacySystemUserId(out userId)) return userId;
if (_user.TryGetOrgNumber(out userId)) return userId;

throw new InvalidOperationException("User external id not found");
}

public async Task<UserInformation?> GetUserInformation(CancellationToken cancellationToken)
public async Task<UserInformation> GetCurrentUserInformation(CancellationToken cancellationToken)
{
if (!TryGetCurrentUserExternalId(out var userExernalId))
{
return null;
}

var userExernalId = GetCurrentUserExternalId();
string? userName;
switch (_user.GetPrincipal().GetUserType())
{
Expand All @@ -55,10 +51,12 @@ public bool TryGetCurrentUserExternalId([NotNullWhen(true)] out string? userExte
case UserType.Enterprise:
userName = await _organizationRegistry.GetOrgShortName(userExernalId, cancellationToken);
break;
case UserType.SystemUser:
// TODO: Implement when we know how system users will be handled
case UserType.Unknown:
case UserType.SystemUser: // Implement when we know how this will be handled
default:
throw new UnreachableException("Unknown user type");
// This should never happen as GetCurrentExternalId should throw if the user type is unknown
throw new UnreachableException();
}

return new(userExernalId, userName);
Expand All @@ -75,12 +73,8 @@ public LocalDevelopmentUserNameRegistryDecorator(IUserNameRegistry userNameRegis
{
_userNameRegistry = userNameRegistry ?? throw new ArgumentNullException(nameof(userNameRegistry));
}
public string GetCurrentUserExternalId() => _userNameRegistry.GetCurrentUserExternalId();

public bool TryGetCurrentUserExternalId([NotNullWhen(true)] out string? userExternalId) =>
_userNameRegistry.TryGetCurrentUserExternalId(out userExternalId);

public Task<UserInformation?> GetUserInformation(CancellationToken cancellationToken)
=> _userNameRegistry.TryGetCurrentUserExternalId(out var userPid)
? Task.FromResult<UserInformation?>(new UserInformation(userPid!, LocalDevelopmentUserPid))
: throw new UnreachableException();
public Task<UserInformation> GetCurrentUserInformation(CancellationToken cancellationToken)
=> Task.FromResult(new UserInformation(GetCurrentUserExternalId(), LocalDevelopmentUserPid));
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ public enum UserType
LegacySystemUser = 2,
SystemUser = 3,
Enterprise = 4
// TODO! Should we add a new type for service owners? ServiceownerOnBehalfOfPerson?
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using AutoMapper;
using Digdir.Domain.Dialogporten.Application.Common;
using Digdir.Domain.Dialogporten.Application.Common.Authentication;
using Digdir.Domain.Dialogporten.Application.Common.ReturnTypes;
using Digdir.Domain.Dialogporten.Application.Externals;
using Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization;
Expand Down Expand Up @@ -45,11 +44,7 @@ public GetDialogSeenLogQueryHandler(
public async Task<GetDialogSeenLogResult> Handle(GetDialogSeenLogQuery request,
CancellationToken cancellationToken)
{
if (!_userNameRegistry.TryGetCurrentUserExternalId(out var userPid))
{
return new Forbidden(Constants.NoAuthenticatedUser);
}

var userId = _userNameRegistry.GetCurrentUserExternalId();
var dialog = await _dbContext.Dialogs
.AsNoTracking()
.Include(x => x.SeenLog.Where(x => x.Id == request.SeenLogId))
Expand Down Expand Up @@ -84,7 +79,7 @@ public async Task<GetDialogSeenLogResult> Handle(GetDialogSeenLogQuery request,
}

var dto = _mapper.Map<GetDialogSeenLogDto>(seenLog);
dto.IsCurrentEndUser = userPid == seenLog.EndUserId;
dto.IsCurrentEndUser = userId == seenLog.EndUserId;
dto.EndUserIdHash = _stringHasher.Hash(seenLog.EndUserId);

return dto;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using AutoMapper;
using Digdir.Domain.Dialogporten.Application.Common;
using Digdir.Domain.Dialogporten.Application.Common.ReturnTypes;
using Digdir.Domain.Dialogporten.Application.Common.Authentication;
using Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization;
using Digdir.Domain.Dialogporten.Application.Externals;
using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities;
Expand Down Expand Up @@ -43,11 +42,7 @@ public SearchDialogSeenLogQueryHandler(

public async Task<SearchDialogSeenLogResult> Handle(SearchDialogSeenLogQuery request, CancellationToken cancellationToken)
{
if (!_userNameRegistry.TryGetCurrentUserExternalId(out var userPid))
{
return new Forbidden(Constants.NoAuthenticatedUser);
}

var userId = _userNameRegistry.GetCurrentUserExternalId();
var dialog = await _db.Dialogs
.AsNoTracking()
.Include(x => x.SeenLog)
Expand Down Expand Up @@ -80,7 +75,7 @@ public async Task<SearchDialogSeenLogResult> Handle(SearchDialogSeenLogQuery req
.Select(x =>
{
var dto = _mapper.Map<SearchDialogSeenLogDto>(x);
dto.IsCurrentEndUser = x.EndUserId == userPid;
dto.IsCurrentEndUser = x.EndUserId == userId;
dto.EndUserIdHash = _stringHasher.Hash(x.EndUserId);
return dto;
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using OneOf;
using AuthenticationConstants = Digdir.Domain.Dialogporten.Application.Common.Authentication.Constants;

namespace Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Dialogs.Queries.Get;

Expand Down Expand Up @@ -54,14 +53,7 @@ public GetDialogQueryHandler(

public async Task<GetDialogResult> Handle(GetDialogQuery request, CancellationToken cancellationToken)
{
var userInformation = await _userNameRegistry.GetUserInformation(cancellationToken);

if (userInformation is null)
{
return new Forbidden(AuthenticationConstants.NoAuthenticatedUser);
}

var (userPid, userName) = userInformation;
var (userId, userName) = await _userNameRegistry.GetCurrentUserInformation(cancellationToken);

// This query could be written without all the includes as ProjectTo will do the job for us.
// However, we need to guarantee an order for sub resources of the dialog aggregate.
Expand Down Expand Up @@ -108,7 +100,7 @@ public async Task<GetDialogResult> Handle(GetDialogQuery request, CancellationTo

// TODO: What if name lookup fails
// https://github.com/digdir/dialogporten/issues/387
dialog.UpdateSeenAt(userPid, userName);
dialog.UpdateSeenAt(userId, userName);

var saveResult = await _unitOfWork
.WithoutAuditableSideEffects()
Expand All @@ -125,7 +117,7 @@ public async Task<GetDialogResult> Handle(GetDialogQuery request, CancellationTo
.Select(log =>
{
var logDto = _mapper.Map<GetDialogDialogSeenLogDto>(log);
logDto.IsCurrentEndUser = log.EndUserId == userPid;
logDto.IsCurrentEndUser = log.EndUserId == userId;
logDto.EndUserIdHash = _stringHasher.Hash(log.EndUserId);
return logDto;
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using AutoMapper;
using AutoMapper.QueryableExtensions;
using Digdir.Domain.Dialogporten.Application.Common;
using Digdir.Domain.Dialogporten.Application.Common.Authentication;
using Digdir.Domain.Dialogporten.Application.Common.Extensions;
using Digdir.Domain.Dialogporten.Application.Common.Extensions.Enumerables;
using Digdir.Domain.Dialogporten.Application.Common.Pagination;
Expand Down Expand Up @@ -137,11 +136,7 @@ public SearchDialogQueryHandler(

public async Task<SearchDialogResult> Handle(SearchDialogQuery request, CancellationToken cancellationToken)
{
if (!_userNameRegistry.TryGetCurrentUserExternalId(out var userPid))
{
return new Forbidden(Constants.NoAuthenticatedUser);
}

var userId = _userNameRegistry.GetCurrentUserExternalId();
var searchExpression = Expressions.LocalizedSearchExpression(request.Search, request.SearchCultureCode);
var authorizedResources = await _altinnAuthorization.GetAuthorizedResourcesForSearch(
request.Party ?? [],
Expand Down Expand Up @@ -181,7 +176,7 @@ public async Task<SearchDialogResult> Handle(SearchDialogQuery request, Cancella
foreach (var seenLog in paginatedList.Items.SelectMany(x => x.SeenSinceLastUpdate))
{
// Before we hash the end user id, check if the seen log entry is for the current user
seenLog.IsCurrentEndUser = userPid == seenLog.EndUserIdHash;
seenLog.IsCurrentEndUser = userId == seenLog.EndUserIdHash;
// TODO: Add test to not expose un-hashed end user id to the client
// https://github.com/digdir/dialogporten/issues/596
seenLog.EndUserIdHash = _stringHasher.Hash(seenLog.EndUserIdHash);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System.Net;
using Azure;
using Digdir.Domain.Dialogporten.Application.Common.Extensions;
using Digdir.Domain.Dialogporten.Application.Externals.Authentication;
using Digdir.Domain.Dialogporten.WebApi.Common.Extensions;
using FluentValidation.Results;

namespace Digdir.Domain.Dialogporten.WebApi.Common.Authentication;

public class UserTypeValidationMiddleware
{
private readonly RequestDelegate _next;

public UserTypeValidationMiddleware(RequestDelegate next)
{
_next = next;
}

public async Task InvokeAsync(HttpContext context)
{
if (context.User.Identity is { IsAuthenticated: true })
{
var userType = context.User.GetUserType();
if (userType == UserType.Unknown)
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
await context.Response.WriteAsJsonAsync(context.ResponseBuilder(
context.Response.StatusCode,
new List<ValidationFailure>() { new("UserType",
"The request was authenticated, but we were unable to determine valid user type (person, enterprise or system user) in order to authorize the request.") }
));

return;
}
}

await _next(context);
}
}

public static class UserTypeValidationMiddlewareExtensions
{
public static IApplicationBuilder UseUserTypeValidation(this IApplicationBuilder app)
=> app.UseMiddleware<UserTypeValidationMiddleware>();
}
1 change: 0 additions & 1 deletion src/Digdir.Domain.Dialogporten.WebApi/Common/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,3 @@ internal static class SwaggerSummary
internal const string OptimisticConcurrencyNote = "Optimistic concurrency control is implemented using the If-Match header. Supply the Revision value from the GetDialog endpoint to ensure that the dialog is not modified/deleted by another request in the meantime.";
}
}

1 change: 1 addition & 0 deletions src/Digdir.Domain.Dialogporten.WebApi/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ static void BuildAndRun(string[] args)
.UseJwtSchemeSelector()
.UseAuthentication()
.UseAuthorization()
.UseUserTypeValidation()
.UseAzureConfiguration()
.UseFastEndpoints(x =>
{
Expand Down

0 comments on commit e2a3517

Please sign in to comment.