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
@@ -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; }
Andreass2 marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Indicates if the user is authorized to sign the correspondence
/// </summary>
[JsonPropertyName("authorizedForSign")]
public bool AuthorizedForSign { get; set; }
Andreass2 marked this conversation as resolved.
Show resolved Hide resolved

/// <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; }
}
}
4 changes: 4 additions & 0 deletions src/Altinn.Correspondence.Application/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ public static void AddApplicationHandlers(this IServiceCollection services)
services.AddScoped<UpdateCorrespondenceStatusHandler>();
services.AddScoped<DownloadCorrespondenceAttachmentHandler>();
services.AddScoped<PurgeCorrespondenceHandler>();
services.AddScoped<UpdateMarkAsUnreadHandler>();
Andreass2 marked this conversation as resolved.
Show resolved Hide resolved
services.AddScoped<MigrateCorrespondenceHandler>();
services.AddScoped<LegacyGetCorrespondencesHandler>();
services.AddScoped<LegacyGetCorrespondenceOverviewHandler>();
Andreass2 marked this conversation as resolved.
Show resolved Hide resolved

// Integrations
services.AddScoped<MalwareScanResultHandler>();
Expand Down
1 change: 1 addition & 0 deletions src/Altinn.Correspondence.Application/Errors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,5 @@ 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 InvalidPartyId = new Error(49, "Invalid partyId", HttpStatusCode.BadRequest);
}
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 @@
_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)

Check failure

Code scanning / CodeQL

User-controlled bypass of sensitive method High

This condition guards a sensitive
action
, but a
user-provided value
controls it.
This condition guards a sensitive
action
, but a
user-provided value
controls it.

Check failure

Code scanning / CodeQL

User-controlled bypass of sensitive method High

This condition guards a sensitive
action
, but a
user-provided value
controls it.

Check failure

Code scanning / CodeQL

User-controlled bypass of sensitive method High

This condition guards a sensitive
action
, but a
user-provided value
controls it.
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 @@
_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 @@
}
}

var response = new GetCorrespondenceOverviewResponse
var response = new LegacyGetCorrespondenceOverviewResponse
{
CorrespondenceId = correspondence.Id,
Content = correspondence.Content,
Expand All @@ -120,6 +123,12 @@
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 AuthroizedForWrite { get; set; }
Andreass2 marked this conversation as resolved.
Show resolved Hide resolved
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 CheckUserAccess(user, rights, resourceId, recipientOrgNo, cancellationToken);
if (responseContent is null) return false;

var validationResult = ValidateAuthorizationResponse(responseContent, user);
return validationResult;
}


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 CheckUserAccess(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?> CheckUserAccess(ClaimsPrincipal user, List<ResourceAccessLevel> rights, string resourceId, string? recipientOrgNo, CancellationToken cancellationToken)
Andreass2 marked this conversation as resolved.
Show resolved Hide resolved
{
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,37 @@ 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 (minimumAuthLevel == null)
{
minimumAuthLevel = Convert.ToInt32(obligation.Value);
}
else if (minimumAuthLevel > Convert.ToInt32(obligation.Value))
{
minimumAuthLevel = Convert.ToInt32(obligation.Value);
}
}
}
}

return minimumAuthLevel;
}
Andreass2 marked this conversation as resolved.
Show resolved Hide resolved


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