From acc1e006648a98c669f53dad849a5545341aa560 Mon Sep 17 00:00:00 2001 From: Andreas Isnes Nilsen Date: Mon, 2 Dec 2024 13:09:41 +0100 Subject: [PATCH] chores: migrate PEP (#118) --- Altinn.Authorization.sln | 26 + renovate.json | 6 + src/Directory.Build.props | 3 +- src/Directory.Packages.props | 57 +- .../Altinn.AccessManagement.csproj | 3 +- .../Altinn.Authorization.csproj | 1 - src/apps/Directory.Build.props | 3 +- .../Altinn.Authorization.PEP.sln | 36 ++ .../Altinn.Authorization.PEP.csproj | 25 + .../Authorization/AppAccessHandler.cs | 71 ++ .../Authorization/AppAccessRequirement.cs | 26 + .../Authorization/ClaimAccessHandler.cs | 56 ++ .../Authorization/ClaimAccessRequirement.cs | 33 + .../Authorization/IScopeAccessRequirement.cs | 16 + .../Authorization/ResourceAccessHandler.cs | 70 ++ .../ResourceAccessRequirement.cs | 33 + .../Authorization/ScopeAccessHandler.cs | 56 ++ .../Authorization/ScopeAccessRequirement.cs | 38 ++ .../Clients/AuthorizationApiClient.cs | 86 +++ .../Configuration/PepSettings.cs | 13 + .../Configuration/PlatformSettings.cs | 18 + .../Constants/AltinnObligations.cs | 13 + .../Constants/AltinnXacmlUrns.cs | 108 ++++ .../AuthorizationBuilderExtensions.cs | 60 ++ .../Extensions/ServiceCollectionExtensions.cs | 26 + .../Helpers/DecisionHelper.cs | 612 ++++++++++++++++++ .../Implementation/PDPAppSI.cs | 62 ++ .../Interfaces/IPDP.cs | 28 + .../Models/EnforcementResult.cs | 20 + .../Models/IDFormat.cs | 32 + .../Models/OrgClaim.cs | 22 + .../Models/SystemUserClaim.cs | 35 + .../Utils/IDFormatDeterminator.cs | 185 ++++++ .../src/Directory.Build.props | 5 + .../Altinn.Authorization.PEP.Tests.csproj | 16 + .../AppAccessHandlerTest.cs | 380 +++++++++++ .../DecisionHelperTest.cs | 457 +++++++++++++ .../GlobalSuppressions.cs | 8 + .../ResourceAccessHandlerTest.cs | 271 ++++++++ .../ScopeAccessHandlerTest.cs | 168 +++++ .../tests/Directory.Build.props | 8 + src/pkgs/Directory.Build.props | 6 +- src/pkgs/Directory.Build.targets | 27 + src/pkgs/Directory.Packages.props | 23 - 44 files changed, 3189 insertions(+), 59 deletions(-) create mode 100644 renovate.json create mode 100644 src/pkgs/Altinn.Authorization.PEP/Altinn.Authorization.PEP.sln create mode 100644 src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Altinn.Authorization.PEP.csproj create mode 100644 src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/AppAccessHandler.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/AppAccessRequirement.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/ClaimAccessHandler.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/ClaimAccessRequirement.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/IScopeAccessRequirement.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/ResourceAccessHandler.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/ResourceAccessRequirement.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/ScopeAccessHandler.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/ScopeAccessRequirement.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Clients/AuthorizationApiClient.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Configuration/PepSettings.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Configuration/PlatformSettings.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Constants/AltinnObligations.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Constants/AltinnXacmlUrns.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Extensions/AuthorizationBuilderExtensions.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Helpers/DecisionHelper.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Implementation/PDPAppSI.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Interfaces/IPDP.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Models/EnforcementResult.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Models/IDFormat.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Models/OrgClaim.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Models/SystemUserClaim.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Utils/IDFormatDeterminator.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/src/Directory.Build.props create mode 100644 src/pkgs/Altinn.Authorization.PEP/tests/Altinn.Authorization.PEP.Tests/Altinn.Authorization.PEP.Tests.csproj create mode 100644 src/pkgs/Altinn.Authorization.PEP/tests/Altinn.Authorization.PEP.Tests/AppAccessHandlerTest.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/tests/Altinn.Authorization.PEP.Tests/DecisionHelperTest.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/tests/Altinn.Authorization.PEP.Tests/GlobalSuppressions.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/tests/Altinn.Authorization.PEP.Tests/ResourceAccessHandlerTest.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/tests/Altinn.Authorization.PEP.Tests/ScopeAccessHandlerTest.cs create mode 100644 src/pkgs/Altinn.Authorization.PEP/tests/Directory.Build.props create mode 100644 src/pkgs/Directory.Build.targets delete mode 100644 src/pkgs/Directory.Packages.props diff --git a/Altinn.Authorization.sln b/Altinn.Authorization.sln index 35d5a780..e6592a5d 100644 --- a/Altinn.Authorization.sln +++ b/Altinn.Authorization.sln @@ -65,6 +65,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{35FE03F3 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Altinn.Authorization.Hosting.Tests", "src\libs\Altinn.Authorization.Hosting\tests\Altinn.Authorization.Hosting.Tests\Altinn.Authorization.Hosting.Tests.csproj", "{95DC14A3-43E1-4DE8-8C40-3DAF2719B864}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "pkgs", "pkgs", "{CA323293-CA35-413A-8EE2-F33902239D11}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Altinn.Authorization.PEP", "Altinn.Authorization.PEP", "{B1E3ACAE-89C4-4693-95D0-A71DDFA728C7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A6AC78FE-D74C-4759-9467-087DFB70D5B6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Altinn.Authorization.PEP", "src\pkgs\Altinn.Authorization.PEP\src\Altinn.Authorization.PEP\Altinn.Authorization.PEP.csproj", "{874B5EF3-BA5F-41F3-B97D-3EC6DF383BA2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{355D903B-A1F0-4640-A528-2DB546AA76AE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Altinn.Authorization.PEP.Tests", "src\pkgs\Altinn.Authorization.PEP\tests\Altinn.Authorization.PEP.Tests\Altinn.Authorization.PEP.Tests.csproj", "{6CD7B4EE-5AE6-4940-9EC9-3000E5F3E9D0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -130,6 +142,14 @@ Global {95DC14A3-43E1-4DE8-8C40-3DAF2719B864}.Debug|Any CPU.Build.0 = Debug|Any CPU {95DC14A3-43E1-4DE8-8C40-3DAF2719B864}.Release|Any CPU.ActiveCfg = Release|Any CPU {95DC14A3-43E1-4DE8-8C40-3DAF2719B864}.Release|Any CPU.Build.0 = Release|Any CPU + {874B5EF3-BA5F-41F3-B97D-3EC6DF383BA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {874B5EF3-BA5F-41F3-B97D-3EC6DF383BA2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {874B5EF3-BA5F-41F3-B97D-3EC6DF383BA2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {874B5EF3-BA5F-41F3-B97D-3EC6DF383BA2}.Release|Any CPU.Build.0 = Release|Any CPU + {6CD7B4EE-5AE6-4940-9EC9-3000E5F3E9D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6CD7B4EE-5AE6-4940-9EC9-3000E5F3E9D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6CD7B4EE-5AE6-4940-9EC9-3000E5F3E9D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6CD7B4EE-5AE6-4940-9EC9-3000E5F3E9D0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {E9D041A5-2AB6-45FD-8D24-EF552025539E} = {2891B160-9E46-42E5-95FF-08523A3192EF} @@ -162,5 +182,11 @@ Global {57E22F21-9A30-4753-851F-AB061A0EA683} = {5AF3188D-BF17-404A-A949-6A88527B61CD} {35FE03F3-263C-4C98-ABB1-B64BE2FB70CF} = {3C5636A1-2425-49FD-96F9-7884BA3F3797} {95DC14A3-43E1-4DE8-8C40-3DAF2719B864} = {35FE03F3-263C-4C98-ABB1-B64BE2FB70CF} + {CA323293-CA35-413A-8EE2-F33902239D11} = {2891B160-9E46-42E5-95FF-08523A3192EF} + {B1E3ACAE-89C4-4693-95D0-A71DDFA728C7} = {CA323293-CA35-413A-8EE2-F33902239D11} + {A6AC78FE-D74C-4759-9467-087DFB70D5B6} = {B1E3ACAE-89C4-4693-95D0-A71DDFA728C7} + {874B5EF3-BA5F-41F3-B97D-3EC6DF383BA2} = {A6AC78FE-D74C-4759-9467-087DFB70D5B6} + {355D903B-A1F0-4640-A528-2DB546AA76AE} = {B1E3ACAE-89C4-4693-95D0-A71DDFA728C7} + {6CD7B4EE-5AE6-4940-9EC9-3000E5F3E9D0} = {355D903B-A1F0-4640-A528-2DB546AA76AE} EndGlobalSection EndGlobal diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000..df49be66 --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "local>Altinn/renovate-config" + ] +} \ No newline at end of file diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 7c7bc05e..a1f783d8 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -10,7 +10,6 @@ MIT true Altinn Authorization - Altinn-Authorization - + \ No newline at end of file diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 0488e24b..e75d8946 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -1,12 +1,14 @@ + + true + + - + - + @@ -17,10 +19,8 @@ - - + + @@ -33,42 +33,44 @@ - + + + + - - + + + + + - + + + + - + - + - + - + @@ -83,8 +85,6 @@ - - @@ -98,8 +98,9 @@ + + + - - true - + \ No newline at end of file diff --git a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement/Altinn.AccessManagement.csproj b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement/Altinn.AccessManagement.csproj index 0a4c5ca5..912b2604 100644 --- a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement/Altinn.AccessManagement.csproj +++ b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement/Altinn.AccessManagement.csproj @@ -27,7 +27,6 @@ - @@ -41,4 +40,4 @@ Include="..\Altinn.AccessManagement.Persistence\Altinn.AccessManagement.Persistence.csproj" /> - + \ No newline at end of file diff --git a/src/apps/Altinn.Authorization/src/Altinn.Authorization/Altinn.Authorization.csproj b/src/apps/Altinn.Authorization/src/Altinn.Authorization/Altinn.Authorization.csproj index deaade4c..d658ee89 100644 --- a/src/apps/Altinn.Authorization/src/Altinn.Authorization/Altinn.Authorization.csproj +++ b/src/apps/Altinn.Authorization/src/Altinn.Authorization/Altinn.Authorization.csproj @@ -38,7 +38,6 @@ - diff --git a/src/apps/Directory.Build.props b/src/apps/Directory.Build.props index 9af0ff8c..20e06d22 100644 --- a/src/apps/Directory.Build.props +++ b/src/apps/Directory.Build.props @@ -1,4 +1,5 @@ + - + \ No newline at end of file diff --git a/src/pkgs/Altinn.Authorization.PEP/Altinn.Authorization.PEP.sln b/src/pkgs/Altinn.Authorization.PEP/Altinn.Authorization.PEP.sln new file mode 100644 index 00000000..2254f8a1 --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/Altinn.Authorization.PEP.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{DA970EDE-848F-4A7A-8F67-13835BD4DA89}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Altinn.Authorization.PEP", "src\Altinn.Authorization.PEP\Altinn.Authorization.PEP.csproj", "{C99D2680-4A77-40E4-99E0-C2FBBB4C709C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{DDA3BB50-F068-4BDB-8684-CBF68ECDE9FC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Altinn.Authorization.PEP.Tests", "tests\Altinn.Authorization.PEP.Tests\Altinn.Authorization.PEP.Tests.csproj", "{9005F003-ECD8-4EA7-9149-F47F4FEC5697}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C99D2680-4A77-40E4-99E0-C2FBBB4C709C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C99D2680-4A77-40E4-99E0-C2FBBB4C709C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C99D2680-4A77-40E4-99E0-C2FBBB4C709C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C99D2680-4A77-40E4-99E0-C2FBBB4C709C}.Release|Any CPU.Build.0 = Release|Any CPU + {9005F003-ECD8-4EA7-9149-F47F4FEC5697}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9005F003-ECD8-4EA7-9149-F47F4FEC5697}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9005F003-ECD8-4EA7-9149-F47F4FEC5697}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9005F003-ECD8-4EA7-9149-F47F4FEC5697}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {C99D2680-4A77-40E4-99E0-C2FBBB4C709C} = {DA970EDE-848F-4A7A-8F67-13835BD4DA89} + {9005F003-ECD8-4EA7-9149-F47F4FEC5697} = {DDA3BB50-F068-4BDB-8684-CBF68ECDE9FC} + EndGlobalSection +EndGlobal diff --git a/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Altinn.Authorization.PEP.csproj b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Altinn.Authorization.PEP.csproj new file mode 100644 index 00000000..8df6b10f --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Altinn.Authorization.PEP.csproj @@ -0,0 +1,25 @@ + + + + + true + Altinn.Authorization.PEP + Altinn;Studio;Authorization;Policy;Enforcement;Point + + Policy Enforcement Point for Attribute-based authorization using + Altinn.Authorization.ABAC in ASP.Net apps. + See our repository for the full documentation. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/AppAccessHandler.cs b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/AppAccessHandler.cs new file mode 100644 index 00000000..00c8cb84 --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/AppAccessHandler.cs @@ -0,0 +1,71 @@ +using System; +using System.Threading.Tasks; +using Altinn.Authorization.ABAC.Xacml.JsonProfile; +using Altinn.Common.PEP.Helpers; +using Altinn.Common.PEP.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Logging; + +namespace Altinn.Common.PEP.Authorization +{ + /// + /// AuthorizationHandler that is created for handling access to app. + /// Authorizes based om AppAccessRequirement and app id from route + /// for details about authorization + /// in asp.net core + /// + public class AppAccessHandler : AuthorizationHandler + { + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IPDP _pdp; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The http context accessor + /// The pdp + /// The logger. + public AppAccessHandler( + IHttpContextAccessor httpContextAccessor, + IPDP pdp, + ILogger logger) + { + _httpContextAccessor = httpContextAccessor; + _pdp = pdp; + _logger = logger; + } + + /// + /// This method authorize access bases on context and requirement + /// Is triggered by annotation on MVC action and setup in startup. + /// + /// The context + /// The requirement + /// A Task + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, AppAccessRequirement requirement) + { + HttpContext httpContext = _httpContextAccessor.HttpContext; + + XacmlJsonRequestRoot request = DecisionHelper.CreateDecisionRequest(context, requirement, _httpContextAccessor.HttpContext.GetRouteData()); + + XacmlJsonResponse response = await _pdp.GetDecisionForRequest(request); + + if (response?.Response == null) + { + throw new ArgumentNullException("response"); + } + + if (!DecisionHelper.ValidatePdpDecision(response.Response, context.User)) + { + context.Fail(); + } + + context.Succeed(requirement); + await Task.CompletedTask; + } + } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/AppAccessRequirement.cs b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/AppAccessRequirement.cs new file mode 100644 index 00000000..e25e988e --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/AppAccessRequirement.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Altinn.Common.PEP.Authorization +{ + /// + /// Requirement for authorization policies used for accessing apps. + /// for details about authorization + /// in asp.net core. + /// + public class AppAccessRequirement : IAuthorizationRequirement + { + /// + /// Initializes a new instance of the class + /// + /// The Action type for this requirement + public AppAccessRequirement(string actionType) + { + this.ActionType = actionType; + } + + /// + /// Gets or sets The Action type defined for the policy using this requirement + /// + public string ActionType { get; set; } + } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/ClaimAccessHandler.cs b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/ClaimAccessHandler.cs new file mode 100644 index 00000000..af04706f --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/ClaimAccessHandler.cs @@ -0,0 +1,56 @@ +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Logging; + +namespace Altinn.Common.PEP.Authorization +{ + /// + /// Authorization handler that verifies that the user has a given claimtype with a given value + /// from a given issuer + /// + public class ClaimAccessHandler : AuthorizationHandler + { + /// + /// Initializes a new instance of the class. + /// + public ClaimAccessHandler() + { + } + + /// + /// This method authorize access bases on context and requirement + /// Is triggered by annotation on MVC action and setup in startup. + /// + /// The context + /// The requirement + /// No object or value is returned by this method when it completes. + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ClaimAccessRequirement requirement) + { + bool isAuthorized = false; + if (context.User != null && context.User.Claims != null) + { + foreach (Claim claim in context.User.Claims) + { + if (claim.Type.Equals(requirement.ClaimType) + && claim.Value.Equals(requirement.ClaimValue)) + { + isAuthorized = true; + break; + } + } + } + + if (isAuthorized) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + + await Task.CompletedTask; + } + } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/ClaimAccessRequirement.cs b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/ClaimAccessRequirement.cs new file mode 100644 index 00000000..b88657a0 --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/ClaimAccessRequirement.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Altinn.Common.PEP.Authorization +{ + /// + /// Requirement for authorization policies used for accessing apps. + /// for details about authorization + /// in asp.net core. + /// + public class ClaimAccessRequirement : IAuthorizationRequirement + { + /// + /// Initializes a new instance of the class + /// + /// The claim type. + /// The claim value + public ClaimAccessRequirement(string claimType, string claimValue) + { + this.ClaimType = claimType; + this.ClaimValue = claimValue; + } + + /// + /// Gets or sets the claim type for the required claim + /// + public string ClaimType { get; set; } + + /// + /// Gets or sets the claim value + /// + public string ClaimValue { get; set; } + } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/IScopeAccessRequirement.cs b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/IScopeAccessRequirement.cs new file mode 100644 index 00000000..ecd63f36 --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/IScopeAccessRequirement.cs @@ -0,0 +1,16 @@ +#nullable enable + +using Microsoft.AspNetCore.Authorization; + +namespace Altinn.Common.PEP.Authorization; + +/// +/// This interface describes the implementation of a scope access requirement in policy based authorization +/// +public interface IScopeAccessRequirement : IAuthorizationRequirement +{ + /// + /// Gets or sets the scope defined for the policy using this requirement + /// + string[] Scope { get; set; } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/ResourceAccessHandler.cs b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/ResourceAccessHandler.cs new file mode 100644 index 00000000..b4a0dfad --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/ResourceAccessHandler.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading.Tasks; +using Altinn.Authorization.ABAC.Xacml.JsonProfile; +using Altinn.Common.PEP.Helpers; +using Altinn.Common.PEP.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Logging; + +namespace Altinn.Common.PEP.Authorization +{ + /// + /// AuthorizationHandler that is created for handling access to app. + /// Authorizes based om AppAccessRequirement and app id from route + /// for details about authorization + /// in asp.net core + /// + public class ResourceAccessHandler : AuthorizationHandler + { + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IPDP _pdp; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The http context accessor + /// The pdp + /// The logger. + public ResourceAccessHandler( + IHttpContextAccessor httpContextAccessor, + IPDP pdp, + ILogger logger) + { + _httpContextAccessor = httpContextAccessor; + _pdp = pdp; + _logger = logger; + } + + /// + /// This method authorize access bases on context and requirement + /// Is triggered by annotation on MVC action and setup in startup. + /// + /// The context + /// The requirement + /// A Task + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ResourceAccessRequirement requirement) + { + HttpContext httpContext = _httpContextAccessor.HttpContext; + + XacmlJsonRequestRoot request = DecisionHelper.CreateDecisionRequest(context, requirement, _httpContextAccessor.HttpContext.GetRouteData(), _httpContextAccessor.HttpContext.Request.Headers); + + XacmlJsonResponse response = await _pdp.GetDecisionForRequest(request); + + if (response?.Response == null) + { + throw new ArgumentNullException("response"); + } + + if (!DecisionHelper.ValidatePdpDecision(response.Response, context.User)) + { + context.Fail(); + } + + context.Succeed(requirement); + await Task.CompletedTask; + } + } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/ResourceAccessRequirement.cs b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/ResourceAccessRequirement.cs new file mode 100644 index 00000000..e6455ef2 --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/ResourceAccessRequirement.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Altinn.Common.PEP.Authorization +{ + /// + /// Requirement for authorization policies used for accessing apps. + /// for details about authorization + /// in asp.net core. + /// + public class ResourceAccessRequirement : IAuthorizationRequirement + { + /// + /// Initializes a new instance of the class + /// + /// The Action type for this requirement + /// The resource id for the resource authorization is verified for + public ResourceAccessRequirement(string actionType, string resourceId) + { + this.ActionType = actionType; + this.ResourceId = resourceId; + } + + /// + /// Gets or sets The Action type defined for the policy using this requirement + /// + public string ActionType { get; set; } + + /// + /// Gets or sets the resourcId for the resource that authorization should verified for + /// + public string ResourceId { get; set; } + } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/ScopeAccessHandler.cs b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/ScopeAccessHandler.cs new file mode 100644 index 00000000..ba95fade --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/ScopeAccessHandler.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Authorization; + +namespace Altinn.Common.PEP.Authorization +{ + /// + /// Represents an authorization handler that can perform authorization based on scope + /// + public class ScopeAccessHandler : AuthorizationHandler + { + /// + /// Performs necessary logic to evaluate the scope requirement. + /// + /// The current + /// The scope requirement to evaluate. + /// Returns a Task for async await + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, IScopeAccessRequirement requirement) + { + // get scope parameter from user claims + string contextScope = context.User?.Identities + ?.FirstOrDefault(i => i.AuthenticationType != null && i.AuthenticationType.Equals("AuthenticationTypes.Federation"))?.Claims + .Where(c => c.Type.Equals("urn:altinn:scope"))? + .Select(c => c.Value).FirstOrDefault(); + + contextScope ??= context.User?.Claims.Where(c => c.Type.Equals("scope")).Select(c => c.Value).FirstOrDefault(); + + bool validScope = false; + + // compare scope claim value to + if (!string.IsNullOrWhiteSpace(contextScope)) + { + string[] requiredScopes = requirement.Scope; + List clientScopes = contextScope.Split(' ').ToList(); + + foreach (string requiredScope in requiredScopes) + { + if (clientScopes.Contains(requiredScope)) + { + validScope = true; + break; + } + } + } + + if (validScope) + { + context.Succeed(requirement); + } + + await Task.CompletedTask; + } + } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/ScopeAccessRequirement.cs b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/ScopeAccessRequirement.cs new file mode 100644 index 00000000..532fd4f3 --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Authorization/ScopeAccessRequirement.cs @@ -0,0 +1,38 @@ +#nullable enable + +using Microsoft.AspNetCore.Authorization; + +namespace Altinn.Common.PEP.Authorization +{ + /// + /// Requirement for authorization policies used for validating a client scope. + /// for details about authorization + /// in asp.net core. + /// + public class ScopeAccessRequirement : IScopeAccessRequirement + { + /// + /// Initializes a new instance of the class and + /// pupulates the Scope property with the given scope. + /// + /// The scope for this requirement + public ScopeAccessRequirement(string scope) + { + Scope = new string[] { scope }; + } + + /// + /// Initializes a new instance of the class with the given scopes. + /// + /// The scope for this requirement + public ScopeAccessRequirement(string[] scopes) + { + Scope = scopes; + } + + /// + /// Gets or sets the scope defined for the policy using this requirement + /// + public string[] Scope { get; set; } + } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Clients/AuthorizationApiClient.cs b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Clients/AuthorizationApiClient.cs new file mode 100644 index 00000000..a9f31462 --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Clients/AuthorizationApiClient.cs @@ -0,0 +1,86 @@ +using System.Diagnostics; +using System.Net; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using Altinn.Authorization.ABAC.Xacml.JsonProfile; +using Altinn.Common.PEP.Configuration; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Altinn.Common.PEP.Clients +{ + /// + /// Represents a form of types HttpClient for communication with the Authorization platform service. + /// + public class AuthorizationApiClient + { + private const string SubscriptionKeyHeaderName = "Ocp-Apim-Subscription-Key"; + private const string ForwardedForHeaderName = "x-forwarded-for"; + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + private readonly IHttpContextAccessor _httpContextAccessor; + + /// + /// Initialize a new instance of the class. + /// + /// the heep context accessor + /// A HttpClient provided by the built in HttpClientFactory. + /// The current platform settings + /// A logger provided by the built in LoggerFactory. + public AuthorizationApiClient( + IHttpContextAccessor httpContextAccessor, + HttpClient client, + IOptions platformSettings, + ILogger logger) + { + _httpContextAccessor = httpContextAccessor; + _httpClient = client; + _logger = logger; + + if (!_httpClient.DefaultRequestHeaders.Contains(ForwardedForHeaderName)) + { + string clientIpAddress = _httpContextAccessor?.HttpContext?.Request?.Headers?[ForwardedForHeaderName]; + _httpClient.DefaultRequestHeaders.Add(ForwardedForHeaderName, clientIpAddress); + } + + client.BaseAddress = new Uri($"{platformSettings.Value.ApiAuthorizationEndpoint}"); + client.DefaultRequestHeaders.Add(SubscriptionKeyHeaderName, platformSettings.Value.SubscriptionKey); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + } + + /// + /// Method for performing authorization. + /// + /// An authorization request. + /// The result of the authorization request. + public async Task AuthorizeRequest(XacmlJsonRequestRoot xacmlJsonRequest) + { + XacmlJsonResponse xacmlJsonResponse = null; + string apiUrl = $"decision"; + string requestJson = JsonSerializer.Serialize(xacmlJsonRequest); + StringContent httpContent = new StringContent(requestJson, Encoding.UTF8, "application/json"); + + Stopwatch stopWatch = new Stopwatch(); + stopWatch.Start(); + HttpResponseMessage response = await _httpClient.PostAsync(apiUrl, httpContent); + stopWatch.Stop(); + TimeSpan ts = stopWatch.Elapsed; + _logger.LogInformation("Authorization PDP time elapsed: " + ts.TotalMilliseconds); + + if (response.StatusCode == HttpStatusCode.OK) + { + string responseData = await response.Content.ReadAsStringAsync(); + xacmlJsonResponse = JsonSerializer.Deserialize(responseData); + } + else + { + _logger.LogInformation($"// PDPAppSI // GetDecisionForRequest // Non-zero status code: {response.StatusCode}"); + _logger.LogInformation($"// PDPAppSI // GetDecisionForRequest // Response: {await response.Content.ReadAsStringAsync()}"); + } + + return xacmlJsonResponse; + } + } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Configuration/PepSettings.cs b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Configuration/PepSettings.cs new file mode 100644 index 00000000..49946439 --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Configuration/PepSettings.cs @@ -0,0 +1,13 @@ +namespace Altinn.Common.PEP.Configuration +{ + /// + /// General configuration settings + /// + public class PepSettings + { + /// + /// The timout on pdp decions + /// + public int PdpDecisionCachingTimeout { get; set; } + } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Configuration/PlatformSettings.cs b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Configuration/PlatformSettings.cs new file mode 100644 index 00000000..353e6a05 --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Configuration/PlatformSettings.cs @@ -0,0 +1,18 @@ +namespace Altinn.Common.PEP.Configuration +{ + /// + /// Configuratin for platform settings + /// + public class PlatformSettings + { + /// + /// Gets or sets the url for the API Authorization endpoint + /// + public string ApiAuthorizationEndpoint { get; set; } + + /// + /// Gets or sets the subscription key value to use in requests against the platform. + /// + public string SubscriptionKey { get; set; } + } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Constants/AltinnObligations.cs b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Constants/AltinnObligations.cs new file mode 100644 index 00000000..cf81e589 --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Constants/AltinnObligations.cs @@ -0,0 +1,13 @@ +namespace Altinn.Common.PEP.Constants +{ + /// + /// Represents a set of Obligation values + /// + public static class AltinnObligations + { + /// + /// Get the name of the obligation authentication level. + /// + public const string RequiredAuthenticationLevel = "RequiredAuthenticationLevel"; + } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Constants/AltinnXacmlUrns.cs b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Constants/AltinnXacmlUrns.cs new file mode 100644 index 00000000..d682a18a --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Constants/AltinnXacmlUrns.cs @@ -0,0 +1,108 @@ +namespace Altinn.Common.PEP.Constants +{ + /// + /// Represents a collection of URN values for different Altinn specific XACML attributes. + /// + public static class AltinnXacmlUrns + { + /// + /// Get the URN value for party id. + /// + public const string PartyId = "urn:altinn:partyid"; + + /// + /// Get the URN value for + /// + public const string Ssn = "urn:altinn:ssn"; + + /// + /// Get the URN value for organization number. This is the legacy version + /// + public const string OrganizationNumber = "urn:altinn:organizationnumber"; + + /// + /// xacml string that represents organization number + /// + public const string OrganizationNumberAttribute = "urn:altinn:organization:identifier-no"; + + /// + /// Get the URN value for instance id + /// + public const string InstanceId = "urn:altinn:instance-id"; + + /// + /// Get the URN value for org (application owner) + /// + public const string OrgId = "urn:altinn:org"; + + /// + /// Get the URN value for app id + /// + public const string AppId = "urn:altinn:app"; + + /// + /// Get the URN value for app resource + /// + public const string AppResource = "urn:altinn:appresource"; + + /// + /// Get the value for for event id + /// + public const string EventId = "urn:altinn:event-id"; + + /// + /// Get the value task id + /// + public const string TaskId = "urn:altinn:task"; + + /// + /// Get the value resourceId + /// + public const string ResourceId = "urn:altinn:resource"; + + /// + /// Get the value Resource Instance + /// + public const string ResourceInstance = "urn:altinn:resourceinstance"; + + /// + /// Get the value eventType + /// + public const string EventType = "urn:altinn:eventtype"; + + /// + /// Get the value EventSource + /// + public const string EventSource = "urn:altinn:eventsource"; + + /// + /// Get the value scope + /// + public const string Scope = "urn:scope"; + + /// + /// Get the value sessionid + /// + public const string SessionId = "urn:altinn:sessionid"; + + /// + /// SystemUserUuid urn + /// + public const string SystemUserUuid = "urn:altinn:systemuser:uuid"; + + /// + /// xacml string that represents user + /// + public const string UserAttribute = "urn:altinn:userid"; + + /// + /// xacml string that represents person universally unique identifier + /// + public const string PersonUuidAttribute = "urn:altinn:person:uuid"; + + /// + /// xacml string that represents party + /// + public const string PartyAttribute = "urn:altinn:partyid"; + } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Extensions/AuthorizationBuilderExtensions.cs b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Extensions/AuthorizationBuilderExtensions.cs new file mode 100644 index 00000000..15b32e9c --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Extensions/AuthorizationBuilderExtensions.cs @@ -0,0 +1,60 @@ +using Altinn.Common.PEP.Authorization; +using Microsoft.AspNetCore.Authorization; + +namespace Altinn.Authorization.PEP.Extensions; + +/// +/// Provides extension methods for configuring Altinn-specific authorization policies. +/// +public static class AuthorizationBuilderExtensions +{ + /// + /// Adds a claim-based access policy to the authorization builder. + /// + /// The to which the policy will be added. + /// The name of the policy. + /// The claim type required by the policy. + /// The claim value required by the policy. + /// Thrown if , , or is null or empty. + /// The updated . + public static AuthorizationBuilder AddAltinnPEPClaimAccessPolicy(this AuthorizationBuilder builder, string name, string type, string value) + { + ArgumentException.ThrowIfNullOrEmpty(name, nameof(name)); + ArgumentException.ThrowIfNullOrEmpty(type, nameof(type)); + ArgumentException.ThrowIfNullOrEmpty(value, nameof(value)); + return builder.AddPolicy(name, policy => policy.Requirements.Add(new ClaimAccessRequirement(type, value))); + } + + /// + /// Adds a resource-based access policy to the authorization builder. + /// + /// The to which the policy will be added. + /// The name of the policy. + /// The identifier of the resource to protect. + /// The type of action permitted by the policy. + /// Thrown if , , or is null or empty. + /// The updated . + public static AuthorizationBuilder AddAltinnPEPResourceAccessPolicy(this AuthorizationBuilder builder, string name, string resourceId, string actionType) + { + ArgumentException.ThrowIfNullOrEmpty(name, nameof(name)); + ArgumentException.ThrowIfNullOrEmpty(resourceId, nameof(resourceId)); + ArgumentException.ThrowIfNullOrEmpty(actionType, nameof(actionType)); + return builder.AddPolicy(name, policy => policy.Requirements.Add(new ResourceAccessRequirement(resourceId, actionType))); + } + + /// + /// Adds a scope-based access policy to the authorization builder. + /// + /// The to which the policy will be added. + /// The name of the policy. + /// An array of scopes required by the policy. + /// Thrown if is null or empty or if contains null or empty values. + /// Thrown if is null. + /// The updated . + public static AuthorizationBuilder AddAltinnPEPScopePolicy(this AuthorizationBuilder builder, string name, params string[] scopes) + { + ArgumentException.ThrowIfNullOrEmpty(name, nameof(name)); + ArgumentNullException.ThrowIfNull(scopes, nameof(scopes)); + return builder.AddPolicy(name, policy => policy.Requirements.Add(new ScopeAccessRequirement(scopes))); + } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Extensions/ServiceCollectionExtensions.cs b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..50642eba --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,26 @@ +using Altinn.Common.PEP.Authorization; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Altinn.Authorization.PEP.Extensions; + +/// +/// Provides extension methods for registering Altinn-specific authorization services. +/// Must be called atleast once if registering using extensions methods from . +/// +public static class ServiceCollectionExtensions +{ + /// + /// Adds authorization handlers required for Altinn PEP (Policy Enforcement Point). + /// Registers handlers for claim-based, resource-based, and scope-based access control. + /// , and + /// + public static IServiceCollection AddAltinnPEP(this IServiceCollection services) + { + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + return services; + } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Helpers/DecisionHelper.cs b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Helpers/DecisionHelper.cs new file mode 100644 index 00000000..37a578c4 --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Helpers/DecisionHelper.cs @@ -0,0 +1,612 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text.Json; +using System.Text.RegularExpressions; +using Altinn.AccessManagement.Core.Models; +using Altinn.Authorization.ABAC.Xacml; +using Altinn.Authorization.ABAC.Xacml.JsonProfile; +using Altinn.Common.PEP.Authorization; +using Altinn.Common.PEP.Constants; +using Altinn.Common.PEP.Models; +using Altinn.Common.PEP.Utils; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +using static Altinn.Authorization.ABAC.Constants.XacmlConstants; + +namespace Altinn.Common.PEP.Helpers +{ + /// + /// Represents a collection of helper methods for creating a decision request + /// + public static class DecisionHelper + { + private const string ParamInstanceOwnerPartyId = "instanceOwnerPartyId"; + private const string ParamInstanceGuid = "instanceGuid"; + private const string ParamApp = "app"; + private const string ParamOrg = "org"; + private const string ParamAppId = "appId"; + private const string ParamParty = "party"; + private const string DefaultIssuer = "Altinn"; + private const string DefaultType = "string"; + private const string PersonHeaderTrigger = "person"; + private const string OrganizationHeaderTrigger = "organization"; + private const string PersonHeader = "Altinn-Party-SocialSecurityNumber"; + private const string OrganizationNumberHeader = "Altinn-Party-OrganizationNumber"; + private const string PolicyObligationMinAuthnLevel = "urn:altinn:minimum-authenticationlevel"; + private const string PolicyObligationMinAuthnLevelOrg = "urn:altinn:minimum-authenticationlevel-org"; + + /// + /// Create decision request based for policy decision point. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// Claims principal user. + /// Policy action type i.e. read, write, delete, instantiate. + /// Unique id of the party that is the owner of the instance. + /// Unique id to identify the instance. + /// The taskid. Will override contexthandler if present + /// The decision request. + public static XacmlJsonRequestRoot CreateDecisionRequest(string org, string app, ClaimsPrincipal user, string actionType, int instanceOwnerPartyId, Guid? instanceGuid, string taskid = null) + { + XacmlJsonRequest request = new XacmlJsonRequest(); + request.AccessSubject = new List(); + request.Action = new List(); + request.Resource = new List(); + + request.AccessSubject.Add(CreateSubjectCategory(user.Claims)); + request.Action.Add(CreateActionCategory(actionType)); + request.Resource.Add(CreateResourceCategory(org, app, instanceOwnerPartyId.ToString(), instanceGuid.ToString(), taskid)); + + XacmlJsonRequestRoot jsonRequest = new XacmlJsonRequestRoot() { Request = request }; + + return jsonRequest; + } + + /// + /// Create decision request based for policy decision point. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// Claims principal user. + /// Policy action type i.e. read, write, delete, instantiate. + /// The decision request. + public static XacmlJsonRequestRoot CreateDecisionRequest(string org, string app, ClaimsPrincipal user, string actionType) + { + XacmlJsonRequest request = new XacmlJsonRequest(); + request.AccessSubject = new List(); + request.Action = new List(); + request.Resource = new List(); + + request.AccessSubject.Add(CreateSubjectCategory(user.Claims)); + request.Action.Add(CreateActionCategory(actionType)); + request.Resource.Add(CreateResourceCategory(org, app, null, null, null)); + + XacmlJsonRequestRoot jsonRequest = new XacmlJsonRequestRoot() { Request = request }; + + return jsonRequest; + } + + /// + /// Create a new to represent a decision request. + /// + /// The current + /// The access requirements + /// The route data from a request. + /// A decision request + public static XacmlJsonRequestRoot CreateDecisionRequest(AuthorizationHandlerContext context, AppAccessRequirement requirement, RouteData routeData) + { + XacmlJsonRequest request = new XacmlJsonRequest(); + request.AccessSubject = new List(); + request.Action = new List(); + request.Resource = new List(); + + string instanceGuid = routeData.Values[ParamInstanceGuid] as string; + string app = routeData.Values[ParamApp] as string; + string org = routeData.Values[ParamOrg] as string; + string instanceOwnerPartyId = routeData.Values[ParamInstanceOwnerPartyId] as string; + + if (string.IsNullOrWhiteSpace(app) && string.IsNullOrWhiteSpace(org)) + { + string appId = routeData.Values[ParamAppId] as string; + if (appId != null) + { + org = appId.Split("/")[0]; + app = appId.Split("/")[1]; + } + } + + request.AccessSubject.Add(CreateSubjectCategory(context.User.Claims)); + request.Action.Add(CreateActionCategory(requirement.ActionType)); + request.Resource.Add(CreateResourceCategory(org, app, instanceOwnerPartyId, instanceGuid, null)); + + XacmlJsonRequestRoot jsonRequest = new XacmlJsonRequestRoot() { Request = request }; + + return jsonRequest; + } + + /// + /// Creates a decision request based on input + /// + /// + public static XacmlJsonRequestRoot CreateDecisionRequest(AuthorizationHandlerContext context, ResourceAccessRequirement requirement, RouteData routeData, IHeaderDictionary headers) + { + XacmlJsonRequest request = new XacmlJsonRequest(); + request.AccessSubject = new List(); + request.Action = new List(); + request.Resource = new List(); + + string party = routeData.Values[ParamParty] as string; + + request.AccessSubject.Add(CreateSubjectCategory(context.User.Claims)); + request.Action.Add(CreateActionCategory(requirement.ActionType)); + + int? partyIid = TryParsePartyId(party); + if (partyIid.HasValue) + { + request.Resource.Add(CreateResourceCategoryForResource(requirement.ResourceId, partyIid, null, null)); + } + else if (party.Equals(OrganizationHeaderTrigger) && headers.ContainsKey(OrganizationNumberHeader) && IDFormatDeterminator.IsValidOrganizationNumber(headers[OrganizationNumberHeader])) + { + request.Resource.Add(CreateResourceCategoryForResource(requirement.ResourceId, null, headers[OrganizationNumberHeader], null)); + } + else if (party.Equals(PersonHeaderTrigger) && headers.ContainsKey(PersonHeader) && IDFormatDeterminator.IsValidSSN(headers[PersonHeader])) + { + request.Resource.Add(CreateResourceCategoryForResource(requirement.ResourceId, null, null, headers[PersonHeader])); + } + else + { + throw new ArgumentException("invalid party " + party); + } + + XacmlJsonRequestRoot jsonRequest = new XacmlJsonRequestRoot() { Request = request }; + + return jsonRequest; + } + + /// + /// Create a new with a list of subject attributes based on the given claims. + /// + /// The list of claims + /// A populated subject category + public static XacmlJsonCategory CreateSubjectCategory(IEnumerable claims) + { + XacmlJsonCategory subjectAttributes = new XacmlJsonCategory(); + subjectAttributes.Attribute = CreateSubjectAttributes(claims); + + return subjectAttributes; + } + + /// + /// Create a new attribute of type Action with the given action type + /// + /// The action type + /// A value indicating whether the value should be included in the result. + /// The created category + public static XacmlJsonCategory CreateActionCategory(string actionType, bool includeResult = false) + { + XacmlJsonCategory actionAttributes = new XacmlJsonCategory(); + actionAttributes.Attribute = new List(); + actionAttributes.Attribute.Add(CreateXacmlJsonAttribute(MatchAttributeIdentifiers.ActionId, actionType, DefaultType, DefaultIssuer, includeResult)); + return actionAttributes; + } + + private static List CreateSubjectAttributes(IEnumerable claims) + { + List attributes = new List(); + + XacmlJsonAttribute userIdAttribute = null; + XacmlJsonAttribute personUuidAttribute = null; + XacmlJsonAttribute partyIdAttribute = null; + XacmlJsonAttribute resourceIdAttribute = null; + XacmlJsonAttribute legacyOrganizationNumberAttibute = null; + XacmlJsonAttribute organizationNumberAttribute = null; + XacmlJsonAttribute systemUserAttribute = null; + + // Mapping all claims on user to attributes + foreach (Claim claim in claims) + { + if (IsCamelCaseOrgnumberClaim(claim.Type)) + { + // Set by Altinn authentication this format + legacyOrganizationNumberAttibute = CreateXacmlJsonAttribute(AltinnXacmlUrns.OrganizationNumber, claim.Value, DefaultType, claim.Issuer); + organizationNumberAttribute = CreateXacmlJsonAttribute(AltinnXacmlUrns.OrganizationNumberAttribute, claim.Value, DefaultType, claim.Issuer); + } + else if (IsScopeClaim(claim.Type)) + { + attributes.Add(CreateXacmlJsonAttribute(AltinnXacmlUrns.Scope, claim.Value, DefaultType, claim.Issuer)); + } + else if (IsJtiClaim(claim.Type)) + { + attributes.Add(CreateXacmlJsonAttribute(AltinnXacmlUrns.SessionId, claim.Value, DefaultType, claim.Issuer)); + } + else if (IsSystemUserClaim(claim, out SystemUserClaim userClaim)) + { + systemUserAttribute = CreateXacmlJsonAttribute(AltinnXacmlUrns.SystemUserUuid, userClaim.Systemuser_id[0], DefaultType, claim.Issuer); + } + else if (IsUserIdClaim(claim.Type)) + { + userIdAttribute = CreateXacmlJsonAttribute(AltinnXacmlUrns.UserAttribute, claim.Value, DefaultType, claim.Issuer); + } + else if (IsPersonUuidClaim(claim.Type)) + { + personUuidAttribute = CreateXacmlJsonAttribute(AltinnXacmlUrns.PersonUuidAttribute, claim.Value, DefaultType, claim.Issuer); + } + else if (IsPartyIdClaim(claim.Type)) + { + partyIdAttribute = CreateXacmlJsonAttribute(AltinnXacmlUrns.PartyAttribute, claim.Value, DefaultType, claim.Issuer); + } + else if (IsResourceClaim(claim.Type)) + { + partyIdAttribute = CreateXacmlJsonAttribute(AltinnXacmlUrns.ResourceId, claim.Value, DefaultType, claim.Issuer); + } + else if (IsOrganizationNumberAttributeClaim(claim.Type)) + { + // If claimlist contains new format of orgnumber reset any old. To ensure there is not a mismatch + organizationNumberAttribute = CreateXacmlJsonAttribute(AltinnXacmlUrns.OrganizationNumberAttribute, claim.Value, DefaultType, claim.Issuer); + legacyOrganizationNumberAttibute = null; + } + else if (IsValidUrn(claim.Type)) + { + attributes.Add(CreateXacmlJsonAttribute(claim.Type, claim.Value, DefaultType, claim.Issuer)); + } + } + + // Adding only one of the subject attributes to make sure we dont have mismatching duplicates for PDP request that potentially could cause issues + if (personUuidAttribute != null) + { + attributes.Add(personUuidAttribute); + } + else if (userIdAttribute != null) + { + attributes.Add(userIdAttribute); + } + else if (partyIdAttribute != null) + { + attributes.Add(partyIdAttribute); + } + else if (resourceIdAttribute != null) + { + attributes.Add(resourceIdAttribute); + } + else if (systemUserAttribute != null) + { + // If we have a system user we only add that. No other attributes allowed by PDP + attributes.Clear(); + attributes.Add(systemUserAttribute); + } + else if (legacyOrganizationNumberAttibute != null) + { + // For legeacy we set both + attributes.Add(legacyOrganizationNumberAttibute); + attributes.Add(organizationNumberAttribute); + } + else if (organizationNumberAttribute != null) + { + attributes.Add(organizationNumberAttribute); + } + + return attributes; + } + + private static XacmlJsonCategory CreateResourceCategory(string org, string app, string instanceOwnerPartyId, string instanceGuid, string task, bool includeResult = false) + { + XacmlJsonCategory resourceCategory = new XacmlJsonCategory(); + resourceCategory.Attribute = new List(); + + if (!string.IsNullOrWhiteSpace(instanceOwnerPartyId)) + { + resourceCategory.Attribute.Add(CreateXacmlJsonAttribute(AltinnXacmlUrns.PartyId, instanceOwnerPartyId, DefaultType, DefaultIssuer, includeResult)); + } + + if (!string.IsNullOrWhiteSpace(instanceGuid) && !string.IsNullOrWhiteSpace(instanceOwnerPartyId)) + { + resourceCategory.Attribute.Add(CreateXacmlJsonAttribute(AltinnXacmlUrns.InstanceId, instanceOwnerPartyId + "/" + instanceGuid, DefaultType, DefaultIssuer, includeResult)); + } + + if (!string.IsNullOrWhiteSpace(org)) + { + resourceCategory.Attribute.Add(CreateXacmlJsonAttribute(AltinnXacmlUrns.OrgId, org, DefaultType, DefaultIssuer)); + } + + if (!string.IsNullOrWhiteSpace(app)) + { + resourceCategory.Attribute.Add(CreateXacmlJsonAttribute(AltinnXacmlUrns.AppId, app, DefaultType, DefaultIssuer)); + } + + if (!string.IsNullOrWhiteSpace(task)) + { + resourceCategory.Attribute.Add(CreateXacmlJsonAttribute(AltinnXacmlUrns.TaskId, task, DefaultType, DefaultIssuer)); + } + + return resourceCategory; + } + + private static XacmlJsonCategory CreateResourceCategoryForResource(string resourceid, int? partyId, string organizationnumber, string ssn, bool includeResult = false) + { + XacmlJsonCategory resourceCategory = new XacmlJsonCategory(); + resourceCategory.Attribute = new List(); + + if (partyId.HasValue) + { + resourceCategory.Attribute.Add(CreateXacmlJsonAttribute(AltinnXacmlUrns.PartyId, partyId.Value.ToString(), DefaultType, DefaultIssuer, includeResult)); + } + else if (!string.IsNullOrEmpty(organizationnumber)) + { + resourceCategory.Attribute.Add(CreateXacmlJsonAttribute(AltinnXacmlUrns.OrganizationNumber, organizationnumber, DefaultType, DefaultIssuer, includeResult)); + } + else if (!string.IsNullOrEmpty(ssn)) + { + resourceCategory.Attribute.Add(CreateXacmlJsonAttribute(AltinnXacmlUrns.Ssn, ssn, DefaultType, DefaultIssuer, includeResult)); + } + + if (!string.IsNullOrWhiteSpace(resourceid)) + { + resourceCategory.Attribute.Add(CreateXacmlJsonAttribute(AltinnXacmlUrns.ResourceId, resourceid, DefaultType, DefaultIssuer)); + } + + return resourceCategory; + } + + /// + /// Create a new with the given values. + /// + /// The attribute id + /// The attribute value + /// The datatype for the attribute value + /// The issuer + /// A value indicating whether the value should be included in the result. + /// A new created attribute + public static XacmlJsonAttribute CreateXacmlJsonAttribute(string attributeId, string value, string dataType, string issuer, bool includeResult = false) + { + XacmlJsonAttribute xacmlJsonAttribute = new XacmlJsonAttribute(); + + xacmlJsonAttribute.AttributeId = attributeId; + xacmlJsonAttribute.Value = value; + xacmlJsonAttribute.DataType = dataType; + xacmlJsonAttribute.Issuer = issuer; + xacmlJsonAttribute.IncludeInResult = includeResult; + + return xacmlJsonAttribute; + } + + private static bool IsValidUrn(string value) + { + return value.StartsWith("urn:", StringComparison.Ordinal); + } + + private static bool IsCamelCaseOrgnumberClaim(string name) + { + return name.Equals("urn:altinn:orgNumber"); + } + + private static bool IsScopeClaim(string name) + { + return name.Equals("scope"); + } + + private static bool IsUserIdClaim(string name) + { + return name.Equals(AltinnXacmlUrns.UserAttribute); + } + + private static bool IsPersonUuidClaim(string name) + { + return name.Equals(AltinnXacmlUrns.PersonUuidAttribute); + } + + private static bool IsPartyIdClaim(string name) + { + return name.Equals(AltinnXacmlUrns.PartyAttribute); + } + + private static bool IsResourceClaim(string name) + { + return name.Equals(AltinnXacmlUrns.ResourceId); + } + + private static bool IsOrganizationNumberAttributeClaim(string name) + { + // The new format of orgnumber + return name.Equals(AltinnXacmlUrns.OrganizationNumberAttribute); + } + + private static bool IsJtiClaim(string name) + { + return name.Equals("jti"); + } + + private static bool IsSystemUserClaim(Claim claim, out SystemUserClaim userClaim) + { + if (claim.Type.Equals("authorization_details")) + { + userClaim = JsonSerializer.Deserialize(claim.Value); + if (userClaim?.Systemuser_id != null && userClaim.Systemuser_id.Count > 0) + { + return true; + } + + return false; + } + else + { + userClaim = null; + return false; + } + } + + /// + /// Validate the response from PDP + /// + /// The response to validate + /// The + /// true or false, valid or not + public static bool ValidatePdpDecision(List results, ClaimsPrincipal user) + { + ArgumentNullException.ThrowIfNull(results, nameof(results)); + ArgumentNullException.ThrowIfNull(user, nameof(user)); + + // We request one thing and then only want one result + if (results.Count != 1) + { + return false; + } + + return ValidateDecisionResult(results.First(), user); + } + + /// + /// Validate the response from PDP + /// + /// The response to validate + /// The + /// The result of the validation + public static EnforcementResult ValidatePdpDecisionDetailed(List results, ClaimsPrincipal user) + { + ArgumentNullException.ThrowIfNull(results, nameof(results)); + ArgumentNullException.ThrowIfNull(user, nameof(user)); + + // We request one thing and then only want one result + if (results.Count != 1) + { + return new EnforcementResult() { Authorized = false }; + } + + return ValidateDecisionResultDetailed(results.First(), user); + } + + /// + /// Validate the response from PDP + /// + /// The response to validate + /// The + /// true or false, valid or not + public static bool ValidateDecisionResult(XacmlJsonResult result, ClaimsPrincipal user) + { + // Checks that the result is nothing else than "permit" + if (!result.Decision.Equals(XacmlContextDecision.Permit.ToString())) + { + return false; + } + + // Checks if the result contains obligation + if (result.Obligations != null) + { + List obligationList = result.Obligations; + XacmlJsonAttributeAssignment attributeMinLvAuth = GetObligation(PolicyObligationMinAuthnLevel, obligationList); + + // Checks if the obligation contains a minimum authentication level attribute + if (attributeMinLvAuth != null) + { + string minAuthenticationLevel = attributeMinLvAuth.Value; + string usersAuthenticationLevel = user.Claims.FirstOrDefault(c => c.Type.Equals("urn:altinn:authlevel")).Value; + + // Checks that the user meets the minimum authentication level + if (Convert.ToInt32(usersAuthenticationLevel) < Convert.ToInt32(minAuthenticationLevel)) + { + if (user.Claims.FirstOrDefault(c => c.Type.Equals("urn:altinn:org")) != null) + { + XacmlJsonAttributeAssignment attributeMinLvAuthOrg = GetObligation(PolicyObligationMinAuthnLevelOrg, obligationList); + if (attributeMinLvAuthOrg != null) + { + if (Convert.ToInt32(usersAuthenticationLevel) >= Convert.ToInt32(attributeMinLvAuthOrg.Value)) + { + return true; + } + } + } + + return false; + } + } + } + + return true; + } + + /// + /// Validate the response from PDP + /// + /// The response to validate + /// The + /// The result of the validation + public static EnforcementResult ValidateDecisionResultDetailed(XacmlJsonResult result, ClaimsPrincipal user) + { + // Checks that the result is nothing else than "permit" + if (!result.Decision.Equals(XacmlContextDecision.Permit.ToString())) + { + return new EnforcementResult() { Authorized = false }; + } + + // Checks if the result contains obligation + if (result.Obligations != null) + { + List obligationList = result.Obligations; + XacmlJsonAttributeAssignment attributeMinLvAuth = GetObligation(PolicyObligationMinAuthnLevel, obligationList); + + // Checks if the obligation contains a minimum authentication level attribute + if (attributeMinLvAuth != null) + { + string minAuthenticationLevel = attributeMinLvAuth.Value; + string usersAuthenticationLevel = user.Claims.FirstOrDefault(c => c.Type.Equals("urn:altinn:authlevel")).Value; + + // Checks that the user meets the minimum authentication level + if (Convert.ToInt32(usersAuthenticationLevel) < Convert.ToInt32(minAuthenticationLevel)) + { + if (user.Claims.FirstOrDefault(c => c.Type.Equals("urn:altinn:org")) != null) + { + XacmlJsonAttributeAssignment attributeMinLvAuthOrg = GetObligation(PolicyObligationMinAuthnLevelOrg, obligationList); + if (attributeMinLvAuthOrg != null) + { + if (Convert.ToInt32(usersAuthenticationLevel) >= Convert.ToInt32(attributeMinLvAuthOrg.Value)) + { + return new EnforcementResult() { Authorized = true }; + } + + minAuthenticationLevel = attributeMinLvAuthOrg.Value; + } + } + + return new EnforcementResult() + { + Authorized = false, + FailedObligations = new Dictionary() + { + { AltinnObligations.RequiredAuthenticationLevel, minAuthenticationLevel } + } + }; + } + } + } + + return new EnforcementResult() { Authorized = true }; + } + + private static XacmlJsonAttributeAssignment GetObligation(string category, List obligations) + { + foreach (XacmlJsonObligationOrAdvice obligation in obligations) + { + XacmlJsonAttributeAssignment assignment = obligation.AttributeAssignment.FirstOrDefault(a => a.Category.Equals(category)); + if (assignment != null) + { + return assignment; + } + } + + return null; + } + + private static int? TryParsePartyId(string party) + { + if (int.TryParse(party, out var partyId)) + { + return partyId; + } + + return null; + } + } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Implementation/PDPAppSI.cs b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Implementation/PDPAppSI.cs new file mode 100644 index 00000000..feeb3e1a --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Implementation/PDPAppSI.cs @@ -0,0 +1,62 @@ +using System.Security.Claims; +using System.Text.Json; +using Altinn.Authorization.ABAC.Xacml.JsonProfile; +using Altinn.Common.PEP.Clients; +using Altinn.Common.PEP.Helpers; +using Altinn.Common.PEP.Interfaces; +using Microsoft.Extensions.Logging; + +namespace Altinn.Common.PEP.Implementation +{ + /// + /// App implementation of the authorization service where the app uses the Altinn platform api. + /// + public class PDPAppSI : IPDP + { + private readonly ILogger _logger; + private readonly AuthorizationApiClient _authorizationApiClient; + + /// + /// Initializes a new instance of the class + /// + /// the handler for logger service + /// A typed Http client accessor + public PDPAppSI(ILogger logger, AuthorizationApiClient authorizationApiClient) + { + _logger = logger; + _authorizationApiClient = authorizationApiClient; + } + + /// + public async Task GetDecisionForRequest(XacmlJsonRequestRoot xacmlJsonRequest) + { + XacmlJsonResponse xacmlJsonResponse = null; + + try + { + xacmlJsonResponse = await _authorizationApiClient.AuthorizeRequest(xacmlJsonRequest); + } + catch (Exception e) + { + _logger.LogError("Unable to retrieve Xacml Json response. An error occured {message}", e.Message); + } + + return xacmlJsonResponse; + } + + /// + public async Task GetDecisionForUnvalidateRequest(XacmlJsonRequestRoot xacmlJsonRequest, ClaimsPrincipal user) + { + XacmlJsonResponse response = await GetDecisionForRequest(xacmlJsonRequest); + + if (response?.Response == null) + { + throw new ArgumentNullException("response"); + } + + _logger.LogInformation("// Altinn PEP // PDPAppSI // Request sent to platform authorization: {xacmlJsonRequest}", JsonSerializer.Serialize(xacmlJsonRequest)); + + return DecisionHelper.ValidatePdpDecision(response.Response, user); + } + } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Interfaces/IPDP.cs b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Interfaces/IPDP.cs new file mode 100644 index 00000000..b633b567 --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Interfaces/IPDP.cs @@ -0,0 +1,28 @@ +using System.Security.Claims; +using System.Threading.Tasks; + +using Altinn.Authorization.ABAC.Xacml.JsonProfile; + +namespace Altinn.Common.PEP.Interfaces +{ + /// + /// This interface describes the minimum set of methods for any implementation of a Policy Decision Point. + /// + public interface IPDP + { + /// + /// Sends in a request and get response with result of the request + /// + /// The Xacml Json Request + /// The Xacml Json response contains the result of the request + Task GetDecisionForRequest(XacmlJsonRequestRoot xacmlJsonRequest); + + /// + /// Change this to a better one??????? + /// + /// The Xacml Json Request + /// The claims principal + /// Returns true if request is permitted and false if not + Task GetDecisionForUnvalidateRequest(XacmlJsonRequestRoot xacmlJsonRequest, ClaimsPrincipal user); + } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Models/EnforcementResult.cs b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Models/EnforcementResult.cs new file mode 100644 index 00000000..9b3bec68 --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Models/EnforcementResult.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Altinn.Common.PEP.Models +{ + /// + /// Represents the result of an authorization enforcement + /// + public class EnforcementResult + { + /// + /// Value indicating whether enforcement result shows authorized or not + /// + public bool Authorized { get; set; } + + /// + /// Collection of obligations that did not pass validation + /// + public Dictionary FailedObligations { get; set; } + } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Models/IDFormat.cs b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Models/IDFormat.cs new file mode 100644 index 00000000..1582e7a7 --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Models/IDFormat.cs @@ -0,0 +1,32 @@ +namespace Altinn.Common.PEP.Models +{ + /// + /// IDFormat is used to communicate the format of a suspected string of either OrgNr or SSN -type. + /// + /// + /// Author: Ole Hansen + /// Date: 29/04/2009 + /// + public enum IDFormat : int + { + /// + /// IDFormat is unknown + /// + Unknown = 0, + + /// + /// IDFormat is SSN + /// + SSN = 1, + + /// + /// IDFormat is OrgNr + /// + OrgNr = 2, + + /// + /// IDFormat is Self Identified User + /// + UserName = 3, + } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Models/OrgClaim.cs b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Models/OrgClaim.cs new file mode 100644 index 00000000..08bf7142 --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Models/OrgClaim.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; + +namespace Altinn.AccessManagement.Core.Models +{ + /// + /// Organization claim matching structure from maskinporten + /// + public class OrgClaim + { + /// + /// The authority that defines organization numbers. + /// + [JsonPropertyName("authority")] + public string Authority => "iso6523-actorid-upis"; + + /// + /// The orgclaim id + /// + [JsonPropertyName("ID")] + public string ID { get; set; } + } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Models/SystemUserClaim.cs b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Models/SystemUserClaim.cs new file mode 100644 index 00000000..11728af5 --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Models/SystemUserClaim.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Altinn.AccessManagement.Core.Models +{ + /// + /// System User claim matching structure from maskinporten + /// + public class SystemUserClaim + { + /// + /// The type + /// + [JsonPropertyName("type")] + public string Type => "urn:altinn:systemuser"; + + /// + /// The organization that created the system user + /// + [JsonPropertyName("systemuser_org")] + public OrgClaim Systemuser_org { get; set; } + + /// + /// The system user id + /// + [JsonPropertyName("systemuser_id")] + public List Systemuser_id { get; set; } + + /// + /// The system id + /// + [JsonPropertyName("system_id")] + public string System_id { get; set; } + } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Utils/IDFormatDeterminator.cs b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Utils/IDFormatDeterminator.cs new file mode 100644 index 00000000..db0ed197 --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/src/Altinn.Authorization.PEP/Utils/IDFormatDeterminator.cs @@ -0,0 +1,185 @@ +using System.Text.RegularExpressions; +using Altinn.Common.PEP.Models; + +namespace Altinn.Common.PEP.Utils +{ + /// + /// This class is used to get the ID-type (if any) from a string input. + /// + public class IDFormatDeterminator + { + /// + /// Method to get the IDFormat from an arbitrary string. + /// Non-numeric strings will yield an IDFormat.Unknown + /// E.g. "000 000 000", "010170 00000" or "01.01.70 00000" + /// Leading and trailing spaces are trimmed off. + /// + /// + /// The string to test, 9 or 11 consecutive digits (e.g. 000000000 or 00000000000) + /// or username of self identified user + /// + /// + /// An IDFormat corresponding to the input + /// + public static IDFormat DetermineIDFormat(string input) + { + if (input == null) + { + return IDFormat.Unknown; + } + + input = input.Trim(); + + if (IsValidOrganizationNumber(input)) + { + return IDFormat.OrgNr; + } + else if (IsValidSSN(input)) + { + return IDFormat.SSN; + } + else if (IsValidUserName(input)) + { + return IDFormat.UserName; + } + else + { + return IDFormat.Unknown; + } + } + + /// + /// Validates that a given organization number is valid. + /// + /// + /// Organization number to validate + /// + /// + /// true if valid, false otherwise. + /// + /// + /// Validates length, numeric and modulus 11. + /// + public static bool IsValidOrganizationNumber(string orgNo) + { + int[] weight = { 3, 2, 7, 6, 5, 4, 3, 2 }; + + // Validation only done for 9 digit numbers + if (orgNo.Length == 9) + { + try + { + int currentDigit = 0; + int sum = 0; + for (int i = 0; i < orgNo.Length - 1; i++) + { + currentDigit = int.Parse(orgNo.Substring(i, 1)); + sum += currentDigit * weight[i]; + } + + int ctrlDigit = 11 - (sum % 11); + if (ctrlDigit == 11) + { + ctrlDigit = 0; + } + + return int.Parse(orgNo.Substring(orgNo.Length - 1)) == ctrlDigit; + } + catch + { + return false; + } + } + + return false; + } + + /// + /// Validates that a given social security number is valid. + /// + /// + /// Social security number to validate + /// + /// + /// true if valid, false otherwise. + /// + /// + /// Validates length, numeric and modulus 11. + /// + public static bool IsValidSSN(string ssnNo) + { + int[] weightDigit10 = { 3, 7, 6, 1, 8, 9, 4, 5, 2 }; + int[] weightDigit11 = { 5, 4, 3, 2, 7, 6, 5, 4, 3, 2 }; + + // Validation only done for 11 digit numbers + if (ssnNo.Length == 11) + { + try + { + int currentDigit = 0; + int sumCtrlDigit10 = 0; + int sumCtrlDigit11 = 0; + int ctrlDigit10 = -1; + int ctrlDigit11 = -1; + + // Calculate control digits + for (int i = 0; i < 9; i++) + { + currentDigit = int.Parse(ssnNo.Substring(i, 1)); + sumCtrlDigit10 += currentDigit * weightDigit10[i]; + sumCtrlDigit11 += currentDigit * weightDigit11[i]; + } + + ctrlDigit10 = 11 - (sumCtrlDigit10 % 11); + if (ctrlDigit10 == 11) + { + ctrlDigit10 = 0; + } + + sumCtrlDigit11 += ctrlDigit10 * weightDigit11[9]; + ctrlDigit11 = 11 - (sumCtrlDigit11 % 11); + if (ctrlDigit11 == 11) + { + ctrlDigit11 = 0; + } + + // Validate control digits in ssn + bool digit10Valid = ctrlDigit10 == int.Parse(ssnNo.Substring(9, 1)); + bool digit11Valid = ctrlDigit11 == int.Parse(ssnNo.Substring(10, 1)); + return digit10Valid && digit11Valid; + } + catch + { + return false; + } + } + + return false; + } + + /// + /// Validates that a given user name is valid. + /// + /// + /// User name to validate + /// + /// + /// true if valid, false otherwise. + /// + /// + /// Validates username with a regular expression + /// + public static bool IsValidUserName(string siUsername) + { + string usernameRegex = @"^(?=.*[a-zA-Z._@-])[a-zA-Z0-9._@-]+$"; + if (Regex.IsMatch(siUsername, usernameRegex)) + { + return true; + } + else + { + return false; + } + } + } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/src/Directory.Build.props b/src/pkgs/Altinn.Authorization.PEP/src/Directory.Build.props new file mode 100644 index 00000000..80de5834 --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/src/Directory.Build.props @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/pkgs/Altinn.Authorization.PEP/tests/Altinn.Authorization.PEP.Tests/Altinn.Authorization.PEP.Tests.csproj b/src/pkgs/Altinn.Authorization.PEP/tests/Altinn.Authorization.PEP.Tests/Altinn.Authorization.PEP.Tests.csproj new file mode 100644 index 00000000..40916a6e --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/tests/Altinn.Authorization.PEP.Tests/Altinn.Authorization.PEP.Tests.csproj @@ -0,0 +1,16 @@ + + + + false + + + + + + + + + + + \ No newline at end of file diff --git a/src/pkgs/Altinn.Authorization.PEP/tests/Altinn.Authorization.PEP.Tests/AppAccessHandlerTest.cs b/src/pkgs/Altinn.Authorization.PEP/tests/Altinn.Authorization.PEP.Tests/AppAccessHandlerTest.cs new file mode 100644 index 00000000..2ad75b38 --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/tests/Altinn.Authorization.PEP.Tests/AppAccessHandlerTest.cs @@ -0,0 +1,380 @@ +using System; +using System.Collections.Generic; +using System.Reflection.Metadata; +using System.Security.Claims; +using System.Text.Json; +using System.Threading.Tasks; +using Altinn.AccessManagement.Core.Models; +using Altinn.Authorization.ABAC.Xacml; +using Altinn.Authorization.ABAC.Xacml.JsonProfile; +using Altinn.Common.PEP.Configuration; +using Altinn.Common.PEP.Interfaces; + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Altinn.Common.PEP.Authorization +{ + public class AppAccessHandlerTest + { + private readonly Mock _httpContextAccessorMock; + private readonly Mock _pdpMock; + private readonly IOptions _generalSettings; + private readonly AppAccessHandler _aah; + + public AppAccessHandlerTest() + { + _httpContextAccessorMock = new Mock(); + _pdpMock = new Mock(); + _generalSettings = Options.Create(new PepSettings()); + _aah = new AppAccessHandler(_httpContextAccessorMock.Object, _pdpMock.Object, new Mock>().Object); + } + + /// + /// Test case: Send request and get response that fulfills all requirements + /// Expected: Context will succeed + /// + [Fact] + public async Task HandleRequirementAsync_TC01Async() + { + // Arrange + AuthorizationHandlerContext context = CreateAuthorizationHandlerContext(); + _httpContextAccessorMock.Setup(h => h.HttpContext).Returns(CreateHttpContext()); + XacmlJsonResponse response = CreateResponse(XacmlContextDecision.Permit.ToString()); + _pdpMock.Setup(a => a.GetDecisionForRequest(It.IsAny())).Returns(Task.FromResult(response)); + + // Act + await _aah.HandleAsync(context); + + // Assert + Assert.True(context.HasSucceeded); + Assert.False(context.HasFailed); + } + + /// + /// Test case: Send request and get respons with result deny + /// Expected: Context will fail + /// + [Fact] + public async Task HandleRequirementAsync_TC02Async() + { + // Arrange + AuthorizationHandlerContext context = CreateAuthorizationHandlerContext(); + _httpContextAccessorMock.Setup(h => h.HttpContext).Returns(CreateHttpContext()); + XacmlJsonResponse response = CreateResponse(XacmlContextDecision.Deny.ToString()); + _pdpMock.Setup(a => a.GetDecisionForRequest(It.IsAny())).Returns(Task.FromResult(response)); + + // Act + await _aah.HandleAsync(context); + + // Assert + Assert.False(context.HasSucceeded); + Assert.True(context.HasFailed); + } + + /// + /// Test case: Send request and get respons with two results + /// Expected: Context will fail + /// + [Fact] + public async Task HandleRequirementAsync_TC03Async() + { + // Arrange + AuthorizationHandlerContext context = CreateAuthorizationHandlerContext(); + _httpContextAccessorMock.Setup(h => h.HttpContext).Returns(CreateHttpContext()); + XacmlJsonResponse response = CreateResponse(XacmlContextDecision.Permit.ToString()); + + // Add extra result + response.Response.Add(new XacmlJsonResult()); + _pdpMock.Setup(a => a.GetDecisionForRequest(It.IsAny())).Returns(Task.FromResult(response)); + + // Act + await _aah.HandleAsync(context); + + // Assert + Assert.False(context.HasSucceeded); + Assert.True(context.HasFailed); + } + + /// + /// Test case: Send request and get respons with obligation that contains min authentication level that the user meets + /// Expected: context will succeed + /// + [Fact] + public async Task HandleRequirementAsync_TC04Async() + { + // Arrange + AuthorizationHandlerContext context = CreateAuthorizationHandlerContext(); + _httpContextAccessorMock.Setup(h => h.HttpContext).Returns(CreateHttpContext()); + XacmlJsonResponse response = CreateResponse(XacmlContextDecision.Permit.ToString()); + AddObligationWithMinAuthLv(response, "2"); + _pdpMock.Setup(a => a.GetDecisionForRequest(It.IsAny())).Returns(Task.FromResult(response)); + + // Act + await _aah.HandleAsync(context); + + // Assert + Assert.True(context.HasSucceeded); + Assert.False(context.HasFailed); + } + + /// + /// Test case: Send request and get respons with obligation that contains min authentication level that the user do not meet + /// Expected: context will fail + /// + [Fact] + public async Task HandleRequirementAsync_TC05Async() + { + // Arrange + AuthorizationHandlerContext context = CreateAuthorizationHandlerContext(); + _httpContextAccessorMock.Setup(h => h.HttpContext).Returns(CreateHttpContext()); + XacmlJsonResponse response = CreateResponse(XacmlContextDecision.Permit.ToString()); + AddObligationWithMinAuthLv(response, "3"); + _pdpMock.Setup(a => a.GetDecisionForRequest(It.IsAny())).Returns(Task.FromResult(response)); + + // Act + await _aah.HandleAsync(context); + + // Assert + Assert.False(context.HasSucceeded); + Assert.True(context.HasFailed); + } + + /// + /// Test case: Send request and get response that is null + /// Expected: Context will fail + /// + [Fact] + public async Task HandleRequirementAsync_TC06Async() + { + // Arrange + AuthorizationHandlerContext context = CreateAuthorizationHandlerContext(); + _httpContextAccessorMock.Setup(h => h.HttpContext).Returns(CreateHttpContext()); + XacmlJsonResponse response = null; + _pdpMock.Setup(a => a.GetDecisionForRequest(It.IsAny())).Returns(Task.FromResult(response)); + + // Act & Assert + await Assert.ThrowsAsync(() => _aah.HandleAsync(context)); + } + + /// + /// Test case: Send request and get response with a result list that is null + /// Expected: Context will fail + /// + [Fact] + public async Task HandleRequirementAsync_TC07Async() + { + // Arrange + AuthorizationHandlerContext context = CreateAuthorizationHandlerContext(); + _httpContextAccessorMock.Setup(h => h.HttpContext).Returns(CreateHttpContext()); + + // Create response with a result list that is null + XacmlJsonResponse response = new XacmlJsonResponse(); + response.Response = null; + _pdpMock.Setup(a => a.GetDecisionForRequest(It.IsAny())).Returns(Task.FromResult(response)); + + // Act & Assert + await Assert.ThrowsAsync(() => _aah.HandleAsync(context)); + } + + /// + /// Test case: Send request verify if the ipaddress from the x-forwarded-for header is received + /// Expected: XForwardedForHeader proeprty in request receives the ipaddress from the header + /// + [Fact] + public async Task HandleRequirementAsync_TC08Async() + { + // Arrange + AuthorizationHandlerContext context = CreateAuthorizationHandlerContext(); + string ipaddress = "18.203.138.153"; + _httpContextAccessorMock.Setup(h => h.HttpContext).Returns(CreateHttpContext(ipaddress)); + XacmlJsonResponse response = CreateResponse(XacmlContextDecision.Permit.ToString()); + AddObligationWithMinAuthLv(response, "2"); + + // verify + _pdpMock.Setup(a => a.GetDecisionForRequest(It.IsAny())).Returns(Task.FromResult(response)); + + // Act + await _aah.HandleAsync(context); + } + + /// + /// Test case: Send request and get response that fulfills all requirements with system user + /// Expected: Context will succeed + /// + [Fact] + public async Task HandleRequirementAsync_TC09Async() + { + // Arrange + AuthorizationHandlerContext context = CreateAuthorizationHandlerContextSystemUser(); + _httpContextAccessorMock.Setup(h => h.HttpContext).Returns(CreateHttpContext()); + XacmlJsonResponse response = CreateResponse(XacmlContextDecision.Permit.ToString()); + _pdpMock.Setup(a => a.GetDecisionForRequest(It.IsAny())).Returns(Task.FromResult(response)); + + // Act + await _aah.HandleAsync(context); + + // Assert + Assert.True(context.HasSucceeded); + Assert.False(context.HasFailed); + } + + /// + /// Test case: Send request and get response that fulfills all requirements with app user + /// Expected: Context will succeed + /// + [Fact] + public async Task HandleRequirementAsync_TC10Async() + { + // Arrange + AuthorizationHandlerContext context = CreateAuthorizationHandlerContextAppUser("app_skd_flyttemelding"); + _httpContextAccessorMock.Setup(h => h.HttpContext).Returns(CreateHttpContext()); + XacmlJsonResponse response = CreateResponse(XacmlContextDecision.Permit.ToString()); + _pdpMock.Setup(a => a.GetDecisionForRequest(It.IsAny())).Returns(Task.FromResult(response)); + + // Act + await _aah.HandleAsync(context); + + // Assert + Assert.True(context.HasSucceeded); + Assert.False(context.HasFailed); + } + + private ClaimsPrincipal CreateUser() + { + // Create the user + List claims = new List(); + + // type, value, valuetype, issuer + claims.Add(new Claim("urn:name", "Ola", "string", "org")); + claims.Add(new Claim("urn:altinn:authlevel", "2", "string", "org")); + + ClaimsPrincipal user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + return user; + } + + private ClaimsPrincipal CreateSystemUser() + { + SystemUserClaim systemUserClaim = new SystemUserClaim + { + Systemuser_id = new List() { "996a686f-d24d-4d92-a92e-5b3cec4a8cf7" }, + Systemuser_org = new OrgClaim() { ID = "myOrg" }, + System_id = "the_matrix" + }; + + List claims = new List(); + claims.Add(new Claim("authorization_details", JsonSerializer.Serialize(systemUserClaim), "string", "org")); + ClaimsPrincipal user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + return user; + } + + private ClaimsPrincipal CreateAppUser(string appId) + { + List claims = new List(); + claims.Add(new Claim("urn:altinn:resource", appId, "string", "org")); + ClaimsPrincipal user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + return user; + } + + private HttpContext CreateHttpContext() + { + HttpContext httpContext = new DefaultHttpContext(); + httpContext.Request.RouteValues.Add("org", "myOrg"); + httpContext.Request.RouteValues.Add("app", "myApp"); + httpContext.Request.RouteValues.Add("instanceGuid", "asdfg"); + httpContext.Request.RouteValues.Add("InstanceOwnerId", "1000"); + + return httpContext; + } + + private HttpContext CreateHttpContext(string xForwardedForHeader) + { + HttpContext httpContext = new DefaultHttpContext(); + httpContext.Request.RouteValues.Add("org", "myOrg"); + httpContext.Request.RouteValues.Add("app", "myApp"); + httpContext.Request.RouteValues.Add("instanceGuid", "asdfg"); + httpContext.Request.RouteValues.Add("InstanceOwnerId", "1000"); + + if (!string.IsNullOrEmpty(xForwardedForHeader)) + { + httpContext.Request.Headers.Add("x-forwarded-for", xForwardedForHeader); + } + + return httpContext; + } + + private XacmlJsonResponse CreateResponse(string decision) + { + // Create response + XacmlJsonResponse response = new XacmlJsonResponse(); + response.Response = new List(); + + // Set result to premit + XacmlJsonResult result = new XacmlJsonResult(); + result.Decision = decision; + response.Response.Add(result); + + return response; + } + + private XacmlJsonResponse AddObligationWithMinAuthLv(XacmlJsonResponse response, string minAuthLv) + { + // Add obligation to result with a minimum authentication level attribute + XacmlJsonResult result = response.Response[0]; + XacmlJsonObligationOrAdvice obligation = new XacmlJsonObligationOrAdvice(); + obligation.AttributeAssignment = new List(); + XacmlJsonAttributeAssignment authenticationAttribute = new XacmlJsonAttributeAssignment() + { + Category = "urn:altinn:minimum-authenticationlevel", + Value = minAuthLv + }; + obligation.AttributeAssignment.Add(authenticationAttribute); + result.Obligations = new List(); + result.Obligations.Add(obligation); + + return response; + } + + private AuthorizationHandlerContext CreateAuthorizationHandlerContext() + { + AppAccessRequirement requirement = new AppAccessRequirement("read"); + ClaimsPrincipal user = CreateUser(); + Document resource = default(Document); + AuthorizationHandlerContext context = new AuthorizationHandlerContext( + new[] { requirement }, + user, + resource); + return context; + } + + private AuthorizationHandlerContext CreateAuthorizationHandlerContextSystemUser() + { + AppAccessRequirement requirement = new AppAccessRequirement("read"); + ClaimsPrincipal user = CreateSystemUser(); + Document resource = default(Document); + AuthorizationHandlerContext context = new AuthorizationHandlerContext( + new[] { requirement }, + user, + resource); + return context; + } + + private AuthorizationHandlerContext CreateAuthorizationHandlerContextAppUser(string appId) + { + AppAccessRequirement requirement = new AppAccessRequirement("read"); + ClaimsPrincipal user = CreateAppUser(appId); + Document resource = default(Document); + AuthorizationHandlerContext context = new AuthorizationHandlerContext( + new[] { requirement }, + user, + resource); + return context; + } + } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/tests/Altinn.Authorization.PEP.Tests/DecisionHelperTest.cs b/src/pkgs/Altinn.Authorization.PEP/tests/Altinn.Authorization.PEP.Tests/DecisionHelperTest.cs new file mode 100644 index 00000000..56c81e60 --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/tests/Altinn.Authorization.PEP.Tests/DecisionHelperTest.cs @@ -0,0 +1,457 @@ +using System; +using System.Collections.Generic; +using System.Security.Claims; + +using Altinn.Authorization.ABAC.Xacml; +using Altinn.Authorization.ABAC.Xacml.JsonProfile; +using Altinn.Common.PEP.Constants; +using Altinn.Common.PEP.Helpers; +using Altinn.Common.PEP.Models; + +using Xunit; + +namespace UnitTests +{ + public class DecisionHelperTest + { + private const string Org = "Altinn"; + private const string App = "App"; + private const string ActionType = "read"; + private const int PartyId = 1000; + + /// + /// Test case: Send attributes and creates request out of it + /// Expected: All values sent in will be created to attributes + /// + [Fact] + public void CreateXacmlJsonRequest_TC01() + { + // Arrange & Act + XacmlJsonRequestRoot requestRoot = DecisionHelper.CreateDecisionRequest(Org, App, CreateUserClaims(false), ActionType, PartyId, null, null); + XacmlJsonRequest request = requestRoot.Request; + + // Assert + Assert.Equal(2, request.AccessSubject[0].Attribute.Count); + Assert.Single(request.Action[0].Attribute); + Assert.Equal(3, request.Resource[0].Attribute.Count); + } + + /// + /// Test case: Send attributes and creates request out of it + /// Expected: Only valid urn values sent in will be created to attributes + /// + [Fact] + public void CreateXacmlJsonRequest_TC02() + { + // Arrange & Act + XacmlJsonRequestRoot requestRoot = DecisionHelper.CreateDecisionRequest(Org, App, CreateUserClaims(true), ActionType, PartyId, null, null); + XacmlJsonRequest request = requestRoot.Request; + + // Assert + Assert.Equal(2, request.AccessSubject[0].Attribute.Count); + Assert.Single(request.Action[0].Attribute); + Assert.Equal(3, request.Resource[0].Attribute.Count); + } + + /// + /// Test case: Send attributes and creates request out of it + /// Expected: Only valid urn, scope and orgnumber with correct values sent in will be created to attributes + /// + [Fact] + public void CreateXacmlJsonRequest_TC03() + { + // Arrange & Act + XacmlJsonRequestRoot requestRoot = DecisionHelper.CreateDecisionRequest(Org, App, CreateMaskinportenClaims("12313", "altinn.master"), ActionType); + XacmlJsonRequest request = requestRoot.Request; + + // Assert + Assert.Equal(4, request.AccessSubject[0].Attribute.Count); + Assert.Single(request.Action[0].Attribute); + Assert.Equal(2, request.Resource[0].Attribute.Count); + } + + /// + /// Test case: Send attributes and creates request out of it + /// Expected: All values sent in will be created to attributes + /// + [Fact] + public void CreateXacmlJsonRequest_TC04() + { + // Arrange & Act + XacmlJsonRequestRoot requestRoot = DecisionHelper.CreateDecisionRequest(Org, App, CreateUserClaims(false), ActionType); + XacmlJsonRequest request = requestRoot.Request; + + // Assert + Assert.Equal(2, request.AccessSubject[0].Attribute.Count); + Assert.Single(request.Action[0].Attribute); + Assert.Equal(2, request.Resource[0].Attribute.Count); + } + + /// + /// Test case: Send attributes and creates request out of it + /// Expected: Only valid urn values sent in will be created to attributes + /// + [Fact] + public void CreateXacmlJsonRequest_TC05() + { + // Arrange & Act + XacmlJsonRequestRoot requestRoot = DecisionHelper.CreateDecisionRequest(Org, App, CreateUserClaims(true), ActionType); + XacmlJsonRequest request = requestRoot.Request; + + // Assert + Assert.Equal(2, request.AccessSubject[0].Attribute.Count); + Assert.Single(request.Action[0].Attribute); + Assert.Equal(2, request.Resource[0].Attribute.Count); + } + + /// + /// Test case: Send attributes and creates request out of it + /// Expected: Only valid urn, scope and orgnumber with correct values sent in will be created to attributes + /// + [Fact] + public void CreateXacmlJsonRequest_TC06() + { + // Arrange & Act + XacmlJsonRequestRoot requestRoot = DecisionHelper.CreateDecisionRequest(Org, App, CreateMaskinportenClaims("12313", "altinn.master"), ActionType, PartyId, null, null); + XacmlJsonRequest request = requestRoot.Request; + + // Assert + Assert.Equal(4, request.AccessSubject[0].Attribute.Count); + Assert.Single(request.Action[0].Attribute); + Assert.Equal(3, request.Resource[0].Attribute.Count); + } + + /// + /// Test case: Response with result permit + /// Expected: Returns true + /// + [Fact] + public void ValidatePdpDecision_TC01() + { + // Arrange + XacmlJsonResponse response = new XacmlJsonResponse(); + response.Response = new List(); + XacmlJsonResult xacmlJsonResult = new XacmlJsonResult(); + xacmlJsonResult.Decision = XacmlContextDecision.Permit.ToString(); + response.Response.Add(xacmlJsonResult); + + // Act + bool result = DecisionHelper.ValidatePdpDecision(response.Response, CreateUserClaims(false)); + + // Assert + Assert.True(result); + } + + /// + /// Test case: Respons contains obligation with min authentication level that the user meets + /// Expected: Returns true + /// + [Fact] + public void ValidatePdpDecision_TC02() + { + // Arrange + XacmlJsonResponse response = new XacmlJsonResponse(); + response.Response = new List(); + XacmlJsonResult xacmlJsonResult = new XacmlJsonResult(); + xacmlJsonResult.Decision = XacmlContextDecision.Permit.ToString(); + response.Response.Add(xacmlJsonResult); + + // Add obligation to result with a minimum authentication level attribute + XacmlJsonObligationOrAdvice obligation = new XacmlJsonObligationOrAdvice(); + obligation.AttributeAssignment = new List(); + XacmlJsonAttributeAssignment authenticationAttribute = new XacmlJsonAttributeAssignment() + { + Category = "urn:altinn:minimum-authenticationlevel", + Value = "2" + }; + obligation.AttributeAssignment.Add(authenticationAttribute); + xacmlJsonResult.Obligations = new List(); + xacmlJsonResult.Obligations.Add(obligation); + + // Act + bool result = DecisionHelper.ValidatePdpDecision(response.Response, CreateUserClaims(false)); + + // Assert + Assert.True(result); + } + + /// + /// Test case: Respons contains obligation with min authentication level that the user do not meet + /// Expected: Returns false + /// + [Fact] + public void ValidatePdpDecision_TC03() + { + // Arrange + XacmlJsonResponse response = new XacmlJsonResponse(); + response.Response = new List(); + XacmlJsonResult xacmlJsonResult = new XacmlJsonResult(); + xacmlJsonResult.Decision = XacmlContextDecision.Permit.ToString(); + response.Response.Add(xacmlJsonResult); + + // Add obligation to result with a minimum authentication level attribute + XacmlJsonObligationOrAdvice obligation = new XacmlJsonObligationOrAdvice(); + obligation.AttributeAssignment = new List(); + XacmlJsonAttributeAssignment authenticationAttribute = new XacmlJsonAttributeAssignment() + { + Category = "urn:altinn:minimum-authenticationlevel", + Value = "3" + }; + obligation.AttributeAssignment.Add(authenticationAttribute); + xacmlJsonResult.Obligations = new List(); + xacmlJsonResult.Obligations.Add(obligation); + + // Act + bool result = DecisionHelper.ValidatePdpDecision(response.Response, CreateUserClaims(false)); + + // Assert + Assert.False(result); + } + + /// + /// Test case: Respons with result deny + /// Expected: Returns false + /// + [Fact] + public void ValidatePdpDecision_TC04() + { + // Arrange + XacmlJsonResponse response = new XacmlJsonResponse(); + response.Response = new List(); + XacmlJsonResult xacmlJsonResult = new XacmlJsonResult(); + xacmlJsonResult.Decision = XacmlContextDecision.Deny.ToString(); + response.Response.Add(xacmlJsonResult); + + // Act + bool result = DecisionHelper.ValidatePdpDecision(response.Response, CreateUserClaims(false)); + + // Assert + Assert.False(result); + } + + /// + /// Test case: Respons with two results + /// Expected: Returns false + /// + [Fact] + public void ValidatePdpDecision_TC05() + { + // Arrange + XacmlJsonResponse response = new XacmlJsonResponse(); + response.Response = new List(); + XacmlJsonResult xacmlJsonResult = new XacmlJsonResult(); + xacmlJsonResult.Decision = XacmlContextDecision.Permit.ToString(); + response.Response.Add(xacmlJsonResult); + response.Response.Add(new XacmlJsonResult()); + + // Act + bool result = DecisionHelper.ValidatePdpDecision(response.Response, CreateUserClaims(false)); + + // Assert + Assert.False(result); + } + + /// + /// Test case: Result list is null + /// Expected: Throws ArgumentNullException + /// + [Fact] + public void ValidatePdpDecision_TC06() + { + // Arrange + XacmlJsonResponse response = new XacmlJsonResponse(); + response.Response = null; + + // Act & Assert + Assert.Throws(() => DecisionHelper.ValidatePdpDecision(response.Response, CreateUserClaims(false))); + } + + /// + /// Test case: User is null + /// Expected: Throws ArgumentNullException + /// + [Fact] + public void ValidatePdpDecision_TC07() + { + // Arrange + XacmlJsonResponse response = new XacmlJsonResponse(); + response.Response = new List(); + + // Act & Assert + Assert.Throws(() => DecisionHelper.ValidatePdpDecision(response.Response, null)); + } + + /// + /// Test case: Response contains obligation with min authentication level that the user do not meet, get detailed response + /// Expected: Returns false + /// + [Fact] + public void ValidatePdpDecision_TC08() + { + // Arrange + XacmlJsonResponse response = new XacmlJsonResponse(); + response.Response = new List(); + XacmlJsonResult xacmlJsonResult = new XacmlJsonResult(); + xacmlJsonResult.Decision = XacmlContextDecision.Permit.ToString(); + response.Response.Add(xacmlJsonResult); + + // Add obligation to result with a minimum authentication level attribute + XacmlJsonObligationOrAdvice obligation = new XacmlJsonObligationOrAdvice(); + obligation.AttributeAssignment = new List(); + string minAuthLevel = "3"; + XacmlJsonAttributeAssignment authenticationAttribute = new XacmlJsonAttributeAssignment() + { + Category = "urn:altinn:minimum-authenticationlevel", + Value = minAuthLevel + }; + obligation.AttributeAssignment.Add(authenticationAttribute); + xacmlJsonResult.Obligations = new List(); + xacmlJsonResult.Obligations.Add(obligation); + + // Act + EnforcementResult result = DecisionHelper.ValidatePdpDecisionDetailed(response.Response, CreateUserClaims(false)); + + // Assert + Assert.False(result.Authorized); + Assert.Contains(AltinnObligations.RequiredAuthenticationLevel, result.FailedObligations.Keys); + Assert.Equal(minAuthLevel, result.FailedObligations[AltinnObligations.RequiredAuthenticationLevel]); + } + + /// + /// Test case: Response contains obligation with min authentication level that the user do not meet, get detailed response + /// Expected: Returns false + /// + [Fact] + public void ValidatePdpDecision_TC09() + { + // Arrange + XacmlJsonResponse response = new XacmlJsonResponse(); + response.Response = new List(); + XacmlJsonResult xacmlJsonResult = new XacmlJsonResult(); + xacmlJsonResult.Decision = XacmlContextDecision.Permit.ToString(); + response.Response.Add(xacmlJsonResult); + + // Add obligation to result with a minimum authentication level attribute + XacmlJsonObligationOrAdvice obligation = new XacmlJsonObligationOrAdvice(); + obligation.AttributeAssignment = new List(); + string minAuthLevel = "4"; + XacmlJsonAttributeAssignment authenticationAttribute = new XacmlJsonAttributeAssignment() + { + Category = "urn:altinn:minimum-authenticationlevel", + Value = minAuthLevel + }; + obligation.AttributeAssignment.Add(authenticationAttribute); + + XacmlJsonObligationOrAdvice obligationOrg = new XacmlJsonObligationOrAdvice(); + obligationOrg.AttributeAssignment = new List(); + string minAuthLevelOrg = "2"; + XacmlJsonAttributeAssignment authenticationAttributeOrg = new XacmlJsonAttributeAssignment() + { + Category = "urn:altinn:minimum-authenticationlevel", + Value = minAuthLevelOrg + }; + obligationOrg.AttributeAssignment.Add(authenticationAttributeOrg); + + xacmlJsonResult.Obligations = new List(); + xacmlJsonResult.Obligations.Add(obligation); + xacmlJsonResult.Obligations.Add(obligationOrg); + + // Act + EnforcementResult result = DecisionHelper.ValidatePdpDecisionDetailed(response.Response, CreateUserClaims(false)); + + // Assert + Assert.False(result.Authorized); + Assert.Contains(AltinnObligations.RequiredAuthenticationLevel, result.FailedObligations.Keys); + Assert.Equal(minAuthLevel, result.FailedObligations[AltinnObligations.RequiredAuthenticationLevel]); + } + + /// + /// Test case: Response contains obligation with min authentication level that the user do not meet, get detailed response + /// Expected: Returns false + /// + [Fact] + public void ValidatePdpDecision_TC10() + { + // Arrange + XacmlJsonResponse response = new XacmlJsonResponse(); + response.Response = new List(); + XacmlJsonResult xacmlJsonResult = new XacmlJsonResult(); + xacmlJsonResult.Decision = XacmlContextDecision.Permit.ToString(); + response.Response.Add(xacmlJsonResult); + + // Add obligation to result with a minimum authentication level attribute + XacmlJsonObligationOrAdvice obligation = new XacmlJsonObligationOrAdvice(); + obligation.AttributeAssignment = new List(); + string minAuthLevel = "4"; + XacmlJsonAttributeAssignment authenticationAttribute = new XacmlJsonAttributeAssignment() + { + Category = "urn:altinn:minimum-authenticationlevel", + Value = minAuthLevel + }; + obligation.AttributeAssignment.Add(authenticationAttribute); + + XacmlJsonObligationOrAdvice obligationOrg = new XacmlJsonObligationOrAdvice(); + obligationOrg.AttributeAssignment = new List(); + string minAuthLevelOrg = "2"; + XacmlJsonAttributeAssignment authenticationAttributeOrg = new XacmlJsonAttributeAssignment() + { + Category = "urn:altinn:minimum-authenticationlevel-org", + Value = minAuthLevelOrg + }; + obligationOrg.AttributeAssignment.Add(authenticationAttributeOrg); + + xacmlJsonResult.Obligations = new List(); + xacmlJsonResult.Obligations.Add(obligationOrg); + xacmlJsonResult.Obligations.Add(obligation); + + // Act + EnforcementResult result = DecisionHelper.ValidatePdpDecisionDetailed(response.Response, CreateUserClaims(false, "ttd")); + + // Assert + Assert.True(result.Authorized); + Assert.Null(result.FailedObligations); + } + + private ClaimsPrincipal CreateUserClaims(bool addExtraClaim, string org = null) + { + // Create the user + List claims = new List(); + + // type, value, valuetype, issuer + claims.Add(new Claim("urn:altinn:authlevel", "2", "string", "org")); + if (org != null) + { + claims.Add(new Claim("urn:altinn:org", org, "string", "org")); + } + else + { + claims.Add(new Claim("urn:name", "Ola", "string", "org")); + } + + if (addExtraClaim) + { + claims.Add(new Claim("a", "a", "string", "a")); + } + + ClaimsPrincipal user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + return user; + } + + private ClaimsPrincipal CreateMaskinportenClaims(string org, string scope) + { + // Create the user + List claims = new List(); + + // type, value, valuetype, issuer + claims.Add(new Claim("urn:altinn:authlevel", "2", "string", "org")); + claims.Add(new Claim("urn:altinn:orgNumber", org, "string", "org")); + if (scope != null) + { + claims.Add(new Claim("scope", scope, "string", "org")); + } + + ClaimsPrincipal user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + return user; + } + } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/tests/Altinn.Authorization.PEP.Tests/GlobalSuppressions.cs b/src/pkgs/Altinn.Authorization.PEP/tests/Altinn.Authorization.PEP.Tests/GlobalSuppressions.cs new file mode 100644 index 00000000..51d4d2b1 --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/tests/Altinn.Authorization.PEP.Tests/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Test description should be in the name of the test.", Scope = "module")] diff --git a/src/pkgs/Altinn.Authorization.PEP/tests/Altinn.Authorization.PEP.Tests/ResourceAccessHandlerTest.cs b/src/pkgs/Altinn.Authorization.PEP/tests/Altinn.Authorization.PEP.Tests/ResourceAccessHandlerTest.cs new file mode 100644 index 00000000..e510ca12 --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/tests/Altinn.Authorization.PEP.Tests/ResourceAccessHandlerTest.cs @@ -0,0 +1,271 @@ +using System; +using System.Collections.Generic; +using System.Reflection.Metadata; +using System.Security.Claims; +using System.Threading.Tasks; + +using Altinn.Authorization.ABAC.Xacml; +using Altinn.Authorization.ABAC.Xacml.JsonProfile; +using Altinn.Common.PEP.Configuration; +using Altinn.Common.PEP.Interfaces; + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Altinn.Common.PEP.Authorization +{ + public class ResourceAccessHandlerTest + { + private readonly Mock _httpContextAccessorMock; + private readonly Mock _pdpMock; + private readonly IOptions _generalSettings; + private readonly ResourceAccessHandler _rah; + + public ResourceAccessHandlerTest() + { + _httpContextAccessorMock = new Mock(); + _pdpMock = new Mock(); + _generalSettings = Options.Create(new PepSettings()); + _rah = new ResourceAccessHandler(_httpContextAccessorMock.Object, _pdpMock.Object, new Mock>().Object); + } + + /// + /// Test case: Send request and get response that fulfills all requirements + /// Expected: Context will succeed + /// + [Fact] + public async Task HandleRequirementAsync_TC01Async() + { + // Arrange + AuthorizationHandlerContext context = CreateAuthorizationHandlerContext(); + _httpContextAccessorMock.Setup(h => h.HttpContext).Returns(CreateHttpContext("23453546", null, null)); + XacmlJsonResponse response = CreateResponse(XacmlContextDecision.Permit.ToString()); + _pdpMock.Setup(a => a.GetDecisionForRequest(It.IsAny())).Returns(Task.FromResult(response)); + + // Act + await _rah.HandleAsync(context); + + // Assert + Assert.True(context.HasSucceeded); + Assert.False(context.HasFailed); + } + + /// + /// Test case: Send request and get response that fulfills all requirements + /// Expected: Context will succeed + /// + [Fact] + public async Task HandleRequirementAsync_TC02Async() + { + // Arrange + AuthorizationHandlerContext context = CreateAuthorizationHandlerContext(); + _httpContextAccessorMock.Setup(h => h.HttpContext).Returns(CreateHttpContext("organization", "991825827", null)); + XacmlJsonResponse response = CreateResponse(XacmlContextDecision.Permit.ToString()); + _pdpMock.Setup(a => a.GetDecisionForRequest(It.IsAny())).Returns(Task.FromResult(response)); + + // Act + await _rah.HandleAsync(context); + + // Assert + Assert.True(context.HasSucceeded); + Assert.False(context.HasFailed); + + XacmlJsonRequestRoot request = _pdpMock.Invocations[0].Arguments[0] as XacmlJsonRequestRoot; + Assert.Equal("urn:altinn:organizationnumber", request.Request.Resource[0].Attribute[0].AttributeId); + Assert.Equal("991825827", request.Request.Resource[0].Attribute[0].Value); + } + + /// + /// Test case: Send request and get response that fulfills all requirements + /// Expected: Exception since format of who is incorrect + /// + [Fact] + + public async Task HandleRequirementAsync_TC03Async() + { + // Arrange + AuthorizationHandlerContext context = CreateAuthorizationHandlerContext(); + _httpContextAccessorMock.Setup(h => h.HttpContext).Returns(CreateHttpContext("organization", "991825827M", null)); + XacmlJsonResponse response = CreateResponse(XacmlContextDecision.Permit.ToString()); + _pdpMock.Setup(a => a.GetDecisionForRequest(It.IsAny())).Returns(Task.FromResult(response)); + + // Act + Task Act() => _rah.HandleAsync(context); + + // Assert + ArgumentException exception = await Assert.ThrowsAsync(Act); + Assert.Equal("invalid party organization", exception.Message); + } + + /// + /// Test case: Send request and get response that fulfills all requirements + /// Expected: True + /// + [Fact] + + public async Task HandleRequirementAsync_TC04Async() + { + // Arrange + AuthorizationHandlerContext context = CreateAuthorizationHandlerContext(); + _httpContextAccessorMock.Setup(h => h.HttpContext).Returns(CreateHttpContext("person", null, "01014922047")); + XacmlJsonResponse response = CreateResponse(XacmlContextDecision.Permit.ToString()); + _pdpMock.Setup(a => a.GetDecisionForRequest(It.IsAny())).Returns(Task.FromResult(response)); + + // Act + await _rah.HandleAsync(context); + + // Assert + Assert.True(context.HasSucceeded); + Assert.False(context.HasFailed); + + XacmlJsonRequestRoot request = _pdpMock.Invocations[0].Arguments[0] as XacmlJsonRequestRoot; + Assert.Equal("urn:altinn:ssn", request.Request.Resource[0].Attribute[0].AttributeId); + Assert.Equal("01014922047", request.Request.Resource[0].Attribute[0].Value); + } + + /// + /// Test case: Send request and get response that fulfills all requirements + /// Expected: Exception since format of who is incorrect + /// + [Fact] + + public async Task HandleRequirementAsync_TC05Async() + { + // Arrange + AuthorizationHandlerContext context = CreateAuthorizationHandlerContext(); + _httpContextAccessorMock.Setup(h => h.HttpContext).Returns(CreateHttpContext("person", null, "a01014922047")); + XacmlJsonResponse response = CreateResponse(XacmlContextDecision.Permit.ToString()); + _pdpMock.Setup(a => a.GetDecisionForRequest(It.IsAny())).Returns(Task.FromResult(response)); + + // Act + Task Act() => _rah.HandleAsync(context); + + // Assert + ArgumentException exception = await Assert.ThrowsAsync(Act); + Assert.Equal("invalid party person", exception.Message); + } + + /// + /// Test case: Send request and verify the XForwardedForHeader property in request + /// Expected: Request header does not have a xforwardedforheader and therefore the header property in xacmljsonrequest will be null + /// + [Fact] + public async Task HandleRequirementAsync_TC06Async() + { + // Arrange + AuthorizationHandlerContext context = CreateAuthorizationHandlerContext(); + _httpContextAccessorMock.Setup(h => h.HttpContext).Returns(CreateHttpContext("23453546", null, null)); + XacmlJsonResponse response = CreateResponse(XacmlContextDecision.Permit.ToString()); + + // Verify + _pdpMock.Setup(a => a.GetDecisionForRequest(It.Is(xr => xr.Request.XForwardedForHeader == null))).Returns(Task.FromResult(response)); + + // Act + await _rah.HandleAsync(context); + } + + /// + /// Test case: Send request verify if the ipaddress from the x-forwarded-for header is received + /// Expected: XForwardedForHeader proeprty in request receives the ipaddress from the header + /// + [Fact] + public async Task HandleRequirementAsync_TC07Async() + { + // Arrange + AuthorizationHandlerContext context = CreateAuthorizationHandlerContext(); + string ipaddress = "18.203.138.153"; + _httpContextAccessorMock.Setup(h => h.HttpContext).Returns(CreateHttpContext("organization", "991825827", null, ipaddress)); + XacmlJsonResponse response = CreateResponse(XacmlContextDecision.Permit.ToString()); + + // verify + _pdpMock.Setup(a => a.GetDecisionForRequest(It.IsAny())).Returns(Task.FromResult(response)); + + // Act + await _rah.HandleAsync(context); + } + + private ClaimsPrincipal CreateUser() + { + // Create the user + List claims = new List(); + + // type, value, valuetype, issuer + claims.Add(new Claim("urn:name", "Ola", "string", "org")); + claims.Add(new Claim("urn:altinn:authlevel", "2", "string", "org")); + + ClaimsPrincipal user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + return user; + } + + private HttpContext CreateHttpContext(string party, string orgHeader, string ssnHeader, string xForwardedForHeader = null) + { + HttpContext httpContext = new DefaultHttpContext(); + httpContext.Request.RouteValues.Add("party", party); + if (!string.IsNullOrEmpty(orgHeader)) + { + httpContext.Request.Headers.Add("Altinn-Party-OrganizationNumber", orgHeader); + } + + if (!string.IsNullOrEmpty(ssnHeader)) + { + httpContext.Request.Headers.Add("Altinn-Party-SocialSecurityNumber", ssnHeader); + } + + if (!string.IsNullOrEmpty(xForwardedForHeader)) + { + httpContext.Request.Headers.Add("x-forwarded-for", xForwardedForHeader); + } + + return httpContext; + } + + private XacmlJsonResponse CreateResponse(string decision) + { + // Create response + XacmlJsonResponse response = new XacmlJsonResponse(); + response.Response = new List(); + + // Set result to premit + XacmlJsonResult result = new XacmlJsonResult(); + result.Decision = decision; + response.Response.Add(result); + + return response; + } + + private XacmlJsonResponse AddObligationWithMinAuthLv(XacmlJsonResponse response, string minAuthLv) + { + // Add obligation to result with a minimum authentication level attribute + XacmlJsonResult result = response.Response[0]; + XacmlJsonObligationOrAdvice obligation = new XacmlJsonObligationOrAdvice(); + obligation.AttributeAssignment = new List(); + XacmlJsonAttributeAssignment authenticationAttribute = new XacmlJsonAttributeAssignment() + { + Category = "urn:altinn:minimum-authenticationlevel", + Value = minAuthLv + }; + obligation.AttributeAssignment.Add(authenticationAttribute); + result.Obligations = new List(); + result.Obligations.Add(obligation); + + return response; + } + + private AuthorizationHandlerContext CreateAuthorizationHandlerContext() + { + ResourceAccessRequirement requirement = new ResourceAccessRequirement("read", "altinn_access_management_apidelegation"); + ClaimsPrincipal user = CreateUser(); + Document resource = default(Document); + AuthorizationHandlerContext context = new AuthorizationHandlerContext( + new[] { requirement }, + user, + resource); + return context; + } + } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/tests/Altinn.Authorization.PEP.Tests/ScopeAccessHandlerTest.cs b/src/pkgs/Altinn.Authorization.PEP/tests/Altinn.Authorization.PEP.Tests/ScopeAccessHandlerTest.cs new file mode 100644 index 00000000..414c0c3e --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/tests/Altinn.Authorization.PEP.Tests/ScopeAccessHandlerTest.cs @@ -0,0 +1,168 @@ +using System.Collections.Generic; +using System.Reflection.Metadata; +using System.Security.Claims; +using System.Threading.Tasks; + +using Altinn.Common.PEP.Authorization; + +using Microsoft.AspNetCore.Authorization; + +using Xunit; + +namespace UnitTests +{ + public class ScopeAccessHandlerTest + { + private readonly ScopeAccessHandler _sah; + + public ScopeAccessHandlerTest() + { + _sah = new ScopeAccessHandler(); + } + + /// + /// Test case: Valid scope claim is included in context. + /// Expected: Context will succeed. + /// + [Fact] + public async Task HandleAsync_ValidScope_ContextSuccess() + { + // Arrange + AuthorizationHandlerContext context = CreateAuthzHandlerContext("altinn:appdeploy"); + + // Act + await _sah.HandleAsync(context); + + // Assert + Assert.True(context.HasSucceeded); + Assert.False(context.HasFailed); + } + + /// + /// Test case: Valid scope claim is included in context. + /// Expected: Context will succeed. + /// + [Fact] + public async Task HandleAsync_ValidScopeOf2_OneInvalidPresent_ContextSuccess() + { + // Arrange + AuthorizationHandlerContext context = CreateAuthzHandlerContext("altinn:resourceregistry:write altinn:resourceregistry:read", new[] { "altinn:resourceregistry:admin", "altinn:resourceregistry:write" }); + + // Act + await _sah.HandleAsync(context); + + // Assert + Assert.True(context.HasSucceeded); + Assert.False(context.HasFailed); + } + + /// + /// Test case: Valid scope is missing in context + /// Expected: Context will fail. + /// + [Fact] + public async Task HandleAsync_ValidScopeOf2_OneInvalidPresent_ContextFail() + { + // Arrange + AuthorizationHandlerContext context = CreateAuthzHandlerContext("altinn:resourceregistry:read", new[] { "altinn:resourceregistry:admin", "altinn:resourceregistry:write" }); + + // Act + await _sah.HandleAsync(context); + + // Assert + Assert.False(context.HasSucceeded); + } + + /// + /// Test case: Valid scope claim is included in context. + /// Expected: Context will succeed. + /// + [Fact] + public async Task HandleAsync_ValidScopeOf2_ContextSuccess() + { + // Arrange + AuthorizationHandlerContext context = CreateAuthzHandlerContext("altinn:resourceregistry:write", new[] { "altinn:resourceregistry:admin", "altinn:resourceregistry:write" }); + + // Act + await _sah.HandleAsync(context); + + // Assert + Assert.True(context.HasSucceeded); + Assert.False(context.HasFailed); + } + + /// + /// Test case: Invalid scope claim is included in context. + /// Expected: Context will fail. + /// + [Fact] + public async Task HandleAsync_InvalidScope_ContextFail() + { + // Arrange + AuthorizationHandlerContext context = CreateAuthzHandlerContext("altinn:invalid"); + + // Act + await _sah.HandleAsync(context); + + // Assert + Assert.False(context.HasSucceeded); + } + + /// + /// Test case: Empty scope claim is included in context. + /// Expected: Context will fail. + /// + [Fact] + public async Task HandleAsync_EmptyScope_ContextFail() + { + // Arrange + AuthorizationHandlerContext context = CreateAuthzHandlerContext(string.Empty); + + // Act + await _sah.HandleAsync(context); + + // Assert + Assert.False(context.HasSucceeded); + } + + private AuthorizationHandlerContext CreateAuthzHandlerContext(string scopeClaim) + { + ScopeAccessRequirement requirement = new ScopeAccessRequirement("altinn:appdeploy"); + + ClaimsPrincipal user = new ClaimsPrincipal( + new ClaimsIdentity( + new List + { + new Claim("urn:altinn:scope", scopeClaim, "string", "org"), + new Claim("urn:altinn:org", "brg", "string", "org") + }, + "AuthenticationTypes.Federation")); + + AuthorizationHandlerContext context = new AuthorizationHandlerContext( + new[] { requirement }, + user, + default(Document)); + return context; + } + + private AuthorizationHandlerContext CreateAuthzHandlerContext(string scopeClaim, string[] requiredScopes) + { + ScopeAccessRequirement requirement = new ScopeAccessRequirement(requiredScopes); + + ClaimsPrincipal user = new ClaimsPrincipal( + new ClaimsIdentity( + new List + { + new Claim("scope", scopeClaim, "string", "org"), + new Claim("urn:altinn:org", "brg", "string", "org") + }, + "AuthenticationTypes.Federation")); + + AuthorizationHandlerContext context = new AuthorizationHandlerContext( + new[] { requirement }, + user, + default(Document)); + return context; + } + } +} diff --git a/src/pkgs/Altinn.Authorization.PEP/tests/Directory.Build.props b/src/pkgs/Altinn.Authorization.PEP/tests/Directory.Build.props new file mode 100644 index 00000000..3c42af90 --- /dev/null +++ b/src/pkgs/Altinn.Authorization.PEP/tests/Directory.Build.props @@ -0,0 +1,8 @@ + + + + + true + + + diff --git a/src/pkgs/Directory.Build.props b/src/pkgs/Directory.Build.props index 8748fdee..20e06d22 100644 --- a/src/pkgs/Directory.Build.props +++ b/src/pkgs/Directory.Build.props @@ -1,3 +1,5 @@ - - + + + + \ No newline at end of file diff --git a/src/pkgs/Directory.Build.targets b/src/pkgs/Directory.Build.targets new file mode 100644 index 00000000..7e060b76 --- /dev/null +++ b/src/pkgs/Directory.Build.targets @@ -0,0 +1,27 @@ + + + + + + + + 9.0.0 + true + Pack + *nupkg + + $(MSBuildThisFileDirectory)artifacts\ + + + + + + + + false + + + + + + \ No newline at end of file diff --git a/src/pkgs/Directory.Packages.props b/src/pkgs/Directory.Packages.props deleted file mode 100644 index e0163e52..00000000 --- a/src/pkgs/Directory.Packages.props +++ /dev/null @@ -1,23 +0,0 @@ - - - - true - - - - - - - - - - - - - - - - - - -