Skip to content

Commit 20e208d

Browse files
HauklandJivarne
authored andcommitted
feat: support email user instantiation (#1657)
1 parent 5c1818b commit 20e208d

46 files changed

Lines changed: 658 additions & 60 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<PackageVersion Include="Altinn.Common.EFormidlingClient" Version="1.3.3" />
1010
<PackageVersion Include="Altinn.Common.PEP" Version="4.2.2" />
1111
<PackageVersion Include="Altinn.Platform.Models" Version="1.6.1" />
12-
<PackageVersion Include="Altinn.Platform.Storage.Interface" Version="4.3.0" />
12+
<PackageVersion Include="Altinn.Platform.Storage.Interface" Version="4.4.0" />
1313
<PackageVersion Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.4.0" />
1414
<PackageVersion Include="Azure.Identity" Version="1.17.1" />
1515
<PackageVersion Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.6.0" />

src/Altinn.App.Api/Controllers/InstancesController.cs

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Globalization;
22
using System.Net;
33
using System.Text;
4+
using System.Text.Encodings.Web;
45
using Altinn.App.Api.Extensions;
56
using Altinn.App.Api.Helpers.Patch;
67
using Altinn.App.Api.Helpers.RequestHandling;
@@ -254,7 +255,13 @@ public async Task<ActionResult<InstanceResponse>> Post(
254255

255256
if (
256257
lookup == null
257-
|| (lookup.PersonNumber == null && lookup.OrganisationNumber == null && lookup.PartyId == null)
258+
|| (
259+
lookup.PersonNumber == null
260+
&& lookup.OrganisationNumber == null
261+
&& lookup.PartyId == null
262+
&& lookup.ExternalIdentifier == null
263+
&& lookup.Username == null
264+
)
258265
)
259266
{
260267
return BadRequest(
@@ -291,7 +298,10 @@ public async Task<ActionResult<InstanceResponse>> Post(
291298
try
292299
{
293300
party = await LookupParty(instanceTemplate.InstanceOwner) ?? throw new Exception("Unknown party");
294-
instanceTemplate.InstanceOwner = InstantiationHelper.PartyToInstanceOwner(party);
301+
instanceTemplate.InstanceOwner = await InstantiationHelper.PartyToInstanceOwner(
302+
party,
303+
_authenticationContext
304+
);
295305
}
296306
catch (Exception partyLookupException)
297307
{
@@ -479,7 +489,13 @@ public async Task<ActionResult<InstanceResponse>> PostSimplified(
479489

480490
if (
481491
lookup == null
482-
|| (lookup.PersonNumber == null && lookup.OrganisationNumber == null && lookup.PartyId == null)
492+
|| (
493+
lookup.PersonNumber == null
494+
&& lookup.OrganisationNumber == null
495+
&& lookup.PartyId == null
496+
&& lookup.ExternalIdentifier == null
497+
&& lookup.Username == null
498+
)
483499
)
484500
{
485501
return BadRequest(
@@ -491,7 +507,10 @@ public async Task<ActionResult<InstanceResponse>> PostSimplified(
491507
try
492508
{
493509
party = await LookupParty(instansiationInstance.InstanceOwner) ?? throw new Exception("Unknown party");
494-
instansiationInstance.InstanceOwner = InstantiationHelper.PartyToInstanceOwner(party);
510+
instansiationInstance.InstanceOwner = await InstantiationHelper.PartyToInstanceOwner(
511+
party,
512+
_authenticationContext
513+
);
495514
}
496515
catch (Exception partyLookupException)
497516
{
@@ -1132,6 +1151,18 @@ string action
11321151
string personOrOrganisationNumber = instanceOwner.PersonNumber ?? instanceOwner.OrganisationNumber;
11331152
try
11341153
{
1154+
if (!string.IsNullOrEmpty(instanceOwner.ExternalIdentifier))
1155+
{
1156+
var partyId = await _altinnPartyClient.GetPartyIdByUrn(instanceOwner.ExternalIdentifier);
1157+
if (partyId == null)
1158+
{
1159+
throw new ServiceException(
1160+
HttpStatusCode.BadRequest,
1161+
$"Failed to lookup party by external identifier: {instanceOwner.ExternalIdentifier}. No partyId found for the provided external identifier."
1162+
);
1163+
}
1164+
return await _registerClient.GetPartyUnchecked(partyId.Value, this.HttpContext.RequestAborted);
1165+
}
11351166
if (!string.IsNullOrEmpty(instanceOwner.PersonNumber))
11361167
{
11371168
lookupNumber = "personNumber";
@@ -1144,6 +1175,22 @@ string action
11441175
new PartyLookup { OrgNo = instanceOwner.OrganisationNumber }
11451176
);
11461177
}
1178+
else if (!string.IsNullOrEmpty(instanceOwner.Username))
1179+
{
1180+
var email = instanceOwner.Username.StartsWith("epost:", StringComparison.InvariantCultureIgnoreCase)
1181+
? instanceOwner.Username[6..]
1182+
: instanceOwner.Username;
1183+
var urn = $"{AltinnUrns.SelfIdentifiedEmail}:{UrlEncoder.Default.Encode(email)}";
1184+
var partyId = await _altinnPartyClient.GetPartyIdByUrn(urn);
1185+
if (partyId == null)
1186+
{
1187+
throw new ServiceException(
1188+
HttpStatusCode.BadRequest,
1189+
$"Failed to lookup party by username: {instanceOwner.Username}. No partyId found for the provided idporten self identified email address."
1190+
);
1191+
}
1192+
return await _registerClient.GetPartyUnchecked(partyId.Value, this.HttpContext.RequestAborted);
1193+
}
11471194
else
11481195
{
11491196
throw new ServiceException(

src/Altinn.App.Api/Models/InstanceResponse.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ internal static InstanceResponse From(Instance instance, Party instanceOwnerPart
113113
PersonNumber = instance.InstanceOwner.PersonNumber,
114114
OrganisationNumber = instance.InstanceOwner.OrganisationNumber,
115115
Username = instance.InstanceOwner.Username,
116+
ExternalIdentifier = instance.InstanceOwner.ExternalIdentifier,
116117
Party = PartyResponse.From(instanceOwnerParty),
117118
},
118119
AppId = instance.AppId,
@@ -159,6 +160,11 @@ public sealed class InstanceOwnerResponse
159160
/// </summary>
160161
public required string Username { get; init; }
161162

163+
/// <summary>
164+
/// The external identifier of a self identified party. Null if the party is not self identified.
165+
/// </summary>
166+
public string ExternalIdentifier { get; init; }
167+
162168
/// <summary>
163169
/// Party information for the instance owner.
164170
/// </summary>

src/Altinn.App.Core/Constants/AltinnUrns.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ internal static class AltinnUrns
1616
public const string PersonId = "urn:altinn:person:identifier-no";
1717
public const string UserId = "urn:altinn:userid";
1818
public const string UserName = "urn:altinn:username";
19+
public const string SelfIdentifiedEmail = "urn:altinn:person:idporten-email";
1920
public const string PartyId = "urn:altinn:partyid";
21+
public const string PartyUuid = "urn:altinn:partyuuid";
2022
public const string RepresentingPartyId = "urn:altinn:representingpartyid";
2123
public const string App = "urn:altinn:app";
2224
public const string AppResource = "urn:altinn:appresource";

src/Altinn.App.Core/Helpers/InstantiationHelper.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System.Globalization;
2+
using Altinn.App.Core.Features.Auth;
3+
using Altinn.Platform.Profile.Models;
24
using Altinn.Platform.Register.Enums;
35
using Altinn.Platform.Register.Models;
46
using Altinn.Platform.Storage.Interface.Models;
@@ -217,4 +219,61 @@ public static InstanceOwner PartyToInstanceOwner(Party party)
217219
// instanceOwnerPartyType == "unknown"
218220
};
219221
}
222+
223+
/// <summary>
224+
/// Get the correct <see cref="InstanceOwner" /> object from the <see cref="Party" /> object of the entity that should own the instance
225+
/// Use authenticationContext to get the external identity for self identified parties
226+
/// </summary>
227+
public static async Task<InstanceOwner> PartyToInstanceOwner(
228+
Party party,
229+
IAuthenticationContext authenticationContext
230+
)
231+
{
232+
if (!string.IsNullOrEmpty(party.SSN))
233+
{
234+
return new() { PartyId = party.PartyId.ToString(CultureInfo.InvariantCulture), PersonNumber = party.SSN };
235+
}
236+
else if (!string.IsNullOrEmpty(party.OrgNumber))
237+
{
238+
return new()
239+
{
240+
PartyId = party.PartyId.ToString(CultureInfo.InvariantCulture),
241+
OrganisationNumber = party.OrgNumber,
242+
};
243+
}
244+
else if (party.PartyTypeName.Equals(PartyType.SelfIdentified))
245+
{
246+
string? externalIdentifier = null;
247+
if (authenticationContext is not null)
248+
{
249+
externalIdentifier = await GetExternalIdentityForSelfIdentifiedParty(party, authenticationContext);
250+
}
251+
return new()
252+
{
253+
PartyId = party.PartyId.ToString(CultureInfo.InvariantCulture),
254+
Username = party.Name,
255+
ExternalIdentifier = externalIdentifier,
256+
};
257+
}
258+
return new()
259+
{
260+
PartyId = party.PartyId.ToString(CultureInfo.InvariantCulture),
261+
// instanceOwnerPartyType == "unknown"
262+
};
263+
}
264+
265+
internal static async Task<string?> GetExternalIdentityForSelfIdentifiedParty(
266+
Party party,
267+
IAuthenticationContext authenticationContext
268+
)
269+
{
270+
if (party.PartyTypeName != PartyType.SelfIdentified)
271+
return null;
272+
273+
if (authenticationContext.Current is not Authenticated.User user)
274+
return null;
275+
276+
UserProfile profile = await user.LookupProfile();
277+
return profile.ExternalIdentity;
278+
}
220279
}

src/Altinn.App.Core/Infrastructure/Clients/Events/EventsClient.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,14 @@ public async Task<string> AddEvent(string eventType, Instance instance)
7373
{
7474
alternativeSubject = $"/org/{instance.InstanceOwner.OrganisationNumber}";
7575
}
76-
77-
if (!string.IsNullOrWhiteSpace(instance.InstanceOwner.PersonNumber))
76+
else if (!string.IsNullOrWhiteSpace(instance.InstanceOwner.PersonNumber))
7877
{
7978
alternativeSubject = $"/person/{instance.InstanceOwner.PersonNumber}";
8079
}
80+
else if (!string.IsNullOrWhiteSpace(instance.InstanceOwner.ExternalIdentifier))
81+
{
82+
alternativeSubject = instance.InstanceOwner.ExternalIdentifier;
83+
}
8184

8285
var baseUrl = _generalSettings.FormattedExternalAppBaseUrl(new AppIdentifier(instance));
8386

src/Altinn.App.Core/Infrastructure/Clients/Register/AltinnPartyClient.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Net;
22
using System.Net.Http.Headers;
3+
using System.Text.Json;
34
using Altinn.App.Core.Configuration;
45
using Altinn.App.Core.Constants;
56
using Altinn.App.Core.Extensions;
@@ -129,4 +130,48 @@ await response.Content.ReadAsStringAsync()
129130

130131
throw await PlatformHttpException.CreateAsync(response);
131132
}
133+
134+
/// <inheritdoc/>
135+
public async Task<int?> GetPartyIdByUrn(string urn)
136+
{
137+
using var activity = _telemetry?.StartLookupPartyActivity();
138+
string endpointUrl = "access-management/parties/query";
139+
var query = new { data = new string[] { urn } };
140+
using var content = new StringContent(JsonSerializer.Serialize(query));
141+
content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
142+
ApplicationMetadata application = await _appMetadata.GetApplicationMetadata();
143+
144+
string token = _userTokenProvider.GetUserToken();
145+
146+
using HttpResponseMessage response = await _client.PostAsync(
147+
token,
148+
endpointUrl,
149+
content,
150+
_accessTokenGenerator.GenerateAccessToken(application.Org, application.AppIdentifier.App)
151+
);
152+
if (response.StatusCode != HttpStatusCode.OK)
153+
{
154+
_logger.LogError(
155+
"// Getting partyId by URN {Urn} failed with statuscode {StatusCode} - {Reason}",
156+
urn,
157+
response.StatusCode,
158+
await response.Content.ReadAsStringAsync()
159+
);
160+
throw await PlatformHttpException.CreateAsync(response);
161+
}
162+
163+
using var responseDocument = JsonDocument.Parse(await response.Content.ReadAsByteArrayAsync());
164+
var listResponse = responseDocument.RootElement.GetProperty("data");
165+
if (listResponse.GetArrayLength() == 0)
166+
{
167+
return null;
168+
}
169+
if (listResponse.GetArrayLength() > 1)
170+
{
171+
throw new InvalidOperationException($"Multiple parties found for URN {urn}");
172+
}
173+
174+
var partyId = listResponse[0].GetProperty("partyId").GetInt32();
175+
return partyId;
176+
}
132177
}

src/Altinn.App.Core/Internal/Expressions/LayoutEvaluatorState.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -224,16 +224,19 @@ public string GetInstanceContext(string key)
224224
?? throw new InvalidOperationException("InstanceOwner or PartyId is null"),
225225
"appId" => Instance.AppId ?? throw new InvalidOperationException("AppId is null"),
226226
"instanceId" => Instance.Id ?? throw new InvalidOperationException("InstanceId is null"),
227-
"instanceOwnerPartyType" => (
228-
!string.IsNullOrWhiteSpace(Instance.InstanceOwner?.OrganisationNumber) ? "org"
229-
: !string.IsNullOrWhiteSpace(Instance.InstanceOwner?.PersonNumber) ? "person"
230-
: !string.IsNullOrWhiteSpace(Instance.InstanceOwner?.Username) ? "selfIdentified"
231-
: "unknown"
232-
),
227+
"instanceOwnerPartyType" => GetInstanceOwnerPartyType(Instance.InstanceOwner),
233228
_ => throw new ExpressionEvaluatorTypeErrorException($"Unknown Instance context property {key}"),
234229
};
235230
}
236231

232+
private static string GetInstanceOwnerPartyType(InstanceOwner? instanceOwner)
233+
{
234+
return !string.IsNullOrWhiteSpace(instanceOwner?.OrganisationNumber) ? "org"
235+
: !string.IsNullOrWhiteSpace(instanceOwner?.PersonNumber) ? "person"
236+
: !string.IsNullOrWhiteSpace(instanceOwner?.Username) ? "selfIdentified"
237+
: "unknown";
238+
}
239+
237240
/// <summary>
238241
/// Count the number of data elements of a specific type
239242
/// </summary>

src/Altinn.App.Core/Internal/Registers/IAltinnPartyClient.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,11 @@ public interface IAltinnPartyClient
2020
/// <param name="partyLookup">A populated lookup object with information about what to look for.</param>
2121
/// <returns>The party lookup containing either SSN or organisation number.</returns>
2222
Task<Party> LookupParty(PartyLookup partyLookup);
23+
24+
/// <summary>
25+
/// Looks up a partyId by a URN. The URN should be in the format urn:altinn:personnumber:12345678901 or urn:altinn:orgnumber:987654321.
26+
/// </summary>
27+
/// <param name="urn">The URN to look up.</param>
28+
/// <returns>The partyId for the given URN, or null if not found.</returns>
29+
Task<int?> GetPartyIdByUrn(string urn);
2330
}

src/Altinn.App.Core/Models/CloudEvent.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ public class CloudEvent
5151
/// Gets or sets the alternative subject of the event.
5252
/// </summary>
5353
[JsonPropertyName("alternativesubject")]
54-
#nullable disable
55-
public string AlternativeSubject { get; set; }
54+
public string? AlternativeSubject { get; set; }
5655

56+
#nullable disable
5757
/// <summary>
5858
/// Gets or sets the cloudEvent data content. The event payload.
5959
/// The payload depends on the type and the dataschema.

0 commit comments

Comments
 (0)