diff --git a/src/Altinn.App.Api/Controllers/AuthorizationController.cs b/src/Altinn.App.Api/Controllers/AuthorizationController.cs index 7590e0dfc..e7963eebb 100644 --- a/src/Altinn.App.Api/Controllers/AuthorizationController.cs +++ b/src/Altinn.App.Api/Controllers/AuthorizationController.cs @@ -5,6 +5,8 @@ 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; using Microsoft.Extensions.Options; @@ -44,57 +46,14 @@ IOptions settings [HttpGet("{org}/{app}/api/authorization/parties/current")] public async Task GetCurrentParty(bool returnPartyObject = false) { - UserContext userContext = await _userHelper.GetUserContext(HttpContext); - 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) - { - bool? isValid = await _authorization.ValidateSelectedParty(userId, userContext.PartyId); - - if (isValid == true) - { - if (returnPartyObject) - { - return Ok(userContext.Party); - } - - return Ok(userContext.PartyId); - } - else if (userContext.UserParty != null) - { - userContext.Party = userContext.UserParty; - userContext.PartyId = userContext.UserParty.PartyId; - } - else - { - userContext.Party = null; - userContext.PartyId = 0; - } - } - - string? cookieValue = Request.Cookies[_settings.GetAltinnPartyCookieName]; - if (!int.TryParse(cookieValue, out int partyIdFromCookie)) - { - partyIdFromCookie = 0; - } - - // Setting cookie to partyID of logged in user if it varies from previus value. - if (partyIdFromCookie != userContext.PartyId) - { - Response.Cookies.Append( - _settings.GetAltinnPartyCookieName, - userContext.PartyId.ToString(CultureInfo.InvariantCulture), - new CookieOptions { Domain = _settings.HostName } - ); - } + (Party? currentParty, _) = await GetCurrentPartyAsync(HttpContext); if (returnPartyObject) { - return Ok(userContext.Party); + return Ok(currentParty); } - return Ok(userContext.PartyId); + return Ok(currentParty?.PartyId ?? 0); } /// @@ -123,4 +82,80 @@ public async Task ValidateSelectedParty(int userId, int partyId) return StatusCode(500, $"Something went wrong when trying to validate party {partyId} for user {userId}"); } } + + /// + /// Fetches roles for current party. + /// + /// List of roles for the current user and party. + // [Authorize] + // [HttpGet("{org}/{app}/api/authorization/roles")] + // [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [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) + { + bool? isValid = await _authorization.ValidateSelectedParty(userId, userContext.PartyId); + if (isValid != true) + { + // Not valid, fall back to userParty if available + if (userContext.UserParty != null) + { + userContext.Party = userContext.UserParty; + userContext.PartyId = userContext.UserParty.PartyId; + } + else + { + userContext.Party = null; + userContext.PartyId = 0; + } + } + } + + // 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 (userContext.Party, userContext); + } } diff --git a/src/Altinn.App.Core/Features/Telemetry/Telemetry.Authorization.Client.cs b/src/Altinn.App.Core/Features/Telemetry/Telemetry.Authorization.Client.cs index 0abd5c909..7edbc6033 100644 --- a/src/Altinn.App.Core/Features/Telemetry/Telemetry.Authorization.Client.cs +++ b/src/Altinn.App.Core/Features/Telemetry/Telemetry.Authorization.Client.cs @@ -15,6 +15,15 @@ partial class Telemetry return activity; } + internal Activity? StartClientGetPartyRoleListActivity(int userId, int partyId) + { + var activity = ActivitySource.StartActivity($"{Prefix}.GetUserRoles"); + activity?.SetUserPartyId(partyId); + activity?.SetUserId(userId); + + return activity; + } + internal Activity? StartClientValidateSelectedPartyActivity(int userId, int partyId) { var activity = ActivitySource.StartActivity($"{Prefix}.ValidateSelectedParty"); diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs index f86ad9976..121ccd0d3 100644 --- a/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs +++ b/src/Altinn.App.Core/Infrastructure/Clients/Authorization/AuthorizationClient.cs @@ -13,6 +13,7 @@ using Altinn.Platform.Register.Models; using Altinn.Platform.Storage.Interface.Models; using AltinnCore.Authentication.Utils; +using Authorization.Platform.Authorization.Models; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -179,4 +180,49 @@ List actions } return MultiDecisionHelper.ValidatePdpMultiDecision(actionsResult, response.Response, user); } + + /// + /// Retrieves roles for a user on a specified party. + /// + /// The user id. + /// The user party id. + /// A list of roles for the user on the specified party. + public async Task> GetUserRoles(int userId, int userPartyId) + { + using var activity = _telemetry?.StartClientGetPartyRoleListActivity(userId, userPartyId); + + List roles = new(); + string apiUrl = $"roles?coveredByUserId={userId}&offeredByPartyId={userPartyId}"; + string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _settings.RuntimeCookieName); + + try + { + HttpResponseMessage response = await _client.GetAsync(token, apiUrl); + if (response.IsSuccessStatusCode) + { + string responseContent = await response.Content.ReadAsStringAsync(); + var deserialized = JsonConvert.DeserializeObject>(responseContent); + if (deserialized is not null) + { + roles = deserialized; + } + } + else + { + throw new Exception("Unexpected response from auth API:" + response.StatusCode); + } + } + catch (Exception ex) + { + _logger.LogError( + ex, + "An error occurred while retrieving roles for userId {UserId} and partyId {PartyId}", + userId, + userPartyId + ); + throw; + } + + return roles; + } } diff --git a/src/Altinn.App.Core/Internal/Auth/IAuthorizationClient.cs b/src/Altinn.App.Core/Internal/Auth/IAuthorizationClient.cs index 7cefe1a91..ed4b630ba 100644 --- a/src/Altinn.App.Core/Internal/Auth/IAuthorizationClient.cs +++ b/src/Altinn.App.Core/Internal/Auth/IAuthorizationClient.cs @@ -2,6 +2,7 @@ using Altinn.App.Core.Models; using Altinn.Platform.Register.Models; using Altinn.Platform.Storage.Interface.Models; +using Authorization.Platform.Authorization.Models; namespace Altinn.App.Core.Internal.Auth; @@ -50,4 +51,12 @@ Task AuthorizeAction( /// /// Task> AuthorizeActions(Instance instance, ClaimsPrincipal user, List actions); + + /// + /// Retrieves roles for a user on a specified party. + /// + /// The user id. + /// The user party id. + /// A list of roles for the user on the specified party. + Task> GetUserRoles(int userId, int userPartyId); } diff --git a/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs b/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs index 25c51b7da..da0c0b82b 100644 --- a/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs +++ b/test/Altinn.App.Api.Tests/Mocks/AuthorizationMock.cs @@ -3,6 +3,8 @@ using Altinn.App.Core.Models; 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; @@ -69,4 +71,16 @@ List actions return authorizedActions; } + + public async Task> GetUserRoles(int userId, int userPartyId) + { + await Task.CompletedTask; + List roles = new List + { + new Role { Type = "altinn", Value = "bobet" }, + new Role { Type = "altinn", Value = "bobes" }, + }; + + return roles; + } } diff --git a/test/Altinn.App.Api.Tests/OpenApi/swagger.json b/test/Altinn.App.Api.Tests/OpenApi/swagger.json index 63f8ea339..d15604a59 100644 --- a/test/Altinn.App.Api.Tests/OpenApi/swagger.json +++ b/test/Altinn.App.Api.Tests/OpenApi/swagger.json @@ -507,6 +507,108 @@ } } }, + "/{org}/{app}/api/authorization/roles": { + "get": { + "tags": [ + "Authorization" + ], + "parameters": [ + { + "name": "org", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "app", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Role" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Role" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Role" + } + } + }, + "application/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Role" + } + } + }, + "text/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Role" + } + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/xml": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, "/{org}/{app}/instances/{instanceOwnerPartyId}/{instanceGuid}/data": { "post": { "tags": [ @@ -7138,6 +7240,20 @@ }, "additionalProperties": false }, + "Role": { + "type": "object", + "properties": { + "type": { + "type": "string", + "nullable": true + }, + "value": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, "ShadowFields": { "type": "object", "properties": { diff --git a/test/Altinn.App.Api.Tests/OpenApi/swagger.yaml b/test/Altinn.App.Api.Tests/OpenApi/swagger.yaml index b51ddaa7e..60416a487 100644 --- a/test/Altinn.App.Api.Tests/OpenApi/swagger.yaml +++ b/test/Altinn.App.Api.Tests/OpenApi/swagger.yaml @@ -306,6 +306,68 @@ paths: responses: '200': description: OK + '/{org}/{app}/api/authorization/roles': + get: + tags: + - Authorization + parameters: + - name: org + in: path + required: true + schema: + type: string + - name: app + in: path + required: true + schema: + type: string + responses: + '200': + description: OK + content: + text/plain: + schema: + type: array + items: + $ref: '#/components/schemas/Role' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Role' + text/json: + schema: + type: array + items: + $ref: '#/components/schemas/Role' + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Role' + text/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Role' + '400': + description: Bad Request + content: + text/plain: + schema: + $ref: '#/components/schemas/ProblemDetails' + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' + text/json: + schema: + $ref: '#/components/schemas/ProblemDetails' + application/xml: + schema: + $ref: '#/components/schemas/ProblemDetails' + text/xml: + schema: + $ref: '#/components/schemas/ProblemDetails' '/{org}/{app}/instances/{instanceOwnerPartyId}/{instanceGuid}/data': post: tags: @@ -4573,6 +4635,16 @@ components: type: string nullable: true additionalProperties: false + Role: + type: object + properties: + type: + type: string + nullable: true + value: + type: string + nullable: true + additionalProperties: false ShadowFields: type: object properties: diff --git a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/AuthorizationClientTests.cs b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/AuthorizationClientTests.cs index 32ca07fc9..73661eea5 100644 --- a/test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/AuthorizationClientTests.cs +++ b/test/Altinn.App.Core.Tests/Infrastructure/Clients/Authorization/AuthorizationClientTests.cs @@ -3,15 +3,18 @@ using System.Text.Json; using Altinn.App.Common.Tests; using Altinn.App.Core.Configuration; +using Altinn.App.Core.Features; using Altinn.App.Core.Infrastructure.Clients.Authorization; using Altinn.Authorization.ABAC.Xacml.JsonProfile; using Altinn.Common.PEP.Interfaces; using Altinn.Platform.Storage.Interface.Models; +using Authorization.Platform.Authorization.Models; using FluentAssertions; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; +using Moq.Protected; namespace Altinn.App.Core.Tests.Infrastructure.Clients.Authorization; @@ -108,6 +111,137 @@ public async Task AuthorizeActions_returns_empty_dictionary_if_no_response_from_ actual.Should().BeEquivalentTo(expected); } + [Fact] + public async Task GetUserRolesAsync_returns_roles_on_success() + { + var userId = 1337; + var userPartyId = 2001; + var expectedRoles = new List() + { + new() { Type = "altinn", Value = "bobet" }, + new() { Type = "altinn", Value = "bobes" }, + }; + + var responseJson = JsonSerializer.Serialize(expectedRoles); + + var httpMessageHandler = new Mock(); + + httpMessageHandler + .Protected() + .Setup>( + "SendAsync", + ItExpr.Is(m => + m.RequestUri != null + && m.RequestUri.ToString() + .Contains($"roles?coveredByUserId={userId}&offeredByPartyId={userPartyId}") + ), + ItExpr.IsAny() + ) + .ReturnsAsync( + new HttpResponseMessage + { + StatusCode = System.Net.HttpStatusCode.OK, + Content = new StringContent(responseJson, System.Text.Encoding.UTF8, "application/json"), + } + ); + var httpClient = new HttpClient(httpMessageHandler.Object); + + TelemetrySink telemetrySink = new(); + var pdpMock = new Mock(); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers["Cookie"] = "AltinnStudioRuntime=myFakeJwtToken"; + + var httpContextAccessorMock = new Mock(); + httpContextAccessorMock.Setup(_ => _.HttpContext).Returns(httpContext); + + var appSettingsMock = new Mock>(); + appSettingsMock + .Setup(s => s.CurrentValue) + .Returns(new AppSettings { RuntimeCookieName = "AltinnStudioRuntime" }); + + var platformSettings = Options.Create( + new PlatformSettings + { + ApiAuthorizationEndpoint = "http://authorization.test/", + SubscriptionKey = "dummyKey", + } + ); + var logger = NullLogger.Instance; + + var client = new AuthorizationClient( + platformSettings, + httpContextAccessorMock.Object, + httpClient, + appSettingsMock.Object, + pdpMock.Object, + logger, + telemetrySink.Object + ); + + var actualRoles = await client.GetUserRoles(userId, userPartyId); + + actualRoles.Should().BeEquivalentTo(expectedRoles); + } + + [Fact] + public async Task GetUserRolesAsync_throws_exception_on_error_status_code() + { + var userId = 1337; + var userPartyId = 2001; + + var httpMessageHandler = new Mock(); + + httpMessageHandler + .Protected() + .Setup>( + "SendAsync", + ItExpr.Is(m => + m.RequestUri != null + && m.RequestUri.ToString() + .Contains($"roles?coveredByUserId={userId}&offeredByPartyId={userPartyId}") + ), + ItExpr.IsAny() + ) + .ReturnsAsync( + new HttpResponseMessage + { + StatusCode = System.Net.HttpStatusCode.InternalServerError, + Content = new StringContent("Internal Server Error"), + } + ); + + var httpClient = new HttpClient(httpMessageHandler.Object); + + var pdpMock = new Mock(); + var appSettingsMock = new Mock>(); + appSettingsMock + .Setup(s => s.CurrentValue) + .Returns(new AppSettings { RuntimeCookieName = "AltinnStudioRuntime" }); + + var platformSettings = Options.Create(new PlatformSettings { SubscriptionKey = "subscription-key" }); + var logger = NullLogger.Instance; + TelemetrySink telemetry = new(); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers["Cookie"] = "AltinnStudioRuntime=myFakeJwtToken"; + + var httpContextAccessorMock = new Mock(); + httpContextAccessorMock.Setup(_ => _.HttpContext).Returns(httpContext); + + var client = new AuthorizationClient( + platformSettings, + httpContextAccessorMock.Object, + httpClient, + appSettingsMock.Object, + pdpMock.Object, + logger, + telemetry.Object + ); + + await Assert.ThrowsAsync(() => client.GetUserRoles(userId, userPartyId)); + } + private static ClaimsPrincipal GetClaims(string partyId) { return new ClaimsPrincipal(