Skip to content

Commit

Permalink
Add new person as user in organization (#1220)
Browse files Browse the repository at this point in the history
* can now add new user (person)

* Add error handling and tests; update localization strings

* made story and mocked new endpoint in GUI

* added check of ModelState to BFF endpoint

* fixed missing sign

* minor fixes

* fix warning

* added yet another test

* renamed file

* remove unnecesary line

---------

Co-authored-by: Vedeler <[email protected]>
  • Loading branch information
allinox and Vedeler authored Jan 15, 2025
1 parent fbeb880 commit ea848b6
Show file tree
Hide file tree
Showing 26 changed files with 763 additions and 68 deletions.
15 changes: 15 additions & 0 deletions .mock/handlers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,19 @@ export const userHandlers = (ACCESSMANAGEMENT_BASE_URL: string) => [
},
});
}),
http.post(
new RegExp(`${ACCESSMANAGEMENT_BASE_URL}/user/reportee/(?:/[^/]+)?/rightholder/person`),
async (req: any) => {
const requestBody = await req.request.json();
const ssn = requestBody?.ssn;
const lastName = requestBody?.lastName;
if (lastName.length < 4 || ssn.length !== 11) {
return HttpResponse.json({ error: 'Invalid input' }, { status: 404 });
} else {
return HttpResponse.json({
partyUuid: '54f128f7-ca7c-4a57-ad49-3787eb79b506',
});
}
},
),
];
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,59 @@

namespace Altinn.AccessManagement.UI.Core.ClientInterfaces
{
/// <summary>
/// Interface for client wrapper for integration with the platform register API
/// </summary>
public interface IRegisterClient
{
/// <summary>
/// Looks up party information for an organization based on the organization number
/// Interface for client wrapper for integration with the platform register API
/// </summary>
/// <param name="organizationNumber">The organization number</param>
/// <returns>
/// Party information
/// </returns>
Task<Party> GetPartyForOrganization(string organizationNumber);
public interface IRegisterClient
{
/// <summary>
/// Looks up party information for an organization based on the organization number
/// </summary>
/// <param name="organizationNumber">The organization number</param>
/// <returns>
/// Party information
/// </returns>
Task<Party> GetPartyForOrganization(string organizationNumber);

/// <summary>
/// Looks up party information for a list of uuids
/// </summary>
/// <param name="uuidList">The list of uuids to be looked up</param>
/// <returns>
/// A list of party information corresponding to the provided uuids
/// </returns>
Task<List<Party>> GetPartyList(List<Guid> uuidList);
/// <summary>
/// Looks up party information for a person based on the ssn
/// </summary>
/// <param name="ssn">The persons ssn</param>
/// <returns>
/// Party information
/// </returns>
Task<Party> GetPartyForPerson(string ssn);

/// <summary>
/// Looks up party name for a list of orgNumbers
/// </summary>
/// <param name="orgNumbers">The list of organisation numbers to be looked up</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>
/// A list of party organisation numbers with corresponding names
/// </returns>
Task<List<PartyName>> GetPartyNames(IEnumerable<string> orgNumbers, CancellationToken cancellationToken);
}
/// <summary>
/// Looks up party information for a list of uuids
/// </summary>
/// <param name="uuidList">The list of uuids to be looked up</param>
/// <returns>
/// A list of party information corresponding to the provided uuids
/// </returns>
Task<List<Party>> GetPartyList(List<Guid> uuidList);

/// <summary>
/// Looks up a person based on ssn and lastname.
/// The endpoint will track the number of failed lookup attempts and block further requests if the number of failed
/// lookups have exceeded a threshold number. The user will be prevented from performing new searches
/// for a set time. (by throwing a 429 status exception)
/// </summary>
/// <param name="ssn">The persons ssn - must match latname</param>
/// <param name="lastname">The persons lastname - must match ssn</param>
/// <returns>
/// Person information
/// </returns>
Task<Person> GetPerson(string ssn, string lastname);

/// <summary>
/// Looks up party name for a list of orgNumbers
/// </summary>
/// <param name="orgNumbers">The list of organisation numbers to be looked up</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>
/// A list of party organisation numbers with corresponding names
/// </returns>
Task<List<PartyName>> GetPartyNames(IEnumerable<string> orgNumbers, CancellationToken cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,13 @@ public interface IUserService
/// <param name="rightHolderUuid">The uuid for the right holder whose accesses are to be returned</param>
/// <returns>All right holder's accesses</returns>
Task<RightHolderAccesses> GetRightHolderAccesses(string reporteeUuid, string rightHolderUuid);

/// <summary>
/// Checks that a person with the provided ssn and lastname exists. If they do, the person's partyUuid is returned.
/// </summary>
/// <param name="ssn">The ssn of the user</param>
/// <param name="lastname">The last name of the user</param>
/// <returns>The person's partyUuid if ssn and lastname correspond to the same person. Returns null if matching person is not found</returns>
Task<Guid?> ValidatePerson(string ssn, string lastname);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
using Altinn.AccessManagement.UI.Core.ClientInterfaces;
using Altinn.AccessManagement.UI.Core.Enums;
using Altinn.AccessManagement.UI.Core.Models;
using Altinn.AccessManagement.UI.Core.Models.AccessManagement;
using Altinn.AccessManagement.UI.Core.Models.Delegation;
using Altinn.AccessManagement.UI.Core.Models.Delegation.Frontend;
using Altinn.AccessManagement.UI.Core.Models.ResourceRegistry;
using Altinn.AccessManagement.UI.Core.Models.User;
using Altinn.AccessManagement.UI.Core.Services.Interfaces;
using Altinn.Platform.Profile.Models;
Expand All @@ -22,6 +18,7 @@ public class UserService : IUserService
private readonly IProfileClient _profileClient;
private readonly IAccessManagementClient _accessManagementClient;
private readonly IAccessManagementClientV0 _accessManagementClientV0;
private readonly IRegisterClient _registerClient;

/// <summary>
/// Initializes a new instance of the <see cref="APIDelegationService"/> class.
Expand All @@ -30,16 +27,19 @@ public class UserService : IUserService
/// <param name="profileClient">handler for profile client</param>
/// <param name="accessManagementClient">handler for AM client</param>
/// <param name="accessManagementClientV0">handler for old AM client</param>
/// <param name="registerClient">handler for register client</param>
public UserService(
ILogger<IAPIDelegationService> logger,
IProfileClient profileClient,
IAccessManagementClient accessManagementClient,
IAccessManagementClientV0 accessManagementClientV0)
IAccessManagementClientV0 accessManagementClientV0,
IRegisterClient registerClient)
{
_logger = logger;
_profileClient = profileClient;
_accessManagementClient = accessManagementClient;
_accessManagementClientV0 = accessManagementClientV0;
_registerClient = registerClient;
}

/// <inheritdoc/>
Expand Down Expand Up @@ -69,5 +69,29 @@ public Task<RightHolderAccesses> GetRightHolderAccesses(string reporteeUuid, str
{
return _accessManagementClient.GetRightHolderAccesses(reporteeUuid, rightHolderUuid);
}

/// <inheritdoc/>
public async Task<Guid?> ValidatePerson(string ssn, string lastname)
{
// Check for bad input
string ssn_cleaned = ssn.Trim().Replace("\"", string.Empty);
string lastname_cleaned = lastname.Trim().Replace("\"", string.Empty);
if (ssn_cleaned.Length != 11 || !ssn_cleaned.All(char.IsDigit))
{
return null;
}

// Check that a person with the provided ssn and last name exists
Person person = await _registerClient.GetPerson(ssn_cleaned, lastname_cleaned);

if (person == null)
{
return null;
}

Party personParty = await _registerClient.GetPartyForPerson(ssn_cleaned);

return personParty?.PartyUuid;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Net;
using System.Text.Json;
using System.Text.Json;
using Altinn.AccessManagement.UI.Core.Helpers;
using Microsoft.Extensions.Logging;

namespace Altinn.AccessManagement.UI.Integration.Util
{
Expand All @@ -14,8 +14,10 @@ public class ClientUtils
/// </summary>
/// <typeparam name="T">The type that the response is to be deserialized into</typeparam>
/// <param name="response">The response message that is to be deserialized</param>
/// <param name="logger">The client logger to be used if errors are to be logged</param>
/// <param name="clientMethodName">The client name and method name to be used in logging if a logger is provided</param>
/// <returns>The response, deserialized into an object of type T</returns>
public async static Task<T> DeserializeIfSuccessfullStatusCode<T>(HttpResponseMessage response)
public async static Task<T> DeserializeIfSuccessfullStatusCode<T>(HttpResponseMessage response, ILogger logger = null, string clientMethodName = "")
{
JsonSerializerOptions serializerOptions = new JsonSerializerOptions
{
Expand All @@ -29,8 +31,23 @@ public async static Task<T> DeserializeIfSuccessfullStatusCode<T>(HttpResponseMe
}
else
{
string responseContent = await response.Content.ReadAsStringAsync();
HttpStatusException error = JsonSerializer.Deserialize<HttpStatusException>(responseContent, serializerOptions);
string responseContent = string.Empty;
HttpStatusException error;
if (response.Content.Headers.ContentLength > 0)
{
responseContent = await response.Content.ReadAsStringAsync();
error = JsonSerializer.Deserialize<HttpStatusException>(responseContent, serializerOptions);
if (error.StatusCode != response.StatusCode)
{
error.StatusCode = response.StatusCode;
}
}
else
{
error = new HttpStatusException("General", response.ReasonPhrase, response.StatusCode, string.Empty);
}

logger?.LogError($"AccessManagement.UI // {clientMethodName} // Unexpected HttpStatusCode: {response.StatusCode}\n {responseContent}");

throw error;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Altinn.AccessManagement.UI.Core.ClientInterfaces;
using Altinn.AccessManagement.UI.Core.Extensions;
using Altinn.AccessManagement.UI.Core.Models.AccessPackage;
using Altinn.AccessManagement.UI.Core.Services.Interfaces;
using Altinn.AccessManagement.UI.Integration.Configuration;
using Altinn.AccessManagement.UI.Integration.Util;
using Altinn.Platform.Register.Models;
using AltinnCore.Authentication.Utils;
using Microsoft.AspNetCore.Http;
Expand Down Expand Up @@ -41,11 +44,11 @@ public RegisterClient(
ILogger<RegisterClient> logger,
IHttpContextAccessor httpContextAccessor,
IOptions<PlatformSettings> platformSettings,
IAccessTokenProvider accessTokenProvider)
IAccessTokenProvider accessTokenProvider)
{
_logger = logger;
_platformSettings = platformSettings.Value;
httpClient.BaseAddress = new Uri(_platformSettings.ApiRegisterEndpoint);
httpClient.BaseAddress = new Uri(_platformSettings.ApiRegisterEndpoint);
httpClient.DefaultRequestHeaders.Add(_platformSettings.SubscriptionKeyHeaderName, _platformSettings.SubscriptionKey);
_client = httpClient;
_httpContextAccessor = httpContextAccessor;
Expand Down Expand Up @@ -82,6 +85,20 @@ public async Task<Party> GetPartyForOrganization(string organizationNumber)
}
}

/// <inheritdoc/>
public async Task<Party> GetPartyForPerson(string ssn)
{
string endpointUrl = $"parties/lookup";
string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _platformSettings.JwtCookieName);
var accessToken = await _accessTokenProvider.GetAccessToken();

StringContent requestBody = new StringContent(JsonSerializer.Serialize(new PartyLookup { Ssn = ssn }, _serializerOptions), Encoding.UTF8, "application/json");

HttpResponseMessage response = await _client.PostAsync(token, endpointUrl, requestBody, accessToken);

return await ClientUtils.DeserializeIfSuccessfullStatusCode<Party>(response, _logger, "RegisterClient // GetPartyForPerson");
}

/// <inheritdoc/>
public async Task<List<Party>> GetPartyList(List<Guid> uuidList)
{
Expand Down Expand Up @@ -111,6 +128,24 @@ public async Task<List<Party>> GetPartyList(List<Guid> uuidList)
}
}

/// <inheritdoc/>
public async Task<Person> GetPerson(string ssn, string lastname)
{
string endpointUrl = $"persons";
string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _platformSettings.JwtCookieName);
var platformAccessToken = await _accessTokenProvider.GetAccessToken();

var request = new HttpRequestMessage(HttpMethod.Get, endpointUrl);
request.Headers.Add("X-Ai-NationalIdentityNumber", ssn);
request.Headers.Add("X-Ai-LastName", lastname);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
request.Headers.Add("PlatformAccessToken", platformAccessToken);

HttpResponseMessage response = await _client.SendAsync(request);

return await ClientUtils.DeserializeIfSuccessfullStatusCode<Person>(response);
}

/// <inheritdoc/>
public async Task<List<PartyName>> GetPartyNames(IEnumerable<string> orgNumbers, CancellationToken cancellationToken)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"FirstName": "Intelligent",
"MiddleName": "",
"LastName": "Albatross",
"Address": "Gata 2",
"MailingAddress": "[email protected]",
"DateOfBirth": null,
"DateOfDeath": null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"FirstName": "Sitrongul",
"MiddleName": "",
"LastName": "Medaljong",
"Address": "Gata",
"MailingAddress": "[email protected]",
"DateOfBirth": null,
"DateOfDeath": null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"FirstName": "Livsglad",
"MiddleName": "",
"LastName": "Film",
"Address": "Gata 3",
"MailingAddress": "[email protected]",
"DateOfBirth": null,
"DateOfDeath": null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"accessPackages": [],
"services": []
}
Loading

0 comments on commit ea848b6

Please sign in to comment.