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

feat: Authorized parties endpoint in enduser API #661

Merged
merged 15 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion docs/schema/V1/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@digdir/dialogporten-schema",
"version": "1.0.7",
"version": "1.1.0",
elsand marked this conversation as resolved.
Show resolved Hide resolved
"description": "GraphQl schema and OpenAPI spec for Dialogporten",
"author": "DigDir",
"repository": {
Expand Down
89 changes: 87 additions & 2 deletions docs/schema/V1/swagger.verified.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
{
"openapi": "3.0.0",
"info": {
"title": "Dialogporten",
Expand Down Expand Up @@ -1606,6 +1606,42 @@
]
}
},
"/api/v1/enduser/parties": {
"get": {
"tags": [
"Enduser"
],
"summary": "Gets the list of authorized parties for the end user",
"description": "Gets the list of authorized parties for the end user. For more information see the documentation (link TBD).",
"operationId": "GetParties",
"responses": {
"200": {
"description": "The list of authorized parties for the end user",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/GetPartyDto"
}
}
}
}
},
"401": {
"description": "Unauthorized"
},
"403": {
"description": "Forbidden"
}
},
"security": [
{
"JWTBearerAuth": []
}
]
}
},
"/api/v1/enduser/dialogs/{dialogId}/seenlog": {
"get": {
"tags": [
Expand Down Expand Up @@ -3732,6 +3768,55 @@
}
}
},
"GetPartyDto": {
"type": "object",
"additionalProperties": false,
"properties": {
"authorizedParties": {
"type": "array",
"items": {
"$ref": "#/components/schemas/AuthorizedPartyDto"
}
}
}
},
"AuthorizedPartyDto": {
"type": "object",
"additionalProperties": false,
"properties": {
"party": {
"type": "string"
},
"name": {
"type": "string"
},
"partyType": {
"type": "string"
},
"isDeleted": {
"type": "boolean"
},
"hasKeyRole": {
"type": "boolean"
},
"isMainAdministrator": {
"type": "boolean"
},
"isAccessManager": {
"type": "boolean"
},
"hasOnlyAccessToSubParties": {
"type": "boolean"
},
"subParties": {
"type": "array",
"nullable": true,
"items": {
"$ref": "#/components/schemas/AuthorizedPartyDto"
}
}
}
},
"SearchDialogSeenLogDto": {
"type": "object",
"additionalProperties": false,
Expand Down Expand Up @@ -4377,4 +4462,4 @@
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public static IServiceCollection AddApplication(this IServiceCollection services
.AddTransient<IUserOrganizationRegistry, UserOrganizationRegistry>()
.AddTransient<IUserResourceRegistry, UserResourceRegistry>()
.AddTransient<IUserNameRegistry, UserNameRegistry>()
.AddTransient<IUserParties, UserParties>()
.AddTransient<IDialogActivityService, DialogActivityService>()
.AddTransient<IClock, Clock>()
.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Digdir.Domain.Dialogporten.Application.Common.Extensions;
using Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization;
using Digdir.Domain.Dialogporten.Application.Externals.Presentation;
using Digdir.Domain.Dialogporten.Domain.Parties;

namespace Digdir.Domain.Dialogporten.Application.Common;

public interface IUserParties
{
public Task<AuthorizedPartiesResult> GetUserParties(CancellationToken cancellationToken = default);
}

public class UserParties : IUserParties
{
private readonly IUser _user;
private readonly IAltinnAuthorization _altinnAuthorization;

public UserParties(IUser user, IAltinnAuthorization altinnAuthorization)
{
_user = user ?? throw new ArgumentNullException(nameof(user));
_altinnAuthorization = altinnAuthorization ?? throw new ArgumentNullException(nameof(altinnAuthorization));
}

public Task<AuthorizedPartiesResult> GetUserParties(CancellationToken cancellationToken = default) =>
_user.TryGetPid(out var pid) &&
NorwegianPersonIdentifier.TryParse(NorwegianPersonIdentifier.PrefixWithSeparator + pid,
out var partyIdentifier)
? _altinnAuthorization.GetAuthorizedParties(partyIdentifier, cancellationToken)
: Task.FromResult(new AuthorizedPartiesResult());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Digdir.Domain.Dialogporten.Domain.Parties.Abstractions;

namespace Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization;

public class AuthorizedPartiesResult
{
public List<AuthorizedParty> AuthorizedParties { get; init; } = new();
}

public class AuthorizedParty
{
public string Party { get; init; } = null!;
public string Name { get; init; } = null!;
public AuthorizedPartyType PartyType { get; init; }
public bool IsDeleted { get; init; }
public bool HasKeyRole { get; init; }
public bool IsMainAdministrator { get; init; }
public bool IsAccessManager { get; init; }
public bool HasOnlyAccessToSubParties { get; init; }
public List<string> AuthorizedResources { get; init; } = new();
public List<AuthorizedParty>? SubParties { get; init; }
}

public enum AuthorizedPartyType
{
Person,
Organization
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities;
using Digdir.Domain.Dialogporten.Domain.Parties.Abstractions;

namespace Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization;

Expand All @@ -13,4 +14,7 @@ public Task<DialogSearchAuthorizationResult> GetAuthorizedResourcesForSearch(
List<string> constraintServiceResources,
string? endUserId = null,
CancellationToken cancellationToken = default);

public Task<AuthorizedPartiesResult> GetAuthorizedParties(IPartyIdentifier authenticatedParty,
CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Parties.Queries.Get;

public class GetPartyDto
{
public List<AuthorizedPartyDto> AuthorizedParties { get; init; } = new();
}

public class AuthorizedPartyDto
{
public string Party { get; init; } = null!;
public string Name { get; init; } = null!;
public string PartyType { get; init; } = null!;
public bool IsDeleted { get; init; }
public bool HasKeyRole { get; init; }
public bool IsMainAdministrator { get; init; }
public bool IsAccessManager { get; init; }
public bool HasOnlyAccessToSubParties { get; init; }
public List<AuthorizedPartyDto>? SubParties { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using AutoMapper;
using Digdir.Domain.Dialogporten.Application.Common;
using MediatR;

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

public sealed class GetPartyQuery : IRequest<GetPartyDto>;

internal sealed class GetPartyQueryHandler : IRequestHandler<GetPartyQuery, GetPartyDto>
{
private readonly IUserParties _userParties;
private readonly IMapper _mapper;

public GetPartyQueryHandler(IUserParties userParties, IMapper mapper)
{
_userParties = userParties;
_mapper = mapper;
}

public async Task<GetPartyDto> Handle(GetPartyQuery request, CancellationToken cancellationToken)
{
var authorizedPartiesResult = await _userParties.GetUserParties(cancellationToken);
return _mapper.Map<GetPartyDto>(authorizedPartiesResult);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using AutoMapper;
using Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization;

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

internal sealed class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<AuthorizedPartiesResult, GetPartyDto>();
CreateMap<AuthorizedParty, AuthorizedPartyDto>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,23 @@
using ZiggyCreatures.Caching.Fusion;

namespace Digdir.Domain.Dialogporten.Infrastructure.Altinn.Authorization;

internal sealed class AltinnAuthorizationClient : IAltinnAuthorization
{
private const string AuthorizeUrl = "authorization/api/v1/authorize";
private const string AuthorizedPartiesUrl = "/accessmanagement/api/v1/resourceowner/authorizedparties?includeAltinn2=true";

private readonly HttpClient _httpClient;
private readonly IFusionCache _cache;
private readonly IUser _user;
private readonly IDialogDbContext _db;
private readonly ILogger _logger;

private static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault
};

public AltinnAuthorizationClient(
HttpClient client,
IFusionCacheProvider cacheProvider,
Expand Down Expand Up @@ -71,7 +79,23 @@ public async Task<DialogSearchAuthorizationResult> GetAuthorizedResourcesForSear
=> await PerformNonScalableDialogSearchAuthorization(request, token), token: cancellationToken);
}

private async Task<DialogSearchAuthorizationResult> PerformNonScalableDialogSearchAuthorization(DialogSearchAuthorizationRequest request, CancellationToken cancellationToken)
public async Task<AuthorizedPartiesResult> GetAuthorizedParties(IPartyIdentifier authenticatedParty,
CancellationToken cancellationToken = default)
{
var authorizedPartiesRequest = new AuthorizedPartiesRequest(authenticatedParty);
return await _cache.GetOrSetAsync(authorizedPartiesRequest.GenerateCacheKey(), async token
=> await PerformAuthorizedPartiesRequest(authorizedPartiesRequest, token), token: cancellationToken);
}
oskogstad marked this conversation as resolved.
Show resolved Hide resolved

private async Task<AuthorizedPartiesResult> PerformAuthorizedPartiesRequest(AuthorizedPartiesRequest authorizedPartiesRequest,
CancellationToken token)
{
var authorizedPartiesDto = await SendAuthorizedPartiesRequest(authorizedPartiesRequest, token);
return AuthorizedPartiesHelper.CreateAuthorizedPartiesResult(authorizedPartiesDto);
}

private async Task<DialogSearchAuthorizationResult> PerformNonScalableDialogSearchAuthorization(
DialogSearchAuthorizationRequest request, CancellationToken cancellationToken)
{
/*
* This is a preliminary implementation as per https://github.com/digdir/dialogporten/issues/249
Expand Down Expand Up @@ -107,14 +131,15 @@ private async Task<DialogSearchAuthorizationResult> PerformNonScalableDialogSear
}

var xacmlJsonRequest = DecisionRequestHelper.NonScalable.CreateDialogSearchRequest(request);
var xamlJsonResponse = await SendRequest(xacmlJsonRequest, cancellationToken);
var xamlJsonResponse = await SendPdpRequest(xacmlJsonRequest, cancellationToken);
return DecisionRequestHelper.NonScalable.CreateDialogSearchResponse(xacmlJsonRequest, xamlJsonResponse);
}

private async Task<DialogDetailsAuthorizationResult> PerformDialogDetailsAuthorization(DialogDetailsAuthorizationRequest request, CancellationToken cancellationToken)
private async Task<DialogDetailsAuthorizationResult> PerformDialogDetailsAuthorization(
DialogDetailsAuthorizationRequest request, CancellationToken cancellationToken)
{
var xacmlJsonRequest = DecisionRequestHelper.CreateDialogDetailsRequest(request);
var xamlJsonResponse = await SendRequest(xacmlJsonRequest, cancellationToken);
var xamlJsonResponse = await SendPdpRequest(xacmlJsonRequest, cancellationToken);
return DecisionRequestHelper.CreateDialogDetailsResponse(request.AltinnActions, xamlJsonResponse);
}

Expand All @@ -133,32 +158,33 @@ private List<Claim> GetOrCreateClaimsBasedOnEndUserId(string? endUserId)
return claims;
}

private static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault
};
private async Task<XacmlJsonResponse?> SendPdpRequest(
XacmlJsonRequestRoot xacmlJsonRequest, CancellationToken cancellationToken) =>
await SendRequest<XacmlJsonResponse>(
AuthorizeUrl, xacmlJsonRequest, cancellationToken);

private async Task<List<AuthorizedPartiesResultDto>?> SendAuthorizedPartiesRequest(
AuthorizedPartiesRequest authorizedPartiesRequest, CancellationToken cancellationToken) =>
await SendRequest<List<AuthorizedPartiesResultDto>>(
AuthorizedPartiesUrl, authorizedPartiesRequest, cancellationToken);

private async Task<XacmlJsonResponse?> SendRequest(XacmlJsonRequestRoot xacmlJsonRequest, CancellationToken cancellationToken)
private async Task<T?> SendRequest<T>(string url, object request, CancellationToken cancellationToken)
{
const string apiUrl = "authorization/api/v1/authorize";
var requestJson = JsonSerializer.Serialize(xacmlJsonRequest, SerializerOptions);
_logger.LogDebug("Generated XACML request: {RequestJson}", requestJson);
var requestJson = JsonSerializer.Serialize(request, SerializerOptions);
_logger.LogDebug("Authorization request to {Url}: {RequestJson}", url, requestJson);
var httpContent = new StringContent(requestJson, Encoding.UTF8, "application/json");

var response = await _httpClient.PostAsync(apiUrl, httpContent, cancellationToken);

var response = await _httpClient.PostAsync(url, httpContent, cancellationToken);
if (response.StatusCode != HttpStatusCode.OK)
{
var errorResponse = await response.Content.ReadAsStringAsync(cancellationToken);
_logger.LogInformation(
"AltinnAuthorizationClient.SendRequest failed with non-successful status code: {StatusCode} {Response}",
_logger.LogWarning(
nameof(AltinnAuthorizationClient) + ".SendRequest failed with non-successful status code: {StatusCode} {Response}",
response.StatusCode, errorResponse);

return null;
return default;
}

var responseData = await response.Content.ReadAsStringAsync(cancellationToken);
return JsonSerializer.Deserialize<XacmlJsonResponse>(responseData, SerializerOptions);
return JsonSerializer.Deserialize<T>(responseData, SerializerOptions);
}
}
Loading