From 9d6f3c50e67767efd28c2b232521993dbdeb300e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Dybvik=20Langfors?= Date: Wed, 21 Feb 2024 10:03:43 +0100 Subject: [PATCH 1/4] Update Altinn Authorization integration --- .../Parties/Abstractions/IPartyIdentifier.cs | 1 + .../Parties/Abstractions/PartyIdentifier.cs | 11 +++-- .../NorwegianOrganizationIdentifier.cs | 5 ++- .../Parties/NorwegianPersonIdentifier.cs | 5 ++- .../Parties/SystemUserIdentifier.cs | 5 ++- .../AltinnAuthorizationClient.cs | 18 ++++---- .../Authorization/DecisionRequestHelper.cs | 42 ++++++++++--------- ....Domain.Dialogporten.Infrastructure.csproj | 2 +- .../InfrastructureExtensions.cs | 10 ++--- .../InfrastructureSettings.cs | 1 - .../appsettings.Development.json | 5 +-- .../appsettings.Development.json | 9 ++-- .../appsettings.staging.json | 5 +-- .../appsettings.test.json | 5 +-- .../DecisionRequestHelperTests.cs | 16 +++---- 15 files changed, 73 insertions(+), 67 deletions(-) diff --git a/src/Digdir.Domain.Dialogporten.Domain/Parties/Abstractions/IPartyIdentifier.cs b/src/Digdir.Domain.Dialogporten.Domain/Parties/Abstractions/IPartyIdentifier.cs index 95008fc59..b8a750a3a 100644 --- a/src/Digdir.Domain.Dialogporten.Domain/Parties/Abstractions/IPartyIdentifier.cs +++ b/src/Digdir.Domain.Dialogporten.Domain/Parties/Abstractions/IPartyIdentifier.cs @@ -7,5 +7,6 @@ public interface IPartyIdentifier string FullId { get; } string Id { get; } static abstract string Prefix { get; } + static abstract string PrefixWithSeparator { get; } static abstract bool TryParse(ReadOnlySpan value, [NotNullWhen(true)] out IPartyIdentifier? identifier); } diff --git a/src/Digdir.Domain.Dialogporten.Domain/Parties/Abstractions/PartyIdentifier.cs b/src/Digdir.Domain.Dialogporten.Domain/Parties/Abstractions/PartyIdentifier.cs index c069f0b39..8c0640220 100644 --- a/src/Digdir.Domain.Dialogporten.Domain/Parties/Abstractions/PartyIdentifier.cs +++ b/src/Digdir.Domain.Dialogporten.Domain/Parties/Abstractions/PartyIdentifier.cs @@ -7,7 +7,10 @@ public static class PartyIdentifier { private delegate bool TryParseDelegate(ReadOnlySpan value, [NotNullWhen(true)] out IPartyIdentifier? identifier); private static readonly Dictionary TryParseByPrefix = CreateTryParseByPrefix(); - private const string Separator = "::"; + public const string Separator = "::"; + + public static string Prefix(this IPartyIdentifier identifier) + => identifier.FullId[..(identifier.FullId.IndexOf(identifier.Id, StringComparison.Ordinal) - Separator.Length)]; public static bool TryParse(ReadOnlySpan value, [NotNullWhen(true)] out IPartyIdentifier? identifier) { @@ -41,10 +44,12 @@ private static Dictionary CreateTryParseByPrefix() ( Type: partyIdentifierType, Prefix: (string)partyIdentifierType - .GetProperty(nameof(IPartyIdentifier.Prefix), BindingFlags.Static | BindingFlags.Public)! + .GetProperty(nameof(IPartyIdentifier.PrefixWithSeparator), + BindingFlags.Static | BindingFlags.Public)! .GetValue(null)!, TryParse: partyIdentifierType - .GetMethod(nameof(IPartyIdentifier.TryParse), [typeof(ReadOnlySpan), typeof(IPartyIdentifier).MakeByRefType() + .GetMethod(nameof(IPartyIdentifier.TryParse), [ + typeof(ReadOnlySpan), typeof(IPartyIdentifier).MakeByRefType() ])! .CreateDelegate() )) diff --git a/src/Digdir.Domain.Dialogporten.Domain/Parties/NorwegianOrganizationIdentifier.cs b/src/Digdir.Domain.Dialogporten.Domain/Parties/NorwegianOrganizationIdentifier.cs index 9ac545234..dd5aa78b2 100644 --- a/src/Digdir.Domain.Dialogporten.Domain/Parties/NorwegianOrganizationIdentifier.cs +++ b/src/Digdir.Domain.Dialogporten.Domain/Parties/NorwegianOrganizationIdentifier.cs @@ -8,14 +8,15 @@ namespace Digdir.Domain.Dialogporten.Domain.Parties; public record NorwegianOrganizationIdentifier : IPartyIdentifier { private static readonly int[] OrgNumberWeights = [3, 2, 7, 6, 5, 4, 3, 2]; - public static string Prefix => "urn:altinn:organization:identifier-no::"; + public static string Prefix => "urn:altinn:organization:identifier-no"; + public static string PrefixWithSeparator => Prefix + PartyIdentifier.Separator; public string FullId { get; } public string Id { get; } private NorwegianOrganizationIdentifier(ReadOnlySpan value) { Id = value.ToString(); - FullId = Prefix + Id; + FullId = PrefixWithSeparator + Id; } public static bool TryParse(ReadOnlySpan value, [NotNullWhen(true)] out IPartyIdentifier? identifier) diff --git a/src/Digdir.Domain.Dialogporten.Domain/Parties/NorwegianPersonIdentifier.cs b/src/Digdir.Domain.Dialogporten.Domain/Parties/NorwegianPersonIdentifier.cs index ac7f9db08..645b5703a 100644 --- a/src/Digdir.Domain.Dialogporten.Domain/Parties/NorwegianPersonIdentifier.cs +++ b/src/Digdir.Domain.Dialogporten.Domain/Parties/NorwegianPersonIdentifier.cs @@ -10,14 +10,15 @@ public class NorwegianPersonIdentifier : IPartyIdentifier private static readonly int[] SocialSecurityNumberWeights1 = [3, 7, 6, 1, 8, 9, 4, 5, 2, 1]; private static readonly int[] SocialSecurityNumberWeights2 = [5, 4, 3, 2, 7, 6, 5, 4, 3, 2, 1]; - public static string Prefix => "urn:altinn:person:identifier-no::"; + public static string Prefix => "urn:altinn:person:identifier-no"; + public static string PrefixWithSeparator => Prefix + PartyIdentifier.Separator; public string FullId { get; } public string Id { get; } private NorwegianPersonIdentifier(ReadOnlySpan value) { Id = value.ToString(); - FullId = Prefix + Id; + FullId = PrefixWithSeparator + Id; } public static bool TryParse(ReadOnlySpan value, [NotNullWhen(true)] out IPartyIdentifier? identifier) diff --git a/src/Digdir.Domain.Dialogporten.Domain/Parties/SystemUserIdentifier.cs b/src/Digdir.Domain.Dialogporten.Domain/Parties/SystemUserIdentifier.cs index 3da6c1f3e..7fe7189bb 100644 --- a/src/Digdir.Domain.Dialogporten.Domain/Parties/SystemUserIdentifier.cs +++ b/src/Digdir.Domain.Dialogporten.Domain/Parties/SystemUserIdentifier.cs @@ -5,14 +5,15 @@ namespace Digdir.Domain.Dialogporten.Domain.Parties; public record SystemUserIdentifier : IPartyIdentifier { - public static string Prefix => "urn:altinn:systemuser::"; + public static string Prefix => "urn:altinn:systemuser"; + public static string PrefixWithSeparator => Prefix + PartyIdentifier.Separator; public string FullId { get; } public string Id { get; } private SystemUserIdentifier(ReadOnlySpan value) { Id = value.ToString(); - FullId = Prefix + Id; + FullId = PrefixWithSeparator + Id; } public static bool TryParse(ReadOnlySpan value, [NotNullWhen(true)] out IPartyIdentifier? identifier) diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs index 61520ab28..1c6bd08d6 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/AltinnAuthorizationClient.cs @@ -8,6 +8,7 @@ using Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization; using Digdir.Domain.Dialogporten.Application.Externals.Presentation; using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities; +using Digdir.Domain.Dialogporten.Domain.Parties.Abstractions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -15,8 +16,6 @@ namespace Digdir.Domain.Dialogporten.Infrastructure.Altinn.Authorization; internal sealed class AltinnAuthorizationClient : IAltinnAuthorization { - private const string AttributePidClaim = "urn:altinn:ssn"; - private readonly HttpClient _httpClient; private readonly IUser _user; private readonly IDialogDbContext _db; @@ -81,6 +80,7 @@ private async Task PerformNonScalableDialogSear .Where(dialog => request.ConstraintServiceResources.Contains(dialog.ServiceResource)) .Select(dialog => dialog.Party) .Distinct() + .Take(20) // Limit to 20 parties to limit request size .ToListAsync(cancellationToken: cancellationToken); } @@ -89,7 +89,9 @@ private async Task PerformNonScalableDialogSear request.ConstraintServiceResources = await _db.Dialogs .Where(dialog => request.ConstraintParties.Contains(dialog.Party)) .Select(x => x.ServiceResource) - .Distinct().ToListAsync(cancellationToken: cancellationToken); + .Distinct() + .Take(20) // Limit to 20 resources to limit request size + .ToListAsync(cancellationToken: cancellationToken); } var xacmlJsonRequest = DecisionRequestHelper.NonScalable.CreateDialogSearchRequest(request); @@ -107,20 +109,18 @@ private async Task PerformDialogDetailsAuthori private List GetOrCreateClaimsBasedOnEndUserId(string? endUserId) { List claims = []; - if (endUserId is not null) + if (endUserId is not null && PartyIdentifier.TryParse(endUserId, out var partyIdentifier)) { - claims.Add(new Claim(AttributePidClaim, ExtractEndUserIdNumber(endUserId)!)); + claims.Add(new Claim(partyIdentifier.Prefix(), partyIdentifier.Id)); } else { claims.AddRange(_user.GetPrincipal().Claims); } + return claims; } - private static string ExtractEndUserIdNumber(string endUserId) => - endUserId.Split("::").LastOrDefault() ?? string.Empty; - private static readonly JsonSerializerOptions _serializerOptions = new() { PropertyNameCaseInsensitive = true, @@ -129,7 +129,7 @@ private static string ExtractEndUserIdNumber(string endUserId) => private async Task SendRequest(XacmlJsonRequestRoot xacmlJsonRequest, CancellationToken cancellationToken) { - const string apiUrl = "authorization/api/v1/Decision"; + const string apiUrl = "authorization/api/v1/authorize"; var requestJson = JsonSerializer.Serialize(xacmlJsonRequest, _serializerOptions); _logger.LogDebug("Generated XACML request: {RequestJson}", requestJson); var httpContent = new StringContent(requestJson, Encoding.UTF8, "application/json"); diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/DecisionRequestHelper.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/DecisionRequestHelper.cs index b3377dc42..97bac85e8 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/DecisionRequestHelper.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/DecisionRequestHelper.cs @@ -14,8 +14,6 @@ internal static class DecisionRequestHelper private const string AltinnUrnNsPrefix = "urn:altinn:"; private const string PidClaimType = "pid"; private const string ConsumerClaimType = "consumer"; - private const string AttributeIdSsn = "urn:altinn:ssn"; - private const string AttributeIdOrganizationNumber = "urn:altinn:organizationnumber"; private const string AttributeIdAction = "urn:oasis:names:tc:xacml:1.0:action:action-id"; private const string AttributeIdResource = "urn:altinn:resource"; private const string AttributeIdResourceInstance = "urn:altinn:resourceinstance"; @@ -56,18 +54,20 @@ private static List CreateAccessSubjectCategory(IEnumerable x switch { - { Type: PidClaimType } => new XacmlJsonAttribute { AttributeId = AttributeIdSsn, Value = x.Value }, + { Type: PidClaimType } => new XacmlJsonAttribute { AttributeId = NorwegianPersonIdentifier.Prefix, Value = x.Value }, { Type: var type } when type.StartsWith(AltinnUrnNsPrefix, StringComparison.Ordinal) => new() { AttributeId = type, Value = x.Value }, - { Type: ConsumerClaimType } when x.TryGetOrgNumber(out var organizationNumber) => new() { AttributeId = AttributeIdOrganizationNumber, Value = organizationNumber }, + { Type: ConsumerClaimType } when x.TryGetOrgNumber(out var organizationNumber) => new() { AttributeId = NorwegianOrganizationIdentifier.Prefix, Value = organizationNumber }, _ => null }) .Where(x => x is not null) .Cast() .ToList(); - if (attributes.Any(x => x.AttributeId == AttributeIdSsn)) + // If we're authorizing a person (ie. ID-porten token), we are not interested in the consumer-claim (organization number) + // as that is not relevant for the authorization decision (it's just the organization owning the OAuth client). + if (attributes.Any(x => x.AttributeId == NorwegianPersonIdentifier.Prefix)) { - attributes.RemoveAll(x => x.AttributeId == AttributeIdOrganizationNumber); + attributes.RemoveAll(x => x.AttributeId == NorwegianOrganizationIdentifier.Prefix); } return [new() { Id = SubjectId, Attribute = attributes }]; @@ -108,7 +108,7 @@ private static List CreateResourceCategories( x => x.id); - var partyAttribute = ExtractPartyAttribute(party); + var partyAttribute = GetPartyAttribute(party); return resourceIdByName .Select(x => CreateResourceCategory(x.Value, serviceResource, dialogId, partyAttribute, x.Key)) @@ -161,16 +161,18 @@ private static (string, string) SplitNsAndValue(string serviceResource) return (ns, value); } - private static XacmlJsonAttribute? ExtractPartyAttribute(string party) + private static XacmlJsonAttribute? GetPartyAttribute(string party) { - // TODO: This can be removed once Altinn Auth has been updated to use the new party format. - var _ = PartyIdentifier.TryParse(party, out var partyIdentifier); - return partyIdentifier switch + if (PartyIdentifier.TryParse(party, out var partyIdentifier)) { - NorwegianOrganizationIdentifier => new XacmlJsonAttribute { AttributeId = AttributeIdOrganizationNumber, Value = partyIdentifier.Id }, - NorwegianPersonIdentifier => new() { AttributeId = AttributeIdSsn, Value = partyIdentifier.Id }, - _ => null - }; + return new XacmlJsonAttribute + { + AttributeId = partyIdentifier.Prefix(), + Value = partyIdentifier.Id + }; + } + + return null; } private static XacmlJsonMultiRequests CreateMultiRequests( @@ -253,16 +255,16 @@ public static DialogSearchAuthorizationResult CreateDialogSearchResponse( string party; var partyOrgNr = xamlJsonRequestRoot.Request.Resource.First(r => r.Id == resourceId).Attribute - .FirstOrDefault(a => a.AttributeId == AttributeIdOrganizationNumber); + .FirstOrDefault(a => a.AttributeId == NorwegianOrganizationIdentifier.Prefix); if (partyOrgNr != null) { - party = NorwegianOrganizationIdentifier.Prefix + partyOrgNr.Value; + party = NorwegianOrganizationIdentifier.PrefixWithSeparator + partyOrgNr.Value; } else { var partySsn = xamlJsonRequestRoot.Request.Resource.First(r => r.Id == resourceId).Attribute - .First(a => a.AttributeId == AttributeIdSsn); - party = NorwegianPersonIdentifier.Prefix + partySsn.Value; + .First(a => a.AttributeId == NorwegianPersonIdentifier.Prefix); + party = NorwegianPersonIdentifier.PrefixWithSeparator + partySsn.Value; } if (!response.PartiesByResources.TryGetValue(serviceResource, out var parties)) @@ -286,7 +288,7 @@ private static List CreateResourceCategoriesForSearch( var resourceCounter = 0; foreach (var party in parties) { - var partyAttribute = ExtractPartyAttribute(party); + var partyAttribute = GetPartyAttribute(party); foreach (var serviceResource in serviceResources) { diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Digdir.Domain.Dialogporten.Infrastructure.csproj b/src/Digdir.Domain.Dialogporten.Infrastructure/Digdir.Domain.Dialogporten.Infrastructure.csproj index f8f56065c..d47f27c0b 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Digdir.Domain.Dialogporten.Infrastructure.csproj +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Digdir.Domain.Dialogporten.Infrastructure.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs index 9df59f5aa..09bd688ee 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs @@ -100,12 +100,12 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi client.BaseAddress = services.GetRequiredService>().Value.Altinn.BaseUri) .AddPolicyHandlerFromRegistry(PollyPolicy.DefaultHttpRetryPolicy); - services.AddHttpClient((services, client) => + services.AddMaskinportenHttpClient( + infrastructureConfigurationSection, + x => x.ClientSettings.ExhangeToAltinnToken = true) + .ConfigureHttpClient((services, client) => { - var altinnSettings = services.GetRequiredService>().Value.Altinn; - client.BaseAddress = altinnSettings.BaseUri; - client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", altinnSettings.SubscriptionKey); - client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + client.BaseAddress = services.GetRequiredService>().Value.Altinn.BaseUri; }) // TODO! Add cache policy based on request body .AddPolicyHandlerFromRegistry(PollyPolicy.DefaultHttpRetryPolicy); diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureSettings.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureSettings.cs index 2bf5d2a6e..3f8eb0a7b 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureSettings.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureSettings.cs @@ -17,7 +17,6 @@ public sealed class InfrastructureSettings public sealed class AltinnPlatformSettings { public required Uri BaseUri { get; init; } - public required string SubscriptionKey { get; init; } } public sealed class AltinnCdnPlatformSettings diff --git a/src/Digdir.Domain.Dialogporten.Service/appsettings.Development.json b/src/Digdir.Domain.Dialogporten.Service/appsettings.Development.json index 2ac3202a6..1da5a57bc 100644 --- a/src/Digdir.Domain.Dialogporten.Service/appsettings.Development.json +++ b/src/Digdir.Domain.Dialogporten.Service/appsettings.Development.json @@ -20,15 +20,14 @@ // 2. Client Id/integration as configured in Maskinporten "ClientId": "TODO: Add to local secrets", // 3. Scope(s) requested, space seperated. Must be provisioned on supplied client id. - "Scope": "altinn:events.publish altinn:events.publish.admin", + "Scope": "altinn:events.publish altinn:events.publish.admin altinn:authorization:pdp", // -------------------------- // Any additional settings are specific for the selected client definition type. // See below for examples using other types. "EncodedJwk": "TODO: Add to local secrets" }, "Altinn": { - "BaseUri": "https://platform.tt02.altinn.no/", - "SubscriptionKey": "TODO: Add to local secrets" + "BaseUri": "https://platform.tt02.altinn.no/" }, "AltinnCdn": { "BaseUri": "https://altinncdn.no/" diff --git a/src/Digdir.Domain.Dialogporten.WebApi/appsettings.Development.json b/src/Digdir.Domain.Dialogporten.WebApi/appsettings.Development.json index 9d1400d78..c2282f4c3 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/appsettings.Development.json +++ b/src/Digdir.Domain.Dialogporten.WebApi/appsettings.Development.json @@ -17,7 +17,7 @@ "ClientId": "TODO: Add to local secrets", // 3. Scope(s) requested, space seperated. Must be provisioned on supplied client id. - "Scope": "altinn:events.publish altinn:events.publish.admin", + "Scope": "altinn:events.publish altinn:events.publish.admin altinn:authorization:pdp", // -------------------------- // Any additional settings are specific for the selected client definition type. @@ -25,8 +25,7 @@ "EncodedJwk": "TODO: Add to local secrets" }, "Altinn": { - "BaseUri": "https://platform.tt02.altinn.no/", - "SubscriptionKey": "TODO: Add to local secrets" + "BaseUri": "https://platform.tt02.altinn.no/" }, "AltinnCdn": { "BaseUri": "https://altinncdn.no/" @@ -62,9 +61,9 @@ "LocalDevelopment": { "UseLocalDevelopmentUser": true, "UseLocalDevelopmentResourceRegister": true, - "UseLocalDevelopmentAltinnAuthorization": true, + "UseLocalDevelopmentAltinnAuthorization": false, "UseLocalDevelopmentCloudEventBus": true, "DisableShortCircuitOutboxDispatcher": true, - "DisableAuth": true + "DisableAuth": false } } diff --git a/src/Digdir.Domain.Dialogporten.WebApi/appsettings.staging.json b/src/Digdir.Domain.Dialogporten.WebApi/appsettings.staging.json index a748cc040..a09681bcb 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/appsettings.staging.json +++ b/src/Digdir.Domain.Dialogporten.WebApi/appsettings.staging.json @@ -17,7 +17,7 @@ "ClientId": "TODO: Add to local secrets", // 3. Scope(s) requested, space seperated. Must be provisioned on supplied client id. - "Scope": "altinn:events.publish altinn:events.publish.admin", + "Scope": "altinn:events.publish altinn:events.publish.admin altinn:authorization:pdp", // -------------------------- // Any additional settings are specific for the selected client definition type. @@ -25,8 +25,7 @@ "EncodedJwk": "TODO: Add to local secrets" }, "Altinn": { - "BaseUri": "https://platform.tt02.altinn.no/", - "SubscriptionKey": "TODO: Add to local secrets" + "BaseUri": "https://platform.tt02.altinn.no/" } }, "Application": { diff --git a/src/Digdir.Domain.Dialogporten.WebApi/appsettings.test.json b/src/Digdir.Domain.Dialogporten.WebApi/appsettings.test.json index 433f19157..ec4b233c3 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/appsettings.test.json +++ b/src/Digdir.Domain.Dialogporten.WebApi/appsettings.test.json @@ -17,7 +17,7 @@ "ClientId": "TODO: Add to local secrets", // 3. Scope(s) requested, space seperated. Must be provisioned on supplied client id. - "Scope": "altinn:events.publish altinn:events.publish.admin", + "Scope": "altinn:events.publish altinn:events.publish.admin altinn:authorization:pdp", // -------------------------- // Any additional settings are specific for the selected client definition type. @@ -25,8 +25,7 @@ "EncodedJwk": "TODO: Add to local secrets" }, "Altinn": { - "BaseUri": "https://platform.tt02.altinn.no/", - "SubscriptionKey": "TODO: Add to local secrets" + "BaseUri": "https://platform.tt02.altinn.no/" } }, "Application": { diff --git a/tests/Digdir.Domain.Dialogporten.Infrastructure.Unit.Tests/DecisionRequestHelperTests.cs b/tests/Digdir.Domain.Dialogporten.Infrastructure.Unit.Tests/DecisionRequestHelperTests.cs index 8e348c6ff..6d4eceb31 100644 --- a/tests/Digdir.Domain.Dialogporten.Infrastructure.Unit.Tests/DecisionRequestHelperTests.cs +++ b/tests/Digdir.Domain.Dialogporten.Infrastructure.Unit.Tests/DecisionRequestHelperTests.cs @@ -23,7 +23,7 @@ public void CreateDialogDetailsRequestShouldReturnCorrectRequest() // This should not be copied as subject claim since there's a "pid"-claim ("consumer", ConsumerClaimValue) ), - $"{NorwegianOrganizationIdentifier.Prefix}713330310"); + $"{NorwegianOrganizationIdentifier.PrefixWithSeparator}713330310"); var dialogId = request.DialogId; // Act @@ -41,8 +41,8 @@ public void CreateDialogDetailsRequestShouldReturnCorrectRequest() var accessSubject = result.Request.AccessSubject.First(); Assert.Equal("s1", accessSubject.Id); Assert.Contains(accessSubject.Attribute, a => a.AttributeId == "urn:altinn:foo" && a.Value == "bar"); - Assert.Contains(accessSubject.Attribute, a => a.AttributeId == "urn:altinn:ssn" && a.Value == "12345678901"); - Assert.DoesNotContain(accessSubject.Attribute, a => a.AttributeId == "urn:altinn:organizationnumber"); + Assert.Contains(accessSubject.Attribute, a => a.AttributeId == "urn:altinn:person:identifier-no" && a.Value == "12345678901"); + Assert.DoesNotContain(accessSubject.Attribute, a => a.AttributeId == "urn:altinn:organization:identifier-no"); // Check Action attributes Assert.Contains(result.Request.Action, a => a.Id == "a1" && a.Attribute.Any(attr => attr.AttributeId == "urn:oasis:names:tc:xacml:1.0:action:action-id" && attr.Value == "read")); @@ -55,7 +55,7 @@ public void CreateDialogDetailsRequestShouldReturnCorrectRequest() Assert.NotNull(resource1); Assert.Contains(resource1.Attribute, a => a.AttributeId == "urn:altinn:resource" && a.Value == "some-service"); Assert.Contains(resource1.Attribute, a => a.AttributeId == "urn:altinn:resourceinstance" && a.Value == dialogId.ToString()); - Assert.Contains(resource1.Attribute, a => a.AttributeId == "urn:altinn:organizationnumber" && a.Value == "713330310"); + Assert.Contains(resource1.Attribute, a => a.AttributeId == "urn:altinn:organization:identifier-no" && a.Value == "713330310"); var resource2 = result.Request.Resource.FirstOrDefault(r => r.Id == "r2"); Assert.NotNull(resource2); @@ -83,7 +83,7 @@ public void CreateDialogDetailsRequestShouldReturnCorrectRequestForConsumerOrgAn // Should be copied as subject claim since there's not a "pid"-claim ("consumer", ConsumerClaimValue) ), - $"{NorwegianPersonIdentifier.Prefix}16073422888"); + $"{NorwegianPersonIdentifier.PrefixWithSeparator}16073422888"); // Act var result = DecisionRequestHelper.CreateDialogDetailsRequest(request); @@ -91,12 +91,12 @@ public void CreateDialogDetailsRequestShouldReturnCorrectRequestForConsumerOrgAn // Assert // Check that we have the organizationnumber var accessSubject = result.Request.AccessSubject.First(); - Assert.Contains(accessSubject.Attribute, a => a.AttributeId == "urn:altinn:organizationnumber" && a.Value == "991825827"); + Assert.Contains(accessSubject.Attribute, a => a.AttributeId == "urn:altinn:organization:identifier-no" && a.Value == "991825827"); // Check that we have the ssn attribute as resource owner var resource1 = result.Request.Resource.FirstOrDefault(r => r.Id == "r1"); Assert.NotNull(resource1); - Assert.Contains(resource1.Attribute, a => a.AttributeId == "urn:altinn:ssn" && a.Value == "16073422888"); + Assert.Contains(resource1.Attribute, a => a.AttributeId == "urn:altinn:person:identifier-no" && a.Value == "16073422888"); } [Fact] @@ -108,7 +108,7 @@ public void CreateDialogDetailsResponseShouldReturnCorrectResponse() // Should be copied as subject claim since there's not a "pid"-claim ("consumer", ConsumerClaimValue) ), - $"{NorwegianPersonIdentifier.Prefix}12345678901"); + $"{NorwegianPersonIdentifier.PrefixWithSeparator}12345678901"); // Add an additional action to the request that the mocked response should give a non-permit response for request.AltinnActions.Add(new AltinnAction("failaction", Constants.MainResource)); From 2a2c474ba0c292f8608f1aa4ad082102e792c504 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Dybvik=20Langfors?= Date: Wed, 21 Feb 2024 12:50:49 +0100 Subject: [PATCH 2/4] Revert local auth settings, reintroduce subscription key --- .../InfrastructureSettings.cs | 1 + .../appsettings.Development.json | 3 ++- .../appsettings.Development.json | 7 ++++--- .../appsettings.staging.json | 3 ++- .../appsettings.test.json | 3 ++- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureSettings.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureSettings.cs index 3f8eb0a7b..2bf5d2a6e 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureSettings.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureSettings.cs @@ -17,6 +17,7 @@ public sealed class InfrastructureSettings public sealed class AltinnPlatformSettings { public required Uri BaseUri { get; init; } + public required string SubscriptionKey { get; init; } } public sealed class AltinnCdnPlatformSettings diff --git a/src/Digdir.Domain.Dialogporten.Service/appsettings.Development.json b/src/Digdir.Domain.Dialogporten.Service/appsettings.Development.json index 1da5a57bc..3272c2478 100644 --- a/src/Digdir.Domain.Dialogporten.Service/appsettings.Development.json +++ b/src/Digdir.Domain.Dialogporten.Service/appsettings.Development.json @@ -27,7 +27,8 @@ "EncodedJwk": "TODO: Add to local secrets" }, "Altinn": { - "BaseUri": "https://platform.tt02.altinn.no/" + "BaseUri": "https://platform.tt02.altinn.no/", + "SubscriptionKey": "TODO: Add to local secrets" }, "AltinnCdn": { "BaseUri": "https://altinncdn.no/" diff --git a/src/Digdir.Domain.Dialogporten.WebApi/appsettings.Development.json b/src/Digdir.Domain.Dialogporten.WebApi/appsettings.Development.json index c2282f4c3..1299ea5fb 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/appsettings.Development.json +++ b/src/Digdir.Domain.Dialogporten.WebApi/appsettings.Development.json @@ -25,7 +25,8 @@ "EncodedJwk": "TODO: Add to local secrets" }, "Altinn": { - "BaseUri": "https://platform.tt02.altinn.no/" + "BaseUri": "https://platform.tt02.altinn.no/", + "SubscriptionKey": "TODO: Add to local secrets" }, "AltinnCdn": { "BaseUri": "https://altinncdn.no/" @@ -61,9 +62,9 @@ "LocalDevelopment": { "UseLocalDevelopmentUser": true, "UseLocalDevelopmentResourceRegister": true, - "UseLocalDevelopmentAltinnAuthorization": false, + "UseLocalDevelopmentAltinnAuthorization": true, "UseLocalDevelopmentCloudEventBus": true, "DisableShortCircuitOutboxDispatcher": true, - "DisableAuth": false + "DisableAuth": true } } diff --git a/src/Digdir.Domain.Dialogporten.WebApi/appsettings.staging.json b/src/Digdir.Domain.Dialogporten.WebApi/appsettings.staging.json index a09681bcb..4c976f84e 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/appsettings.staging.json +++ b/src/Digdir.Domain.Dialogporten.WebApi/appsettings.staging.json @@ -25,7 +25,8 @@ "EncodedJwk": "TODO: Add to local secrets" }, "Altinn": { - "BaseUri": "https://platform.tt02.altinn.no/" + "BaseUri": "https://platform.tt02.altinn.no/", + "SubscriptionKey": "TODO: Add to local secrets" } }, "Application": { diff --git a/src/Digdir.Domain.Dialogporten.WebApi/appsettings.test.json b/src/Digdir.Domain.Dialogporten.WebApi/appsettings.test.json index ec4b233c3..614fc8f35 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/appsettings.test.json +++ b/src/Digdir.Domain.Dialogporten.WebApi/appsettings.test.json @@ -25,7 +25,8 @@ "EncodedJwk": "TODO: Add to local secrets" }, "Altinn": { - "BaseUri": "https://platform.tt02.altinn.no/" + "BaseUri": "https://platform.tt02.altinn.no/", + "SubscriptionKey": "TODO: Add to local secrets" } }, "Application": { From 1c14d42aaf9e6a91d0e0dc31ee4079b7b542aa5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Dybvik=20Langfors?= Date: Mon, 11 Mar 2024 11:21:24 +0100 Subject: [PATCH 3/4] Re-add APIM key auth, handle possible index out of range exceptions --- .../Altinn/Authorization/DecisionRequestHelper.cs | 2 +- .../InfrastructureExtensions.cs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/DecisionRequestHelper.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/DecisionRequestHelper.cs index 97bac85e8..84d4a8b50 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/DecisionRequestHelper.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/Authorization/DecisionRequestHelper.cs @@ -243,7 +243,7 @@ public static DialogSearchAuthorizationResult CreateDialogSearchResponse( for (var i = 0; i < xamlJsonRequestRoot.Request.MultiRequests.RequestReference.Count; i++) { - if (xamlJsonResponse.Response[i].Decision != PermitResponse) + if (i >= xamlJsonResponse.Response.Count || xamlJsonResponse.Response[i].Decision != PermitResponse) { continue; } diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs index 09bd688ee..c61df095a 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs @@ -105,7 +105,9 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi x => x.ClientSettings.ExhangeToAltinnToken = true) .ConfigureHttpClient((services, client) => { - client.BaseAddress = services.GetRequiredService>().Value.Altinn.BaseUri; + var altinnSettings = services.GetRequiredService>().Value.Altinn; + client.BaseAddress = altinnSettings.BaseUri; + client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", altinnSettings.SubscriptionKey); }) // TODO! Add cache policy based on request body .AddPolicyHandlerFromRegistry(PollyPolicy.DefaultHttpRetryPolicy); From 87a311ec4175c1a56ab7e0c0045e18ecba3954ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Dybvik=20Langfors?= Date: Mon, 11 Mar 2024 19:49:41 +0100 Subject: [PATCH 4/4] Update e2e tests --- tests/k6/common/token.js | 2 +- tests/k6/tests/enduser/dialogSearch.js | 22 +++++++++---------- tests/k6/tests/serviceowner/dialogSearch.js | 8 +++---- .../serviceowner/testdata/01-create-dialog.js | 4 ++-- 4 files changed, 17 insertions(+), 19 deletions(-) diff --git a/tests/k6/common/token.js b/tests/k6/common/token.js index 3b7acfebc..9b1574f38 100644 --- a/tests/k6/common/token.js +++ b/tests/k6/common/token.js @@ -63,7 +63,7 @@ export function getServiceOwnerTokenFromGenerator(tokenOptions = null) { export function getEnduserTokenFromGenerator(tokenOptions = null) { let fullTokenOptions = extend({}, defaultTokenOptions, tokenOptions); - const url = `http://altinn-testtools-token-generator.azurewebsites.net/api/GetPersonalToken?env=tt02&scopes=${encodeURIComponent(fullTokenOptions.scopes)}&pid=${fullTokenOptions.ssn}&userId=123&partyId=123&ttl=${tokenTtl}`; + const url = `http://altinn-testtools-token-generator.azurewebsites.net/api/GetPersonalToken?env=tt02&scopes=${encodeURIComponent(fullTokenOptions.scopes)}&pid=${fullTokenOptions.ssn}&ttl=${tokenTtl}`; return fetchToken(url, fullTokenOptions, `end user (ssn:${fullTokenOptions.ssn})`); } diff --git a/tests/k6/tests/enduser/dialogSearch.js b/tests/k6/tests/enduser/dialogSearch.js index 7bf30b886..7417df345 100644 --- a/tests/k6/tests/enduser/dialogSearch.js +++ b/tests/k6/tests/enduser/dialogSearch.js @@ -8,8 +8,6 @@ import { setSenderName, setStatus, setExtendedStatus, - setServiceResource, - setParty, setDueAt, setExpiresAt, setVisibleFrom, @@ -17,6 +15,8 @@ import { putSO, deleteSO } from '../../common/testimports.js' +import { defaultEndUserOrgNo } from "../../common/config.js"; + import { default as dialogToInsert } from '../serviceowner/testdata/01-create-dialog.js'; export default function () { @@ -30,21 +30,21 @@ export default function () { let extendedStatusToSearchFor = "status:" + uuidv4(); let secondExtendedStatusToSearchFor = "status:" + uuidv4(); let senderNameToSearchFor = uuidv4() - let enduserParty = "urn:altinn:person:identifier-no::07874299582"; - let auxResource = "urn:altinn:resource:ttd-altinn-events-automated-tests"; // Note! We assume that this exists! + let enduserParty = "urn:altinn:organization:identifier-no::" + defaultEndUserOrgNo; + let resource = "urn:altinn:resource:ttd-dialogporten-automated-tests"; // Note! We assume that this exists! let titleForDueAtItem = "due_" + uuidv4(); let titleForExpiresAtItem = "expires_" + uuidv4(); let titleForUpdatedItem = "updated_" + uuidv4(); let titleForLastItem = "last_" + uuidv4(); let createdAfter = (new Date()).toISOString(); // We use this on all tests to hopefully avoid clashing with unrelated dialogs let defaultFilter = "?CreatedAfter=" + createdAfter + "&Party=" + enduserParty; + const numberOfDialogs = 11; describe('Arrange: Create some dialogs to test against', () => { - for (let i = 0; i < 11; i++) { + for (let i = 0; i < numberOfDialogs; i++) { let d = dialogToInsert(); setTitle(d, "e2e-test-dialog eu #" + (i+1), "nn_NO"); - setParty(d, enduserParty); setVisibleFrom(d, null); dialogs.push(d); } @@ -58,9 +58,6 @@ export default function () { setSenderName(dialogs[++d], senderNameToSearchFor); setExtendedStatus(dialogs[d], secondExtendedStatusToSearchFor); - - setServiceResource(dialogs[++d], auxResource); - setParty(dialogs[++d], enduserParty); setTitle(dialogs[++d], titleForDueAtItem); setDueAt(dialogs[d], new Date("2033-12-07T10:13:00Z")); @@ -155,11 +152,11 @@ export default function () { }); describe('List with resource filter', () => { - let r = getEU('dialogs/' + defaultFilter + '&ServiceResource=' + auxResource); + let r = getEU('dialogs/' + defaultFilter + '&ServiceResource=' + resource); expectStatusFor(r).to.equal(200); expect(r, 'response').to.have.validJsonBody(); - expect(r.json(), 'response json').to.have.property("items").with.lengthOf(1); - expect(r.json().items[0], 'party').to.have.property("serviceResource").that.equals(auxResource); + expect(r.json(), 'response json').to.have.property("items").with.lengthOf(numberOfDialogs); + expect(r.json().items[0], 'party').to.have.property("serviceResource").that.equals(resource); }); describe("Cleanup", () => { @@ -173,4 +170,5 @@ export default function () { let r = getEU('dialogs/' + dialogIds[0]); expectStatusFor(r).to.equal(410); }); + } \ No newline at end of file diff --git a/tests/k6/tests/serviceowner/dialogSearch.js b/tests/k6/tests/serviceowner/dialogSearch.js index e53294d1f..19ffa7e7e 100644 --- a/tests/k6/tests/serviceowner/dialogSearch.js +++ b/tests/k6/tests/serviceowner/dialogSearch.js @@ -19,7 +19,7 @@ import { deleteSO } from '../../common/testimports.js' import { default as dialogToInsert } from './testdata/01-create-dialog.js'; -import { defaultEndUserOrgNo } from '../../common/config.js' +import { defaultEndUserSsn } from '../../common/config.js' export default function () { @@ -32,8 +32,8 @@ export default function () { let extendedStatusToSearchFor = "status:" + uuidv4(); let secondExtendedStatusToSearchFor = "status:" + uuidv4(); let senderNameToSearchFor = uuidv4() - let auxParty = "urn:altinn:organization:identifier-no::" + defaultEndUserOrgNo; // A party other than ourselves that we authorized for - let auxResource = "urn:altinn:resource:ttd-dialogporten-automated-tests"; // This must exist in Resource Registry + let auxParty = "urn:altinn:person:identifier-no::" + defaultEndUserSsn; + let auxResource = "urn:altinn:resource:ttd-dialogporten-automated-tests-2"; // This must exist in Resource Registry let titleForDueAtItem = uuidv4(); let titleForExpiresAtItem = uuidv4(); let titleForVisibleFromItem = uuidv4(); @@ -162,7 +162,7 @@ export default function () { expectStatusFor(r).to.equal(200); expect(r, 'response').to.have.validJsonBody(); expect(r.json(), 'response json').to.have.property("items").with.lengthOf(1); - expect(r.json().items[0], 'party').to.have.property("party").that.equals(auxParty); + expect(r.json().items[0], 'party').to.have.property("party").that.contains(defaultEndUserSsn); }); describe('List with resource filter', () => { diff --git a/tests/k6/tests/serviceowner/testdata/01-create-dialog.js b/tests/k6/tests/serviceowner/testdata/01-create-dialog.js index a0d85e6cb..ffc4c35d0 100644 --- a/tests/k6/tests/serviceowner/testdata/01-create-dialog.js +++ b/tests/k6/tests/serviceowner/testdata/01-create-dialog.js @@ -1,12 +1,12 @@ import { uuidv4 } from '../../../common/testimports.js' -import { defaultEndUserSsn } from "../../../common/config.js"; +import { defaultEndUserOrgNo } from "../../../common/config.js"; export default function () { let dialogElementId = uuidv4(); return { "serviceResource": "urn:altinn:resource:ttd-dialogporten-automated-tests", // urn starting with urn:altinn:resource: - "party": "urn:altinn:person:identifier-no::" + defaultEndUserSsn, // or urn:altinn:organization:identifier-no::<9 digits> + "party": "urn:altinn:organization:identifier-no::" + defaultEndUserOrgNo, "status": "new", // valid values: new, inprogress, waiting, signing, cancelled, completed "extendedStatus": "urn:any/valid/uri", "dueAt": "2033-11-25T06:37:54.2920190Z", // must be UTC