diff --git a/src/Altinn.App.Api/Controllers/StatelessDataController.cs b/src/Altinn.App.Api/Controllers/StatelessDataController.cs index a17ab21f9..fe842ae5f 100644 --- a/src/Altinn.App.Api/Controllers/StatelessDataController.cs +++ b/src/Altinn.App.Api/Controllers/StatelessDataController.cs @@ -35,7 +35,6 @@ 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"; @@ -63,7 +62,6 @@ IAuthenticationContext authenticationContext _prefillService = prefillService; _altinnPartyClientClient = altinnPartyClientClient; _pdp = pdp; - _authenticationContext = authenticationContext; } /// @@ -85,7 +83,7 @@ public async Task Get( [FromRoute] string org, [FromRoute] string app, [FromQuery] string dataType, - [FromHeader(Name = "party")] string partyFromHeader, + [FromHeader(Name = "party")] string? partyFromHeader, [FromQuery] string? language = null ) { @@ -105,11 +103,20 @@ public async Task Get( ); } + if (partyFromHeader is null) + { + return BadRequest( + $"Invalid party header. Please provide a party header on the form partyid:123, org:[orgnr] or person:[ssn]" + ); + } + InstanceOwner? owner = await GetInstanceOwner(partyFromHeader); + if (owner is null) { return BadRequest( - $"Invalid party header. Please provide a party header on the form partyid:123, org:[orgnr] or person:[ssn]" + $"Invalid party header. Could not lookup instance owner from the provided partyid: ${partyFromHeader}. " + + $"Make sure partyid is represented with prefix \"partyId:\", \"person:\" or \"org:\" (eg: \"partyId:123\")" ); } @@ -307,58 +314,33 @@ public async Task PostAnonymous([FromQuery] string dataType, [From return Ok(appModel); } - private async Task GetInstanceOwner(string? partyFromHeader) + private async Task GetInstanceOwner(string partyFromHeader) { - // Use the party id of the logged in user, if no party id is given in the header - // Not sure if this is really used anywhere. It doesn't seem useful, as you'd - // always want to create an instance based on the selected party, not the person - // you happened to log in as. - if (partyFromHeader is null) + // Get the party as read in from the header. Authorization happens later. + var headerParts = partyFromHeader.Split(':'); + if (partyFromHeader.Contains(',') || headerParts.Length != 2) { - 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; - - return InstantiationHelper.PartyToInstanceOwner(party); + return null; } - else + + var id = headerParts[1]; + var idPrefix = headerParts[0].ToLowerInvariant(); + var party = idPrefix switch { - // 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), - - // 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; - } - - return InstantiationHelper.PartyToInstanceOwner(party); + 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, + }; + + if (party is null || party.PartyId == 0) + { + return null; } + + return InstantiationHelper.PartyToInstanceOwner(party); } private async Task AuthorizeAction(string org, string app, int partyId, string action) diff --git a/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs index c88e56558..3018a61f5 100644 --- a/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs +++ b/test/Altinn.App.Api.Tests/Controllers/StatelessDataControllerTests.cs @@ -290,6 +290,9 @@ public async Task Get_Returns_Forbidden_when_returned_descision_is_Deny() var dataProcessorMock = new Mock(); var prefillMock = new Mock(); var registerMock = new Mock(); + registerMock + .Setup(r => r.GetParty(501337)) + .ReturnsAsync(new Platform.Register.Models.Party { PartyId = 501337 }); var pdpMock = new Mock(); var authContextMock = new Mock(); var dataType = "some-value"; @@ -307,6 +310,7 @@ public async Task Get_Returns_Forbidden_when_returned_descision_is_Deny() statelessDataController.ControllerContext = new ControllerContext(); statelessDataController.ControllerContext.HttpContext = new DefaultHttpContext(); statelessDataController.ControllerContext.HttpContext.User = TestAuthentication.GetUserPrincipal(); + authContextMock.Setup(c => c.Current).Returns(TestAuthentication.GetUserAuthentication()); pdpMock .Setup(p => p.GetDecisionForRequest(It.IsAny())) @@ -322,7 +326,7 @@ public async Task Get_Returns_Forbidden_when_returned_descision_is_Deny() // Act appResourcesMock.Setup(x => x.GetClassRefForLogicDataType(dataType)).Returns(typeof(DummyModel).FullName!); - var result = await statelessDataController.Get("ttd", "demo-app", dataType, null!); + var result = await statelessDataController.Get("ttd", "demo-app", dataType, "partyId:501337"); // Assert result.Should().BeOfType().Which.StatusCode.Should().Be(403); @@ -332,7 +336,6 @@ public async Task Get_Returns_Forbidden_when_returned_descision_is_Deny() pdpMock.VerifyNoOtherCalls(); dataProcessorMock.VerifyNoOtherCalls(); prefillMock.VerifyNoOtherCalls(); - registerMock.VerifyNoOtherCalls(); } [Fact] @@ -344,6 +347,9 @@ public async Task Get_Returns_OK_with_appModel() var dataProcessorMock = new Mock(); var prefillMock = new Mock(); var registerMock = new Mock(); + registerMock + .Setup(r => r.GetParty(501337)) + .ReturnsAsync(new Platform.Register.Models.Party { PartyId = 501337 }); var pdpMock = new Mock(); var authContextMock = new Mock(); var dataType = "some-value"; @@ -379,7 +385,7 @@ public async Task Get_Returns_OK_with_appModel() // Act appResourcesMock.Setup(x => x.GetClassRefForLogicDataType(dataType)).Returns(classRef); - var result = await statelessDataController.Get("ttd", "demo-app", dataType, null!); + var result = await statelessDataController.Get("ttd", "demo-app", dataType, "partyId:501337"); // Assert result.Should().BeOfType().Which.StatusCode.Should().Be(200); @@ -400,6 +406,82 @@ public async Task Get_Returns_OK_with_appModel() pdpMock.VerifyNoOtherCalls(); dataProcessorMock.VerifyNoOtherCalls(); prefillMock.VerifyNoOtherCalls(); - registerMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Get_Returns_OK_When_Party_Is_Valid() + { + // Arrange + var dataType = "some-value"; + var classRef = typeof(DummyModel).FullName!; + + // Create mocks + var appModelMock = new Mock(); + var appResourcesMock = new Mock(); + var dataProcessorMock = new Mock(); + var prefillMock = new Mock(); + var registerMock = new Mock(); + var pdpMock = new Mock(); + var authContextMock = new Mock(); + + // Set up the app resources so we have a valid classRef + appResourcesMock.Setup(x => x.GetClassRefForLogicDataType(dataType)).Returns(classRef); + + // Set up the AltinnPartyClient to return a valid party + registerMock.Setup(r => r.GetParty(123)).ReturnsAsync(new Platform.Register.Models.Party { PartyId = 123 }); + + // Set up PD decision to Permit + pdpMock + .Setup(p => p.GetDecisionForRequest(It.IsAny())) + .ReturnsAsync( + new XacmlJsonResponse + { + Response = new List + { + new XacmlJsonResult { Decision = XacmlContextDecision.Permit.ToString() }, + }, + } + ); + + // Set up IAppModel so it can create a dummy model + appModelMock.Setup(a => a.Create(classRef)).Returns(new DummyModel()); + + // For demonstration, mock the user principal in the controller context + var principal = TestAuthentication.GetUserPrincipal(); + authContextMock.Setup(c => c.Current).Returns(TestAuthentication.GetUserAuthentication()); + + // Instantiate the controller + ILogger logger = new NullLogger(); + var controller = new StatelessDataController( + logger, + appModelMock.Object, + appResourcesMock.Object, + prefillMock.Object, + registerMock.Object, + pdpMock.Object, + new IDataProcessor[] { dataProcessorMock.Object }, + authContextMock.Object + ) + { + ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = principal } }, + }; + + // Act + // We pass in "partyid:123" as the party header to match the mock above + var result = await controller.Get("ttd", "demo-app", dataType, "partyid:123"); + + // Assert + result.Should().BeOfType().Which.StatusCode.Should().Be(200); + result.Should().BeOfType().Which.Value.Should().BeOfType(); + + // Verify the calls + registerMock.Verify(r => r.GetParty(123), Times.Once); + appResourcesMock.Verify(x => x.GetClassRefForLogicDataType(dataType), Times.Once); + pdpMock.Verify(p => p.GetDecisionForRequest(It.IsAny()), Times.Once); + appModelMock.Verify(a => a.Create(classRef), Times.Once); + dataProcessorMock.Verify( + p => p.ProcessDataRead(It.IsAny(), null, It.IsAny(), null), + Times.Once + ); } }