From ec2196506fd44a0d6a6ae5ebb281a05fddfe4dd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Dybvik=20Langfors?= Date: Thu, 12 Dec 2024 12:13:19 +0100 Subject: [PATCH] Add sync adapter scope for administrative access --- .../Authorization/StorageAccessHandler.cs | 25 +++++ src/Storage/Configuration/GeneralSettings.cs | 5 + .../Controllers/InstancesController.cs | 2 +- src/Storage/appsettings.json | 1 + .../InstancesControllerTests.cs | 96 +++++++++++++++++++ test/UnitTest/appsettings.unittest.json | 1 + 6 files changed, 129 insertions(+), 1 deletion(-) diff --git a/src/Storage/Authorization/StorageAccessHandler.cs b/src/Storage/Authorization/StorageAccessHandler.cs index 456cdff4..ddedf9db 100644 --- a/src/Storage/Authorization/StorageAccessHandler.cs +++ b/src/Storage/Authorization/StorageAccessHandler.cs @@ -8,6 +8,9 @@ using Altinn.Common.PEP.Constants; using Altinn.Common.PEP.Helpers; using Altinn.Common.PEP.Interfaces; +using Altinn.Platform.Storage.Configuration; +using Altinn.Platform.Storage.Extensions; +using Altinn.Platform.Storage.Helpers; using Altinn.Platform.Storage.Interface.Models; using Altinn.Platform.Storage.Repository; using Microsoft.AspNetCore.Authorization; @@ -31,6 +34,8 @@ public class StorageAccessHandler : AuthorizationHandler private readonly IInstanceRepository _instanceRepository; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IPDP _pdp; + private readonly IAuthorization _authorizationService; + private readonly GeneralSettings _generalSettings; private readonly ILogger _logger; private readonly IMemoryCache _memoryCache; private readonly PepSettings _pepSettings; @@ -47,6 +52,8 @@ public class StorageAccessHandler : AuthorizationHandler public StorageAccessHandler( IHttpContextAccessor httpContextAccessor, IPDP pdp, + IAuthorization authorizationService, + IOptions generalSettings, IOptions pepSettings, ILogger logger, IInstanceRepository instanceRepository, @@ -54,6 +61,8 @@ public StorageAccessHandler( { _httpContextAccessor = httpContextAccessor; _pdp = pdp; + _authorizationService = authorizationService; + _generalSettings = generalSettings.Value; _logger = logger; _pepSettings = pepSettings.Value; _instanceRepository = instanceRepository; @@ -69,6 +78,12 @@ public StorageAccessHandler( /// A Task protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, AppAccessRequirement requirement) { + if (IsValidSyncAdapterRequest(requirement)) + { + context.Succeed(requirement); + return; + } + XacmlJsonRequestRoot request = DecisionHelper.CreateDecisionRequest(context, requirement, _httpContextAccessor.HttpContext.GetRouteData()); _logger.LogInformation("// Storage PEP // AppAccessHandler // Request sent: {request}", JsonConvert.SerializeObject(request)); @@ -186,5 +201,15 @@ private static string GetCacheKeyForDecisionRequest(XacmlJsonRequestRoot request return subjectKey.ToString() + actionKey.ToString() + resourceKey.ToString(); } + + private bool IsValidSyncAdapterRequest(AppAccessRequirement requirement) + { + if (requirement.ActionType != "read" && requirement.ActionType != "delete") + { + return false; + } + + return _authorizationService.UserHasRequiredScope([_generalSettings.InstanceSyncAdapterScope]); + } } } diff --git a/src/Storage/Configuration/GeneralSettings.cs b/src/Storage/Configuration/GeneralSettings.cs index 0c67a320..46c2f4f2 100644 --- a/src/Storage/Configuration/GeneralSettings.cs +++ b/src/Storage/Configuration/GeneralSettings.cs @@ -33,6 +33,11 @@ public class GeneralSettings /// public List InstanceReadScope { get; set; } + /// + /// Gets or sets the scope for storage sync adapters. + /// + public string InstanceSyncAdapterScope { get; set; } + /// /// Gets or sets the cache lifetime for text resources. /// diff --git a/src/Storage/Controllers/InstancesController.cs b/src/Storage/Controllers/InstancesController.cs index 3acb8ffb..4b444846 100644 --- a/src/Storage/Controllers/InstancesController.cs +++ b/src/Storage/Controllers/InstancesController.cs @@ -287,7 +287,7 @@ public async Task> Get(int instanceOwnerPartyId, Guid ins { (Instance result, _) = await _instanceRepository.GetOne(instanceGuid, true); - if (User.GetOrg() != result.Org) + if (User.GetOrg() != result.Org && !_authorizationService.UserHasRequiredScope([_generalSettings.InstanceSyncAdapterScope])) { FilterOutDeletedDataElements(result); } diff --git a/src/Storage/appsettings.json b/src/Storage/appsettings.json index b29d2440..9fe816e6 100644 --- a/src/Storage/appsettings.json +++ b/src/Storage/appsettings.json @@ -29,6 +29,7 @@ "AppTitleCacheLifeTimeInSeconds": 3600, "AppMetadataCacheLifeTimeInSeconds": 300, "InstanceReadScope": [ "altinn:serviceowner/instances.read" ], + "InstanceSyncAdapterScope": "altinn:storage/instances.syncadapter", "MigrationIpWhiteList": "" }, "PlatformSettings": { diff --git a/test/UnitTest/TestingControllers/InstancesControllerTests.cs b/test/UnitTest/TestingControllers/InstancesControllerTests.cs index db09576c..0893c912 100644 --- a/test/UnitTest/TestingControllers/InstancesControllerTests.cs +++ b/test/UnitTest/TestingControllers/InstancesControllerTests.cs @@ -130,6 +130,30 @@ public async Task Get_One_Twice_Ok() Assert.Equal(HttpStatusCode.OK, response2.StatusCode); } + [Fact] + public async Task Get_One_With_SyncAdapterScope_Ok() + { + // Arrange + int instanceOwnerPartyId = 1337; + string instanceGuid = "377efa97-80ee-4cc6-8d48-09de12cc273d"; + string requestUri = $"{BasePath}/{instanceOwnerPartyId}/{instanceGuid}"; + + HttpClient client = GetTestClient(); + string token = PrincipalUtil.GetOrgToken("foo", scope: "altinn:storage/instances.syncadapter"); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + // Act + RequestTracker.Clear(); + HttpResponseMessage response = await client.GetAsync(requestUri); + + // Assert + Assert.Equal(0, RequestTracker.GetRequestCount("GetDecisionForRequest1337/377efa97-80ee-4cc6-8d48-09de12cc273d")); // We should not be hitting the PDP as sync adapter + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + string responseContent = await response.Content.ReadAsStringAsync(); + Instance instance = (Instance)JsonConvert.DeserializeObject(responseContent, typeof(Instance)); + Assert.Equal("1337", instance.InstanceOwner.PartyId); + } + /// /// Test case: User tries to access element that he is not authorized for /// Expected: Returns status forbidden. @@ -178,6 +202,31 @@ public async Task Post_ReponseIsDeny_ReturnsStatusForbidden() Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); } + /// + /// Test case: Sync adapters should not be allowed to write (only delete). + /// Expected: Returns status forbidden. + /// + [Fact] + public async Task Post_ReponseIsDenyForSyncAdapter_ReturnsStatusForbidden() + { + // Arrange + string appId = "tdd/endring-av-navn"; + string requestUri = $"{BasePath}?appId={appId}"; + + HttpClient client = GetTestClient(); + string token = PrincipalUtil.GetOrgToken("foo", scope: "altinn:storage/instances.syncadapter"); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + // Laste opp test instance.. + Instance instance = new Instance() { InstanceOwner = new InstanceOwner() { PartyId = "1337" }, Org = "tdd", AppId = "tdd/endring-av-navn" }; + + // Act + HttpResponseMessage response = await client.PostAsync(requestUri, JsonContent.Create(instance, new MediaTypeHeaderValue("application/json"))); + + // Assert + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + /// /// Test case: User has to low authentication level. /// Expected: Returns status forbidden. @@ -310,6 +359,32 @@ public async Task Delete_OrgHardDeletesInstance_ReturnedInstanceHasStatusBothSof Assert.Equal(deletedInstance.Status.HardDeleted, deletedInstance.Status.SoftDeleted); } + [Fact] + public async Task Delete_With_SyncAdapterScope_Ok() + { + // Arrange + int instanceOwnerPartyId = 1337; + string instanceGuid = "377efa97-80ee-4cc6-8d48-09de12cc273d"; + string requestUri = $"{BasePath}/{instanceOwnerPartyId}/{instanceGuid}?hard=true"; + + HttpClient client = GetTestClient(); + string token = PrincipalUtil.GetOrgToken("foo", scope: "altinn:storage/instances.syncadapter"); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + // Act + RequestTracker.Clear(); + HttpResponseMessage response = await client.DeleteAsync(requestUri); + + string json = await response.Content.ReadAsStringAsync(); + Instance deletedInstance = JsonConvert.DeserializeObject(json); + + // Assert + Assert.Equal(0, RequestTracker.GetRequestCount("GetDecisionForRequest1337/377efa97-80ee-4cc6-8d48-09de12cc273d")); // We should not be hitting the PDP as sync adapter + Assert.NotNull(deletedInstance.Status.HardDeleted); + Assert.NotNull(deletedInstance.Status.SoftDeleted); + Assert.Equal(deletedInstance.Status.HardDeleted, deletedInstance.Status.SoftDeleted); + } + /// /// Test case: End user system tries to soft delete an instance /// Expected: Returns success and deleted instance @@ -805,6 +880,27 @@ public async Task GetMany_IncorrectScope_ReturnsForbidden() Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); } + /// + /// Test case: Get Multiple instances using client with sync adapter scope should faile. + /// Expected: Returns status forbidden. + /// + [Fact] + public async Task GetMany_SyncAdapterScope_ReturnsForbidden() + { + // Arrange + string requestUri = $"{BasePath}?org=testOrg"; + + HttpClient client = GetTestClient(); + string token = PrincipalUtil.GetOrgToken("testOrg", scope: "altinn:storage/instances.syncadapter"); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); + + // Assert + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + /// /// Scenario: /// An app owner calls the API via apps-endpoints with a token that specifies a different diff --git a/test/UnitTest/appsettings.unittest.json b/test/UnitTest/appsettings.unittest.json index 28396075..c2844dd7 100644 --- a/test/UnitTest/appsettings.unittest.json +++ b/test/UnitTest/appsettings.unittest.json @@ -22,6 +22,7 @@ "RuntimeCookieName": "AltinnStudioRuntime", "BridgeApiAuthorizationEndpoint": "http://localhost:5055/sblbridge/authorization/api/", "InstanceReadScope": [ "altinn:serviceowner/instances.read" ], + "InstanceSyncAdapterScope": "altinn:storage/instances.syncadapter", "AppTitleCacheLifeTimeInSeconds": 60, "AppMetadataCacheLifeTimeInSeconds": 60, "TextResourceCacheLifeTimeInSeconds": 60,