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

Legacy/overview auth! #430

Merged
merged 12 commits into from
Nov 5, 2024
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Altinn.Correspondence.API.Models;
using Altinn.Correspondence.API.Models;
using Altinn.Correspondence.API.Models.Enums;
using Altinn.Correspondence.Application;
using Altinn.Correspondence.Application.Configuration;
Expand Down Expand Up @@ -60,9 +60,8 @@ public async Task<ActionResult<CorrespondenceOverviewExt>> GetCorrespondenceOver
};

var commandResult = await handler.Process(request, cancellationToken);

return commandResult.Match(
data => Ok(CorrespondenceOverviewMapper.MapToExternal(data)),
data => Ok(LegacyCorrespondenceOverviewMapper.MapToExternal(data)),
Problem
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Altinn.Correspondence.API.Models;
using Altinn.Correspondence.API.Models.Enums;
using Altinn.Correspondence.Application.GetCorrespondenceOverview;

namespace Altinn.Correspondence.Mappers;

internal static class LegacyCorrespondenceOverviewMapper
{
internal static LegacyCorrespondenceOverviewExt MapToExternal(LegacyGetCorrespondenceOverviewResponse correspondenceOverview)
{
Andreass2 marked this conversation as resolved.
Show resolved Hide resolved
var Correspondence = new LegacyCorrespondenceOverviewExt
{
CorrespondenceId = correspondenceOverview.CorrespondenceId,
Status = (CorrespondenceStatusExt)correspondenceOverview.Status,
StatusText = correspondenceOverview.StatusText,
StatusChanged = (DateTimeOffset)correspondenceOverview.StatusChanged,
Andreass2 marked this conversation as resolved.
Show resolved Hide resolved
SendersReference = correspondenceOverview.SendersReference,
Sender = correspondenceOverview.Sender,
MessageSender = correspondenceOverview.MessageSender,
Created = correspondenceOverview.Created,
Notifications = correspondenceOverview.Notifications,
Recipient = correspondenceOverview.Recipient,
Content = CorrespondenceContentMapper.MapToExternal(correspondenceOverview.Content),
ReplyOptions = CorrespondenceReplyOptionsMapper.MapListToExternal(correspondenceOverview.ReplyOptions),
ExternalReferences = ExternalReferenceMapper.MapListToExternal(correspondenceOverview.ExternalReferences),
ResourceId = correspondenceOverview.ResourceId.ToString(),
Andreass2 marked this conversation as resolved.
Show resolved Hide resolved
RequestedPublishTime = correspondenceOverview.RequestedPublishTime,
MarkedUnread = correspondenceOverview.MarkedUnread,
AllowSystemDeleteAfter = correspondenceOverview.AllowSystemDeleteAfter,
DueDateTime = correspondenceOverview.DueDateTime,
PropertyList = correspondenceOverview.PropertyList,
IgnoreReservation = correspondenceOverview.IgnoreReservation,
Published = correspondenceOverview.Published,
IsConfirmationNeeded = correspondenceOverview.IsConfirmationNeeded,
AllowDelete = correspondenceOverview.AllowDelete,
Archived = correspondenceOverview.Archived,
MinimumAuthenticationLevel = correspondenceOverview.MinimumAuthenticationLevel,
AuthorizedForSign = correspondenceOverview.AuthorizedForSign,
};
return Correspondence;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,6 @@ public class LegacyCorrespondenceItemExt
/// The minumum authentication level required to view this correspondence
/// </summary>
[JsonPropertyName("minimumAuthenticationlevel")]
public required int MinimumAuthenticationlevel { get; set; }
public required int MinimumAuthenticationLevel { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Altinn.Correspondence.API.Models.Enums;
using Altinn.Correspondence.Application.GetCorrespondenceOverview;
using System.Text.Json.Serialization;

namespace Altinn.Correspondence.API.Models
{
/// <summary>
/// An object representing an overview of a correspondence for the legacy API
/// </summary>
public class LegacyCorrespondenceOverviewExt : CorrespondenceOverviewExt
{
/// <summary>
/// The minimum authentication level required to view the correspondence
/// </summary>
[JsonPropertyName("minimumAuthenticationLevel")]
public int MinimumAuthenticationLevel { get; set; }

/// <summary>
/// Indicates if the user is authorized to sign the correspondence
/// </summary>
[JsonPropertyName("authorizedForSign")]
public bool AuthorizedForSign { get; set; }

/// <summary>
/// The due date for the correspondence
/// </summary>
[JsonPropertyName("dueDateTime")]
public DateTimeOffset? DueDateTime { get; set; }

/// <summary>
/// Indicates if the correspondence can be deleted
/// </summary>
[JsonPropertyName("allowDelete")]
public bool AllowDelete { get; set; }

/// <summary>
/// The date the correspondence was archived
/// </summary>
[JsonPropertyName("archived")]
public DateTimeOffset? Archived { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public static void AddApplicationHandlers(this IServiceCollection services)
services.AddScoped<UpdateCorrespondenceStatusHandler>();
services.AddScoped<DownloadCorrespondenceAttachmentHandler>();
services.AddScoped<PurgeCorrespondenceHandler>();
services.AddScoped<MigrateCorrespondenceHandler>();

// Integrations
services.AddScoped<MalwareScanResultHandler>();
Expand Down
5 changes: 3 additions & 2 deletions src/Altinn.Correspondence.Application/Errors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public static class Errors
public static Error MessageSummaryEmpty = new Error(46, "Message summary cannot be empty", HttpStatusCode.BadRequest);
public static Error InvalidLanguage = new Error(47, "Invalid language chosen. Supported languages is Norsk bokmål (nb), Nynorsk (nn) and English (en)", HttpStatusCode.BadRequest);
public static Error LegacyNoAccessToCorrespondence = new Error(48, "User does not have access to the correspondence", HttpStatusCode.Unauthorized);
public static Error ConfirmBeforeFetched = new Error(49, "Correspondence must be fetched before it can be confirmed", HttpStatusCode.BadRequest);
public static Error ReadBeforeFetched = new Error(50, "Correspondence must be fetched before it can be read", HttpStatusCode.BadRequest);
public static Error InvalidPartyId = new Error(49, "Invalid partyId", HttpStatusCode.BadRequest);
public static Error ConfirmBeforeFetched = new Error(50, "Correspondence must be fetched before it can be confirmed", HttpStatusCode.BadRequest);
public static Error ReadBeforeFetched = new Error(51, "Correspondence must be fetched before it can be read", HttpStatusCode.BadRequest);
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public async Task<OneOf<LegacyGetCorrespondencesResponse, Error>> Process(Legacy
MessageTitle = correspondence.Content.MessageTitle,
Status = correspondence.GetLatestStatusWithoutPurged().Status,
CorrespondenceId = correspondence.Id,
MinimumAuthenticationlevel = 0, // Insert from response from PDP multirequest
MinimumAuthenticationLevel = 0, // Insert from response from PDP multirequest
Andreass2 marked this conversation as resolved.
Show resolved Hide resolved
Published = correspondence.Published,
PurgedStatus = purgedStatus?.Status,
Purged = purgedStatus?.StatusChanged,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public class LegacyCorrespondenceItem
public required string MessageTitle { get; set; }
public required string ServiceOwnerName { get; set; }
public required CorrespondenceStatus Status { get; set; }
public required int MinimumAuthenticationlevel { get; set; }
public required int MinimumAuthenticationLevel { get; set; }
public DateTimeOffset? Published { get; set; }
public CorrespondenceStatus? PurgedStatus { get; set; }
public DateTimeOffset? Purged { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

namespace Altinn.Correspondence.Application.GetCorrespondenceOverview;

public class LegacyGetCorrespondenceOverviewHandler : IHandler<LegacyGetCorrespondenceOverviewRequest, GetCorrespondenceOverviewResponse>
public class LegacyGetCorrespondenceOverviewHandler : IHandler<LegacyGetCorrespondenceOverviewRequest, LegacyGetCorrespondenceOverviewResponse>
{
private readonly IAltinnAccessManagementService _altinnAccessManagementService;
private readonly IAltinnAuthorizationService _altinnAuthorizationService;
Expand All @@ -30,24 +30,28 @@ public LegacyGetCorrespondenceOverviewHandler(IAltinnAccessManagementService alt
_logger = logger;
}

public async Task<OneOf<GetCorrespondenceOverviewResponse, Error>> Process(LegacyGetCorrespondenceOverviewRequest request, CancellationToken cancellationToken)
public async Task<OneOf<LegacyGetCorrespondenceOverviewResponse, Error>> Process(LegacyGetCorrespondenceOverviewRequest request, CancellationToken cancellationToken)
{
if (request.PartyId == 0 || request.PartyId == int.MinValue)
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
{
return Errors.CouldNotFindOrgNo; // TODO: Update to better error message
return Errors.InvalidPartyId;
}

var userParty = await _altinnRegisterService.LookUpPartyByPartyId(request.PartyId, cancellationToken);
if (userParty == null || (string.IsNullOrEmpty(userParty.SSN) && string.IsNullOrEmpty(userParty.OrgNumber)))
{
return Errors.CouldNotFindOrgNo; // TODO: Update to better error message
return Errors.CouldNotFindOrgNo;
}
var correspondence = await _correspondenceRepository.GetCorrespondenceById(request.CorrespondenceId, true, true, cancellationToken);
if (correspondence == null)
{
return Errors.CorrespondenceNotFound;
}

var minimumAuthLevel = await _altinnAuthorizationService.CheckUserAccessAndGetMinimumAuthLevel(correspondence.ResourceId, new List<ResourceAccessLevel> { ResourceAccessLevel.Read }, cancellationToken);
if (minimumAuthLevel == null)
{
return Errors.LegacyNoAccessToCorrespondence;
}
var recipients = new List<string>();
if (correspondence.Recipient != userParty.SSN && correspondence.Recipient != ("0192:" + userParty.OrgNumber))
{
Expand All @@ -64,7 +68,6 @@ public async Task<OneOf<GetCorrespondenceOverviewResponse, Error>> Process(Legac
_logger.LogWarning("Latest status not found for correspondence");
return Errors.CorrespondenceNotFound;
}

if (!latestStatus.Status.IsAvailableForRecipient())
{
_logger.LogWarning("Rejected because correspondence not available for recipient in current state.");
Expand Down Expand Up @@ -98,7 +101,7 @@ await _correspondenceStatusRepository.AddCorrespondenceStatus(new Correspondence
}
}

var response = new GetCorrespondenceOverviewResponse
var response = new LegacyGetCorrespondenceOverviewResponse
{
CorrespondenceId = correspondence.Id,
Content = correspondence.Content,
Expand All @@ -120,6 +123,12 @@ await _correspondenceStatusRepository.AddCorrespondenceStatus(new Correspondence
AllowSystemDeleteAfter = correspondence.AllowSystemDeleteAfter,
Published = correspondence.Published,
IsConfirmationNeeded = correspondence.IsConfirmationNeeded,
MinimumAuthenticationLevel = (int)minimumAuthLevel,
AuthorizedForSign = true,
DueDateTime = correspondence.DueDateTime,
AllowDelete = true,
Archived = correspondence.Statuses?.FirstOrDefault(s => s.Status == CorrespondenceStatus.Archived)?.StatusChanged,
PropertyList = correspondence.PropertyList ?? new Dictionary<string, string>()
};
return response;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Altinn.Correspondence.Core.Models.Entities;
using Altinn.Correspondence.Core.Models.Enums;

namespace Altinn.Correspondence.Application.GetCorrespondenceOverview;

public class LegacyGetCorrespondenceOverviewResponse : GetCorrespondenceOverviewResponse
{
public bool AllowDelete { get; set; }
public bool AuthorizedForWrite { get; set; }
public bool AuthorizedForSign { get; set; }
public DateTimeOffset? Archived { get; set; }
public int MinimumAuthenticationLevel { get; set; }
}

Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ namespace Altinn.Correspondence.Core.Repositories;
public interface IAltinnAuthorizationService
{
Task<bool> CheckUserAccess(string resourceId, List<ResourceAccessLevel> rights, CancellationToken cancellationToken = default, string? recipientOrgNo = null);
Task<int?> CheckUserAccessAndGetMinimumAuthLevel(string resourceId, List<ResourceAccessLevel> rights, CancellationToken cancellationToken = default, string? recipientOrgNo = null);
Task<bool> CheckMigrationAccess(string resourceId, List<ResourceAccessLevel> rights, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,10 @@ public Task<bool> CheckUserAccess(string resourceId, List<ResourceAccessLevel> r
{
return Task.FromResult(true);
}

public Task<int?> CheckUserAccessAndGetMinimumAuthLevel(string resourceId, List<ResourceAccessLevel> rights, CancellationToken cancellationToken = default, string? recipientOrgNo = null)
{
return Task.FromResult((int?)3);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,50 @@ public AltinnAuthorizationService(HttpClient httpClient, IOptions<AltinnOptions>
public async Task<bool> CheckUserAccess(string resourceId, List<ResourceAccessLevel> rights, CancellationToken cancellationToken = default, string? recipientOrgNo = null)
{
var user = _httpContextAccessor.HttpContext?.User;
if (_httpClient.BaseAddress is null)
var validation = await ValidateCheckUserAccess(user, resourceId, cancellationToken);
if (validation != null) return (bool)validation;
var responseContent = await AuthorizeRequest(user, rights, resourceId, recipientOrgNo, cancellationToken);
if (responseContent is null) return false;

var validationResult = ValidateAuthorizationResponse(responseContent, user);
return validationResult;
}
Andreass2 marked this conversation as resolved.
Show resolved Hide resolved


public async Task<int?> CheckUserAccessAndGetMinimumAuthLevel(string resourceId, List<ResourceAccessLevel> rights, CancellationToken cancellationToken = default, string? recipientOrgNo = null)
{
var user = _httpContextAccessor.HttpContext?.User;
var validation = await ValidateCheckUserAccess(user, resourceId, cancellationToken);
if (validation != null) return (bool)validation ? 3 : null;
var responseContent = await AuthorizeRequest(user, rights, resourceId, recipientOrgNo, cancellationToken);
if (responseContent is null) return null;

var validationResult = ValidateAuthorizationResponse(responseContent, user);
if (!validationResult)
{
return null;
}
int? minLevel = IdportenXacmlMapper.GetMinimumAuthLevel(responseContent, user);
return minLevel;
}

public async Task<bool> CheckMigrationAccess(string resourceId, List<ResourceAccessLevel> rights, CancellationToken cancellationToken = default)
{
if (_hostEnvironment.IsDevelopment())
{
return true;
}
var serviceOwnerId = await _resourceRepository.GetServiceOwnerOfResource(resourceId, cancellationToken);
if (string.IsNullOrWhiteSpace(serviceOwnerId))
{
return false;
}

return true;
}
private async Task<bool?> ValidateCheckUserAccess(ClaimsPrincipal user, string resourceId, CancellationToken cancellationToken)
{
if (_httpClient.BaseAddress is null)
{
_logger.LogWarning("Authorization service disabled");
return true;
Expand All @@ -61,36 +104,25 @@ public async Task<bool> CheckUserAccess(string resourceId, List<ResourceAccessLe
_logger.LogError("Unexpected null value. User was null when checking access to resource");
return false;
}
return null;
}

private async Task<XacmlJsonResponse?> AuthorizeRequest(ClaimsPrincipal user, List<ResourceAccessLevel> rights, string resourceId, string? recipientOrgNo, CancellationToken cancellationToken)
{
var actionIds = rights.Select(GetActionId).ToList();
XacmlJsonRequestRoot jsonRequest = CreateDecisionRequest(user, actionIds, resourceId, recipientOrgNo);
var response = await _httpClient.PostAsJsonAsync("authorization/api/v1/authorize", jsonRequest, cancellationToken);
if (!response.IsSuccessStatusCode)
{
return false;
return null;
}
var responseContent = await response.Content.ReadFromJsonAsync<XacmlJsonResponse>(cancellationToken: cancellationToken);
if (responseContent is null)
{
_logger.LogError("Unexpected null or invalid json response from Authorization.");
return false;
}
var validationResult = ValidateAuthorizationResponse(responseContent, user);
return validationResult;
}

public async Task<bool> CheckMigrationAccess(string resourceId, List<ResourceAccessLevel> rights, CancellationToken cancellationToken = default)
{
if (_hostEnvironment.IsDevelopment())
{
return true;
}
var serviceOwnerId = await _resourceRepository.GetServiceOwnerOfResource(resourceId, cancellationToken);
if (string.IsNullOrWhiteSpace(serviceOwnerId))
{
return false;
return null;
}

return true;
return responseContent;
}

private XacmlJsonRequestRoot CreateDecisionRequest(ClaimsPrincipal user, List<string> actionTypes, string resourceId, string? recipientOrgNo)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,34 @@ public static bool ValidateIdportenAuthorizationResponse(XacmlJsonResponse respo

return true;
}
public static int? GetMinimumAuthLevel(XacmlJsonResponse response, ClaimsPrincipal user)
{
int? minimumAuthLevel = null;
foreach (var result in response.Response)
{
if (!result.Decision.Equals(XacmlContextDecision.Permit.ToString()))
{
return null;
}

if (result.Obligations != null)
{
List<XacmlJsonObligationOrAdvice> obligations = result.Obligations;
XacmlJsonAttributeAssignment? obligation = GetObligation("urn:altinn:minimum-authenticationlevel", obligations);
if (obligation != null)
{
if (!int.TryParse(obligation.Value, out int currentLevel))
{
continue;
}
minimumAuthLevel = minimumAuthLevel.HasValue ? Math.Min(minimumAuthLevel.Value, currentLevel) : currentLevel;
}
}
}

return minimumAuthLevel;
}


private static XacmlJsonAttributeAssignment? GetObligation(string category, List<XacmlJsonObligationOrAdvice> obligations)
{
Expand Down
Loading