From 60f6c969bda0bcad15ae1e0e0fbaeca9f20b378e Mon Sep 17 00:00:00 2001 From: Martin Othamar Date: Thu, 6 Feb 2025 13:14:46 +0100 Subject: [PATCH] `IAuthenticationContext` for current auth information, systemuser support (#880) --- src/Altinn.App.Api/Altinn.App.Api.csproj | 2 +- .../Controllers/ActionsController.cs | 15 +- .../Controllers/AuthenticationController.cs | 1 - .../Controllers/AuthorizationController.cs | 198 ++-- .../Controllers/DataController.cs | 38 +- .../Controllers/PartiesController.cs | 328 ++++-- .../Controllers/ProfileController.cs | 38 +- .../Controllers/StatelessDataController.cs | 70 +- .../UserDefinedMetadataController.cs | 12 +- .../Extensions/ServiceCollectionExtensions.cs | 5 + .../Helpers/DataElementAccessChecker.cs | 82 +- .../TelemetryEnrichingMiddleware.cs | 62 +- .../Telemetry/IdentityTelemetryFilter.cs | 7 + src/Altinn.App.Core/Altinn.App.Core.csproj | 7 +- .../Configuration/GeneralSettings.cs | 2 + .../Extensions/InstanceEventExtensions.cs | 5 + .../Extensions/ProcessStateExtensions.cs | 2 + .../Extensions/ServiceCollectionExtensions.cs | 5 +- .../Features/Action/SigningUserAction.cs | 49 +- .../Action/UniqueSignatureAuthorizer.cs | 21 +- .../Action/UserActionAuthorizerContext.cs | 14 +- .../Features/Auth/Authenticated.cs | 968 ++++++++++++++++++ .../Features/Auth/AuthenticationContext.cs | 89 ++ .../Features/Auth/AuthenticationContextDI.cs | 12 + .../Auth/AuthenticationContextException.cs | 14 + .../Features/Auth/IAuthenticationContext.cs | 12 + src/Altinn.App.Core/Features/Auth/Scopes.cs | 174 ++++ .../Features/Auth/TokenIssuer.cs | 37 + .../Features/Cache/AppConfigurationCache.cs | 109 ++ .../Features/Cache/AppConfigurationCacheDI.cs | 15 + .../Features/Cache/IAppConfigurationCache.cs | 8 + .../Features/Telemetry/Telemetry.cs | 25 + .../Telemetry/TelemetryActivityExtensions.cs | 65 +- .../Helpers/InstantiationHelper.cs | 7 +- src/Altinn.App.Core/Helpers/UserHelper.cs | 1 + .../Implementation/PrefillSI.cs | 52 +- .../Authentication/AuthenticationClient.cs | 15 +- .../Clients/Storage/SignClient.cs | 1 + .../Internal/Auth/AuthorizationService.cs | 13 +- .../Internal/LocaltestValidation.cs | 8 +- .../Internal/Pdf/PdfService.cs | 47 +- .../Process/Elements/AppProcessElementInfo.cs | 2 + .../Internal/Process/ProcessEngine.cs | 142 +-- .../Internal/Sign/SignatureContext.cs | 5 + .../Models/OrganisationNumber.cs | 5 +- .../Models/UserAction/UserActionContext.cs | 31 +- .../Controllers/ActionsControllerTests.cs | 34 +- .../Conventions/EnumSerializationTests.cs | 2 +- ...alidTestValue_ReturnsConflict.verified.txt | 18 +- .../Controllers/DataControllerTests.cs | 10 +- .../DataController_LayoutEvaluatorTests.cs | 2 +- .../Controllers/DataController_PatchTests.cs | 8 +- .../Controllers/DataController_PostTests.cs | 6 +- .../Controllers/DataController_PutTests.cs | 4 +- .../DataController_UserAccessTests.cs | 58 +- .../EventsReceiverControllerTests.cs | 4 +- .../InstancesController_CopyInstanceTests.cs | 24 +- ...ostNewInstance_Simplified_Org.verified.txt | 54 + ...Simplified_SelfIdentifiedUser.verified.txt | 57 ++ ...tance_Simplified_ServiceOwner.verified.txt | 57 ++ ...nstance_Simplified_SystemUser.verified.txt | 57 ++ ...stNewInstance_Simplified_User.verified.txt | 60 ++ ...nstancesController_PostNewInstanceTests.cs | 50 +- .../LookupOrganisationControllerTests.cs | 2 +- .../LookupPersonControllerTests.cs | 2 +- .../Controllers/PdfControllerTests.cs | 59 +- ...dator_ReturnsValidationErrors.verified.txt | 17 +- ...sNext_PdfFails_DataIsUnlocked.verified.txt | 15 +- .../Controllers/ProcessControllerTests.cs | 22 +- .../StatelessDataControllerTests.cs | 60 +- .../UserDefinedMetadataControllerTests.cs | 16 +- ...alidateController_ValidateInstanceTests.cs | 4 +- .../CustomWebApplicationFactory.cs | 25 +- .../Data/Register/Party/5001337.json | 13 + .../config/applicationmetadata.json | 2 +- .../config/authorization/policy.xml | 2 +- .../apps/tdd/permissive-app/appsettings.json | 35 + .../config/applicationmetadata.json | 105 ++ .../config/authorization/policy.xml | 289 ++++++ .../config/process/process.bpmn | 50 + .../config/texts/resource.nb.json | 169 +++ .../apps/tdd/permissive-app/models/Skjema.cs | 144 +++ .../models/default.validation.json | 18 + .../options/fileSourceOptions.json | 24 + .../permissive-app/ui/default/Settings.json | 8 + .../ui/default/layouts/page.json | 31 + .../tdd/permissive-app/ui/layout-sets.json | 10 + .../Helpers/DataElementAccessCheckerTests.cs | 50 +- .../Helpers/UserHelperTest.cs | 14 +- ...Should_Always_Be_A_Root_Trace.verified.txt | 15 +- ...ys_Be_A_Root_Trace_Unless_Pdf.verified.txt | 15 +- ...ave_Root_AspNetCore_Trace_Org.verified.txt | 19 +- ...ve_Root_AspNetCore_Trace_User.verified.txt | 15 +- .../TelemetryEnrichingMiddlewareTests.cs | 8 +- .../Mocks/AppConfigurationCacheMock.cs | 12 + .../Mocks/AuthorizationMock.cs | 3 +- ...angeDetection.SaveJsonSwagger.verified.txt | 5 +- .../Altinn.App.Api.Tests/OpenApi/swagger.json | 3 +- test/Altinn.App.Api.Tests/Program.cs | 7 +- .../Utils/PrincipalUtil.cs | 167 --- .../Utils/TestAuthentication.cs | 578 +++++++++++ .../Features/Action/SigningUserActionTests.cs | 183 ++-- .../Action/UniqueSignatureAuthorizerTests.cs | 34 +- .../Features/Auth/AuthenticatedTests.cs | 182 ++++ .../Features/Auth/AuthenticationInfoTests.cs | 42 + .../Features/Auth/ScopesTests.cs | 86 ++ .../CorrespondenceClientTests.cs | 3 +- .../MaskinportenDelegatingHandlerTest.cs | 2 +- .../Maskinporten/MaskinportenClientTest.cs | 19 +- .../Features/Maskinporten/TestHelpers.cs | 2 +- .../Implementation/PrefillSITest.cs | 7 +- .../Auth/AuthorizationServiceTests.cs | 38 +- .../Internal/Pdf/PdfServiceTests.cs | 103 +- ...first_task_SelfIdentifiedUser.verified.txt | 61 ++ ...es_to_first_task_ServiceOwner.verified.txt | 61 ++ ...oves_to_first_task_SystemUser.verified.txt | 61 ++ ..._and_moves_to_first_task_User.verified.txt | 61 ++ .../Internal/Process/ProcessEngineTest.cs | 135 +-- .../Models/OrganisationNumberTests.cs | 1 + 119 files changed, 5254 insertions(+), 1185 deletions(-) create mode 100644 src/Altinn.App.Core/Features/Auth/Authenticated.cs create mode 100644 src/Altinn.App.Core/Features/Auth/AuthenticationContext.cs create mode 100644 src/Altinn.App.Core/Features/Auth/AuthenticationContextDI.cs create mode 100644 src/Altinn.App.Core/Features/Auth/AuthenticationContextException.cs create mode 100644 src/Altinn.App.Core/Features/Auth/IAuthenticationContext.cs create mode 100644 src/Altinn.App.Core/Features/Auth/Scopes.cs create mode 100644 src/Altinn.App.Core/Features/Auth/TokenIssuer.cs create mode 100644 src/Altinn.App.Core/Features/Cache/AppConfigurationCache.cs create mode 100644 src/Altinn.App.Core/Features/Cache/AppConfigurationCacheDI.cs create mode 100644 src/Altinn.App.Core/Features/Cache/IAppConfigurationCache.cs create mode 100644 test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_Org.verified.txt create mode 100644 test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_SelfIdentifiedUser.verified.txt create mode 100644 test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_ServiceOwner.verified.txt create mode 100644 test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_SystemUser.verified.txt create mode 100644 test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_User.verified.txt create mode 100644 test/Altinn.App.Api.Tests/Data/Register/Party/5001337.json create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/appsettings.json create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/config/applicationmetadata.json create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/config/authorization/policy.xml create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/config/process/process.bpmn create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/config/texts/resource.nb.json create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/models/Skjema.cs create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/models/default.validation.json create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/options/fileSourceOptions.json create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/ui/default/Settings.json create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/ui/default/layouts/page.json create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/ui/layout-sets.json create mode 100644 test/Altinn.App.Api.Tests/Mocks/AppConfigurationCacheMock.cs delete mode 100644 test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs create mode 100644 test/Altinn.App.Api.Tests/Utils/TestAuthentication.cs create mode 100644 test/Altinn.App.Core.Tests/Features/Auth/AuthenticatedTests.cs create mode 100644 test/Altinn.App.Core.Tests/Features/Auth/AuthenticationInfoTests.cs create mode 100644 test/Altinn.App.Core.Tests/Features/Auth/ScopesTests.cs create mode 100644 test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.StartProcess_starts_process_and_moves_to_first_task_SelfIdentifiedUser.verified.txt create mode 100644 test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.StartProcess_starts_process_and_moves_to_first_task_ServiceOwner.verified.txt create mode 100644 test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.StartProcess_starts_process_and_moves_to_first_task_SystemUser.verified.txt create mode 100644 test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.StartProcess_starts_process_and_moves_to_first_task_User.verified.txt diff --git a/src/Altinn.App.Api/Altinn.App.Api.csproj b/src/Altinn.App.Api/Altinn.App.Api.csproj index 7ddda42db..98702a418 100644 --- a/src/Altinn.App.Api/Altinn.App.Api.csproj +++ b/src/Altinn.App.Api/Altinn.App.Api.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/Altinn.App.Api/Controllers/ActionsController.cs b/src/Altinn.App.Api/Controllers/ActionsController.cs index 7b542a4d9..54f9aa0c2 100644 --- a/src/Altinn.App.Api/Controllers/ActionsController.cs +++ b/src/Altinn.App.Api/Controllers/ActionsController.cs @@ -1,9 +1,9 @@ using Altinn.App.Api.Extensions; using Altinn.App.Api.Infrastructure.Filters; using Altinn.App.Api.Models; -using Altinn.App.Core.Extensions; using Altinn.App.Core.Features; using Altinn.App.Core.Features.Action; +using Altinn.App.Core.Features.Auth; using Altinn.App.Core.Helpers.Serialization; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Data; @@ -35,6 +35,7 @@ public class ActionsController : ControllerBase private readonly IDataClient _dataClient; private readonly IAppMetadata _appMetadata; private readonly ModelSerializationService _modelSerialization; + private readonly IAuthenticationContext _authenticationContext; /// /// Create new instance of the class @@ -46,7 +47,8 @@ public ActionsController( IValidationService validationService, IDataClient dataClient, IAppMetadata appMetadata, - ModelSerializationService modelSerialization + ModelSerializationService modelSerialization, + IAuthenticationContext authenticationContext ) { _authorization = authorization; @@ -56,6 +58,7 @@ ModelSerializationService modelSerialization _dataClient = dataClient; _appMetadata = appMetadata; _modelSerialization = modelSerialization; + _authenticationContext = authenticationContext; } /// @@ -109,11 +112,9 @@ public async Task> Perform( return Conflict($"Process is ended."); } - int? userId = HttpContext.User.GetUserIdAsInt(); - if (userId == null) - { + var currentAuth = _authenticationContext.Current; + if (currentAuth is not Authenticated.User user) return Unauthorized(); - } bool authorized = await _authorization.AuthorizeAction( new AppIdentifier(org, app), @@ -136,7 +137,7 @@ await _appMetadata.GetApplicationMetadata(), ); UserActionContext userActionContext = new( dataMutator, - userId.Value, + user.UserId, actionRequest.ButtonId, actionRequest.Metadata, language diff --git a/src/Altinn.App.Api/Controllers/AuthenticationController.cs b/src/Altinn.App.Api/Controllers/AuthenticationController.cs index 60c4504f8..f5e8d3cfa 100644 --- a/src/Altinn.App.Api/Controllers/AuthenticationController.cs +++ b/src/Altinn.App.Api/Controllers/AuthenticationController.cs @@ -1,4 +1,3 @@ -#nullable disable using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.Internal.Auth; diff --git a/src/Altinn.App.Api/Controllers/AuthorizationController.cs b/src/Altinn.App.Api/Controllers/AuthorizationController.cs index e7963eebb..b26db73f0 100644 --- a/src/Altinn.App.Api/Controllers/AuthorizationController.cs +++ b/src/Altinn.App.Api/Controllers/AuthorizationController.cs @@ -1,11 +1,7 @@ using System.Globalization; using Altinn.App.Core.Configuration; -using Altinn.App.Core.Helpers; +using Altinn.App.Core.Features.Auth; using Altinn.App.Core.Internal.Auth; -using Altinn.App.Core.Internal.Profile; -using Altinn.App.Core.Internal.Registers; -using Altinn.App.Core.Models; -using Altinn.Platform.Register.Models; using Authorization.Platform.Authorization.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -19,22 +15,21 @@ namespace Altinn.App.Api.Controllers; public class AuthorizationController : Controller { private readonly IAuthorizationClient _authorization; - private readonly UserHelper _userHelper; private readonly GeneralSettings _settings; + private readonly IAuthenticationContext _authenticationContext; /// /// Initializes a new instance of the class /// public AuthorizationController( IAuthorizationClient authorization, - IProfileClient profileClient, - IAltinnPartyClient altinnPartyClientClient, - IOptions settings + IOptions settings, + IAuthenticationContext authenticationContext ) { - _userHelper = new UserHelper(profileClient, altinnPartyClientClient, settings); _authorization = authorization; _settings = settings.Value; + _authenticationContext = authenticationContext; } /// @@ -46,14 +41,89 @@ IOptions settings [HttpGet("{org}/{app}/api/authorization/parties/current")] public async Task GetCurrentParty(bool returnPartyObject = false) { - (Party? currentParty, _) = await GetCurrentPartyAsync(HttpContext); - - if (returnPartyObject) + var context = _authenticationContext.Current; + switch (context) { - return Ok(currentParty); - } + case Authenticated.None: + return Unauthorized(); + case Authenticated.User user: + { + var details = await user.LoadDetails(validateSelectedParty: true); + if (details.CanRepresent is not bool canRepresent) + throw new Exception("Couldn't validate selected party"); + + if (canRepresent) + { + if (returnPartyObject) + { + return Ok(details.SelectedParty); + } + + return Ok(details.SelectedParty.PartyId); + } + + // Now we know the user can't represent the selected party (reportee) + // so we will automatically switch to the user's own party (from the profile) + var reportee = details.Profile.Party; + if (user.SelectedPartyId != reportee.PartyId) + { + // Setting cookie to partyID of logged in user if it varies from previus value. + Response.Cookies.Append( + _settings.GetAltinnPartyCookieName, + reportee.PartyId.ToString(CultureInfo.InvariantCulture), + new CookieOptions { Domain = _settings.HostName } + ); + } + + if (returnPartyObject) + { + return Ok(reportee); + } + return Ok(reportee.PartyId); + } + case Authenticated.SelfIdentifiedUser selfIdentified: + { + var details = await selfIdentified.LoadDetails(); + if (returnPartyObject) + { + return Ok(details.Party); + } + + return Ok(details.Party.PartyId); + } + case Authenticated.Org org: + { + var details = await org.LoadDetails(); + if (returnPartyObject) + { + return Ok(details.Party); + } + + return Ok(details.Party.PartyId); + } + case Authenticated.ServiceOwner so: + { + var details = await so.LoadDetails(); + if (returnPartyObject) + { + return Ok(details.Party); + } + + return Ok(details.Party.PartyId); + } + case Authenticated.SystemUser su: + { + var details = await su.LoadDetails(); + if (returnPartyObject) + { + return Ok(details.Party); + } - return Ok(currentParty?.PartyId ?? 0); + return Ok(details.Party.PartyId); + } + default: + throw new Exception($"Unknown authentication context: {context.GetType().Name}"); + } } /// @@ -97,65 +167,53 @@ public async Task ValidateSelectedParty(int userId, int partyId) [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] public async Task GetRolesForCurrentParty() { - (Party? currentParty, UserContext userContext) = await GetCurrentPartyAsync(HttpContext); - - if (currentParty == null) - { - return BadRequest("Both userId and partyId must be provided."); - } - - int userId = userContext.UserId; - IEnumerable roles = await _authorization.GetUserRoles(userId, currentParty.PartyId); - - return Ok(roles); - } - - /// - /// Helper method to retrieve the current party and user context from the HTTP context. - /// - /// The current HttpContext. - /// A tuple containing the current party and user context. - private async Task<(Party? party, UserContext userContext)> GetCurrentPartyAsync(HttpContext context) - { - UserContext userContext = await _userHelper.GetUserContext(context); - int userId = userContext.UserId; - - // If selected party is different than party for user self need to verify - if (userContext.UserParty == null || userContext.PartyId != userContext.UserParty.PartyId) + var context = _authenticationContext.Current; + switch (context) { - bool? isValid = await _authorization.ValidateSelectedParty(userId, userContext.PartyId); - if (isValid != true) + case Authenticated.None: + return Unauthorized(); + case Authenticated.User user: { - // Not valid, fall back to userParty if available - if (userContext.UserParty != null) - { - userContext.Party = userContext.UserParty; - userContext.PartyId = userContext.UserParty.PartyId; - } - else + var details = await user.LoadDetails(validateSelectedParty: true); + if (details.CanRepresent is not bool canRepresent) + throw new Exception("Couldn't validate selected party"); + if (!canRepresent) { - userContext.Party = null; - userContext.PartyId = 0; + // automatically switch to the user's own party + var reportee = details.Profile.Party; + if (user.SelectedPartyId != reportee.PartyId) + { + // Setting cookie to partyID of logged in user if it varies from previus value. + Response.Cookies.Append( + _settings.GetAltinnPartyCookieName, + reportee.PartyId.ToString(CultureInfo.InvariantCulture), + new CookieOptions { Domain = _settings.HostName } + ); + } + return Unauthorized(); } - } - } - - // Sync cookie if needed - string? cookieValue = Request.Cookies[_settings.GetAltinnPartyCookieName]; - if (!int.TryParse(cookieValue, out int partyIdFromCookie)) - { - partyIdFromCookie = 0; - } - if (partyIdFromCookie != userContext.PartyId) - { - Response.Cookies.Append( - _settings.GetAltinnPartyCookieName, - userContext.PartyId.ToString(CultureInfo.InvariantCulture), - new CookieOptions { Domain = _settings.HostName } - ); + return Ok(details.Roles); + } + case Authenticated.SelfIdentifiedUser: + { + return Ok(Array.Empty()); + } + case Authenticated.Org: + { + return Ok(Array.Empty()); + } + case Authenticated.ServiceOwner: + { + return Ok(Array.Empty()); + } + case Authenticated.SystemUser: + { + // NOTE: system users can't have Altinn 2 roles, but they will get support for tilgangspakker, as of 26.01.2025 + return Ok(Array.Empty()); + } + default: + throw new Exception($"Unknown authentication context: {context.GetType().Name}"); } - - return (userContext.Party, userContext); } } diff --git a/src/Altinn.App.Api/Controllers/DataController.cs b/src/Altinn.App.Api/Controllers/DataController.cs index be2f7646d..db8f82b0d 100644 --- a/src/Altinn.App.Api/Controllers/DataController.cs +++ b/src/Altinn.App.Api/Controllers/DataController.cs @@ -10,6 +10,7 @@ using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; using Altinn.App.Core.Features; +using Altinn.App.Core.Features.Auth; using Altinn.App.Core.Features.FileAnalysis; using Altinn.App.Core.Features.FileAnalyzis; using Altinn.App.Core.Helpers; @@ -55,7 +56,7 @@ public class DataController : ControllerBase private readonly IFeatureManager _featureManager; private readonly InternalPatchService _patchService; private readonly ModelSerializationService _modelDeserializer; - + private readonly IAuthenticationContext _authenticationContext; private const long REQUEST_SIZE_LIMIT = 2000 * 1024 * 1024; /// @@ -74,6 +75,7 @@ public class DataController : ControllerBase /// Service used to validate files uploaded. /// Service for applying a json patch to a json serializable object /// Service for serializing and deserializing models + /// The authentication context service public DataController( ILogger logger, IInstanceClient instanceClient, @@ -87,7 +89,8 @@ public DataController( IAppMetadata appMetadata, IFeatureManager featureManager, InternalPatchService patchService, - ModelSerializationService modelDeserializer + ModelSerializationService modelDeserializer, + IAuthenticationContext authenticationContext ) { _logger = logger; @@ -104,6 +107,7 @@ ModelSerializationService modelDeserializer _featureManager = featureManager; _patchService = patchService; _modelDeserializer = modelDeserializer; + _authenticationContext = authenticationContext; } /// @@ -247,7 +251,10 @@ private async Task> PostImpl( var (instance, dataType, applicationMetadata) = instanceResult.Ok; - if (DataElementAccessChecker.GetCreateProblem(instance, dataType, User) is { } accessProblem) + if ( + DataElementAccessChecker.GetCreateProblem(instance, dataType, _authenticationContext.Current) is + { } accessProblem + ) { return accessProblem; } @@ -522,7 +529,10 @@ public async Task Get( } var (instance, dataType, dataElement) = instanceResult.Ok; - if (DataElementAccessChecker.GetReaderProblem(instance, dataType, User) is { } accessProblem) + if ( + DataElementAccessChecker.GetReaderProblem(instance, dataType, _authenticationContext.Current) is + { } accessProblem + ) { return Problem(accessProblem); } @@ -588,7 +598,10 @@ public async Task Put( } var (instance, dataType, dataElement) = instanceResult.Ok; - if (DataElementAccessChecker.GetUpdateProblem(instance, dataType, User) is { } accessProblem) + if ( + DataElementAccessChecker.GetUpdateProblem(instance, dataType, _authenticationContext.Current) is + { } accessProblem + ) { return Problem(accessProblem); } @@ -705,7 +718,10 @@ public async Task> PatchFormDataMultiple // Verify that the data elements isn't restricted for the user foreach (var dataType in dataTypes) { - if (DataElementAccessChecker.GetUpdateProblem(instance, dataType, User) is { } accessProblem) + if ( + DataElementAccessChecker.GetUpdateProblem(instance, dataType, _authenticationContext.Current) is + { } accessProblem + ) { return Problem(accessProblem); } @@ -773,7 +789,15 @@ public async Task> Delete( } var (instance, dataType, dataElement) = instanceResult.Ok; - if (DataElementAccessChecker.GetDeleteProblem(instance, dataType, dataGuid, User) is { } accessProblem) + if ( + DataElementAccessChecker.GetDeleteProblem( + instance, + dataType, + dataGuid, + _authenticationContext.Current + ) is + { } accessProblem + ) { return Problem(accessProblem); } diff --git a/src/Altinn.App.Api/Controllers/PartiesController.cs b/src/Altinn.App.Api/Controllers/PartiesController.cs index 36d7acc6a..cb1e52195 100644 --- a/src/Altinn.App.Api/Controllers/PartiesController.cs +++ b/src/Altinn.App.Api/Controllers/PartiesController.cs @@ -1,15 +1,8 @@ using System.Globalization; using Altinn.App.Core.Configuration; -using Altinn.App.Core.Helpers; -using Altinn.App.Core.Internal.App; -using Altinn.App.Core.Internal.Auth; -using Altinn.App.Core.Internal.Profile; -using Altinn.App.Core.Internal.Registers; -using Altinn.App.Core.Models; +using Altinn.App.Core.Features.Auth; using Altinn.App.Core.Models.Validation; -using Altinn.Platform.Profile.Models; using Altinn.Platform.Register.Models; -using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -23,28 +16,16 @@ namespace Altinn.App.Api.Controllers; [ApiController] public class PartiesController : ControllerBase { - private readonly IAuthorizationClient _authorizationClient; - private readonly UserHelper _userHelper; - private readonly IProfileClient _profileClient; private readonly GeneralSettings _settings; - private readonly IAppMetadata _appMetadata; + private readonly IAuthenticationContext _authenticationContext; /// /// Initializes a new instance of the class /// - public PartiesController( - IAuthorizationClient authorizationClient, - IProfileClient profileClient, - IAltinnPartyClient altinnPartyClientClient, - IOptions settings, - IAppMetadata appMetadata - ) + public PartiesController(IOptions settings, IAuthenticationContext authenticationContext) { - _authorizationClient = authorizationClient; - _userHelper = new UserHelper(profileClient, altinnPartyClientClient, settings); - _profileClient = profileClient; _settings = settings.Value; - _appMetadata = appMetadata; + _authenticationContext = authenticationContext; } /// @@ -58,20 +39,43 @@ IAppMetadata appMetadata [HttpGet("{org}/{app}/api/v1/parties")] public async Task Get(string org, string app, bool allowedToInstantiateFilter = false) { - UserContext userContext = await _userHelper.GetUserContext(HttpContext); - List? partyList = await _authorizationClient.GetPartyList(userContext.UserId); - - if (allowedToInstantiateFilter) + var context = _authenticationContext.Current; + switch (context) { - Application application = await _appMetadata.GetApplicationMetadata(); - List validParties = InstantiationHelper.FilterPartiesByAllowedPartyTypes( - partyList, - application.PartyTypesAllowed - ); - return Ok(validParties); + case Authenticated.None: + return Unauthorized(); + case Authenticated.User user: + { + var details = await user.LoadDetails(validateSelectedParty: false); + return allowedToInstantiateFilter ? Ok(details.PartiesAllowedToInstantiate) : Ok(details.Parties); + } + case Authenticated.SelfIdentifiedUser selfIdentified: + { + var details = await selfIdentified.LoadDetails(); + IReadOnlyList parties = [details.Party]; + return Ok(parties); + } + case Authenticated.Org orgInfo: + { + var details = await orgInfo.LoadDetails(); + IReadOnlyList parties = [details.Party]; + return Ok(parties); + } + case Authenticated.ServiceOwner serviceOwner: + { + var details = await serviceOwner.LoadDetails(); + IReadOnlyList parties = [details.Party]; + return Ok(parties); + } + case Authenticated.SystemUser su: + { + var details = await su.LoadDetails(); + IReadOnlyList parties = [details.Party]; + return Ok(parties); + } + default: + throw new Exception($"Unexpected authentication context: {context.GetType().Name}"); } - - return Ok(partyList); } /// @@ -82,66 +86,134 @@ public async Task Get(string org, string app, bool allowedToInsta /// The selected partyId /// A validation status [Authorize] + [Obsolete("Will be removed in the future")] [HttpPost("{org}/{app}/api/v1/parties/validateInstantiation")] public async Task ValidateInstantiation(string org, string app, [FromQuery] int partyId) { - UserContext userContext = await _userHelper.GetUserContext(HttpContext); - UserProfile? user = await _profileClient.GetUserProfile(userContext.UserId); - if (user is null) + var currentAuth = _authenticationContext.Current; + switch (currentAuth) { - return StatusCode(500, "Could not get user profile while validating instantiation"); - } - List? partyList = await _authorizationClient.GetPartyList(userContext.UserId); - Application application = await _appMetadata.GetApplicationMetadata(); - - PartyTypesAllowed partyTypesAllowed = application.PartyTypesAllowed; - Party? partyUserRepresents = null; - - // Check if the user can represent the supplied partyId - if (partyId != user.PartyId) - { - Party? represents = InstantiationHelper.GetPartyByPartyId(partyList, partyId); - if (represents == null) + case Authenticated.User auth: { - // the user does not represent the chosen party id, is not allowed to initiate - return Ok( - new InstantiationValidationResult - { - Valid = false, - Message = "The user does not represent the supplied party", - ValidParties = InstantiationHelper.FilterPartiesByAllowedPartyTypes( - partyList, - partyTypesAllowed - ), - } - ); - } - - partyUserRepresents = represents; - } + var details = await auth.LoadDetails(validateSelectedParty: false); + if (!details.CanRepresentParty(partyId)) + { + // The user does not represent the chosen party id, is not allowed to initiate + return Ok( + new InstantiationValidationResult + { + Valid = false, + Message = "The user does not represent the supplied party", + ValidParties = details.PartiesAllowedToInstantiate.ToList(), + } + ); + } + if (!details.CanInstantiateAsParty(partyId)) + { + // Can represent the party, but the party is not allowed to instantiate in this app + return Ok( + new InstantiationValidationResult + { + Valid = false, + Message = "The supplied party is not allowed to instantiate the application", + ValidParties = details.PartiesAllowedToInstantiate.ToList(), + } + ); + } - if (partyUserRepresents == null) - { - // if not set, the user represents itself - partyUserRepresents = user.Party; - } + return Ok(new InstantiationValidationResult { Valid = true }); + } + case Authenticated.SelfIdentifiedUser auth: + { + var details = await auth.LoadDetails(); + if (details.Party.PartyId != partyId) + { + return Ok( + new InstantiationValidationResult + { + Valid = false, + Message = "The user does not represent the supplied party", + ValidParties = new List { details.Party }, + } + ); + } + if (!details.CanInstantiate) + { + return Ok( + new InstantiationValidationResult + { + Valid = false, + Message = "The supplied party is not allowed to instantiate the application", + ValidParties = new List { details.Party }, + } + ); + } - // Check if the application can be initiated with the party chosen - bool canInstantiate = InstantiationHelper.IsPartyAllowedToInstantiate(partyUserRepresents, partyTypesAllowed); + return Ok(new InstantiationValidationResult { Valid = true }); + } + case Authenticated.Org auth: + { + var details = await auth.LoadDetails(); + if (details.Party.PartyId != partyId) + { + return Ok( + new InstantiationValidationResult + { + Valid = false, + Message = "The user does not represent the supplied party", + ValidParties = new List { details.Party }, + } + ); + } + if (!details.CanInstantiate) + { + return Ok( + new InstantiationValidationResult + { + Valid = false, + Message = "The supplied party is not allowed to instantiate the application", + ValidParties = new List { details.Party }, + } + ); + } - if (!canInstantiate) - { - return Ok( - new InstantiationValidationResult + return Ok(new InstantiationValidationResult { Valid = true }); + } + case Authenticated.ServiceOwner: + { + return Ok(new InstantiationValidationResult { Valid = true }); + } + case Authenticated.SystemUser auth: + { + var details = await auth.LoadDetails(); + if (details.Party.PartyId != partyId) { - Valid = false, - Message = "The supplied party is not allowed to instantiate the application", - ValidParties = InstantiationHelper.FilterPartiesByAllowedPartyTypes(partyList, partyTypesAllowed), + return Ok( + new InstantiationValidationResult + { + Valid = false, + Message = "The user does not represent the supplied party", + ValidParties = new List { details.Party }, + } + ); + } + if (!details.CanInstantiate) + { + return Ok( + new InstantiationValidationResult + { + Valid = false, + Message = "The supplied party is not allowed to instantiate the application", + ValidParties = new List { details.Party }, + } + ); } - ); - } - return Ok(new InstantiationValidationResult { Valid = true }); + return Ok(new InstantiationValidationResult { Valid = true }); + } + default: + return StatusCode(500, "Invalid authentication context"); + } } /// @@ -152,26 +224,80 @@ public async Task ValidateInstantiation(string org, string app, [ [HttpPut("{org}/{app}/api/v1/parties/{partyId}")] public async Task UpdateSelectedParty(int partyId) { - UserContext userContext = await _userHelper.GetUserContext(HttpContext); - int userId = userContext.UserId; + var currentAuth = _authenticationContext.Current; + switch (currentAuth) + { + case Authenticated.User auth: + { + var details = await auth.LoadDetails(validateSelectedParty: false); + if (!details.CanRepresentParty(partyId)) + return BadRequest($"User {auth.UserId} cannot represent party {partyId}."); - bool? isValid = await _authorizationClient.ValidateSelectedParty(userId, partyId); + Response.Cookies.Append( + _settings.GetAltinnPartyCookieName, + partyId.ToString(CultureInfo.InvariantCulture), + new CookieOptions { Domain = _settings.HostName } + ); - if (!isValid.HasValue) - { - return StatusCode(500, "Something went wrong when trying to update selectedparty."); - } - else if (isValid.Value == false) - { - return BadRequest($"User {userId} cannot represent party {partyId}."); - } + return Ok("Party successfully updated"); + } + case Authenticated.SelfIdentifiedUser auth: + { + if (auth.PartyId != partyId) + return BadRequest($"User {auth.UserId} cannot represent party {partyId}."); - Response.Cookies.Append( - _settings.GetAltinnPartyCookieName, - partyId.ToString(CultureInfo.InvariantCulture), - new CookieOptions { Domain = _settings.HostName } - ); + Response.Cookies.Append( + _settings.GetAltinnPartyCookieName, + partyId.ToString(CultureInfo.InvariantCulture), + new CookieOptions { Domain = _settings.HostName } + ); + + return Ok("Party successfully updated"); + } + case Authenticated.Org auth: + { + var details = await auth.LoadDetails(); + if (details.Party.PartyId != partyId) + return BadRequest($"Org {details.Party.OrgNumber} cannot represent party {partyId}."); + + Response.Cookies.Append( + _settings.GetAltinnPartyCookieName, + partyId.ToString(CultureInfo.InvariantCulture), + new CookieOptions { Domain = _settings.HostName } + ); + + return Ok("Party successfully updated"); + } + case Authenticated.ServiceOwner auth: + { + var details = await auth.LoadDetails(); + if (details.Party.PartyId != partyId) + return BadRequest($"Service owner {auth.Name} cannot represent party {partyId}."); - return Ok("Party successfully updated"); + Response.Cookies.Append( + _settings.GetAltinnPartyCookieName, + partyId.ToString(CultureInfo.InvariantCulture), + new CookieOptions { Domain = _settings.HostName } + ); + + return Ok("Party successfully updated"); + } + case Authenticated.SystemUser auth: + { + var details = await auth.LoadDetails(); + if (details.Party.PartyId != partyId) + return BadRequest($"System user {auth.SystemUserId} cannot represent party {partyId}."); + + Response.Cookies.Append( + _settings.GetAltinnPartyCookieName, + partyId.ToString(CultureInfo.InvariantCulture), + new CookieOptions { Domain = _settings.HostName } + ); + + return Ok("Party successfully updated"); + } + default: + return StatusCode(500, "Invalid authentication context"); + } } } diff --git a/src/Altinn.App.Api/Controllers/ProfileController.cs b/src/Altinn.App.Api/Controllers/ProfileController.cs index 900e8c2e7..dfc93ef11 100644 --- a/src/Altinn.App.Api/Controllers/ProfileController.cs +++ b/src/Altinn.App.Api/Controllers/ProfileController.cs @@ -1,5 +1,4 @@ -using Altinn.App.Core.Helpers; -using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Features.Auth; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -13,14 +12,14 @@ namespace Altinn.App.Api.Controllers; [ApiController] public class ProfileController : Controller { - private readonly IProfileClient _profileClient; + private readonly IAuthenticationContext _authenticationContext; /// /// Initializes a new instance of the class /// - public ProfileController(IProfileClient profileClient) + public ProfileController(IAuthenticationContext authenticationContext) { - _profileClient = profileClient; + _authenticationContext = authenticationContext; } /// @@ -31,26 +30,21 @@ public ProfileController(IProfileClient profileClient) [HttpGet("user")] public async Task GetUser() { - int userId = AuthenticationHelper.GetUserId(HttpContext); - if (userId == 0) + var context = _authenticationContext.Current; + switch (context) { - return BadRequest("The userId is not proviced in the context."); - } - - try - { - var user = await _profileClient.GetUserProfile(userId); - - if (user == null) + case Authenticated.User user: { - return NotFound(); + var details = await user.LoadDetails(validateSelectedParty: false); + return Ok(details.Profile); } - - return Ok(user); - } - catch (Exception e) - { - return StatusCode(500, e.Message); + case Authenticated.SelfIdentifiedUser selfIdentifiedUser: + { + var details = await selfIdentifiedUser.LoadDetails(); + return Ok(details.Profile); + } + default: + return BadRequest($"Unknown authentication context: {context.GetType().Name}"); } } } diff --git a/src/Altinn.App.Api/Controllers/StatelessDataController.cs b/src/Altinn.App.Api/Controllers/StatelessDataController.cs index 7aaa0f322..a17ab21f9 100644 --- a/src/Altinn.App.Api/Controllers/StatelessDataController.cs +++ b/src/Altinn.App.Api/Controllers/StatelessDataController.cs @@ -1,8 +1,8 @@ using System.Globalization; using System.Net; using Altinn.App.Api.Infrastructure.Filters; -using Altinn.App.Core.Extensions; using Altinn.App.Core.Features; +using Altinn.App.Core.Features.Auth; using Altinn.App.Core.Helpers; using Altinn.App.Core.Helpers.Serialization; using Altinn.App.Core.Internal.App; @@ -35,7 +35,7 @@ public class StatelessDataController : ControllerBase private readonly IPrefill _prefillService; private readonly IAltinnPartyClient _altinnPartyClientClient; private readonly IPDP _pdp; - + private readonly IAuthenticationContext _authenticationContext; private const long REQUEST_SIZE_LIMIT = 2000 * 1024 * 1024; private const string PartyPrefix = "partyid"; @@ -52,7 +52,8 @@ public StatelessDataController( IPrefill prefillService, IAltinnPartyClient altinnPartyClientClient, IPDP pdp, - IEnumerable dataProcessors + IEnumerable dataProcessors, + IAuthenticationContext authenticationContext ) { _logger = logger; @@ -62,6 +63,7 @@ IEnumerable dataProcessors _prefillService = prefillService; _altinnPartyClientClient = altinnPartyClientClient; _pdp = pdp; + _authenticationContext = authenticationContext; } /// @@ -313,46 +315,50 @@ public async Task PostAnonymous([FromQuery] string dataType, [From // you happened to log in as. if (partyFromHeader is null) { - var partyId = Request.HttpContext.User.GetPartyIdAsInt(); - if (partyId is null) + var currentAuth = _authenticationContext.Current; + Party? party = currentAuth switch { + Authenticated.User auth => await auth.LookupSelectedParty(), + Authenticated.SelfIdentifiedUser auth => (await auth.LoadDetails()).Party, + Authenticated.Org auth => (await auth.LoadDetails()).Party, + Authenticated.ServiceOwner auth => (await auth.LoadDetails()).Party, + Authenticated.SystemUser auth => (await auth.LoadDetails()).Party, + _ => null, + }; + + if (party is null) return null; - } - var partyFromUser = await _altinnPartyClientClient.GetParty(partyId.Value); - if (partyFromUser is null) + return InstantiationHelper.PartyToInstanceOwner(party); + } + else + { + // Get the party as read in from the header. Authorization happens later. + var headerParts = partyFromHeader.Split(':'); + if (partyFromHeader.Contains(',') || headerParts.Length != 2) { return null; } - return InstantiationHelper.PartyToInstanceOwner(partyFromUser); - } - - // Get the party as read in from the header. Authorization happens later. - var headerParts = partyFromHeader.Split(':'); - if (partyFromHeader.Contains(',') || headerParts.Length != 2) - { - return null; - } + var id = headerParts[1]; + var idPrefix = headerParts[0].ToLowerInvariant(); + var party = idPrefix switch + { + PartyPrefix => await _altinnPartyClientClient.GetParty(int.TryParse(id, out var partyId) ? partyId : 0), - var id = headerParts[1]; - var idPrefix = headerParts[0].ToLowerInvariant(); - var party = idPrefix switch - { - PartyPrefix => await _altinnPartyClientClient.GetParty(int.TryParse(id, out var partyId) ? partyId : 0), + // Frontend seems to only use partyId, not orgnr or ssn. + PersonPrefix => await _altinnPartyClientClient.LookupParty(new PartyLookup { Ssn = id }), + OrgPrefix => await _altinnPartyClientClient.LookupParty(new PartyLookup { OrgNo = id }), + _ => null, + }; - // Frontend seems to only use partyId, not orgnr or ssn. - PersonPrefix => await _altinnPartyClientClient.LookupParty(new PartyLookup { Ssn = id }), - OrgPrefix => await _altinnPartyClientClient.LookupParty(new PartyLookup { OrgNo = id }), - _ => null, - }; + if (party is null || party.PartyId == 0) + { + return null; + } - if (party is null || party.PartyId == 0) - { - return null; + return InstantiationHelper.PartyToInstanceOwner(party); } - - return InstantiationHelper.PartyToInstanceOwner(party); } private async Task AuthorizeAction(string org, string app, int partyId, string action) diff --git a/src/Altinn.App.Api/Controllers/UserDefinedMetadataController.cs b/src/Altinn.App.Api/Controllers/UserDefinedMetadataController.cs index 47f831835..d8eda92f4 100644 --- a/src/Altinn.App.Api/Controllers/UserDefinedMetadataController.cs +++ b/src/Altinn.App.Api/Controllers/UserDefinedMetadataController.cs @@ -3,6 +3,7 @@ using Altinn.App.Api.Infrastructure.Filters; using Altinn.App.Api.Models; using Altinn.App.Core.Constants; +using Altinn.App.Core.Features.Auth; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Instances; @@ -28,6 +29,7 @@ public class UserDefinedMetadataController : ControllerBase private readonly IInstanceClient _instanceClient; private readonly IDataClient _dataClient; private readonly IAppMetadata _appMetadata; + private readonly IAuthenticationContext _authenticationContext; /// /// Initialize a new instance of with the given services. @@ -35,15 +37,18 @@ public class UserDefinedMetadataController : ControllerBase /// A client that can be used to send instance requests to storage. /// A client that can be used to send data requests to storage. /// The app metadata service + /// The authentication context service public UserDefinedMetadataController( IInstanceClient instanceClient, IDataClient dataClient, - IAppMetadata appMetadata + IAppMetadata appMetadata, + IAuthenticationContext authenticationContext ) { _instanceClient = instanceClient; _dataClient = dataClient; _appMetadata = appMetadata; + _authenticationContext = authenticationContext; } /// @@ -136,7 +141,10 @@ [FromBody] UserDefinedMetadataDto userDefinedMetadataDto ); } - if (DataElementAccessChecker.GetUpdateProblem(instance, dataTypeFromMetadata, User) is { } problem) + if ( + DataElementAccessChecker.GetUpdateProblem(instance, dataTypeFromMetadata, _authenticationContext.Current) is + { } problem + ) { return StatusCode(problem.Status ?? 500, problem); } diff --git a/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs index da203e0e6..fc021629c 100644 --- a/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; using Altinn.App.Core.Features; +using Altinn.App.Core.Features.Cache; using Altinn.App.Core.Features.Correspondence.Extensions; using Altinn.App.Core.Features.Maskinporten; using Altinn.App.Core.Features.Maskinporten.Extensions; @@ -44,6 +45,10 @@ public static class ServiceCollectionExtensions /// public static void AddAltinnAppControllersWithViews(this IServiceCollection services) { + // We add this here because it uses a hosted service and we want it to run as early as possible + // so that consumers of the cache can rely on it being available. + services.AddAppConfigurationCache(); + // Add API controllers from Altinn.App.Api IMvcBuilder mvcBuilder = services.AddControllersWithViews(options => { diff --git a/src/Altinn.App.Api/Helpers/DataElementAccessChecker.cs b/src/Altinn.App.Api/Helpers/DataElementAccessChecker.cs index 23b6c9a72..774c6e944 100644 --- a/src/Altinn.App.Api/Helpers/DataElementAccessChecker.cs +++ b/src/Altinn.App.Api/Helpers/DataElementAccessChecker.cs @@ -1,7 +1,6 @@ -using System.Globalization; using System.Net; -using System.Security.Claims; -using Altinn.App.Core.Extensions; +using Altinn.App.Core.Features.Auth; +using Altinn.App.Core.Models; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Mvc; @@ -15,34 +14,46 @@ namespace Altinn.App.Api.Helpers; /// internal static class DataElementAccessChecker { - internal static bool IsValidContributor(DataType dataType, string? org, int? orgNr) + internal static bool IsValidContributor(DataType dataType, Authenticated auth) { if (dataType.AllowedContributers is null || dataType.AllowedContributers.Count == 0) { return true; } + var (org, orgNr) = auth switch + { + Authenticated.Org a => (null, a.OrgNo), + Authenticated.ServiceOwner a => (a.Name, a.OrgNo), + Authenticated.SystemUser a => (null, a.SystemUserOrgNr.Get(OrganisationNumberFormat.Local)), + _ => (null, null), + }; + foreach (string item in dataType.AllowedContributers) { - string key = item.Split(':')[0]; - string value = item.Split(':')[1]; + var splitIndex = item.IndexOf(':'); + ReadOnlySpan key = item.AsSpan(0, splitIndex); + ReadOnlySpan value = item.AsSpan(splitIndex + 1); - switch (key.ToLowerInvariant()) + if (key.Equals("org", StringComparison.OrdinalIgnoreCase)) { - case "org": - if (value.Equals(org, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - break; - case "orgno": - if (value.Equals(orgNr?.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - break; + if (org is null) + continue; + + if (value.Equals(org, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + else if (key.Equals("orgno", StringComparison.OrdinalIgnoreCase)) + { + if (orgNr is null) + continue; + + if (value.Equals(orgNr, StringComparison.OrdinalIgnoreCase)) + { + return true; + } } } @@ -53,7 +64,7 @@ internal static bool IsValidContributor(DataType dataType, string? org, int? org /// Checks if the user has access to read a data element of a given data type on an instance. /// /// null for success or ProblemDetails that can be an error response in the Apis - internal static ProblemDetails? GetReaderProblem(Instance instance, DataType dataType, ClaimsPrincipal user) + internal static ProblemDetails? GetReaderProblem(Instance instance, DataType dataType, Authenticated auth) { // We don't have any way to restrict reads based on data type yet, so just return null. // Might be used if we get a concept of internal server only data types or similar. @@ -61,9 +72,9 @@ internal static bool IsValidContributor(DataType dataType, string? org, int? org } // Common checks for create, update and delete - private static ProblemDetails? GetMutationProblem(Instance instance, DataType dataType, ClaimsPrincipal user) + private static ProblemDetails? GetMutationProblem(Instance instance, DataType dataType, Authenticated auth) { - if (GetReaderProblem(instance, dataType, user) is { } readProblem) + if (GetReaderProblem(instance, dataType, auth) is { } readProblem) { return readProblem; } @@ -76,7 +87,7 @@ internal static bool IsValidContributor(DataType dataType, string? org, int? org Status = (int)HttpStatusCode.Conflict, }; } - if (!IsValidContributor(dataType, user.GetOrg(), user.GetOrgNumber())) + if (!IsValidContributor(dataType, auth)) { return new ProblemDetails { @@ -94,12 +105,12 @@ internal static bool IsValidContributor(DataType dataType, string? org, int? org internal static ProblemDetails? GetCreateProblem( Instance instance, DataType dataType, - ClaimsPrincipal user, + Authenticated auth, long? contentLength = null ) { // Run the general mutation checks - if (GetMutationProblem(instance, dataType, user) is { } problemDetails) + if (GetMutationProblem(instance, dataType, auth) is { } problemDetails) { return problemDetails; } @@ -129,7 +140,7 @@ internal static bool IsValidContributor(DataType dataType, string? org, int? org } // Verify that only orgs can create data elements when DisallowUserCreate is true - if (dataType.AppLogic?.DisallowUserCreate == true && string.IsNullOrWhiteSpace(user.GetOrg())) + if (dataType.AppLogic?.DisallowUserCreate == true && auth is not Authenticated.ServiceOwner) { return new ProblemDetails() { @@ -146,9 +157,9 @@ internal static bool IsValidContributor(DataType dataType, string? org, int? org /// Checks if the user has access to mutate a data element of a given data type on an instance. /// /// null for success or ProblemDetails that can be an error response in the Apis - internal static ProblemDetails? GetUpdateProblem(Instance instance, DataType dataType, ClaimsPrincipal user) + internal static ProblemDetails? GetUpdateProblem(Instance instance, DataType dataType, Authenticated auth) { - if (GetMutationProblem(instance, dataType, user) is { } problemDetails) + if (GetMutationProblem(instance, dataType, auth) is { } problemDetails) { return problemDetails; } @@ -164,10 +175,10 @@ internal static bool IsValidContributor(DataType dataType, string? org, int? org Instance instance, DataType dataType, Guid dataElementId, - ClaimsPrincipal user + Authenticated auth ) { - if (GetMutationProblem(instance, dataType, user) is { } problemDetails) + if (GetMutationProblem(instance, dataType, auth) is { } problemDetails) { return problemDetails; } @@ -184,7 +195,7 @@ ClaimsPrincipal user }; } - if (dataType.AppLogic?.DisallowUserDelete == true && !UserHasValidOrgClaim(user)) + if (dataType.AppLogic?.DisallowUserDelete == true && auth is not Authenticated.ServiceOwner) { return new ProblemDetails() { @@ -201,9 +212,4 @@ private static bool InstanceIsActive(Instance i) { return i?.Status?.Archived is null && i?.Status?.SoftDeleted is null && i?.Status?.HardDeleted is null; } - - /// - /// Checks if the current claims principal has a valid `urn:altinn:org` claim - /// - private static bool UserHasValidOrgClaim(ClaimsPrincipal user) => !string.IsNullOrWhiteSpace(user.GetOrg()); } diff --git a/src/Altinn.App.Api/Infrastructure/Middleware/TelemetryEnrichingMiddleware.cs b/src/Altinn.App.Api/Infrastructure/Middleware/TelemetryEnrichingMiddleware.cs index e5c551ac6..1e05b391e 100644 --- a/src/Altinn.App.Api/Infrastructure/Middleware/TelemetryEnrichingMiddleware.cs +++ b/src/Altinn.App.Api/Infrastructure/Middleware/TelemetryEnrichingMiddleware.cs @@ -1,8 +1,5 @@ -using System.Collections.Frozen; -using System.Diagnostics; -using System.Security.Claims; using Altinn.App.Core.Features; -using AltinnCore.Authentication.Constants; +using Altinn.App.Core.Features.Auth; using Microsoft.AspNetCore.Http.Features; namespace Altinn.App.Api.Infrastructure.Middleware; @@ -11,53 +8,6 @@ internal sealed class TelemetryEnrichingMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; - private static readonly FrozenDictionary> _claimActions; - - static TelemetryEnrichingMiddleware() - { - var actions = new Dictionary>(StringComparer.OrdinalIgnoreCase) - { - { AltinnCoreClaimTypes.UserName, static (claim, activity) => activity.SetUsername(claim.Value) }, - { - AltinnCoreClaimTypes.UserId, - static (claim, activity) => - { - if (int.TryParse(claim.Value, out var result)) - { - activity.SetUserId(result); - } - } - }, - { - AltinnCoreClaimTypes.PartyID, - static (claim, activity) => - { - if (int.TryParse(claim.Value, out var result)) - { - activity.SetUserPartyId(result); - } - } - }, - { - AltinnCoreClaimTypes.AuthenticateMethod, - static (claim, activity) => activity.SetAuthenticationMethod(claim.Value) - }, - { - AltinnCoreClaimTypes.AuthenticationLevel, - static (claim, activity) => - { - if (int.TryParse(claim.Value, out var result)) - { - activity.SetAuthenticationLevel(result); - } - } - }, - { AltinnCoreClaimTypes.Org, static (claim, activity) => activity.SetOrganisationName(claim.Value) }, - { AltinnCoreClaimTypes.OrgNumber, static (claim, activity) => activity.SetOrganisationNumber(claim.Value) }, - }; - - _claimActions = actions.ToFrozenDictionary(); - } /// /// Initializes a new instance of the class. @@ -85,13 +35,9 @@ public async Task InvokeAsync(HttpContext context) try { - foreach (var claim in context.User.Claims) - { - if (_claimActions.TryGetValue(claim.Type, out var action)) - { - action(claim, activity); - } - } + var authenticationContext = context.RequestServices.GetRequiredService(); + var currentAuth = authenticationContext.Current; + activity.SetAuthenticated(currentAuth); // Set telemetry tags with route values if available. if ( diff --git a/src/Altinn.App.Api/Infrastructure/Telemetry/IdentityTelemetryFilter.cs b/src/Altinn.App.Api/Infrastructure/Telemetry/IdentityTelemetryFilter.cs index 15141dd86..6b27f1397 100644 --- a/src/Altinn.App.Api/Infrastructure/Telemetry/IdentityTelemetryFilter.cs +++ b/src/Altinn.App.Api/Infrastructure/Telemetry/IdentityTelemetryFilter.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using Altinn.App.Core.Extensions; +using Altinn.App.Core.Features.Auth; using Microsoft.ApplicationInsights.Channel; using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.Extensibility; @@ -55,6 +56,12 @@ public void Process(ITelemetry item) { request.Properties.Add("orgNumber", orgNumber?.ToString(CultureInfo.InvariantCulture) ?? ""); } + + var authContext = ctx.RequestServices.GetRequiredService(); + var auth = authContext.Current; + request.Properties.Add("tokenIssuer", auth.TokenIssuer.ToString()); + request.Properties.Add("tokenIsExchanged", auth.TokenIsExchanged.ToString()); + request.Properties.Add("authType", auth.GetType().Name); } } diff --git a/src/Altinn.App.Core/Altinn.App.Core.csproj b/src/Altinn.App.Core/Altinn.App.Core.csproj index d65d977f6..2f9e764cc 100644 --- a/src/Altinn.App.Core/Altinn.App.Core.csproj +++ b/src/Altinn.App.Core/Altinn.App.Core.csproj @@ -16,12 +16,11 @@ - + - - + + diff --git a/src/Altinn.App.Core/Configuration/GeneralSettings.cs b/src/Altinn.App.Core/Configuration/GeneralSettings.cs index 9b9bd1152..288c755fa 100644 --- a/src/Altinn.App.Core/Configuration/GeneralSettings.cs +++ b/src/Altinn.App.Core/Configuration/GeneralSettings.cs @@ -39,6 +39,8 @@ public class GeneralSettings /// public bool DisableLocaltestValidation { get; set; } + internal bool DisableAppConfigurationCache { get; set; } + /// /// The externally accesible base url for the app with trailing / /// diff --git a/src/Altinn.App.Core/Extensions/InstanceEventExtensions.cs b/src/Altinn.App.Core/Extensions/InstanceEventExtensions.cs index 70c23bb4a..0db03eb88 100644 --- a/src/Altinn.App.Core/Extensions/InstanceEventExtensions.cs +++ b/src/Altinn.App.Core/Extensions/InstanceEventExtensions.cs @@ -33,11 +33,13 @@ public static InstanceEvent CopyValues(this InstanceEvent original) Name = original.ProcessInfo?.CurrentTask?.Name, Started = original.ProcessInfo?.CurrentTask?.Started, Ended = original.ProcessInfo?.CurrentTask?.Ended, +#pragma warning disable CS0618 // Type or member is obsolete Validated = new ValidationStatus { CanCompleteTask = original.ProcessInfo?.CurrentTask?.Validated?.CanCompleteTask ?? false, Timestamp = original.ProcessInfo?.CurrentTask?.Validated?.Timestamp, }, +#pragma warning restore CS0618 // Type or member is obsolete }, StartEvent = original.ProcessInfo?.StartEvent, @@ -49,6 +51,9 @@ public static InstanceEvent CopyValues(this InstanceEvent original) OrgId = original.User.OrgId, UserId = original.User.UserId, NationalIdentityNumber = original.User?.NationalIdentityNumber, + SystemUserId = original.User?.SystemUserId, + SystemUserOwnerOrgNo = original.User?.SystemUserOwnerOrgNo, + SystemUserName = original.User?.SystemUserName, }, }; } diff --git a/src/Altinn.App.Core/Extensions/ProcessStateExtensions.cs b/src/Altinn.App.Core/Extensions/ProcessStateExtensions.cs index 14a63ee65..a2df96345 100644 --- a/src/Altinn.App.Core/Extensions/ProcessStateExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ProcessStateExtensions.cs @@ -21,7 +21,9 @@ public static ProcessState Copy(this ProcessState original) copyOfState.CurrentTask = new ProcessElementInfo(); copyOfState.CurrentTask.FlowType = original.CurrentTask.FlowType; copyOfState.CurrentTask.Name = original.CurrentTask.Name; +#pragma warning disable CS0618 // Type or member is obsolete copyOfState.CurrentTask.Validated = original.CurrentTask.Validated; +#pragma warning restore CS0618 // Type or member is obsolete copyOfState.CurrentTask.AltinnTaskType = original.CurrentTask.AltinnTaskType; copyOfState.CurrentTask.Flow = original.CurrentTask.Flow; copyOfState.CurrentTask.ElementId = original.CurrentTask.ElementId; diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs index d574b97e5..452810665 100644 --- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Features; using Altinn.App.Core.Features.Action; +using Altinn.App.Core.Features.Auth; using Altinn.App.Core.Features.DataLists; using Altinn.App.Core.Features.DataProcessing; using Altinn.App.Core.Features.ExternalApi; @@ -115,6 +116,8 @@ IWebHostEnvironment env services.TryAddTransient(); services.TryAddTransient(); services.TryAddTransient(); + + services.AddAuthenticationContext(); } private static void AddApplicationIdentifier(IServiceCollection services) @@ -178,7 +181,7 @@ IWebHostEnvironment env services.Configure(configuration.GetSection(nameof(PdfGeneratorSettings))); if (env.IsDevelopment()) - services.AddLocaltestValidation(configuration); + services.AddLocaltestValidation(); AddValidationServices(services, configuration); AddAppOptions(services); diff --git a/src/Altinn.App.Core/Features/Action/SigningUserAction.cs b/src/Altinn.App.Core/Features/Action/SigningUserAction.cs index 43fd0efa8..0bb64fbcd 100644 --- a/src/Altinn.App.Core/Features/Action/SigningUserAction.cs +++ b/src/Altinn.App.Core/Features/Action/SigningUserAction.cs @@ -1,8 +1,8 @@ using System.Globalization; +using Altinn.App.Core.Features.Auth; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Process.Elements; -using Altinn.App.Core.Internal.Profile; using Altinn.App.Core.Internal.Sign; using Altinn.App.Core.Models; using Altinn.App.Core.Models.Process; @@ -21,7 +21,6 @@ public class SigningUserAction : IUserAction private readonly IProcessReader _processReader; private readonly IAppMetadata _appMetadata; private readonly ILogger _logger; - private readonly IProfileClient _profileClient; private readonly ISignClient _signClient; /// @@ -29,19 +28,16 @@ public class SigningUserAction : IUserAction /// /// The process reader /// The logger - /// The profile client /// The sign client /// The application metadata public SigningUserAction( IProcessReader processReader, ILogger logger, - IProfileClient profileClient, ISignClient signClient, IAppMetadata appMetadata ) { _logger = logger; - _profileClient = profileClient; _signClient = signClient; _processReader = processReader; _appMetadata = appMetadata; @@ -55,7 +51,12 @@ IAppMetadata appMetadata /// public async Task HandleAction(UserActionContext context) { - if (context.UserId == null) + if ( + context.Authentication + is not Authenticated.User + and not Authenticated.SelfIdentifiedUser + and not Authenticated.SystemUser + ) { return UserActionResult.FailureResult( error: new ActionError() { Code = "NoUserId", Message = "User id is missing in token" }, @@ -85,7 +86,7 @@ public async Task HandleAction(UserActionContext context) new InstanceIdentifier(context.Instance), currentTask.Id, signatureDataType, - await GetSignee(context.UserId.Value), + await GetSignee(context), dataElementSignatures ); await _signClient.SignDataElements(signatureContext); @@ -141,17 +142,31 @@ private static List GetDataElementSignatures( return connectedDataElements; } - private async Task GetSignee(int userId) + private static async Task GetSignee(UserActionContext context) { - var userProfile = - await _profileClient.GetUserProfile(userId) - ?? throw new Exception("Could not get user profile while getting signee"); - - return new Signee + switch (context.Authentication) { - UserId = userProfile.UserId.ToString(CultureInfo.InvariantCulture), - PersonNumber = userProfile.Party.SSN, - OrganisationNumber = userProfile.Party.OrgNumber, - }; + case Authenticated.User user: + { + var userProfile = await user.LookupProfile(); + + return new Signee + { + UserId = userProfile.UserId.ToString(CultureInfo.InvariantCulture), + PersonNumber = userProfile.Party.SSN, + OrganisationNumber = userProfile.Party.OrgNumber, + }; + } + case Authenticated.SelfIdentifiedUser selfIdentifiedUser: + return new Signee { UserId = selfIdentifiedUser.UserId.ToString(CultureInfo.InvariantCulture) }; + case Authenticated.SystemUser systemUser: + return new Signee + { + SystemUserId = systemUser.SystemUserId[0], + OrganisationNumber = systemUser.SystemUserOrgNr.Get(OrganisationNumberFormat.Local), + }; + default: + throw new Exception("Could not get signee"); + } } } diff --git a/src/Altinn.App.Core/Features/Action/UniqueSignatureAuthorizer.cs b/src/Altinn.App.Core/Features/Action/UniqueSignatureAuthorizer.cs index 7954dbee9..2a30239a9 100644 --- a/src/Altinn.App.Core/Features/Action/UniqueSignatureAuthorizer.cs +++ b/src/Altinn.App.Core/Features/Action/UniqueSignatureAuthorizer.cs @@ -1,5 +1,6 @@ +using System.Globalization; using System.Text.Json; -using Altinn.App.Core.Extensions; +using Altinn.App.Core.Features.Auth; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Instances; @@ -74,12 +75,20 @@ public async Task AuthorizeAction(UserActionAuthorizerContext context) var signatureDataElements = instance.Data.Where(d => dataTypes.Contains(d.DataType)).ToList(); foreach (var signatureDataElement in signatureDataElements) { - var userId = await GetUserIdFromDataElementContainingSignDocument( + var signee = await GetSigneeFromSignDocument( appMetadata.AppIdentifier, context.InstanceIdentifier, signatureDataElement ); - if (userId == context.User.GetUserOrOrgId()) + bool unauthorized = context.Authentication switch + { + Authenticated.User a => a.UserId.ToString(CultureInfo.InvariantCulture) == signee?.UserId, + Authenticated.SelfIdentifiedUser a => a.UserId.ToString(CultureInfo.InvariantCulture) + == signee?.UserId, + Authenticated.SystemUser a => a.SystemUserId[0].ToString() == signee?.UserId, // TODO: wait for systemuserid + _ => false, + }; + if (unauthorized) { return false; } @@ -89,7 +98,7 @@ public async Task AuthorizeAction(UserActionAuthorizerContext context) return true; } - private async Task GetUserIdFromDataElementContainingSignDocument( + private async Task GetSigneeFromSignDocument( AppIdentifier appIdentifier, InstanceIdentifier instanceIdentifier, DataElement dataElement @@ -105,11 +114,11 @@ DataElement dataElement try { var signDocument = await JsonSerializer.DeserializeAsync(data, _jsonSerializerOptions); - return signDocument?.SigneeInfo.UserId ?? ""; + return signDocument?.SigneeInfo; } catch (JsonException) { - return ""; + return null; } } } diff --git a/src/Altinn.App.Core/Features/Action/UserActionAuthorizerContext.cs b/src/Altinn.App.Core/Features/Action/UserActionAuthorizerContext.cs index bd1341085..57c96dd38 100644 --- a/src/Altinn.App.Core/Features/Action/UserActionAuthorizerContext.cs +++ b/src/Altinn.App.Core/Features/Action/UserActionAuthorizerContext.cs @@ -1,4 +1,5 @@ using System.Security.Claims; +using Altinn.App.Core.Features.Auth; using Altinn.App.Core.Models; namespace Altinn.App.Core.Features.Action; @@ -15,14 +16,19 @@ public class UserActionAuthorizerContext /// for the instance /// The id of the task /// The action to authorize + /// Information about the authenticated party public UserActionAuthorizerContext( ClaimsPrincipal user, InstanceIdentifier instanceIdentifier, string? taskId, - string action + string action, + Authenticated authentication ) { +#pragma warning disable CS0618 // Type or member is obsolete User = user; +#pragma warning restore CS0618 // Type or member is obsolete + Authentication = authentication; InstanceIdentifier = instanceIdentifier; TaskId = taskId; Action = action; @@ -31,8 +37,14 @@ string action /// /// Gets or sets the user /// + [Obsolete("Use the Authentication property instead")] public ClaimsPrincipal User { get; set; } + /// + /// Gets or sets the authentication information + /// + public Authenticated Authentication { get; } + /// /// Gets or sets the instance identifier /// diff --git a/src/Altinn.App.Core/Features/Auth/Authenticated.cs b/src/Altinn.App.Core/Features/Auth/Authenticated.cs new file mode 100644 index 000000000..bf468fc2d --- /dev/null +++ b/src/Altinn.App.Core/Features/Auth/Authenticated.cs @@ -0,0 +1,968 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text.Json; +using System.Text.Json.Serialization; +using Altinn.App.Core.Features.Maskinporten.Constants; +using Altinn.App.Core.Helpers; +using Altinn.App.Core.Internal.Language; +using Altinn.App.Core.Models; +using Altinn.Platform.Profile.Models; +using Altinn.Platform.Register.Models; +using AltinnCore.Authentication.Constants; +using Authorization.Platform.Authorization.Models; + +namespace Altinn.App.Core.Features.Auth; + +/// +/// Contains information about the current logged in client/user. +/// Represented as a union/type hierarchy to express which information is available. +/// +public abstract class Authenticated +{ + /// + /// Token issuer + /// + public TokenIssuer TokenIssuer { get; } + + /// + /// True if the token is exchanged through Altinn Authentication exchange endpoint + /// + public bool TokenIsExchanged { get; } + + /// + /// The scopes of the JWT token + /// + public Scopes Scopes { get; } + + /// + /// The JWT token + /// + public string Token { get; } + + private Authenticated(TokenIssuer tokenIssuer, bool tokenIsExchanged, Scopes scopes, string token) + { + TokenIssuer = tokenIssuer; + TokenIsExchanged = tokenIsExchanged; + Scopes = scopes; + Token = token; + } + + /// + /// Resolves the language for the current authenticated client. + /// If the client is a user, we will look up the language from the user profile. + /// In all other cases we default to "nb". + /// + /// + public async Task GetLanguage() + { + string language = LanguageConst.Nb; + + if (this is not User and not SelfIdentifiedUser) + return language; + + var profile = this switch + { + User user => await user.LookupProfile(), + SelfIdentifiedUser selfIdentifiedUser => (await selfIdentifiedUser.LoadDetails()).Profile, + _ => throw new InvalidOperationException($"Unexpected case: {this.GetType().Name}"), + }; + + if (!string.IsNullOrEmpty(profile.ProfileSettingPreference?.Language)) + language = profile.ProfileSettingPreference.Language; + + return language; + } + + /// + /// Type to indicate that the current request is not uathenticated. + /// + public sealed class None : Authenticated + { + internal None(TokenIssuer tokenIssuer, bool tokenIsExchanged, Scopes scopes, string token) + : base(tokenIssuer, tokenIsExchanged, scopes, token) { } + } + + /// + /// The logged in client is a user (e.g. Altinn portal/ID-porten) + /// + public sealed class User : Authenticated + { + /// + /// User ID + /// + public int UserId { get; } + + /// + /// Party ID + /// + public int UserPartyId { get; } + + /// + /// The party the user has selected through party selection + /// + public int SelectedPartyId { get; } + + /// + /// Authentication level + /// + public int AuthenticationLevel { get; } + + /// + /// Method of authentication, e.g. "idporten" or "maskinporten" + /// + public string AuthenticationMethod { get; } + + /// + /// True if the user was authenticated through the Altinn portal + /// + public bool InAltinnPortal { get; } + + private Details? _extra; + private readonly Func> _getUserProfile; + private readonly Func> _lookupParty; + private readonly Func?>> _getPartyList; + private readonly Func> _validateSelectedParty; + private readonly Func>> _getUserRoles; + private readonly ApplicationMetadata _appMetadata; + + internal User( + int userId, + int userPartyId, + int authenticationLevel, + string authenticationMethod, + int selectedPartyId, + bool inAltinnPortal, + TokenIssuer tokenIssuer, + bool tokenIsExchanged, + Scopes scopes, + string token, + Func> getUserProfile, + Func> lookupParty, + Func?>> getPartyList, + Func> validateSelectedParty, + Func>> getUserRoles, + ApplicationMetadata appMetadata + ) + : base(tokenIssuer, tokenIsExchanged, scopes, token) + { + UserId = userId; + UserPartyId = userPartyId; + SelectedPartyId = selectedPartyId; + AuthenticationLevel = authenticationLevel; + AuthenticationMethod = authenticationMethod; + InAltinnPortal = inAltinnPortal; + _getUserProfile = getUserProfile; + _lookupParty = lookupParty; + _getPartyList = getPartyList; + _validateSelectedParty = validateSelectedParty; + _getUserRoles = getUserRoles; + _appMetadata = appMetadata; + } + + /// + /// Detailed information about a logged in user + /// + /// Party object for the user. This means that the user is currently representing themselves as a person + /// + /// Party object for the selected party. + /// Selected party and user party will differ when the user has chosed to represent a different entity during party selection (e.g. an organisation) + /// + /// Users profile + /// True if the user represents itself (user party will equal selected party) + /// List of parties the user can represent + /// List of parties the user can instantiate as + /// List of roles the user has + /// True if the user can represent the selected party. Only set if details were loaded with validateSelectedParty set to true + public sealed record Details( + Party UserParty, + Party SelectedParty, + UserProfile Profile, + bool RepresentsSelf, + IReadOnlyList Parties, + IReadOnlyList PartiesAllowedToInstantiate, + IReadOnlyList Roles, + bool? CanRepresent = null + ) + { + /// + /// Check if the user can represent a party. + /// + /// Party ID + /// + public bool CanRepresentParty(int partyId) + { + if (partyId == UserParty.PartyId || partyId == SelectedParty.PartyId) + return true; + + var partiesToCheck = new Queue(Parties); + while (partiesToCheck.Count > 0) + { + var party = partiesToCheck.Dequeue(); + if (party.PartyId == partyId) + return true; + + if (party.ChildParties is not null) + { + foreach (var childParty in party.ChildParties) + partiesToCheck.Enqueue(childParty); + } + } + + return false; + } + + /// + /// Checks if the current user can instantiate a specific party by ID. + /// + /// Party ID + /// + public bool CanInstantiateAsParty(int partyId) + { + var partiesToCheck = new Queue(PartiesAllowedToInstantiate); + while (partiesToCheck.Count > 0) + { + var party = partiesToCheck.Dequeue(); + if (party.PartyId == partyId && !party.OnlyHierarchyElementWithNoAccess) + return true; + + if (party.ChildParties is not null) + { + foreach (var childParty in party.ChildParties) + partiesToCheck.Enqueue(childParty); + } + } + + return false; + } + } + + /// + /// Lookup the party for the selected party ID. + /// + /// + /// If the party couldn't be resolved + public async Task LookupSelectedParty() => + _extra?.SelectedParty + ?? await _lookupParty(SelectedPartyId) + ?? throw new InvalidOperationException($"Could not load party for selected party ID: {SelectedPartyId}"); + + /// + /// Lookup the user profile for the current user. + /// + /// + /// + public async Task LookupProfile() => + _extra?.Profile + ?? await _getUserProfile(UserId) + ?? throw new InvalidOperationException("Could not get user profile while getting user context"); + + /// + /// Load the details for the current user. + /// + /// If true, will verify that the logged in user has access to the selected party + /// + /// Thrown if the user doesn't have access to the selected party + public async Task
LoadDetails(bool validateSelectedParty = false) + { + if (_extra is not null) + return _extra; + + var userProfile = + await _getUserProfile(UserId) + ?? throw new AuthenticationContextException($"Could not get user profile for logged in user: {UserId}"); + if (userProfile.Party is null) + throw new AuthenticationContextException($"Could not get user party from profile for user: {UserId}"); + + var lookupPartyTask = + SelectedPartyId == userProfile.PartyId + ? Task.FromResult((Party?)userProfile.Party) + : _lookupParty(SelectedPartyId); + var partiesTask = _getPartyList(UserId); + await Task.WhenAll(lookupPartyTask, partiesTask); + + var parties = await partiesTask ?? []; + if (parties.Count == 0) + parties.Add(userProfile.Party); + + var selectedParty = await lookupPartyTask; + if (selectedParty is null) + throw new AuthenticationContextException( + $"Could not load party for selected party ID: {SelectedPartyId}" + ); + + var representsSelf = SelectedPartyId == userProfile.PartyId; + bool? canRepresent = null; + if (representsSelf) + canRepresent = true; + + if (validateSelectedParty && !representsSelf) + { + // The selected party must either be the profile/default party or a party the user can represent, + // which can be validated against the user's party list. + canRepresent = await _validateSelectedParty(UserId, SelectedPartyId); + } + + var roles = await _getUserRoles(UserId, SelectedPartyId); + + var partiesAllowedToInstantiate = InstantiationHelper.FilterPartiesByAllowedPartyTypes( + parties, + _appMetadata.PartyTypesAllowed + ); + + _extra = new Details( + userProfile.Party, + selectedParty, + userProfile, + representsSelf, + parties, + partiesAllowedToInstantiate, + roles.ToArray(), + canRepresent + ); + return _extra; + } + } + + /// + /// The logged in client is a user (e.g. Altinn portal/ID-porten) with auth level 0. + /// This means that the user has authenticated with a username/password, which can happen using + /// * Altinn "self registered users" + /// * ID-porten through Ansattporten ("low"), MinID self registered eID + /// These have limited access to Altinn and can only represent themselves. + /// + public sealed class SelfIdentifiedUser : Authenticated + { + /// + /// Username + /// + public string Username { get; } + + /// + /// User ID + /// + public int UserId { get; } + + /// + /// Party ID + /// + public int PartyId { get; } + + /// + /// Method of authentication, e.g. "idporten" or "maskinporten" + /// + public string AuthenticationMethod { get; } + + private Details? _extra; + private readonly Func> _getUserProfile; + private readonly ApplicationMetadata _appMetadata; + + internal SelfIdentifiedUser( + string username, + int userId, + int partyId, + string authenticationMethod, + TokenIssuer tokenIssuer, + bool tokenIsExchanged, + Scopes scopes, + string token, + Func> getUserProfile, + ApplicationMetadata appMetadata + ) + : base(tokenIssuer, tokenIsExchanged, scopes, token) + { + Username = username; + UserId = userId; + PartyId = partyId; + AuthenticationMethod = authenticationMethod; + // Since they are self-identified, they are always 0 + AuthenticationLevel = 0; + _getUserProfile = getUserProfile; + _appMetadata = appMetadata; + } + + /// + /// Authentication level + /// + public int AuthenticationLevel { get; } + + /// + /// Detailed information about a logged in user + /// + public sealed record Details(Party Party, UserProfile Profile, bool RepresentsSelf, bool CanInstantiate); + + /// + /// Load the details for the current user. + /// + /// + public async Task
LoadDetails() + { + if (_extra is not null) + return _extra; + + var userProfile = + await _getUserProfile(UserId) + ?? throw new AuthenticationContextException( + $"Could not get user profile for logged in self identified user: {UserId}" + ); + + var party = userProfile.Party; + var canInstantiate = InstantiationHelper.IsPartyAllowedToInstantiate(party, _appMetadata.PartyTypesAllowed); + _extra = new Details(party, userProfile, RepresentsSelf: true, canInstantiate); + return _extra; + } + } + + /// + /// The logged in client is an organisation (but they have not authenticated as an Altinn service owner). + /// Authentication has been done through Maskinporten. + /// + public sealed class Org : Authenticated + { + /// + /// Organisation number + /// + public string OrgNo { get; } + + /// + /// Authentication level + /// + public int AuthenticationLevel { get; } + + /// + /// Method of authentication, e.g. "idporten" or "maskinporten" + /// + public string AuthenticationMethod { get; } + + private readonly Func> _lookupParty; + private readonly ApplicationMetadata _appMetadata; + + internal Org( + string orgNo, + int authenticationLevel, + string authenticationMethod, + TokenIssuer tokenIssuer, + bool tokenIsExchanged, + Scopes scopes, + string token, + Func> lookupParty, + ApplicationMetadata appMetadata + ) + : base(tokenIssuer, tokenIsExchanged, scopes, token) + { + OrgNo = orgNo; + AuthenticationLevel = authenticationLevel; + AuthenticationMethod = authenticationMethod; + _lookupParty = lookupParty; + _appMetadata = appMetadata; + } + + /// + /// Detailed information about an organisation + /// + /// Party of the org + /// True if the org can instantiate applications + public sealed record Details(Party Party, bool CanInstantiate); + + /// + /// Load the details for the current organisation. + /// + /// Details + public async Task
LoadDetails() + { + var party = await _lookupParty(OrgNo); + + var canInstantiate = InstantiationHelper.IsPartyAllowedToInstantiate(party, _appMetadata.PartyTypesAllowed); + + return new Details(party, canInstantiate); + } + } + + /// + /// The logged in client is an Altinn service owner (i.e. they have the "urn:altinn:org" claim). + /// The service owner may or may not own the current app. + /// + public sealed class ServiceOwner : Authenticated + { + /// + /// Organisation/service owner name + /// + public string Name { get; } + + /// + /// Organisation number + /// + public string OrgNo { get; } + + /// + /// Authentication level + /// + public int AuthenticationLevel { get; } + + /// + /// Method of authentication, e.g. "idporten" or "maskinporten" + /// + public string AuthenticationMethod { get; } + + private readonly Func> _lookupParty; + + internal ServiceOwner( + string name, + string orgNo, + int authenticationLevel, + string authenticationMethod, + TokenIssuer tokenIssuer, + bool tokenIsExchanged, + Scopes scopes, + string token, + Func> lookupParty + ) + : base(tokenIssuer, tokenIsExchanged, scopes, token) + { + Name = name; + OrgNo = orgNo; + AuthenticationLevel = authenticationLevel; + AuthenticationMethod = authenticationMethod; + _lookupParty = lookupParty; + } + + /// + /// Detailed information about a service owner + /// + /// Party of the service owner + public sealed record Details(Party Party); + + /// + /// Load the details for the current service owner. + /// + /// Details + public async Task
LoadDetails() + { + var party = await _lookupParty(OrgNo); + return new Details(party); + } + } + + /// + /// The logged in client is a system user. + /// System users authenticate through Maskinporten. + /// The caller is the system, which impersonates the system user (which represents the organisation/owner of the user). + /// + public sealed class SystemUser : Authenticated + { + /// + /// System user ID + /// + public IReadOnlyList SystemUserId { get; } + + /// + /// Organisation number of the system user + /// + public OrganisationNumber SystemUserOrgNr { get; } + + /// + /// Organisation number of the supplier system + /// + public OrganisationNumber SupplierOrgNr { get; } + + /// + /// System ID + /// + public string SystemId { get; } + + /// + /// Authentication level + /// + public int AuthenticationLevel { get; } + + /// + /// Method of authentication + /// + public string AuthenticationMethod { get; } + + private readonly Func> _lookupParty; + private readonly ApplicationMetadata _appMetadata; + + internal SystemUser( + IReadOnlyList systemUserId, + OrganisationNumber systemUserOrgNr, + OrganisationNumber supplierOrgNr, + string systemId, + int? authenticationLevel, + string? authenticationMethod, + TokenIssuer tokenIssuer, + bool tokenIsExchanged, + Scopes scopes, + string token, + Func> lookupParty, + ApplicationMetadata appMetadata + ) + : base(tokenIssuer, tokenIsExchanged, scopes, token) + { + SystemUserId = systemUserId; + SystemUserOrgNr = systemUserOrgNr; + SupplierOrgNr = supplierOrgNr; + SystemId = systemId; + // System user tokens can either be raw Maskinporten or exchanged atm. + // If the token is not exchanged, we don't have these claims and so we default to what altinn-authentication currently does. + AuthenticationLevel = authenticationLevel ?? 3; + AuthenticationMethod = authenticationMethod ?? "maskinporten"; + _lookupParty = lookupParty; + _appMetadata = appMetadata; + } + + /// + /// Detailed information about a system user + /// + /// Party of the system user + /// True if the system user can instantiate applications + public sealed record Details(Party Party, bool CanInstantiate); + + /// + /// Load the details for the current system user. + /// + /// Details + public async Task
LoadDetails() + { + var party = await _lookupParty(SystemUserOrgNr.Get(OrganisationNumberFormat.Local)); + + var canInstantiate = InstantiationHelper.IsPartyAllowedToInstantiate(party, _appMetadata.PartyTypesAllowed); + + return new Details(party, canInstantiate); + } + } + + // TODO: app token? + // public sealed record App(string Token) : Authenticated; + + internal static (TokenIssuer Issuer, bool IsExchanged) ResolveIssuer( + string? iss, + string? authMethod, + string? acr, + bool isInAltinnPortal + ) + { + if (string.IsNullOrWhiteSpace(iss) && string.IsNullOrWhiteSpace(authMethod)) + return (TokenIssuer.None, false); + + // A token is exchanged if + // * issuer is altinn.no (either by verifying iss or that the urn:altinn:authenticatemethod claim is set) + // * scope does not contain altinn:portal/enduser (this is a special scope used only by altinn-authentication). + // This should hold true as long as we know we only get tokens from Altinn Authentication or ID porten/Maskinporten directly (otherwise the scope ownership is unclear) + var isExchanged = + ( + iss?.Contains("altinn.no", StringComparison.OrdinalIgnoreCase) is true + || !string.IsNullOrWhiteSpace(authMethod) + ) && !isInAltinnPortal; + + // If we have the special scope, we know the login was done through Altinn portal directly + // In any other case we want the underlying authentication method (ID-porten, Maskinporten) + if (isInAltinnPortal) + return (TokenIssuer.Altinn, isExchanged); + + if (iss is not null) + { + // If the issuer is not altinn.no, we know it is not exchanged and we can directly determine the issuer + if (iss.Contains("studio", StringComparison.OrdinalIgnoreCase)) + return (TokenIssuer.AltinnStudio, isExchanged); + if (iss.Contains("idporten.no", StringComparison.OrdinalIgnoreCase)) + return (TokenIssuer.IDporten, isExchanged); + if (iss.Contains("maskinporten.no", StringComparison.OrdinalIgnoreCase)) + return (TokenIssuer.Maskinporten, isExchanged); + } + if (authMethod is not null) + { + // IdportenTestId is the authmetod when logging into altinn portal with test users, e.g. in a tt02 app + // though this case should already be handled by the portal/enduser scope check + if (authMethod.Equals("IdportenTestId", StringComparison.OrdinalIgnoreCase)) + return (TokenIssuer.Altinn, isExchanged); + + if ( + authMethod.Equals("maskinporten", StringComparison.OrdinalIgnoreCase) // From altinn-authentication + || authMethod.Equals("systemuser", StringComparison.OrdinalIgnoreCase) // From AltinnTestTools + || authMethod.Equals("virksomhetsbruker", StringComparison.OrdinalIgnoreCase) // From altinn-authentication when using virksomhetsbruker + ) + { + Debug.Assert(isExchanged, "When we have authMethod, the token should always be exchanged"); + return (TokenIssuer.Maskinporten, isExchanged); + } + } + + // IDportens authenticationlevel equivalent will only be present if the token originates from ID-porten + // We should already be handling the ID-porten through Altinn portal case (with the scope) + if (acr?.StartsWith("idporten", StringComparison.OrdinalIgnoreCase) ?? false) + return (TokenIssuer.IDporten, isExchanged); + + return (TokenIssuer.Unknown, isExchanged); + } + + internal static Authenticated From( + string tokenStr, + bool isAuthenticated, + ApplicationMetadata appMetadata, + Func getSelectedParty, + Func> getUserProfile, + Func> lookupUserParty, + Func> lookupOrgParty, + Func?>> getPartyList, + Func> validateSelectedParty, + Func>> getUserRoles + ) + { + if (string.IsNullOrWhiteSpace(tokenStr)) + return new None(TokenIssuer.None, false, Scopes.None, tokenStr); + + var handler = new JwtSecurityTokenHandler(); + var token = handler.ReadJwtToken(tokenStr); + var claims = token.Claims; + + Claim? issuerClaim = null; + Claim? authLevelClaim = null; + Claim? authMethodClaim = null; + Claim? scopeClaim = null; + Claim? acrClaim = null; + Claim? orgClaim = null; + Claim? orgNoClaim = null; + Claim? partyIdClaim = null; + Claim? authorizationDetailsClaim = null; + Claim? userIdClaim = null; + Claim? usernameClaim = null; + Claim? consumerClaim = null; + OrgClaim? consumerClaimValue = null; + + static bool TryAssign(Claim claim, string name, [NotNullWhen(true)] ref Claim? dest) + { + if (claim.Type.Equals(name, StringComparison.OrdinalIgnoreCase)) + { + dest = claim; + return true; + } + return false; + } + + foreach (var claim in claims) + { + TryAssign(claim, JwtClaimTypes.Issuer, ref issuerClaim); + TryAssign(claim, AltinnCoreClaimTypes.AuthenticationLevel, ref authLevelClaim); + TryAssign(claim, AltinnCoreClaimTypes.AuthenticateMethod, ref authMethodClaim); + TryAssign(claim, JwtClaimTypes.Scope, ref scopeClaim); + TryAssign(claim, "acr", ref acrClaim); + TryAssign(claim, AltinnCoreClaimTypes.Org, ref orgClaim); + TryAssign(claim, AltinnCoreClaimTypes.OrgNumber, ref orgNoClaim); + TryAssign(claim, AltinnCoreClaimTypes.PartyID, ref partyIdClaim); + TryAssign(claim, "authorization_details", ref authorizationDetailsClaim); + TryAssign(claim, AltinnCoreClaimTypes.UserId, ref userIdClaim); + TryAssign(claim, AltinnCoreClaimTypes.UserName, ref usernameClaim); + if (TryAssign(claim, "consumer", ref consumerClaim)) + consumerClaimValue = JsonSerializer.Deserialize(consumerClaim.Value); + } + + var scopes = new Scopes(scopeClaim?.Value); + var isInAltinnPortal = scopes.HasScope("altinn:portal/enduser"); + + var (tokenIssuer, isExchanged) = ResolveIssuer( + issuerClaim?.Value, + authMethodClaim?.Value, + acrClaim?.Value, + isInAltinnPortal + ); + + if (!isAuthenticated) + return new None(tokenIssuer, isExchanged, scopes, tokenStr); + + int? partyId = null; + if (!string.IsNullOrWhiteSpace(partyIdClaim?.Value)) + { + if (!int.TryParse(partyIdClaim.Value, CultureInfo.InvariantCulture, out var partyIdClaimValue)) + throw new AuthenticationContextException( + $"Invalid party ID claim value for token: {partyIdClaim.Value}" + ); + partyId = partyIdClaimValue; + } + + static void ParseAuthLevel(string? value, out int authLevel) + { + if (string.IsNullOrWhiteSpace(value)) + throw new AuthenticationContextException($"Missing authentication level claim value for token"); + if (!int.TryParse(value, CultureInfo.InvariantCulture, out authLevel)) + throw new AuthenticationContextException( + $"Invalid authentication level claim value for token: {value}" + ); + + if (authLevel is < 0 or > 4) + throw new AuthenticationContextException( + $"Invalid authentication level claim value for token: {authLevel}" + ); + } + + int authLevel; + if (!string.IsNullOrWhiteSpace(authorizationDetailsClaim?.Value)) + { + var authorizationDetails = JsonSerializer.Deserialize( + authorizationDetailsClaim.Value + ); + if (authorizationDetails is null) + throw new AuthenticationContextException( + "Invalid authorization details claim value for systemuser token" + ); + if (authorizationDetails is not SystemUserAuthorizationDetailsClaim systemUser) + throw new AuthenticationContextException( + $"Unsupported authorization details claim value for systemuser token: {authorizationDetails.GetType().Name}" + ); + + if (systemUser is null) + throw new AuthenticationContextException( + "Invalid system user authorization details claim value for systemuser token" + ); + if (systemUser.SystemUserId is null || systemUser.SystemUserId.Count == 0) + throw new AuthenticationContextException("Missing system user ID claim for systemuser token"); + if (string.IsNullOrWhiteSpace(systemUser.SystemId)) + throw new AuthenticationContextException("Missing system ID claim for systemuser token"); + if (systemUser.SystemUserOrg.Authority != "iso6523-actorid-upis") + throw new AuthenticationContextException( + $"Unsupported organisation authority in systemuser token: {systemUser.SystemUserOrg.Authority}" + ); + if (!OrganisationNumber.TryParse(systemUser.SystemUserOrg.Id, out var orgNr)) + throw new AuthenticationContextException( + $"Invalid system user organisation number in system user token: {systemUser.SystemUserOrg.Id}" + ); + if (!OrganisationNumber.TryParse(consumerClaimValue?.Id, out var supplierOrgNr)) + throw new AuthenticationContextException( + $"Invalid organisation number in supplier organisation number claim for system user token: {consumerClaimValue?.Id}" + ); + + return new SystemUser( + systemUser.SystemUserId, + orgNr, + supplierOrgNr, + systemUser.SystemId, + int.TryParse(authLevelClaim?.Value, CultureInfo.InvariantCulture, out authLevel) ? authLevel : null, + !string.IsNullOrWhiteSpace(authMethodClaim?.Value) ? authMethodClaim.Value : null, + tokenIssuer, + isExchanged, + scopes, + tokenStr, + lookupOrgParty, + appMetadata + ); + } + else if (!string.IsNullOrWhiteSpace(orgClaim?.Value) && orgClaim.Value == appMetadata.Org) + { + // In this case the token should have a serviceowner scope, + // due to the `urn:altinn:org` claim + if (string.IsNullOrWhiteSpace(orgNoClaim?.Value)) + throw new AuthenticationContextException("Missing org number claim for service owner token"); + if (string.IsNullOrWhiteSpace(authMethodClaim?.Value)) + throw new AuthenticationContextException("Missing authentication method claim for service owner token"); + + ParseAuthLevel(authLevelClaim?.Value, out authLevel); + + return new ServiceOwner( + orgClaim.Value, + orgNoClaim.Value, + authLevel, + authMethodClaim.Value, + tokenIssuer, + isExchanged, + scopes, + tokenStr, + lookupOrgParty + ); + } + else if (!string.IsNullOrWhiteSpace(orgNoClaim?.Value)) + { + ParseAuthLevel(authLevelClaim?.Value, out authLevel); + if (string.IsNullOrWhiteSpace(authMethodClaim?.Value)) + throw new AuthenticationContextException("Missing authentication method claim for org token"); + + return new Org( + orgNoClaim.Value, + authLevel, + authMethodClaim.Value, + tokenIssuer, + isExchanged, + scopes, + tokenStr, + lookupOrgParty, + appMetadata + ); + } + + if (string.IsNullOrWhiteSpace(userIdClaim?.Value)) + throw new AuthenticationContextException("Missing user ID claim for user token"); + if (!int.TryParse(userIdClaim.Value, CultureInfo.InvariantCulture, out int userId)) + throw new AuthenticationContextException( + $"Invalid user ID claim value for user token: {userIdClaim.Value}" + ); + + if (partyId is null) + throw new AuthenticationContextException("Missing party ID for user token"); + if (string.IsNullOrWhiteSpace(authMethodClaim?.Value)) + throw new AuthenticationContextException("Missing authentication method claim for user token"); + + ParseAuthLevel(authLevelClaim?.Value, out authLevel); + if (authLevel == 0) + { + if (string.IsNullOrWhiteSpace(usernameClaim?.Value)) + throw new AuthenticationContextException("Missing username claim for self-identified user token"); + + return new SelfIdentifiedUser( + usernameClaim.Value, + userId, + partyId.Value, + authMethodClaim.Value, + tokenIssuer, + isExchanged, + scopes, + tokenStr, + getUserProfile, + appMetadata + ); + } + + int selectedPartyId = partyId.Value; + if (getSelectedParty() is { } selectedPartyStr) + { + if (!int.TryParse(selectedPartyStr, CultureInfo.InvariantCulture, out var selectedParty)) + throw new AuthenticationContextException($"Invalid party ID in cookie: {selectedPartyStr}"); // TODO: maybe not throw? + + selectedPartyId = selectedParty; + } + + return new User( + userId, + partyId.Value, + authLevel, + authMethodClaim.Value, + selectedPartyId, + isInAltinnPortal, + tokenIssuer, + isExchanged, + scopes, + tokenStr, + getUserProfile, + lookupUserParty, + getPartyList, + validateSelectedParty, + getUserRoles, + appMetadata + ); + } + + [JsonPolymorphic(TypeDiscriminatorPropertyName = "type")] + [JsonDerivedType(typeof(SystemUserAuthorizationDetailsClaim), typeDiscriminator: "urn:altinn:systemuser")] + internal record AuthorizationDetailsClaim(); + + internal sealed record SystemUserAuthorizationDetailsClaim( + [property: JsonPropertyName("systemuser_id")] IReadOnlyList SystemUserId, + [property: JsonPropertyName("system_id")] string SystemId, + [property: JsonPropertyName("systemuser_org")] OrgClaim SystemUserOrg + ) : AuthorizationDetailsClaim(); + + internal sealed record OrgClaim( + [property: JsonPropertyName("authority")] string Authority, + [property: JsonPropertyName("ID")] string Id + ); +} diff --git a/src/Altinn.App.Core/Features/Auth/AuthenticationContext.cs b/src/Altinn.App.Core/Features/Auth/AuthenticationContext.cs new file mode 100644 index 000000000..c61a1cd7d --- /dev/null +++ b/src/Altinn.App.Core/Features/Auth/AuthenticationContext.cs @@ -0,0 +1,89 @@ +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features.Cache; +using Altinn.App.Core.Internal.Auth; +using Altinn.App.Core.Internal.Profile; +using Altinn.App.Core.Internal.Registers; +using Altinn.Platform.Register.Models; +using AltinnCore.Authentication.Utils; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; + +namespace Altinn.App.Core.Features.Auth; + +internal sealed class AuthenticationContext : IAuthenticationContext +{ + private const string ItemsKey = "Internal_AltinnAuthenticationInfo"; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IOptionsMonitor _appSettings; + private readonly IOptionsMonitor _generalSettings; + private readonly IProfileClient _profileClient; + private readonly IAltinnPartyClient _altinnPartyClient; + private readonly IAuthorizationClient _authorizationClient; + private readonly IAppConfigurationCache _appConfigurationCache; + + public AuthenticationContext( + IHttpContextAccessor httpContextAccessor, + IOptionsMonitor appSettings, + IOptionsMonitor generalSettings, + IProfileClient profileClient, + IAltinnPartyClient altinnPartyClient, + IAuthorizationClient authorizationClient, + IAppConfigurationCache appConfigurationCache + ) + { + _httpContextAccessor = httpContextAccessor; + _appSettings = appSettings; + _generalSettings = generalSettings; + _profileClient = profileClient; + _altinnPartyClient = altinnPartyClient; + _authorizationClient = authorizationClient; + _appConfigurationCache = appConfigurationCache; + } + + // Currently we're coupling this to the HTTP context directly. + // In the future we might want to run work (e.g. service tasks) in the background, + // at which point we won't always have a HTTP context available. + // At that point we probably want to implement something like an `IExecutionContext`, `IExecutionContextAccessor` + // to decouple ourselves from the ASP.NET request context. + // TODO: consider removing dependcy on HTTP context + private HttpContext _httpContext => + _httpContextAccessor.HttpContext ?? throw new AuthenticationContextException("No HTTP context available"); + + public Authenticated Current + { + get + { + var httpContext = _httpContext; + + Authenticated authInfo; + if (!httpContext.Items.TryGetValue(ItemsKey, out var authInfoObj)) + { + var token = JwtTokenUtil.GetTokenFromContext(httpContext, _appSettings.CurrentValue.RuntimeCookieName); + var isAuthenticated = httpContext.User?.Identity?.IsAuthenticated ?? false; + + authInfo = Authenticated.From( + tokenStr: token, + isAuthenticated: isAuthenticated, + _appConfigurationCache.ApplicationMetadata, + () => _httpContext.Request.Cookies[_generalSettings.CurrentValue.GetAltinnPartyCookieName], + _profileClient.GetUserProfile, + _altinnPartyClient.GetParty, + (string orgNr) => _altinnPartyClient.LookupParty(new PartyLookup { OrgNo = orgNr }), + _authorizationClient.GetPartyList, + _authorizationClient.ValidateSelectedParty, + _authorizationClient.GetUserRoles + ); + httpContext.Items[ItemsKey] = authInfo; + } + else + { + authInfo = + authInfoObj as Authenticated + ?? throw new AuthenticationContextException( + "Unexpected type for authentication info in HTTP context" + ); + } + return authInfo; + } + } +} diff --git a/src/Altinn.App.Core/Features/Auth/AuthenticationContextDI.cs b/src/Altinn.App.Core/Features/Auth/AuthenticationContextDI.cs new file mode 100644 index 000000000..1bcad4f43 --- /dev/null +++ b/src/Altinn.App.Core/Features/Auth/AuthenticationContextDI.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Altinn.App.Core.Features.Auth; + +internal static class AuthenticationContextDI +{ + internal static void AddAuthenticationContext(this IServiceCollection services) + { + services.TryAddSingleton(); + } +} diff --git a/src/Altinn.App.Core/Features/Auth/AuthenticationContextException.cs b/src/Altinn.App.Core/Features/Auth/AuthenticationContextException.cs new file mode 100644 index 000000000..1181ee626 --- /dev/null +++ b/src/Altinn.App.Core/Features/Auth/AuthenticationContextException.cs @@ -0,0 +1,14 @@ +namespace Altinn.App.Core.Features.Auth; + +/// +/// Exception thrown when there is an issue with parsing the current authentication info. +/// +public class AuthenticationContextException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + /// + public AuthenticationContextException(string message) + : base(message) { } +} diff --git a/src/Altinn.App.Core/Features/Auth/IAuthenticationContext.cs b/src/Altinn.App.Core/Features/Auth/IAuthenticationContext.cs new file mode 100644 index 000000000..25be95d0e --- /dev/null +++ b/src/Altinn.App.Core/Features/Auth/IAuthenticationContext.cs @@ -0,0 +1,12 @@ +namespace Altinn.App.Core.Features.Auth; + +/// +/// Provides access to the current authentication context. +/// +public interface IAuthenticationContext +{ + /// + /// The current authentication info. + /// + Authenticated Current { get; } +} diff --git a/src/Altinn.App.Core/Features/Auth/Scopes.cs b/src/Altinn.App.Core/Features/Auth/Scopes.cs new file mode 100644 index 000000000..7fc7b7a1b --- /dev/null +++ b/src/Altinn.App.Core/Features/Auth/Scopes.cs @@ -0,0 +1,174 @@ +namespace Altinn.App.Core.Features.Auth; + +/// +/// Collection of scopes from a JWT token. +/// +public readonly struct Scopes : IEquatable +{ + /// + /// Empty scopes. + /// + public static readonly Scopes None = new Scopes(null); + + private readonly string? _scope; + + /// + /// Initializes a new instance of the struct. + /// + /// + public Scopes(string? scope) => _scope = scope; + + /// + /// Compares two objects for equality. + /// + /// + /// + public bool Equals(Scopes other) => _scope == other._scope; + + /// + /// Compares two objects for equality. + /// + /// + /// + public override bool Equals(object? obj) => obj is Scopes other && Equals(other); + + /// + /// Returns the hash code for this instance. + /// + /// + public override int GetHashCode() => _scope?.GetHashCode() ?? 0; + + /// + /// Compares two objects for equality. + /// + /// + /// + /// + public static bool operator ==(Scopes left, Scopes right) + { + return left.Equals(right); + } + + /// + /// Compares two objects for equality. + /// + /// + /// + /// + public static bool operator !=(Scopes left, Scopes right) + { + return !(left == right); + } + + /// + /// Returns a string that represents the current object. + /// + /// + public override string ToString() => _scope ?? ""; + + // private static readonly SearchValues _whitespace = SearchValues.Create(" \t\r\n"); + + /// + /// Returns an enumerator that iterates through the scopes. + /// + /// + public ScopeEnumerator GetEnumerator() => new ScopeEnumerator(_scope.AsSpan()); + + /// + /// Enumerator for iterating through the scopes. + /// + public ref struct ScopeEnumerator + { + private ReadOnlySpan _scopes; + private ReadOnlySpan _currentScope; + + /// + /// Initializes a new instance of the struct. + /// + /// + public ScopeEnumerator(ReadOnlySpan scopes) + { + _scopes = scopes; + _currentScope = ReadOnlySpan.Empty; + } + + /// + /// Gets the current scope of the enumerator. + /// + public readonly ReadOnlySpan Current => _currentScope; + + /// + /// Moves to the next scope in the enumerator. + /// + /// + public bool MoveNext() + { + if (_scopes.IsEmpty) + return false; + + for (var i = 0; i < _scopes.Length; i++) + { + if (!char.IsWhiteSpace(_scopes[i])) + { + for (int j = i + 1; j <= _scopes.Length; j++) + { + if (j == _scopes.Length) + { + _currentScope = _scopes.Slice(i); + _scopes = ReadOnlySpan.Empty; + return true; + } + else if (char.IsWhiteSpace(_scopes[j])) + { + _currentScope = _scopes.Slice(i, j - i); + _scopes = _scopes.Slice(j); + return true; + } + } + } + } + + return false; + } + } + + /// + /// Checks if the scopes contains a specific scope. + /// + /// the scope to search for + /// + public bool HasScope(string scopeToFind) + { + if (string.IsNullOrWhiteSpace(_scope)) + return false; + + foreach (var scope in this) + { + if (scope.Equals(scopeToFind, StringComparison.Ordinal)) + return true; + } + + return false; + } + + /// + /// Checks if any of the scopes contains a specific string prefix. + /// NOTE: this is not to be confused with the prefix concept in Maskinporten/ID porten, + /// this is strictly a string prefix, so you decide if there should be a delimiter at the end of the prefix + /// + /// the prefix to search for + /// + public bool HasScopeWithPrefix(string scopePrefix) + { + if (string.IsNullOrWhiteSpace(_scope)) + return false; + + foreach (var scope in this) + { + if (scope.StartsWith(scopePrefix, StringComparison.Ordinal)) + return true; + } + + return false; + } +} diff --git a/src/Altinn.App.Core/Features/Auth/TokenIssuer.cs b/src/Altinn.App.Core/Features/Auth/TokenIssuer.cs new file mode 100644 index 000000000..0f0a3dd1b --- /dev/null +++ b/src/Altinn.App.Core/Features/Auth/TokenIssuer.cs @@ -0,0 +1,37 @@ +namespace Altinn.App.Core.Features.Auth; + +/// +/// The type of the token, meaning how the user logged in +/// +public enum TokenIssuer +{ + /// + /// Token is missing or invalid + /// + None, + + /// + /// Token is unknown or not recognized + /// + Unknown, + + /// + /// Token is from Altinn portal or Altinn Authentication through token exchange + /// + Altinn, + + /// + /// Token is from Altinn Studio + /// + AltinnStudio, + + /// + /// Token is from external ID-porten, e.g. SBS + /// + IDporten, + + /// + /// Token is from Maskinporten directly, e.g. service owner token, org token, system user token (when not exchanged) + /// + Maskinporten, +} diff --git a/src/Altinn.App.Core/Features/Cache/AppConfigurationCache.cs b/src/Altinn.App.Core/Features/Cache/AppConfigurationCache.cs new file mode 100644 index 000000000..2c6016e8e --- /dev/null +++ b/src/Altinn.App.Core/Features/Cache/AppConfigurationCache.cs @@ -0,0 +1,109 @@ +using Altinn.App.Core.Configuration; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Models; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Altinn.App.Core.Features.Cache; + +internal sealed class AppConfigurationCache( + ILogger logger, + IServiceProvider serviceProvider, + IOptions generalSettings +) : BackgroundService, IAppConfigurationCache +{ + private readonly ILogger _logger = logger; + private readonly IServiceProvider _serviceProvider = serviceProvider; + private readonly IOptions _generalSettings = generalSettings; + + private ApplicationMetadata? _appMetadata; + + public ApplicationMetadata ApplicationMetadata => + _appMetadata ?? throw new InvalidOperationException("Cache not initialized"); + + private readonly TaskCompletionSource _firstTick = new(TaskCreationOptions.RunContinuationsAsynchronously); + + public override async Task StartAsync(CancellationToken cancellationToken) + { + await base.StartAsync(cancellationToken); + if (_generalSettings.Value.DisableAppConfigurationCache) + return; + await _firstTick.Task; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (_generalSettings.Value.DisableAppConfigurationCache) + return; + try + { + var env = _serviceProvider.GetRequiredService(); + if (env.IsDevelopment()) + { + // local dev, config can change + { + await using var scope = await Scope.Create(_serviceProvider); + await UpdateCache(this, scope, stoppingToken); + } + + using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5)); + + while (await timer.WaitForNextTickAsync(stoppingToken)) + { + await using var scope = await Scope.Create(_serviceProvider); + await UpdateCache(this, scope, stoppingToken); + } + } + else if (env.IsStaging()) + { + // tt02 (container deployment, immutable infra) + await using var scope = await Scope.Create(_serviceProvider); + await UpdateCache(this, scope, stoppingToken); + } + else if (env.IsProduction()) + { + // prod (container deployment, immutable infra) + await using var scope = await Scope.Create(_serviceProvider); + await UpdateCache(this, scope, stoppingToken); + } + } + catch (OperationCanceledException) + { + _firstTick.TrySetCanceled(stoppingToken); + } + catch (Exception ex) + { + _firstTick.TrySetException(ex); + _logger.LogError(ex, "Error starting AppConfigurationCache"); + } + + static async ValueTask UpdateCache(AppConfigurationCache self, Scope scope, CancellationToken cancellationToken) + { + self._appMetadata = await scope.AppMetadata.GetApplicationMetadata(); + + self._firstTick.TrySetResult(); + } + } + + private readonly record struct Scope(AsyncServiceScope Value, IAppMetadata AppMetadata) : IAsyncDisposable + { + public static async ValueTask Create(IServiceProvider serviceProvider) + { + var scope = serviceProvider.CreateAsyncScope(); + try + { + var appMetadata = serviceProvider.GetRequiredService(); + return new Scope(scope, appMetadata); + } + catch + { + await scope.DisposeAsync(); + throw; + } + } + + public ValueTask DisposeAsync() => Value.DisposeAsync(); + } +} diff --git a/src/Altinn.App.Core/Features/Cache/AppConfigurationCacheDI.cs b/src/Altinn.App.Core/Features/Cache/AppConfigurationCacheDI.cs new file mode 100644 index 000000000..deed347c1 --- /dev/null +++ b/src/Altinn.App.Core/Features/Cache/AppConfigurationCacheDI.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Altinn.App.Core.Features.Cache; + +internal static class AppConfigurationCacheDI +{ + public static IServiceCollection AddAppConfigurationCache(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(sp => sp.GetRequiredService()); + services.AddSingleton(sp => sp.GetRequiredService()); + return services; + } +} diff --git a/src/Altinn.App.Core/Features/Cache/IAppConfigurationCache.cs b/src/Altinn.App.Core/Features/Cache/IAppConfigurationCache.cs new file mode 100644 index 000000000..2047236ee --- /dev/null +++ b/src/Altinn.App.Core/Features/Cache/IAppConfigurationCache.cs @@ -0,0 +1,8 @@ +using Altinn.App.Core.Models; + +namespace Altinn.App.Core.Features.Cache; + +internal interface IAppConfigurationCache +{ + public ApplicationMetadata ApplicationMetadata { get; } +} diff --git a/src/Altinn.App.Core/Features/Telemetry/Telemetry.cs b/src/Altinn.App.Core/Features/Telemetry/Telemetry.cs index f0bf97423..1c87ea865 100644 --- a/src/Altinn.App.Core/Features/Telemetry/Telemetry.cs +++ b/src/Altinn.App.Core/Features/Telemetry/Telemetry.cs @@ -166,6 +166,26 @@ public static class Labels ///
public const string UserAuthenticationLevel = "user.authentication.level"; + /// + /// Label for the authentication type for the current client. + /// + public const string UserAuthenticationType = "user.authentication.type"; + + /// + /// Label for the authentication token issuer. + /// + public const string UserAuthenticationTokenIssuer = "user.authentication.token.issuer"; + + /// + /// Label for the authentication token isExchanged flag. + /// + public const string UserAuthenticationTokenIsExchanged = "user.authentication.token.isExchanged"; + + /// + /// Label for the authentication token is issued from Altinn portal. + /// + public const string UserAuthenticationInAltinnPortal = "user.authentication.inAltinnPortal"; + /// /// Label for the organisation name. /// @@ -176,6 +196,11 @@ public static class Labels ///
public const string OrganisationNumber = "organisation.number"; + /// + /// Label for the ID of the system user. + /// + public const string OrganisationSystemUserId = "organisation.systemuser.id"; + /// /// Label for the Correspondence ID. /// diff --git a/src/Altinn.App.Core/Features/Telemetry/TelemetryActivityExtensions.cs b/src/Altinn.App.Core/Features/Telemetry/TelemetryActivityExtensions.cs index 2e9c37a49..f4031fb9f 100644 --- a/src/Altinn.App.Core/Features/Telemetry/TelemetryActivityExtensions.cs +++ b/src/Altinn.App.Core/Features/Telemetry/TelemetryActivityExtensions.cs @@ -1,5 +1,7 @@ using System.Diagnostics; +using Altinn.App.Core.Features.Auth; using Altinn.App.Core.Features.Correspondence.Models; +using Altinn.App.Core.Models; using Altinn.App.Core.Models.Process; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Mvc; @@ -332,7 +334,6 @@ internal static Activity SetProblemDetails(this Activity activity, ProblemDetail if (fromTask is not null) { tags.Add("from.task.name", fromTask.Name); - tags.Add("from.task.validation.status", fromTask.Validated?.ToString()); } } var to = change.NewProcessState; @@ -343,7 +344,6 @@ internal static Activity SetProblemDetails(this Activity activity, ProblemDetail if (toTask is not null) { tags.Add("to.task.name", toTask.Name); - tags.Add("to.task.validation.status", toTask.Validated?.ToString()); } } activity.AddEvent(new ActivityEvent("change", tags: tags)); @@ -352,6 +352,67 @@ internal static Activity SetProblemDetails(this Activity activity, ProblemDetail return activity; } + internal static Activity? SetAuthenticated(this Activity? activity, Authenticated currentAuth) + { + if (activity is null) + return null; + + activity.SetTag(Labels.UserAuthenticationType, currentAuth.GetType().Name); + activity.SetTag(Labels.UserAuthenticationTokenIssuer, currentAuth.TokenIssuer); + activity.SetTag(Labels.UserAuthenticationTokenIsExchanged, currentAuth.TokenIsExchanged); + switch (currentAuth) + { + case Authenticated.None: + break; + case Authenticated.User auth: + { + activity.SetUserId(auth.UserId); + activity.SetUserPartyId(auth.SelectedPartyId); + activity.SetAuthenticationMethod(auth.AuthenticationMethod); + activity.SetAuthenticationLevel(auth.AuthenticationLevel); + activity.SetTag(Labels.UserAuthenticationInAltinnPortal, auth.InAltinnPortal); + break; + } + case Authenticated.SelfIdentifiedUser auth: + { + activity.SetUserId(auth.UserId); + activity.SetUserPartyId(auth.PartyId); + activity.SetAuthenticationMethod(auth.AuthenticationMethod); + activity.SetAuthenticationLevel(auth.AuthenticationLevel); + break; + } + case Authenticated.Org auth: + { + activity.SetOrganisationNumber(auth.OrgNo); + activity.SetAuthenticationMethod(auth.AuthenticationMethod); + activity.SetAuthenticationLevel(auth.AuthenticationLevel); + break; + } + case Authenticated.ServiceOwner auth: + { + activity.SetOrganisationNumber(auth.OrgNo); + activity.SetOrganisationName(auth.Name); + activity.SetAuthenticationMethod(auth.AuthenticationMethod); + activity.SetAuthenticationLevel(auth.AuthenticationLevel); + break; + } + case Authenticated.SystemUser auth: + { + if (auth.SystemUserId is [var systemUserId, ..]) + activity.SetTag(Labels.OrganisationSystemUserId, systemUserId); + + activity.SetOrganisationNumber(auth.SystemUserOrgNr.Get(OrganisationNumberFormat.Local)); + activity.SetAuthenticationLevel(auth.AuthenticationLevel); + activity.SetAuthenticationMethod(auth.AuthenticationMethod); + break; + } + default: + break; + } + + return activity; + } + /// /// Used to record an exception on the activity. /// Should be used when diff --git a/src/Altinn.App.Core/Helpers/InstantiationHelper.cs b/src/Altinn.App.Core/Helpers/InstantiationHelper.cs index c93cf2108..ba3d0b7c8 100644 --- a/src/Altinn.App.Core/Helpers/InstantiationHelper.cs +++ b/src/Altinn.App.Core/Helpers/InstantiationHelper.cs @@ -21,7 +21,7 @@ public static class InstantiationHelper /// The allowed party types /// A list with the filtered parties public static List FilterPartiesByAllowedPartyTypes( - List? parties, + IReadOnlyList? parties, PartyTypesAllowed? partyTypesAllowed ) { @@ -31,7 +31,7 @@ public static List FilterPartiesByAllowedPartyTypes( return allowed; } - parties.ForEach(party => + foreach (var party in parties) { bool canPartyInstantiate = IsPartyAllowedToInstantiate(party, partyTypesAllowed); bool isChildPartyAllowed = false; @@ -75,7 +75,8 @@ public static List FilterPartiesByAllowedPartyTypes( party.ChildParties = new List(); allowed.Add(party); } - }); + } + return allowed; } diff --git a/src/Altinn.App.Core/Helpers/UserHelper.cs b/src/Altinn.App.Core/Helpers/UserHelper.cs index 742d2ee67..342b8faad 100644 --- a/src/Altinn.App.Core/Helpers/UserHelper.cs +++ b/src/Altinn.App.Core/Helpers/UserHelper.cs @@ -14,6 +14,7 @@ namespace Altinn.App.Core.Helpers; /// /// The helper for user functionality /// +[Obsolete("Use Altinn.App.Core.Features.Auth.IAuthenticationContext instead")] public class UserHelper { private readonly IProfileClient _profileClient; diff --git a/src/Altinn.App.Core/Implementation/PrefillSI.cs b/src/Altinn.App.Core/Implementation/PrefillSI.cs index e5bd7c577..72b5054b4 100644 --- a/src/Altinn.App.Core/Implementation/PrefillSI.cs +++ b/src/Altinn.App.Core/Implementation/PrefillSI.cs @@ -1,14 +1,12 @@ using System.Globalization; using System.Reflection; using Altinn.App.Core.Features; -using Altinn.App.Core.Helpers; +using Altinn.App.Core.Features.Auth; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Prefill; -using Altinn.App.Core.Internal.Profile; using Altinn.App.Core.Internal.Registers; using Altinn.Platform.Profile.Models; using Altinn.Platform.Register.Models; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; @@ -18,10 +16,9 @@ namespace Altinn.App.Core.Implementation; public class PrefillSI : IPrefill { private readonly ILogger _logger; - private readonly IProfileClient _profileClient; private readonly IAppResources _appResourcesService; private readonly IAltinnPartyClient _altinnPartyClientClient; - private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IAuthenticationContext _authenticationContext; private readonly Telemetry? _telemetry; private static readonly string _erKey = "ER"; private static readonly string _dsfKey = "DSF"; @@ -33,25 +30,22 @@ public class PrefillSI : IPrefill /// Creates a new instance of the class /// /// The logger - /// The profile client /// The app's resource service /// The register client - /// A service with access to the http context. + /// The authentication context /// Telemetry for traces and metrics. public PrefillSI( ILogger logger, - IProfileClient profileClient, IAppResources appResourcesService, IAltinnPartyClient altinnPartyClientClient, - IHttpContextAccessor httpContextAccessor, + IAuthenticationContext authenticationContext, Telemetry? telemetry = null ) { _logger = logger; - _profileClient = profileClient; _appResourcesService = appResourcesService; _altinnPartyClientClient = altinnPartyClientClient; - _httpContextAccessor = httpContextAccessor; + _authenticationContext = authenticationContext; _telemetry = telemetry; } @@ -94,7 +88,17 @@ public async Task PrefillDataModel( _allowOverwrite = allowOverwriteToken.ToObject(); } - Party? party = await _altinnPartyClientClient.GetParty(int.Parse(partyId, CultureInfo.InvariantCulture)); + var currentAuth = _authenticationContext.Current; + var partyIdNum = int.Parse(partyId, CultureInfo.InvariantCulture); + Party? party = currentAuth switch + { + Authenticated.User user when user.SelectedPartyId == partyIdNum => await user.LookupSelectedParty(), + Authenticated.SelfIdentifiedUser user when user.PartyId == partyIdNum => (await user.LoadDetails()).Party, + Authenticated.SystemUser systemUser + when await systemUser.LoadDetails() is { } details && details.Party.PartyId == partyIdNum => + details.Party, + _ => await _altinnPartyClientClient.GetParty(partyIdNum), + }; if (party == null) { string errorMessage = $"Could find party for partyId: {partyId}"; @@ -113,13 +117,23 @@ public async Task PrefillDataModel( if (userProfileDict.Count > 0) { - var httpContext = - _httpContextAccessor.HttpContext - ?? throw new Exception( - "Could not get HttpContext - must be in a request context to get current user" - ); - int userId = AuthenticationHelper.GetUserId(httpContext); - UserProfile? userProfile = userId != 0 ? await _profileClient.GetUserProfile(userId) : null; + UserProfile? userProfile = null; + switch (currentAuth) + { + case Authenticated.User user: + { + var details = await user.LoadDetails(validateSelectedParty: false); + userProfile = details.Profile; + break; + } + case Authenticated.SelfIdentifiedUser user: + { + var details = await user.LoadDetails(); + userProfile = details.Profile; + break; + } + } + if (userProfile != null) { JObject userProfileJsonObject = JObject.FromObject(userProfile); diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Authentication/AuthenticationClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Authentication/AuthenticationClient.cs index 29b1de1ca..cc574c86d 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Authentication/AuthenticationClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Authentication/AuthenticationClient.cs @@ -2,9 +2,8 @@ using Altinn.App.Core.Configuration; using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; +using Altinn.App.Core.Features.Auth; using Altinn.App.Core.Internal.Auth; -using AltinnCore.Authentication.Utils; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -16,36 +15,36 @@ namespace Altinn.App.Core.Infrastructure.Clients.Authentication; public class AuthenticationClient : IAuthenticationClient { private readonly ILogger _logger; - private readonly IHttpContextAccessor _httpContextAccessor; private readonly HttpClient _client; + private readonly IAuthenticationContext _authenticationContext; /// /// Initializes a new instance of the class /// /// The current platform settings. /// the logger - /// The http context accessor /// A HttpClient provided by the HttpClientFactory. + /// The authentication context. public AuthenticationClient( IOptions platformSettings, ILogger logger, - IHttpContextAccessor httpContextAccessor, - HttpClient httpClient + HttpClient httpClient, + IAuthenticationContext authenticationContext ) { _logger = logger; - _httpContextAccessor = httpContextAccessor; httpClient.BaseAddress = new Uri(platformSettings.Value.ApiAuthenticationEndpoint); httpClient.DefaultRequestHeaders.Add(General.SubscriptionKeyHeaderName, platformSettings.Value.SubscriptionKey); httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); _client = httpClient; + _authenticationContext = authenticationContext; } /// public async Task RefreshToken() { string endpointUrl = $"refresh"; - string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, General.RuntimeCookieName); + string token = _authenticationContext.Current.Token; // TODO: check if authenticated? HttpResponseMessage response = await _client.GetAsync(token, endpointUrl); if (response.StatusCode == System.Net.HttpStatusCode.OK) diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Storage/SignClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Storage/SignClient.cs index 08e2a9eb3..5fa32d42d 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Storage/SignClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Storage/SignClient.cs @@ -64,6 +64,7 @@ private static JsonContent BuildSignRequest(SignatureContext signatureContext) UserId = signatureContext.Signee.UserId, PersonNumber = signatureContext.Signee.PersonNumber, OrganisationNumber = signatureContext.Signee.OrganisationNumber, + SystemUserId = signatureContext.Signee.SystemUserId, }, SignatureDocumentDataType = signatureContext.SignatureDataTypeId, DataElementSignatures = new(), diff --git a/src/Altinn.App.Core/Internal/Auth/AuthorizationService.cs b/src/Altinn.App.Core/Internal/Auth/AuthorizationService.cs index 910060db2..fcad888d5 100644 --- a/src/Altinn.App.Core/Internal/Auth/AuthorizationService.cs +++ b/src/Altinn.App.Core/Internal/Auth/AuthorizationService.cs @@ -1,6 +1,7 @@ using System.Security.Claims; using Altinn.App.Core.Features; using Altinn.App.Core.Features.Action; +using Altinn.App.Core.Features.Auth; using Altinn.App.Core.Internal.Process.Authorization; using Altinn.App.Core.Internal.Process.Elements; using Altinn.App.Core.Internal.Process.Elements.AltinnExtensionProperties; @@ -17,6 +18,7 @@ public class AuthorizationService : IAuthorizationService { private readonly IAuthorizationClient _authorizationClient; private readonly IEnumerable _userActionAuthorizers; + private readonly IAuthenticationContext _authenticationContext; private readonly Telemetry? _telemetry; /// @@ -24,15 +26,18 @@ public class AuthorizationService : IAuthorizationService /// /// The authorization client /// The user action authorizers + /// The authentication context /// Telemetry for traces and metrics. public AuthorizationService( IAuthorizationClient authorizationClient, IEnumerable userActionAuthorizers, + IAuthenticationContext authenticationContext, Telemetry? telemetry = null ) { _authorizationClient = authorizationClient; _userActionAuthorizers = userActionAuthorizers; + _authenticationContext = authenticationContext; _telemetry = telemetry; } @@ -71,7 +76,13 @@ var authorizerRegistrator in _userActionAuthorizers.Where(a => ) ) { - var context = new UserActionAuthorizerContext(user, instanceIdentifier, taskId, action); + var context = new UserActionAuthorizerContext( + user, + instanceIdentifier, + taskId, + action, + _authenticationContext.Current + ); if (!await authorizerRegistrator.Authorizer.AuthorizeAction(context)) { return false; diff --git a/src/Altinn.App.Core/Internal/LocaltestValidation.cs b/src/Altinn.App.Core/Internal/LocaltestValidation.cs index f4255c6ee..ac97fec0a 100644 --- a/src/Altinn.App.Core/Internal/LocaltestValidation.cs +++ b/src/Altinn.App.Core/Internal/LocaltestValidation.cs @@ -2,7 +2,6 @@ using System.Net; using System.Threading.Channels; using Altinn.App.Core.Configuration; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -12,13 +11,8 @@ namespace Altinn.App.Core.Internal; internal static class LocaltestValidationDI { - public static IServiceCollection AddLocaltestValidation( - this IServiceCollection services, - IConfiguration configuration - ) + public static IServiceCollection AddLocaltestValidation(this IServiceCollection services) { - if (configuration.GetValue("GeneralSettings:DisableLocaltestValidation")) - return services; services.AddHostedService(); return services; } diff --git a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs index 47e708466..8e5e220bd 100644 --- a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs +++ b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs @@ -1,15 +1,12 @@ using System.Globalization; -using System.Security.Claims; using Altinn.App.Core.Configuration; -using Altinn.App.Core.Extensions; using Altinn.App.Core.Features; +using Altinn.App.Core.Features.Auth; using Altinn.App.Core.Helpers.Extensions; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Data; using Altinn.App.Core.Internal.Language; -using Altinn.App.Core.Internal.Profile; using Altinn.App.Core.Models; -using Altinn.Platform.Profile.Models; using Altinn.Platform.Storage.Interface.Models; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -26,11 +23,11 @@ public class PdfService : IPdfService private readonly IAppResources _resourceService; private readonly IDataClient _dataClient; private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IProfileClient _profileClient; private readonly IPdfGeneratorClient _pdfGeneratorClient; private readonly PdfGeneratorSettings _pdfGeneratorSettings; private readonly ILogger _logger; + private readonly IAuthenticationContext _authenticationContext; private readonly GeneralSettings _generalSettings; private readonly Telemetry? _telemetry; private const string PdfElementType = "ref-data-as-pdf"; @@ -42,32 +39,32 @@ public class PdfService : IPdfService /// The service giving access to local resources. /// The data client. /// The httpContextAccessor - /// The profile client /// PDF generator client for the experimental PDF generator service /// PDF generator related settings. /// The app general settings. /// The logger. + /// The auth context. /// Telemetry for metrics and traces. public PdfService( IAppResources appResources, IDataClient dataClient, IHttpContextAccessor httpContextAccessor, - IProfileClient profileClient, IPdfGeneratorClient pdfGeneratorClient, IOptions pdfGeneratorSettings, IOptions generalSettings, ILogger logger, + IAuthenticationContext authenticationContext, Telemetry? telemetry = null ) { _resourceService = appResources; _dataClient = dataClient; _httpContextAccessor = httpContextAccessor; - _profileClient = profileClient; _pdfGeneratorClient = pdfGeneratorClient; _pdfGeneratorSettings = pdfGeneratorSettings.Value; _generalSettings = generalSettings.Value; _logger = logger; + _authenticationContext = authenticationContext; _telemetry = telemetry; } @@ -78,9 +75,9 @@ public async Task GenerateAndStorePdf(Instance instance, string taskId, Cancella HttpContext? httpContext = _httpContextAccessor.HttpContext; var queries = httpContext?.Request.Query; - var user = httpContext?.User; + var auth = _authenticationContext.Current; - var language = GetOverriddenLanguage(queries) ?? await GetLanguage(user); + var language = GetOverriddenLanguage(queries) ?? await auth.GetLanguage(); TextResource? textResource = await GetTextResource(instance, language); @@ -97,9 +94,9 @@ public async Task GeneratePdf(Instance instance, string taskId, bool isP HttpContext? httpContext = _httpContextAccessor.HttpContext; var queries = httpContext?.Request.Query; - var user = httpContext?.User; + var auth = _authenticationContext.Current; - var language = GetOverriddenLanguage(queries) ?? await GetLanguage(user); + var language = GetOverriddenLanguage(queries) ?? await auth.GetLanguage(); TextResource? textResource = await GetTextResource(instance, language); @@ -162,32 +159,6 @@ private static Uri BuildUri(string baseUrl, string pagePath, string language) return new Uri(url); } - internal async Task GetLanguage(ClaimsPrincipal? user) - { - string language = LanguageConst.Nb; - - if (user is null) - { - return language; - } - - int? userId = user.GetUserIdAsInt(); - - if (userId is not null) - { - UserProfile userProfile = - await _profileClient.GetUserProfile((int)userId) - ?? throw new Exception("Could not get user profile while getting language"); - - if (!string.IsNullOrEmpty(userProfile.ProfileSettingPreference?.Language)) - { - language = userProfile.ProfileSettingPreference.Language; - } - } - - return language; - } - internal static string? GetOverriddenLanguage(IQueryCollection? queries) { if (queries is null) diff --git a/src/Altinn.App.Core/Internal/Process/Elements/AppProcessElementInfo.cs b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessElementInfo.cs index b4990df81..2d904e364 100644 --- a/src/Altinn.App.Core/Internal/Process/Elements/AppProcessElementInfo.cs +++ b/src/Altinn.App.Core/Internal/Process/Elements/AppProcessElementInfo.cs @@ -29,7 +29,9 @@ public AppProcessElementInfo(ProcessElementInfo processElementInfo) Name = processElementInfo.Name; AltinnTaskType = processElementInfo.AltinnTaskType; Ended = processElementInfo.Ended; +#pragma warning disable CS0618 // Type or member is obsolete Validated = processElementInfo.Validated; +#pragma warning restore CS0618 // Type or member is obsolete FlowType = processElementInfo.FlowType; Actions = new Dictionary(); UserActions = new List(); diff --git a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs index f9c8dea05..d68f3e3d3 100644 --- a/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs +++ b/src/Altinn.App.Core/Internal/Process/ProcessEngine.cs @@ -1,8 +1,8 @@ using System.Diagnostics; -using System.Security.Claims; using Altinn.App.Core.Extensions; using Altinn.App.Core.Features; using Altinn.App.Core.Features.Action; +using Altinn.App.Core.Features.Auth; using Altinn.App.Core.Helpers; using Altinn.App.Core.Helpers.Serialization; using Altinn.App.Core.Internal.App; @@ -11,10 +11,8 @@ using Altinn.App.Core.Internal.Process.Elements; using Altinn.App.Core.Internal.Process.Elements.Base; using Altinn.App.Core.Internal.Process.ProcessTasks; -using Altinn.App.Core.Internal.Profile; using Altinn.App.Core.Models.Process; using Altinn.App.Core.Models.UserAction; -using Altinn.Platform.Profile.Models; using Altinn.Platform.Storage.Interface.Enums; using Altinn.Platform.Storage.Interface.Models; @@ -26,12 +24,12 @@ namespace Altinn.App.Core.Internal.Process; public class ProcessEngine : IProcessEngine { private readonly IProcessReader _processReader; - private readonly IProfileClient _profileClient; private readonly IProcessNavigator _processNavigator; private readonly IProcessEventHandlerDelegator _processEventHandlerDelegator; private readonly IProcessEventDispatcher _processEventDispatcher; private readonly UserActionService _userActionService; private readonly Telemetry? _telemetry; + private readonly IAuthenticationContext _authenticationContext; private readonly IProcessTaskCleaner _processTaskCleaner; private readonly IDataClient _dataClient; private readonly IInstanceClient _instanceClient; @@ -43,7 +41,6 @@ public class ProcessEngine : IProcessEngine ///
public ProcessEngine( IProcessReader processReader, - IProfileClient profileClient, IProcessNavigator processNavigator, IProcessEventHandlerDelegator processEventsDelegator, IProcessEventDispatcher processEventDispatcher, @@ -53,11 +50,11 @@ public ProcessEngine( IInstanceClient instanceClient, ModelSerializationService modelSerialization, IAppMetadata appMetadata, + IAuthenticationContext authenticationContext, Telemetry? telemetry = null ) { _processReader = processReader; - _profileClient = profileClient; _processNavigator = processNavigator; _processEventHandlerDelegator = processEventsDelegator; _processEventDispatcher = processEventDispatcher; @@ -68,6 +65,7 @@ public ProcessEngine( _modelSerialization = modelSerialization; _appMetadata = appMetadata; _telemetry = telemetry; + _authenticationContext = authenticationContext; } /// @@ -111,13 +109,9 @@ out ProcessError? startEventError ); // start process - ProcessStateChange? startChange = await ProcessStart( - processStartRequest.Instance, - validStartElement, - processStartRequest.User - ); + ProcessStateChange? startChange = await ProcessStart(processStartRequest.Instance, validStartElement); InstanceEvent? startEvent = startChange?.Events?[0].CopyValues(); - ProcessStateChange? nextChange = await ProcessNext(processStartRequest.Instance, processStartRequest.User); + ProcessStateChange? nextChange = await ProcessNext(processStartRequest.Instance); InstanceEvent? goToNextEvent = nextChange?.Events?[0].CopyValues(); List events = []; if (startEvent is not null) @@ -168,7 +162,7 @@ public async Task Next(ProcessNextRequest request) // TODO: Move this logic to ProcessTaskInitializer.Initialize once the authentication model supports a service/app user with the appropriate scopes await _processTaskCleaner.RemoveAllDataElementsGeneratedFromTask(instance, currentElementId); - int? userId = request.User.GetUserIdAsInt(); + var currentAuth = _authenticationContext.Current; IUserAction? actionHandler = _userActionService.GetActionHandler(request.Action); var cachedDataMutator = new InstanceDataUnitOfWork( instance, @@ -178,10 +172,21 @@ await _appMetadata.GetApplicationMetadata(), _modelSerialization ); + int? userId = currentAuth switch + { + Authenticated.User auth => auth.UserId, + Authenticated.SelfIdentifiedUser auth => auth.UserId, + _ => null, + }; UserActionResult actionResult = actionHandler is null ? UserActionResult.SuccessResult() : await actionHandler.HandleAction( - new UserActionContext(cachedDataMutator, userId, language: request.Language) + new UserActionContext( + cachedDataMutator, + userId, + language: request.Language, + authentication: currentAuth + ) ); if (actionResult.ResultType != ResultType.Success) @@ -209,7 +214,7 @@ await _appMetadata.GetApplicationMetadata(), // TODO: consider using the same cachedDataMutator for the rest of the process to avoid refetching data from storage - ProcessStateChange? nextResult = await HandleMoveToNext(instance, request.User, request.Action); + ProcessStateChange? nextResult = await HandleMoveToNext(instance, request.Action); if (nextResult?.NewProcessState?.Ended is not null) { @@ -241,7 +246,7 @@ public async Task HandleEventsAndUpdateStorage( /// /// Does not save process. Instance object is updated. /// - private async Task ProcessStart(Instance instance, string startEvent, ClaimsPrincipal user) + private async Task ProcessStart(Instance instance, string startEvent) { if (instance.Process != null) { @@ -260,7 +265,7 @@ public async Task HandleEventsAndUpdateStorage( List events = [ - await GenerateProcessChangeEvent(InstanceEventType.process_StartEvent.ToString(), instance, now, user), + await GenerateProcessChangeEvent(InstanceEventType.process_StartEvent.ToString(), instance, now), ]; // ! TODO: should probably improve nullability handling in the next major version @@ -275,11 +280,7 @@ await GenerateProcessChangeEvent(InstanceEventType.process_StartEvent.ToString() /// /// Moves instance's process to nextElement id. Returns the instance together with process events. /// - private async Task ProcessNext( - Instance instance, - ClaimsPrincipal userContext, - string? action = null - ) + private async Task ProcessNext(Instance instance, string? action = null) { if (instance.Process == null) { @@ -294,17 +295,13 @@ await GenerateProcessChangeEvent(InstanceEventType.process_StartEvent.ToString() CurrentTask = instance.Process.CurrentTask, StartEvent = instance.Process.StartEvent, }, - Events = await MoveProcessToNext(instance, userContext, action), + Events = await MoveProcessToNext(instance, action), NewProcessState = instance.Process, }; return result; } - private async Task> MoveProcessToNext( - Instance instance, - ClaimsPrincipal user, - string? action = null - ) + private async Task> MoveProcessToNext(Instance instance, string? action = null) { List events = []; @@ -328,7 +325,7 @@ private async Task> MoveProcessToNext( eventType = InstanceEventType.process_AbandonTask.ToString(); } - events.Add(await GenerateProcessChangeEvent(eventType, instance, now, user)); + events.Add(await GenerateProcessChangeEvent(eventType, instance, now)); instance.Process = currentState; } @@ -346,12 +343,10 @@ private async Task> MoveProcessToNext( currentState.Ended = now; currentState.EndEvent = nextElementId; - events.Add( - await GenerateProcessChangeEvent(InstanceEventType.process_EndEvent.ToString(), instance, now, user) - ); + events.Add(await GenerateProcessChangeEvent(InstanceEventType.process_EndEvent.ToString(), instance, now)); // add submit event (to support Altinn2 SBL) - events.Add(await GenerateProcessChangeEvent(InstanceEventType.Submited.ToString(), instance, now, user)); + events.Add(await GenerateProcessChangeEvent(InstanceEventType.Submited.ToString(), instance, now)); } else if (_processReader.IsProcessTask(nextElementId)) { @@ -366,12 +361,9 @@ await GenerateProcessChangeEvent(InstanceEventType.process_EndEvent.ToString(), FlowType = action is "reject" ? ProcessSequenceFlowType.AbandonCurrentMoveToNext.ToString() : ProcessSequenceFlowType.CompleteCurrentMoveToNext.ToString(), - Validated = null, }; - events.Add( - await GenerateProcessChangeEvent(InstanceEventType.process_StartTask.ToString(), instance, now, user) - ); + events.Add(await GenerateProcessChangeEvent(InstanceEventType.process_StartTask.ToString(), instance, now)); } // current state points to the instance's process object. The following statement is unnecessary, but clarifies logic. @@ -380,41 +372,75 @@ await GenerateProcessChangeEvent(InstanceEventType.process_StartTask.ToString(), return events; } - private async Task GenerateProcessChangeEvent( - string eventType, - Instance instance, - DateTime now, - ClaimsPrincipal user - ) + private async Task GenerateProcessChangeEvent(string eventType, Instance instance, DateTime now) { - int? userId = user.GetUserIdAsInt(); + var currentAuth = _authenticationContext.Current; + PlatformUser user; + switch (currentAuth) + { + case Authenticated.User auth: + { + var details = await auth.LoadDetails(validateSelectedParty: true); + user = new PlatformUser + { + UserId = auth.UserId, + AuthenticationLevel = auth.AuthenticationLevel, + NationalIdentityNumber = details.Profile.Party.SSN, + }; + break; + } + case Authenticated.SelfIdentifiedUser auth: + { + var details = await auth.LoadDetails(); + user = new PlatformUser + { + UserId = auth.UserId, + AuthenticationLevel = auth.AuthenticationLevel, + NationalIdentityNumber = details.Profile.Party.SSN, // This is probably null? + }; + break; + } + case Authenticated.Org: + { + user = new PlatformUser { }; // TODO: what do we do here? + break; + } + case Authenticated.ServiceOwner auth: + { + user = new PlatformUser { OrgId = auth.Name, AuthenticationLevel = auth.AuthenticationLevel }; + break; + } + case Authenticated.SystemUser auth: + { + user = new PlatformUser + { + SystemUserId = auth.SystemUserId[0], + SystemUserOwnerOrgNo = auth.SystemUserOrgNr.Get(Models.OrganisationNumberFormat.Local), + SystemUserName = null, // TODO: will get this name later when a lookup API is implemented or the name is passed in token + AuthenticationLevel = auth.AuthenticationLevel, + }; + break; + } + default: + throw new Exception($"Unknown authentication context: {currentAuth.GetType().Name}"); + } + InstanceEvent instanceEvent = new() { InstanceId = instance.Id, InstanceOwnerPartyId = instance.InstanceOwner.PartyId, EventType = eventType, Created = now, - User = new PlatformUser - { - UserId = userId, - AuthenticationLevel = user.GetAuthenticationLevel(), - OrgId = user.GetOrg(), - }, + User = user, ProcessInfo = instance.Process, }; - if (string.IsNullOrEmpty(instanceEvent.User.OrgId) && userId != null) - { - UserProfile? up = await _profileClient.GetUserProfile((int)userId); - instanceEvent.User.NationalIdentityNumber = up?.Party.SSN; //TODO: Should we throw error if both OrgId and userProfile is null? - } - return instanceEvent; } - private async Task HandleMoveToNext(Instance instance, ClaimsPrincipal user, string? action) + private async Task HandleMoveToNext(Instance instance, string? action) { - ProcessStateChange? processStateChange = await ProcessNext(instance, user, action); + ProcessStateChange? processStateChange = await ProcessNext(instance, action); if (processStateChange == null) { diff --git a/src/Altinn.App.Core/Internal/Sign/SignatureContext.cs b/src/Altinn.App.Core/Internal/Sign/SignatureContext.cs index f65ef6b8f..70c1e504d 100644 --- a/src/Altinn.App.Core/Internal/Sign/SignatureContext.cs +++ b/src/Altinn.App.Core/Internal/Sign/SignatureContext.cs @@ -92,6 +92,11 @@ public class Signee #nullable restore + /// + /// The system user id of the user performing the signing + /// + public Guid? SystemUserId { get; set; } + /// /// The SSN of the user performing the signing, set if the signer is a person /// diff --git a/src/Altinn.App.Core/Models/OrganisationNumber.cs b/src/Altinn.App.Core/Models/OrganisationNumber.cs index 2b3cf3cd7..3edd126bd 100644 --- a/src/Altinn.App.Core/Models/OrganisationNumber.cs +++ b/src/Altinn.App.Core/Models/OrganisationNumber.cs @@ -64,10 +64,13 @@ public static OrganisationNumber Parse(string value) /// The value to parse /// The resulting instance /// true on successful parse, false otherwise - public static bool TryParse(string value, out OrganisationNumber organisationNumber) + public static bool TryParse(string? value, out OrganisationNumber organisationNumber) { organisationNumber = default; + if (string.IsNullOrWhiteSpace(value)) + return false; + // Either local="991825827" or international="0192:991825827" if (value.Length != 9 && value.Length != 14) return false; diff --git a/src/Altinn.App.Core/Models/UserAction/UserActionContext.cs b/src/Altinn.App.Core/Models/UserAction/UserActionContext.cs index 441548ab7..ca793a00f 100644 --- a/src/Altinn.App.Core/Models/UserAction/UserActionContext.cs +++ b/src/Altinn.App.Core/Models/UserAction/UserActionContext.cs @@ -1,4 +1,5 @@ using Altinn.App.Core.Features; +using Altinn.App.Core.Features.Auth; using Altinn.Platform.Storage.Interface.Models; namespace Altinn.App.Core.Models.UserAction; @@ -16,17 +17,20 @@ public class UserActionContext /// The id of the button that triggered the action (optional) /// /// The currently used language by the user (or null if not available) + /// Information about the authenticated party public UserActionContext( IInstanceDataMutator dataMutator, int? userId, string? buttonId = null, Dictionary? actionMetadata = null, - string? language = null + string? language = null, + Authenticated? authentication = null ) { Instance = dataMutator.Instance; DataMutator = dataMutator; - UserId = userId; + _userId = userId; + Authentication = authentication; ButtonId = buttonId; ActionMetadata = actionMetadata ?? []; Language = language; @@ -40,19 +44,22 @@ public UserActionContext( /// The id of the button that triggered the action (optional) /// /// The currently used language by the user (or null if not available) + /// Information about the authenticated party [Obsolete("Use the constructor with IInstanceDataAccessor instead")] public UserActionContext( Instance instance, int? userId, string? buttonId = null, Dictionary? actionMetadata = null, - string? language = null + string? language = null, + Authenticated? authentication = null ) { Instance = instance; // ! TODO: Deprecated constructor, remove in v9 DataMutator = null!; - UserId = userId; + _userId = userId; + Authentication = authentication; ButtonId = buttonId; ActionMetadata = actionMetadata ?? []; Language = language; @@ -68,10 +75,24 @@ public UserActionContext( ///
public IInstanceDataMutator DataMutator { get; } + private readonly int? _userId; + /// /// The user performing the action /// - public int? UserId { get; } + public int? UserId => + _userId + ?? Authentication switch + { + Authenticated.User user => user.UserId, + Authenticated.SelfIdentifiedUser selfIdentifiedUser => selfIdentifiedUser.UserId, + _ => null, + }; + + /// + /// Information about the authenticated party + /// + public Authenticated? Authentication { get; } /// /// The id of the button that triggered the action (optional) diff --git a/test/Altinn.App.Api.Tests/Controllers/ActionsControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/ActionsControllerTests.cs index c45192e09..1ecc994d8 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ActionsControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/ActionsControllerTests.cs @@ -40,7 +40,7 @@ public async Task Perform_returns_403_if_user_not_authorized() HttpClient client = GetRootedClient(org, app); Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); TestData.PrepareInstance(org, app, 1337, guid); - string token = PrincipalUtil.GetToken(1000, null, 3); + string token = TestAuthentication.GetUserToken(1000, authenticationLevel: 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); using var content = new StringContent( "{\"action\":\"lookup_unauthorized\"}", @@ -88,8 +88,6 @@ public async Task Perform_returns_401_if_userId_is_null() HttpClient client = GetRootedClient(org, app); Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); TestData.PrepareInstance(org, app, 1337, guid); - string token = PrincipalUtil.GetToken(null, null, 3); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); using var content = new StringContent( "{\"action\":\"lookup_unauthorized\"}", Encoding.UTF8, @@ -113,7 +111,7 @@ public async Task Perform_returns_400_if_action_is_null() HttpClient client = GetRootedClient(org, app); Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); TestData.PrepareInstance(org, app, 1337, guid); - string token = PrincipalUtil.GetToken(1000, null, 3); + string token = TestAuthentication.GetUserToken(1000, authenticationLevel: 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); using var content = new StringContent("{\"action\":null}", Encoding.UTF8, "application/json"); using HttpResponseMessage response = await client.PostAsync( @@ -134,7 +132,7 @@ public async Task Perform_returns_409_if_process_not_started() HttpClient client = GetRootedClient(org, app); Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef43"); TestData.PrepareInstance(org, app, 1337, guid); - string token = PrincipalUtil.GetToken(1000, null, 3); + string token = TestAuthentication.GetUserToken(authenticationLevel: 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); using var content = new StringContent("{\"action\":\"lookup\"}", Encoding.UTF8, "application/json"); using HttpResponseMessage response = await client.PostAsync( @@ -155,7 +153,7 @@ public async Task Perform_returns_409_if_process_ended() HttpClient client = GetRootedClient(org, app); Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef42"); TestData.PrepareInstance(org, app, 1337, guid); - string token = PrincipalUtil.GetToken(1000, null, 3); + string token = TestAuthentication.GetUserToken(1000, authenticationLevel: 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); using var content = new StringContent("{\"action\":\"lookup\"}", Encoding.UTF8, "application/json"); using HttpResponseMessage response = await client.PostAsync( @@ -180,7 +178,7 @@ public async Task Perform_returns_200_if_action_succeeded() HttpClient client = GetRootedClient(org, app); Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); TestData.PrepareInstance(org, app, 1337, guid); - string token = PrincipalUtil.GetToken(1000, null, 3); + string token = TestAuthentication.GetUserToken(1000, authenticationLevel: 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); using var requestContent = new StringContent("{\"action\":\"lookup\"}", Encoding.UTF8, "application/json"); using HttpResponseMessage response = await client.PostAsync( @@ -232,7 +230,7 @@ public async Task Perform_returns_400_if_action_failed_and_errorType_is_BadReque HttpClient client = GetRootedClient(org, app); Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); TestData.PrepareInstance(org, app, 1337, guid); - string token = PrincipalUtil.GetToken(400, null, 3); + string token = TestAuthentication.GetUserToken(400, authenticationLevel: 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); using var content = new StringContent("{\"action\":\"lookup\"}", Encoding.UTF8, "application/json"); using HttpResponseMessage response = await client.PostAsync( @@ -257,7 +255,7 @@ public async Task Perform_returns_401_if_action_failed_and_errorType_is_Unauthor HttpClient client = GetRootedClient(org, app); Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); TestData.PrepareInstance(org, app, 1337, guid); - string token = PrincipalUtil.GetToken(401, null, 3); + string token = TestAuthentication.GetUserToken(userId: 401, authenticationLevel: 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); using var content = new StringContent("{\"action\":\"lookup\"}", Encoding.UTF8, "application/json"); using HttpResponseMessage response = await client.PostAsync( @@ -282,7 +280,7 @@ public async Task Perform_returns_409_if_action_failed_and_errorType_is_Conflict HttpClient client = GetRootedClient(org, app); Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); TestData.PrepareInstance(org, app, 1337, guid); - string token = PrincipalUtil.GetToken(409, null, 3); + string token = TestAuthentication.GetUserToken(userId: 409, authenticationLevel: 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); using var content = new StringContent("{\"action\":\"lookup\"}", Encoding.UTF8, "application/json"); using HttpResponseMessage response = await client.PostAsync( @@ -307,7 +305,7 @@ public async Task Perform_returns_500_if_action_failed_and_errorType_is_Internal HttpClient client = GetRootedClient(org, app); Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); TestData.PrepareInstance(org, app, 1337, guid); - string token = PrincipalUtil.GetToken(500, null, 3); + string token = TestAuthentication.GetUserToken(userId: 500, authenticationLevel: 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); using var content = new StringContent("{\"action\":\"lookup\"}", Encoding.UTF8, "application/json"); using HttpResponseMessage response = await client.PostAsync( @@ -332,7 +330,7 @@ public async Task Perform_returns_404_if_action_implementation_not_found() HttpClient client = GetRootedClient(org, app); Guid guid = new Guid("b1135209-628e-4a6e-9efd-e4282068ef41"); TestData.PrepareInstance(org, app, 1337, guid); - string token = PrincipalUtil.GetToken(1001, null, 3); + string token = TestAuthentication.GetUserToken(userId: 1001, authenticationLevel: 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); using var content = new StringContent("{\"action\":\"notfound\"}", Encoding.UTF8, "application/json"); using HttpResponseMessage response = await client.PostAsync( @@ -357,8 +355,8 @@ public async Task PerformFillActionThatMutatesData() { services.AddTransient(); }; - var client = GetRootedClient(org, app, 1337, null); - string token = PrincipalUtil.GetToken(1001, null, 3); + var client = GetRootedUserClient(org, app, 1337); + string token = TestAuthentication.GetUserToken(userId: 1001, authenticationLevel: 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); // Run buttonId "add" @@ -481,8 +479,8 @@ public async Task PerformFillAction_GetClientActions() { services.AddTransient(); }; - var client = GetRootedClient(org, app, 1337, null); - string token = PrincipalUtil.GetToken(1001, null, 3); + var client = GetRootedUserClient(org, app, 1337); + string token = TestAuthentication.GetUserToken(userId: 1001, authenticationLevel: 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); // run buttonId "getClientActions" @@ -513,8 +511,8 @@ public async Task PerformFillAction_Fail() { services.AddTransient(); }; - var client = GetRootedClient(org, app, 1337, null); - string token = PrincipalUtil.GetToken(1001, null, 3); + var client = GetRootedUserClient(org, app, 1337); + string token = TestAuthentication.GetUserToken(userId: 1001, authenticationLevel: 3); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); // Run buttonId "fail" diff --git a/test/Altinn.App.Api.Tests/Controllers/Conventions/EnumSerializationTests.cs b/test/Altinn.App.Api.Tests/Controllers/Conventions/EnumSerializationTests.cs index b5c4bd183..13178c5ab 100644 --- a/test/Altinn.App.Api.Tests/Controllers/Conventions/EnumSerializationTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/Conventions/EnumSerializationTests.cs @@ -59,7 +59,7 @@ public EnumSerializationTests(WebApplicationFactory factory, ITestOutpu public async Task ValidateInstantiation_SerializesPartyTypesAllowedAsNumber() { // Arrange - using var client = GetRootedClient(Org, App, 1337, PartyId); + using var client = GetRootedUserClient(Org, App, 1337, PartyId); // Act var response = await client.PostAsync( diff --git a/test/Altinn.App.Api.Tests/Controllers/DataControllerPatchTests.InvalidTestValue_ReturnsConflict.verified.txt b/test/Altinn.App.Api.Tests/Controllers/DataControllerPatchTests.InvalidTestValue_ReturnsConflict.verified.txt index 372e02d11..c8bf29990 100644 --- a/test/Altinn.App.Api.Tests/Controllers/DataControllerPatchTests.InvalidTestValue_ReturnsConflict.verified.txt +++ b/test/Altinn.App.Api.Tests/Controllers/DataControllerPatchTests.InvalidTestValue_ReturnsConflict.verified.txt @@ -48,17 +48,29 @@ { url.scheme: http }, + { + user.authentication.inAltinnPortal: True + }, { user.authentication.level: 2 }, { - user.authentication.method: Mock + user.authentication.method: BankID + }, + { + user.authentication.token.isExchanged: False + }, + { + user.authentication.token.issuer: Altinn + }, + { + user.authentication.type: User }, { user.id: 1337 }, { - user.name: User1337 + user.party.id: 500600 } ], IdFormat: W3C, @@ -75,4 +87,4 @@ } ], Metrics: [] -} +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs index 1332fd63f..1b57524b1 100644 --- a/test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/DataControllerTests.cs @@ -27,7 +27,7 @@ public async Task PutDataElement_MissingDataType_ReturnsBadRequest() int instanceOwnerPartyId = 1337; Guid guid = new Guid("0fc98a23-fe31-4ef5-8fb9-dd3f479354cd"); HttpClient client = GetRootedClient(org, app); - string token = PrincipalUtil.GetOrgToken("nav", "160694123"); + string token = TestAuthentication.GetServiceOwnerToken("405003309", org: "nav"); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); TestData.PrepareInstance(org, app, instanceOwnerPartyId, guid); @@ -49,7 +49,7 @@ public async Task PostBinaryElement_ContentTooLarge_ReturnsBadRequest() string dataType = "specificFileType"; // Should have restrictions on 1 mb in app metadata Guid guid = new Guid("0fc98a23-fe31-4ef5-8fb9-dd3f479354cd"); HttpClient client = GetRootedClient(org, app); - string token = PrincipalUtil.GetOrgToken("nav", "160694123"); + string token = TestAuthentication.GetServiceOwnerToken("405003309", org: "nav"); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); TestData.PrepareInstance(org, app, instanceOwnerPartyId, guid); @@ -94,7 +94,7 @@ public async Task CreateDataElement_BinaryPdf_AnalyserShouldRunOk() TestData.PrepareInstance(org, app, 1337, guid); // Setup the request - string token = PrincipalUtil.GetOrgToken("nav", "160694123"); + string token = TestAuthentication.GetServiceOwnerToken("405003309", org: "nav"); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); ByteArrayContent fileContent = await CreateBinaryContent(org, app, "example.pdf", "application/pdf"); string url = $"/{org}/{app}/instances/1337/{guid}/data?dataType=specificFileType"; @@ -127,7 +127,7 @@ public async Task CreateDataElement_ZeroBytes_BinaryPdf_AnalyserShouldReturnBadR TestData.PrepareInstance(org, app, 1337, guid); // Setup the request - string token = PrincipalUtil.GetOrgToken("nav", "160694123"); + string token = TestAuthentication.GetServiceOwnerToken("405003309", org: "nav"); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); ByteArrayContent fileContent = await CreateBinaryContent(org, app, "zero.pdf", "application/pdf"); string url = $"/{org}/{app}/instances/1337/{guid}/data?dataType=specificFileType"; @@ -162,7 +162,7 @@ public async Task CreateDataElement_JpgFakedAsPdf_AnalyserShouldRunAndFail() TestData.PrepareInstance(org, app, 1337, guid); // Setup the request - string token = PrincipalUtil.GetOrgToken("nav", "160694123"); + string token = TestAuthentication.GetServiceOwnerToken("405003309", org: "nav"); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); ByteArrayContent fileContent = await CreateBinaryContent(org, app, "example.jpg.pdf", "application/pdf"); string url = $"/{org}/{app}/instances/1337/{guid}/data?dataType=specificFileType"; diff --git a/test/Altinn.App.Api.Tests/Controllers/DataController_LayoutEvaluatorTests.cs b/test/Altinn.App.Api.Tests/Controllers/DataController_LayoutEvaluatorTests.cs index 54239934e..61d54b747 100644 --- a/test/Altinn.App.Api.Tests/Controllers/DataController_LayoutEvaluatorTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/DataController_LayoutEvaluatorTests.cs @@ -72,7 +72,7 @@ public async Task PutDataElement_LegacyLayoutEvaluatorState_ReturnsOk() int instanceOwnerPartyId = 500600; Guid instanceGuid = Guid.Parse("cff1cb24-5bc1-4888-8e06-c634753c5144"); Guid dataGuid = Guid.Parse("f3e04c65-aa70-40ec-84df-087cc2583402"); - HttpClient client = GetRootedClient(org, app, 1337, instanceOwnerPartyId); + using HttpClient client = GetRootedUserClient(org, app, 1337, instanceOwnerPartyId); TestData.PrepareInstance(org, app, instanceOwnerPartyId, instanceGuid); diff --git a/test/Altinn.App.Api.Tests/Controllers/DataController_PatchTests.cs b/test/Altinn.App.Api.Tests/Controllers/DataController_PatchTests.cs index ec278cb53..8f062d55a 100644 --- a/test/Altinn.App.Api.Tests/Controllers/DataController_PatchTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/DataController_PatchTests.cs @@ -56,7 +56,7 @@ public class DataControllerPatchTests : ApiTestBase, IClassFixture _client ??= GetRootedClient(Org, App, UserId, null); + private HttpClient GetClient() => _client ??= GetRootedUserClient(Org, App, UserId, InstanceOwnerPartyId); // Constructor with common setup public DataControllerPatchTests(WebApplicationFactory factory, ITestOutputHelper outputHelper) @@ -91,7 +91,7 @@ TResponse parsedResponse } OutputHelper.WriteLine($"Calling PATCH {url}"); using var httpClient = GetRootedClient(Org, App); - string token = PrincipalUtil.GetToken(1337, null); + string token = TestAuthentication.GetUserToken(userId: 1337); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); var serializedPatch = JsonSerializer.Serialize( new DataPatchRequest() { Patch = patch, IgnoredValidators = ignoredValidators }, @@ -315,7 +315,7 @@ public async Task NullName_ReturnsOkAndValidationError() .Be("melding.name is required in component with id default.page.name for binding simpleBinding"); // Run full validation to see that result is the same - using var client = GetRootedClient(Org, App, UserId, null); + using var client = GetRootedUserClient(Org, App, UserId, InstanceOwnerPartyId); var validationResponse = await client.GetAsync($"/{Org}/{App}/instances/{_instanceId}/validate"); validationResponse.Should().HaveStatusCode(HttpStatusCode.OK); var validationResponseString = await validationResponse.Content.ReadAsStringAsync(); @@ -855,7 +855,7 @@ public async Task DataReadChanges_IsPreservedWhenCallingPatch() var url = $"/{Org}/{App}/instances/{_instanceId}/data/{_dataGuid}?language=nn"; OutputHelper.WriteLine($"Calling GET {url}"); using var httpClient = GetRootedClient(Org, App); - string token = PrincipalUtil.GetToken(1337, null); + string token = TestAuthentication.GetUserToken(userId: UserId, InstanceOwnerPartyId); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); var response = await httpClient.GetAsync(url); var responseString = await response.Content.ReadAsStringAsync(); diff --git a/test/Altinn.App.Api.Tests/Controllers/DataController_PostTests.cs b/test/Altinn.App.Api.Tests/Controllers/DataController_PostTests.cs index 68a85ab30..5bcb87e26 100644 --- a/test/Altinn.App.Api.Tests/Controllers/DataController_PostTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/DataController_PostTests.cs @@ -90,7 +90,7 @@ public async Task PostFormElement_DataProcessorsModifiesOtherElement_ReturnsChan Times.Once() ); - HttpClient client = GetRootedClient(_org, _app, 1337, null, authenticationLevel: 2); + HttpClient client = GetRootedUserClient(_org, _app, 1337, _instanceOwnerPartyId, authenticationLevel: 2); var content = JsonContent.Create(new Skjema() { Melding = new() { Random = "fromClient" } }); var response = await client.PostAsync( $"/{_org}/{_app}/instances/{_instanceOwnerPartyId}/{_instanceGuid}/data/{dataTypeString}", @@ -149,7 +149,7 @@ public async Task PostBinaryElement_DataProcessorAbandons_ReturnsIssues() Times.Exactly(1) ); - HttpClient client = GetRootedClient(_org, _app, 1337, null, authenticationLevel: 2); + HttpClient client = GetRootedUserClient(_org, _app, 1337, _instanceOwnerPartyId, authenticationLevel: 2); // Update data element var updateDataElementContent = new ByteArrayContent(binaryData) @@ -217,7 +217,7 @@ public async Task PostBinaryElement_DataWriteProcessorAddsAndRemovesElements() Times.Exactly(1) ); - HttpClient client = GetRootedClient(_org, _app, 1337, null, authenticationLevel: 2); + HttpClient client = GetRootedUserClient(_org, _app, 1337, _instanceOwnerPartyId, authenticationLevel: 2); // Update data element var updateDataElementContent = new ByteArrayContent(binaryData) diff --git a/test/Altinn.App.Api.Tests/Controllers/DataController_PutTests.cs b/test/Altinn.App.Api.Tests/Controllers/DataController_PutTests.cs index 9c4004234..319e0e3ee 100644 --- a/test/Altinn.App.Api.Tests/Controllers/DataController_PutTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/DataController_PutTests.cs @@ -38,7 +38,7 @@ public async Task PutDataElement_TestSinglePartUpdate_ReturnsOk() string app = "contributer-restriction"; int instanceOwnerPartyId = 501337; HttpClient client = GetRootedClient(org, app); - string token = PrincipalUtil.GetToken(1337, null, org: "abc"); + string token = TestAuthentication.GetUserToken(1337, instanceOwnerPartyId); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); _dataProcessor @@ -179,7 +179,7 @@ public async Task PutDataElement_TestMultiPartUpdateWithCustomDataProcessor_Retu string app = "contributer-restriction"; int instanceOwnerPartyId = 501337; HttpClient client = GetRootedClient(org, app); - string token = PrincipalUtil.GetToken(1337, null, org: "abc"); + string token = TestAuthentication.GetUserToken(1337, instanceOwnerPartyId); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); // Create instance diff --git a/test/Altinn.App.Api.Tests/Controllers/DataController_UserAccessTests.cs b/test/Altinn.App.Api.Tests/Controllers/DataController_UserAccessTests.cs index b21488e37..b4bac478b 100644 --- a/test/Altinn.App.Api.Tests/Controllers/DataController_UserAccessTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/DataController_UserAccessTests.cs @@ -1,7 +1,5 @@ using System.Net; -using System.Net.Http.Headers; using Altinn.App.Api.Tests.Data; -using Altinn.App.Api.Tests.Utils; using Altinn.App.Core.Features; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; @@ -28,18 +26,18 @@ public DataController_UserAccessTests(WebApplicationFactory factory, IT } [Theory] - [InlineData("userInteractionUnspecified", null, HttpStatusCode.Created)] - [InlineData("userInteractionUnspecified", OrgId, HttpStatusCode.Created)] - [InlineData("disallowUserCreate", null, HttpStatusCode.BadRequest)] - [InlineData("disallowUserCreate", OrgId, HttpStatusCode.Created)] + [InlineData("userInteractionUnspecified", false, HttpStatusCode.Created)] + [InlineData("userInteractionUnspecified", true, HttpStatusCode.Created)] + [InlineData("disallowUserCreate", false, HttpStatusCode.BadRequest)] + [InlineData("disallowUserCreate", true, HttpStatusCode.Created)] public async Task CreateDataElement_ImplementsAndValidates_AllowUserCreateProperty( string dataModelId, - string? tokenOrgClaim, + bool actAsOrg, HttpStatusCode expectedStatusCode ) { // Arrange - var instance = await CreateAppInstance(tokenOrgClaim); + var instance = await CreateAppInstance(actAsOrg); // Act var response = await instance.AuthenticatedClient.PostAsync( @@ -54,25 +52,21 @@ HttpStatusCode expectedStatusCode } [Theory] - [InlineData("userInteractionUnspecified", null, HttpStatusCode.OK)] - [InlineData("userInteractionUnspecified", OrgId, HttpStatusCode.OK)] - [InlineData("disallowUserDelete", null, HttpStatusCode.OK)] - [InlineData("disallowUserDelete", OrgId, HttpStatusCode.OK)] + [InlineData("userInteractionUnspecified", false, HttpStatusCode.OK)] + [InlineData("userInteractionUnspecified", true, HttpStatusCode.OK)] + [InlineData("disallowUserDelete", false, HttpStatusCode.OK)] + [InlineData("disallowUserDelete", true, HttpStatusCode.OK)] public async Task DeleteDataElement_ImplementsAndValidates_AllowUserDeleteProperty( string dataModelId, - string? tokenOrgClaim, + bool instantiateAsOrg, HttpStatusCode expectedStatusCode ) { // Arrange - var instance = await CreateAppInstance(tokenOrgClaim); + var instance = await CreateAppInstance(instantiateAsOrg); /* Create a datamodel so we have something to delete */ - var systemClient = CreateAuthenticatedHttpClient( - rootOrg: instance.Org, - rootApp: instance.App, - tokenOrgClaim: OrgId - ); + using var systemClient = GetRootedOrgClient(OrgId, AppId, serviceOwnerOrg: OrgId); var createResponse = await systemClient.PostAsync( $"/{instance.Org}/{instance.App}/instances/{instance.Id}/data?dataType={dataModelId}", null @@ -93,16 +87,13 @@ HttpStatusCode expectedStatusCode TestData.DeleteInstanceAndData(OrgId, AppId, instance.Id); } - private async Task CreateAppInstance(string? tokenOrgClaim) + private async Task CreateAppInstance(bool actAsOrg) { var instanceOwnerPartyId = 501337; var userId = 1337; - HttpClient client = CreateAuthenticatedHttpClient( - rootOrg: OrgId, - rootApp: AppId, - tokenUserClaim: userId, - tokenOrgClaim: tokenOrgClaim - ); + HttpClient client = actAsOrg + ? GetRootedOrgClient(OrgId, AppId, serviceOwnerOrg: OrgId) + : GetRootedUserClient(OrgId, AppId, userId, instanceOwnerPartyId); var response = await client.PostAsync( $"{OrgId}/{AppId}/instances/?instanceOwnerPartyId={instanceOwnerPartyId}", @@ -113,20 +104,5 @@ private async Task CreateAppInstance(string? tokenOrgClaim) return new AppInstance(createResponseParsed.Id, OrgId, AppId, client); } - private HttpClient CreateAuthenticatedHttpClient( - string rootOrg, - string rootApp, - int? tokenUserClaim = default, - int? tokenPartyIdClaim = default, - string? tokenOrgClaim = default - ) - { - HttpClient client = GetRootedClient(rootOrg, rootApp); - string token = PrincipalUtil.GetToken(tokenUserClaim, tokenPartyIdClaim, org: tokenOrgClaim); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - - return client; - } - private record AppInstance(string Id, string Org, string App, HttpClient AuthenticatedClient); } diff --git a/test/Altinn.App.Api.Tests/Controllers/EventsReceiverControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/EventsReceiverControllerTests.cs index 6b459ea24..fe0d39357 100644 --- a/test/Altinn.App.Api.Tests/Controllers/EventsReceiverControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/EventsReceiverControllerTests.cs @@ -25,7 +25,7 @@ public async Task Post_ValidEventType_ShouldReturnOk() var org = "tdd"; var app = "contributer-restriction"; - var client = GetRootedClient(org, app, 1338, null); + using var client = GetRootedUserClient(org, app, 1338); CloudEvent cloudEvent = new() { Id = Guid.NewGuid().ToString(), @@ -60,7 +60,7 @@ public async Task Post_NonValidEventType_ShouldReturnBadRequest() var org = "tdd"; var app = "contributer-restriction"; - var client = GetRootedClient(org, app, userId: 1338, partyId: null); + using var client = GetRootedUserClient(org, app, userId: 1338); CloudEvent cloudEvent = new() { Id = Guid.NewGuid().ToString(), diff --git a/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs b/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs index fa35683b7..8c6e42715 100644 --- a/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/InstancesController_CopyInstanceTests.cs @@ -150,7 +150,9 @@ public async Task CopyInstance_CopyInstanceNotEnabled_ReturnsBadRequest() public async Task CopyInstance_AsAppOwner_ReturnsForbidResult() { // Arrange - _httpContextMock.Setup(httpContext => httpContext.User).Returns(PrincipalUtil.GetOrgPrincipal("ttd")); + _httpContextMock + .Setup(httpContext => httpContext.User) + .Returns(TestAuthentication.GetServiceOwnerPrincipal(org: "ttd")); // Act ActionResult actual = await SUT.CopyInstance("ttd", "copy-instance", 343234, Guid.NewGuid()); @@ -168,7 +170,7 @@ public async Task CopyInstance_AsUnauthorized_ReturnsForbidden() // Arrange const string Org = "ttd"; const string AppName = "copy-instance"; - _httpContextMock.Setup(httpContext => httpContext.User).Returns(PrincipalUtil.GetUserPrincipal(1337, null)); + _httpContextMock.Setup(httpContext => httpContext.User).Returns(TestAuthentication.GetUserPrincipal(1337)); _appMetadata.Setup(a => a.GetApplicationMetadata()).ReturnsAsync(CreateApplicationMetadata(Org, AppName, true)); _pdp.Setup>(p => p.GetDecisionForRequest(It.IsAny())) .ReturnsAsync(CreateXacmlResponse("Deny")); @@ -200,7 +202,9 @@ public async Task CopyInstance_InstanceNotArchived_ReturnsBadRequest() Status = new InstanceStatus() { IsArchived = false }, }; - _httpContextMock.Setup(httpContext => httpContext.User).Returns(PrincipalUtil.GetUserPrincipal(1337, null)); + _httpContextMock + .Setup(httpContext => httpContext.User) + .Returns(TestAuthentication.GetUserPrincipal(1337, instanceOwnerPartyId)); _appMetadata.Setup(a => a.GetApplicationMetadata()).ReturnsAsync(CreateApplicationMetadata(Org, AppName, true)); _pdp.Setup>(p => p.GetDecisionForRequest(It.IsAny())) .ReturnsAsync(CreateXacmlResponse("Permit")); @@ -236,7 +240,9 @@ public async Task CopyInstance_InstanceDoesNotExists_ReturnsBadRequest() new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden) ); - _httpContextMock.Setup(httpContext => httpContext.User).Returns(PrincipalUtil.GetUserPrincipal(1337, null)); + _httpContextMock + .Setup(httpContext => httpContext.User) + .Returns(TestAuthentication.GetUserPrincipal(1337, instanceOwnerPartyId)); _appMetadata.Setup(a => a.GetApplicationMetadata()).ReturnsAsync(CreateApplicationMetadata(Org, AppName, true)); _pdp.Setup>(p => p.GetDecisionForRequest(It.IsAny())) .ReturnsAsync(CreateXacmlResponse("Permit")); @@ -272,7 +278,9 @@ public async Task CopyInstance_PlatformReturnsError_ThrowsException() new HttpResponseMessage(System.Net.HttpStatusCode.BadGateway) ); - _httpContextMock.Setup(httpContext => httpContext.User).Returns(PrincipalUtil.GetUserPrincipal(1337, null)); + _httpContextMock + .Setup(httpContext => httpContext.User) + .Returns(TestAuthentication.GetUserPrincipal(1337, instanceOwnerPartyId)); _appMetadata.Setup(a => a.GetApplicationMetadata()).ReturnsAsync(CreateApplicationMetadata(Org, AppName, true)); _pdp.Setup>(p => p.GetDecisionForRequest(It.IsAny())) .ReturnsAsync(CreateXacmlResponse("Permit")); @@ -316,7 +324,9 @@ public async Task CopyInstance_InstantiationValidationFails_ReturnsForbidden() }; InstantiationValidationResult? instantiationValidationResult = new() { Valid = false }; - _httpContextMock.Setup(httpContext => httpContext.User).Returns(PrincipalUtil.GetUserPrincipal(1337, null)); + _httpContextMock + .Setup(httpContext => httpContext.User) + .Returns(TestAuthentication.GetUserPrincipal(1337, instanceOwnerPartyId)); _appMetadata.Setup(a => a.GetApplicationMetadata()).ReturnsAsync(CreateApplicationMetadata(Org, AppName, true)); _pdp.Setup>(p => p.GetDecisionForRequest(It.IsAny())) .ReturnsAsync(CreateXacmlResponse("Permit")); @@ -367,7 +377,7 @@ public async Task CopyInstance_EverythingIsFine_ReturnsRedirect() }; InstantiationValidationResult? instantiationValidationResult = new() { Valid = true }; - _httpContextMock.Setup(hc => hc.User).Returns(PrincipalUtil.GetUserPrincipal(1337, null)); + _httpContextMock.Setup(hc => hc.User).Returns(TestAuthentication.GetUserPrincipal(1337, InstanceOwnerPartyId)); _httpContextMock.Setup(hc => hc.Request).Returns(Mock.Of()); _appMetadata.Setup(a => a.GetApplicationMetadata()).ReturnsAsync(CreateApplicationMetadata(Org, AppName, true)); _pdp.Setup>(p => p.GetDecisionForRequest(It.IsAny())) diff --git a/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_Org.verified.txt b/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_Org.verified.txt new file mode 100644 index 000000000..9ecc15e79 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_Org.verified.txt @@ -0,0 +1,54 @@ +{ + Activities: [ + { + ActivityName: POST {org}/{app}/instances/create, + Tags: [ + { + http.request.method: POST + }, + { + http.response.status_code: 201 + }, + { + http.route: {org}/{app}/instances/create + }, + { + network.protocol.version: 1.1 + }, + { + organisation.number: 405003309 + }, + { + server.address: localhost + }, + { + TestId: Guid_1 + }, + { + url.path: /tdd/permissive-app/instances/create + }, + { + url.scheme: http + }, + { + user.authentication.level: 3 + }, + { + user.authentication.method: Mock + }, + { + user.authentication.token.isExchanged: True + }, + { + user.authentication.token.issuer: Unknown + }, + { + user.authentication.type: Org + } + ], + IdFormat: W3C, + Kind: Server + } + ], + Metrics: [] +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_SelfIdentifiedUser.verified.txt b/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_SelfIdentifiedUser.verified.txt new file mode 100644 index 000000000..ba6d44f6e --- /dev/null +++ b/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_SelfIdentifiedUser.verified.txt @@ -0,0 +1,57 @@ +{ + Activities: [ + { + ActivityName: POST {org}/{app}/instances/create, + Tags: [ + { + http.request.method: POST + }, + { + http.response.status_code: 201 + }, + { + http.route: {org}/{app}/instances/create + }, + { + network.protocol.version: 1.1 + }, + { + server.address: localhost + }, + { + TestId: Guid_1 + }, + { + url.path: /tdd/permissive-app/instances/create + }, + { + url.scheme: http + }, + { + user.authentication.level: 0 + }, + { + user.authentication.method: Mock + }, + { + user.authentication.token.isExchanged: False + }, + { + user.authentication.token.issuer: Altinn + }, + { + user.authentication.type: SelfIdentifiedUser + }, + { + user.id: 1337 + }, + { + user.party.id: 501337 + } + ], + IdFormat: W3C, + Kind: Server + } + ], + Metrics: [] +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_ServiceOwner.verified.txt b/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_ServiceOwner.verified.txt new file mode 100644 index 000000000..606a78525 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_ServiceOwner.verified.txt @@ -0,0 +1,57 @@ +{ + Activities: [ + { + ActivityName: POST {org}/{app}/instances/create, + Tags: [ + { + http.request.method: POST + }, + { + http.response.status_code: 201 + }, + { + http.route: {org}/{app}/instances/create + }, + { + network.protocol.version: 1.1 + }, + { + organisation.name: tdd + }, + { + organisation.number: 405003309 + }, + { + server.address: localhost + }, + { + TestId: Guid_1 + }, + { + url.path: /tdd/permissive-app/instances/create + }, + { + url.scheme: http + }, + { + user.authentication.level: 3 + }, + { + user.authentication.method: maskinporten + }, + { + user.authentication.token.isExchanged: True + }, + { + user.authentication.token.issuer: Maskinporten + }, + { + user.authentication.type: ServiceOwner + } + ], + IdFormat: W3C, + Kind: Server + } + ], + Metrics: [] +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_SystemUser.verified.txt b/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_SystemUser.verified.txt new file mode 100644 index 000000000..ae428b551 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_SystemUser.verified.txt @@ -0,0 +1,57 @@ +{ + Activities: [ + { + ActivityName: POST {org}/{app}/instances/create, + Tags: [ + { + http.request.method: POST + }, + { + http.response.status_code: 201 + }, + { + http.route: {org}/{app}/instances/create + }, + { + network.protocol.version: 1.1 + }, + { + organisation.number: 310702641 + }, + { + organisation.systemuser.id: Guid_1 + }, + { + server.address: localhost + }, + { + TestId: Guid_2 + }, + { + url.path: /tdd/permissive-app/instances/create + }, + { + url.scheme: http + }, + { + user.authentication.level: 3 + }, + { + user.authentication.method: maskinporten + }, + { + user.authentication.token.isExchanged: True + }, + { + user.authentication.token.issuer: Maskinporten + }, + { + user.authentication.type: SystemUser + } + ], + IdFormat: W3C, + Kind: Server + } + ], + Metrics: [] +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_User.verified.txt b/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_User.verified.txt new file mode 100644 index 000000000..82a7d0c34 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.PostNewInstance_Simplified_User.verified.txt @@ -0,0 +1,60 @@ +{ + Activities: [ + { + ActivityName: POST {org}/{app}/instances/create, + Tags: [ + { + http.request.method: POST + }, + { + http.response.status_code: 201 + }, + { + http.route: {org}/{app}/instances/create + }, + { + network.protocol.version: 1.1 + }, + { + server.address: localhost + }, + { + TestId: Guid_1 + }, + { + url.path: /tdd/permissive-app/instances/create + }, + { + url.scheme: http + }, + { + user.authentication.inAltinnPortal: True + }, + { + user.authentication.level: 2 + }, + { + user.authentication.method: BankID + }, + { + user.authentication.token.isExchanged: False + }, + { + user.authentication.token.issuer: Altinn + }, + { + user.authentication.type: User + }, + { + user.id: 1337 + }, + { + user.party.id: 501337 + } + ], + IdFormat: W3C, + Kind: Server + } + ], + Metrics: [] +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.cs b/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.cs index 931a6d545..367cc8879 100644 --- a/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstanceTests.cs @@ -8,6 +8,7 @@ using Altinn.App.Api.Tests.Data; using Altinn.App.Api.Tests.Data.apps.tdd.contributer_restriction.models; using Altinn.App.Api.Tests.Utils; +using Altinn.App.Common.Tests; using Altinn.App.Core.Features; using Altinn.App.Core.Internal.Pdf; using Altinn.Platform.Storage.Interface.Models; @@ -47,7 +48,7 @@ public async Task PostNewInstanceWithContent_EnsureDataIsPresent() string app = "contributer-restriction"; int instanceOwnerPartyId = 501337; HttpClient client = GetRootedClient(org, app); - string token = PrincipalUtil.GetToken(1337, null); + string token = TestAuthentication.GetUserToken(userId: 1337, partyId: instanceOwnerPartyId); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); // Create instance data @@ -153,17 +154,27 @@ private async Task CreateInstanceSimplified( return createResponseParsed; } - [Fact] - public async Task PostNewInstance_Simplified() + [Theory] + [ClassData(typeof(TestAuthentication.AllTokens))] + public async Task PostNewInstance_Simplified(TestJwtToken token) { // Setup test data string org = "tdd"; - string app = "contributer-restriction"; - int instanceOwnerPartyId = 501337; - HttpClient client = GetRootedClient(org, app); - string token = PrincipalUtil.GetToken(1337, null); + string app = "permissive-app"; + int instanceOwnerPartyId = token.PartyId; - var createResponseParsed = await CreateInstanceSimplified(org, app, instanceOwnerPartyId, client, token); + this.OverrideServicesForThisTest = (services) => + { + services.AddTelemetrySink( + shouldAlsoListenToActivities: (_, source) => source.Name == "Microsoft.AspNetCore", + activityFilter: (_, activity) => + this.ActivityFilter(_, activity) && activity.DisplayName == "POST {org}/{app}/instances/create" + ); + }; + + using HttpClient client = GetRootedClient(org, app, includeTraceContext: true); + + var createResponseParsed = await CreateInstanceSimplified(org, app, instanceOwnerPartyId, client, token.Token); var instanceId = createResponseParsed.Id; createResponseParsed.Data.Should().HaveCount(1, "Create instance should create a data element"); var dataGuid = createResponseParsed.Data.First().Id; @@ -175,6 +186,9 @@ public async Task PostNewInstance_Simplified() var readDataElementResponseParsed = JsonSerializer.Deserialize(readDataElementResponseContent)!; readDataElementResponseParsed.Melding.Should().BeNull(); // No content yet TestData.DeleteInstanceAndData(org, app, instanceId); + + var telemetry = this.Services.GetRequiredService(); + await telemetry.SnapshotActivities(settings => settings.UseTextForParameters(token.Type.ToString())); } [Fact] @@ -185,7 +199,7 @@ public async Task PostNewInstance_Simplified_With_Prefill() string app = "contributer-restriction"; int instanceOwnerPartyId = 501337; HttpClient client = GetRootedClient(org, app); - string token = PrincipalUtil.GetToken(1337, null); + string token = TestAuthentication.GetUserToken(userId: 1337, partyId: instanceOwnerPartyId); var prefill = new Dictionary { { "melding.name", "TestName" } }; var createResponseParsed = await CreateInstanceSimplified( @@ -219,7 +233,7 @@ public async Task PostNewInstanceWithInvalidData_EnsureInvalidResponse() string app = "contributer-restriction"; int instanceOwnerPartyId = 501337; HttpClient client = GetRootedClient(org, app); - string token = PrincipalUtil.GetToken(1337, null); + string token = TestAuthentication.GetUserToken(userId: 1337, partyId: instanceOwnerPartyId); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); // Create instance data @@ -245,7 +259,7 @@ public async Task PostNewInstanceWithWrongPartname_EnsureBadRequest() string app = "contributer-restriction"; int instanceOwnerPartyId = 501337; HttpClient client = GetRootedClient(org, app); - string token = PrincipalUtil.GetToken(1337, null); + string token = TestAuthentication.GetUserToken(userId: 1337, partyId: instanceOwnerPartyId); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); // Create instance data @@ -282,7 +296,7 @@ public async Task InstationAllowedByOrg_Returns_Forbidden_For_user() this.OverrideServicesForThisTest = services => services.AddSingleton(new AppMetadataMutationHook(app => app.DisallowUserInstantiation = true)); HttpClient client = GetRootedClient(org, app); - string token = PrincipalUtil.GetToken(1337, null); + string token = TestAuthentication.GetUserToken(userId: 1337, partyId: instanceOwnerPartyId); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); // Create instance data @@ -312,7 +326,7 @@ public async Task PostNewInstanceWithInstanceTemplate() string app = "contributer-restriction"; int instanceOwnerPartyId = 501337; int userId = 1337; - HttpClient client = GetRootedClient(org, app, userId, null); + using HttpClient client = GetRootedUserClient(org, app, userId, instanceOwnerPartyId); using var content = JsonContent.Create( new Instance() { InstanceOwner = new InstanceOwner() { PartyId = instanceOwnerPartyId.ToString() } } @@ -336,7 +350,7 @@ public async Task PostNewInstanceWithInstanceTemplateString() int instanceOwnerPartyId = 501337; // Get an org token // (to avoid issues with read status being set when initialized by normal users) - HttpClient client = GetRootedClient(org, app, 0, null, serviceOwnerOrg: org); + using HttpClient client = GetRootedOrgClient(org, app, serviceOwnerOrg: org); using var content = new StringContent( $$""" @@ -380,7 +394,7 @@ public async Task PostNewInstanceWithMissingTemplate() string app = "contributer-restriction"; int instanceOwnerPartyId = 501337; int userId = 1337; - HttpClient client = GetRootedClient(org, app, userId, null); + using HttpClient client = GetRootedUserClient(org, app, userId, instanceOwnerPartyId); using var content = new ByteArrayContent([]) { @@ -410,7 +424,7 @@ public async Task InstationAllowedByOrg_Returns_Forbidden_For_User_SimplifiedEnd this.OverrideServicesForThisTest = services => services.AddSingleton(new AppMetadataMutationHook(app => app.DisallowUserInstantiation = true)); HttpClient client = GetRootedClient(org, app); - string token = PrincipalUtil.GetToken(1337, null); + string token = TestAuthentication.GetUserToken(userId: 1337, partyId: instanceOwnerPartyId); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); // Create instance data @@ -450,8 +464,8 @@ public async Task InstationAllowedByOrg_Returns_Ok_For_User_When_Copying_Simplif }; HttpClient client = GetRootedClient(org, app); - string orgToken = PrincipalUtil.GetOrgToken("tdd", "160694123"); - string userToken = PrincipalUtil.GetToken(1337, 501337); + string orgToken = TestAuthentication.GetServiceOwnerToken("405003309", org: "tdd"); + string userToken = TestAuthentication.GetUserToken(1337, 501337); var sourceInstance = await CreateInstanceSimplified(org, app, instanceOwnerPartyId, client, orgToken); sourceInstance.Data.Should().HaveCount(1, "Create instance should create a data element"); diff --git a/test/Altinn.App.Api.Tests/Controllers/LookupOrganisationControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/LookupOrganisationControllerTests.cs index bcad6c630..7a7d6b095 100644 --- a/test/Altinn.App.Api.Tests/Controllers/LookupOrganisationControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/LookupOrganisationControllerTests.cs @@ -160,7 +160,7 @@ public async Task Post_LookupOrganisation_General_Exception_Returned_Correctly() private HttpClient GetHttpClient() { HttpClient client = GetRootedClient(Org, App); - string token = PrincipalUtil.GetToken(1337, null); + string token = TestAuthentication.GetUserToken(1337); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); return client; } diff --git a/test/Altinn.App.Api.Tests/Controllers/LookupPersonControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/LookupPersonControllerTests.cs index 8560b896f..7e682efb0 100644 --- a/test/Altinn.App.Api.Tests/Controllers/LookupPersonControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/LookupPersonControllerTests.cs @@ -281,7 +281,7 @@ public async Task Post_PersonSearch_General_Exception_Returned_Correctly() private HttpClient GetHttpClient() { HttpClient client = GetRootedClient(Org, App); - string token = PrincipalUtil.GetToken(1337, null); + string token = TestAuthentication.GetUserToken(1337); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); return client; } diff --git a/test/Altinn.App.Api.Tests/Controllers/PdfControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/PdfControllerTests.cs index fc02dbab6..14e30864f 100644 --- a/test/Altinn.App.Api.Tests/Controllers/PdfControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/PdfControllerTests.cs @@ -1,6 +1,8 @@ using Altinn.App.Api.Controllers; +using Altinn.App.Api.Tests.Utils; using Altinn.App.Core.Configuration; using Altinn.App.Core.Features; +using Altinn.App.Core.Features.Auth; using Altinn.App.Core.Infrastructure.Clients.Pdf; using Altinn.App.Core.Internal.AppModel; using Altinn.App.Core.Internal.Auth; @@ -33,7 +35,6 @@ public class PdfControllerTests private readonly Mock _appResources = new(); private readonly Mock _dataClient = new(); - private readonly Mock _profile = new(); private readonly IOptions _platformSettingsOptions = Options.Create(new() { }); private readonly Mock _instanceClient = new(); private readonly Mock _pdfFormatter = new(); @@ -44,6 +45,8 @@ public class PdfControllerTests new() { } ); + private readonly Mock _authenticationContext = new(); + private readonly Mock> _logger = new(); public PdfControllerTests() @@ -61,6 +64,27 @@ public PdfControllerTests() } ) ); + + _authenticationContext.Setup(s => s.Current).Returns(TestAuthentication.GetUserAuthentication()); + } + + private PdfService NewPdfService( + Mock httpContextAccessor, + PdfGeneratorClient pdfGeneratorClient, + IOptions generalSettingsOptions + ) + { + var pdfService = new PdfService( + _appResources.Object, + _dataClient.Object, + httpContextAccessor.Object, + pdfGeneratorClient, + _pdfGeneratorSettingsOptions, + generalSettingsOptions, + _logger.Object, + _authenticationContext.Object + ); + return pdfService; } [Fact] @@ -90,16 +114,7 @@ public async Task Request_In_Dev_Should_Generate() _userTokenProvider.Object, httpContextAccessor.Object ); - var pdfService = new PdfService( - _appResources.Object, - _dataClient.Object, - httpContextAccessor.Object, - _profile.Object, - pdfGeneratorClient, - _pdfGeneratorSettingsOptions, - generalSettingsOptions, - _logger.Object - ); + var pdfService = NewPdfService(httpContextAccessor, pdfGeneratorClient, generalSettingsOptions); var pdfController = new PdfController( _instanceClient.Object, _pdfFormatter.Object, @@ -169,16 +184,7 @@ public async Task Request_In_Dev_Should_Include_Frontend_Version() _userTokenProvider.Object, httpContextAccessor.Object ); - var pdfService = new PdfService( - _appResources.Object, - _dataClient.Object, - httpContextAccessor.Object, - _profile.Object, - pdfGeneratorClient, - _pdfGeneratorSettingsOptions, - generalSettingsOptions, - _logger.Object - ); + var pdfService = NewPdfService(httpContextAccessor, pdfGeneratorClient, generalSettingsOptions); var pdfController = new PdfController( _instanceClient.Object, _pdfFormatter.Object, @@ -250,16 +256,7 @@ public async Task Request_In_TT02_Should_Ignore_Frontend_Version() _userTokenProvider.Object, httpContextAccessor.Object ); - var pdfService = new PdfService( - _appResources.Object, - _dataClient.Object, - httpContextAccessor.Object, - _profile.Object, - pdfGeneratorClient, - _pdfGeneratorSettingsOptions, - generalSettingsOptions, - _logger.Object - ); + var pdfService = NewPdfService(httpContextAccessor, pdfGeneratorClient, generalSettingsOptions); var pdfController = new PdfController( _instanceClient.Object, _pdfFormatter.Object, diff --git a/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.RunProcessNext_FailingValidator_ReturnsValidationErrors.verified.txt b/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.RunProcessNext_FailingValidator_ReturnsValidationErrors.verified.txt index baa654b72..eb5eaaca9 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.RunProcessNext_FailingValidator_ReturnsValidationErrors.verified.txt +++ b/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.RunProcessNext_FailingValidator_ReturnsValidationErrors.verified.txt @@ -126,17 +126,26 @@ { url.scheme: http }, + { + user.authentication.inAltinnPortal: True + }, { user.authentication.level: 2 }, { - user.authentication.method: Mock + user.authentication.method: BankID }, { - user.id: 1337 + user.authentication.token.isExchanged: False + }, + { + user.authentication.token.issuer: Altinn + }, + { + user.authentication.type: User }, { - user.name: User1337 + user.id: 1337 }, { user.party.id: 500600 @@ -277,4 +286,4 @@ } ], Metrics: [] -} +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.RunProcessNext_PdfFails_DataIsUnlocked.verified.txt b/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.RunProcessNext_PdfFails_DataIsUnlocked.verified.txt index fc95830e6..da6b474ee 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.RunProcessNext_PdfFails_DataIsUnlocked.verified.txt +++ b/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.RunProcessNext_PdfFails_DataIsUnlocked.verified.txt @@ -134,17 +134,26 @@ { url.scheme: http }, + { + user.authentication.inAltinnPortal: True + }, { user.authentication.level: 2 }, { - user.authentication.method: Mock + user.authentication.method: BankID }, { - user.id: 1337 + user.authentication.token.isExchanged: False + }, + { + user.authentication.token.issuer: Altinn + }, + { + user.authentication.type: User }, { - user.name: User1337 + user.id: 1337 }, { user.party.id: 500600 diff --git a/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs index 83cc330e7..99451bdaf 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs @@ -70,7 +70,7 @@ public async Task Get_ShouldReturnProcessTasks() int partyId = 500000; Guid instanceId = new Guid("5d9e906b-83ed-44df-85a7-2f104c640bff"); - HttpClient client = GetRootedClient(org, app, 1337, partyId, 3); + HttpClient client = GetRootedUserClient(org, app, 1337, partyId, 3); TestData.PrepareInstance(org, app, partyId, instanceId); @@ -150,7 +150,7 @@ public async Task RunProcessNextWithLang_VerifyPdfCallWithLanguage() Content = new StringContent("this is the binary pdf content"), }; }; - using var client = GetRootedClient(Org, App, 1337, InstanceOwnerPartyId); + using var client = GetRootedUserClient(Org, App, 1337, InstanceOwnerPartyId); // both "?lang" and "?language" should work var nextResponse = await client.PutAsync( $"{Org}/{App}/instances/{_instanceId}/process/next?lang={language}", @@ -183,7 +183,7 @@ public async Task RunProcessNextWithLanguage_VerifyPdfCall() Content = new StringContent("this is the binary pdf content"), }; }; - using var client = GetRootedClient(Org, App, 1337, InstanceOwnerPartyId); + using var client = GetRootedUserClient(Org, App, 1337, InstanceOwnerPartyId); // both "?lang" and "?language" should work var nextResponse = await client.PutAsync( $"{Org}/{App}/instances/{_instanceId}/process/next?language={language}", @@ -227,7 +227,7 @@ public async Task RunProcessNext_PdfFails_DataIsUnlocked() // Return a 429 to simulate pdf generation failure return new HttpResponseMessage(HttpStatusCode.TooManyRequests); }; - using var client = GetRootedClient(Org, App, 1337, InstanceOwnerPartyId); + using var client = GetRootedUserClient(Org, App, 1337, InstanceOwnerPartyId); var nextResponse = await client.PutAsync($"{Org}/{App}/instances/{_instanceId}/process/next", null); var nextResponseContent = await nextResponse.Content.ReadAsStringAsync(); OutputHelper.WriteLine(nextResponseContent); @@ -278,7 +278,7 @@ public async Task RunProcessNext_FailingValidator_ReturnsValidationErrors() activityFilter: this.ActivityFilter ); }; - using var client = GetRootedClient(Org, App, 1337, InstanceOwnerPartyId); + using var client = GetRootedUserClient(Org, App, 1337, InstanceOwnerPartyId); var nextResponse = await client.PutAsync($"{Org}/{App}/instances/{_instanceId}/process/next", null); var nextResponseContent = await nextResponse.Content.ReadAsStringAsync(); OutputHelper.WriteLine(nextResponseContent); @@ -333,7 +333,7 @@ public async Task RunProcessNext_DataFromHiddenComponents_GetsRemoved() .Verifiable(Times.Once); // create client for tests - using var client = GetRootedClient(Org, App, 1337, InstanceOwnerPartyId); + using var client = GetRootedUserClient(Org, App, 1337, InstanceOwnerPartyId); var dataPath = TestData.GetDataBlobPath(Org, App, InstanceOwnerPartyId, _instanceGuid, _dataGuid); // Update hidden data value @@ -426,7 +426,7 @@ public async Task RunProcessNext_ShadowFields_GetsRemoved(string? saveToDataType .Verifiable(Times.Once); // create client for tests - using var client = GetRootedClient(Org, App, 1337, InstanceOwnerPartyId); + using var client = GetRootedUserClient(Org, App, 1337, InstanceOwnerPartyId); // Update hidden data value var serializedPatch = JsonSerializer.Serialize( @@ -534,7 +534,7 @@ public async Task RunProcessNext_NonErrorValidations_ReturnsOk() services.AddSingleton(dataValidator.Object); services.AddSingleton(pdfMock.Object); }; - using var client = GetRootedClient(Org, App, 1337, InstanceOwnerPartyId); + using var client = GetRootedUserClient(Org, App, 1337, InstanceOwnerPartyId); var nextResponse = await client.PutAsync($"{Org}/{App}/instances/{_instanceId}/process/next", null); var nextResponseContent = await nextResponse.Content.ReadAsStringAsync(); OutputHelper.WriteLine(nextResponseContent); @@ -560,7 +560,7 @@ public async Task RunCompleteTask_GoesToEndEvent() { services.AddSingleton(pdfMock.Object); }; - using var client = GetRootedClient(Org, App, 1337, InstanceOwnerPartyId); + using var client = GetRootedUserClient(Org, App, 1337, InstanceOwnerPartyId); var nextResponse = await client.PutAsync($"{Org}/{App}/instances/{_instanceId}/process/completeProcess", null); var nextResponseContent = await nextResponse.Content.ReadAsStringAsync(); OutputHelper.WriteLine(nextResponseContent); @@ -582,7 +582,7 @@ public async Task RunNextWithAction_WhenActionIsNotAuthorized_ReturnsUnauthorize { services.AddSingleton(pdfMock.Object); }; - using var client = GetRootedClient(Org, App, 1337, InstanceOwnerPartyId); + using var client = GetRootedUserClient(Org, App, 1337, InstanceOwnerPartyId); using var content = new StringContent( """{"action": "unknown-action_unauthorized"}""", Encoding.UTF8, @@ -615,7 +615,7 @@ public async Task ProcessHistory_ShouldReturnProcessHistory() } ); }; - HttpClient client = GetRootedClient(Org, App, 1337, InstanceOwnerPartyId); + HttpClient client = GetRootedUserClient(Org, App, 1337, InstanceOwnerPartyId); string url = $"/{Org}/{App}/instances/{InstanceOwnerPartyId}/{_instanceGuid}/process/history"; HttpResponseMessage response = await client.GetAsync(url); diff --git a/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs index 4b3d48614..c88e56558 100644 --- a/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs @@ -1,3 +1,4 @@ +using System.Globalization; using System.Net.Http.Headers; using System.Security.Claims; using Altinn.App.Api.Controllers; @@ -5,6 +6,7 @@ using Altinn.App.Api.Tests.Utils; using Altinn.App.Core.Constants; using Altinn.App.Core.Features; +using Altinn.App.Core.Features.Auth; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; using Altinn.App.Core.Internal.Prefill; @@ -38,6 +40,7 @@ public async Task Get_Returns_BadRequest_when_dataType_is_null() var prefillMock = new Mock(); var registerMock = new Mock(); var pdpMock = new Mock(); + var authContextMock = new Mock(); ILogger logger = new NullLogger(); var statelessDataController = new StatelessDataController( logger, @@ -46,7 +49,8 @@ public async Task Get_Returns_BadRequest_when_dataType_is_null() prefillMock.Object, registerMock.Object, pdpMock.Object, - new IDataProcessor[] { dataProcessorMock.Object } + new IDataProcessor[] { dataProcessorMock.Object }, + authContextMock.Object ); string dataType = null!; // this is what we're testing @@ -77,6 +81,7 @@ public async Task Get_Returns_BadRequest_when_appResource_classRef_is_null() var prefillMock = new Mock(); var registerMock = new Mock(); var pdpMock = new Mock(); + var authContextMock = new Mock(); var dataType = "some-value"; ILogger logger = new NullLogger(); var statelessDataController = new StatelessDataController( @@ -86,7 +91,8 @@ public async Task Get_Returns_BadRequest_when_appResource_classRef_is_null() prefillMock.Object, registerMock.Object, pdpMock.Object, - new IDataProcessor[] { dataProcessorMock.Object } + new IDataProcessor[] { dataProcessorMock.Object }, + authContextMock.Object ); // Act @@ -142,7 +148,7 @@ public async Task Get_Returns_BadRequest_when_party_header_count_greater_than_on var factory = new StatelessDataControllerWebApplicationFactory(); var client = factory.CreateClient(); - string token = PrincipalUtil.GetToken(1337, null); + string token = TestAuthentication.GetUserToken(1337); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); using var request = new HttpRequestMessage(HttpMethod.Get, "/tdd/demo-app/v1/data?dataType=xml"); request.Headers.Add("party", new string[] { "partyid:234", "partyid:234" }); // Double header @@ -170,7 +176,7 @@ public async Task Get_Returns_Forbidden_when_party_has_no_rights() var factory = new StatelessDataControllerWebApplicationFactory(); var client = factory.CreateClient(); - string token = PrincipalUtil.GetToken(1337, null); + string token = TestAuthentication.GetUserToken(1337); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); using var request = new HttpRequestMessage(HttpMethod.Get, "/tdd/demo-app/v1/data?dataType=xml"); request.Headers.Add("party", new string[] { "partyid:234" }); @@ -199,6 +205,7 @@ public async Task Get_Returns_BadRequest_when_instance_owner_is_empty_party_head var prefillMock = new Mock(); var registerMock = new Mock(); var pdpMock = new Mock(); + var authContextMock = new Mock(); var dataType = "some-value"; ILogger logger = new NullLogger(); var statelessDataController = new StatelessDataController( @@ -208,7 +215,8 @@ public async Task Get_Returns_BadRequest_when_instance_owner_is_empty_party_head prefillMock.Object, registerMock.Object, pdpMock.Object, - new IDataProcessor[] { dataProcessorMock.Object } + new IDataProcessor[] { dataProcessorMock.Object }, + authContextMock.Object ); // Act @@ -237,6 +245,7 @@ public async Task Get_Returns_BadRequest_when_instance_owner_is_empty_user_in_co var prefillMock = new Mock(); var registerMock = new Mock(); var pdpMock = new Mock(); + var authContextMock = new Mock(); var dataType = "some-value"; ILogger logger = new NullLogger(); var statelessDataController = new StatelessDataController( @@ -246,7 +255,8 @@ public async Task Get_Returns_BadRequest_when_instance_owner_is_empty_user_in_co prefillMock.Object, registerMock.Object, pdpMock.Object, - new IDataProcessor[] { dataProcessorMock.Object } + new IDataProcessor[] { dataProcessorMock.Object }, + authContextMock.Object ); statelessDataController.ControllerContext = new ControllerContext(); statelessDataController.ControllerContext.HttpContext = new DefaultHttpContext(); @@ -281,6 +291,7 @@ public async Task Get_Returns_Forbidden_when_returned_descision_is_Deny() var prefillMock = new Mock(); var registerMock = new Mock(); var pdpMock = new Mock(); + var authContextMock = new Mock(); var dataType = "some-value"; ILogger logger = new NullLogger(); var statelessDataController = new StatelessDataController( @@ -290,16 +301,13 @@ public async Task Get_Returns_Forbidden_when_returned_descision_is_Deny() prefillMock.Object, registerMock.Object, pdpMock.Object, - new IDataProcessor[] { dataProcessorMock.Object } + new IDataProcessor[] { dataProcessorMock.Object }, + authContextMock.Object ); statelessDataController.ControllerContext = new ControllerContext(); statelessDataController.ControllerContext.HttpContext = new DefaultHttpContext(); - statelessDataController.ControllerContext.HttpContext.User = new ClaimsPrincipal( - new List() - { - new ClaimsIdentity(new List { new Claim(AltinnUrns.PartyId, "12345", "#integer") }), - } - ); + statelessDataController.ControllerContext.HttpContext.User = TestAuthentication.GetUserPrincipal(); + authContextMock.Setup(c => c.Current).Returns(TestAuthentication.GetUserAuthentication()); pdpMock .Setup(p => p.GetDecisionForRequest(It.IsAny())) .ReturnsAsync( @@ -311,7 +319,6 @@ public async Task Get_Returns_Forbidden_when_returned_descision_is_Deny() }, } ); - registerMock.Setup(r => r.GetParty(12345)).ReturnsAsync(new Platform.Register.Models.Party { PartyId = 12345 }); // Act appResourcesMock.Setup(x => x.GetClassRefForLogicDataType(dataType)).Returns(typeof(DummyModel).FullName!); @@ -321,7 +328,6 @@ public async Task Get_Returns_Forbidden_when_returned_descision_is_Deny() result.Should().BeOfType().Which.StatusCode.Should().Be(403); appResourcesMock.Verify(x => x.GetClassRefForLogicDataType(dataType), Times.Once); appResourcesMock.VerifyNoOtherCalls(); - registerMock.Verify(r => r.GetParty(12345)); pdpMock.Verify(p => p.GetDecisionForRequest(It.IsAny())); pdpMock.VerifyNoOtherCalls(); dataProcessorMock.VerifyNoOtherCalls(); @@ -339,6 +345,7 @@ public async Task Get_Returns_OK_with_appModel() var prefillMock = new Mock(); var registerMock = new Mock(); var pdpMock = new Mock(); + var authContextMock = new Mock(); var dataType = "some-value"; var classRef = typeof(DummyModel).FullName!; ILogger logger = new NullLogger(); @@ -349,16 +356,14 @@ public async Task Get_Returns_OK_with_appModel() prefillMock.Object, registerMock.Object, pdpMock.Object, - new IDataProcessor[] { dataProcessorMock.Object } + new IDataProcessor[] { dataProcessorMock.Object }, + authContextMock.Object ); statelessDataController.ControllerContext = new ControllerContext(); statelessDataController.ControllerContext.HttpContext = new DefaultHttpContext(); - statelessDataController.ControllerContext.HttpContext.User = new ClaimsPrincipal( - new List() - { - new ClaimsIdentity(new List { new Claim(AltinnUrns.PartyId, "12345", "#integer") }), - } - ); + var auth = TestAuthentication.GetUserAuthentication(); + statelessDataController.ControllerContext.HttpContext.User = TestAuthentication.GetUserPrincipal(); + authContextMock.Setup(c => c.Current).Returns(auth); pdpMock .Setup(p => p.GetDecisionForRequest(It.IsAny())) .ReturnsAsync( @@ -371,7 +376,6 @@ public async Task Get_Returns_OK_with_appModel() } ); appModelMock.Setup(a => a.Create(classRef)).Returns(new DummyModel()); - registerMock.Setup(r => r.GetParty(12345)).ReturnsAsync(new Platform.Register.Models.Party { PartyId = 12345 }); // Act appResourcesMock.Setup(x => x.GetClassRefForLogicDataType(dataType)).Returns(classRef); @@ -383,9 +387,15 @@ public async Task Get_Returns_OK_with_appModel() appResourcesMock.Verify(x => x.GetClassRefForLogicDataType(dataType), Times.Once); pdpMock.Verify(p => p.GetDecisionForRequest(It.IsAny())); appModelMock.Verify(a => a.Create(classRef), Times.Once); - prefillMock.Verify(p => p.PrefillDataModel("12345", dataType, It.IsAny(), null)); + prefillMock.Verify(p => + p.PrefillDataModel( + auth.SelectedPartyId.ToString(CultureInfo.InvariantCulture), + dataType, + It.IsAny(), + null + ) + ); dataProcessorMock.Verify(a => a.ProcessDataRead(It.IsAny(), null, It.IsAny(), null)); - registerMock.Verify(r => r.GetParty(12345)); appResourcesMock.VerifyNoOtherCalls(); pdpMock.VerifyNoOtherCalls(); dataProcessorMock.VerifyNoOtherCalls(); diff --git a/test/Altinn.App.Api.Tests/Controllers/UserDefinedMetadataControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/UserDefinedMetadataControllerTests.cs index 069386505..d27237814 100644 --- a/test/Altinn.App.Api.Tests/Controllers/UserDefinedMetadataControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/UserDefinedMetadataControllerTests.cs @@ -22,7 +22,7 @@ public UserDefinedMetadataControllerTests(WebApplicationFactory factory [Fact] public async Task PutUserDefinedMetadata_HappyPath_ReturnsOk() { - HttpClient client = GetHttpClient(); + using HttpClient client = GetRootedUserClient(Org, App, 1337); (string instanceId, string dataGuid) = await CreateInstanceAndDataElement(client); // Update custom metadata @@ -63,7 +63,7 @@ public async Task PutUserDefinedMetadata_HappyPath_ReturnsOk() [Fact] public async Task PutUserDefinedMetadata_DuplicatedKey_ReturnsBadRequest() { - HttpClient client = GetHttpClient(); + using HttpClient client = GetRootedUserClient(Org, App, 1337); (string instanceId, string dataGuid) = await CreateInstanceAndDataElement(client); // Update custom metadata @@ -88,7 +88,7 @@ public async Task PutUserDefinedMetadata_DuplicatedKey_ReturnsBadRequest() [Fact] public async Task PutUserDefinedMetadata_NotAllowedKey_ReturnsBadRequest() { - HttpClient client = GetHttpClient(); + using HttpClient client = GetRootedUserClient(Org, App, 1337); (string instanceId, string dataGuid) = await CreateInstanceAndDataElement(client); // Update custom metadata @@ -113,7 +113,7 @@ public async Task PutUserDefinedMetadata_NotAllowedKey_ReturnsBadRequest() [Fact] public async Task PutUserDefinedMetadata_InvalidDataElementId_ReturnsNotFound() { - HttpClient client = GetHttpClient(); + using HttpClient client = GetRootedUserClient(Org, App, 1337); // Create instance HttpResponseMessage createResponse = await client.PostAsync( @@ -158,12 +158,4 @@ public async Task PutUserDefinedMetadata_InvalidDataElementId_ReturnsNotFound() return (instanceId, dataGuid); } - - private HttpClient GetHttpClient() - { - HttpClient client = GetRootedClient(Org, App); - string token = PrincipalUtil.GetToken(1337, null); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - return client; - } } diff --git a/test/Altinn.App.Api.Tests/Controllers/ValidateController_ValidateInstanceTests.cs b/test/Altinn.App.Api.Tests/Controllers/ValidateController_ValidateInstanceTests.cs index 3ef623c1c..830fc5b82 100644 --- a/test/Altinn.App.Api.Tests/Controllers/ValidateController_ValidateInstanceTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/ValidateController_ValidateInstanceTests.cs @@ -56,7 +56,7 @@ ITestOutputHelper outputHelper private async Task CallValidateInstanceApi() { using var httpClient = GetRootedClient(Org, App); - string token = PrincipalUtil.GetToken(1337, null); + string token = TestAuthentication.GetUserToken(userId: 1337); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); return await httpClient.GetAsync($"/{Org}/{App}/instances/{InstanceId}/validate"); } @@ -64,7 +64,7 @@ private async Task CallValidateInstanceApi() private async Task<(HttpResponseMessage response, string responseString)> CallValidateDataApi() { using var httpClient = GetRootedClient(Org, App); - string token = PrincipalUtil.GetToken(1337, null); + string token = TestAuthentication.GetUserToken(userId: 1337); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); var response = await httpClient.GetAsync($"/{Org}/{App}/instances/{InstanceId}/data/{DataGuid}/validate"); var responseString = await LogResponse(response); diff --git a/test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs b/test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs index 95422408e..0e2288467 100644 --- a/test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs +++ b/test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs @@ -98,17 +98,30 @@ public Task Invoke(HttpContext httpContext) } } - public HttpClient GetRootedClient( + public HttpClient GetRootedUserClient( string org, string app, - int userId, - int? partyId, - int authenticationLevel = 2, - string? serviceOwnerOrg = null + int userId = TestAuthentication.DefaultUserId, + int partyId = TestAuthentication.DefaultUserPartyId, + int authenticationLevel = TestAuthentication.DefaultUserAuthenticationLevel ) { var client = GetRootedClient(org, app); - string token = PrincipalUtil.GetToken(userId, partyId, authenticationLevel, org: serviceOwnerOrg); + string token = TestAuthentication.GetUserToken(userId, partyId, authenticationLevel); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + return client; + } + + public HttpClient GetRootedOrgClient( + string org, + string app, + string orgNumber = TestAuthentication.DefaultOrgNumber, + string scope = TestAuthentication.DefaultServiceOwnerScope, + string serviceOwnerOrg = TestAuthentication.DefaultOrg + ) + { + var client = GetRootedClient(org, app); + string token = TestAuthentication.GetServiceOwnerToken(orgNumber, org: serviceOwnerOrg, scope: scope); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); return client; } diff --git a/test/Altinn.App.Api.Tests/Data/Register/Party/5001337.json b/test/Altinn.App.Api.Tests/Data/Register/Party/5001337.json new file mode 100644 index 000000000..c232b2d4e --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/Register/Party/5001337.json @@ -0,0 +1,13 @@ +{ + "partyId": "5001337", + "partyTypeName": 2, + "orgNumber": "405003309", + "ssn": null, + "unitType": "BEDR", + "name": "Testdepartementet", + "isDeleted": false, + "onlyHierarchyElementWithNoAccess": false, + "person": null, + "organisation": null, + "childParties": null +} diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/applicationmetadata.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/applicationmetadata.json index 724329672..30cdea087 100644 --- a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/applicationmetadata.json +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/applicationmetadata.json @@ -33,7 +33,7 @@ "maxCount": 1, "allowedContributers": [ "org:tdd", - "orgno:160694123", + "orgno:405003309", "invalidKey:value" ], "taskId": "Task_1" diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/authorization/policy.xml b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/authorization/policy.xml index 098ffe13c..1d73b76c7 100644 --- a/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/authorization/policy.xml +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/config/authorization/policy.xml @@ -185,7 +185,7 @@ - 160694123 + 405003309 diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/appsettings.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/appsettings.json new file mode 100644 index 000000000..a54ed444a --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/appsettings.json @@ -0,0 +1,35 @@ +{ + "Kestrel": { + "EndPoints": { + "Http": { + "Url": "http://*:5005" + } + } + }, + "AppSettings": { + "RuntimeCookieName": "AltinnStudioRuntime", + "RequiredValidation": true, + "ExpressionValidation": true + }, + "GeneralSettings": { + "HostName": "altinn3.no", + "TemplateLocation": "../Templates", + "RuntimeMode": "AltinnStudio", + "LanguageFilesLocation": "../Common/Languages/ini/", + "SoftValidationPrefix": "*WARNING*", + "AltinnStudioEndpoint": "http://altinn3.no/", + "AltinnPartyCookieName": "AltinnPartyId" + }, + "PlatformSettings": { + "ApiStorageEndpoint": "http://localhost:5101/storage/api/v1/", + "ApiRegisterEndpoint": "http://localhost:5101/register/api/v1/", + "ApiProfileEndpoint": "http://localhost:5101/profile/api/v1/", + "ApiAuthenticationEndpoint": "http://localhost:5101/authentication/api/v1/", + "ApiAuthorizationEndpoint": "http://localhost:5101/authorization/api/v1/", + "ApiEventsEndpoint": "http://localhost:5101/events/api/v1/", + "SubscriptionKey": "retrieved from environment at runtime" + }, + "ApplicationInsights": { + "InstrumentationKey": "b1020135-1b69-4e4d-8b8e-217072c70879" + } +} diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/config/applicationmetadata.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/config/applicationmetadata.json new file mode 100644 index 000000000..3befb18cb --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/config/applicationmetadata.json @@ -0,0 +1,105 @@ +{ + "id": "tdd/permissive-app", + "org": "tdd", + "created": "2019-09-24T10:02:41.0839253Z", + "createdBy": "Kritsi", + "lastChanged": "2019-09-24T10:02:41.0839254Z", + "lastChangedBy": "Kritsi", + "title": { + "nb": "Endring av navn (RF-1453)", + "nb-NO": "Endring av navn (RF-1453)" + }, + "copyInstanceSettings": { + "enabled": true + }, + "dataTypes": [ + { + "id": "default", + "allowedContentTypes": [ + "application/xml" + ], + "maxCount": 1, + "appLogic": { + "autoCreate": true, + "ClassRef": "Altinn.App.Api.Tests.Data.apps.tdd.permissive_app.models.Skjema" + }, + "taskId": "Task_1", + "AllowedKeysForUserDefinedMetadata": [ + "TheKey" + ] + }, + { + "id": "customElement", + "maxCount": 1, + "allowedContributers": [ + "org:tdd", + "orgno:405003309", + "invalidKey:value" + ], + "taskId": "Task_1" + }, + { + "id": "9edd53de-f46f-40a1-bb4d-3efb93dc113d", + "taskId": "Task_1", + "maxSize": 1, + "maxCount": 1, + "minCount": 0 + }, + { + "id": "specificFileType", + "taskId": "Task_1", + "maxSize": 1, + "maxCount": 1, + "minCount": 0, + "allowedContentTypes": [ + "application/pdf", + "image/png", + "application/json" + ], + "enabledFileAnalysers": [ + "mimeTypeAnalyser" + ], + "enabledFileValidators": [ + "mimeTypeValidator" + ] + }, + { + "id": "userInteractionUnspecified", + "allowedContentTypes": [ + "application/xml" + ], + "maxCount": 10, + "appLogic": { + "ClassRef": "Altinn.App.Api.Tests.Data.apps.tdd.permissive_app.models.Skjema" + }, + "taskId": "Task_1", + "AllowedKeysForUserDefinedMetadata": [ + "TheKey" + ] + }, + { + "id": "disallowUserCreate", + "allowedContentTypes": [ + "application/xml" + ], + "maxCount": 10, + "appLogic": { + "ClassRef": "Altinn.App.Api.Tests.Data.apps.tdd.permissive_app.models.Skjema", + "disallowUserCreate": true + }, + "taskId": "Task_1" + }, + { + "id": "disallowUserDelete", + "allowedContentTypes": [ + "application/xml" + ], + "maxCount": 10, + "appLogic": { + "ClassRef": "Altinn.App.Api.Tests.Data.apps.tdd.permissive_app.models.Skjema", + "diallowUserDelete": true + }, + "taskId": "Task_1" + } + ] +} diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/config/authorization/policy.xml b/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/config/authorization/policy.xml new file mode 100644 index 000000000..92f575edf --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/config/authorization/policy.xml @@ -0,0 +1,289 @@ + + + + + A rule giving user with role REGNA or DAGL the right to instantiate a instance of a given app of tdd/permissive-app + + + + + REGNA + + + + + + DAGL + + + + + + tdd + + + + + + 405003309 + + + + + + f58fe166-bc22-4899-beb7-c3e8e3332f43 + + + + + + + + tdd + + + + permissive-app + + + + + + + + instantiate + + + + + + + + Eksempel på samleregel som spesifiserer at både REGNA og DAGL m/sikkerhetsnivå; 2, for ressursen; SKD/TaxReport får tilgang til operasjonene; Read, Write og Instantiate både for Event; Tasks; FormFilling og Signing + + + + + REGNA + + + + + + DAGL + + + + + + tdd + + + + + + 405003309 + + + + + + f58fe166-bc22-4899-beb7-c3e8e3332f43 + + + + + + + + tdd + + + + permissive-app + + + + Task_1 + + + + + + + + read + + + + + + write + + + + + + + + Eksempel på tilleggsregel som spesifiserer at bare DAGL m/sikkerhetsnivå; 2, for ressursen; SKD/TaxReport får tilgang til operasjonen; Sign både for Event; SluttEvent_1b + + + + + DAGL + + + + + + + + tdd + + + + permissive-app + + + + signing + + + + + + + + sign + + + + + + + + Example rule that gives org nav and skd read right to the app inn all states + + + + + tdd + + + + + + + + tdd + + + + + + + + tdd + + + + permissive-app + + + + + + + + read + + + + + + + + Example rule that gives org write right for app inn all states + + + + + tdd + + + + + + 405003309 + + + + + + + + tdd + + + + permissive-app + + + + + + + + write + + + + + + + + Rule that defines that org can complete an instance of tdd/permissive-app which state is at the end event. + + + + + tdd + + + + + + + + tdd + + + + permissive-app + + + + EndEvent_1 + + + + + + + + complete + + + + + + + + + + 0 + + + + diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/config/process/process.bpmn b/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/config/process/process.bpmn new file mode 100644 index 000000000..2274ca316 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/config/process/process.bpmn @@ -0,0 +1,50 @@ + + + + + SequenceFlow_1n56yn5 + + + SequenceFlow_1n56yn5 + SequenceFlow_1oot28q + + + data + + + + + SequenceFlow_1oot28q + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/config/texts/resource.nb.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/config/texts/resource.nb.json new file mode 100644 index 000000000..9c9ca4534 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/config/texts/resource.nb.json @@ -0,0 +1,169 @@ +{ + "language": "nb-NO", + "resources": [ + { + "id": "Radioknapp.Label", + "value": "Radioknapp komponent for automatisert testing" + }, + { + "id": "34882.MelderMalformdatadef34882.Label", + "value": "Målform" + }, + { + "id": "34883.SignererTredjeMalformdatadef34883.Label", + "value": "Målform" + }, + { + "id": "34884.SignererTredjeFodselsnummerdatadef34884.Label", + "value": "Fødselsnummer" + }, + { + "id": "34885.SignererTredjeEtternavndatadef34885.Label", + "value": "Etternavn" + }, + { + "id": "34886.SignererTredjeFornavndatadef34886.Label", + "value": "Fornavn" + }, + { + "id": "34887.SignererTredjeMellomnavndatadef34887.Label", + "value": "Mellomnavn" + }, + { + "id": "34889.SignererTredjeEpostdatadef34889.Label", + "value": "epost" + }, + { + "id": "34890.SignererTredjeMobiltelefonnummerdatadef34890.Label", + "value": "Mobiltelefonnummer" + }, + { + "id": "34891.SignererTredjeReferanseAltinndatadef34891.Label", + "value": "Referansenummer Altinn" + }, + { + "id": "AppTittel", + "value": "Endring av navn" + }, + { + "id": "AutomatedTest_Radioknapp.Label", + "value": "Radioknapp komponent for automatisert testing" + }, + { + "id": "BegrunnelseAnnet", + "value": "Forklar hvorfor du ønsker å ta navnet, og eventuelt hvilken tilknytning du har til navnet:" + }, + { + "id": "BegrunnelseGard1", + "value": "Gårdsbruk du vil ta navnet fra" + }, + { + "id": "BegrunnelseGard2", + "value": "Kommune gårdsbruket ligger i" + }, + { + "id": "BegrunnelseGard3", + "value": "Gårdsnummer" + }, + { + "id": "BegrunnelseGard4", + "value": "Bruksnummer" + }, + { + "id": "BegrunnelseGard5", + "value": "Forklar din tilknytning til gårdsbruket" + }, + { + "id": "BegrunnelseNyttNavn", + "value": "Du ønsker å ta et navn som du tror er nytt i Norge. Forklar hvorfor du ønsker dette navnet:" + }, + { + "id": "BegrunnelseSamboer1", + "value": "Etternavn på samboer" + }, + { + "id": "BegrunnelseSamboer2", + "value": "Fødselsnummer på samboer" + }, + { + "id": "BegrunnelseSlektskap", + "value": "Forklar hvem du tar navnet fra, og hvilken side slektskapet ligger" + }, + { + "id": "BegrunnelseSteforeldre", + "value": "Etternavn på ste- eller fosterforeldre du ønsker å ta navnet til" + }, + { + "id": "BegrunnelseValgNavn", + "value": "Vennligst oppgi begrunnelse for endring av navn" + }, + { + "id": "BekreftNavnTittel", + "value": "Bekreftelse av navn" + }, + { + "id": "DineEndringer", + "value": "Dine endringer" + }, + { + "id": "EndreNavnFra", + "value": "Du har valgt å endre:" + }, + { + "id": "EndreNavnTil", + "value": "Til:" + }, + { + "id": "Epost", + "value": "E-post" + }, + { + "id": "Fodselsnummer", + "value": "Fødselsnummer" + }, + { + "id": "HintEtternavn", + "value": "Bindestrek må benyttes dersom du ønsker to etternavn." + }, + { + "id": "HintMellomnavn", + "value": "Mellomnavn må være et navn som kan benyttes som etternavn. " + }, + { + "id": "Kontaktinformasjon", + "value": "Kontaktinformasjon" + }, + { + "id": "NavarendeNavn", + "value": "Nåværende navn" + }, + { + "id": "PersonNyttEtternavn", + "value": "Nytt etternavn" + }, + { + "id": "PersonNyttFornavn", + "value": "Nytt fornavn" + }, + { + "id": "PersonNyttMellomnavn", + "value": "Nytt mellomnavn" + }, + { + "id": "ServiceName", + "value": "Contribuer restriction app" + }, + { + "id": "Skjema.Label", + "value": "Radioknapp komponent for automatisert testing" + }, + { + "id": "Telefonnummer", + "value": "Telefonnummer" + }, + { + "id": "kontaktinfoBeskrivelse", + "value": "Kontaktinformasjon dersom saksbehandler ønsker å ta kontakt vedrørende denne saken" + } + ] +} diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/models/Skjema.cs b/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/models/Skjema.cs new file mode 100644 index 000000000..578441990 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/models/Skjema.cs @@ -0,0 +1,144 @@ +#pragma warning disable IDE1006 // Naming Styles does not matter in model classes +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; +using System.Xml.Serialization; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Newtonsoft.Json; + +namespace Altinn.App.Api.Tests.Data.apps.tdd.permissive_app.models; + +public class Skjema +{ + [XmlElement("melding", Order = 1)] + [JsonProperty("melding")] + [JsonPropertyName("melding")] + public Dummy? Melding { get; set; } +} + +public class Dummy +{ + [XmlElement("name", Order = 1)] + [JsonProperty("name")] + [JsonPropertyName("name")] + public string? Name { get; set; } + + [XmlElement("random", Order = 2)] + [JsonProperty("random")] + [JsonPropertyName("random")] + public string? Random { get; set; } + + [XmlElement("tags", Order = 3)] + [JsonProperty("tags")] + [JsonPropertyName("tags")] + public string? Tags { get; set; } + + [XmlElement("simple_list", Order = 4)] + [JsonProperty("simple_list")] + [JsonPropertyName("simple_list")] + public ValuesList? SimpleList { get; set; } + + [XmlElement("nested_list", Order = 5)] + [JsonProperty("nested_list")] + [JsonPropertyName("nested_list")] + public List? NestedList { get; set; } + + [XmlElement("toggle", Order = 6)] + [JsonProperty("toggle")] + [JsonPropertyName("toggle")] + public bool Toggle { get; set; } + + [XmlElement("tag-with-attribute", IsNullable = true, Order = 7)] + [JsonProperty("tag-with-attribute")] + [JsonPropertyName("tag-with-attribute")] + public TagWithAttribute? TagWithAttribute { get; set; } + + public bool ShouldSerializeTagWithAttribute() + { + return TagWithAttribute?.value != null; + } + + [XmlElement("hidden", Order = 8)] + [JsonProperty("hidden")] + [JsonPropertyName("hidden")] + public string? Hidden { get; set; } + + [XmlElement("SF_test", Order = 9)] + [JsonProperty("SF_test")] + [JsonPropertyName("SF_test")] + public string? SF_test { get; set; } +} + +public class TagWithAttribute +{ + [Range(1, Int32.MaxValue)] + [XmlAttribute("orid")] + [BindNever] + public decimal orid { get; set; } = 34730; + + [MinLength(1)] + [MaxLength(60)] + [XmlText()] + public string? value { get; set; } +} + +public class ValuesList +{ + [XmlElement("simple_keyvalues", Order = 1)] + [JsonProperty("simple_keyvalues")] + [JsonPropertyName("simple_keyvalues")] + public List? SimpleKeyvalues { get; set; } +} + +public class SimpleKeyvalues +{ + [XmlAttribute("altinnRowId")] + [JsonPropertyName("altinnRowId")] + [System.Text.Json.Serialization.JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public Guid AltinnRowId { get; set; } + + public bool AltinnRowIdSpecified() + { + return AltinnRowId != default; + } + + [XmlElement("key", Order = 1)] + [JsonProperty("key")] + [JsonPropertyName("key")] + public string? Key { get; set; } + + [XmlElement("doubleValue", Order = 2)] + [JsonProperty("doubleValue")] + [JsonPropertyName("doubleValue")] + public decimal? DoubleValue { get; set; } + + [Range(int.MinValue, int.MaxValue)] + [XmlElement("intValue", Order = 3)] + [JsonProperty("intValue")] + [JsonPropertyName("intValue")] + public decimal? IntValue { get; set; } +} + +public class Nested +{ + [XmlAttribute("altinnRowId")] + [JsonPropertyName("altinnRowId")] + [System.Text.Json.Serialization.JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public Guid AltinnRowId { get; set; } + + public bool AltinnRowIdSpecified() + { + return AltinnRowId != default; + } + + [XmlElement("key", Order = 1)] + [JsonProperty("key")] + [JsonPropertyName("key")] + public string? Key { get; set; } + + [XmlElement("values", Order = 2)] + [JsonProperty("values")] + [JsonPropertyName("values")] + public List? Values { get; set; } +} + +#pragma warning restore IDE1006 // Naming Styles diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/models/default.validation.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/models/default.validation.json new file mode 100644 index 000000000..73f38c259 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/models/default.validation.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://altinncdn.no/toolkits/altinn-app-frontend/4/schemas/json/validation/validation.schema.v1.json", + "validations": { + "melding.name": [ + { + "condition": [ + "equals", + [ + "dataModel", + "melding.name" + ], + "Model" + ], + "message": "The file must be of type Model" + } + ] + } +} diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/options/fileSourceOptions.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/options/fileSourceOptions.json new file mode 100644 index 000000000..0e9b9e454 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/options/fileSourceOptions.json @@ -0,0 +1,24 @@ +[ + // Comment is valid + { + "value": null, + "label": "" + }, + { + "value": "string-value", + "label": "string-label" + }, + { + "value": 3, + "label": "number" + }, + { + "value": true, + "label": "boolean-true" + }, + { + "value": false, + "label": "boolean-false", + // Trailing comma should be valid + }, +] \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/ui/default/Settings.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/ui/default/Settings.json new file mode 100644 index 000000000..7e63e1852 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/ui/default/Settings.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://altinncdn.no/schemas/json/layout/layoutSettings.schema.v1.json", + "pages": { + "order": [ + "page" + ] + } +} \ No newline at end of file diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/ui/default/layouts/page.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/ui/default/layouts/page.json new file mode 100644 index 000000000..4d7d98ae9 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/ui/default/layouts/page.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "Heading2-2d1ba9d7-e284-4acd-a37b-e4c8ce45142a", + "type": "Header", + "size": "h2", + "textResourceBindings": { + "title": "Brukeropp-side-overskrift" + } + }, + { + "id": "name", + "type": "Input", + "required": true, + "dataModelBindings": { + "simpleBinding": "melding.name" + } + }, + { + "id": "hidden", + "type": "Input", + "hidden": true, + "dataModelBindings": { + "simpleBinding": "melding.hidden" + } + } + ] + } +} diff --git a/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/ui/layout-sets.json b/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/ui/layout-sets.json new file mode 100644 index 000000000..960ed988c --- /dev/null +++ b/test/Altinn.App.Api.Tests/Data/apps/tdd/permissive-app/ui/layout-sets.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://altinncdn.no/schemas/json/layout/layout-sets.schema.v1.json", + "sets":[ + { + "id": "default", + "dataType": "default", + "tasks": ["Task_1"] + } + ] +} diff --git a/test/Altinn.App.Api.Tests/Helpers/DataElementAccessCheckerTests.cs b/test/Altinn.App.Api.Tests/Helpers/DataElementAccessCheckerTests.cs index b3a06c143..9367d889c 100644 --- a/test/Altinn.App.Api.Tests/Helpers/DataElementAccessCheckerTests.cs +++ b/test/Altinn.App.Api.Tests/Helpers/DataElementAccessCheckerTests.cs @@ -1,4 +1,7 @@ +using System.Globalization; using Altinn.App.Api.Helpers; +using Altinn.App.Api.Tests.Utils; +using Altinn.App.Core.Features.Auth; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; @@ -7,21 +10,29 @@ namespace Altinn.App.Api.Tests.Helpers; public class DataElementAccessCheckerTests { [Theory] - [InlineData(null, null, null, true)] // No allowed contributors, should be true - [InlineData("org:altinn", "altinn", null, true)] // Matching org - [InlineData("org:altinn", "Altinn", null, true)] // Matching org, case insensitive - [InlineData("org:altinn", "notAltinn", null, false)] // Non-matching org - [InlineData("orgno:12345678", null, 12345678, true)] // Matching orgNr - [InlineData("orgno:12345678", null, 87654321, false)] // Non-matching orgNr - [InlineData("orgno:12345678", null, null, false)] // orgNr is null - [InlineData("org:altinn,orgno:12345678", "altinn", 12345678, true)] // Matching both - [InlineData("org:altinn,orgno:12345678", "altinn", 87654321, true)] // Matching org only - [InlineData("org:altinn,orgno:12345678", "notAltinn", 12345678, true)] // Matching orgNr only - [InlineData("org:altinn,orgno:12345678", "notAltinn", 87654321, false)] // Non-matching both + [InlineData(null, null, null, false, true)] // No allowed contributors, should be true + [InlineData("org:altinn", "altinn", 370194483, false, true)] // Matching org + [InlineData("org:altinn", "Altinn", 370194483, false, true)] // Matching org, case insensitive + [InlineData("org:altinn", "notAltinn", 370194483, false, false)] // Non-matching org + [InlineData("orgno:370194483", "altinn", 370194483, false, true)] // Matching orgNr + [InlineData("orgno:370194483", "altinn", 556750777, false, false)] // Non-matching orgNr + [InlineData("orgno:370194483", null, 370194483, false, true)] // Matching orgNr (not serviceowner) + [InlineData("orgno:370194483", null, 556750777, false, false)] // Non-matching orgNr (not serviceowner) + [InlineData("orgno:370194483", null, null, false, false)] // orgNr is null + [InlineData("org:altinn,orgno:370194483", "altinn", 370194483, false, true)] // Matching both + [InlineData("org:altinn,orgno:370194483", "altinn", 556750777, false, true)] // Matching org only + [InlineData("org:altinn,orgno:370194483", "notAltinn", 370194483, false, true)] // Matching orgNr only + [InlineData("org:altinn,orgno:370194483", "notAltinn", 556750777, false, false)] // Non-matching both + [InlineData("org:altinn,orgno:556750777", null, 556750777, true, true)] // Matching second rule + [InlineData("org:altinn,orgno:556750777", null, 556750777, false, true)] // Matching second rule + [InlineData("orgno:370194483", null, 370194483, true, true)] // Matching orgNr (as systemuser) + [InlineData("orgno:370194483", null, 556750777, true, false)] // Non-matching orgNr (as systemuser) + [InlineData("org:altinn", null, 370194483, true, false)] // Org (as systemuser) public void IsValidContributor_ShouldReturnExpectedResult( string? allowedContributors, string? org, int? orgNr, + bool isSystemUser, bool expectedResult ) { @@ -30,9 +41,24 @@ bool expectedResult { AllowedContributers = allowedContributors?.Split(',')?.ToList() ?? new List(), }; + Authenticated auth = (org, orgNr, isSystemUser) switch + { + (null, null, _) => TestAuthentication.GetNoneAuthentication(), + (string orgName, int orgNo, _) => TestAuthentication.GetServiceOwnerAuthentication( + orgNo.ToString(CultureInfo.InvariantCulture), + orgName + ), + (null, int orgNumber, bool systemUser) when !systemUser => TestAuthentication.GetOrgAuthentication( + orgNumber.ToString(CultureInfo.InvariantCulture) + ), + (null, int orgNumber, bool systemUser) when systemUser => TestAuthentication.GetSystemUserAuthentication( + systemUserOrgNumber: orgNumber.ToString(CultureInfo.InvariantCulture) + ), + _ => throw new Exception("Unhandled case"), + }; // Act - bool result = DataElementAccessChecker.IsValidContributor(dataType, org, orgNr); + bool result = DataElementAccessChecker.IsValidContributor(dataType, auth); // Assert result.Should().Be(expectedResult); diff --git a/test/Altinn.App.Api.Tests/Helpers/UserHelperTest.cs b/test/Altinn.App.Api.Tests/Helpers/UserHelperTest.cs index 626a64729..6abc292fc 100644 --- a/test/Altinn.App.Api.Tests/Helpers/UserHelperTest.cs +++ b/test/Altinn.App.Api.Tests/Helpers/UserHelperTest.cs @@ -50,7 +50,7 @@ public async Task GetUserContext_PerformsCorrectLogic(int userId, int partyId, s { // Arrange const int authLevel = 3; - var userPrincipal = PrincipalUtil.GetUserPrincipal(userId, partyId, authLevel); + var userPrincipal = TestAuthentication.GetUserPrincipal(userId, partyId, authLevel); await using var fixture = Fixture.Create(userPrincipal); var userHelper = new UserHelper( profileClient: fixture.ProfileClientMock, @@ -71,10 +71,10 @@ public async Task GetUserContext_PerformsCorrectLogic(int userId, int partyId, s result .Should() .BeEquivalentTo( - new Altinn.App.Core.Models.UserContext + new Core.Models.UserContext { SocialSecurityNumber = ssn, - UserName = $"User{userId}", + UserName = null, UserId = userId, PartyId = partyId, AuthenticationLevel = authLevel, @@ -91,7 +91,7 @@ public async Task GetUserContext_HandlesMissingClaims() // Arrange const int userId = 1001; const int authLevel = 3; - var userPrincipal = PrincipalUtil.GetUserPrincipal(userId, default, authLevel); + var userPrincipal = TestAuthentication.GetUserPrincipal(userId, default, authLevel); await using var fixture = Fixture.Create(userPrincipal); var userHelper = new UserHelper( profileClient: fixture.ProfileClientMock, @@ -109,10 +109,10 @@ public async Task GetUserContext_HandlesMissingClaims() result .Should() .BeEquivalentTo( - new Altinn.App.Core.Models.UserContext + new Core.Models.UserContext { SocialSecurityNumber = null, - UserName = $"User{userId}", + UserName = null, UserId = userId, PartyId = default, AuthenticationLevel = authLevel, @@ -127,7 +127,7 @@ public async Task GetUserContext_HandlesMissingClaims() public async Task GetUserContext_ThrowsOnMissingUserId() { // Arrange - var userPrincipal = PrincipalUtil.GetUserPrincipal(default, default); + var userPrincipal = TestAuthentication.GetUserPrincipal(default, default); await using var fixture = Fixture.Create(userPrincipal); var userHelper = new UserHelper( profileClient: fixture.ProfileClientMock, diff --git a/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Always_Be_A_Root_Trace.verified.txt b/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Always_Be_A_Root_Trace.verified.txt index c719e047a..9b93a388d 100644 --- a/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Always_Be_A_Root_Trace.verified.txt +++ b/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Always_Be_A_Root_Trace.verified.txt @@ -27,17 +27,26 @@ { url.scheme: http }, + { + user.authentication.inAltinnPortal: True + }, { user.authentication.level: 4 }, { - user.authentication.method: Mock + user.authentication.method: BankID }, { - user.id: 10 + user.authentication.token.isExchanged: False + }, + { + user.authentication.token.issuer: Altinn + }, + { + user.authentication.type: User }, { - user.name: User10 + user.id: 10 }, { user.party.id: Scrubbed diff --git a/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Always_Be_A_Root_Trace_Unless_Pdf.verified.txt b/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Always_Be_A_Root_Trace_Unless_Pdf.verified.txt index c719e047a..9b93a388d 100644 --- a/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Always_Be_A_Root_Trace_Unless_Pdf.verified.txt +++ b/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Always_Be_A_Root_Trace_Unless_Pdf.verified.txt @@ -27,17 +27,26 @@ { url.scheme: http }, + { + user.authentication.inAltinnPortal: True + }, { user.authentication.level: 4 }, { - user.authentication.method: Mock + user.authentication.method: BankID }, { - user.id: 10 + user.authentication.token.isExchanged: False + }, + { + user.authentication.token.issuer: Altinn + }, + { + user.authentication.type: User }, { - user.name: User10 + user.id: 10 }, { user.party.id: Scrubbed diff --git a/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Have_Root_AspNetCore_Trace_Org.verified.txt b/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Have_Root_AspNetCore_Trace_Org.verified.txt index 7f276cd4a..d43bdcc8d 100644 --- a/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Have_Root_AspNetCore_Trace_Org.verified.txt +++ b/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Have_Root_AspNetCore_Trace_Org.verified.txt @@ -16,16 +16,16 @@ network.protocol.version: 1.1 }, { - organisation.name: Guid_1 + organisation.name: tdd }, { - organisation.number: 160694123 + organisation.number: 405003309 }, { server.address: localhost }, { - TestId: Guid_2 + TestId: Guid_1 }, { url.path: /tdd/contributer-restriction/api/v1/applicationmetadata @@ -34,10 +34,19 @@ url.scheme: http }, { - user.authentication.level: 4 + user.authentication.level: 3 }, { - user.authentication.method: Mock + user.authentication.method: maskinporten + }, + { + user.authentication.token.isExchanged: True + }, + { + user.authentication.token.issuer: Maskinporten + }, + { + user.authentication.type: ServiceOwner } ], IdFormat: W3C, diff --git a/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Have_Root_AspNetCore_Trace_User.verified.txt b/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Have_Root_AspNetCore_Trace_User.verified.txt index c719e047a..9b93a388d 100644 --- a/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Have_Root_AspNetCore_Trace_User.verified.txt +++ b/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.Should_Have_Root_AspNetCore_Trace_User.verified.txt @@ -27,17 +27,26 @@ { url.scheme: http }, + { + user.authentication.inAltinnPortal: True + }, { user.authentication.level: 4 }, { - user.authentication.method: Mock + user.authentication.method: BankID }, { - user.id: 10 + user.authentication.token.isExchanged: False + }, + { + user.authentication.token.issuer: Altinn + }, + { + user.authentication.type: User }, { - user.name: User10 + user.id: 10 }, { user.party.id: Scrubbed diff --git a/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.cs b/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.cs index 4d825c93b..d1a5c0029 100644 --- a/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.cs +++ b/test/Altinn.App.Api.Tests/Middleware/TelemetryEnrichingMiddlewareTests.cs @@ -46,7 +46,7 @@ public TelemetryEnrichingMiddlewareTests(WebApplicationFactory factory, public async Task Should_Have_Root_AspNetCore_Trace_Org() { var org = Guid.NewGuid().ToString(); - string token = PrincipalUtil.GetOrgToken(org, "160694123", 4); + string token = TestAuthentication.GetServiceOwnerToken(); var (telemetry, request) = AnalyzeTelemetry(token); await request(); @@ -68,7 +68,7 @@ public async Task Should_Have_Root_AspNetCore_Trace_Org() public async Task Should_Have_Root_AspNetCore_Trace_User() { var partyId = Random.Shared.Next(); - var principal = PrincipalUtil.GetUserPrincipal(10, partyId, 4); + var principal = TestAuthentication.GetUserPrincipal(10, partyId, 4); var token = JwtTokenMock.GenerateToken(principal, new TimeSpan(1, 1, 1)); var (telemetry, request) = AnalyzeTelemetry(token); @@ -91,7 +91,7 @@ public async Task Should_Have_Root_AspNetCore_Trace_User() public async Task Should_Always_Be_A_Root_Trace() { var partyId = Random.Shared.Next(); - var principal = PrincipalUtil.GetUserPrincipal(10, partyId, 4); + var principal = TestAuthentication.GetUserPrincipal(10, partyId, 4); var token = JwtTokenMock.GenerateToken(principal, new TimeSpan(1, 1, 1)); var (telemetry, request) = AnalyzeTelemetry(token, includeTraceContext: true); @@ -118,7 +118,7 @@ public async Task Should_Always_Be_A_Root_Trace() public async Task Should_Always_Be_A_Root_Trace_Unless_Pdf() { var partyId = Random.Shared.Next(); - var principal = PrincipalUtil.GetUserPrincipal(10, partyId, 4); + var principal = TestAuthentication.GetUserPrincipal(10, partyId, 4); var token = JwtTokenMock.GenerateToken(principal, new TimeSpan(1, 1, 1)); var (telemetry, request) = AnalyzeTelemetry(token, includeTraceContext: true, includePdfHeader: true); diff --git a/test/Altinn.App.Api.Tests/Mocks/AppConfigurationCacheMock.cs b/test/Altinn.App.Api.Tests/Mocks/AppConfigurationCacheMock.cs new file mode 100644 index 000000000..801b4e615 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Mocks/AppConfigurationCacheMock.cs @@ -0,0 +1,12 @@ +using Altinn.App.Core.Features.Cache; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Models; + +namespace App.IntegrationTests.Mocks.Services; + +public sealed class AppConfigurationCacheMock(IAppMetadata appMetadata) : IAppConfigurationCache +{ + private readonly IAppMetadata _appMetadata = appMetadata; + + public ApplicationMetadata ApplicationMetadata => _appMetadata.GetApplicationMetadata().GetAwaiter().GetResult(); +} diff --git a/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs b/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs index da0c0b82b..83237ad35 100644 --- a/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs +++ b/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs @@ -4,7 +4,6 @@ using Altinn.Platform.Register.Models; using Altinn.Platform.Storage.Interface.Models; using Authorization.Platform.Authorization.Models; -using Microsoft.AspNetCore.Http.HttpResults; namespace Altinn.App.Api.Tests.Mocks; @@ -12,7 +11,7 @@ public class AuthorizationMock : IAuthorizationClient { public Task?> GetPartyList(int userId) { - throw new NotImplementedException(); + return Task.FromResult?>([]); } public Task ValidateSelectedParty(int userId, int partyId) diff --git a/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.txt b/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.txt index 14d3f68e0..8a4a0e2ec 100644 --- a/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.txt +++ b/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.txt @@ -1,4 +1,4 @@ -{ +{ openapi: 3.0.1, info: { title: Altinn App Api, @@ -3288,7 +3288,8 @@ The body of the request isn't used for anything despite this being a POST operat 200: { description: OK } - } + }, + deprecated: true } }, /{org}/{app}/api/v1/parties/{partyId}: { diff --git a/test/Altinn.App.Api.Tests/OpenApi/swagger.json b/test/Altinn.App.Api.Tests/OpenApi/swagger.json index c52af90c8..ef2a0fb6b 100644 --- a/test/Altinn.App.Api.Tests/OpenApi/swagger.json +++ b/test/Altinn.App.Api.Tests/OpenApi/swagger.json @@ -3284,7 +3284,8 @@ "200": { "description": "OK" } - } + }, + "deprecated": true } }, "/{org}/{app}/api/v1/parties/{partyId}": { diff --git a/test/Altinn.App.Api.Tests/Program.cs b/test/Altinn.App.Api.Tests/Program.cs index 7a6982368..cdcb8b3d8 100644 --- a/test/Altinn.App.Api.Tests/Program.cs +++ b/test/Altinn.App.Api.Tests/Program.cs @@ -8,6 +8,7 @@ using Altinn.App.Common.Tests; using Altinn.App.Core.Configuration; using Altinn.App.Core.Features; +using Altinn.App.Core.Features.Cache; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; using Altinn.App.Core.Internal.Auth; @@ -57,7 +58,10 @@ ); builder.Configuration.GetSection("MetricsSettings:Enabled").Value = "false"; builder.Configuration.GetSection("AppSettings:UseOpenTelemetry").Value = "true"; -builder.Configuration.GetSection("GeneralSettings:DisableLocaltestValidation").Value = "true"; +builder.Services.Configure(settings => settings.DisableLocaltestValidation = true); +builder.Services.Configure(settings => settings.DisableAppConfigurationCache = true); + +// AppConfigurationCache.Disable = true; ConfigureServices(builder.Services, builder.Configuration); ConfigureMockServices(builder.Services, builder.Configuration); @@ -95,6 +99,7 @@ void ConfigureMockServices(IServiceCollection services, ConfigurationManager con services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddSingleton(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs b/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs deleted file mode 100644 index 8c5858c72..000000000 --- a/test/Altinn.App.Api.Tests/Utils/PrincipalUtil.cs +++ /dev/null @@ -1,167 +0,0 @@ -using System.Security.Claims; -using Altinn.App.Api.Tests.Mocks; -using Altinn.App.Core.Features.Maskinporten; -using Altinn.App.Core.Features.Maskinporten.Constants; -using Altinn.App.Core.Features.Maskinporten.Models; -using Altinn.App.Core.Models; -using AltinnCore.Authentication.Constants; - -namespace Altinn.App.Api.Tests.Utils; - -public static class PrincipalUtil -{ - public static string GetToken(int? userId, int? partyId, int authenticationLevel = 2, string? org = null) - { - ClaimsPrincipal principal = GetUserPrincipal(userId, partyId, authenticationLevel, org); - string token = JwtTokenMock.GenerateToken(principal, new TimeSpan(1, 1, 1)); - return token; - } - - public static ClaimsPrincipal GetUserPrincipal( - int? userId, - int? partyId, - int authenticationLevel = 2, - string? org = null - ) - { - List claims = new List(); - string issuer = "www.altinn.no"; - - claims.Add(new Claim(ClaimTypes.NameIdentifier, $"user-{userId}-{partyId}", ClaimValueTypes.String, issuer)); - if (userId > 0) - { - claims.Add(new Claim(AltinnCoreClaimTypes.UserId, userId.Value.ToString(), ClaimValueTypes.String, issuer)); - } - - if (partyId > 0) - { - claims.Add( - new Claim(AltinnCoreClaimTypes.PartyID, partyId.Value.ToString(), ClaimValueTypes.Integer32, issuer) - ); - } - - if (org is not null) - { - claims.Add(new Claim(AltinnCoreClaimTypes.Org, org, ClaimValueTypes.String, issuer)); - } - - claims.Add(new Claim(AltinnCoreClaimTypes.UserName, $"User{userId}", ClaimValueTypes.String, issuer)); - claims.Add(new Claim(AltinnCoreClaimTypes.AuthenticateMethod, "Mock", ClaimValueTypes.String, issuer)); - claims.Add( - new Claim( - AltinnCoreClaimTypes.AuthenticationLevel, - authenticationLevel.ToString(), - ClaimValueTypes.Integer32, - issuer - ) - ); - - ClaimsIdentity identity = new ClaimsIdentity("mock"); - identity.AddClaims(claims); - ClaimsPrincipal principal = new ClaimsPrincipal(identity); - return principal; - } - - public static ClaimsPrincipal GetOrgPrincipal(string org, int authenticationLevel = 3) - { - List claims = new List(); - string issuer = "www.altinn.no"; - claims.Add(new Claim(AltinnCoreClaimTypes.Org, org, ClaimValueTypes.String, issuer)); - claims.Add(new Claim(AltinnCoreClaimTypes.AuthenticateMethod, "Mock", ClaimValueTypes.String, issuer)); - claims.Add( - new Claim( - AltinnCoreClaimTypes.AuthenticationLevel, - authenticationLevel.ToString(), - ClaimValueTypes.Integer32, - issuer - ) - ); - - ClaimsIdentity identity = new ClaimsIdentity("mock"); - identity.AddClaims(claims); - - return new ClaimsPrincipal(identity); - } - - public static string GetOrgToken(string org, int authenticationLevel = 3) - { - ClaimsPrincipal principal = GetOrgPrincipal(org, authenticationLevel); - return JwtTokenMock.GenerateToken(principal, new TimeSpan(1, 1, 1)); - } - - public static string GetSelfIdentifiedUserToken(string username, string partyId, string userId) - { - List claims = new List(); - string issuer = "www.altinn.no"; - claims.Add(new Claim(ClaimTypes.NameIdentifier, userId.ToString(), ClaimValueTypes.String, issuer)); - claims.Add(new Claim(AltinnCoreClaimTypes.UserId, userId.ToString(), ClaimValueTypes.String, issuer)); - claims.Add(new Claim(AltinnCoreClaimTypes.UserName, username, ClaimValueTypes.String, issuer)); - claims.Add(new Claim(AltinnCoreClaimTypes.PartyID, partyId.ToString(), ClaimValueTypes.Integer32, issuer)); - claims.Add(new Claim(AltinnCoreClaimTypes.AuthenticateMethod, "Mock", ClaimValueTypes.String, issuer)); - claims.Add(new Claim(AltinnCoreClaimTypes.AuthenticationLevel, "0", ClaimValueTypes.Integer32, issuer)); - - ClaimsIdentity identity = new ClaimsIdentity("mock"); - identity.AddClaims(claims); - ClaimsPrincipal principal = new ClaimsPrincipal(identity); - string token = JwtTokenMock.GenerateToken(principal, new TimeSpan(1, 1, 1)); - - return token; - } - - public static string GetOrgToken( - string org, - string orgNo, - int authenticationLevel = 4, - TimeSpan? expiry = null, - TimeProvider? timeProvider = null - ) - { - List claims = new List(); - string issuer = "www.altinn.no"; - claims.Add(new Claim(AltinnCoreClaimTypes.Org, org, ClaimValueTypes.String, issuer)); - claims.Add(new Claim(AltinnCoreClaimTypes.OrgNumber, orgNo, ClaimValueTypes.String, issuer)); - claims.Add(new Claim(AltinnCoreClaimTypes.AuthenticateMethod, "Mock", ClaimValueTypes.String, issuer)); - claims.Add( - new Claim( - AltinnCoreClaimTypes.AuthenticationLevel, - authenticationLevel.ToString(), - ClaimValueTypes.Integer32, - issuer - ) - ); - - ClaimsIdentity identity = new ClaimsIdentity("mock"); - identity.AddClaims(claims); - ClaimsPrincipal principal = new ClaimsPrincipal(identity); - expiry ??= new TimeSpan(1, 1, 1); - string token = JwtTokenMock.GenerateToken(principal, expiry.Value, timeProvider); - - return token; - } - - internal static MaskinportenTokenResponse GetMaskinportenToken( - string scope, - TimeSpan? expiry = null, - TimeProvider? timeProvider = null - ) - { - List claims = []; - const string issuer = "https://test.maskinporten.no/"; - claims.Add(new Claim(JwtClaimTypes.Scope, scope, ClaimValueTypes.String, issuer)); - claims.Add(new Claim(JwtClaimTypes.Maskinporten.AuthenticationMethod, "Mock", ClaimValueTypes.String, issuer)); - - ClaimsIdentity identity = new("mock"); - identity.AddClaims(claims); - ClaimsPrincipal principal = new(identity); - expiry ??= TimeSpan.FromMinutes(2); - string accessToken = JwtTokenMock.GenerateToken(principal, expiry.Value, timeProvider); - - return new MaskinportenTokenResponse - { - AccessToken = JwtToken.Parse(accessToken), - ExpiresIn = (int)expiry.Value.TotalSeconds, - Scope = scope, - TokenType = "Bearer", - }; - } -} diff --git a/test/Altinn.App.Api.Tests/Utils/TestAuthentication.cs b/test/Altinn.App.Api.Tests/Utils/TestAuthentication.cs new file mode 100644 index 000000000..ea4310474 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Utils/TestAuthentication.cs @@ -0,0 +1,578 @@ +using System.Security.Claims; +using System.Text.Json; +using Altinn.App.Api.Tests.Mocks; +using Altinn.App.Core.Features.Auth; +using Altinn.App.Core.Features.Maskinporten.Constants; +using Altinn.App.Core.Features.Maskinporten.Models; +using Altinn.App.Core.Models; +using Altinn.Platform.Profile.Models; +using Altinn.Platform.Register.Enums; +using Altinn.Platform.Register.Models; +using Altinn.Platform.Storage.Interface.Models; +using AltinnCore.Authentication.Constants; +using Authorization.Platform.Authorization.Models; +using static Altinn.App.Core.Features.Auth.Authenticated; + +namespace Altinn.App.Api.Tests.Utils; + +public enum AuthenticationTypes +{ + User, + SelfIdentifiedUser, + Org, + SystemUser, + ServiceOwner, +} + +public sealed record TestJwtToken(AuthenticationTypes Type, int PartyId, string Token, Authenticated Auth) +{ + public override string ToString() => $"{Type}={PartyId}"; +} + +public static class TestAuthentication +{ + internal const int DefaultUserId = 1337; + internal const int DefaultUserPartyId = 501337; + internal const string DefaultUsername = "testuser"; + internal const int DefaultUserAuthenticationLevel = 2; + + internal const string DefaultOrgNumber = "405003309"; + internal const string DefaultOrg = "tdd"; + internal const int DefaultOrgPartyId = 5001337; + internal const string DefaultServiceOwnerScope = + "altinn:serviceowner/instances.read altinn:serviceowner/instances.write"; + internal const string DefaultOrgScope = "altinn:instances.read altinn:instances.write"; + + internal const string DefaultSystemUserId = "f58fe166-bc22-4899-beb7-c3e8e3332f43"; + internal const string DefaultSystemId = "1cb8b115-31bf-421f-8029-8bb0cd23c954"; + internal const string DefaultSystemUserOrgNumber = "310702641"; + internal const string DefaultSystemUserSupplierOrgNumber = "991825827"; + + public sealed class AllTokens : TheoryData + { + public AllTokens() + { + Add(new(AuthenticationTypes.User, DefaultUserPartyId, GetUserToken(), GetUserAuthentication())); + Add( + new( + AuthenticationTypes.SelfIdentifiedUser, + DefaultUserPartyId, + GetSelfIdentifiedUserToken(), + GetSelfIdentifiedUserAuthentication() + ) + ); + // Add(new(AuthenticationTypes.Org, DefaultOrgPartyId, GetOrgAuthentication())); + Add( + new( + AuthenticationTypes.ServiceOwner, + DefaultOrgPartyId, + GetServiceOwnerToken(), + GetServiceOwnerAuthentication() + ) + ); + Add( + new( + AuthenticationTypes.SystemUser, + DefaultOrgPartyId, + GetSystemUserToken(), + GetSystemUserAuthentication() + ) + ); + } + } + + public sealed class AllTypes : TheoryData + { + public AllTypes() + { + Add(AuthenticationTypes.User); + Add(AuthenticationTypes.SelfIdentifiedUser); + // Add(AuthenticationTypes.Org); + Add(AuthenticationTypes.ServiceOwner); + Add(AuthenticationTypes.SystemUser); + } + } + + public static None GetNoneAuthentication() + { + return new None(TokenIssuer.None, false, Scopes.None, "None"); + } + + public static string GetUserToken( + int userId = DefaultUserId, + int partyId = DefaultUserPartyId, + int authenticationLevel = DefaultUserAuthenticationLevel + ) + { + ClaimsPrincipal principal = GetUserPrincipal(userId, partyId, authenticationLevel); + string token = JwtTokenMock.GenerateToken(principal, TimeSpan.FromMinutes(10)); + return token; + } + + public static ClaimsPrincipal GetUserPrincipal( + int userId = DefaultUserId, + int partyId = DefaultUserPartyId, + int authLevel = DefaultUserAuthenticationLevel + ) + { + // Returns a principal that looks like a token issed in tt02 Altinn portal using TestID + string iss = "https://platform.tt02.altinn.no/authentication/api/v1/openid/"; + + Claim[] claims = + [ + new(ClaimTypes.NameIdentifier, $"user-{userId}-{partyId}", ClaimValueTypes.String, iss), + new(AltinnCoreClaimTypes.UserId, userId.ToString(), ClaimValueTypes.String, iss), + new(AltinnCoreClaimTypes.PartyID, partyId.ToString(), ClaimValueTypes.Integer32, iss), + new(AltinnCoreClaimTypes.AuthenticateMethod, "BankID", ClaimValueTypes.String, iss), + new(AltinnCoreClaimTypes.AuthenticationLevel, authLevel.ToString(), ClaimValueTypes.Integer32, iss), + new("jti", Guid.NewGuid().ToString(), ClaimValueTypes.String, iss), + new(JwtClaimTypes.Scope, "altinn:portal/enduser", ClaimValueTypes.String, iss), + ]; + + return new ClaimsPrincipal(new ClaimsIdentity(claims, "mock")); + } + + public static User GetUserAuthentication( + int userId = DefaultUserId, + int userPartyId = DefaultUserPartyId, + int authenticationLevel = DefaultUserAuthenticationLevel, + string? email = null, + string? ssn = null, + ProfileSettingPreference? profileSettingPreference = null + ) + { + var party = new Party() + { + PartyId = userPartyId, + PartyTypeName = PartyType.Person, + OrgNumber = null, + SSN = ssn ?? "12345678901", + Name = "Test Testesen", + }; + return new User( + userId, + userPartyId, + authenticationLevel, + "idporten", + userPartyId, + inAltinnPortal: true, + tokenIssuer: TokenIssuer.Altinn, + tokenIsExchanged: false, + new Scopes("altinn:portal/enduser"), + token: "", + getUserProfile: uid => + { + Assert.Equal(userId, uid); + return Task.FromResult( + new UserProfile() + { + UserId = userId, + PartyId = userPartyId, + Party = party, + Email = email ?? "test@testesen.no", + ProfileSettingPreference = profileSettingPreference, + } + ); + }, + lookupParty: partyId => + { + Assert.Equal(userPartyId, partyId); + return Task.FromResult(party); + }, + getPartyList: uid => + { + Assert.Equal(userId, uid); + return Task.FromResult?>([party]); + }, + validateSelectedParty: (uid, pid) => + { + Assert.Equal(userId, uid); + Assert.Equal(userPartyId, pid); + return Task.FromResult(true); + }, + getUserRoles: (uid, pid) => + { + Assert.Equal(userId, uid); + Assert.Equal(userPartyId, pid); + return Task.FromResult>([]); + }, + appMetadata: NewApplicationMetadata() + ); + } + + public static string GetSelfIdentifiedUserToken( + string username = DefaultUsername, + int userId = DefaultUserId, + int partyId = DefaultUserPartyId + ) + { + ClaimsPrincipal principal = GetSelfIdentifiedUserPrincipal(username, userId, partyId); + string token = JwtTokenMock.GenerateToken(principal, TimeSpan.FromMinutes(10)); + return token; + } + + public static ClaimsPrincipal GetSelfIdentifiedUserPrincipal( + string username = DefaultUsername, + int userId = DefaultUserId, + int partyId = DefaultUserPartyId + ) + { + // Returns a principal that looks like a token issed in tt02 Altinn portal using the + // "Logg inn uten fødselsnummer/D-nummber" login method + string iss = "https://platform.tt02.altinn.no/authentication/api/v1/openid/"; + + Claim[] claims = + [ + new(ClaimTypes.NameIdentifier, $"user-{userId}-{partyId}", ClaimValueTypes.String, iss), + new(AltinnCoreClaimTypes.UserId, userId.ToString(), ClaimValueTypes.String, iss), + new(AltinnCoreClaimTypes.UserName, username, ClaimValueTypes.String, iss), + new(AltinnCoreClaimTypes.PartyID, partyId.ToString(), ClaimValueTypes.Integer32, iss), + new(AltinnCoreClaimTypes.AuthenticateMethod, "Mock", ClaimValueTypes.String, iss), + new(AltinnCoreClaimTypes.AuthenticationLevel, "0", ClaimValueTypes.Integer32, iss), + new("jti", Guid.NewGuid().ToString(), ClaimValueTypes.String, iss), + new(JwtClaimTypes.Scope, "altinn:portal/enduser", ClaimValueTypes.String, iss), + ]; + + return new ClaimsPrincipal(new ClaimsIdentity(claims, "mock")); + } + + public static SelfIdentifiedUser GetSelfIdentifiedUserAuthentication( + string username = DefaultUsername, + int userId = DefaultUserId, + int partyId = DefaultUserPartyId, + string? email = null, + ProfileSettingPreference? profileSettingPreference = null + ) + { + var party = new Party() + { + PartyId = partyId, + PartyTypeName = PartyType.SelfIdentified, + OrgNumber = null, + Name = "Test Testesen", + }; + return new SelfIdentifiedUser( + username, + userId, + partyId, + "idporten", + tokenIssuer: TokenIssuer.Altinn, + tokenIsExchanged: false, + new Scopes("altinn:portal/enduser"), + token: "", + getUserProfile: uid => + { + Assert.Equal(userId, uid); + return Task.FromResult( + new UserProfile() + { + UserId = userId, + UserName = username, + PartyId = partyId, + Party = party, + Email = email ?? "test@testesen.no", + ProfileSettingPreference = profileSettingPreference, + } + ); + }, + appMetadata: NewApplicationMetadata() + ); + } + + public static ClaimsPrincipal GetOrgPrincipal(string orgNumber = DefaultOrgNumber, string scope = DefaultOrgScope) + { + // Returns a principal that looks like a token issued by Maskinporten and exchanged to Altinn token in tt02 + // This is not a service owner token, so there should be no service owner scope + string iss = "https://platform.tt02.altinn.no/authentication/api/v1/openid/"; + + var scopes = new Scopes(scope); + if (scopes.HasScopeWithPrefix("altinn:serviceowner/")) + throw new InvalidOperationException("Org token cannot have serviceowner scopes"); + + var consumer = JsonSerializer.Serialize( + new OrgClaim( + "iso6523-actorid-upis", + OrganisationNumber.Parse(orgNumber).Get(OrganisationNumberFormat.International) + ) + ); + Claim[] claims = + [ + new(JwtClaimTypes.Scope, scope, ClaimValueTypes.String, iss), + new("token_type", "Bearer", ClaimValueTypes.String, iss), + new("client_id", Guid.NewGuid().ToString(), ClaimValueTypes.String, iss), + new("consumer", consumer, ClaimValueTypes.String, iss), + new(AltinnCoreClaimTypes.OrgNumber, orgNumber, ClaimValueTypes.String, iss), + new(AltinnCoreClaimTypes.AuthenticateMethod, "maskinporten", ClaimValueTypes.String, iss), + new(AltinnCoreClaimTypes.AuthenticationLevel, "3", ClaimValueTypes.Integer32, iss), + new(JwtClaimTypes.Issuer, iss, ClaimValueTypes.String, iss), + new("jti", Guid.NewGuid().ToString(), ClaimValueTypes.String, iss), + ]; + + return new ClaimsPrincipal(new ClaimsIdentity(claims, "mock")); + } + + public static string GetOrgToken( + string orgNumber = DefaultOrgNumber, + string scope = DefaultOrgScope, + TimeSpan? expiry = null, + TimeProvider? timeProvider = null + ) + { + ClaimsPrincipal principal = GetOrgPrincipal(orgNumber, scope); + return JwtTokenMock.GenerateToken(principal, expiry ?? TimeSpan.FromMinutes(2), timeProvider); + } + + public static Org GetOrgAuthentication( + string orgNumber = DefaultOrgNumber, + int partyId = DefaultOrgPartyId, + int authenticationLevel = DefaultUserAuthenticationLevel + ) + { + var party = new Party() + { + PartyId = partyId, + PartyTypeName = PartyType.Organisation, + OrgNumber = orgNumber, + Name = "Test AS", + }; + return new Org( + orgNumber, + authenticationLevel, + "maskinporten", + tokenIssuer: TokenIssuer.Maskinporten, + tokenIsExchanged: true, + new Scopes("altinn:instances.read altinn:instances.write"), + token: "", + lookupParty: orgNo => + { + Assert.Equal(orgNumber, orgNo); + return Task.FromResult(party); + }, + appMetadata: NewApplicationMetadata() + ); + } + + public static ClaimsPrincipal GetServiceOwnerPrincipal( + string orgNumber = DefaultOrgNumber, + string scope = DefaultServiceOwnerScope, + string org = DefaultOrg + ) + { + // Returns a principal that looks like a token issued by Maskinporten and exchanged to Altinn token in tt02 + // This is a service owner token, so there should be atleast 1 service owner scope + string iss = "https://platform.tt02.altinn.no/authentication/api/v1/openid/"; + + var scopes = new Scopes(scope); + if (!scopes.HasScopeWithPrefix("altinn:serviceowner/")) + throw new InvalidOperationException("Service owner token must have serviceowner scopes"); + + var consumer = JsonSerializer.Serialize( + new OrgClaim( + "iso6523-actorid-upis", + OrganisationNumber.Parse(orgNumber).Get(OrganisationNumberFormat.International) + ) + ); + Claim[] claims = + [ + new(JwtClaimTypes.Scope, scope, ClaimValueTypes.String, iss), + new("token_type", "Bearer", ClaimValueTypes.String, iss), + new("client_id", Guid.NewGuid().ToString(), ClaimValueTypes.String, iss), + new("consumer", consumer, ClaimValueTypes.String, iss), + new(AltinnCoreClaimTypes.Org, org, ClaimValueTypes.String, iss), + new(AltinnCoreClaimTypes.OrgNumber, orgNumber, ClaimValueTypes.String, iss), + new(AltinnCoreClaimTypes.AuthenticateMethod, "maskinporten", ClaimValueTypes.String, iss), + new(AltinnCoreClaimTypes.AuthenticationLevel, "3", ClaimValueTypes.Integer32, iss), + new(JwtClaimTypes.Issuer, iss, ClaimValueTypes.String, iss), + new("jti", Guid.NewGuid().ToString(), ClaimValueTypes.String, iss), + ]; + + return new ClaimsPrincipal(new ClaimsIdentity(claims, "mock")); + } + + public static string GetServiceOwnerToken( + string orgNumber = DefaultOrgNumber, + string scope = DefaultServiceOwnerScope, + string org = DefaultOrg, + TimeSpan? expiry = null, + TimeProvider? timeProvider = null + ) + { + ClaimsPrincipal principal = GetServiceOwnerPrincipal(orgNumber, scope, org); + return JwtTokenMock.GenerateToken(principal, expiry ?? TimeSpan.FromMinutes(2), timeProvider); + } + + public static ServiceOwner GetServiceOwnerAuthentication( + string orgNumber = DefaultOrgNumber, + string org = DefaultOrg, + int partyId = DefaultOrgPartyId, + int authenticationLevel = DefaultUserAuthenticationLevel + ) + { + var party = new Party() + { + PartyId = partyId, + PartyTypeName = PartyType.Organisation, + OrgNumber = orgNumber, + Name = "Test AS", + }; + return new ServiceOwner( + org, + orgNumber, + authenticationLevel, + "maskinporten", + tokenIssuer: TokenIssuer.Maskinporten, + tokenIsExchanged: true, + new Scopes("altinn:serviceowner/instances.read altinn:serviceowner/instances.write"), + token: "", + lookupParty: orgNo => + { + Assert.Equal(orgNumber, orgNo); + return Task.FromResult(party); + } + ); + } + + public static ClaimsPrincipal GetSystemUserPrincipal( + string systemId = DefaultSystemId, + string systemUserId = DefaultSystemUserId, + string systemUserOrgNumber = DefaultSystemUserOrgNumber, + string supplierOrgNumber = DefaultSystemUserSupplierOrgNumber, + string scope = DefaultOrgScope + ) + { + // Returns a principal that looks like a token issued by Maskinporten and exchanged to Altinn token in tt02 + // This is a service owner token, so there should be atleast 1 service owner scope + string iss = "https://platform.tt02.altinn.no/authentication/api/v1/openid/"; + + var scopes = new Scopes(scope); + if (scopes.HasScopeWithPrefix("altinn:serviceowner/")) + throw new InvalidOperationException("System user tokens cannot have serviceowner scopes"); + + AuthorizationDetailsClaim details = new SystemUserAuthorizationDetailsClaim( + [Guid.Parse(systemUserId)], + systemId, + new OrgClaim( + "iso6523-actorid-upis", + OrganisationNumber.Parse(systemUserOrgNumber).Get(OrganisationNumberFormat.International) + ) + ); + var consumer = JsonSerializer.Serialize( + new OrgClaim( + "iso6523-actorid-upis", + OrganisationNumber.Parse(supplierOrgNumber).Get(OrganisationNumberFormat.International) + ) + ); + List claims = + [ + new("authorization_details", JsonSerializer.Serialize(details), ClaimValueTypes.String, iss), + new(JwtClaimTypes.Scope, scope, ClaimValueTypes.String, iss), + new("token_type", "Bearer", ClaimValueTypes.String, iss), + new("client_id", Guid.NewGuid().ToString(), ClaimValueTypes.String, iss), + new("consumer", consumer, ClaimValueTypes.String, iss), + new(AltinnCoreClaimTypes.OrgNumber, supplierOrgNumber, ClaimValueTypes.String, iss), + new(AltinnCoreClaimTypes.AuthenticateMethod, "maskinporten", ClaimValueTypes.String, iss), + new(AltinnCoreClaimTypes.AuthenticationLevel, "3", ClaimValueTypes.Integer32, iss), + new(JwtClaimTypes.Issuer, iss, ClaimValueTypes.String, iss), + new("jti", Guid.NewGuid().ToString(), ClaimValueTypes.String, iss), + ]; + + return new ClaimsPrincipal(new ClaimsIdentity(claims, "mock")); + } + + public static string GetSystemUserToken( + string systemId = DefaultSystemId, + string systemUserId = DefaultSystemUserId, + string systemUserOrgNumber = DefaultSystemUserOrgNumber, + string supplierOrgNumber = DefaultSystemUserSupplierOrgNumber, + string scope = DefaultOrgScope, + TimeSpan? expiry = null, + TimeProvider? timeProvider = null + ) + { + ClaimsPrincipal principal = GetSystemUserPrincipal( + systemId, + systemUserId, + systemUserOrgNumber, + supplierOrgNumber, + scope + ); + return JwtTokenMock.GenerateToken(principal, expiry ?? TimeSpan.FromMinutes(2), timeProvider); + } + + public static SystemUser GetSystemUserAuthentication( + string systemId = DefaultSystemId, + string systemUserId = DefaultSystemUserId, + string systemUserOrgNumber = DefaultSystemUserOrgNumber, + string supplierOrgNumber = DefaultSystemUserSupplierOrgNumber, + int partyId = DefaultOrgPartyId, + bool exchanged = true + ) + { + var party = new Party() + { + PartyId = partyId, + PartyTypeName = PartyType.Organisation, + OrgNumber = systemUserOrgNumber, + Name = "Test AS", + }; + return new SystemUser( + [Guid.Parse(systemUserId)], + OrganisationNumber.Parse(systemUserOrgNumber), + OrganisationNumber.Parse(supplierOrgNumber), + systemId, + 3, + "maskinporten", + tokenIssuer: TokenIssuer.Maskinporten, + tokenIsExchanged: exchanged, + new Scopes("altinn:instances.read altinn:instances.write"), + token: "", + lookupParty: orgNo => + { + Assert.Equal(systemUserOrgNumber, orgNo); + return Task.FromResult(party); + }, + appMetadata: NewApplicationMetadata() + ); + } + + public static ApplicationMetadata NewApplicationMetadata(string org = "ttd") + { + return new ApplicationMetadata($"{org}/app") + { + Org = org, + PartyTypesAllowed = new PartyTypesAllowed() + { + BankruptcyEstate = true, + Organisation = true, + Person = true, + SubUnit = true, + }, + }; + } + + internal static MaskinportenTokenResponse GetMaskinportenToken( + string scope, + TimeSpan? expiry = null, + TimeProvider? timeProvider = null + ) + { + List claims = []; + const string issuer = "https://test.maskinporten.no/"; + claims.Add(new Claim(JwtClaimTypes.Scope, scope, ClaimValueTypes.String, issuer)); + claims.Add(new Claim(JwtClaimTypes.Maskinporten.AuthenticationMethod, "Mock", ClaimValueTypes.String, issuer)); + + ClaimsIdentity identity = new("mock"); + identity.AddClaims(claims); + ClaimsPrincipal principal = new(identity); + expiry ??= TimeSpan.FromMinutes(2); + string accessToken = JwtTokenMock.GenerateToken(principal, expiry.Value, timeProvider); + + return new MaskinportenTokenResponse + { + AccessToken = JwtToken.Parse(accessToken), + ExpiresIn = (int)expiry.Value.TotalSeconds, + Scope = scope, + TokenType = "Bearer", + }; + } +} diff --git a/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs b/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs index 348226a8e..ce8d0c4de 100644 --- a/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Action/SigningUserActionTests.cs @@ -1,16 +1,16 @@ #nullable disable +using System.Globalization; +using Altinn.App.Api.Tests.Utils; using Altinn.App.Core.Features.Action; +using Altinn.App.Core.Features.Auth; using Altinn.App.Core.Helpers; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Process; -using Altinn.App.Core.Internal.Profile; using Altinn.App.Core.Internal.Sign; using Altinn.App.Core.Models; using Altinn.App.Core.Models.Process; using Altinn.App.Core.Models.UserAction; using Altinn.App.Core.Tests.Internal.Process.TestUtils; -using Altinn.Platform.Profile.Models; -using Altinn.Platform.Register.Models; using Altinn.Platform.Storage.Interface.Models; using FluentAssertions; using Microsoft.Extensions.Logging.Abstractions; @@ -26,68 +26,121 @@ public class SigningUserActionTests DataTypes = [new DataType { Id = "model" }], }; - [Fact] - public async Task HandleAction_returns_ok_if_user_is_valid() + [Theory] + [ClassData(typeof(TestAuthentication.AllTokens))] + public async Task HandleAction_returns_ok_if_user_is_valid(TestJwtToken token) { // Arrange - UserProfile userProfile = new UserProfile() - { - UserId = 1337, - Party = new Party() { SSN = "12345678901" }, - }; (var userAction, var signClientMock) = CreateSigningUserAction( - applicationMetadataToReturn: _defaultAppMetadata, - userProfileToReturn: userProfile + applicationMetadataToReturn: _defaultAppMetadata ); var instance = new Instance() { Id = "500000/b194e9f5-02d0-41bc-8461-a0cbac8a6efc", - InstanceOwner = new() { PartyId = "5000" }, + InstanceOwner = new() { PartyId = token.PartyId.ToString(CultureInfo.InvariantCulture) }, Process = new() { CurrentTask = new() { ElementId = "Task2" } }, Data = new() { new() { Id = "a499c3ef-e88a-436b-8650-1c43e5037ada", DataType = "Model" }, }, }; - var userActionContext = new UserActionContext(instance, 1337); + var userActionContext = new UserActionContext(instance, null, authentication: token.Auth); // Act var result = await userAction.HandleAction(userActionContext); // Assert - SignatureContext expected = new SignatureContext( - new InstanceIdentifier(instance), - instance.Process.CurrentTask.ElementId, - "signature", - new Signee() { UserId = "1337", PersonNumber = "12345678901" }, - new DataElementSignature("a499c3ef-e88a-436b-8650-1c43e5037ada") - ); - signClientMock.Verify( - s => s.SignDataElements(It.Is(sc => AssertSigningContextAsExpected(sc, expected))), - Times.Once - ); - result.Should().BeEquivalentTo(UserActionResult.SuccessResult()); - signClientMock.VerifyNoOtherCalls(); + switch (token.Auth) + { + case Authenticated.User user: + { + var details = await user.LoadDetails(); + SignatureContext expected = new SignatureContext( + new InstanceIdentifier(instance), + instance.Process.CurrentTask.ElementId, + "signature", + new Signee() + { + UserId = user.UserId.ToString(CultureInfo.InvariantCulture), + PersonNumber = details.SelectedParty.SSN, + }, + new DataElementSignature("a499c3ef-e88a-436b-8650-1c43e5037ada") + ); + signClientMock.Verify( + s => + s.SignDataElements( + It.Is(sc => AssertSigningContextAsExpected(sc, expected)) + ), + Times.Once + ); + result.Should().BeEquivalentTo(UserActionResult.SuccessResult()); + signClientMock.VerifyNoOtherCalls(); + } + break; + case Authenticated.SelfIdentifiedUser selfIdentifiedUser: + { + SignatureContext expected = new SignatureContext( + new InstanceIdentifier(instance), + instance.Process.CurrentTask.ElementId, + "signature", + new Signee() + { + UserId = selfIdentifiedUser.UserId.ToString(CultureInfo.InvariantCulture), + PersonNumber = null, + }, + new DataElementSignature("a499c3ef-e88a-436b-8650-1c43e5037ada") + ); + signClientMock.Verify( + s => + s.SignDataElements( + It.Is(sc => AssertSigningContextAsExpected(sc, expected)) + ), + Times.Once + ); + result.Should().BeEquivalentTo(UserActionResult.SuccessResult()); + signClientMock.VerifyNoOtherCalls(); + } + break; + case Authenticated.SystemUser systemUser: + { + SignatureContext expected = new SignatureContext( + new InstanceIdentifier(instance), + instance.Process.CurrentTask.ElementId, + "signature", + new Signee() + { + SystemUserId = systemUser.SystemUserId[0], + OrganisationNumber = systemUser.SystemUserOrgNr.Get(OrganisationNumberFormat.Local), + }, + new DataElementSignature("a499c3ef-e88a-436b-8650-1c43e5037ada") + ); + signClientMock.Verify( + s => + s.SignDataElements( + It.Is(sc => AssertSigningContextAsExpected(sc, expected)) + ), + Times.Once + ); + result.Should().BeEquivalentTo(UserActionResult.SuccessResult()); + signClientMock.VerifyNoOtherCalls(); + } + break; + default: + Assert.Equal(ProcessErrorType.Unauthorized, result.ErrorType); + break; + } } [Fact] public async Task HandleAction_returns_ok_if_no_dataElementSignature_and_optional_datatypes() { // Arrange - UserProfile userProfile = new UserProfile() - { - UserId = 1337, - Party = new Party() { SSN = "12345678901" }, - }; var appMetadata = new ApplicationMetadata("org/id") { // Optional because MinCount == 0 DataTypes = [new DataType { Id = "model", MinCount = 0 }], }; - (var userAction, var signClientMock) = CreateSigningUserAction( - applicationMetadataToReturn: appMetadata, - userProfileToReturn: userProfile - ); + (var userAction, var signClientMock) = CreateSigningUserAction(applicationMetadataToReturn: appMetadata); var instance = new Instance() { Id = "500000/b194e9f5-02d0-41bc-8461-a0cbac8a6efc", @@ -98,7 +151,11 @@ public async Task HandleAction_returns_ok_if_no_dataElementSignature_and_optiona new() { Id = "a499c3ef-e88a-436b-8650-1c43e5037ada", DataType = "Model" }, }, }; - var userActionContext = new UserActionContext(instance, 1337); + var userActionContext = new UserActionContext( + instance, + 1337, + authentication: TestAuthentication.GetUserAuthentication(1337) + ); // Act var result = await userAction.HandleAction(userActionContext); @@ -123,12 +180,7 @@ public async Task HandleAction_returns_ok_if_no_dataElementSignature_and_optiona public async Task HandleAction_returns_error_when_UserId_not_set_in_context() { // Arrange - UserProfile userProfile = new UserProfile() - { - UserId = 1337, - Party = new Party() { SSN = "12345678901" }, - }; - (var userAction, var signClientMock) = CreateSigningUserAction(_defaultAppMetadata, userProfile); + (var userAction, var signClientMock) = CreateSigningUserAction(_defaultAppMetadata); var instance = new Instance() { Id = "500000/b194e9f5-02d0-41bc-8461-a0cbac8a6efc", @@ -157,11 +209,6 @@ public async Task HandleAction_returns_error_when_UserId_not_set_in_context() public async Task HandleAction_throws_ApplicationConfigException_when_no_dataElementSignature_and_mandatory_datatypes() { // Arrange - UserProfile userProfile = new UserProfile() - { - UserId = 1337, - Party = new Party() { SSN = "12345678901" }, - }; var appMetadata = new ApplicationMetadata("org/id") { // Mandatory because MinCount != 0 @@ -171,10 +218,7 @@ public async Task HandleAction_throws_ApplicationConfigException_when_no_dataEle new DataType { Id = "not_match_2", MinCount = 1 }, ], }; - (var userAction, var signClientMock) = CreateSigningUserAction( - applicationMetadataToReturn: appMetadata, - userProfileToReturn: userProfile - ); + (var userAction, var signClientMock) = CreateSigningUserAction(applicationMetadataToReturn: appMetadata); var instance = new Instance() { Id = "500000/b194e9f5-02d0-41bc-8461-a0cbac8a6efc", @@ -185,7 +229,11 @@ public async Task HandleAction_throws_ApplicationConfigException_when_no_dataEle new() { Id = "a499c3ef-e88a-436b-8650-1c43e5037ada", DataType = "Model" }, }, }; - var userActionContext = new UserActionContext(instance, 1337); + var userActionContext = new UserActionContext( + instance, + 1337, + authentication: TestAuthentication.GetUserAuthentication(1337) + ); // Act await Assert.ThrowsAsync( @@ -198,14 +246,8 @@ await Assert.ThrowsAsync( public async Task HandleAction_throws_ApplicationConfigException_If_SignatureDataType_is_null() { // Arrange - UserProfile userProfile = new UserProfile() - { - UserId = 1337, - Party = new Party() { SSN = "12345678901" }, - }; (var userAction, var signClientMock) = CreateSigningUserAction( applicationMetadataToReturn: _defaultAppMetadata, - userProfileToReturn: userProfile, testBpmnfilename: "signing-task-process-missing-config.bpmn" ); var instance = new Instance() @@ -218,7 +260,11 @@ public async Task HandleAction_throws_ApplicationConfigException_If_SignatureDat new() { Id = "a499c3ef-e88a-436b-8650-1c43e5037ada", DataType = "Model" }, }, }; - var userActionContext = new UserActionContext(instance, 1337); + var userActionContext = new UserActionContext( + instance, + 1337, + authentication: TestAuthentication.GetUserAuthentication(1337) + ); // Act await Assert.ThrowsAsync( @@ -231,15 +277,8 @@ await Assert.ThrowsAsync( public async Task HandleAction_throws_ApplicationConfigException_If_Empty_DataTypesToSign() { // Arrange - UserProfile userProfile = new UserProfile() - { - UserId = 1337, - Party = new Party() { SSN = "12345678901" }, - }; - var appMetadata = new ApplicationMetadata("org/id") { DataTypes = [] }; (var userAction, var signClientMock) = CreateSigningUserAction( - userProfileToReturn: userProfile, applicationMetadataToReturn: appMetadata, testBpmnfilename: "signing-task-process-empty-datatypes-to-sign.bpmn" ); @@ -253,7 +292,11 @@ public async Task HandleAction_throws_ApplicationConfigException_If_Empty_DataTy new() { Id = "a499c3ef-e88a-436b-8650-1c43e5037ada", DataType = "Model" }, }, }; - var userActionContext = new UserActionContext(instance, 1337); + var userActionContext = new UserActionContext( + instance, + 1337, + authentication: TestAuthentication.GetUserAuthentication(1337) + ); // Act await Assert.ThrowsAsync( @@ -264,7 +307,6 @@ await Assert.ThrowsAsync( private static (SigningUserAction SigningUserAction, Mock SignClientMock) CreateSigningUserAction( ApplicationMetadata applicationMetadataToReturn, - UserProfile userProfileToReturn = null, PlatformHttpException platformHttpExceptionToThrow = null, string testBpmnfilename = "signing-task-process.bpmn" ) @@ -274,11 +316,9 @@ private static (SigningUserAction SigningUserAction, Mock SignClien Path.Combine("Features", "Action", "TestData") ); - var profileClientMock = new Mock(); var signingClientMock = new Mock(); var appMetadataMock = new Mock(); appMetadataMock.Setup(m => m.GetApplicationMetadata()).ReturnsAsync(applicationMetadataToReturn); - profileClientMock.Setup(p => p.GetUserProfile(It.IsAny())).ReturnsAsync(userProfileToReturn); if (platformHttpExceptionToThrow != null) { signingClientMock @@ -290,7 +330,6 @@ private static (SigningUserAction SigningUserAction, Mock SignClien new SigningUserAction( processReader, new NullLogger(), - profileClientMock.Object, signingClientMock.Object, appMetadataMock.Object ), diff --git a/test/Altinn.App.Core.Tests/Features/Action/UniqueSignatureAuthorizerTests.cs b/test/Altinn.App.Core.Tests/Features/Action/UniqueSignatureAuthorizerTests.cs index 9e74659a7..97ebea1cd 100644 --- a/test/Altinn.App.Core.Tests/Features/Action/UniqueSignatureAuthorizerTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Action/UniqueSignatureAuthorizerTests.cs @@ -1,4 +1,5 @@ using System.Security.Claims; +using Altinn.App.Api.Tests.Utils; using Altinn.App.Core.Features.Action; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Data; @@ -39,7 +40,8 @@ public async Task AuthorizeAction_returns_true_if_uniqueFromSignaturesInDataType new ClaimsPrincipal(), new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", - "sign" + "sign", + TestAuthentication.GetUserAuthentication() ) ); _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); @@ -56,7 +58,8 @@ public async Task AuthorizeAction_returns_true_if_uniqueFromSignaturesInDataType new ClaimsPrincipal(), new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", - "sign" + "sign", + TestAuthentication.GetUserAuthentication() ) ); _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); @@ -87,7 +90,8 @@ public async Task AuthorizeAction_returns_true_if_SignatureConfiguration_is_null user, new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", - "sign" + "sign", + TestAuthentication.GetUserAuthentication() ) ); _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); @@ -115,7 +119,8 @@ public async Task AuthorizeAction_returns_true_if_TaskExtension_is_null() user, new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", - "sign" + "sign", + TestAuthentication.GetUserAuthentication(userId: 1000, authenticationLevel: 2) ) ); _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); @@ -152,7 +157,8 @@ public async Task AuthorizeAction_returns_true_if_other_user_has_signed_previous user, new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", - "sign" + "sign", + TestAuthentication.GetUserAuthentication(userId: 1000, authenticationLevel: 2) ) ); _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); @@ -202,7 +208,8 @@ public async Task AuthorizeAction_returns_false_if_same_user_has_signed_previous user, new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", - "sign" + "sign", + TestAuthentication.GetUserAuthentication(userId: 1337, authenticationLevel: 2) ) ); _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); @@ -252,7 +259,8 @@ public async Task AuthorizeAction_returns_true_if_taskID_is_null() user, new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), null, - "sign" + "sign", + TestAuthentication.GetUserAuthentication(userId: 1337, authenticationLevel: 2) ) ); result.Should().BeTrue(); @@ -291,7 +299,8 @@ public async Task AuthorizeAction_returns_true_if_dataelement_not_of_type_SignDo user, new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", - "sign" + "sign", + TestAuthentication.GetUserAuthentication(userId: 1337, authenticationLevel: 2) ) ); _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); @@ -344,7 +353,8 @@ public async Task AuthorizeAction_returns_true_if_signdumcument_is_missing_signe user, new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", - "sign" + "sign", + TestAuthentication.GetUserAuthentication(userId: 1337, authenticationLevel: 2) ) ); _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); @@ -397,7 +407,8 @@ public async Task AuthorizeAction_returns_true_if_signdumcument_is_missing_signe user, new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", - "sign" + "sign", + TestAuthentication.GetUserAuthentication(userId: 1337, authenticationLevel: 2) ) ); _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); @@ -450,7 +461,8 @@ public async Task AuthorizeAction_returns_true_if_signdumcument_signee_userid_is user, new InstanceIdentifier("500001/abba2e90-f86f-4881-b0e8-38334408bcb4"), "Task_2", - "sign" + "sign", + TestAuthentication.GetUserAuthentication(userId: 1337, authenticationLevel: 2) ) ); _processReaderMock.Verify(p => p.GetFlowElement("Task_2")); diff --git a/test/Altinn.App.Core.Tests/Features/Auth/AuthenticatedTests.cs b/test/Altinn.App.Core.Tests/Features/Auth/AuthenticatedTests.cs new file mode 100644 index 000000000..bd7b7ea07 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Auth/AuthenticatedTests.cs @@ -0,0 +1,182 @@ +namespace Altinn.App.Core.Tests.Features.Auth; + +using System.IdentityModel.Tokens.Jwt; +using Altinn.App.Api.Tests.Utils; +using Altinn.App.Core.Features.Auth; + +public class AuthenticatedTests +{ + // These are real tokens used from tt02/test login methods across Altinn, ID-porten and Maskinporten + public static TheoryData Tokens => + new() + { + { + // ID-porten testclient raw (demo-client.test.idporten.no) + "eyJraWQiOiJkaWdpdGFsaXNlcmluZ3NkaXJla3RvcmF0ZXQtLWNlcnQwIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiIxOTkxNDg5NzI4MSIsImFjciI6ImlkcG9ydGVuLWxvYS1zdWJzdGFudGlhbCIsInNjb3BlIjoiYWx0aW5uOmluc3RhbmNlcy5yZWFkIG9wZW5pZCBwcm9maWxlIiwiaXNzIjoiaHR0cHM6Ly90ZXN0LmlkcG9ydGVuLm5vIiwiY2xpZW50X2FtciI6ImNsaWVudF9zZWNyZXRfYmFzaWMiLCJwaWQiOiIxOTkxNDg5NzI4MSIsImV4cCI6MTczNzgxNTc0NiwiaWF0IjoxNzM3ODE1MTQ2LCJqdGkiOiJmVFJYUTNCWkRqTSIsImNsaWVudF9pZCI6ImRlbW9jbGllbnRfaWRwb3J0ZW5fdGVzdCIsImNvbnN1bWVyIjp7ImF1dGhvcml0eSI6ImlzbzY1MjMtYWN0b3JpZC11cGlzIiwiSUQiOiIwMTkyOjk5MTgyNTgyNyJ9fQ.h6clB-UEAkChH5aaqIEmmUqmq3vdCrazixBahfBi7bHMtZ1LtOrHtT0gdOaDIvamMxFDUhOc8fvu7jUpicd5hmDmvHULp_u-RS_qasAlZEVNzzV-ds4RXnhROVh0cCkO2XvZBJKS6RTWv8UmGrK_iaklZwhs5qhMiBs1bRAJ0isLwnbxTKXsUFgaY0RRtgNLzhXW6qwT00roL9GMSCAMb-rBXdXJ5zn41gacGejN5mdQTJe3TQbxyxk52uDU4Biy1TCAh3kRU12Cxx-6T39eJdCKtj-qKCHE44mYp-k8MenTbV1l613ObuVTbiZet4WehKlIXLYFctMB4LMrTQmWD2XS5WyMGXMrincoULZ7VO3Q7BAatbxtIBRT56C_9xhNHg5UOaGATjTp2X6U0XiwzAGE1sZoi-MdMVnUQC3ViJ7bIv3vHL4YU3qKX9iGjpZR0Lnqq8PkN-HTjz1mO0VvYZ3Gz71KKd1_p-DiyJo5lRgp4Ms5FSESz0gWkQ3YUm2Y", + AuthenticationTypes.User, + TokenIssuer.IDporten, + false, + false // we don't support raw ID-porten tokens atm + }, + { + // ID-porten testclient exchanged (demo-client.test.idporten.no) + "eyJhbGciOiJSUzI1NiIsImtpZCI6IkQ4RDg2N0M3RDUyMTM2MEY0RjM1Q0Q1MTU4MEM0OUEwNTE2NUQ0RTEiLCJ4NXQiOiIyTmhueDlVaE5nOVBOYzFSV0F4Sm9GRmwxT0UiLCJ0eXAiOiJKV1QifQ.eyJuYW1laWQiOiIxNDMzOTUzIiwidXJuOmFsdGlubjp1c2VyaWQiOiIxNDMzOTUzIiwidXJuOmFsdGlubjp1c2VybmFtZSI6IiIsInVybjphbHRpbm46cGFydHlpZCI6NTA1OTMxOTMsInVybjphbHRpbm46YXV0aGVudGljYXRlbWV0aG9kIjoiTm90RGVmaW5lZCIsInVybjphbHRpbm46YXV0aGxldmVsIjozLCJhY3IiOiJpZHBvcnRlbi1sb2Etc3Vic3RhbnRpYWwiLCJzY29wZSI6ImFsdGlubjppbnN0YW5jZXMucmVhZCBvcGVuaWQgcHJvZmlsZSIsImNsaWVudF9hbXIiOiJjbGllbnRfc2VjcmV0X2Jhc2ljIiwicGlkIjoiMTk5MTQ4OTcyODEiLCJleHAiOjE3Mzc4MTU3NDYsImlhdCI6MTczNzgxNTE1NywiY2xpZW50X2lkIjoiZGVtb2NsaWVudF9pZHBvcnRlbl90ZXN0IiwiY29uc3VtZXIiOnsiYXV0aG9yaXR5IjoiaXNvNjUyMy1hY3RvcmlkLXVwaXMiLCJJRCI6IjAxOTI6OTkxODI1ODI3In0sImlzcyI6Imh0dHBzOi8vcGxhdGZvcm0udHQwMi5hbHRpbm4ubm8vYXV0aGVudGljYXRpb24vYXBpL3YxL29wZW5pZC8iLCJqdGkiOiI3MTMzYmMwNy1iZWE0LTRkNDAtOWRjNC1jMWFmZGZjMmU2NTEiLCJuYmYiOjE3Mzc4MTUxNTd9.W71Z1FiSYUBJ8G1De-aGYOiUbpD_FCB9gTceLSItZN33y98IzAvNRKJEfXUxVge-GPInjm1DmJ6MVs6ZcVRunigiLa5gNR_W5kkV6kBkaTbZ4SJQsMdT3AaHoBziJEL2ey_ONyDT4ffScx-lRoF_qKQXbkpqLm-Qkj1VKjEBVSTsaqKxJMQrhmKZ4zK6rwhFOPZv5HnGSt56CWh2jrkk8IFzIJZbvO738qHscZ--1UhwHcZ_hpjsdLGaxENiC25kAiqV8gTAyihOAg9ii7jwxLiQYRe_ahqBv5IqT_ZNKKa3q9t7Yh57hQjPWOqtTFTgaBCCYQohYqv-FQtOenbd5g", + AuthenticationTypes.User, + TokenIssuer.IDporten, + true, + true + }, + { + // Altinn portal tt02 test login, token extracted from AltinnStudioRuntime cookie + "eyJhbGciOiJSUzI1NiIsImtpZCI6IkQ4RDg2N0M3RDUyMTM2MEY0RjM1Q0Q1MTU4MEM0OUEwNTE2NUQ0RTEiLCJ4NXQiOiIyTmhueDlVaE5nOVBOYzFSV0F4Sm9GRmwxT0UiLCJ0eXAiOiJKV1QifQ.eyJuYW1laWQiOiIxNDMzOTUzIiwidXJuOmFsdGlubjp1c2VyaWQiOiIxNDMzOTUzIiwidXJuOmFsdGlubjpwYXJ0eWlkIjo1MDU5MzE5MywidXJuOmFsdGlubjphdXRoZW50aWNhdGVtZXRob2QiOiJJZHBvcnRlblRlc3RJZCIsInVybjphbHRpbm46YXV0aGxldmVsIjozLCJqdGkiOiJkMDk2YjBkYy1lYTgyLTRiNGUtYjY0ZS04Y2NjYzA2OWVmYzYiLCJzY29wZSI6ImFsdGlubjpwb3J0YWwvZW5kdXNlciIsIm5iZiI6MTczNzgxNTM0MSwiZXhwIjoxNzM3ODE3MTQxLCJpYXQiOjE3Mzc4MTUzNDF9.dLYVBOSu99_CjcrIqsw6OmdwfKTKthnG2j1kl2pdsVUgn34vFVDF-VAE40IfSpnKUk4VKG-EkGHISi7S2U9rvLrIU_ptTcchg7XCXYSpQhm1DqfsbVwKarTEDBtlSITeqzUI5t2C6DOGSg4QEip92cOAVPw-m34bxBmnXxo6g-3babb-qSsLty_IPBhj86M0Y6zEomE-ysNVNSLJ-0ccAWi4ByzdfdsA5PnBDoeNTzPSZ3PscMOWe3z5d43WRVq30uKVE3XYWt6W0Yf2CbGXVSCTM9J45P2Ps4qiJysQM0zw2guh3s1IIdF7c0-IrppB-3sLNrDzAZ71kyKJXrc5JQ", + AuthenticationTypes.User, + TokenIssuer.Altinn, + false, + true + }, + { + // Altinn portal tt02 self identified, token extracted from AltinnStudioRuntime cookie + "eyJhbGciOiJSUzI1NiIsImtpZCI6IkQ4RDg2N0M3RDUyMTM2MEY0RjM1Q0Q1MTU4MEM0OUEwNTE2NUQ0RTEiLCJ4NXQiOiIyTmhueDlVaE5nOVBOYzFSV0F4Sm9GRmwxT0UiLCJ0eXAiOiJKV1QifQ.eyJuYW1laWQiOiIxNDI4ODEzIiwidXJuOmFsdGlubjp1c2VyaWQiOiIxNDI4ODEzIiwidXJuOmFsdGlubjp1c2VybmFtZSI6Im1hcnRpbm90aGFtYXIiLCJ1cm46YWx0aW5uOnBhcnR5aWQiOjUzMzI4NjYwLCJ1cm46YWx0aW5uOmF1dGhlbnRpY2F0ZW1ldGhvZCI6IlNlbGZJZGVudGlmaWVkIiwidXJuOmFsdGlubjphdXRobGV2ZWwiOjAsImp0aSI6Ijg3OGFkMDZiLWE0Y2EtNDFhZi04YjQzLWY3NTE2Mzk3Yzc3NyIsInNjb3BlIjoiYWx0aW5uOnBvcnRhbC9lbmR1c2VyIiwibmJmIjoxNzM3ODE1NjU0LCJleHAiOjE3Mzc4MTc0NTQsImlhdCI6MTczNzgxNTY1NH0.RLnlBcd_mfgixYkZcePG09iMsAk2XM25FdifashYcLEtebutWEub89GZyFHus7oLbCj_yDiyE1Rilpi3qBxUo9wVPH20ZsmFK5XX1jq7K_wzTsQGYlXPkjROyuXObOW1vuPZL973PEuSsFSc0MX38RfpHlOx7QZHx8gxOES3LwLFqCpdSCmTvsbPXmpHu4SKUb0BcaUFH3flexgbry4hixQbO65v6cQP7Od3A-5tTLCtPsBzCRY3u4EqbCVSJvXAj5x0PEYe-rKgQmY6nQl_dPfCre3uksPlKQeWtdDrmR1YiFfvKfg1DD_Fcf9wjeVGavyRh4qYhFV_7jueueGoqQ", + AuthenticationTypes.SelfIdentifiedUser, + TokenIssuer.Altinn, + false, + true + }, + { + // Maskinporten raw org token (not service owner) + "eyJraWQiOiJiZFhMRVduRGpMSGpwRThPZnl5TUp4UlJLbVo3MUxCOHUxeUREbVBpdVQwIiwiYWxnIjoiUlMyNTYifQ.eyJzY29wZSI6ImFsdGlubjppbnN0YW5jZXMucmVhZCBhbHRpbm46aW5zdGFuY2VzLndyaXRlIiwiaXNzIjoiaHR0cHM6Ly90ZXN0Lm1hc2tpbnBvcnRlbi5uby8iLCJjbGllbnRfYW1yIjoicHJpdmF0ZV9rZXlfand0IiwidG9rZW5fdHlwZSI6IkJlYXJlciIsImV4cCI6MTczNzg0NTkzNSwiaWF0IjoxNzM3ODQyMzM1LCJjbGllbnRfaWQiOiIwNDRmNTA0MC01NGUzLTRhMjctYTIyMS1hODUxNGZkMzBjYTkiLCJqdGkiOiJabUFxZU1fOENrVDMxSWNFXzBlSDhDLVBJM0x2b2ZQVlBielBIc3N2MW1rIiwiY29uc3VtZXIiOnsiYXV0aG9yaXR5IjoiaXNvNjUyMy1hY3RvcmlkLXVwaXMiLCJJRCI6IjAxOTI6OTkxODI1ODI3In19.wI3lgUfpFAWW2dp6SLThjXUhdcBVqym2epd8XG1_AoBbK93lH-lKveMqG14HuNYyy3uivaLwRkHgcIspmlupJAm7r2l-UrflnUX-yQVMa_xYpIDvJIn4gmN-271AnXXLt_lE6WtiYEWaekK37cVs3GzPkwfelx4mPkAg-t-bQeXBIDBLCuvUTy5uzBWbcoMFE7n1fEYu7tTuea2XCbshOcpcLUvLR2D6ZE2brj7Oh91IVizMOtULmf__ZxtwfYW7JfYvBqzd-dASs0Nl0xRwf4kuqEHzZk48VX2x_yizB7RITUAXZ_CffUxvS-NM7dd5q_tlKCRR5Fb7z7yqsLklheeNcyuyXdNqbQEL0iAxotJSNjL2FLjuidOqzh1d8dk45N8019bnYKWAopeb-bj2MNdUEWNEQvubejoKGfO5f_xJ6kS9Oeh4B7JW30xyuYSz8_D9LEVCSFfUp73n4XhQ_DeLWZsq9_G0uWftPUuDbGLaQBwVqujQdXXQCd5MVg7V", + AuthenticationTypes.Org, + TokenIssuer.Maskinporten, + false, + false // we don't support raw Maskinporten tokens atm + }, + { + // Maskinporten exchanged org token (not service owner) + "eyJhbGciOiJSUzI1NiIsImtpZCI6IkQ4RDg2N0M3RDUyMTM2MEY0RjM1Q0Q1MTU4MEM0OUEwNTE2NUQ0RTEiLCJ4NXQiOiIyTmhueDlVaE5nOVBOYzFSV0F4Sm9GRmwxT0UiLCJ0eXAiOiJKV1QifQ.eyJzY29wZSI6ImFsdGlubjppbnN0YW5jZXMucmVhZCBhbHRpbm46aW5zdGFuY2VzLndyaXRlIiwidG9rZW5fdHlwZSI6IkJlYXJlciIsImV4cCI6MTczNzg0NDE1NywiaWF0IjoxNzM3ODQyMzU3LCJjbGllbnRfaWQiOiIwNDRmNTA0MC01NGUzLTRhMjctYTIyMS1hODUxNGZkMzBjYTkiLCJjb25zdW1lciI6eyJhdXRob3JpdHkiOiJpc282NTIzLWFjdG9yaWQtdXBpcyIsIklEIjoiMDE5Mjo5OTE4MjU4MjcifSwidXJuOmFsdGlubjpvcmdOdW1iZXIiOiI5OTE4MjU4MjciLCJ1cm46YWx0aW5uOmF1dGhlbnRpY2F0ZW1ldGhvZCI6Im1hc2tpbnBvcnRlbiIsInVybjphbHRpbm46YXV0aGxldmVsIjozLCJpc3MiOiJodHRwczovL3BsYXRmb3JtLnR0MDIuYWx0aW5uLm5vL2F1dGhlbnRpY2F0aW9uL2FwaS92MS9vcGVuaWQvIiwianRpIjoiMzUwYWNhOTYtYjNkZi00YTdmLTg4YWItOWY4ZDc4ZDYxMjI0IiwibmJmIjoxNzM3ODQyMzU3fQ.g6EFkX6pAKtA64p11CpoTDU6Nzzst4duOzBletMAexEmX-V5C4rXsndkwK3pL9JpZNBbjBZZaEAbBta177PIQo208dZwzYV2meLrip5fQ-hnWF3Ub0VdpxcgggDbcx8WqT1HSix-GQlNcSe2uyZB0KZ_8GRB2aKXjatX4R392A3CZfzBq8Dt3ra5AP0pWVxJAd4NuKHPQRKGbNWkC62J92zLYYtTz4j8DS9yogeP28hrcLzuqyVScDndmOiIjeexXXWdgrwVLDBO2mpVU_i4xqRUbjK9UdySrrkYfv-ZIZQRoZsyPE3ab0SDym-4kVxSIp4xyH3nQuzZJqz24LuBcw", + AuthenticationTypes.Org, + TokenIssuer.Maskinporten, + true, + true + }, + { + // Maskinporten raw service owner token + "eyJraWQiOiJiZFhMRVduRGpMSGpwRThPZnl5TUp4UlJLbVo3MUxCOHUxeUREbVBpdVQwIiwiYWxnIjoiUlMyNTYifQ.eyJzY29wZSI6ImFsdGlubjpzZXJ2aWNlb3duZXIvaW5zdGFuY2VzLndyaXRlIGFsdGlubjpzZXJ2aWNlb3duZXIvaW5zdGFuY2VzLnJlYWQiLCJpc3MiOiJodHRwczovL3Rlc3QubWFza2lucG9ydGVuLm5vLyIsImNsaWVudF9hbXIiOiJwcml2YXRlX2tleV9qd3QiLCJ0b2tlbl90eXBlIjoiQmVhcmVyIiwiZXhwIjoxNzM3ODE3NjU5LCJpYXQiOjE3Mzc4MTc1MzksImNsaWVudF9pZCI6IjA0NGY1MDQwLTU0ZTMtNGEyNy1hMjIxLWE4NTE0ZmQzMGNhOSIsImp0aSI6IlVVVUZ3LU5KZk11R29kOUJsT0pJaGVUalA0YmN3Vzl3aF9kLUJDWm05dE0iLCJjb25zdW1lciI6eyJhdXRob3JpdHkiOiJpc282NTIzLWFjdG9yaWQtdXBpcyIsIklEIjoiMDE5Mjo5OTE4MjU4MjcifX0.t-STvjL2uSFqg_BFtopLenq3hVjZV1_nzkUXh6LxSj8FOGC8OTvtjmIZtOyg1ZHGC3J6M4ZF3QXCHVmZYRl_rXIAU4u8_xG6_HZMuIet9WcoBEadCYAJb-LQEQwvifMyUnwtwMjbtcurh8Wuj6h6lCidsfm2qUC0H3A7W6AxRtXF7CzHkbfSVcK_kkUe2vUn6VLFqB6ZdC_mULonaJtBD3i6hHt7LjbO0U1GuGf-wH80QXHEEr_7qpl2m0NI4CWe_4v638myV1PQVfx5tmwrVLk4BALB1_oP0Ugbp4Wrq8TMZY0Fn4-NpTVStWh3M3pRLXZy-OsycV_GZTBWTBWZRCUtsXcVNI3roFblsqh59IIkgSYDuJXomMfxEuAVoWL20SZJt50Vy3cvJecqN6NJ0hZ4q9kpFkkXs6D3TVUiPhtl_JdrpQEN8mVyUnqo7wj1hrkfHTt6UGpGIGma1CCXjn0pfCjejrMUkpcw2AIwPRfw1syzw3IRvMKZBwcA_ORU", + AuthenticationTypes.ServiceOwner, + TokenIssuer.Maskinporten, + false, + false // we don't support raw Maskinporten tokens atm + }, + { + // Maskinporten exchanged service owner token + "eyJhbGciOiJSUzI1NiIsImtpZCI6IkQ4RDg2N0M3RDUyMTM2MEY0RjM1Q0Q1MTU4MEM0OUEwNTE2NUQ0RTEiLCJ4NXQiOiIyTmhueDlVaE5nOVBOYzFSV0F4Sm9GRmwxT0UiLCJ0eXAiOiJKV1QifQ.eyJzY29wZSI6ImFsdGlubjpzZXJ2aWNlb3duZXIvaW5zdGFuY2VzLndyaXRlIGFsdGlubjpzZXJ2aWNlb3duZXIvaW5zdGFuY2VzLnJlYWQiLCJ0b2tlbl90eXBlIjoiQmVhcmVyIiwiZXhwIjoxNzM3ODE5Mzc2LCJpYXQiOjE3Mzc4MTc1NzYsImNsaWVudF9pZCI6IjA0NGY1MDQwLTU0ZTMtNGEyNy1hMjIxLWE4NTE0ZmQzMGNhOSIsImNvbnN1bWVyIjp7ImF1dGhvcml0eSI6ImlzbzY1MjMtYWN0b3JpZC11cGlzIiwiSUQiOiIwMTkyOjk5MTgyNTgyNyJ9LCJ1cm46YWx0aW5uOm9yZyI6ImRpZ2RpciIsInVybjphbHRpbm46b3JnTnVtYmVyIjoiOTkxODI1ODI3IiwidXJuOmFsdGlubjphdXRoZW50aWNhdGVtZXRob2QiOiJtYXNraW5wb3J0ZW4iLCJ1cm46YWx0aW5uOmF1dGhsZXZlbCI6MywiaXNzIjoiaHR0cHM6Ly9wbGF0Zm9ybS50dDAyLmFsdGlubi5uby9hdXRoZW50aWNhdGlvbi9hcGkvdjEvb3BlbmlkLyIsImp0aSI6IjFmN2RlMDFhLTgyYjMtNDc3Yi04MmQzLTBiY2I5NDkzOGVjNyIsIm5iZiI6MTczNzgxNzU3Nn0.G9uZ_YK2IxUgv8ySP3zy_IG0kOO3qJtEqHPds2f3jh1_YHcQlHEnQXUecUR-xD-Qi8qtI_GEJizA3l-zXc9DLkxgv4HVamzBrOcm9aQWqd3s8_OuI1nF4WjWrcw5FHpaXl1DqbgqPQI8rxOJhmW-H4rE944TKHLwvBlsX-9brYU_CC1WfnymFwODKsGhT1hm5ljQeV6O6j0GkNsRANiQlUnIMJcMVQEBtcuHGBLeNq-u5JSXzs17GLB371IN9Jb8IoYKu7njW3-Pat-QWebDYT9jdMQHOYslr0WByTvlnhL6Z7aQ8KbllbNw3GoYOvCBphVVpmk7aaqcchUiBh30-g", + AuthenticationTypes.ServiceOwner, + TokenIssuer.Maskinporten, + true, + true + }, + { + // Maskinporten for systemuser, coming from the smartcloudaltinn.azurewebsites.net/api/maskinporten test endpoint + "eyJraWQiOiJiZFhMRVduRGpMSGpwRThPZnl5TUp4UlJLbVo3MUxCOHUxeUREbVBpdVQwIiwiYWxnIjoiUlMyNTYifQ.eyJhdXRob3JpemF0aW9uX2RldGFpbHMiOlt7InR5cGUiOiJ1cm46YWx0aW5uOnN5c3RlbXVzZXIiLCJzeXN0ZW11c2VyX29yZyI6eyJhdXRob3JpdHkiOiJpc282NTIzLWFjdG9yaWQtdXBpcyIsIklEIjoiMDE5MjozMTA3MDI2NDEifSwic3lzdGVtdXNlcl9pZCI6WyJmOTUwZGRhZS0wNmMzLTRiNTctYjk0MC03MDI0MTdhOTBkZjAiXSwic3lzdGVtX2lkIjoiOTkxODI1ODI3X3NtYXJ0Y2xvdWQiLCJleHRlcm5hbFJlZiI6ImVkMWMzOGY0LWY1MjgtNGVmMS04NjUyLWIxYWViODU2M2RmZSJ9XSwic2NvcGUiOiJhbHRpbm46c3lzdGVtYnJ1a2VyLmRlbW8iLCJpc3MiOiJodHRwczovL3Rlc3QubWFza2lucG9ydGVuLm5vLyIsImNsaWVudF9hbXIiOiJwcml2YXRlX2tleV9qd3QiLCJ0b2tlbl90eXBlIjoiQmVhcmVyIiwiZXhwIjoxNzM3ODEzMzk4LCJpYXQiOjE3Mzc4MTMyNzgsImNsaWVudF9pZCI6ImEyZWQ3MTJkLTQxNDQtNDQ3MS04MzlmLTgwYWU0YTY4MTQ2YiIsImp0aSI6InZrWFlhNGxIR2lyOFFpcGVtc1dvdnNUMzdIci10ZExOaTYyODlTajVzU1UiLCJjb25zdW1lciI6eyJhdXRob3JpdHkiOiJpc282NTIzLWFjdG9yaWQtdXBpcyIsIklEIjoiMDE5Mjo5OTE4MjU4MjcifX0.b3JozeLFCBn76a703n1ZqbjqHaFaaCadpZb3T0cuqJvLnELEjiOKXvwSI1kuqnpqfEqxEX1UyBIyJh-rvIOZ-B7dXj1usWoh-oWofvENT0AzJcwpPrhuBBo-ZPoCtTVIJraWBJfYbjEFfajfFZdz0D_poz2i5MbJO9mSn5FMAEMdzmjZSA7O9n8Y02uApN31B3pHlKpqIEUK1HhxGl_z8_rzcAcbs2uOKUxVCK_vnEMk3XpcaEDsk0hr-ohkALshWYVUZCxzl7MzJ2wFhyr9JZqSLSV-dS_WY09cwUA-I1dyIlQE1LW1Ye4Ow5xExArBO8UT3sB-q23EqRY07PC1lh-YdFB1rR1pO6pklnQdz8zPzyrUYYcxlQQDVTRVjRgZcF43TMxQIUmAB0LG6QvP8oRp8SV4oAAK9ZB_fCVkwd8gbVwvVIhuQdPEA6aUoV8mPRPz1IW-eVi-XXEQdkOC8OArNQd92y5DiEQE2puZI1nUmXFwypltks-3Xjam6-_5", + AuthenticationTypes.SystemUser, + TokenIssuer.Maskinporten, + false, + true + }, + { + // Exchanged Maskinporten for systemuser, coming from the smartcloudaltinn.azurewebsites.net/api/maskinporten test endpoint + "eyJhbGciOiJSUzI1NiIsImtpZCI6IkQ4RDg2N0M3RDUyMTM2MEY0RjM1Q0Q1MTU4MEM0OUEwNTE2NUQ0RTEiLCJ4NXQiOiIyTmhueDlVaE5nOVBOYzFSV0F4Sm9GRmwxT0UiLCJ0eXAiOiJKV1QifQ.eyJhdXRob3JpemF0aW9uX2RldGFpbHMiOnsidHlwZSI6InVybjphbHRpbm46c3lzdGVtdXNlciIsInN5c3RlbXVzZXJfb3JnIjp7ImF1dGhvcml0eSI6ImlzbzY1MjMtYWN0b3JpZC11cGlzIiwiSUQiOiIwMTkyOjMxMDcwMjY0MSJ9LCJzeXN0ZW11c2VyX2lkIjpbImY5NTBkZGFlLTA2YzMtNGI1Ny1iOTQwLTcwMjQxN2E5MGRmMCJdLCJzeXN0ZW1faWQiOiI5OTE4MjU4Mjdfc21hcnRjbG91ZCIsImV4dGVybmFsUmVmIjoiZWQxYzM4ZjQtZjUyOC00ZWYxLTg2NTItYjFhZWI4NTYzZGZlIn0sInNjb3BlIjoiYWx0aW5uOnN5c3RlbWJydWtlci5kZW1vIiwidG9rZW5fdHlwZSI6IkJlYXJlciIsImV4cCI6MTczNzgxNTEyMCwiaWF0IjoxNzM3ODEzMzIwLCJjbGllbnRfaWQiOiJhMmVkNzEyZC00MTQ0LTQ0NzEtODM5Zi04MGFlNGE2ODE0NmIiLCJjb25zdW1lciI6eyJhdXRob3JpdHkiOiJpc282NTIzLWFjdG9yaWQtdXBpcyIsIklEIjoiMDE5Mjo5OTE4MjU4MjcifSwidXJuOmFsdGlubjpvcmdOdW1iZXIiOiI5OTE4MjU4MjciLCJ1cm46YWx0aW5uOmF1dGhlbnRpY2F0ZW1ldGhvZCI6Im1hc2tpbnBvcnRlbiIsInVybjphbHRpbm46YXV0aGxldmVsIjozLCJpc3MiOiJodHRwczovL3BsYXRmb3JtLnR0MDIuYWx0aW5uLm5vL2F1dGhlbnRpY2F0aW9uL2FwaS92MS9vcGVuaWQvIiwianRpIjoiMTliNGUyOWItNjRiMi00YTUyLWFlNDAtZTc3Njg1MjMxM2YyIiwibmJmIjoxNzM3ODEzMzIwfQ.m-5x5GscJYjD_rCnd0EWQBKSwymPyN4AE9Yti7bjeUuvAVyiPdtske9fahEGONVUvY9Pk2bDjdNOfAeaOPMHB2skEqMNYGqxxQRgBraZawAPFuQSSufTgkt5dEgOAymUam2x9NQ_giFPPtaWHez23rtDGSGAXkMaIBWe93XbO-z_4dFcyDEXvmK4SfkLeJWxizhugVcwwDstYrN7VlcQz3gBGuGKIcC8lnBPIT8u7tmJKt0JQ2L9oZQkGjatUu-4_qlcfzvDr40ojyNtOszoc1UblNRkTI-QxB2yRuvG9a7wMtej_Xgo9Pst8cz4MfBSxOq-2U_wHX95-d5GTLnFfw", + AuthenticationTypes.SystemUser, + TokenIssuer.Maskinporten, + true, + true + }, + }; + + [Theory] + [MemberData(nameof(Tokens))] + public void Can_Parse_Real_Tokens( + string token, + AuthenticationTypes tokenType, + TokenIssuer issuer, + bool isExchanged, + bool succeeds + ) + { + var handler = new JwtSecurityTokenHandler(); + var jwtToken = handler.ReadJwtToken(token); + var exp = jwtToken.Payload.Exp; + Assert.NotNull(exp); + var expiryDateTime = DateTimeOffset.FromUnixTimeSeconds(exp.Value); + Assert.True(expiryDateTime < DateTimeOffset.UtcNow, "Tokens used for testing should be expired"); + + if (!succeeds) + { + Assert.ThrowsAny( + () => + Authenticated.From( + tokenStr: token, + isAuthenticated: true, + appMetadata: TestAuthentication.NewApplicationMetadata("digdir"), + getSelectedParty: () => null, + getUserProfile: _ => null!, + lookupUserParty: _ => null!, + lookupOrgParty: _ => null!, + getPartyList: _ => null!, + validateSelectedParty: (_, _) => null!, + getUserRoles: (_, _) => null! + ) + ); + return; + } + + var auth = Authenticated.From( + tokenStr: token, + isAuthenticated: true, + appMetadata: TestAuthentication.NewApplicationMetadata("digdir"), + getSelectedParty: () => null, + getUserProfile: _ => null!, + lookupUserParty: _ => null!, + lookupOrgParty: _ => null!, + getPartyList: _ => null!, + validateSelectedParty: (_, _) => null!, + getUserRoles: (_, _) => null! + ); + + switch (tokenType) + { + case AuthenticationTypes.User: + Assert.IsType(auth); + Assert.Equal(issuer, auth.TokenIssuer); + Assert.Equal(isExchanged, auth.TokenIsExchanged); + Assert.Equal(token, auth.Token); + break; + case AuthenticationTypes.SelfIdentifiedUser: + Assert.IsType(auth); + Assert.Equal(issuer, auth.TokenIssuer); + Assert.Equal(isExchanged, auth.TokenIsExchanged); + Assert.Equal(token, auth.Token); + break; + case AuthenticationTypes.Org: + Assert.IsType(auth); + Assert.Equal(issuer, auth.TokenIssuer); + Assert.Equal(isExchanged, auth.TokenIsExchanged); + Assert.Equal(token, auth.Token); + break; + case AuthenticationTypes.ServiceOwner: + Assert.IsType(auth); + Assert.Equal(issuer, auth.TokenIssuer); + Assert.Equal(isExchanged, auth.TokenIsExchanged); + Assert.Equal(token, auth.Token); + break; + case AuthenticationTypes.SystemUser: + Assert.IsType(auth); + Assert.Equal(issuer, auth.TokenIssuer); + Assert.Equal(isExchanged, auth.TokenIsExchanged); + Assert.Equal(token, auth.Token); + break; + default: + Assert.Fail("Unknown token type: " + tokenType); + break; + } + } +} diff --git a/test/Altinn.App.Core.Tests/Features/Auth/AuthenticationInfoTests.cs b/test/Altinn.App.Core.Tests/Features/Auth/AuthenticationInfoTests.cs new file mode 100644 index 000000000..5182aed6e --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Auth/AuthenticationInfoTests.cs @@ -0,0 +1,42 @@ +using Altinn.App.Api.Tests.Utils; +using Altinn.App.Core.Features.Auth; +using Altinn.App.Core.Internal.Language; +using Altinn.Platform.Profile.Models; +using FluentAssertions; + +namespace Altinn.App.Core.Tests.Features.Auth; + +public class AuthenticationInfoTests +{ + [Fact] + public async Task Test_User_Get_Language_From_Profile() + { + var user = TestAuthentication.GetUserAuthentication( + profileSettingPreference: new ProfileSettingPreference { Language = LanguageConst.En } + ); + + var lang = await user.GetLanguage(); + + lang.Should().Be(LanguageConst.En); + } + + [Fact] + public async Task Test_User_Get_Default_Language() + { + var user = TestAuthentication.GetUserAuthentication(); + + var lang = await user.GetLanguage(); + + lang.Should().Be(LanguageConst.Nb); + } + + [Fact] + public async Task Test_Unauth_Get_Default_Language() + { + var user = TestAuthentication.GetNoneAuthentication(); + + var lang = await user.GetLanguage(); + + lang.Should().Be(LanguageConst.Nb); + } +} diff --git a/test/Altinn.App.Core.Tests/Features/Auth/ScopesTests.cs b/test/Altinn.App.Core.Tests/Features/Auth/ScopesTests.cs new file mode 100644 index 000000000..bc06a0ee9 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Features/Auth/ScopesTests.cs @@ -0,0 +1,86 @@ +namespace Altinn.App.Core.Tests.Features.Auth; + +using Altinn.App.Core.Features.Auth; + +public class ScopesTests +{ + [Theory] + [InlineData("scope1", "scope1", true)] + [InlineData("SCOPE1", "scope1", false)] + [InlineData(" scope1", "scope1", true)] + [InlineData(" scope1 ", "scope1", true)] + [InlineData("scope1", "scope2", false)] + [InlineData("scope1 scope2", "scope1", true)] + [InlineData("scope1 scope2", "scope1", true)] + [InlineData("scope1 scope2", "scope1", true)] + [InlineData("scope1\tscope2", "scope1", true)] + [InlineData("scope1\nscope2", "scope1", true)] + [InlineData("scope1\r\nscope2", "scope1", true)] + [InlineData("scope1\tscope2", "scope2", true)] + [InlineData("scope1\nscope2", "scope2", true)] + [InlineData("scope1\r\nscope2", "scope2", true)] + [InlineData("scope1 scope2", "scope2", true)] + [InlineData("scope1 scope2", "scope3", false)] + [InlineData("scope1 scope2", "scope3", false)] + [InlineData("scope1 scope2", "scope3", false)] + [InlineData("scope1\tscope2", "scope3", false)] + [InlineData("scope1\nscope2", "scope3", false)] + [InlineData("prefixscope1", "scope1", false)] + [InlineData("scope1suffix", "scope1", false)] + [InlineData("prefixscope1suffix", "scope1", false)] + [InlineData(null, "scope1", false)] + [InlineData("", "scope1", false)] + [InlineData(" ", "scope1", false)] + public void HasScope_Returns(string? inputScopes, string scopeToCheck, bool expected) + { + var scopes = new Scopes(inputScopes); + Assert.Equal(expected, scopes.HasScope(scopeToCheck)); + } + + [Theory] + [InlineData("altinn:instances.write", "altinn:", true)] + [InlineData("altinn:instances.write", "altinn:serviceowner", false)] + [InlineData("altinn:serviceowner/instances.write", "altinn:serviceowner", true)] + [InlineData("altinn:serviceowner/instances.write", "altinn:serviceowner/", true)] + [InlineData("ALTINN:serviceowner/instances.write", "altinn:serviceowner", false)] + [InlineData("test:altinn:serviceowner/instances.write", "altinn:serviceowner", false)] + [InlineData("aaltinn:serviceowner/instances.write", "altinn:serviceowner", false)] + [InlineData(null, "scope1", false)] + [InlineData("", "scope1", false)] + [InlineData(" ", "scope1", false)] + public void HasScopePrefix_Returns(string? inputScopes, string prefixToCheck, bool expected) + { + var scopes = new Scopes(inputScopes); + Assert.Equal(expected, scopes.HasScopeWithPrefix(prefixToCheck)); + } + + public static TheoryData IterationInputs = new() + { + { "scope1 scope2", ["scope1", "scope2"] }, + { " scope1 scope2", ["scope1", "scope2"] }, + { " scope1 scope2 ", ["scope1", "scope2"] }, + { " scope1 scope2 ", ["scope1", "scope2"] }, + { " scope1 scope2 scope3 ", ["scope1", "scope2", "scope3"] }, + { "scope1", ["scope1"] }, + { " scope1", ["scope1"] }, + { "scope1 ", ["scope1"] }, + { " scope1 ", ["scope1"] }, + { "", [] }, + { null!, [] }, + { " ", [] }, + }; + + [Theory] + [MemberData(nameof(IterationInputs))] + public void Iteration(string? inputScopes, string[] expectedScopes) + { + var scopes = new Scopes(inputScopes); + int i = 0; + foreach (var scope in scopes) + { + Assert.True(expectedScopes[i].AsSpan().SequenceEqual(scope)); + i++; + } + Assert.Equal(i, expectedScopes.Length); + } +} diff --git a/test/Altinn.App.Core.Tests/Features/Correspondence/CorrespondenceClientTests.cs b/test/Altinn.App.Core.Tests/Features/Correspondence/CorrespondenceClientTests.cs index 050a7f74d..ee69aee63 100644 --- a/test/Altinn.App.Core.Tests/Features/Correspondence/CorrespondenceClientTests.cs +++ b/test/Altinn.App.Core.Tests/Features/Correspondence/CorrespondenceClientTests.cs @@ -334,7 +334,8 @@ IEnumerable expectedScopes var mockHttpClientFactory = fixture.HttpClientFactoryMock; var mockMaskinportenClient = fixture.MaskinportenClientMock; var mockHttpClient = new Mock(); - var altinnTokenResponse = PrincipalUtil.GetOrgToken("ttd"); + var correspondencePayload = PayloadFactory.Send(authorisation: CorrespondenceAuthorisation.Maskinporten); + var altinnTokenResponse = TestAuthentication.GetServiceOwnerToken(org: "ttd"); var altinnTokenWrapperResponse = JwtToken.Parse(altinnTokenResponse); Func> action = async () => diff --git a/test/Altinn.App.Core.Tests/Features/Maskinporten/Delegates/MaskinportenDelegatingHandlerTest.cs b/test/Altinn.App.Core.Tests/Features/Maskinporten/Delegates/MaskinportenDelegatingHandlerTest.cs index be0b706cf..aabeb4a86 100644 --- a/test/Altinn.App.Core.Tests/Features/Maskinporten/Delegates/MaskinportenDelegatingHandlerTest.cs +++ b/test/Altinn.App.Core.Tests/Features/Maskinporten/Delegates/MaskinportenDelegatingHandlerTest.cs @@ -12,7 +12,7 @@ public async Task SendAsync_AddsAuthorizationHeader() { // Arrange var scopes = new[] { "scope1", "scope2" }; - var accessToken = PrincipalUtil.GetMaskinportenToken(scope: "-").AccessToken; + var accessToken = TestAuthentication.GetMaskinportenToken(scope: "-").AccessToken; var (client, handler) = TestHelpers.MockMaskinportenDelegatingHandlerFactory( TokenAuthorities.Maskinporten, scopes, diff --git a/test/Altinn.App.Core.Tests/Features/Maskinporten/MaskinportenClientTest.cs b/test/Altinn.App.Core.Tests/Features/Maskinporten/MaskinportenClientTest.cs index 59518d080..c37d69511 100644 --- a/test/Altinn.App.Core.Tests/Features/Maskinporten/MaskinportenClientTest.cs +++ b/test/Altinn.App.Core.Tests/Features/Maskinporten/MaskinportenClientTest.cs @@ -179,7 +179,7 @@ public async Task GetAccessToken_ReturnsAToken(string variant) await using var fixture = Fixture.Create(); string[] scopes = ["scope1", "scope2"]; string formattedScopes = MaskinportenClient.FormattedScopes(scopes); - var maskinportenTokenResponse = PrincipalUtil.GetMaskinportenToken( + var maskinportenTokenResponse = TestAuthentication.GetMaskinportenToken( scope: formattedScopes, expiry: TimeSpan.FromMinutes(2), fixture.FakeTime @@ -207,13 +207,18 @@ public async Task GetAltinnExchangedToken_ReturnsAToken(string variant) // Arrange await using var fixture = Fixture.Create(); string[] scopes = ["scope1", "scope2"]; - var maskinportenTokenResponse = PrincipalUtil.GetMaskinportenToken( + var maskinportenTokenResponse = TestAuthentication.GetMaskinportenToken( scope: MaskinportenClient.FormattedScopes(scopes), expiry: TimeSpan.FromMinutes(2), fixture.FakeTime ); var expiresIn = TimeSpan.FromMinutes(30); - var altinnAccessToken = PrincipalUtil.GetOrgToken("ttd", "160694123", 3, expiresIn, fixture.FakeTime); + var altinnAccessToken = TestAuthentication.GetServiceOwnerToken( + "405003309", + org: "ttd", + expiry: expiresIn, + timeProvider: fixture.FakeTime + ); fixture .HttpClientFactoryMock.Setup(x => x.CreateClient(It.IsAny())) .Returns(() => @@ -239,7 +244,7 @@ public async Task GetAccessToken_ThrowsExceptionWhenTokenIsExpired(string varian { // Arrange await using var fixture = Fixture.Create(); - var maskinportenTokenResponse = PrincipalUtil.GetMaskinportenToken( + var maskinportenTokenResponse = TestAuthentication.GetMaskinportenToken( scope: "-", expiry: MaskinportenClient.TokenExpirationMargin - TimeSpan.FromSeconds(1), fixture.FakeTime @@ -276,7 +281,7 @@ public async Task GetAccessToken_UsesCachedTokenIfAvailable(string variant) await using var fixture = Fixture.Create(); string[] scopes = ["scope1", "scope2"]; var maskinportenTokenResponse = () => - PrincipalUtil.GetMaskinportenToken( + TestAuthentication.GetMaskinportenToken( scope: MaskinportenClient.FormattedScopes(scopes), expiry: TimeSpan.FromMinutes(2), fixture.FakeTime @@ -306,7 +311,7 @@ public async Task GetAccessToken_GeneratesNewTokenIfRequired(string variant) await using var fixture = Fixture.Create(); string[] scopes = ["scope1", "scope2"]; var maskinportenTokenResponse = () => - PrincipalUtil.GetMaskinportenToken( + TestAuthentication.GetMaskinportenToken( scope: MaskinportenClient.FormattedScopes(scopes), expiry: MaskinportenClient.TokenExpirationMargin + TimeSpan.FromSeconds(1), fixture.FakeTime @@ -378,7 +383,7 @@ await act.Should() public async Task ParseServerResponse_ThrowsOn_DisposedObject() { // Arrange - var maskinportenTokenResponse = PrincipalUtil.GetMaskinportenToken( + var maskinportenTokenResponse = TestAuthentication.GetMaskinportenToken( scope: "a b", expiry: MaskinportenClient.TokenExpirationMargin + TimeSpan.FromSeconds(1) ); diff --git a/test/Altinn.App.Core.Tests/Features/Maskinporten/TestHelpers.cs b/test/Altinn.App.Core.Tests/Features/Maskinporten/TestHelpers.cs index c8f06ff66..b614a69e4 100644 --- a/test/Altinn.App.Core.Tests/Features/Maskinporten/TestHelpers.cs +++ b/test/Altinn.App.Core.Tests/Features/Maskinporten/TestHelpers.cs @@ -42,7 +42,7 @@ public static Mock MockHttpMessageHandlerFactory( } ); - altinnAccessToken ??= PrincipalUtil.GetOrgToken("ttd", "160694123", 3); + altinnAccessToken ??= TestAuthentication.GetServiceOwnerToken("405003309", org: "ttd"); protectedMock .Setup>( "SendAsync", diff --git a/test/Altinn.App.Core.Tests/Implementation/PrefillSITest.cs b/test/Altinn.App.Core.Tests/Implementation/PrefillSITest.cs index 76b97ebc0..26ef2f967 100644 --- a/test/Altinn.App.Core.Tests/Implementation/PrefillSITest.cs +++ b/test/Altinn.App.Core.Tests/Implementation/PrefillSITest.cs @@ -1,3 +1,4 @@ +using Altinn.App.Core.Features.Auth; using Altinn.App.Core.Implementation; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Profile; @@ -43,17 +44,15 @@ public void PrefillDataModel_AssignsValuesCorrectly() var dataModel = new PrefillTestDataModel(); var loggerMock = new Mock>(); - var profileClientMock = new Mock(); var appResourcesMock = new Mock(); var altinnPartyClientMock = new Mock(); - var httpContextAccessorMock = new Mock(); + var authenticationContextMock = new Mock(); var prefillToTest = new PrefillSI( loggerMock.Object, - profileClientMock.Object, appResourcesMock.Object, altinnPartyClientMock.Object, - httpContextAccessorMock.Object + authenticationContextMock.Object ); prefillToTest.PrefillDataModel(dataModel, externalPrefill, continueOnError: false); diff --git a/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs b/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs index 785788786..9b7472362 100644 --- a/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Auth/AuthorizationServiceTests.cs @@ -1,7 +1,9 @@ using System.Security.Claims; +using Altinn.App.Api.Tests.Utils; using Altinn.App.Common.Tests; using Altinn.App.Core.Features; using Altinn.App.Core.Features.Action; +using Altinn.App.Core.Features.Auth; using Altinn.App.Core.Internal.Auth; using Altinn.App.Core.Internal.Process.Authorization; using Altinn.App.Core.Internal.Process.Elements; @@ -16,6 +18,14 @@ namespace Altinn.App.Core.Tests.Internal.Auth; public class AuthorizationServiceTests { + private IAuthenticationContext MockAuthContext(int userId = 1337, int partyId = 1338) + { + var mock = new Mock(); + mock.SetupGet(a => a.Current) + .Returns(TestAuthentication.GetUserAuthentication(userId: userId, userPartyId: partyId)); + return mock.Object; + } + [Fact] public async Task GetPartyList_returns_party_list_from_AuthorizationClient() { @@ -30,6 +40,7 @@ public async Task GetPartyList_returns_party_list_from_AuthorizationClient() AuthorizationService authorizationService = new AuthorizationService( authorizationClientMock.Object, new List(), + MockAuthContext(userId: userId), telemetrySink.Object ); @@ -55,7 +66,8 @@ public async Task ValidateSelectedParty_returns_validation_from_AuthorizationCli authorizationClientMock.Setup(a => a.ValidateSelectedParty(userId, partyId)).ReturnsAsync(true); AuthorizationService authorizationService = new AuthorizationService( authorizationClientMock.Object, - new List() + new List(), + MockAuthContext(userId: userId, partyId: partyId) ); // Act @@ -86,7 +98,8 @@ public async Task AuthorizeAction_returns_true_when_AutorizationClient_true_and_ .ReturnsAsync(true); AuthorizationService authorizationService = new AuthorizationService( authorizationClientMock.Object, - new List() + new List(), + MockAuthContext(partyId: instanceIdentifier.InstanceOwnerPartyId) ); // Act @@ -126,7 +139,8 @@ public async Task AuthorizeAction_returns_false_when_AutorizationClient_false_an .ReturnsAsync(false); AuthorizationService authorizationService = new AuthorizationService( authorizationClientMock.Object, - new List() + new List(), + MockAuthContext(partyId: instanceIdentifier.InstanceOwnerPartyId) ); // Act @@ -177,7 +191,8 @@ public async Task AuthorizeAction_returns_false_when_AutorizationClient_true_and AuthorizationService authorizationService = new AuthorizationService( authorizationClientMock.Object, - new List() { userActionAuthorizerProvider } + new List() { userActionAuthorizerProvider }, + MockAuthContext(partyId: instanceIdentifier.InstanceOwnerPartyId) ); // Act @@ -229,7 +244,8 @@ public async Task AuthorizeAction_does_not_call_UserActionAuthorizer_if_Authoriz AuthorizationService authorizationService = new AuthorizationService( authorizationClientMock.Object, - new List() { userActionAuthorizerProvider } + new List() { userActionAuthorizerProvider }, + MockAuthContext(partyId: instanceIdentifier.InstanceOwnerPartyId) ); // Act @@ -294,7 +310,8 @@ public async Task AuthorizeAction_calls_all_providers_and_return_true_if_all_tru { userActionAuthorizerOneProvider, userActionAuthorizerTwoProvider, - } + }, + MockAuthContext(partyId: instanceIdentifier.InstanceOwnerPartyId) ); // Act @@ -372,7 +389,8 @@ public async Task AuthorizeAction_does_not_call_providers_with_non_matching_task userActionAuthorizerOneProvider, userActionAuthorizerTwoProvider, userActionAuthorizerThreeProvider, - } + }, + MockAuthContext(partyId: instanceIdentifier.InstanceOwnerPartyId) ); // Act @@ -460,7 +478,8 @@ public async Task AuthorizeAction_calls_providers_with_task_null_and_or_action_n userActionAuthorizerOneProvider, userActionAuthorizerTwoProvider, userActionAuthorizerThreeProvider, - } + }, + MockAuthContext(partyId: instanceIdentifier.InstanceOwnerPartyId) ); // Actπ @@ -517,7 +536,8 @@ private async Task AuthorizeActions_returns_list_of_UserActions_with_auth_decisi AuthorizationService authorizationService = new AuthorizationService( authorizationClientMock.Object, - new List() + new List(), + MockAuthContext() ); // Act diff --git a/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs index fbd689d81..6490ee45a 100644 --- a/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs +++ b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs @@ -1,7 +1,8 @@ using System.Net; -using System.Security.Claims; +using Altinn.App.Api.Tests.Utils; using Altinn.App.Common.Tests; using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features.Auth; using Altinn.App.Core.Infrastructure.Clients.Pdf; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.Auth; @@ -11,10 +12,7 @@ using Altinn.App.Core.Internal.Profile; using Altinn.App.PlatformServices.Tests.Helpers; using Altinn.App.PlatformServices.Tests.Mocks; -using Altinn.Platform.Profile.Models; using Altinn.Platform.Storage.Interface.Models; -using AltinnCore.Authentication.Constants; -using Castle.Core.Logging; using FluentAssertions; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -32,7 +30,6 @@ public class PdfServiceTests private readonly Mock _dataClient = new(); private readonly Mock _httpContextAccessor = new(); private readonly Mock _pdfGeneratorClient = new(); - private readonly Mock _profile = new(); private readonly IOptions _pdfGeneratorSettingsOptions = Options.Create( new() { } ); @@ -45,6 +42,8 @@ public class PdfServiceTests private readonly Mock _userTokenProvider; + private readonly Mock _authenticationContext = new(); + private readonly Mock> _logger = new(); public PdfServiceTests() @@ -67,6 +66,8 @@ public PdfServiceTests() _userTokenProvider = new Mock(); _userTokenProvider.Setup(s => s.GetUserToken()).Returns("usertoken"); + + _authenticationContext.Setup(s => s.Current).Returns(TestAuthentication.GetUserAuthentication()); } [Fact] @@ -257,95 +258,6 @@ public async Task GenerateAndStorePdf_with_generatedFrom() ); } - [Fact] - public async Task GetLanguage_ShouldReturnLanguageFromUserPreference() - { - // Arrange - var profileMock = new Mock(); - profileMock - .Setup(s => s.GetUserProfile(It.IsAny())) - .Returns( - Task.FromResult( - new UserProfile - { - UserId = 123, - ProfileSettingPreference = new ProfileSettingPreference { Language = LanguageConst.En }, - } - ) - ); - var user = new ClaimsPrincipal(new ClaimsIdentity([new(AltinnCoreClaimTypes.UserId, "123")], "TestAuthType")); - - var target = SetupPdfService(profile: profileMock); - - // Act - var language = await target.GetLanguage(user); - - // Assert - language.Should().Be(LanguageConst.En); - } - - [Fact] - public async Task GetLanguage_NoLanguageInUserPreference_ShouldReturnBokmål() - { - // Arrange - var profileMock = new Mock(); - profileMock - .Setup(s => s.GetUserProfile(It.IsAny())) - .Returns( - Task.FromResult( - new UserProfile - { - UserId = 123, - ProfileSettingPreference = new ProfileSettingPreference - { - /* No language preference set*/ - }, - } - ) - ); - var user = new ClaimsPrincipal(new ClaimsIdentity([new(AltinnCoreClaimTypes.UserId, "123")], "TestAuthType")); - - var target = SetupPdfService(profile: profileMock); - - // Act - var language = await target.GetLanguage(user); - - // Assert - language.Should().Be(LanguageConst.Nb); - } - - [Fact] - public async Task GetLanguage_UserIsNull_ShouldReturnBokmål() - { - // Arrange - ClaimsPrincipal? user = null; - var target = SetupPdfService(); - - // Act - var language = await target.GetLanguage(user); - - // Assert - language.Should().Be(LanguageConst.Nb); - } - - [Fact] - public async Task GetLanguage_UserProfileIsNull_ShouldThrow() - { - // Arrange - var user = new ClaimsPrincipal(new ClaimsIdentity([new(AltinnCoreClaimTypes.UserId, "123")], "TestAuthType")); - - var profileMock = new Mock(); - profileMock.Setup(s => s.GetUserProfile(It.IsAny())).Returns(Task.FromResult(null)); - - var target = SetupPdfService(profile: profileMock); - - // Act - var func = async () => await target.GetLanguage(user); - - // Assert - await func.Should().ThrowAsync().WithMessage("Could not get user profile while getting language"); - } - [Fact] public void GetOverridenLanguage_ShouldReturnLanguageFromQuery() { @@ -393,6 +305,7 @@ private PdfService SetupPdfService( Mock? pdfGeneratorClient = null, IOptions? pdfGeneratorSettingsOptions = null, IOptions? generalSettingsOptions = null, + Mock? authenticationContext = null, TelemetrySink? telemetrySink = null ) { @@ -400,11 +313,11 @@ private PdfService SetupPdfService( appResources?.Object ?? _appResources.Object, dataClient?.Object ?? _dataClient.Object, httpContentAccessor?.Object ?? _httpContextAccessor.Object, - profile?.Object ?? _profile.Object, pdfGeneratorClient?.Object ?? _pdfGeneratorClient.Object, pdfGeneratorSettingsOptions ?? _pdfGeneratorSettingsOptions, generalSettingsOptions ?? _generalSettingsOptions, _logger.Object, + authenticationContext?.Object ?? _authenticationContext.Object, telemetrySink?.Object ); } diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.StartProcess_starts_process_and_moves_to_first_task_SelfIdentifiedUser.verified.txt b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.StartProcess_starts_process_and_moves_to_first_task_SelfIdentifiedUser.verified.txt new file mode 100644 index 000000000..0aaedd362 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.StartProcess_starts_process_and_moves_to_first_task_SelfIdentifiedUser.verified.txt @@ -0,0 +1,61 @@ +{ + Activities: [ + { + ActivityName: Process.HandleEvents, + Tags: [ + { + instance.guid: Guid_1 + } + ], + IdFormat: W3C + }, + { + ActivityName: Process.Start, + Tags: [ + { + instance.guid: Guid_1 + } + ], + IdFormat: W3C, + Status: Ok, + Events: [ + { + Name: change, + Timestamp: DateTimeOffset_1, + Tags: [ + { + events: [ + Type=process_StartEvent DataId=, + Type=process_StartTask DataId= + ] + }, + { + to.started: DateTime_1 + }, + { + to.task.name: Utfylling + } + ] + } + ] + }, + { + ActivityName: Process.StoreEvents, + Tags: [ + { + instance.guid: Guid_1 + } + ], + IdFormat: W3C + } + ], + Metrics: [ + { + altinn_app_lib_processes_started: [ + { + Value: 1 + } + ] + } + ] +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.StartProcess_starts_process_and_moves_to_first_task_ServiceOwner.verified.txt b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.StartProcess_starts_process_and_moves_to_first_task_ServiceOwner.verified.txt new file mode 100644 index 000000000..0aaedd362 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.StartProcess_starts_process_and_moves_to_first_task_ServiceOwner.verified.txt @@ -0,0 +1,61 @@ +{ + Activities: [ + { + ActivityName: Process.HandleEvents, + Tags: [ + { + instance.guid: Guid_1 + } + ], + IdFormat: W3C + }, + { + ActivityName: Process.Start, + Tags: [ + { + instance.guid: Guid_1 + } + ], + IdFormat: W3C, + Status: Ok, + Events: [ + { + Name: change, + Timestamp: DateTimeOffset_1, + Tags: [ + { + events: [ + Type=process_StartEvent DataId=, + Type=process_StartTask DataId= + ] + }, + { + to.started: DateTime_1 + }, + { + to.task.name: Utfylling + } + ] + } + ] + }, + { + ActivityName: Process.StoreEvents, + Tags: [ + { + instance.guid: Guid_1 + } + ], + IdFormat: W3C + } + ], + Metrics: [ + { + altinn_app_lib_processes_started: [ + { + Value: 1 + } + ] + } + ] +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.StartProcess_starts_process_and_moves_to_first_task_SystemUser.verified.txt b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.StartProcess_starts_process_and_moves_to_first_task_SystemUser.verified.txt new file mode 100644 index 000000000..0aaedd362 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.StartProcess_starts_process_and_moves_to_first_task_SystemUser.verified.txt @@ -0,0 +1,61 @@ +{ + Activities: [ + { + ActivityName: Process.HandleEvents, + Tags: [ + { + instance.guid: Guid_1 + } + ], + IdFormat: W3C + }, + { + ActivityName: Process.Start, + Tags: [ + { + instance.guid: Guid_1 + } + ], + IdFormat: W3C, + Status: Ok, + Events: [ + { + Name: change, + Timestamp: DateTimeOffset_1, + Tags: [ + { + events: [ + Type=process_StartEvent DataId=, + Type=process_StartTask DataId= + ] + }, + { + to.started: DateTime_1 + }, + { + to.task.name: Utfylling + } + ] + } + ] + }, + { + ActivityName: Process.StoreEvents, + Tags: [ + { + instance.guid: Guid_1 + } + ], + IdFormat: W3C + } + ], + Metrics: [ + { + altinn_app_lib_processes_started: [ + { + Value: 1 + } + ] + } + ] +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.StartProcess_starts_process_and_moves_to_first_task_User.verified.txt b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.StartProcess_starts_process_and_moves_to_first_task_User.verified.txt new file mode 100644 index 000000000..0aaedd362 --- /dev/null +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.StartProcess_starts_process_and_moves_to_first_task_User.verified.txt @@ -0,0 +1,61 @@ +{ + Activities: [ + { + ActivityName: Process.HandleEvents, + Tags: [ + { + instance.guid: Guid_1 + } + ], + IdFormat: W3C + }, + { + ActivityName: Process.Start, + Tags: [ + { + instance.guid: Guid_1 + } + ], + IdFormat: W3C, + Status: Ok, + Events: [ + { + Name: change, + Timestamp: DateTimeOffset_1, + Tags: [ + { + events: [ + Type=process_StartEvent DataId=, + Type=process_StartTask DataId= + ] + }, + { + to.started: DateTime_1 + }, + { + to.task.name: Utfylling + } + ] + } + ] + }, + { + ActivityName: Process.StoreEvents, + Tags: [ + { + instance.guid: Guid_1 + } + ], + IdFormat: W3C + } + ], + Metrics: [ + { + altinn_app_lib_processes_started: [ + { + Value: 1 + } + ] + } + ] +} \ No newline at end of file diff --git a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs index dae2d3015..d56bc07e0 100644 --- a/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs +++ b/test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs @@ -1,9 +1,11 @@ +using System.Globalization; using System.Security.Claims; -using System.Security.Cryptography.X509Certificates; +using Altinn.App.Api.Tests.Utils; using Altinn.App.Common.Tests; using Altinn.App.Core.Extensions; using Altinn.App.Core.Features; using Altinn.App.Core.Features.Action; +using Altinn.App.Core.Features.Auth; using Altinn.App.Core.Helpers.Serialization; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; @@ -12,12 +14,9 @@ using Altinn.App.Core.Internal.Process; using Altinn.App.Core.Internal.Process.Elements; using Altinn.App.Core.Internal.Process.ProcessTasks; -using Altinn.App.Core.Internal.Profile; using Altinn.App.Core.Models; using Altinn.App.Core.Models.Process; using Altinn.App.Core.Models.UserAction; -using Altinn.Platform.Profile.Models; -using Altinn.Platform.Register.Models; using Altinn.Platform.Storage.Interface.Enums; using Altinn.Platform.Storage.Interface.Models; using AltinnCore.Authentication.Constants; @@ -35,7 +34,7 @@ public sealed class ProcessEngineTest : IDisposable private static readonly Guid _instanceGuid = new("00000000-DEAD-BABE-0000-001230000000"); private static readonly string _instanceId = $"{_instanceOwnerPartyId}/{_instanceGuid}"; private readonly Mock _processReaderMock = new(); - private readonly Mock _profileMock = new(MockBehavior.Strict); + private readonly Mock _authenticationContextMock = new(); private readonly Mock _processNavigatorMock = new(MockBehavior.Strict); private readonly Mock _processEventHandlingDelegatorMock = new(); private readonly Mock _processEventDispatcherMock = new(); @@ -115,29 +114,29 @@ public async Task StartProcess_starts_process_and_moves_to_first_task_without_ev result.Success.Should().BeTrue(); } - [Fact] - public async Task StartProcess_starts_process_and_moves_to_first_task() + [Theory] + [ClassData(typeof(TestAuthentication.AllTokens))] + public async Task StartProcess_starts_process_and_moves_to_first_task(TestJwtToken token) { TelemetrySink telemetrySink = new(); - ProcessEngine processEngine = GetProcessEngine(telemetrySink: telemetrySink); + ProcessEngine processEngine = GetProcessEngine(telemetrySink: telemetrySink, token: token); + var instanceOwnerPartyId = token.Auth switch + { + Authenticated.User auth when await auth.LoadDetails() is { } details => details.SelectedParty.PartyId, + Authenticated.SelfIdentifiedUser auth => auth.PartyId, + Authenticated.ServiceOwner => _instanceOwnerPartyId, + Authenticated.SystemUser auth when await auth.LoadDetails() is { } details => details.Party.PartyId, + _ => throw new NotImplementedException(), + }; + var instanceOwnerPartyIdStr = instanceOwnerPartyId.ToString(CultureInfo.InvariantCulture); Instance instance = new Instance() { - Id = _instanceId, + Id = $"{instanceOwnerPartyIdStr}/{_instanceGuid}", AppId = "org/app", - InstanceOwner = new InstanceOwner() { PartyId = "1337" }, + InstanceOwner = new InstanceOwner() { PartyId = instanceOwnerPartyIdStr }, Data = [], }; - ClaimsPrincipal user = new( - new ClaimsIdentity( - new List() - { - new(AltinnCoreClaimTypes.UserId, "1337"), - new(AltinnCoreClaimTypes.AuthenticationLevel, "2"), - new(AltinnCoreClaimTypes.Org, "tdd"), - } - ) - ); - ProcessStartRequest processStartRequest = new ProcessStartRequest() { Instance = instance, User = user }; + ProcessStartRequest processStartRequest = new ProcessStartRequest() { Instance = instance, User = null }; ProcessChangeResult result = await processEngine.GenerateProcessStartEvents(processStartRequest); await processEngine.HandleEventsAndUpdateStorage(instance, null, result.ProcessStateChange?.Events); _processReaderMock.Verify(r => r.GetStartEventIds(), Times.Once); @@ -147,9 +146,9 @@ public async Task StartProcess_starts_process_and_moves_to_first_task() _processNavigatorMock.Verify(n => n.GetNextTask(It.IsAny(), "StartEvent_1", null), Times.Once); var expectedInstance = new Instance() { - Id = _instanceId, + Id = $"{instanceOwnerPartyIdStr}/{_instanceGuid}", AppId = "org/app", - InstanceOwner = new InstanceOwner() { PartyId = "1337" }, + InstanceOwner = new InstanceOwner() { PartyId = instanceOwnerPartyIdStr }, Data = [], Process = new ProcessState() { @@ -164,19 +163,40 @@ public async Task StartProcess_starts_process_and_moves_to_first_task() StartEvent = "StartEvent_1", }, }; + PlatformUser platformUser = token.Auth switch + { + Authenticated.User auth when await auth.LoadDetails() is { } details => new() + { + UserId = auth.UserId, + NationalIdentityNumber = details.SelectedParty.SSN, + AuthenticationLevel = auth.AuthenticationLevel, + }, + Authenticated.SelfIdentifiedUser auth => new() + { + UserId = auth.UserId, + AuthenticationLevel = auth.AuthenticationLevel, + }, + Authenticated.ServiceOwner auth => new() + { + OrgId = auth.Name, + AuthenticationLevel = auth.AuthenticationLevel, + }, + Authenticated.SystemUser auth => new() + { + SystemUserId = auth.SystemUserId[0], + SystemUserOwnerOrgNo = auth.SystemUserOrgNr.Get(OrganisationNumberFormat.Local), + AuthenticationLevel = auth.AuthenticationLevel, + }, + _ => throw new NotImplementedException(), + }; var expectedInstanceEvents = new List() { new() { - InstanceId = $"{_instanceOwnerPartyId}/{_instanceGuid}", + InstanceId = $"{instanceOwnerPartyIdStr}/{_instanceGuid}", EventType = InstanceEventType.process_StartEvent.ToString(), - InstanceOwnerPartyId = "1337", - User = new() - { - UserId = 1337, - OrgId = "tdd", - AuthenticationLevel = 2, - }, + InstanceOwnerPartyId = instanceOwnerPartyIdStr, + User = platformUser, ProcessInfo = new() { StartEvent = "StartEvent_1", @@ -190,15 +210,10 @@ public async Task StartProcess_starts_process_and_moves_to_first_task() }, new() { - InstanceId = $"{_instanceOwnerPartyId}/{_instanceGuid}", + InstanceId = $"{instanceOwnerPartyIdStr}/{_instanceGuid}", EventType = InstanceEventType.process_StartTask.ToString(), - InstanceOwnerPartyId = "1337", - User = new() - { - UserId = 1337, - OrgId = "tdd", - AuthenticationLevel = 2, - }, + InstanceOwnerPartyId = instanceOwnerPartyIdStr, + User = platformUser, ProcessInfo = new() { StartEvent = "StartEvent_1", @@ -231,7 +246,7 @@ public async Task StartProcess_starts_process_and_moves_to_first_task() result.Success.Should().BeTrue(); - await Verify(telemetrySink.GetSnapshot()); + await Verify(telemetrySink.GetSnapshot()).UseTextForParameters(token.Type.ToString()); } [Fact] @@ -298,8 +313,8 @@ public async Task StartProcess_starts_process_and_moves_to_first_task_with_prefi User = new() { UserId = 1337, - OrgId = "tdd", AuthenticationLevel = 2, + NationalIdentityNumber = "22927774937", }, ProcessInfo = new() { @@ -320,8 +335,8 @@ public async Task StartProcess_starts_process_and_moves_to_first_task_with_prefi User = new() { UserId = 1337, - OrgId = "tdd", AuthenticationLevel = 2, + NationalIdentityNumber = "22927774937", }, ProcessInfo = new() { @@ -547,7 +562,7 @@ public async Task Next_moves_instance_to_next_task_and_produces_instanceevents() User = new() { UserId = 1337, - OrgId = "tdd", + NationalIdentityNumber = "22927774937", AuthenticationLevel = 2, }, ProcessInfo = new() @@ -570,7 +585,7 @@ public async Task Next_moves_instance_to_next_task_and_produces_instanceevents() User = new() { UserId = 1337, - OrgId = "tdd", + NationalIdentityNumber = "22927774937", AuthenticationLevel = 2, }, ProcessInfo = new() @@ -696,7 +711,7 @@ public async Task Next_moves_instance_to_next_task_and_produces_abandon_instance User = new() { UserId = 1337, - OrgId = "tdd", + NationalIdentityNumber = "22927774937", AuthenticationLevel = 2, }, ProcessInfo = new() @@ -719,7 +734,7 @@ public async Task Next_moves_instance_to_next_task_and_produces_abandon_instance User = new() { UserId = 1337, - OrgId = "tdd", + NationalIdentityNumber = "22927774937", AuthenticationLevel = 2, }, ProcessInfo = new() @@ -825,7 +840,6 @@ public async Task Next_moves_instance_to_end_event_and_ends_proces() ProcessChangeResult result = await processEngine.Next(processNextRequest); _processReaderMock.Verify(r => r.IsProcessTask("Task_2"), Times.Once); _processReaderMock.Verify(r => r.IsEndEvent("EndEvent_1"), Times.Once); - _profileMock.Verify(p => p.GetUserProfile(1337), Times.Exactly(3)); _processNavigatorMock.Verify(n => n.GetNextTask(It.IsAny(), "Task_2", null), Times.Once); var expectedInstanceEvents = new List() @@ -974,7 +988,7 @@ public async Task UpdateInstanceAndRerunEvents_sends_instance_and_events_to_even User = new() { UserId = 1337, - OrgId = "tdd", + NationalIdentityNumber = "22927774937", AuthenticationLevel = 2, }, ProcessInfo = new() @@ -1020,7 +1034,8 @@ private ProcessEngine GetProcessEngine( bool setupProcessReaderMock = true, Instance? updatedInstance = null, List? userActions = null, - TelemetrySink? telemetrySink = null + TelemetrySink? telemetrySink = null, + TestJwtToken? token = null ) { if (setupProcessReaderMock) @@ -1035,16 +1050,15 @@ private ProcessEngine GetProcessEngine( _processReaderMock.Setup(r => r.IsProcessTask("EndEvent_1")).Returns(false); } - _profileMock - .Setup(p => p.GetUserProfile(1337)) - .ReturnsAsync( - () => - new UserProfile() - { - UserId = 1337, - Email = "test@example.com", - Party = new Party() { SSN = "22927774937" }, - } + _authenticationContextMock + .Setup(a => a.Current) + .Returns( + token?.Auth + ?? TestAuthentication.GetUserAuthentication( + userId: 1337, + email: "test@example.com", + ssn: "22927774937" + ) ); _processNavigatorMock .Setup(pn => pn.GetNextTask(It.IsAny(), "StartEvent_1", It.IsAny())) @@ -1091,7 +1105,6 @@ private ProcessEngine GetProcessEngine( return new ProcessEngine( _processReaderMock.Object, - _profileMock.Object, _processNavigatorMock.Object, _processEventHandlingDelegatorMock.Object, _processEventDispatcherMock.Object, @@ -1101,6 +1114,7 @@ private ProcessEngine GetProcessEngine( _instanceClientMock.Object, new ModelSerializationService(_appModelMock.Object, telemetrySink?.Object), _appMetadataMock.Object, + _authenticationContextMock.Object, telemetrySink?.Object ); } @@ -1108,7 +1122,6 @@ private ProcessEngine GetProcessEngine( public void Dispose() { _processReaderMock.VerifyNoOtherCalls(); - _profileMock.VerifyNoOtherCalls(); _processNavigatorMock.VerifyNoOtherCalls(); _processEventHandlingDelegatorMock.VerifyNoOtherCalls(); _processEventDispatcherMock.VerifyNoOtherCalls(); diff --git a/test/Altinn.App.Core.Tests/Models/OrganisationNumberTests.cs b/test/Altinn.App.Core.Tests/Models/OrganisationNumberTests.cs index 110232db8..8db2e5591 100644 --- a/test/Altinn.App.Core.Tests/Models/OrganisationNumberTests.cs +++ b/test/Altinn.App.Core.Tests/Models/OrganisationNumberTests.cs @@ -31,6 +31,7 @@ public class OrganisationNumberTests "207154156", "601050765", "085483285", + "004430301", ]; internal static readonly string[] InvalidOrganisationNumbers =