diff --git a/src/Authorization/Altinn.Platform.Authorization.csproj b/src/Authorization/Altinn.Platform.Authorization.csproj index 8b183ccc..87aba3db 100644 --- a/src/Authorization/Altinn.Platform.Authorization.csproj +++ b/src/Authorization/Altinn.Platform.Authorization.csproj @@ -8,14 +8,21 @@ + + + + + + + @@ -28,6 +35,8 @@ + + diff --git a/src/Authorization/Configuration/TokenGeneratorSettings.cs b/src/Authorization/Configuration/TokenGeneratorSettings.cs new file mode 100644 index 00000000..a6a2656a --- /dev/null +++ b/src/Authorization/Configuration/TokenGeneratorSettings.cs @@ -0,0 +1,27 @@ +namespace Altinn.Platform.Authorization.Configuration; + +/// +/// Represents a set of configuration options needed for using the Altinn Platform Token Generator. +/// +public class TokenGeneratorSettings +{ + /// + /// Gets or sets the url for the token generator. + /// + public string Url { get; set; } + + /// + /// Gets or sets user for authorized access to the token generator. + /// + public string User { get; set; } + + /// + /// Gets or sets password for authorized access to the token generator. + /// + public string Password { get; set; } + + /// + /// Gets or sets the environment to use for the token generator. + /// + public string Env { get; set; } +} diff --git a/src/Authorization/Constants/AuthzConstants.cs b/src/Authorization/Constants/AuthzConstants.cs index f4557ec2..c63d90dc 100644 --- a/src/Authorization/Constants/AuthzConstants.cs +++ b/src/Authorization/Constants/AuthzConstants.cs @@ -10,6 +10,16 @@ public static class AuthzConstants /// public const string POLICY_STUDIO_DESIGNER = "StudioDesignerAccess"; + /// + /// Policy tag for authorizing PlatformAccessTokens issued by Platform + /// + public const string POLICY_PLATFORMISSUER_ACCESSTOKEN = "PlatformIssuedAccessToken"; + + /// + /// The issuer of access tokens for the platform cluster + /// + public const string PLATFORM_ACCESSTOKEN_ISSUER = "platform"; + /// /// Policy tag for authorizing Altinn.Platform.Authorization API access from AltinnII Authorization /// @@ -25,11 +35,6 @@ public static class AuthzConstants /// public const string AUTHORIZESCOPEACCESS = "AuthorizeScopeAccess"; - /// - /// (deprecated) Scope that gives access to Authorize API - /// - public const string PDP_SCOPE = "altinn:authorization:pdp"; - /// /// Scope that gives access to external Authorize API for service/resource owners /// diff --git a/src/Authorization/Controllers/AccessListAuthorizationController.cs b/src/Authorization/Controllers/AccessListAuthorizationController.cs new file mode 100644 index 00000000..a000fbd7 --- /dev/null +++ b/src/Authorization/Controllers/AccessListAuthorizationController.cs @@ -0,0 +1,66 @@ +using System.Linq; +using System.Net.Mime; +using System.Threading; +using System.Threading.Tasks; +using Altinn.Authorization.Errors; +using Altinn.Authorization.Models.ResourceRegistry; +using Altinn.Authorization.ProblemDetails; +using Altinn.Platform.Authorization.Constants; +using Altinn.Platform.Authorization.Models; +using Altinn.Platform.Authorization.Services.Interface; +using AltinnCore.Authentication.Constants; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Altinn.Platform.Authorization.Controllers; + +/// +/// Contains all actions related to the roles model +/// +[Route("authorization/api/v1/accesslist")] +[ApiController] +public class AccessListAuthorizationController : ControllerBase +{ + private readonly IAccessListAuthorization _accessListAuthorization; + private readonly IResourceRegistry _resourceRegistry; + + /// + /// Initializes a new instance of the class + /// + public AccessListAuthorizationController(IAccessListAuthorization accessListAuthorization, IResourceRegistry resourceRegistry) + { + _accessListAuthorization = accessListAuthorization; + _resourceRegistry = resourceRegistry; + } + + /// + /// Internal API for local cluster requests only. + /// Authorization of a given subject for resource access through access lists for any service owner organization's access lists and resources. + /// + /// Access list authorization request model + /// The + /// AccessListAuthorizationResponse + [HttpPost] + [Route("accessmanagement/authorization")] + [ApiExplorerSettings(IgnoreApi = true)] + [Authorize(Policy = AuthzConstants.POLICY_PLATFORMISSUER_ACCESSTOKEN)] + [Consumes(MediaTypeNames.Application.Json)] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(typeof(AccessListAuthorizationResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] + [ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public async Task AuthorizeInternal(AccessListAuthorizationRequest accessListAuthorizationRequest, CancellationToken cancellationToken = default) + { + Result result = await _accessListAuthorization.Authorize(accessListAuthorizationRequest, cancellationToken); + + if (result.IsProblem) + { + return result.Problem.ToActionResult(); + } + + return Ok(result.Value); + } +} diff --git a/src/Authorization/Controllers/DecisionController.cs b/src/Authorization/Controllers/DecisionController.cs index 945069dd..88f3196c 100644 --- a/src/Authorization/Controllers/DecisionController.cs +++ b/src/Authorization/Controllers/DecisionController.cs @@ -10,7 +10,13 @@ using Altinn.Authorization.ABAC.Utils; using Altinn.Authorization.ABAC.Xacml; using Altinn.Authorization.ABAC.Xacml.JsonProfile; +using Altinn.Authorization.Enums; +using Altinn.Authorization.Models; +using Altinn.Authorization.Models.Register; +using Altinn.Authorization.Models.ResourceRegistry; +using Altinn.Authorization.ProblemDetails; using Altinn.Platform.Authorization.Constants; +using Altinn.Platform.Authorization.Helpers; using Altinn.Platform.Authorization.ModelBinding; using Altinn.Platform.Authorization.Models; using Altinn.Platform.Authorization.Models.AccessManagement; @@ -18,6 +24,7 @@ using Altinn.Platform.Authorization.Repositories.Interface; using Altinn.Platform.Authorization.Services.Interface; using Altinn.Platform.Authorization.Services.Interfaces; +using Altinn.Platform.Register.Models; using Altinn.Platform.Storage.Interface.Models; using AutoMapper; using Azure.Core; @@ -47,6 +54,9 @@ public class DecisionController : ControllerBase private readonly IEventLog _eventLog; private readonly IFeatureManager _featureManager; private readonly IAccessManagementWrapper _accessManagement; + private readonly IResourceRegistry _resourceRegistry; + private readonly IRegisterService _registerService; + private readonly IAccessListAuthorization _accessListAuthorization; private readonly IMapper _mapper; private readonly SortedDictionary _appInstanceInfo = new(); @@ -55,6 +65,9 @@ public class DecisionController : ControllerBase /// Initializes a new instance of the class. /// /// Service for making request the to Access Management API (PIP) + /// Service for making requests to the Resource Registry API + /// Service for making requests to the Register API + /// Service for authorization of subjects based on Resource Registry access lists /// The Context handler /// The delegation context handler /// The policy Retrieval point @@ -64,7 +77,20 @@ public class DecisionController : ControllerBase /// the authorization event logger /// the feature manager /// The model mapper - public DecisionController(IAccessManagementWrapper accessManagement, IContextHandler contextHandler, IDelegationContextHandler delegationContextHandler, IPolicyRetrievalPoint policyRetrievalPoint, IDelegationMetadataRepository delegationRepository, ILogger logger, IMemoryCache memoryCache, IEventLog eventLog, IFeatureManager featureManager, IMapper mapper) + public DecisionController( + IAccessManagementWrapper accessManagement, + IResourceRegistry resourceRegistry, + IRegisterService registerService, + IAccessListAuthorization accessListAuthorization, + IContextHandler contextHandler, + IDelegationContextHandler delegationContextHandler, + IPolicyRetrievalPoint policyRetrievalPoint, + IDelegationMetadataRepository delegationRepository, + ILogger logger, + IMemoryCache memoryCache, + IEventLog eventLog, + IFeatureManager featureManager, + IMapper mapper) { _pdp = new PolicyDecisionPoint(); _prp = policyRetrievalPoint; @@ -75,6 +101,9 @@ public DecisionController(IAccessManagementWrapper accessManagement, IContextHan _eventLog = eventLog; _featureManager = featureManager; _accessManagement = accessManagement; + _resourceRegistry = resourceRegistry; + _registerService = registerService; + _accessListAuthorization = accessListAuthorization; _mapper = mapper; } @@ -267,28 +296,17 @@ private async Task Authorize(XacmlContextRequest decisionR { decisionRequest = await this._contextHandler.Enrich(decisionRequest, isExernalRequest, _appInstanceInfo); - ////_logger.LogInformation($"// DecisionController // Authorize // Roles // Enriched request: {JsonConvert.SerializeObject(decisionRequest)}."); XacmlPolicy policy = await _prp.GetPolicyAsync(decisionRequest); XacmlContextResponse rolesContextResponse = _pdp.Authorize(decisionRequest, policy); - ////_logger.LogInformation($"// DecisionController // Authorize // Roles // XACML ContextResponse: {JsonConvert.SerializeObject(rolesContextResponse)}."); - XacmlContextResult roleResult = rolesContextResponse.Results.First(); + + XacmlContextResponse delegationContextResponse = null; if (roleResult.Decision.Equals(XacmlContextDecision.NotApplicable)) { try { - XacmlContextResponse delegationContextResponse = await AuthorizeUsingDelegations(decisionRequest, policy, cancellationToken); - XacmlContextResult delegationResult = delegationContextResponse.Results.First(); - if (delegationResult.Decision.Equals(XacmlContextDecision.Permit)) - { - if (logEvent) - { - await _eventLog.CreateAuthorizationEvent(_featureManager, decisionRequest, HttpContext, delegationContextResponse, cancellationToken); - } - - return delegationContextResponse; - } + delegationContextResponse = await AuthorizeUsingDelegations(decisionRequest, policy, cancellationToken); } catch (Exception ex) { @@ -296,12 +314,27 @@ private async Task Authorize(XacmlContextRequest decisionR } } + XacmlContextResponse finalResponse = delegationContextResponse ?? rolesContextResponse; + XacmlContextResult finalResult = finalResponse.Results.First(); + if (finalResult.Decision.Equals(XacmlContextDecision.Permit) && !await IsAccessListAuthorized(decisionRequest, cancellationToken)) + { + return new XacmlContextResponse(new XacmlContextResult(XacmlContextDecision.Deny) + { + Status = new XacmlContextStatus(XacmlContextStatusCode.Success) + { + StatusMessage = "Access list authorization of resource party required. Access list authorization is controlled by the service owner of the resource/service of the authorization request.", + } + }); + } + + // If the delegation context response is NOT permit, the final response should be the roles context response + finalResponse = finalResult.Decision.Equals(XacmlContextDecision.Permit) ? finalResponse : rolesContextResponse; if (logEvent) { - await _eventLog.CreateAuthorizationEvent(_featureManager, decisionRequest, HttpContext, rolesContextResponse, cancellationToken); + await _eventLog.CreateAuthorizationEvent(_featureManager, decisionRequest, HttpContext, finalResponse, cancellationToken); } - return rolesContextResponse; + return finalResponse; } private async Task ProcessDelegationResult(XacmlContextRequest decisionRequest, XacmlPolicy resourcePolicy, IEnumerable delegations, CancellationToken cancellationToken = default) @@ -324,6 +357,46 @@ private async Task ProcessDelegationResult(XacmlContextReq }); } + private async Task IsAccessListAuthorized(XacmlContextRequest decisionRequest, CancellationToken cancellationToken = default) + { + PolicyResourceType policyResourceType = PolicyHelper.GetPolicyResourceType(decisionRequest, out string resourceId); + if (!policyResourceType.Equals(PolicyResourceType.ResourceRegistry)) + { + return true; + } + + ServiceResource resource = await _resourceRegistry.GetResourceAsync(resourceId, cancellationToken); + if (resource != null && resource.AccessListMode == ResourceAccessListMode.Enabled) + { + var resourceAttributes = _delegationContextHandler.GetResourceAttributes(decisionRequest); + + Party party = await _registerService.GetParty(int.Parse(resourceAttributes.ResourcePartyValue)); + if (party?.PartyTypeName != Register.Enums.PartyType.Organisation) + { + // Currently only Organization support in AccessLists + return false; + } + + resourceAttributes.OrganizationNumber = party.OrgNumber; + AccessListAuthorizationRequest accessListAuthorizationRequest = new AccessListAuthorizationRequest + { + Subject = PartyUrn.OrganizationIdentifier.Create(OrganizationNumber.CreateUnchecked(resourceAttributes.OrganizationNumber)), + Resource = ResourceIdUrn.ResourceId.Create(Altinn.Authorization.Models.ResourceRegistry.ResourceIdentifier.CreateUnchecked(resourceId)), + Action = ActionUrn.ActionId.Create(ActionIdentifier.CreateUnchecked(_delegationContextHandler.GetActionString(decisionRequest))) + }; + Result result = await _accessListAuthorization.Authorize(accessListAuthorizationRequest, cancellationToken); + + if (result.IsProblem) + { + return false; + } + + return result.Value.Result == AccessListAuthorizationResult.Authorized; + } + + return true; + } + private static string CreateCacheKey(params string[] cacheKeys) => string.Join("-", cacheKeys.Where(c => c != null && (c != string.Empty || !c.EndsWith(':')))); diff --git a/src/Authorization/Errors/Problems.cs b/src/Authorization/Errors/Problems.cs new file mode 100644 index 00000000..aaa4ecd7 --- /dev/null +++ b/src/Authorization/Errors/Problems.cs @@ -0,0 +1,21 @@ +#nullable enable + +using System.Net; +using Altinn.Authorization.ProblemDetails; + +namespace Altinn.Authorization.Errors; + +/// +/// Problem descriptors for Authorization +/// +public static class Problems +{ + private static readonly ProblemDescriptorFactory _factory + = ProblemDescriptorFactory.New("AUTHZ"); + + /// + /// Gets a for not implemented feature. + /// + public static ProblemDescriptor NotImplemented { get; } + = _factory.Create(0, HttpStatusCode.NotImplemented, "Not implemented."); +} diff --git a/src/Authorization/Errors/ValidationErrors.cs b/src/Authorization/Errors/ValidationErrors.cs new file mode 100644 index 00000000..aa40a17f --- /dev/null +++ b/src/Authorization/Errors/ValidationErrors.cs @@ -0,0 +1,32 @@ +#nullable enable + +using Altinn.Authorization.ProblemDetails; + +namespace Altinn.Authorization.Errors; + +/// +/// Validation errors for Authorization +/// +public static class ValidationErrors +{ + private static readonly ValidationErrorDescriptorFactory _factory + = ValidationErrorDescriptorFactory.New("AUTHZ"); + + /// + /// Gets a validation error descriptor for when a provided resource registry identifier is not found as a valid resource. + /// + public static ValidationErrorDescriptor ResourceRegistry_ResourceIdentifier_NotFound { get; } + = _factory.Create(0, "Unknown resource registry identifier."); + + /// + /// Gets a validation error descriptor for when the authencticated organization number is not authorized as the competent authority owner of a resource registry resource. + /// + public static ValidationErrorDescriptor ResourceRegistry_CompetentAuthority_NotMatchingAuthenticatedOrganization { get; } + = _factory.Create(1, "Authorized organization is not the competent authority owner of the requested resource."); + + /// + /// Gets a validation error descriptor for when the authencticated organization code is not authorized as the competent authority owner of a resource registry resource. + /// + public static ValidationErrorDescriptor ResourceRegistry_CompetentAuthority_NotMatchingAuthenticatedOrgCode { get; } + = _factory.Create(2, "Authorized organization code is not the competent authority owner of the requested resource."); +} diff --git a/src/Authorization/Extensions/HttpClientExtension.cs b/src/Authorization/Extensions/HttpClientExtension.cs index 2461a3d6..807b39fd 100644 --- a/src/Authorization/Extensions/HttpClientExtension.cs +++ b/src/Authorization/Extensions/HttpClientExtension.cs @@ -14,43 +14,54 @@ public static class HttpClientExtension /// Extension that add authorization header to request /// /// The HttpClient - /// the authorization token (jwt) /// The request Uri /// The http content + /// the authorization token (jwt) /// The platformAccess tokens + /// The /// A HttpResponseMessage - public static Task PostAsync(this HttpClient httpClient, string authorizationToken, string requestUri, HttpContent content, string platformAccessToken = null) + public static Task PostAsync(this HttpClient httpClient, string requestUri, HttpContent content, string authorizationToken = null, string platformAccessToken = null, CancellationToken cancellationToken = default) { HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, new Uri(requestUri, UriKind.Relative)); - request.Headers.Add("Authorization", "Bearer " + authorizationToken); request.Content = content; + if (!string.IsNullOrEmpty(authorizationToken)) + { + request.Headers.Add("Authorization", "Bearer " + authorizationToken); + } + if (!string.IsNullOrEmpty(platformAccessToken)) { request.Headers.Add("PlatformAccessToken", platformAccessToken); } - return httpClient.SendAsync(request, CancellationToken.None); + return httpClient.SendAsync(request, cancellationToken); } /// /// Extension that add authorization header to request /// /// The HttpClient - /// the authorization token (jwt) /// The request Uri + /// the authorization token (jwt) /// The platformAccess tokens + /// The /// A HttpResponseMessage - public static Task GetAsync(this HttpClient httpClient, string authorizationToken, string requestUri, string platformAccessToken = null) + public static Task GetAsync(this HttpClient httpClient, string requestUri, string authorizationToken = null, string platformAccessToken = null, CancellationToken cancellationToken = default) { HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Add("Authorization", "Bearer " + authorizationToken); + + if (!string.IsNullOrEmpty(authorizationToken)) + { + request.Headers.Add("Authorization", "Bearer " + authorizationToken); + } + if (!string.IsNullOrEmpty(platformAccessToken)) { request.Headers.Add("PlatformAccessToken", platformAccessToken); } - return httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead, CancellationToken.None); + return httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead, cancellationToken); } } } diff --git a/src/Authorization/Extensions/PlatformAccessTokenDependencyInjectionExtensions.cs b/src/Authorization/Extensions/PlatformAccessTokenDependencyInjectionExtensions.cs new file mode 100644 index 00000000..a9b9f3fe --- /dev/null +++ b/src/Authorization/Extensions/PlatformAccessTokenDependencyInjectionExtensions.cs @@ -0,0 +1,36 @@ +using Altinn.Authorization.Services; +using Altinn.Common.AccessTokenClient.Services; +using Altinn.Platform.Authorization.Configuration; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Altinn.Platform.Authorization.Extensions; + +/// +/// Extension methods for adding services to the dependency injection container in order to support platform service integrations requiring platform access token. +/// +public static class PlatformAccessTokenDependencyInjectionExtensions +{ + /// + /// Registers services to the dependency injection container in order to support platform service integrations requiring platform access token. + /// + /// The . + /// The . + /// Whether the setup is for local dev environment. Will setup the platform token support using the web based Altinn test token generator. + /// for further chaining. + public static IServiceCollection AddPlatformAccessTokenSupport( + this IServiceCollection services, IConfiguration config, bool isDevelopment) + { + if (isDevelopment) + { + services.Configure(config.GetSection("TokenGeneratorSettings")); + services.AddSingleton(); + } + else + { + services.AddSingleton(); + } + + return services; + } +} \ No newline at end of file diff --git a/src/Authorization/Helpers/ServiceResourceHelper.cs b/src/Authorization/Helpers/ServiceResourceHelper.cs new file mode 100644 index 00000000..a66c34ce --- /dev/null +++ b/src/Authorization/Helpers/ServiceResourceHelper.cs @@ -0,0 +1,16 @@ +using System.Text.RegularExpressions; + +namespace Altinn.Authorization.Helpers +{ + /// + /// ServiceResource helper methods + /// + public static partial class ServiceResourceHelper + { + /// + /// Resource identifier regex. + /// + [GeneratedRegex("^[a-z0-9_-]{4,}$")] + internal static partial Regex ResourceIdentifierRegex(); + } +} diff --git a/src/Authorization/Models/AccessListAuthorizationRequest.cs b/src/Authorization/Models/AccessListAuthorizationRequest.cs new file mode 100644 index 00000000..6c975eb9 --- /dev/null +++ b/src/Authorization/Models/AccessListAuthorizationRequest.cs @@ -0,0 +1,34 @@ +#nullable enable + +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; +using Altinn.Authorization.Models.Register; +using Altinn.Authorization.Models.ResourceRegistry; +using Altinn.Urn.Json; + +namespace Altinn.Platform.Authorization.Models; + +/// +/// Contains attribute match info about the reportee party and resource that's to be authorized +/// +public class AccessListAuthorizationRequest +{ + /// + /// Gets or sets the attributes identifying the party to be authorized + /// + [Required] + [JsonRequired] + public UrnJsonTypeValue Subject { get; set; } + + /// + /// Gets or sets the attributes identifying the resource to authorize the party for + /// + [Required] + [JsonRequired] + public UrnJsonTypeValue Resource { get; set; } + + /// + /// Gets or sets an optional action value to authorize + /// + public UrnJsonTypeValue Action { get; set; } +} \ No newline at end of file diff --git a/src/Authorization/Models/AccessListAuthorizationResponse.cs b/src/Authorization/Models/AccessListAuthorizationResponse.cs new file mode 100644 index 00000000..120214e6 --- /dev/null +++ b/src/Authorization/Models/AccessListAuthorizationResponse.cs @@ -0,0 +1,51 @@ +using System; +using Altinn.Authorization.Enums; +using Altinn.Authorization.Models.Register; +using Altinn.Authorization.Models.ResourceRegistry; +using Altinn.Urn.Json; + +namespace Altinn.Platform.Authorization.Models; + +/// +/// Contains attribute match info about the reportee party and resource that's to be authorized +/// +public class AccessListAuthorizationResponse +{ + /// + /// Creates a new from an . + /// + /// The request. + /// The mapped . + public static AccessListAuthorizationResponse From(AccessListAuthorizationRequest request) + { + request = request ?? throw new ArgumentNullException(nameof(request)); + + return new AccessListAuthorizationResponse + { + Subject = request.Subject, + Resource = request.Resource, + Action = request.Action, + Result = AccessListAuthorizationResult.NotDetermined + }; + } + + /// + /// Gets or sets the attributes identifying the party to be authorized + /// + public UrnJsonTypeValue Subject { get; set; } + + /// + /// Gets or sets the attributes identifying the resource to authorize the party for + /// + public UrnJsonTypeValue Resource { get; set; } + + /// + /// Gets or sets an optional action value to authorize + /// + public UrnJsonTypeValue Action { get; set; } + + /// + /// Gets or sets the result of the access list authorization + /// + public AccessListAuthorizationResult Result { get; set; } +} \ No newline at end of file diff --git a/src/Authorization/Models/AccessListAuthorizationResult.cs b/src/Authorization/Models/AccessListAuthorizationResult.cs new file mode 100644 index 00000000..1e52f289 --- /dev/null +++ b/src/Authorization/Models/AccessListAuthorizationResult.cs @@ -0,0 +1,29 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Altinn.Authorization.Enums; + +/// +/// Enum defining the different results of an access list authorization +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum AccessListAuthorizationResult +{ + /// + /// Result is not yet determined + /// + [EnumMember(Value = "NotDetermined")] + NotDetermined, + + /// + /// Subject is not authorized to access the resource through any access lists + /// + [EnumMember(Value = "NotAuthorized")] + NotAuthorized, + + /// + /// Subject is authorized to access the resource through one or more access lists + /// + [EnumMember(Value = "Authorized")] + Authorized +} \ No newline at end of file diff --git a/src/Authorization/Models/ActionIdentifier.cs b/src/Authorization/Models/ActionIdentifier.cs new file mode 100644 index 00000000..f823fd08 --- /dev/null +++ b/src/Authorization/Models/ActionIdentifier.cs @@ -0,0 +1,121 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; +using Altinn.Swashbuckle.Examples; + +namespace Altinn.Authorization.Models; + +/// +/// A xacml action string. +/// +[JsonConverter(typeof(JsonConverter))] +public class ActionIdentifier + : ISpanParsable, + ISpanFormattable, + IExampleDataProvider +{ + private readonly string _value; + + private ActionIdentifier(string value) + { + _value = value; + } + + /// + /// Creates a new from the specified value without validation. + /// + /// The action identifier. + /// A . + public static ActionIdentifier CreateUnchecked(string value) + => new(value); + + /// + public static IEnumerable? GetExamples(ExampleDataOptions options) + { + yield return new ActionIdentifier("read"); + yield return new ActionIdentifier("write"); + } + + /// + public static ActionIdentifier Parse(string s) + => Parse(s, provider: null); + + /// + public static ActionIdentifier Parse(string s, IFormatProvider? provider) + => TryParse(s, provider, out var result) + ? result + : throw new FormatException("Invalid action"); + + /// + public static ActionIdentifier Parse(ReadOnlySpan s) + => Parse(s, provider: null); + + /// + public static ActionIdentifier Parse(ReadOnlySpan s, IFormatProvider? provider) + => TryParse(s, provider, out var result) + ? result + : throw new FormatException("Invalid action"); + + /// + public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out ActionIdentifier result) + => TryParse(s.AsSpan(), s, out result); + + /// + public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, [MaybeNullWhen(false)] out ActionIdentifier result) + => TryParse(s, original: null, out result); + + private static bool TryParse(ReadOnlySpan s, string? original, [MaybeNullWhen(false)] out ActionIdentifier result) + { + result = new ActionIdentifier(original ?? new string(s)); + return true; + } + + /// + public override string ToString() + => _value; + + /// + public string ToString(string? format) + => _value; + + /// + public string ToString(string? format, IFormatProvider? formatProvider) + => _value; + + /// + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) + { + if (destination.Length < _value.Length) + { + charsWritten = 0; + return false; + } + + _value.AsSpan().CopyTo(destination); + charsWritten = _value.Length; + return true; + } + + private sealed class JsonConverter : JsonConverter + { + public override ActionIdentifier? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var str = reader.GetString(); + if (!TryParse(str, null, out var result)) + { + throw new JsonException("Invalid action"); + } + + return result; + } + + public override void Write(Utf8JsonWriter writer, ActionIdentifier value, JsonSerializerOptions options) + { + writer.WriteStringValue(value._value); + } + } +} diff --git a/src/Authorization/Models/ActionUrn.cs b/src/Authorization/Models/ActionUrn.cs new file mode 100644 index 00000000..d8a10caa --- /dev/null +++ b/src/Authorization/Models/ActionUrn.cs @@ -0,0 +1,20 @@ +#nullable enable + +using Altinn.Urn; + +namespace Altinn.Authorization.Models.ResourceRegistry; + +/// +/// A unique reference to an action in the form of an URN. +/// +[KeyValueUrn] +public abstract partial record ActionUrn +{ + /// + /// Try to get the urn as an action. + /// + /// The resulting action. + /// if this is an action, otherwise . + [UrnKey("oasis:names:tc:xacml:1.0:action:action-id")] + public partial bool IsActionId(out ActionIdentifier action); +} diff --git a/src/Authorization/Models/Register/OrganizationNumber.cs b/src/Authorization/Models/Register/OrganizationNumber.cs new file mode 100644 index 00000000..4dbe9e4d --- /dev/null +++ b/src/Authorization/Models/Register/OrganizationNumber.cs @@ -0,0 +1,135 @@ +#nullable enable + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; +using Altinn.Swashbuckle.Examples; + +namespace Altinn.Authorization.Models.Register; + +/// +/// A organization number (a string of 9 digits). +/// +[JsonConverter(typeof(JsonConverter))] +public class OrganizationNumber + : ISpanParsable, + ISpanFormattable, + IExampleDataProvider +{ + private static readonly SearchValues NUMBERS = SearchValues.Create(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']); + private readonly string _value; + + private OrganizationNumber(string value) + { + _value = value; + } + + /// + /// Creates a new from the specified value without validation. + /// + /// The organization identifier. + /// A . + public static OrganizationNumber CreateUnchecked(string value) + => new(value); + + /// + public static IEnumerable? GetExamples(ExampleDataOptions options) + { + yield return new OrganizationNumber("123456789"); + yield return new OrganizationNumber("987654321"); + } + + /// + public static OrganizationNumber Parse(string s) + => Parse(s, provider: null); + + /// + public static OrganizationNumber Parse(string s, IFormatProvider? provider) + => TryParse(s, provider, out var result) + ? result + : throw new FormatException("Invalid organization number"); + + /// + public static OrganizationNumber Parse(ReadOnlySpan s) + => Parse(s, provider: null); + + /// + public static OrganizationNumber Parse(ReadOnlySpan s, IFormatProvider? provider) + => TryParse(s, provider, out var result) + ? result + : throw new FormatException("Invalid organization number"); + + /// + public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out OrganizationNumber result) + => TryParse(s.AsSpan(), s, out result); + + /// + public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, [MaybeNullWhen(false)] out OrganizationNumber result) + => TryParse(s, original: null, out result); + + private static bool TryParse(ReadOnlySpan s, string? original, [MaybeNullWhen(false)] out OrganizationNumber result) + { + if (s.Length != 9) + { + result = null; + return false; + } + + if (s.ContainsAnyExcept(NUMBERS)) + { + result = null; + return false; + } + + result = new OrganizationNumber(original ?? new string(s)); + return true; + } + + /// + public override string ToString() + => _value; + + /// + public string ToString(string? format) + => _value; + + /// + public string ToString(string? format, IFormatProvider? formatProvider) + => _value; + + /// + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) + { + if (destination.Length < _value.Length) + { + charsWritten = 0; + return false; + } + + _value.AsSpan().CopyTo(destination); + charsWritten = _value.Length; + return true; + } + + private sealed class JsonConverter : JsonConverter + { + public override OrganizationNumber? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var str = reader.GetString(); + if (!TryParse(str, null, out var result)) + { + throw new JsonException("Invalid organization number"); + } + + return result; + } + + public override void Write(Utf8JsonWriter writer, OrganizationNumber value, JsonSerializerOptions options) + { + writer.WriteStringValue(value._value); + } + } +} diff --git a/src/Authorization/Models/Register/PartyIdentifiers.cs b/src/Authorization/Models/Register/PartyIdentifiers.cs new file mode 100644 index 00000000..4078c7c8 --- /dev/null +++ b/src/Authorization/Models/Register/PartyIdentifiers.cs @@ -0,0 +1,31 @@ +#nullable enable + +using System; + +namespace Altinn.Authorization.Models.Register; + +/// +/// A set of identifiers for a party. +/// +public record PartyIdentifiers +{ + /// + /// The party id. + /// + public required int PartyId { get; init; } + + /// + /// The party uuid. + /// + public required Guid PartyUuid { get; init; } + + /// + /// The organization number of the party (if applicable). + /// + public required string? OrgNumber { get; init; } + + /// + /// The social security number of the party (if applicable and included). + /// + public string? SSN { get; init; } +} diff --git a/src/Authorization/Models/Register/PartyUrn.cs b/src/Authorization/Models/Register/PartyUrn.cs new file mode 100644 index 00000000..3eab2243 --- /dev/null +++ b/src/Authorization/Models/Register/PartyUrn.cs @@ -0,0 +1,34 @@ +#nullable enable + +using System; +using System.Globalization; +using Altinn.Urn; + +namespace Altinn.Authorization.Models.Register; + +/// +/// A unique reference to a party in the form of an URN. +/// +[KeyValueUrn] +public abstract partial record PartyUrn +{ + /// + /// Try to get the urn as a party uuid. + /// + /// The resulting party uuid. + /// if this party reference is a party uuid, otherwise . + [UrnKey("altinn:party:uuid")] + public partial bool IsPartyUuid(out Guid partyUuid); + + /// + /// Try to get the urn as an organization number. + /// + /// The resulting organization number. + /// if this party reference is an organization number, otherwise . + [UrnKey("altinn:organization:identifier-no")] + public partial bool IsOrganizationIdentifier(out OrganizationNumber organizationNumber); + + // Manually overridden to disallow negative party ids + private static bool TryParsePartyId(ReadOnlySpan segment, IFormatProvider? provider, out int value) + => int.TryParse(segment, NumberStyles.None, provider, out value); +} diff --git a/src/Authorization/Models/ResourceRegistry/AccessListResourceMembershipWithActionFilterDto.cs b/src/Authorization/Models/ResourceRegistry/AccessListResourceMembershipWithActionFilterDto.cs new file mode 100644 index 00000000..2c357ca4 --- /dev/null +++ b/src/Authorization/Models/ResourceRegistry/AccessListResourceMembershipWithActionFilterDto.cs @@ -0,0 +1,29 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Altinn.Authorization.Models.Register; + +namespace Altinn.Authorization.Models.ResourceRegistry; + +/// +/// Represents a party's membership of a access list connected to a specific resource with an optional set of action filters. +/// +/// The party UUID. +/// The resource id. +/// Since when this party has been a member of the list connected to the party. +/// Optional set of action filters. +public record AccessListResourceMembershipWithActionFilterDto( + PartyUrn.PartyUuid Party, + ResourceUrn.ResourceId Resource, + DateTimeOffset Since, + IReadOnlyCollection? ActionFilters) +{ + /// + /// Gets the allowed actions or if all actions are allowed. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IReadOnlyCollection? ActionFilters { get; } + = ActionFilters is null or { Count: 0 } ? null : ActionFilters; +} diff --git a/src/Authorization/Models/ResourceRegistry/AuthorizationReferenceAttribute.cs b/src/Authorization/Models/ResourceRegistry/AuthorizationReferenceAttribute.cs new file mode 100644 index 00000000..a1f0a745 --- /dev/null +++ b/src/Authorization/Models/ResourceRegistry/AuthorizationReferenceAttribute.cs @@ -0,0 +1,17 @@ +namespace Altinn.Authorization.Models.ResourceRegistry; + +/// +/// The reference +/// +public class AuthorizationReferenceAttribute +{ + /// + /// The key for authorization reference. Used for authorization api related to resource + /// + public string Id { get; set; } + + /// + /// The value for authorization reference. Used for authorization api related to resource + /// + public string Value { get; set; } +} diff --git a/src/Authorization/Models/ResourceRegistry/CompetentAuthority.cs b/src/Authorization/Models/ResourceRegistry/CompetentAuthority.cs new file mode 100644 index 00000000..657e3611 --- /dev/null +++ b/src/Authorization/Models/ResourceRegistry/CompetentAuthority.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace Altinn.Authorization.Models.ResourceRegistry; + +/// +/// Model representation of Competent Authority part of the ServiceResource model +/// +public class CompetentAuthority +{ + /// + /// The organization number + /// + public string Organization { get; set; } + + /// + /// The organization code + /// + public string Orgcode { get; set; } + + /// + /// The organization name. If not set it will be retrived from register based on Organization number + /// + public Dictionary Name { get; set; } +} diff --git a/src/Authorization/Models/ResourceRegistry/ContactPoint.cs b/src/Authorization/Models/ResourceRegistry/ContactPoint.cs new file mode 100644 index 00000000..0ee732b8 --- /dev/null +++ b/src/Authorization/Models/ResourceRegistry/ContactPoint.cs @@ -0,0 +1,27 @@ +namespace Altinn.Authorization.Models.ResourceRegistry; + +/// +/// Defines a contact point +/// +public class ContactPoint +{ + /// + /// The type of contact point, phone, email ++ + /// + public string Category { get; set; } + + /// + /// The contact details. The actual phone number, email adress + /// + public string Email { get; set; } + + /// + /// Phone details + /// + public string Telephone { get; set; } + + /// + /// Contact page + /// + public string ContactPage { get; set; } +} diff --git a/src/Authorization/Models/ResourceRegistry/Keyword.cs b/src/Authorization/Models/ResourceRegistry/Keyword.cs new file mode 100644 index 00000000..8dab955e --- /dev/null +++ b/src/Authorization/Models/ResourceRegistry/Keyword.cs @@ -0,0 +1,17 @@ +namespace Altinn.Authorization.Models.ResourceRegistry; + +/// +/// Model for defining keywords +/// +public class Keyword +{ + /// + /// The key word + /// + public string Word { get; set; } + + /// + /// Language of the key word + /// + public string Language { get; set; } +} diff --git a/src/Authorization/Models/ResourceRegistry/ListObject.cs b/src/Authorization/Models/ResourceRegistry/ListObject.cs new file mode 100644 index 00000000..e9e5174d --- /dev/null +++ b/src/Authorization/Models/ResourceRegistry/ListObject.cs @@ -0,0 +1,53 @@ +#nullable enable + +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.Annotations; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Altinn.Authorization.Models.ResourceRegistry; + +/// +/// A list object is a wrapper around a list of items to allow for the API to be +/// extended in the future without breaking backwards compatibility. +/// +[SwaggerSchemaFilter(typeof(SchemaFilter))] +public abstract record ListObject +{ + /// + /// Creates a new from a list of items. + /// + /// The list type. + /// The list of items. + /// A . + public static ListObject Create(IEnumerable items) + => new(items); + + /// + /// Default schema filter for . + /// + protected class SchemaFilter : ISchemaFilter + { + /// + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + foreach (var prop in schema.Properties) + { + schema.Required.Add(prop.Key); + } + + schema.Properties["data"].Nullable = false; + } + } +} + +/// +/// A concrete list object. +/// +/// The item type. +/// The items. +public record ListObject( + [property: JsonPropertyName("data")] + IEnumerable Items) + : ListObject; diff --git a/src/Authorization/Models/ResourceRegistry/ReferenceSource.cs b/src/Authorization/Models/ResourceRegistry/ReferenceSource.cs new file mode 100644 index 00000000..ef756c3a --- /dev/null +++ b/src/Authorization/Models/ResourceRegistry/ReferenceSource.cs @@ -0,0 +1,24 @@ +using System.Runtime.Serialization; + +namespace Altinn.Authorization.Models.ResourceRegistry; + +/// +/// Enum for the different reference sources for resources in the resource registry +/// +public enum ReferenceSource +{ + [EnumMember(Value = "Default")] + Default = 0, + + [EnumMember(Value = "Altinn1")] + Altinn1 = 1, + + [EnumMember(Value = "Altinn2")] + Altinn2 = 2, + + [EnumMember(Value = "Altinn3")] + Altinn3 = 3, + + [EnumMember(Value = "ExternalPlatform")] + ExternalPlatform = 4 +} diff --git a/src/Authorization/Models/ResourceRegistry/ReferenceType.cs b/src/Authorization/Models/ResourceRegistry/ReferenceType.cs new file mode 100644 index 00000000..93f36cb1 --- /dev/null +++ b/src/Authorization/Models/ResourceRegistry/ReferenceType.cs @@ -0,0 +1,30 @@ +using System.Runtime.Serialization; + +namespace Altinn.Authorization.Models.ResourceRegistry; + +/// +/// Enum for reference types of resources in the resource registry +/// +public enum ReferenceType +{ + [EnumMember(Value = "Default")] + Default = 0, + + [EnumMember(Value = "Uri")] + Uri = 1, + + [EnumMember(Value = "DelegationSchemeId")] + DelegationSchemeId = 2, + + [EnumMember(Value = "MaskinportenScope")] + MaskinportenScope = 3, + + [EnumMember(Value = "ServiceCode")] + ServiceCode = 4, + + [EnumMember(Value = "ServiceEditionCode")] + ServiceEditionCode = 5, + + [EnumMember(Value = "ApplicationId")] + ApplicationId = 6, +} diff --git a/src/Authorization/Models/ResourceRegistry/ResourceAccessListMode.cs b/src/Authorization/Models/ResourceRegistry/ResourceAccessListMode.cs new file mode 100644 index 00000000..fb4a1d7d --- /dev/null +++ b/src/Authorization/Models/ResourceRegistry/ResourceAccessListMode.cs @@ -0,0 +1,11 @@ +namespace Altinn.Authorization.Models.ResourceRegistry; + +/// +/// Enum representation of the different types of ResourceAccessListModes supported by the resource registry +/// +public enum ResourceAccessListMode +{ + Disabled = 0, + + Enabled = 1 +} diff --git a/src/Authorization/Models/ResourceRegistry/ResourceIdUrn.cs b/src/Authorization/Models/ResourceRegistry/ResourceIdUrn.cs new file mode 100644 index 00000000..95daa97e --- /dev/null +++ b/src/Authorization/Models/ResourceRegistry/ResourceIdUrn.cs @@ -0,0 +1,20 @@ +#nullable enable + +using Altinn.Urn; + +namespace Altinn.Authorization.Models.ResourceRegistry; + +/// +/// A unique reference to a resource in the form of an URN. +/// +[KeyValueUrn] +public abstract partial record ResourceIdUrn +{ + /// + /// Try to get the urn as a resource id. + /// + /// The resulting resource id. + /// if this resource reference is a resource id, otherwise . + [UrnKey("altinn:resource")] + public partial bool IsResourceId(out ResourceIdentifier resourceId); +} diff --git a/src/Authorization/Models/ResourceRegistry/ResourceIdentifier.cs b/src/Authorization/Models/ResourceRegistry/ResourceIdentifier.cs new file mode 100644 index 00000000..ef41e8fa --- /dev/null +++ b/src/Authorization/Models/ResourceRegistry/ResourceIdentifier.cs @@ -0,0 +1,130 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; +using Altinn.Authorization.Helpers; +using Altinn.Swashbuckle.Examples; + +namespace Altinn.Authorization.Models.ResourceRegistry; + +/// +/// A valid resource identifier. +/// +[JsonConverter(typeof(JsonConverter))] +[DebuggerDisplay("{_value}")] +public sealed record ResourceIdentifier + : ISpanParsable, + ISpanFormattable, + IExampleDataProvider +{ + private readonly string _value; + + private ResourceIdentifier(string value) + { + _value = value; + } + + /// + public static IEnumerable? GetExamples(ExampleDataOptions options) + { + yield return new ResourceIdentifier("example-resourceid"); + yield return new ResourceIdentifier("app_skd_flyttemelding"); + } + + /// + /// Creates a new from the specified value without validation. + /// + /// The resource identifier. + /// A . + public static ResourceIdentifier CreateUnchecked(string value) + => new(value); + + /// + public static ResourceIdentifier Parse(string s) + => Parse(s, provider: null); + + /// + public static ResourceIdentifier Parse(string s, IFormatProvider? provider) + => TryParse(s, provider, out var result) + ? result + : throw new FormatException("Invalid resource identifier"); + + /// + public static ResourceIdentifier Parse(ReadOnlySpan s) + => Parse(s, provider: null); + + /// + public static ResourceIdentifier Parse(ReadOnlySpan s, IFormatProvider? provider) + => TryParse(s, provider, out var result) + ? result + : throw new FormatException("Invalid resource identifier"); + + /// + public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out ResourceIdentifier result) + => TryParse(s.AsSpan(), s, out result); + + /// + public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, [MaybeNullWhen(false)] out ResourceIdentifier result) + => TryParse(s, original: null, out result); + + private static bool TryParse(ReadOnlySpan s, string? original, [MaybeNullWhen(false)] out ResourceIdentifier result) + { + if (!ServiceResourceHelper.ResourceIdentifierRegex().IsMatch(s)) + { + result = null; + return false; + } + + result = new ResourceIdentifier(original ?? new string(s)); + return true; + } + + /// + public override string ToString() + => _value; + + /// + public string ToString(string? format) + => _value; + + /// + public string ToString(string? format, IFormatProvider? formatProvider) + => _value; + + /// + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) + { + if (destination.Length < _value.Length) + { + charsWritten = 0; + return false; + } + + _value.AsSpan().CopyTo(destination); + charsWritten = _value.Length; + return true; + } + + private sealed class JsonConverter : JsonConverter + { + public override ResourceIdentifier? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var str = reader.GetString(); + if (!TryParse(str, null, out var result)) + { + throw new JsonException("Invalid resource identifier"); + } + + return result; + } + + public override void Write(Utf8JsonWriter writer, ResourceIdentifier value, JsonSerializerOptions options) + { + writer.WriteStringValue(value._value); + } + } +} diff --git a/src/Authorization/Models/ResourceRegistry/ResourcePartyType.cs b/src/Authorization/Models/ResourceRegistry/ResourcePartyType.cs new file mode 100644 index 00000000..2e83b277 --- /dev/null +++ b/src/Authorization/Models/ResourceRegistry/ResourcePartyType.cs @@ -0,0 +1,26 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Altinn.Authorization.Models.ResourceRegistry; + +/// +/// Defines the type of party that a resource is targeting +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum ResourcePartyType +{ + [EnumMember(Value = "PrivatePerson")] + PrivatePerson = 0, + + [EnumMember(Value = "LegalEntityEnterprise")] + LegalEntityEnterprise = 1, + + [EnumMember(Value = "Company")] + Company = 2, + + [EnumMember(Value = "BankruptcyEstate")] + BankruptcyEstate = 3, + + [EnumMember(Value = "SelfRegisteredUser")] + SelfRegisteredUser = 4 +} diff --git a/src/Authorization/Models/ResourceRegistry/ResourceReference.cs b/src/Authorization/Models/ResourceRegistry/ResourceReference.cs new file mode 100644 index 00000000..5a73bb94 --- /dev/null +++ b/src/Authorization/Models/ResourceRegistry/ResourceReference.cs @@ -0,0 +1,27 @@ +#nullable enable +using System.Text.Json.Serialization; + +namespace Altinn.Authorization.Models.ResourceRegistry; + +/// +/// Model representation of the resource reference part of the ServiceResource model +/// +public class ResourceReference +{ + /// + /// The source the reference identifier points to + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + public ReferenceSource? ReferenceSource { get; set; } + + /// + /// The reference identifier + /// + public string? Reference { get; set; } + + /// + /// The reference type + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + public ReferenceType? ReferenceType { get; set; } +} diff --git a/src/Authorization/Models/ResourceRegistry/ResourceType.cs b/src/Authorization/Models/ResourceRegistry/ResourceType.cs new file mode 100644 index 00000000..6dd8b1a0 --- /dev/null +++ b/src/Authorization/Models/ResourceRegistry/ResourceType.cs @@ -0,0 +1,33 @@ +using NpgsqlTypes; + +namespace Altinn.Authorization.Models.ResourceRegistry; + +/// +/// Enum representation of the different types of resources supported by the resource registry +/// +public enum ResourceType +{ + [PgName("default")] + Default = 0, + + [PgName("systemresource")] + Systemresource = 1 << 0, + + [PgName("maskinportenschema")] + MaskinportenSchema = 1 << 1, + + [PgName("altinn2service")] + Altinn2Service = 1 << 2, + + [PgName("altinnapp")] + AltinnApp = 1 << 3, + + [PgName("genericaccessresource")] + GenericAccessResource = 1 << 4, + + [PgName("brokerservice")] + BrokerService = 1 << 5, + + [PgName("correspondenceservice")] + CorrespondenceService = 1 << 6, +} diff --git a/src/Authorization/Models/ResourceRegistry/ResourceUrn.cs b/src/Authorization/Models/ResourceRegistry/ResourceUrn.cs new file mode 100644 index 00000000..9b191438 --- /dev/null +++ b/src/Authorization/Models/ResourceRegistry/ResourceUrn.cs @@ -0,0 +1,20 @@ +#nullable enable + +using Altinn.Urn; + +namespace Altinn.Authorization.Models.ResourceRegistry; + +/// +/// A unique reference to a resource in the form of an URN. +/// +[KeyValueUrn] +public abstract partial record ResourceUrn +{ + /// + /// Try to get the urn as a resource id. + /// + /// The resulting resource id. + /// if this resource reference is a resource id, otherwise . + [UrnKey("altinn:resource")] + public partial bool IsResourceId(out ResourceIdentifier resourceId); +} diff --git a/src/Authorization/Models/ResourceRegistry/ServiceResource.cs b/src/Authorization/Models/ResourceRegistry/ServiceResource.cs new file mode 100644 index 00000000..29a5c802 --- /dev/null +++ b/src/Authorization/Models/ResourceRegistry/ServiceResource.cs @@ -0,0 +1,144 @@ +#nullable enable +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Altinn.Authorization.Models.ResourceRegistry; + +/// +/// Model describing a complete resource from the resrouce registry +/// +public class ServiceResource +{ + /// + /// The identifier of the resource + /// + [Required] + public string? Identifier { get; set; } + + /// + /// The version of the resource + /// + public string? Version { get; set; } + + /// + /// The title of service + /// + [Required] + public Dictionary? Title { get; set; } + + /// + /// Description + /// + [Required] + public Dictionary? Description { get; set; } + + /// + /// Description explaining the rights a recipient will receive if given access to the resource + /// + public Dictionary? RightDescription { get; set; } + + /// + /// The homepage + /// + public string? Homepage { get; set; } + + /// + /// The status + /// + public string? Status { get; set; } + + /// + /// spatial coverage + /// This property represents that area(s) a Public Service is likely to be available only within, typically the area(s) covered by a particular public authority. + /// + public List? Spatial { get; set; } + + /// + /// List of possible contact points + /// + [Required] + public List? ContactPoints { get; set; } + + /// + /// Linkes to the outcome of a public service + /// + public List? Produces { get; set; } + + /// + /// IsPartOf + /// + public string? IsPartOf { get; set; } + + /// + /// ThematicAreas + /// + public List? ThematicAreas { get; set; } + + /// + /// ResourceReference + /// + public List? ResourceReferences { get; set; } + + /// + /// Is this resource possible to delegate to others or not + /// + public bool Delegable { get; set; } = true; + + /// + /// The visibility of the resource + /// + public bool Visible { get; set; } = true; + + /// + /// HasCompetentAuthority + /// + [Required] + public CompetentAuthority? HasCompetentAuthority { get; set; } + + /// + /// Keywords + /// + public List? Keywords { get; set; } + + /// + /// Sets the access list mode for the resource + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + public ResourceAccessListMode AccessListMode { get; set; } + + /// + /// The user acting on behalf of party can be a selfidentifed users + /// + public bool SelfIdentifiedUserEnabled { get; set; } + + /// + /// The user acting on behalf of party can be an enterprise users + /// + public bool EnterpriseUserEnabled { get; set; } + + /// + /// ResourceType + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + public ResourceType ResourceType { get; set; } + + /// + /// Available for type defines which type of entity / person that resource targets + /// + public List? AvailableForType { get; set; } + + /// + /// List of autorizationReference attributes to reference this resource in authorization API + /// + public List? AuthorizationReference { get; set; } + + /// + /// Writes key information when this object is written to Log. + /// + /// + public override string ToString() + { + return $"Identifier: {Identifier}, ResourceType: {ResourceType}"; + } +} diff --git a/src/Authorization/Program.cs b/src/Authorization/Program.cs index ce1b798e..b83f03a1 100644 --- a/src/Authorization/Program.cs +++ b/src/Authorization/Program.cs @@ -1,15 +1,20 @@ using System; +using System.Collections.Generic; using System.IO; using System.Reflection; using System.Threading.Tasks; using Altinn.ApiClients.Maskinporten.Extensions; using Altinn.ApiClients.Maskinporten.Services; +using Altinn.Common.AccessToken; +using Altinn.Common.AccessToken.Configuration; +using Altinn.Common.AccessToken.Services; using Altinn.Common.AccessTokenClient.Services; using Altinn.Common.PEP.Authorization; using Altinn.Platform.Authorization.Clients; using Altinn.Platform.Authorization.Clients.Interfaces; using Altinn.Platform.Authorization.Configuration; using Altinn.Platform.Authorization.Constants; +using Altinn.Platform.Authorization.Extensions; using Altinn.Platform.Authorization.Filters; using Altinn.Platform.Authorization.Health; using Altinn.Platform.Authorization.ModelBinding; @@ -20,8 +25,6 @@ using Altinn.Platform.Authorization.Services.Interface; using Altinn.Platform.Authorization.Services.Interfaces; using Altinn.Platform.Telemetry; - -using AltinnCore.Authentication.Constants; using AltinnCore.Authentication.JwtCookie; using Azure.Identity; @@ -46,7 +49,7 @@ using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; using Npgsql; - +using Swashbuckle.AspNetCore.Filters; using Yuniql.AspNetCore; using Yuniql.PostgreSql; @@ -213,12 +216,15 @@ void ConfigureServices(IServiceCollection services, IConfiguration config) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.Configure(config.GetSection("GeneralSettings")); services.Configure(config.GetSection("AzureStorageConfiguration")); services.Configure(config.GetSection("AzureCosmosSettings")); services.Configure(config.GetSection("PostgreSQLSettings")); services.Configure(config.GetSection("PlatformSettings")); + services.Configure(config.GetSection("kvSetting")); OedAuthzMaskinportenClientSettings oedAuthzMaskinportenClientSettings = config.GetSection("OedAuthzMaskinportenClientSettings").Get(); services.Configure(config.GetSection("OedAuthzMaskinportenClientSettings")); services.AddMaskinportenHttpClient(oedAuthzMaskinportenClientSettings); @@ -232,7 +238,6 @@ void ConfigureServices(IServiceCollection services, IConfiguration config) services.AddHttpClient(); services.AddHttpClient(); services.TryAddSingleton(); - services.AddSingleton(); services.AddTransient(); services.AddSingleton(); services.AddSingleton(); @@ -263,12 +268,16 @@ void ConfigureServices(IServiceCollection services, IConfiguration config) { options.AddPolicy(AuthzConstants.POLICY_STUDIO_DESIGNER, policy => policy.Requirements.Add(new ClaimAccessRequirement("urn:altinn:app", "studio.designer"))); options.AddPolicy(AuthzConstants.ALTINNII_AUTHORIZATION, policy => policy.Requirements.Add(new ClaimAccessRequirement("urn:altinn:app", "sbl.authorization"))); + options.AddPolicy(AuthzConstants.POLICY_PLATFORMISSUER_ACCESSTOKEN, policy => policy.Requirements.Add(new AccessTokenRequirement(AuthzConstants.PLATFORM_ACCESSTOKEN_ISSUER))); options.AddPolicy(AuthzConstants.DELEGATIONEVENT_FUNCTION_AUTHORIZATION, policy => policy.Requirements.Add(new ClaimAccessRequirement("urn:altinn:app", "platform.authorization"))); - options.AddPolicy(AuthzConstants.AUTHORIZESCOPEACCESS, policy => policy.Requirements.Add(new ScopeAccessRequirement(new string[] { AuthzConstants.PDP_SCOPE, AuthzConstants.AUTHORIZE_SCOPE, AuthzConstants.AUTHORIZE_ADMIN_SCOPE }))); + options.AddPolicy(AuthzConstants.AUTHORIZESCOPEACCESS, policy => policy.Requirements.Add(new ScopeAccessRequirement([AuthzConstants.AUTHORIZE_SCOPE, AuthzConstants.AUTHORIZE_ADMIN_SCOPE]))); }); services.AddTransient(); services.AddTransient(); + services.AddSingleton(); + + services.AddPlatformAccessTokenSupport(config, builder.Environment.IsDevelopment()); services.Configure(options => { @@ -298,21 +307,61 @@ void ConfigureServices(IServiceCollection services, IConfiguration config) }); // Add Swagger support (Swashbuckle) - services.AddSwaggerGen(c => + services.AddSwaggerGen(options => { - c.SwaggerDoc("v1", new OpenApiInfo { Title = "Altinn Platform Authorization", Version = "v1" }); + options.SwaggerDoc("v1", new OpenApiInfo { Title = "Altinn Platform Authorization", Version = "v1" }); try { string filePath = GetXmlCommentsPathForControllers(); - c.IncludeXmlComments(filePath); + options.IncludeXmlComments(filePath); } catch { // catch swashbuckle exception if it doesn't find the generated xml documentation file } + + options.AddSecurityDefinition("AuthorizeAPI", new OpenApiSecurityScheme + { + Name = "AuthorizeAPI", + Description = $"Requires one of the following Scopes: [{AuthzConstants.AUTHORIZE_SCOPE}, {AuthzConstants.AUTHORIZE_ADMIN_SCOPE}]", + Type = SecuritySchemeType.Http, + In = ParameterLocation.Header, + Scheme = "bearer", + BearerFormat = "JWT" + }); + options.AddSecurityDefinition("SubscriptionKey", new OpenApiSecurityScheme + { + Name = "SubscriptionKey", + Description = $"Requires a valid product subscription key as header value: \"Ocp-Apim-Subscription-Key\"", + Type = SecuritySchemeType.ApiKey, + In = ParameterLocation.Header + }); + options.OperationFilter(); + + var originalIdSelector = options.SchemaGeneratorOptions.SchemaIdSelector; + options.SchemaGeneratorOptions.SchemaIdSelector = (Type t) => + { + if (!t.IsNested) + { + return originalIdSelector(t); + } + + var chain = new List(); + do + { + chain.Add(originalIdSelector(t)); + t = t.DeclaringType; + } + while (t != null); + + chain.Reverse(); + return string.Join(".", chain); + }; }); + services.AddUrnSwaggerSupport(); + services.AddFeatureManagement(); } diff --git a/src/Authorization/Services/Implementation/AccessListAuthorization.cs b/src/Authorization/Services/Implementation/AccessListAuthorization.cs new file mode 100644 index 00000000..8983baf2 --- /dev/null +++ b/src/Authorization/Services/Implementation/AccessListAuthorization.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Altinn.Authorization.Enums; +using Altinn.Authorization.Models.ResourceRegistry; +using Altinn.Authorization.ProblemDetails; +using Altinn.Platform.Authorization.Models; +using Altinn.Platform.Authorization.Services.Interface; + +namespace Altinn.Platform.Authorization.Services.Implementation; + +/// +/// The service used to map internal delegation change to delegation change events and push them to the event queue. +/// +public class AccessListAuthorization : IAccessListAuthorization +{ + private readonly IResourceRegistry _client; + + /// + /// Initializes a new instance of the class. + /// + public AccessListAuthorization(IResourceRegistry client) + { + _client = client; + } + + /// + public async Task> Authorize(AccessListAuthorizationRequest request, CancellationToken cancellationToken = default) + { + AccessListAuthorizationResponse response = AccessListAuthorizationResponse.From(request); + IEnumerable memberships = await _client.GetMembershipsForResourceForParty(request.Subject.Value, request.Resource.Value, cancellationToken); + + if (memberships == null || !memberships.Any()) + { + response.Result = AccessListAuthorizationResult.NotAuthorized; + } + else if (memberships.Any(m => m.ActionFilters == null || m.ActionFilters.Any(actionFilter => actionFilter == request.Action.Value.ValueSpan.ToString()))) + { + response.Result = AccessListAuthorizationResult.Authorized; + } + else + { + response.Result = AccessListAuthorizationResult.NotAuthorized; + } + + return new(response); + } +} diff --git a/src/Authorization/Services/Implementation/ContextHandler.cs b/src/Authorization/Services/Implementation/ContextHandler.cs index 6fa7a1bc..d0fd578d 100644 --- a/src/Authorization/Services/Implementation/ContextHandler.cs +++ b/src/Authorization/Services/Implementation/ContextHandler.cs @@ -183,11 +183,11 @@ protected async Task EnrichResourceParty(XacmlContextAttributes requestResourceA { if (string.IsNullOrEmpty(resourceAttributes.ResourcePartyValue) && !string.IsNullOrEmpty(resourceAttributes.OrganizationNumber)) { - int partyId = await _registerService.PartyLookup(resourceAttributes.OrganizationNumber, null); - if (partyId != 0) + Party party = await _registerService.PartyLookup(resourceAttributes.OrganizationNumber, null); + if (party != null) { - resourceAttributes.ResourcePartyValue = partyId.ToString(); - requestResourceAttributes.Attributes.Add(GetPartyIdsAttribute(new List { partyId })); + resourceAttributes.ResourcePartyValue = party.PartyId.ToString(); + requestResourceAttributes.Attributes.Add(GetPartyIdsAttribute(new List { party.PartyId })); } } else if (string.IsNullOrEmpty(resourceAttributes.ResourcePartyValue) && !string.IsNullOrEmpty(resourceAttributes.PersonId)) @@ -197,11 +197,11 @@ protected async Task EnrichResourceParty(XacmlContextAttributes requestResourceA throw new ArgumentException("Not allowed to use ssn for internal API"); } - int partyId = await _registerService.PartyLookup(null, resourceAttributes.PersonId); - if (partyId != 0) + Party party = await _registerService.PartyLookup(null, resourceAttributes.PersonId); + if (party != null) { - resourceAttributes.ResourcePartyValue = partyId.ToString(); - requestResourceAttributes.Attributes.Add(GetPartyIdsAttribute(new List { partyId })); + resourceAttributes.ResourcePartyValue = party.PartyId.ToString(); + requestResourceAttributes.Attributes.Add(GetPartyIdsAttribute(new List { party.PartyId })); } } } @@ -397,8 +397,8 @@ protected async Task EnrichSubjectAttributes(XacmlContextRequest request, string if (isExternalRequest && !string.IsNullOrEmpty(subjectOrgnNo)) { - int partyId = await _registerService.PartyLookup(subjectOrgnNo, null); - subjectContextAttributes.Attributes.Add(GetPartyIdsAttribute(new List { partyId })); + Party party = await _registerService.PartyLookup(subjectOrgnNo, null); + subjectContextAttributes.Attributes.Add(GetPartyIdsAttribute(new List { party.PartyId })); } // No need for further enrichment of roles of no user subject exists diff --git a/src/Authorization/Services/Implementation/DelegationContextHandler.cs b/src/Authorization/Services/Implementation/DelegationContextHandler.cs index 747d6777..7f23f6b4 100644 --- a/src/Authorization/Services/Implementation/DelegationContextHandler.cs +++ b/src/Authorization/Services/Implementation/DelegationContextHandler.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; -using Altinn.Authorization.ABAC.Interface; +using Altinn.Authorization.ABAC.Constants; using Altinn.Authorization.ABAC.Xacml; using Altinn.Platform.Authorization.Configuration; using Altinn.Platform.Authorization.Constants; @@ -112,6 +113,13 @@ public XacmlResourceAttributes GetResourceAttributes(XacmlContextRequest request return GetResourceAttributeValues(resourceContextAttributes); } + /// + public string GetActionString(XacmlContextRequest request) + { + XacmlContextAttributes actionAttributes = request.Attributes.FirstOrDefault(a => a.Category.OriginalString.Equals(XacmlConstants.MatchAttributeCategory.Action)); + return actionAttributes?.Attributes.FirstOrDefault(a => a.AttributeId.OriginalString.Equals(XacmlConstants.MatchAttributeIdentifiers.ActionId))?.AttributeValues.FirstOrDefault()?.Value; + } + /// /// Gets the list of mainunits for a subunit /// diff --git a/src/Authorization/Services/Implementation/DevAccessTokenGenerator.cs b/src/Authorization/Services/Implementation/DevAccessTokenGenerator.cs new file mode 100644 index 00000000..299618f0 --- /dev/null +++ b/src/Authorization/Services/Implementation/DevAccessTokenGenerator.cs @@ -0,0 +1,47 @@ +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using System.Security.Cryptography.X509Certificates; +using Altinn.Common.AccessTokenClient.Services; +using Altinn.Platform.Authorization.Configuration; +using Microsoft.Extensions.Options; + +namespace Altinn.Authorization.Services; + +/// +/// Sets up an access token generator for development environment using the web based Altinn test token generator. +/// +[ExcludeFromCodeCoverage] +public class DevAccessTokenGenerator : IAccessTokenGenerator +{ + private readonly TokenGeneratorSettings _settings; + + /// + /// Initializes a new instance of the class. + /// + public DevAccessTokenGenerator(IOptions settings) + { + _settings = settings.Value; + } + + /// + public string GenerateAccessToken(string issuer, string app) + { + return GetToken(app); + } + + /// + public string GenerateAccessToken(string issuer, string app, X509Certificate2 certificate) + { + return GetToken(app); + } + + private string GetToken(string app) + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{_settings.Url}?env={_settings.Env}&app={app}"); + request.Headers.Authorization = new BasicAuthenticationHeaderValue(_settings.User, _settings.Password); + + using HttpClient client = new HttpClient(); + var response = client.SendAsync(request).Result.EnsureSuccessStatusCode(); + return response.Content.ReadAsStringAsync().Result; + } +} diff --git a/src/Authorization/Services/Implementation/RegisterService.cs b/src/Authorization/Services/Implementation/RegisterService.cs index 2700b90c..4e92e817 100644 --- a/src/Authorization/Services/Implementation/RegisterService.cs +++ b/src/Authorization/Services/Implementation/RegisterService.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Http; using System.Net.Http.Headers; @@ -12,6 +13,7 @@ using Altinn.Platform.Register.Models; using AltinnCore.Authentication.Utils; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -20,6 +22,7 @@ namespace Altinn.Platform.Authorization.Services /// /// Handles register service /// + [ExcludeFromCodeCoverage] public class RegisterService : IRegisterService { private readonly HttpClient _client; @@ -27,6 +30,7 @@ public class RegisterService : IRegisterService private readonly GeneralSettings _generalSettings; private readonly IAccessTokenGenerator _accessTokenGenerator; private readonly ILogger _logger; + private readonly IMemoryCache _memoryCache; private readonly JsonSerializerOptions _serializerOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; /// @@ -38,7 +42,8 @@ public RegisterService( IAccessTokenGenerator accessTokenGenerator, IOptions generalSettings, IOptions platformSettings, - ILogger logger) + ILogger logger, + IMemoryCache memoryCache) { httpClient.BaseAddress = new Uri(platformSettings.Value.ApiRegisterEndpoint); httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); @@ -47,58 +52,94 @@ public RegisterService( _generalSettings = generalSettings.Value; _accessTokenGenerator = accessTokenGenerator; _logger = logger; + _memoryCache = memoryCache; } /// public async Task GetParty(int partyId) { - Party party = null; + string cacheKey = $"p:{partyId}"; + if (!_memoryCache.TryGetValue(cacheKey, out Party party)) + { + string endpointUrl = $"parties/{partyId}"; + string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _generalSettings.RuntimeCookieName); + string accessToken = _accessTokenGenerator.GenerateAccessToken("platform", "authorization"); - string endpointUrl = $"parties/{partyId}"; - string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _generalSettings.RuntimeCookieName); - string accessToken = _accessTokenGenerator.GenerateAccessToken("platform", "authorization"); + HttpResponseMessage response = await _client.GetAsync(endpointUrl, token, accessToken); - HttpResponseMessage response = await _client.GetAsync(token, endpointUrl, accessToken); - if (response.StatusCode == HttpStatusCode.OK) - { - string responseContent = await response.Content.ReadAsStringAsync(); - party = JsonSerializer.Deserialize(responseContent, _serializerOptions); - } - else - { - _logger.LogError("// Getting party with partyID {partyId} failed with statuscode {response.StatusCode}", partyId, response.StatusCode); + if (response.StatusCode == HttpStatusCode.OK) + { + string responseContent = await response.Content.ReadAsStringAsync(); + party = JsonSerializer.Deserialize(responseContent, _serializerOptions); + PutInCache(cacheKey, 10, party); + } + else + { + _logger.LogError("// Getting party with partyID {partyId} failed with statuscode {response.StatusCode}", partyId, response.StatusCode); + } } return party; } /// - public async Task PartyLookup(string orgNo, string person) + public async Task PartyLookup(string orgNo, string person) { - string endpointUrl = "parties/lookup"; - - PartyLookup partyLookup = new PartyLookup() { Ssn = person, OrgNo = orgNo }; + string cacheKey; + PartyLookup partyLookup; - string bearerToken = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _generalSettings.RuntimeCookieName); - string accessToken = _accessTokenGenerator.GenerateAccessToken("platform", "authorization"); - - StringContent content = new StringContent(JsonSerializer.Serialize(partyLookup)); - content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - - HttpResponseMessage response = await _client.PostAsync(bearerToken, endpointUrl, content, accessToken); - if (response.StatusCode == HttpStatusCode.OK) + if (!string.IsNullOrWhiteSpace(orgNo)) + { + cacheKey = $"org:{orgNo}"; + partyLookup = new PartyLookup { OrgNo = orgNo }; + } + else if (!string.IsNullOrWhiteSpace(person)) { - string responseContent = await response.Content.ReadAsStringAsync(); - Party party = JsonSerializer.Deserialize(responseContent, _serializerOptions); - return party.PartyId; + cacheKey = $"fnr:{person}"; + partyLookup = new PartyLookup { Ssn = person }; } else { - string reason = await response.Content.ReadAsStringAsync(); - _logger.LogError("// RegisterService // PartyLookup // Failed to lookup party in platform register. Response {response}. \n Reason {reason}.", response, reason); + return null; + } - throw await PlatformHttpException.CreateAsync(response); + if (!_memoryCache.TryGetValue(cacheKey, out Party party)) + { + string endpointUrl = "parties/lookup"; + + string bearerToken = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _generalSettings.RuntimeCookieName); + string accessToken = _accessTokenGenerator.GenerateAccessToken("platform", "authorization"); + + StringContent content = new StringContent(JsonSerializer.Serialize(partyLookup)); + content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + + HttpResponseMessage response = await _client.PostAsync(endpointUrl, content, bearerToken, accessToken); + + if (response.StatusCode == HttpStatusCode.OK) + { + string responseContent = await response.Content.ReadAsStringAsync(); + party = JsonSerializer.Deserialize(responseContent, _serializerOptions); + PutInCache(cacheKey, 10, party); + } + else + { + string reason = await response.Content.ReadAsStringAsync(); + _logger.LogError("// RegisterService // PartyLookup // Failed to lookup party in platform register. Response {response}. \n Reason {reason}.", response, reason); + + throw await PlatformHttpException.CreateAsync(response); + } } + + return party; + } + + private void PutInCache(string cachekey, int cacheTimeout, object cacheObject) + { + var cacheEntryOptions = new MemoryCacheEntryOptions() + .SetPriority(CacheItemPriority.High) + .SetAbsoluteExpiration(new TimeSpan(0, cacheTimeout, 0)); + + _memoryCache.Set(cachekey, cacheObject, cacheEntryOptions); } } } diff --git a/src/Authorization/Services/Implementation/ResourceRegistryWrapper.cs b/src/Authorization/Services/Implementation/ResourceRegistryWrapper.cs index 1f41ae01..adb451a5 100644 --- a/src/Authorization/Services/Implementation/ResourceRegistryWrapper.cs +++ b/src/Authorization/Services/Implementation/ResourceRegistryWrapper.cs @@ -1,12 +1,21 @@ using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Net; using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using Altinn.Authorization.ABAC.Xacml; +using Altinn.Authorization.Models.Register; +using Altinn.Authorization.Models.ResourceRegistry; +using Altinn.Common.AccessTokenClient.Services; using Altinn.Platform.Authorization.Clients; using Altinn.Platform.Authorization.Configuration; +using Altinn.Platform.Authorization.Extensions; using Altinn.Platform.Authorization.Helpers; -using Altinn.Platform.Authorization.Models; using Altinn.Platform.Authorization.Services.Interface; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; @@ -16,53 +25,102 @@ namespace Altinn.Platform.Authorization.Services.Implementation /// /// Wrapper for resource registry /// + [ExcludeFromCodeCoverage] public class ResourceRegistryWrapper : IResourceRegistry { - private readonly ResourceRegistryClient _client; + private readonly ResourceRegistryClient _resourceRegistry; + private readonly IAccessTokenGenerator _accessTokenGenerator; private readonly IMemoryCache _memoryCache; private readonly GeneralSettings _generalSettings; + private readonly JsonSerializerOptions _jsonOptions; /// /// Initializes a new instance of the class. /// - public ResourceRegistryWrapper(ResourceRegistryClient resourceRegistryClient, IMemoryCache memoryCache, IOptions settings) + public ResourceRegistryWrapper(ResourceRegistryClient resourceRegistryClient, IAccessTokenGenerator accessTokenGenerator, IMemoryCache memoryCache, IOptions settings) { - _client = resourceRegistryClient; + _resourceRegistry = resourceRegistryClient; _generalSettings = settings.Value; _memoryCache = memoryCache; + _accessTokenGenerator = accessTokenGenerator; + _jsonOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + } + + /// + public async Task GetResourceAsync(string resourceId, CancellationToken cancellationToken = default) + { + string cacheKey = "r:" + resourceId; + if (!_memoryCache.TryGetValue(cacheKey, out ServiceResource resource)) + { + string apiurl = $"resource/{resourceId}"; + HttpResponseMessage response = await _resourceRegistry.Client.GetAsync(apiurl, cancellationToken); + + if (response.StatusCode == HttpStatusCode.OK) + { + resource = await response.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken); + PutInCache(cacheKey, _generalSettings.PolicyCacheTimeout, resource); + } + } + + return resource; } /// - public async Task GetResourcePolicyAsync(string resourceId) + public async Task GetResourcePolicyAsync(string resourceId, CancellationToken cancellationToken = default) { string cacheKey = "resourcepolicy:" + resourceId; if (!_memoryCache.TryGetValue(cacheKey, out XacmlPolicy policy)) { string apiurl = $"resource/{resourceId}/policy"; - HttpResponseMessage response = await _client.Client.GetAsync(apiurl); + HttpResponseMessage response = await _resourceRegistry.Client.GetAsync(apiurl, cancellationToken); - if (response.StatusCode == System.Net.HttpStatusCode.OK) + if (response.StatusCode == HttpStatusCode.OK) { - Stream policyBlob = await response.Content.ReadAsStreamAsync(); + Stream policyBlob = await response.Content.ReadAsStreamAsync(cancellationToken); using (policyBlob) { policy = (policyBlob.Length > 0) ? PolicyHelper.ParsePolicy(policyBlob) : null; } - PutXacmlPolicyInCache(cacheKey, policy); + PutInCache(cacheKey, _generalSettings.PolicyCacheTimeout, policy); } } return policy; } - private void PutXacmlPolicyInCache(string cachekey, XacmlPolicy policy) + /// + public async Task> GetMembershipsForResourceForParty(PartyUrn partyUrn, ResourceIdUrn resourceIdUrn, CancellationToken cancellationToken = default) + { + string cacheKey = $"AccListMemb|{partyUrn}|{resourceIdUrn}"; + if (!_memoryCache.TryGetValue(cacheKey, out IEnumerable memberships)) + { + string apiurl = $"access-lists/memberships?party={partyUrn}&resource={resourceIdUrn}"; + string accessToken = _accessTokenGenerator.GenerateAccessToken("platform", "authorization"); + HttpResponseMessage response = await _resourceRegistry.Client.GetAsync(apiurl, platformAccessToken: accessToken, cancellationToken: cancellationToken); + + if (response.StatusCode == HttpStatusCode.OK) + { + ListObject result = await response.Content.ReadFromJsonAsync>(_jsonOptions, cancellationToken); + memberships = result.Items; + PutInCache(cacheKey, _generalSettings.PolicyCacheTimeout, memberships); + } + } + + return memberships; + } + + private void PutInCache(string cachekey, int cacheTimeout, object cacheObject) { var cacheEntryOptions = new MemoryCacheEntryOptions() .SetPriority(CacheItemPriority.High) - .SetAbsoluteExpiration(new TimeSpan(0, _generalSettings.PolicyCacheTimeout, 0)); + .SetAbsoluteExpiration(new TimeSpan(0, cacheTimeout, 0)); - _memoryCache.Set(cachekey, policy, cacheEntryOptions); + _memoryCache.Set(cachekey, cacheObject, cacheEntryOptions); } } } diff --git a/src/Authorization/Services/Interface/IAccessListAuthorization.cs b/src/Authorization/Services/Interface/IAccessListAuthorization.cs new file mode 100644 index 00000000..2860c0e0 --- /dev/null +++ b/src/Authorization/Services/Interface/IAccessListAuthorization.cs @@ -0,0 +1,20 @@ +using System.Threading; +using System.Threading.Tasks; +using Altinn.Authorization.ProblemDetails; +using Altinn.Platform.Authorization.Models; + +namespace Altinn.Platform.Authorization.Services.Interface; + +/// +/// The service used to map internal delegation change to delegation change events and push them to the event queue. +/// +public interface IAccessListAuthorization +{ + /// + /// Authorization of a given party for a resource, through RRR access lists + /// + /// Accesslist authorization request model + /// The + /// Boolean whether the access list authorization check passes or not + public Task> Authorize(AccessListAuthorizationRequest request, CancellationToken cancellationToken = default); +} diff --git a/src/Authorization/Services/Interface/IDelegationContextHandler.cs b/src/Authorization/Services/Interface/IDelegationContextHandler.cs index 31cb63c3..8672b8c5 100644 --- a/src/Authorization/Services/Interface/IDelegationContextHandler.cs +++ b/src/Authorization/Services/Interface/IDelegationContextHandler.cs @@ -47,6 +47,13 @@ public interface IDelegationContextHandler : IContextHandler /// XacmlResourceAttributes model public XacmlResourceAttributes GetResourceAttributes(XacmlContextRequest request); + /// + /// Gets a the Action string from the XacmlContextRequest + /// + /// The Xacml Context Request + /// Action attribute string value + public string GetActionString(XacmlContextRequest request); + /// /// Gets the list of mainunits for a subunit /// diff --git a/src/Authorization/Services/Interface/IRegisterService.cs b/src/Authorization/Services/Interface/IRegisterService.cs index 3fc09d65..6381ea31 100644 --- a/src/Authorization/Services/Interface/IRegisterService.cs +++ b/src/Authorization/Services/Interface/IRegisterService.cs @@ -21,6 +21,6 @@ public interface IRegisterService /// organisation number /// f or d number /// - Task PartyLookup(string orgNo, string person); + Task PartyLookup(string orgNo, string person); } } diff --git a/src/Authorization/Services/Interface/IResourceRegistry.cs b/src/Authorization/Services/Interface/IResourceRegistry.cs index 568ef1b5..b6388d85 100644 --- a/src/Authorization/Services/Interface/IResourceRegistry.cs +++ b/src/Authorization/Services/Interface/IResourceRegistry.cs @@ -1,18 +1,39 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Altinn.Authorization.ABAC.Xacml; +using Altinn.Authorization.Models.Register; +using Altinn.Authorization.Models.ResourceRegistry; -namespace Altinn.Platform.Authorization.Services.Interface +namespace Altinn.Platform.Authorization.Services.Interface; + +/// +/// Interface for resource registry +/// +public interface IResourceRegistry { /// - /// Interface for resource registry + /// Returns the service resource based on the resourceId, if it exists. + /// + /// the policyid + /// The + /// ServiceResource + Task GetResourceAsync(string resourceId, CancellationToken cancellationToken = default); + + /// + /// Returns a policy based on the resourceId + /// + /// the policyid + /// The + /// XacmlPolicy + Task GetResourcePolicyAsync(string resourceId, CancellationToken cancellationToken = default); + + /// + /// Returns all memberships a given party has access to through access lists, for a given resource. /// - public interface IResourceRegistry - { - /// - /// Returns a policy based on the resourceId - /// - /// the policyid - /// XacmlPolicy - Task GetResourcePolicyAsync(string resourceId); - } + /// Urn identifying the party + /// Urn identifying the resource + /// The + /// List of memberships + Task> GetMembershipsForResourceForParty(PartyUrn partyUrn, ResourceIdUrn resourceIdUrn, CancellationToken cancellationToken = default); } diff --git a/src/Authorization/appsettings.Development.json b/src/Authorization/appsettings.Development.json index e203e940..0bbe6dcb 100644 --- a/src/Authorization/appsettings.Development.json +++ b/src/Authorization/appsettings.Development.json @@ -5,5 +5,11 @@ "System": "Information", "Microsoft": "Information" } + }, + "TokenGeneratorSettings": { + "Url": "https://altinn-testtools-token-generator.azurewebsites.net/api/GetPlatformAccessToken", + "User": "", + "Password": "", + "Env": "at22" } } diff --git a/test/Bruno/Altinn.Authorization/Automatic Test Collection/Authorize/AccessList_AC1_Delegation_Permit.bru b/test/Bruno/Altinn.Authorization/Automatic Test Collection/Authorize/AccessList_AC1_Delegation_Permit.bru new file mode 100644 index 00000000..4cb1db40 --- /dev/null +++ b/test/Bruno/Altinn.Authorization/Automatic Test Collection/Authorize/AccessList_AC1_Delegation_Permit.bru @@ -0,0 +1,116 @@ +meta { + name: AccessList_AC1_Delegation_Permit + type: http + seq: 2 +} + +post { + url: {{baseUrl}}/authorization/api/v1/authorize + body: json + auth: inherit +} + +headers { + Content-Type: application/json + Ocp-Apim-Subscription-Key: {{apimSubscriptionKey}} +} + +body:json { + /* + See Docs tab for test case description + */ + { + "Request": { + "ReturnPolicyIdList": false, + "AccessSubject": [ + { + "Attribute": [ + { + "AttributeId": "urn:altinn:person:identifier-no", + "Value": "08827798585" + } + ] + } + ], + "Action": [ + { + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "read", + "DataType": "http://www.w3.org/2001/XMLSchema#string" + } + ] + } + ], + "Resource": [ + { + "Attribute": [ + { + "AttributeId": "urn:altinn:resource", + "Value": "devtest_gar_bruno_accesslist" + }, + { + "AttributeId": "urn:altinn:organization:identifier-no", + "Value": "313776735", + "DataType": "http://www.w3.org/2001/XMLSchema#string" + } + ] + } + ] + } + } +} + +assert { + ~res.status: eq 200 + ~res.body: contains created +} + +script:pre-request { + const testdata = require(`./Testdata/Authorization/${bru.getEnvVar("tokenEnv")}testdata.json`); + const sharedtestdata = require(`./Testdata/Authorization/sharedtestdata.json`); + + var getTokenParameters = { + auth_tokenType: sharedtestdata.authTokenType.enterprise, + auth_scopes: sharedtestdata.auth_scopes.authorize, + auth_org: "digdir", + auth_orgNo: "991825827" + } + + const token = await testTokenGenerator.getToken(getTokenParameters); + + bru.setVar("bearerToken", token); +} + +script:post-response { + //console.log("request url (after): " + req.getUrl()); +} + +tests { + + test("POST Authorize AccessList_AC1_Delegation_Permit result", function() { + const testdata = require(`./Testdata/Authorization/${bru.getEnvVar("tokenEnv")}testdata.json`); + const data = res.getBody(); + expect(res.status).to.equal(200); + expect(data.response[0]).to.have.property('decision', "Permit"); + }); +} + +docs { + Issue: + https://github.com/Altinn/altinn-access-management/issues/748 + + Acceptance Criteria: + AC1 - Avgiver med Tilgangssliste tilgang uten action filter - Permit + + GITT en bruker med tilgang til ressurs for avgiver gjennom rolle eller enkeltdelegering + NÅR ressursen krever tilgangsliste autorisasjon + OG avgiver er medlem av minst en tilgangsliste som er knytt til ressursen + OG tilgangslisten for ressursen ikke har noe action filter begrensning for gitte actions + SÅ skal bruker få Permit + + Scenario/Testdata setup: + The user has received delegation of read action for the resource from the party. + The party have access list membership for the resource without action filter. +} diff --git a/test/Bruno/Altinn.Authorization/Automatic Test Collection/Authorize/AccessList_AC1_Permit.bru b/test/Bruno/Altinn.Authorization/Automatic Test Collection/Authorize/AccessList_AC1_Permit.bru new file mode 100644 index 00000000..e26fb0d4 --- /dev/null +++ b/test/Bruno/Altinn.Authorization/Automatic Test Collection/Authorize/AccessList_AC1_Permit.bru @@ -0,0 +1,115 @@ +meta { + name: AccessList_AC1_Permit + type: http + seq: 1 +} + +post { + url: {{baseUrl}}/authorization/api/v1/authorize + body: json + auth: inherit +} + +headers { + Content-Type: application/json + Ocp-Apim-Subscription-Key: {{apimSubscriptionKey}} +} + +body:json { + /* + See Docs tab for test case description + */ + { + "Request": { + "ReturnPolicyIdList": false, + "AccessSubject": [ + { + "Attribute": [ + { + "AttributeId": "urn:altinn:person:identifier-no", + "Value": "12819498464" + } + ] + } + ], + "Action": [ + { + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "write", + "DataType": "http://www.w3.org/2001/XMLSchema#string" + } + ] + } + ], + "Resource": [ + { + "Attribute": [ + { + "AttributeId": "urn:altinn:resource", + "Value": "devtest_gar_bruno_accesslist" + }, + { + "AttributeId": "urn:altinn:organization:identifier-no", + "Value": "313776735", + "DataType": "http://www.w3.org/2001/XMLSchema#string" + } + ] + } + ] + } + } +} + +assert { + ~res.status: eq 200 + ~res.body: contains created +} + +script:pre-request { + const testdata = require(`./Testdata/Authorization/${bru.getEnvVar("tokenEnv")}testdata.json`); + const sharedtestdata = require(`./Testdata/Authorization/sharedtestdata.json`); + + var getTokenParameters = { + auth_tokenType: sharedtestdata.authTokenType.enterprise, + auth_scopes: sharedtestdata.auth_scopes.authorize, + auth_org: "digdir", + auth_orgNo: "991825827" + } + + const token = await testTokenGenerator.getToken(getTokenParameters); + + bru.setVar("bearerToken", token); +} + +script:post-response { + //console.log("request url (after): " + req.getUrl()); +} + +tests { + + test("POST Authorize AccessList_AC1_Permit result", function() { + const testdata = require(`./Testdata/Authorization/${bru.getEnvVar("tokenEnv")}testdata.json`); + const data = res.getBody(); + expect(res.status).to.equal(200); + expect(data.response[0]).to.have.property('decision', "Permit"); + }); +} + +docs { + Issue: + https://github.com/Altinn/altinn-access-management/issues/748 + + Acceptance Criteria: + AC1 - Avgiver med Tilgangssliste tilgang uten action filter - Permit + + GITT en bruker med tilgang til ressurs for avgiver gjennom rolle eller enkeltdelegering + NÅR ressursen krever tilgangsliste autorisasjon + OG avgiver er medlem av minst en tilgangsliste som er knytt til ressursen + OG tilgangslisten for ressursen ikke har noe action filter begrensning for gitte actions + SÅ skal bruker få Permit + + Scenario/Testdata setup: + The user is DAGL for the party. The party have access list membership for the resource without action filter. +} diff --git a/test/Bruno/Altinn.Authorization/Automatic Test Collection/Authorize/AccessList_AC2_Delegation_Permit.bru b/test/Bruno/Altinn.Authorization/Automatic Test Collection/Authorize/AccessList_AC2_Delegation_Permit.bru new file mode 100644 index 00000000..21706781 --- /dev/null +++ b/test/Bruno/Altinn.Authorization/Automatic Test Collection/Authorize/AccessList_AC2_Delegation_Permit.bru @@ -0,0 +1,116 @@ +meta { + name: AccessList_AC2_Delegation_Permit + type: http + seq: 4 +} + +post { + url: {{baseUrl}}/authorization/api/v1/authorize + body: json + auth: inherit +} + +headers { + Content-Type: application/json + Ocp-Apim-Subscription-Key: {{apimSubscriptionKey}} +} + +body:json { + /* + See Docs tab for test case description + */ + { + "Request": { + "ReturnPolicyIdList": false, + "AccessSubject": [ + { + "Attribute": [ + { + "AttributeId": "urn:altinn:person:identifier-no", + "Value": "08827798585" + } + ] + } + ], + "Action": [ + { + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "read", + "DataType": "http://www.w3.org/2001/XMLSchema#string" + } + ] + } + ], + "Resource": [ + { + "Attribute": [ + { + "AttributeId": "urn:altinn:resource", + "Value": "devtest_gar_bruno_accesslist_actionfilter" + }, + { + "AttributeId": "urn:altinn:organization:identifier-no", + "Value": "313776735", + "DataType": "http://www.w3.org/2001/XMLSchema#string" + } + ] + } + ] + } + } +} + +assert { + ~res.status: eq 200 + ~res.body: contains created +} + +script:pre-request { + const testdata = require(`./Testdata/Authorization/${bru.getEnvVar("tokenEnv")}testdata.json`); + const sharedtestdata = require(`./Testdata/Authorization/sharedtestdata.json`); + + var getTokenParameters = { + auth_tokenType: sharedtestdata.authTokenType.enterprise, + auth_scopes: sharedtestdata.auth_scopes.authorize, + auth_org: "digdir", + auth_orgNo: "991825827" + } + + const token = await testTokenGenerator.getToken(getTokenParameters); + + bru.setVar("bearerToken", token); +} + +script:post-response { + //console.log("request url (after): " + req.getUrl()); +} + +tests { + + test("POST Authorize AccessList_AC2_Delegation_Permit result", function() { + const testdata = require(`./Testdata/Authorization/${bru.getEnvVar("tokenEnv")}testdata.json`); + const data = res.getBody(); + expect(res.status).to.equal(200); + expect(data.response[0]).to.have.property('decision', "Permit"); + }); +} + +docs { + Issue: + https://github.com/Altinn/altinn-access-management/issues/748 + + Acceptance Criteria: + AC2 - Avgiver med Tilgangssliste tilgang med action filter - Permit + + GITT en bruker med tilgang til ressurs for avgiver gjennom rolle eller enkeltdelegering + NÅR ressursen krever tilgangsliste autorisasjon + OG avgiver er medlem av minst en tilgangsliste som er knytt til ressursen + OG tilgangslisten for ressursen har action filter begrensning som matcher action brukeren skal autoriseres for + SÅ skal bruker få Permit + + Scenario/Testdata setup: + The user has received delegation of read action for the resource from the party. + The party has access list membership for the resource with action filter for the action: read. +} diff --git a/test/Bruno/Altinn.Authorization/Automatic Test Collection/Authorize/AccessList_AC2_Permit.bru b/test/Bruno/Altinn.Authorization/Automatic Test Collection/Authorize/AccessList_AC2_Permit.bru new file mode 100644 index 00000000..1d454e5e --- /dev/null +++ b/test/Bruno/Altinn.Authorization/Automatic Test Collection/Authorize/AccessList_AC2_Permit.bru @@ -0,0 +1,115 @@ +meta { + name: AccessList_AC2_Permit + type: http + seq: 3 +} + +post { + url: {{baseUrl}}/authorization/api/v1/authorize + body: json + auth: inherit +} + +headers { + Content-Type: application/json + Ocp-Apim-Subscription-Key: {{apimSubscriptionKey}} +} + +body:json { + /* + See Docs tab for test case description + */ + { + "Request": { + "ReturnPolicyIdList": false, + "AccessSubject": [ + { + "Attribute": [ + { + "AttributeId": "urn:altinn:person:identifier-no", + "Value": "12819498464" + } + ] + } + ], + "Action": [ + { + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "read", + "DataType": "http://www.w3.org/2001/XMLSchema#string" + } + ] + } + ], + "Resource": [ + { + "Attribute": [ + { + "AttributeId": "urn:altinn:resource", + "Value": "devtest_gar_bruno_accesslist_actionfilter" + }, + { + "AttributeId": "urn:altinn:organization:identifier-no", + "Value": "313776735", + "DataType": "http://www.w3.org/2001/XMLSchema#string" + } + ] + } + ] + } + } +} + +assert { + ~res.status: eq 200 + ~res.body: contains created +} + +script:pre-request { + const testdata = require(`./Testdata/Authorization/${bru.getEnvVar("tokenEnv")}testdata.json`); + const sharedtestdata = require(`./Testdata/Authorization/sharedtestdata.json`); + + var getTokenParameters = { + auth_tokenType: sharedtestdata.authTokenType.enterprise, + auth_scopes: sharedtestdata.auth_scopes.authorize, + auth_org: "digdir", + auth_orgNo: "991825827" + } + + const token = await testTokenGenerator.getToken(getTokenParameters); + + bru.setVar("bearerToken", token); +} + +script:post-response { + //console.log("request url (after): " + req.getUrl()); +} + +tests { + + test("POST Authorize AccessList_AC2_Permit result", function() { + const testdata = require(`./Testdata/Authorization/${bru.getEnvVar("tokenEnv")}testdata.json`); + const data = res.getBody(); + expect(res.status).to.equal(200); + expect(data.response[0]).to.have.property('decision', "Permit"); + }); +} + +docs { + Issue: + https://github.com/Altinn/altinn-access-management/issues/748 + + Acceptance Criteria: + AC2 - Avgiver med Tilgangssliste tilgang med action filter - Permit + + GITT en bruker med tilgang til ressurs for avgiver gjennom rolle eller enkeltdelegering + NÅR ressursen krever tilgangsliste autorisasjon + OG avgiver er medlem av minst en tilgangsliste som er knytt til ressursen + OG tilgangslisten for ressursen har action filter begrensning som matcher action brukeren skal autoriseres for + SÅ skal bruker få Permit + + Scenario/Testdata setup: + The user is DAGL for the party. The party have access list membership for the resource with action filter for the action: read. +} diff --git a/test/Bruno/Altinn.Authorization/Automatic Test Collection/Authorize/AccessList_AC3_Delegation_Deny.bru b/test/Bruno/Altinn.Authorization/Automatic Test Collection/Authorize/AccessList_AC3_Delegation_Deny.bru new file mode 100644 index 00000000..603f9f13 --- /dev/null +++ b/test/Bruno/Altinn.Authorization/Automatic Test Collection/Authorize/AccessList_AC3_Delegation_Deny.bru @@ -0,0 +1,115 @@ +meta { + name: AccessList_AC3_Delegation_Deny + type: http + seq: 6 +} + +post { + url: {{baseUrl}}/authorization/api/v1/authorize + body: json + auth: inherit +} + +headers { + Content-Type: application/json + Ocp-Apim-Subscription-Key: {{apimSubscriptionKey}} +} + +body:json { + /* + See Docs tab for test case description + */ + { + "Request": { + "ReturnPolicyIdList": false, + "AccessSubject": [ + { + "Attribute": [ + { + "AttributeId": "urn:altinn:person:identifier-no", + "Value": "12819498464" + } + ] + } + ], + "Action": [ + { + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "read", + "DataType": "http://www.w3.org/2001/XMLSchema#string" + } + ] + } + ], + "Resource": [ + { + "Attribute": [ + { + "AttributeId": "urn:altinn:resource", + "Value": "devtest_gar_bruno_accesslist" + }, + { + "AttributeId": "urn:altinn:organization:identifier-no", + "Value": "310631302", + "DataType": "http://www.w3.org/2001/XMLSchema#string" + } + ] + } + ] + } + } +} + +assert { + ~res.status: eq 200 + ~res.body: contains created +} + +script:pre-request { + const testdata = require(`./Testdata/Authorization/${bru.getEnvVar("tokenEnv")}testdata.json`); + const sharedtestdata = require(`./Testdata/Authorization/sharedtestdata.json`); + + var getTokenParameters = { + auth_tokenType: sharedtestdata.authTokenType.enterprise, + auth_scopes: sharedtestdata.auth_scopes.authorize, + auth_org: "digdir", + auth_orgNo: "991825827" + } + + const token = await testTokenGenerator.getToken(getTokenParameters); + + bru.setVar("bearerToken", token); +} + +script:post-response { + //console.log("request url (after): " + req.getUrl()); +} + +tests { + + test("POST Authorize AccessList_AC3_Delegation_Deny result", function() { + const testdata = require(`./Testdata/Authorization/${bru.getEnvVar("tokenEnv")}testdata.json`); + const data = res.getBody(); + expect(res.status).to.equal(200); + expect(data.response[0]).to.have.property('decision', "Deny"); + }); +} + +docs { + Issue: + https://github.com/Altinn/altinn-access-management/issues/748 + + Acceptance Criteria: + AC3 - Avgiver uten Tilgangssliste tilgang - Deny + + GITT en bruker med tilgang til ressurs for avgiver gjennom rolle eller enkeltdelegering + NÅR ressursen krever tilgangsliste autorisasjon + OG avgiver IKKE er medlem av noen tilgangsliste som er knytt til ressursen + SÅ skal bruker få Deny + + Scenario/Testdata setup: + The user has received delegation of read action for the resource from the party. + The party does not have access list membership for the resource. +} diff --git a/test/Bruno/Altinn.Authorization/Automatic Test Collection/Authorize/AccessList_AC3_Deny.bru b/test/Bruno/Altinn.Authorization/Automatic Test Collection/Authorize/AccessList_AC3_Deny.bru new file mode 100644 index 00000000..dec141f3 --- /dev/null +++ b/test/Bruno/Altinn.Authorization/Automatic Test Collection/Authorize/AccessList_AC3_Deny.bru @@ -0,0 +1,114 @@ +meta { + name: AccessList_AC3_Deny + type: http + seq: 5 +} + +post { + url: {{baseUrl}}/authorization/api/v1/authorize + body: json + auth: inherit +} + +headers { + Content-Type: application/json + Ocp-Apim-Subscription-Key: {{apimSubscriptionKey}} +} + +body:json { + /* + See Docs tab for test case description + */ + { + "Request": { + "ReturnPolicyIdList": false, + "AccessSubject": [ + { + "Attribute": [ + { + "AttributeId": "urn:altinn:person:identifier-no", + "Value": "08827798585" + } + ] + } + ], + "Action": [ + { + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "read", + "DataType": "http://www.w3.org/2001/XMLSchema#string" + } + ] + } + ], + "Resource": [ + { + "Attribute": [ + { + "AttributeId": "urn:altinn:resource", + "Value": "devtest_gar_bruno_accesslist" + }, + { + "AttributeId": "urn:altinn:organization:identifier-no", + "Value": "310631302", + "DataType": "http://www.w3.org/2001/XMLSchema#string" + } + ] + } + ] + } + } +} + +assert { + ~res.status: eq 200 + ~res.body: contains created +} + +script:pre-request { + const testdata = require(`./Testdata/Authorization/${bru.getEnvVar("tokenEnv")}testdata.json`); + const sharedtestdata = require(`./Testdata/Authorization/sharedtestdata.json`); + + var getTokenParameters = { + auth_tokenType: sharedtestdata.authTokenType.enterprise, + auth_scopes: sharedtestdata.auth_scopes.authorize, + auth_org: "digdir", + auth_orgNo: "991825827" + } + + const token = await testTokenGenerator.getToken(getTokenParameters); + + bru.setVar("bearerToken", token); +} + +script:post-response { + //console.log("request url (after): " + req.getUrl()); +} + +tests { + + test("POST Authorize AccessList_AC3_Deny result", function() { + const testdata = require(`./Testdata/Authorization/${bru.getEnvVar("tokenEnv")}testdata.json`); + const data = res.getBody(); + expect(res.status).to.equal(200); + expect(data.response[0]).to.have.property('decision', "Deny"); + }); +} + +docs { + Issue: + https://github.com/Altinn/altinn-access-management/issues/748 + + Acceptance Criteria: + AC3 - Avgiver uten Tilgangssliste tilgang - Deny + + GITT en bruker med tilgang til ressurs for avgiver gjennom rolle eller enkeltdelegering + NÅR ressursen krever tilgangsliste autorisasjon + OG avgiver IKKE er medlem av noen tilgangsliste som er knytt til ressursen + SÅ skal bruker få Deny + + Scenario/Testdata setup: + The user is DAGL for the party. The party does not have access list membership for the resource. +} diff --git a/test/Bruno/Altinn.Authorization/Automatic Test Collection/Authorize/AccessList_AC4_Delegation_Deny.bru b/test/Bruno/Altinn.Authorization/Automatic Test Collection/Authorize/AccessList_AC4_Delegation_Deny.bru new file mode 100644 index 00000000..8cc0234b --- /dev/null +++ b/test/Bruno/Altinn.Authorization/Automatic Test Collection/Authorize/AccessList_AC4_Delegation_Deny.bru @@ -0,0 +1,116 @@ +meta { + name: AccessList_AC4_Delegation_Deny + type: http + seq: 8 +} + +post { + url: {{baseUrl}}/authorization/api/v1/authorize + body: json + auth: inherit +} + +headers { + Content-Type: application/json + Ocp-Apim-Subscription-Key: {{apimSubscriptionKey}} +} + +body:json { + /* + See Docs tab for test case description + */ + { + "Request": { + "ReturnPolicyIdList": false, + "AccessSubject": [ + { + "Attribute": [ + { + "AttributeId": "urn:altinn:person:identifier-no", + "Value": "08827798585" + } + ] + } + ], + "Action": [ + { + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "write", + "DataType": "http://www.w3.org/2001/XMLSchema#string" + } + ] + } + ], + "Resource": [ + { + "Attribute": [ + { + "AttributeId": "urn:altinn:resource", + "Value": "devtest_gar_bruno_accesslist_actionfilter" + }, + { + "AttributeId": "urn:altinn:organization:identifier-no", + "Value": "313776735", + "DataType": "http://www.w3.org/2001/XMLSchema#string" + } + ] + } + ] + } + } +} + +assert { + ~res.status: eq 200 + ~res.body: contains created +} + +script:pre-request { + const testdata = require(`./Testdata/Authorization/${bru.getEnvVar("tokenEnv")}testdata.json`); + const sharedtestdata = require(`./Testdata/Authorization/sharedtestdata.json`); + + var getTokenParameters = { + auth_tokenType: sharedtestdata.authTokenType.enterprise, + auth_scopes: sharedtestdata.auth_scopes.authorize, + auth_org: "digdir", + auth_orgNo: "991825827" + } + + const token = await testTokenGenerator.getToken(getTokenParameters); + + bru.setVar("bearerToken", token); +} + +script:post-response { + //console.log("request url (after): " + req.getUrl()); +} + +tests { + + test("POST Authorize AccessList_AC4_Deny result", function() { + const testdata = require(`./Testdata/Authorization/${bru.getEnvVar("tokenEnv")}testdata.json`); + const data = res.getBody(); + expect(res.status).to.equal(200); + expect(data.response[0]).to.have.property('decision', "Deny"); + }); +} + +docs { + Issue: + https://github.com/Altinn/altinn-access-management/issues/748 + + Acceptance Criteria: + AC4 - Avgiver med Tilgangssliste tilgang med action filter - Deny + + GITT en bruker med tilgang til ressurs for avgiver gjennom rolle eller enkeltdelegering + NÅR ressursen krever tilgangsliste autorisasjon + OG avgiver er medlem av minst en tilgangsliste som er knytt til ressursen + OG tilgangslisten for ressursen har action filter begrensning som IKKE matcher action brukeren skal autoriseres for + SÅ skal bruker få Deny + + Scenario/Testdata setup: + The user has received delegation of both read and write action for the resource from the party. + The party have access list membership for the resource with action filter for the action: read. +} diff --git a/test/Bruno/Altinn.Authorization/Automatic Test Collection/Authorize/AccessList_AC4_Deny.bru b/test/Bruno/Altinn.Authorization/Automatic Test Collection/Authorize/AccessList_AC4_Deny.bru new file mode 100644 index 00000000..70aaabc4 --- /dev/null +++ b/test/Bruno/Altinn.Authorization/Automatic Test Collection/Authorize/AccessList_AC4_Deny.bru @@ -0,0 +1,115 @@ +meta { + name: AccessList_AC4_Deny + type: http + seq: 7 +} + +post { + url: {{baseUrl}}/authorization/api/v1/authorize + body: json + auth: inherit +} + +headers { + Content-Type: application/json + Ocp-Apim-Subscription-Key: {{apimSubscriptionKey}} +} + +body:json { + /* + See Docs tab for test case description + */ + { + "Request": { + "ReturnPolicyIdList": false, + "AccessSubject": [ + { + "Attribute": [ + { + "AttributeId": "urn:altinn:person:identifier-no", + "Value": "12819498464" + } + ] + } + ], + "Action": [ + { + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "write", + "DataType": "http://www.w3.org/2001/XMLSchema#string" + } + ] + } + ], + "Resource": [ + { + "Attribute": [ + { + "AttributeId": "urn:altinn:resource", + "Value": "devtest_gar_bruno_accesslist_actionfilter" + }, + { + "AttributeId": "urn:altinn:organization:identifier-no", + "Value": "313776735", + "DataType": "http://www.w3.org/2001/XMLSchema#string" + } + ] + } + ] + } + } +} + +assert { + ~res.status: eq 200 + ~res.body: contains created +} + +script:pre-request { + const testdata = require(`./Testdata/Authorization/${bru.getEnvVar("tokenEnv")}testdata.json`); + const sharedtestdata = require(`./Testdata/Authorization/sharedtestdata.json`); + + var getTokenParameters = { + auth_tokenType: sharedtestdata.authTokenType.enterprise, + auth_scopes: sharedtestdata.auth_scopes.authorize, + auth_org: "digdir", + auth_orgNo: "991825827" + } + + const token = await testTokenGenerator.getToken(getTokenParameters); + + bru.setVar("bearerToken", token); +} + +script:post-response { + //console.log("request url (after): " + req.getUrl()); +} + +tests { + + test("POST Authorize AccessList_AC4_Deny result", function() { + const testdata = require(`./Testdata/Authorization/${bru.getEnvVar("tokenEnv")}testdata.json`); + const data = res.getBody(); + expect(res.status).to.equal(200); + expect(data.response[0]).to.have.property('decision', "Deny"); + }); +} + +docs { + Issue: + https://github.com/Altinn/altinn-access-management/issues/748 + + Acceptance Criteria: + AC4 - Avgiver med Tilgangssliste tilgang med action filter - Deny + + GITT en bruker med tilgang til ressurs for avgiver gjennom rolle eller enkeltdelegering + NÅR ressursen krever tilgangsliste autorisasjon + OG avgiver er medlem av minst en tilgangsliste som er knytt til ressursen + OG tilgangslisten for ressursen har action filter begrensning som IKKE matcher action brukeren skal autoriseres for + SÅ skal bruker få Deny + + Scenario/Testdata setup: + The user is DAGL for the party. The party have access list membership for the resource with action filter for the action: read. +} diff --git "a/test/Bruno/Altinn.Authorization/Manual Test Collection/AccessList Authorization/\303\230rsta for devtest_gar_rrr_accesslist.bru" "b/test/Bruno/Altinn.Authorization/Manual Test Collection/AccessList Authorization/\303\230rsta for devtest_gar_rrr_accesslist.bru" new file mode 100644 index 00000000..7e9a209d --- /dev/null +++ "b/test/Bruno/Altinn.Authorization/Manual Test Collection/AccessList Authorization/\303\230rsta for devtest_gar_rrr_accesslist.bru" @@ -0,0 +1,73 @@ +meta { + name: Ørsta for devtest_gar_rrr_accesslist + type: http + seq: 1 +} + +post { + url: {{baseUrl}}/authorization/api/v1/accesslist/authorize + body: json + auth: bearer +} + +headers { + Content-Type: application/json + Ocp-Apim-Subscription-Key: {{appsAccessKey}} +} + +auth:bearer { + token: eyJhbGciOiJSUzI1NiIsImtpZCI6IjM4OTJENDgyRTYyMDI2NzI1MTJBRTBDMkQ5REJBQzBERTRBNEVDMzciLCJ0eXAiOiJKV1QiLCJ4NWMiOiIzODkyRDQ4MkU2MjAyNjcyNTEyQUUwQzJEOURCQUMwREU0QTRFQzM3In0.eyJzY29wZSI6ImFsdGlubjphdXRob3JpemF0aW9uL2F1dGhvcml6ZS5hZG1pbiIsInRva2VuX3R5cGUiOiJCZWFyZXIiLCJleHAiOjE3Mjg4ODEzNzQsImlhdCI6MTcyNTI4MTM3NCwiY2xpZW50X2lkIjoiOTAwMjhjODctNmIxOC00MzllLTg1YWYtOGE0NmFmZGI5MzljIiwianRpIjoiZHNoSDR1R1ZDQ0VZb3RvODBDMTdqd2JtbVZhMnZyVWFPcDg2LXZ6QkRBWCIsImNvbnN1bWVyIjp7ImF1dGhvcml0eSI6ImlzbzY1MjMtYWN0b3JpZC11cGlzIiwiSUQiOiIwMTkyOjk5MTgyNTgyNyJ9LCJ1cm46YWx0aW5uOm9yZ051bWJlciI6Ijk5MTgyNTgyNyIsInVybjphbHRpbm46YXV0aGVudGljYXRlbWV0aG9kIjoibWFza2lucG9ydGVuIiwidXJuOmFsdGlubjphdXRobGV2ZWwiOjMsImlzcyI6Imh0dHBzOi8vcGxhdGZvcm0uYXQyMi5hbHRpbm4uY2xvdWQvYXV0aGVudGljYXRpb24vYXBpL3YxL29wZW5pZC8iLCJhY3R1YWxfaXNzIjoiYWx0aW5uLXRlc3QtdG9vbHMiLCJuYmYiOjE3MjUyODEzNzQsInVybjphbHRpbm46b3JnIjoiZGlnZGlyIn0.N09_m_fwptp6ChwTrMLKVxIEtCcDN-hdhqpyLVpWrxYoTcIcFcjpiHkUV0MyOKu-3TzQCcRDXaiA_Mhhp957EL7cZI5METV7wuFnnzxbwuwv3y6LplIYMvtb-8BWQ6pvxJB_vuGhp9AtCPpVZRS1M1JgwHXC6D16OMlu5rLwN3q_ck8VquD3uob8Toh7IMqrQk_9u0LwwFiPn8zmOKuZblYIL9tfBAnDt_yQPaLOEXBGHCqcZNeJM3P2T-bAoponpFYs6pC4Gbeu6QDpr1ULFXcl2e1Hy14SF5OiHXVi0nDjeQtYXJEabX6sPQbfOuILCwv03EN8GOBZuwCXxlB26w +} + +body:json { + { + "subject": { + "type": "urn:altinn:organization:identifier-no", + "value": "910459880" + }, + "resource": { + "type": "urn:altinn:resource", + "value": "devtest_gar_rrr_accesslist" + }, + "action": { + "type": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "value": "whatever" + } + } +} + +vars:pre-request { + auth_tokenType: Enterprise + auth_scopes: altinn:authorization/authorize + auth_org: ttd + auth_orgNo: 991825827 +} + +assert { + ~res.status: eq 200 + ~res.body: contains created +} + +script:pre-request { + //await testTokenGenerator.getToken(); +} + +tests { + + test("3 POST Decision result on read is permit", function() { + const testdata = require(`./Testdata/Authorization/${bru.getEnvVar("tokenEnv")}testdata.json`); + const data = res.getBody(); + expect(res.status).to.equal(200); + expect(data.response[0]).to.have.property('decision', "Permit"); + }); +} + +docs { + Get a decision from PDP with appOwner details and validate response to have Permit. + + AccessSubject: ['urn:altinn:org'] + + Action: ['read'] + + Resource: ['urn:altinn:app', 'urn:altinn:org'] +} diff --git a/test/Bruno/Altinn.Authorization/environments/PROD.bru b/test/Bruno/Altinn.Authorization/environments/PROD.bru index b8d10fb6..6ffdb653 100644 --- a/test/Bruno/Altinn.Authorization/environments/PROD.bru +++ b/test/Bruno/Altinn.Authorization/environments/PROD.bru @@ -1,3 +1,4 @@ vars { - baseUrl: https://platform.altinn.cloud + baseUrl: https://platform.altinn.no + apimSubscriptionKey: {{process.env.PROD_APIM_SUBSCRIPTION_KEY}} } diff --git a/test/IntegrationTests/AccessListAuthorizationControllerTest.cs b/test/IntegrationTests/AccessListAuthorizationControllerTest.cs new file mode 100644 index 00000000..10a3b5b1 --- /dev/null +++ b/test/IntegrationTests/AccessListAuthorizationControllerTest.cs @@ -0,0 +1,112 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Altinn.Common.AccessToken.Services; +using Altinn.Common.Authentication.Configuration; +using Altinn.Platform.Authorization.Controllers; +using Altinn.Platform.Authorization.IntegrationTests.MockServices; +using Altinn.Platform.Authorization.IntegrationTests.Util; +using Altinn.Platform.Authorization.IntegrationTests.Webfactory; +using Altinn.Platform.Authorization.Models; +using Altinn.Platform.Authorization.Services.Interface; +using Altinn.Platform.Authorization.Services.Interfaces; +using Altinn.Platform.Events.Tests.Mocks; +using Altinn.ResourceRegistry.Tests.Mocks; +using AltinnCore.Authentication.JwtCookie; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Altinn.Platform.Authorization.IntegrationTests; + +public class AccessListAuthorizationControllerTest : IClassFixture> +{ + private static readonly JsonSerializerOptions _serializerOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + private readonly CustomWebApplicationFactory _factory; + + public AccessListAuthorizationControllerTest(CustomWebApplicationFactory fixture) + { + _factory = fixture; + } + + /// + /// Tests the scenario where the request does not have a valid platform access token. + /// + [Fact] + public async Task AccessList_Authorization_Unauthorized_MissingPlatformAccessToken() + { + // Act + HttpResponseMessage response = await GetTestClient().SendAsync(GetPostRequestMessage("Permit_WithoutActionFilter")); + + // Assert + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + /// + /// Tests the scenario where the subject organization has access to the resource 'ttd-accesslist-resource' through access list membership without any action filter. + /// + [Fact] + public async Task AccessList_Authorization_Permit_WithoutActionFilter() + { + string testCase = "Permit_WithoutActionFilter"; + AccessListAuthorizationResponse expected = GetExpectedResponse("Permit_WithoutActionFilter"); + + // Act + HttpResponseMessage response = await GetTestClient().SendAsync(GetPostRequestMessage(testCase, PrincipalUtil.GetAccessToken("access-management", "platform"))); + string responseContent = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + AccessListAuthorizationResponse actual = JsonSerializer.Deserialize(responseContent, _serializerOptions); + AssertionUtil.AssertEqual(expected, actual); + } + + private HttpClient GetTestClient() + { + HttpClient client = _factory.WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(services => + { + services.AddSingleton(); + services.AddSingleton, JwtCookiePostConfigureOptionsStub>(); + services.AddSingleton, OidcProviderPostConfigureSettingsStub>(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + }); + }).CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); + + return client; + } + + private static HttpRequestMessage GetPostRequestMessage(string testCase, string platformAccessToken = null) + { + string requestPath = Path.Combine(Path.GetDirectoryName(new Uri(typeof(AccessListAuthorizationControllerTest).Assembly.Location).LocalPath), "Data", "Json", "AccessListAuthorization"); + string requestText = File.ReadAllText(Path.Combine(requestPath, testCase + "_Request.json")); + + HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post, "authorization/api/v1/accesslist/accessmanagement/authorization") + { + Content = new StringContent(requestText, Encoding.UTF8, "application/json") + }; + + if (!string.IsNullOrEmpty(platformAccessToken)) + { + message.Headers.Add("PlatformAccessToken", platformAccessToken); + } + + return message; + } + + private static AccessListAuthorizationResponse GetExpectedResponse(string testCase) + { + string requestPath = Path.Combine(Path.GetDirectoryName(new Uri(typeof(AccessListAuthorizationControllerTest).Assembly.Location).LocalPath), "Data", "Json", "AccessListAuthorization"); + return (AccessListAuthorizationResponse)JsonSerializer.Deserialize(File.ReadAllText(Path.Combine(requestPath, testCase + "_Response.json")), typeof(AccessListAuthorizationResponse), _serializerOptions); + } +} diff --git a/test/IntegrationTests/Altinn.Platform.Authorization.IntegrationTests.csproj b/test/IntegrationTests/Altinn.Platform.Authorization.IntegrationTests.csproj index 9f10cc23..8f3e6d04 100644 --- a/test/IntegrationTests/Altinn.Platform.Authorization.IntegrationTests.csproj +++ b/test/IntegrationTests/Altinn.Platform.Authorization.IntegrationTests.csproj @@ -48,6 +48,12 @@ Always + + Always + + + Always + Always @@ -63,213 +69,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - - - - - diff --git a/test/IntegrationTests/Data/Json/AccessListAuthorization/Permit_WithoutActionFilter_Request.json b/test/IntegrationTests/Data/Json/AccessListAuthorization/Permit_WithoutActionFilter_Request.json new file mode 100644 index 00000000..366311d1 --- /dev/null +++ b/test/IntegrationTests/Data/Json/AccessListAuthorization/Permit_WithoutActionFilter_Request.json @@ -0,0 +1,14 @@ +{ + "subject": { + "type": "urn:altinn:organization:identifier-no", + "value": "910459880" + }, + "resource": { + "type": "urn:altinn:resource", + "value": "ttd-accesslist-resource" + }, + "action": { + "type": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "value": "whatever" + } +} \ No newline at end of file diff --git a/test/IntegrationTests/Data/Json/AccessListAuthorization/Permit_WithoutActionFilter_Response.json b/test/IntegrationTests/Data/Json/AccessListAuthorization/Permit_WithoutActionFilter_Response.json new file mode 100644 index 00000000..9b340de3 --- /dev/null +++ b/test/IntegrationTests/Data/Json/AccessListAuthorization/Permit_WithoutActionFilter_Response.json @@ -0,0 +1,15 @@ +{ + "subject": { + "type": "urn:altinn:organization:identifier-no", + "value": "910459880" + }, + "resource": { + "type": "urn:altinn:resource", + "value": "ttd-accesslist-resource" + }, + "action": { + "type": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "value": "whatever" + }, + "result": "Authorized" +} \ No newline at end of file diff --git a/test/IntegrationTests/Data/Json/ResourceList/ResourceList.json b/test/IntegrationTests/Data/Json/ResourceList/ResourceList.json new file mode 100644 index 00000000..dd61d295 --- /dev/null +++ b/test/IntegrationTests/Data/Json/ResourceList/ResourceList.json @@ -0,0 +1,137 @@ +[ + { + "identifier": "ttd-externalpdp-resource1", + "title": { + "en": "External PDP Test Resource1", + "nb": "External PDP Test Resource1", + "nn": "External PDP Test Resource1" + }, + "description": { + "en": "Very nice test resource", + "nb": "Veldig fin test ressurs", + "nn": "Steikje fine test ressurs" + }, + "rightDescription": { + "en": "You'll give access to this nice test resource", + "nb": "Du gir tilgang til denne fine test ressursen", + "nn": "Du gir tilgong til dinne steikje fine test ressursen" + }, + "homepage": "", + "status": "Active", + "contactPoints": [], + "isPartOf": "", + "resourceReferences": [], + "delegable": true, + "visible": true, + "hasCompetentAuthority": { + "name": { + "en": "Test Ministry", + "nb": "Testdepartementet", + "nn": "Testdepartementet" + }, + "organization": "991825827", + "orgcode": "ttd" + }, + "keywords": [], + "AccessListMode": "Disabled", + "selfIdentifiedUserEnabled": false, + "enterpriseUserEnabled": false, + "resourceType": "GenericAccessResource", + "authorizationReference": [ + { + "id": "urn:altinn:resource", + "value": "ttd-externalpdp-resource1" + } + ] + }, + { + "identifier": "ttd-accesslist-resource", + "title": { + "en": "PDP Test Resource Requiring AccessList Authorization", + "nb": "PDP Test Resource Requiring AccessList Authorization", + "nn": "PDP Test Resource Requiring AccessList Authorization" + }, + "description": { + "en": "Very nice test resource", + "nb": "Veldig fin test ressurs", + "nn": "Steikje fine test ressurs" + }, + "rightDescription": { + "en": "You'll give access to this nice test resource", + "nb": "Du gir tilgang til denne fine test ressursen", + "nn": "Du gir tilgong til dinne steikje fine test ressursen" + }, + "homepage": "", + "status": "Active", + "contactPoints": [], + "isPartOf": "", + "resourceReferences": [], + "delegable": true, + "visible": true, + "hasCompetentAuthority": { + "name": { + "en": "Test Ministry", + "nb": "Testdepartementet", + "nn": "Testdepartementet" + }, + "organization": "991825827", + "orgcode": "ttd" + }, + "keywords": [], + "AccessListMode": "Enabled", + "selfIdentifiedUserEnabled": false, + "enterpriseUserEnabled": false, + "resourceType": "GenericAccessResource", + "authorizationReference": [ + { + "id": "urn:altinn:resource", + "value": "ttd-accesslist-resource" + } + ] + }, + { + "identifier": "ttd-accesslist-resource-with-actionfilter", + "title": { + "en": "PDP Test Resource Requiring AccessList Authorization, AccessLists include action filter", + "nb": "PDP Test Resource Requiring AccessList Authorization, AccessLists include action filter", + "nn": "PDP Test Resource Requiring AccessList Authorization, AccessLists include action filter" + }, + "description": { + "en": "Very nice test resource", + "nb": "Veldig fin test ressurs", + "nn": "Steikje fine test ressurs" + }, + "rightDescription": { + "en": "You'll give access to this nice test resource", + "nb": "Du gir tilgang til denne fine test ressursen", + "nn": "Du gir tilgong til dinne steikje fine test ressursen" + }, + "homepage": "", + "status": "Active", + "contactPoints": [], + "isPartOf": "", + "resourceReferences": [], + "delegable": true, + "visible": true, + "hasCompetentAuthority": { + "name": { + "en": "Test Ministry", + "nb": "Testdepartementet", + "nn": "Testdepartementet" + }, + "organization": "991825827", + "orgcode": "ttd" + }, + "keywords": [], + "AccessListMode": "Enabled", + "selfIdentifiedUserEnabled": false, + "enterpriseUserEnabled": false, + "resourceType": "GenericAccessResource", + "authorizationReference": [ + { + "id": "urn:altinn:resource", + "value": "ttd-accesslist-resource-with-actionfilter" + } + ] + } +] diff --git a/test/IntegrationTests/Data/Register/50005545.json b/test/IntegrationTests/Data/Register/50005545.json new file mode 100644 index 00000000..53ce3b14 --- /dev/null +++ b/test/IntegrationTests/Data/Register/50005545.json @@ -0,0 +1,29 @@ +{ + "PartyTypeName": 2, + "SSN": "", + "OrgNumber": "910459880", + "Person": null, + "Organization": { + "OrgNumber": "910459880", + "Name": "ØRSTA OG HEGGEDAL REGNSKAP", + "UnitType": "AS", + "TelephoneNumber": null, + "MobileNumber": null, + "FaxNumber": null, + "EMailAddress": "test@test.test", + "InternetAddress": null, + "MailingAddress": null, + "MailingPostalCode": null, + "MailingPostalCity": null, + "BusinessAddress": null, + "BusinessPostalCode": null, + "BusinessPostalCity": null + }, + "PartyId": 50005545, + "PartyUuid": "00000000-0000-0000-0005-000000005545", + "UnitType": "AS", + "Name": "ØRSTA OG HEGGEDAL REGNSKAP", + "IsDeleted": false, + "OnlyHierarchyElementWithNoAccess": false, + "ChildParties": null +} \ No newline at end of file diff --git a/test/IntegrationTests/Data/Register/Org/910459880.json b/test/IntegrationTests/Data/Register/Org/910459880.json new file mode 100644 index 00000000..53ce3b14 --- /dev/null +++ b/test/IntegrationTests/Data/Register/Org/910459880.json @@ -0,0 +1,29 @@ +{ + "PartyTypeName": 2, + "SSN": "", + "OrgNumber": "910459880", + "Person": null, + "Organization": { + "OrgNumber": "910459880", + "Name": "ØRSTA OG HEGGEDAL REGNSKAP", + "UnitType": "AS", + "TelephoneNumber": null, + "MobileNumber": null, + "FaxNumber": null, + "EMailAddress": "test@test.test", + "InternetAddress": null, + "MailingAddress": null, + "MailingPostalCode": null, + "MailingPostalCity": null, + "BusinessAddress": null, + "BusinessPostalCode": null, + "BusinessPostalCity": null + }, + "PartyId": 50005545, + "PartyUuid": "00000000-0000-0000-0005-000000005545", + "UnitType": "AS", + "Name": "ØRSTA OG HEGGEDAL REGNSKAP", + "IsDeleted": false, + "OnlyHierarchyElementWithNoAccess": false, + "ChildParties": null +} \ No newline at end of file diff --git a/test/IntegrationTests/Data/Roles/user_20000490/party_50005545/roles.json b/test/IntegrationTests/Data/Roles/user_20000490/party_50005545/roles.json new file mode 100644 index 00000000..6986aad0 --- /dev/null +++ b/test/IntegrationTests/Data/Roles/user_20000490/party_50005545/roles.json @@ -0,0 +1,10 @@ +[ + { + "Type": "altinn", + "value": "dagl" + }, + { + "Type": "altinn", + "value": "apiadm" + } +] \ No newline at end of file diff --git a/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_DenyAccessListDontSupportPersonRequest.json b/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_DenyAccessListDontSupportPersonRequest.json new file mode 100644 index 00000000..ae68a794 --- /dev/null +++ b/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_DenyAccessListDontSupportPersonRequest.json @@ -0,0 +1,40 @@ +{ + "Request": { + "ReturnPolicyIdList": true, + "AccessSubject": [ + { + "Attribute": [ + { + "AttributeId": "urn:altinn:userid", + "Value": "1337" + } + ] + } + ], + "Action": [ + { + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "write", + "DataType": "http://www.w3.org/2001/XMLSchema#string" + } + ] + } + ], + "Resource": [ + { + "Attribute": [ + { + "AttributeId": "urn:altinn:resource", + "Value": "ttd-accesslist-resource-with-actionfilter" + }, + { + "AttributeId": "urn:altinn:partyid", + "Value": "501337" + } + ] + } + ] + } +} diff --git a/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_DenyAccessListDontSupportPersonResponse.json b/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_DenyAccessListDontSupportPersonResponse.json new file mode 100644 index 00000000..daf3682e --- /dev/null +++ b/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_DenyAccessListDontSupportPersonResponse.json @@ -0,0 +1,19 @@ +{ + "response": [ + { + "decision": "Deny", + "status": { + "statusMessage": null, + "statusDetails": null, + "statusCode": { + "value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "statusCode": null + } + }, + "obligations": null, + "associateAdvice": null, + "category": null, + "policyIdentifierList": null + } + ] +} \ No newline at end of file diff --git a/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_DenyActionFilterNotMatchingRequest.json b/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_DenyActionFilterNotMatchingRequest.json new file mode 100644 index 00000000..7de33af7 --- /dev/null +++ b/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_DenyActionFilterNotMatchingRequest.json @@ -0,0 +1,40 @@ +{ + "Request": { + "ReturnPolicyIdList": true, + "AccessSubject": [ + { + "Attribute": [ + { + "AttributeId": "urn:altinn:userid", + "Value": "20000490" + } + ] + } + ], + "Action": [ + { + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "write", + "DataType": "http://www.w3.org/2001/XMLSchema#string" + } + ] + } + ], + "Resource": [ + { + "Attribute": [ + { + "AttributeId": "urn:altinn:resource", + "Value": "ttd-accesslist-resource-with-actionfilter" + }, + { + "AttributeId": "urn:altinn:organization:identifier-no", + "Value": "910459880" + } + ] + } + ] + } +} diff --git a/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_DenyActionFilterNotMatchingResponse.json b/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_DenyActionFilterNotMatchingResponse.json new file mode 100644 index 00000000..daf3682e --- /dev/null +++ b/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_DenyActionFilterNotMatchingResponse.json @@ -0,0 +1,19 @@ +{ + "response": [ + { + "decision": "Deny", + "status": { + "statusMessage": null, + "statusDetails": null, + "statusCode": { + "value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "statusCode": null + } + }, + "obligations": null, + "associateAdvice": null, + "category": null, + "policyIdentifierList": null + } + ] +} \ No newline at end of file diff --git a/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_DenyRequest.json b/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_DenyRequest.json new file mode 100644 index 00000000..de2d33d1 --- /dev/null +++ b/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_DenyRequest.json @@ -0,0 +1,40 @@ +{ + "Request": { + "ReturnPolicyIdList": true, + "AccessSubject": [ + { + "Attribute": [ + { + "AttributeId": "urn:altinn:userid", + "Value": "1337" + } + ] + } + ], + "Action": [ + { + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "write", + "DataType": "http://www.w3.org/2001/XMLSchema#string" + } + ] + } + ], + "Resource": [ + { + "Attribute": [ + { + "AttributeId": "urn:altinn:resource", + "Value": "ttd-accesslist-resource" + }, + { + "AttributeId": "urn:altinn:partyid", + "Value": "500000" + } + ] + } + ] + } +} diff --git a/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_DenyResponse.json b/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_DenyResponse.json new file mode 100644 index 00000000..daf3682e --- /dev/null +++ b/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_DenyResponse.json @@ -0,0 +1,19 @@ +{ + "response": [ + { + "decision": "Deny", + "status": { + "statusMessage": null, + "statusDetails": null, + "statusCode": { + "value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "statusCode": null + } + }, + "obligations": null, + "associateAdvice": null, + "category": null, + "policyIdentifierList": null + } + ] +} \ No newline at end of file diff --git a/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_PermitRequest.json b/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_PermitRequest.json new file mode 100644 index 00000000..ac478238 --- /dev/null +++ b/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_PermitRequest.json @@ -0,0 +1,40 @@ +{ + "Request": { + "ReturnPolicyIdList": true, + "AccessSubject": [ + { + "Attribute": [ + { + "AttributeId": "urn:altinn:userid", + "Value": "20000490" + } + ] + } + ], + "Action": [ + { + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "read", + "DataType": "http://www.w3.org/2001/XMLSchema#string" + } + ] + } + ], + "Resource": [ + { + "Attribute": [ + { + "AttributeId": "urn:altinn:resource", + "Value": "ttd-accesslist-resource" + }, + { + "AttributeId": "urn:altinn:partyid", + "Value": "50005545" + } + ] + } + ] + } +} diff --git a/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_PermitResponse.json b/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_PermitResponse.json new file mode 100644 index 00000000..f6efcc08 --- /dev/null +++ b/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_PermitResponse.json @@ -0,0 +1,32 @@ +{ + "response": [ + { + "decision": "Permit", + "status": { + "statusMessage": null, + "statusDetails": null, + "statusCode": { + "value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "statusCode": null + } + }, + "obligations": [ + { + "id": "urn:altinn:obligation:1", + "attributeAssignment": [ + { + "attributeId": "urn:altinn:obligation-assignment:1", + "value": "3", + "category": "urn:altinn:minimum-authenticationlevel", + "dataType": "http://www.w3.org/2001/XMLSchema#integer", + "issuer": null + } + ] + } + ], + "associateAdvice": null, + "category": null, + "policyIdentifierList": null + } + ] +} \ No newline at end of file diff --git a/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_PermitWithActionFilterMatchRequest.json b/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_PermitWithActionFilterMatchRequest.json new file mode 100644 index 00000000..9d683a72 --- /dev/null +++ b/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_PermitWithActionFilterMatchRequest.json @@ -0,0 +1,40 @@ +{ + "Request": { + "ReturnPolicyIdList": true, + "AccessSubject": [ + { + "Attribute": [ + { + "AttributeId": "urn:altinn:userid", + "Value": "20000490" + } + ] + } + ], + "Action": [ + { + "Attribute": [ + { + "AttributeId": "urn:oasis:names:tc:xacml:1.0:action:action-id", + "Value": "read", + "DataType": "http://www.w3.org/2001/XMLSchema#string" + } + ] + } + ], + "Resource": [ + { + "Attribute": [ + { + "AttributeId": "urn:altinn:resource", + "Value": "ttd-accesslist-resource-with-actionfilter" + }, + { + "AttributeId": "urn:altinn:partyid", + "Value": "50005545" + } + ] + } + ] + } +} diff --git a/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_PermitWithActionFilterMatchResponse.json b/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_PermitWithActionFilterMatchResponse.json new file mode 100644 index 00000000..f6efcc08 --- /dev/null +++ b/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ResourceRegistry_AccessListAuthorization_Json_PermitWithActionFilterMatchResponse.json @@ -0,0 +1,32 @@ +{ + "response": [ + { + "decision": "Permit", + "status": { + "statusMessage": null, + "statusDetails": null, + "statusCode": { + "value": "urn:oasis:names:tc:xacml:1.0:status:ok", + "statusCode": null + } + }, + "obligations": [ + { + "id": "urn:altinn:obligation:1", + "attributeAssignment": [ + { + "attributeId": "urn:altinn:obligation-assignment:1", + "value": "3", + "category": "urn:altinn:minimum-authenticationlevel", + "dataType": "http://www.w3.org/2001/XMLSchema#integer", + "issuer": null + } + ] + } + ], + "associateAdvice": null, + "category": null, + "policyIdentifierList": null + } + ] +} \ No newline at end of file diff --git a/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ttd-accesslist-resource-with-actionfilter/policy.xml b/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ttd-accesslist-resource-with-actionfilter/policy.xml new file mode 100644 index 00000000..ede33bac --- /dev/null +++ b/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ttd-accesslist-resource-with-actionfilter/policy.xml @@ -0,0 +1,58 @@ + + + + + A rule giving user with role PRIV or DAGL and the ttd the right to read and write to this AccessList Authorized Resource + + + + + PRIV + + + + + + DAGL + + + + + + ttd + + + + + + + + ttd-accesslist-resource-with-actionfilter + + + + + + + + read + + + + + + write + + + + + + + + + + 3 + + + + \ No newline at end of file diff --git a/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ttd-accesslist-resource/policy.xml b/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ttd-accesslist-resource/policy.xml new file mode 100644 index 00000000..f159c11c --- /dev/null +++ b/test/IntegrationTests/Data/Xacml/3.0/ResourceRegistry/ttd-accesslist-resource/policy.xml @@ -0,0 +1,58 @@ + + + + + A rule giving user with role PRIV or DAGL and the ttd the right to read and write to this AccessList Authorized Resource + + + + + PRIV + + + + + + DAGL + + + + + + ttd + + + + + + + + ttd-accesslist-resource + + + + + + + + read + + + + + + write + + + + + + + + + + 3 + + + + \ No newline at end of file diff --git a/test/IntegrationTests/DelegationsControllerTest.cs b/test/IntegrationTests/DelegationsControllerTest.cs index da8609ec..79e8f217 100644 --- a/test/IntegrationTests/DelegationsControllerTest.cs +++ b/test/IntegrationTests/DelegationsControllerTest.cs @@ -6,6 +6,7 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; +using Altinn.Common.AccessToken.Services; using Altinn.Platform.Authorization.Constants; using Altinn.Platform.Authorization.Controllers; using Altinn.Platform.Authorization.IntegrationTests.Data; @@ -1287,6 +1288,7 @@ private HttpClient GetTestClient(DelegationChangeEventQueueMock queueMock = null services.AddSingleton(); services.AddSingleton(queueMock); services.AddSingleton, JwtCookiePostConfigureOptionsStub>(); + services.AddSingleton(); }); }).CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); diff --git a/test/IntegrationTests/ExternalDecisionTest.cs b/test/IntegrationTests/ExternalDecisionTest.cs index df5dadbc..b85e5984 100644 --- a/test/IntegrationTests/ExternalDecisionTest.cs +++ b/test/IntegrationTests/ExternalDecisionTest.cs @@ -4,6 +4,7 @@ using Altinn.Authorization.ABAC.Interface; using Altinn.Authorization.ABAC.Xacml; using Altinn.Authorization.ABAC.Xacml.JsonProfile; +using Altinn.Common.AccessToken.Services; using Altinn.Common.Authentication.Configuration; using Altinn.Platform.Authorization.Controllers; using Altinn.Platform.Authorization.IntegrationTests.MockServices; @@ -36,7 +37,7 @@ public ExternalDecisionTest(CustomWebApplicationFactory fixt [Fact] public async Task PDPExternal_Decision_AltinnApps0008() { - string token = PrincipalUtil.GetOrgToken("skd", "974761076", "altinn:authorization:pdp"); + string token = PrincipalUtil.GetOrgToken("skd", "974761076", "altinn:authorization/authorize"); string testCase = "AltinnApps0008"; HttpClient client = GetTestClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token); @@ -53,7 +54,7 @@ public async Task PDPExternal_Decision_AltinnApps0008() [Fact] public async Task PDPExternal_Decision_AltinnApps0010() { - string token = PrincipalUtil.GetOrgToken("skd", "974761076", "altinn:authorization:pdp"); + string token = PrincipalUtil.GetOrgToken("skd", "974761076", "altinn:authorization/authorize"); string testCase = "AltinnApps0010"; HttpClient client = GetTestClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token); @@ -70,7 +71,7 @@ public async Task PDPExternal_Decision_AltinnApps0010() [Fact] public async Task PDPExternal_Decision_AltinnResourceRegistry0005() { - string token = PrincipalUtil.GetOrgToken("skd", "974761076", "altinn:authorization:pdp"); + string token = PrincipalUtil.GetOrgToken("skd", "974761076", "altinn:authorization/authorize"); string testCase = "AltinnResourceRegistry0005"; HttpClient client = GetTestClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token); @@ -87,7 +88,7 @@ public async Task PDPExternal_Decision_AltinnResourceRegistry0005() [Fact] public async Task PDPExternal_Decision_AltinnResourceRegistry0006() { - string token = PrincipalUtil.GetOrgToken("skd", "974761076", "altinn:authorization:pdp"); + string token = PrincipalUtil.GetOrgToken("skd", "974761076", "altinn:authorization/authorize"); string testCase = "AltinnResourceRegistry0006"; HttpClient client = GetTestClient(); @@ -106,7 +107,7 @@ public async Task PDPExternal_Decision_AltinnResourceRegistry0006() [Fact] public async Task PDPExternal_Decision_AltinnResourceRegistry0007() { - string token = PrincipalUtil.GetOrgToken("skd", "974761076", "altinn:authorization:pdp"); + string token = PrincipalUtil.GetOrgToken("skd", "974761076", "altinn:authorization/authorize"); string testCase = "AltinnResourceRegistry0007"; HttpClient client = GetTestClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token); @@ -123,7 +124,7 @@ public async Task PDPExternal_Decision_AltinnResourceRegistry0007() [Fact] public async Task PDPExternal_Decision_AltinnResourceRegistry0008() { - string token = PrincipalUtil.GetOrgToken("skd", "974761076", "altinn:authorization:pdp"); + string token = PrincipalUtil.GetOrgToken("skd", "974761076", "altinn:authorization/authorize"); string testCase = "AltinnResourceRegistry0008"; HttpClient client = GetTestClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token); @@ -143,7 +144,7 @@ public async Task PDPExternal_Decision_AltinnResourceRegistry0008() [Fact] public async Task PDPExternal_Decision_AltinnResourceRegistry0009() { - string token = PrincipalUtil.GetOrgToken("skd", "974761076", "altinn:authorization:pdp"); + string token = PrincipalUtil.GetOrgToken("skd", "974761076", "altinn:authorization/authorize"); string testCase = "AltinnResourceRegistry0009"; HttpClient client = GetTestClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token); @@ -163,7 +164,7 @@ public async Task PDPExternal_Decision_AltinnResourceRegistry0009() [Fact] public async Task PDPExternal_Decision_AltinnResourceRegistry0010() { - string token = PrincipalUtil.GetOrgToken("skd", "974761076", "altinn:authorization:pdp"); + string token = PrincipalUtil.GetOrgToken("skd", "974761076", "altinn:authorization/authorize"); string testCase = "AltinnResourceRegistry0010"; HttpClient client = GetTestClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token); @@ -183,7 +184,7 @@ public async Task PDPExternal_Decision_AltinnResourceRegistry0010() [Fact] public async Task PDPExternal_Decision_AltinnResourceRegistry0011() { - string token = PrincipalUtil.GetOrgToken("skd", "974761076", "altinn:authorization:pdp"); + string token = PrincipalUtil.GetOrgToken("skd", "974761076", "altinn:authorization/authorize"); string testCase = "AltinnResourceRegistry0011"; HttpClient client = GetTestClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token); @@ -203,7 +204,7 @@ public async Task PDPExternal_Decision_AltinnResourceRegistry0011() [Fact] public async Task PDPExternal_Decision_SystemUserWithResourceDelegation_Permit() { - string token = PrincipalUtil.GetOrgToken("skd", "974761076", "altinn:authorization:pdp"); + string token = PrincipalUtil.GetOrgToken("skd", "974761076", "altinn:authorization/authorize"); string testCase = "ResourceRegistry_SystemUserWithDelegation_Permit"; HttpClient client = GetTestClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token); @@ -223,7 +224,7 @@ public async Task PDPExternal_Decision_SystemUserWithResourceDelegation_Permit() [Fact] public async Task PDPExternal_Decision_SystemUserWithAppDelegation_Permit() { - string token = PrincipalUtil.GetOrgToken("skd", "974761076", "altinn:authorization:pdp"); + string token = PrincipalUtil.GetOrgToken("skd", "974761076", "altinn:authorization/authorize"); string testCase = "AltinnApps_SystemUserWithDelegation_Permit"; HttpClient client = GetTestClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token); @@ -243,7 +244,7 @@ public async Task PDPExternal_Decision_SystemUserWithAppDelegation_Permit() [Fact] public async Task PDPExternal_Decision_SystemUserWithDelegation_TooManyRequestSubjects_Indeterminate() { - string token = PrincipalUtil.GetOrgToken("skd", "974761076", "altinn:authorization:pdp"); + string token = PrincipalUtil.GetOrgToken("skd", "974761076", "altinn:authorization/authorize"); string testCase = "ResourceRegistry_SystemUserWithDelegation_TooManyRequestSubjects_Indeterminate"; HttpClient client = GetTestClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token); @@ -275,7 +276,9 @@ private HttpClient GetTestClient() services.AddSingleton, JwtCookiePostConfigureOptionsStub>(); services.AddSingleton, OidcProviderPostConfigureSettingsStub>(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); }); }).CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); diff --git a/test/IntegrationTests/MockServices/PublicSigningKeyProviderMock.cs b/test/IntegrationTests/MockServices/PublicSigningKeyProviderMock.cs new file mode 100644 index 00000000..3e9cee93 --- /dev/null +++ b/test/IntegrationTests/MockServices/PublicSigningKeyProviderMock.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; + +using Altinn.Common.AccessToken.Services; + +using Microsoft.IdentityModel.Tokens; + +namespace Altinn.Platform.Authorization.IntegrationTests.MockServices; + +public class PublicSigningKeyProviderMock : IPublicSigningKeyProvider +{ + public Task> GetSigningKeys(string issuer) + { + List signingKeys = new List(); + + X509Certificate2 cert = new X509Certificate2($"{issuer}-org.pem"); + SecurityKey key = new X509SecurityKey(cert); + + signingKeys.Add(key); + + return Task.FromResult(signingKeys.AsEnumerable()); + } +} diff --git a/test/IntegrationTests/MockServices/RegisterServiceMock.cs b/test/IntegrationTests/MockServices/RegisterServiceMock.cs index 5bc32f16..033aedcb 100644 --- a/test/IntegrationTests/MockServices/RegisterServiceMock.cs +++ b/test/IntegrationTests/MockServices/RegisterServiceMock.cs @@ -7,7 +7,7 @@ using Altinn.Platform.Authorization.Exceptions; using Altinn.Platform.Authorization.Services.Interfaces; using Altinn.Platform.Register.Models; - +using Microsoft.Extensions.Caching.Memory; using Newtonsoft.Json; namespace Altinn.Platform.Events.Tests.Mocks @@ -15,6 +15,7 @@ namespace Altinn.Platform.Events.Tests.Mocks public class RegisterServiceMock : IRegisterService { private readonly int _partiesCollection; + private IMemoryCache _memoryCache = new MemoryCache(new MemoryCacheOptions()); public RegisterServiceMock(int partiesCollection = 1) { @@ -23,39 +24,72 @@ public RegisterServiceMock(int partiesCollection = 1) public async Task GetParty(int partyId) { - Party party = null; - string partyPath = GetPartyPath(partyId); - if (File.Exists(partyPath)) + string cacheKey = $"p:{partyId}"; + if (!_memoryCache.TryGetValue(cacheKey, out Party party)) { - string content = File.ReadAllText(partyPath); - party = JsonConvert.DeserializeObject(content); + string partyPath = GetPartyPath(partyId); + if (File.Exists(partyPath)) + { + string content = File.ReadAllText(partyPath); + party = JsonConvert.DeserializeObject(content); + } + + if (party != null) + { + PutInCache(cacheKey, 10, party); + } } return await Task.FromResult(party); } - public async Task PartyLookup(string orgNo, string person) + public async Task PartyLookup(string orgNo, string person) { - string eventsPath = Path.Combine(GetPartiesPath(), $@"{_partiesCollection}.json"); - int partyId = 0; + string cacheKey; + PartyLookup partyLookup; - if (File.Exists(eventsPath)) + if (!string.IsNullOrWhiteSpace(orgNo)) + { + cacheKey = $"org:{orgNo}"; + partyLookup = new PartyLookup { OrgNo = orgNo }; + } + else if (!string.IsNullOrWhiteSpace(person)) + { + cacheKey = $"fnr:{person}"; + partyLookup = new PartyLookup { Ssn = person }; + } + else { - string content = File.ReadAllText(eventsPath); - List parties = JsonConvert.DeserializeObject>(content); + return null; + } + + if (!_memoryCache.TryGetValue(cacheKey, out Party party)) + { + string eventsPath = Path.Combine(GetPartiesPath(), $@"{_partiesCollection}.json"); - if (!string.IsNullOrEmpty(orgNo)) + if (File.Exists(eventsPath)) { - partyId = parties.Where(p => p.OrgNumber != null && p.OrgNumber.Equals(orgNo)).Select(p => p.PartyId).FirstOrDefault(); + string content = File.ReadAllText(eventsPath); + List parties = JsonConvert.DeserializeObject>(content); + + if (!string.IsNullOrEmpty(orgNo)) + { + party = parties.Where(p => p.OrgNumber != null && p.OrgNumber.Equals(orgNo)).FirstOrDefault(); + } + else + { + party = parties.Where(p => p.SSN != null && p.SSN.Equals(person)).FirstOrDefault(); + } } - else + + if (party != null) { - partyId = parties.Where(p => p.SSN != null && p.SSN.Equals(person)).Select(p => p.PartyId).FirstOrDefault(); + PutInCache(cacheKey, 10, party); } } - return partyId > 0 - ? partyId + return party != null + ? await Task.FromResult(party) : throw await PlatformHttpException.CreateAsync(new HttpResponseMessage { Content = new StringContent(string.Empty), StatusCode = System.Net.HttpStatusCode.NotFound }); } @@ -71,5 +105,14 @@ private static string GetPartyPath(int partyId) string unitTestFolder = Path.GetDirectoryName(new Uri(typeof(RegisterServiceMock).Assembly.Location).LocalPath); return Path.Combine(unitTestFolder, "..", "..", "..", "Data", "Register", partyId.ToString() + ".json"); } + + private void PutInCache(string cachekey, int cacheTimeout, object cacheObject) + { + var cacheEntryOptions = new MemoryCacheEntryOptions() + .SetPriority(CacheItemPriority.High) + .SetAbsoluteExpiration(new TimeSpan(0, cacheTimeout, 0)); + + _memoryCache.Set(cachekey, cacheObject, cacheEntryOptions); + } } } diff --git a/test/IntegrationTests/MockServices/ResourceRegistryMock.cs b/test/IntegrationTests/MockServices/ResourceRegistryMock.cs index c56d3772..f85c1106 100644 --- a/test/IntegrationTests/MockServices/ResourceRegistryMock.cs +++ b/test/IntegrationTests/MockServices/ResourceRegistryMock.cs @@ -1,45 +1,149 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using System.Xml; using Altinn.Authorization.ABAC.Utils; using Altinn.Authorization.ABAC.Xacml; +using Altinn.Authorization.Models.Register; +using Altinn.Authorization.Models.ResourceRegistry; using Altinn.Platform.Authorization.Services.Interface; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Caching.Memory; -namespace Altinn.Platform.Authorization.IntegrationTests.MockServices +namespace Altinn.Platform.Authorization.IntegrationTests.MockServices; + +public class ResourceRegistryMock : IResourceRegistry { - public class ResourceRegistryMock : IResourceRegistry + private readonly JsonSerializerOptions options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + private IMemoryCache _memoryCache = new MemoryCache(new MemoryCacheOptions()); + + public Task GetResourceAsync(string resourceId, CancellationToken cancellationToken = default) { - public async Task GetResourcePolicyAsync(string resourceId) + string cacheKey = "r:" + resourceId; + if (!_memoryCache.TryGetValue(cacheKey, out ServiceResource resource)) { - if (File.Exists(Path.Combine(GetResourceRegistryPolicyPath(resourceId), "policy.xml"))) + string unitTestFolder = Path.GetDirectoryName(new Uri(typeof(AltinnApps_DecisionTests).Assembly.Location).LocalPath); + string resourceListPath = Path.Combine(unitTestFolder, "Data", "Json", "ResourceList", "ResourceList.json"); + if (File.Exists(resourceListPath)) + { + string content = File.ReadAllText(resourceListPath); + List resourceList = JsonSerializer.Deserialize>(content, options); + return Task.FromResult(resourceList.Find(r => r.Identifier.Equals(resourceId))); + } + + if (resource != null) { - return await Task.FromResult(ParsePolicy("policy.xml", GetResourceRegistryPolicyPath(resourceId))); + PutInCache(cacheKey, 5, resource); } - - return null; } - private static string GetResourceRegistryPolicyPath(string resourceId) + return Task.FromResult(resource); + } + + public async Task GetResourcePolicyAsync(string resourceId, CancellationToken cancellationToken = default) + { + string cacheKey = "resourcepolicy:" + resourceId; + if (!_memoryCache.TryGetValue(cacheKey, out XacmlPolicy policy)) { - string unitTestFolder = Path.GetDirectoryName(new Uri(typeof(AltinnApps_DecisionTests).Assembly.Location).LocalPath); - return Path.Combine(unitTestFolder, "..", "..", "..", "Data", "Xacml", "3.0", "ResourceRegistry", resourceId); + if (File.Exists(Path.Combine(GetResourceRegistryPolicyPath(resourceId), "policy.xml"))) + { + policy = await Task.FromResult(ParsePolicy("policy.xml", GetResourceRegistryPolicyPath(resourceId))); + } + + if (policy != null) + { + if (File.Exists(Path.Combine(GetResourceRegistryPolicyPath(resourceId), "policy.xml"))) + { + return await Task.FromResult(ParsePolicy("policy.xml", GetResourceRegistryPolicyPath(resourceId))); + } + + PutInCache(cacheKey, 5, policy); + } } - - public static XacmlPolicy ParsePolicy(string policyDocumentTitle, string policyPath) + + return policy; + } + + public Task> GetMembershipsForResourceForParty(PartyUrn partyUrn, ResourceIdUrn resourceIdUrn, CancellationToken cancellationToken = default) + { + partyUrn.IsPartyUuid(out Guid partyUuid); + partyUrn.IsOrganizationIdentifier(out OrganizationNumber partyOrgNum); + resourceIdUrn.IsResourceId(out ResourceIdentifier resourceId); + + string cacheKey = $"AccListMemb|{partyUrn}|{resourceIdUrn}"; + if (!_memoryCache.TryGetValue(cacheKey, out IEnumerable memberships)) { - XmlDocument policyDocument = new XmlDocument(); + if (partyOrgNum.ToString() == "910459880" && resourceId.ToString() == "ttd-accesslist-resource") + { + memberships = JsonSerializer.Deserialize>( + """ + [ + { + "party": "urn:altinn:party:uuid:00000000-0000-0000-0005-000000005545", + "resource": "urn:altinn:resource:ttd-accesslist-resource", + "since": "2024-08-27T15:15:55.446051+00:00" + } + ] + """, + options); + } + else if (partyOrgNum.ToString() == "910459880" && resourceId.ToString() == "ttd-accesslist-resource-with-actionfilter") + { + memberships = JsonSerializer.Deserialize>( + """ + [ + { + "party": "urn:altinn:party:uuid:00000000-0000-0000-0005-000000005545", + "resource": "urn:altinn:resource:ttd-accesslist-resource", + "since": "2024-08-27T15:15:55.446051+00:00", + "actionFilters": [ + "read" + ] + } + ] + """, + options); + } - policyDocument.Load(Path.Combine(policyPath, policyDocumentTitle)); - XacmlPolicy policy; - using (XmlReader reader = XmlReader.Create(new StringReader(policyDocument.OuterXml))) + if (memberships != null) { - policy = XacmlParser.ParseXacmlPolicy(reader); + PutInCache(cacheKey, 5, memberships); + return Task.FromResult(memberships); } + } + + return Task.FromResult(Enumerable.Empty()); + } - return policy; + private static string GetResourceRegistryPolicyPath(string resourceId) + { + string unitTestFolder = Path.GetDirectoryName(new Uri(typeof(AltinnApps_DecisionTests).Assembly.Location).LocalPath); + return Path.Combine(unitTestFolder, "..", "..", "..", "Data", "Xacml", "3.0", "ResourceRegistry", resourceId); + } + + public static XacmlPolicy ParsePolicy(string policyDocumentTitle, string policyPath) + { + XmlDocument policyDocument = new XmlDocument(); + + policyDocument.Load(Path.Combine(policyPath, policyDocumentTitle)); + XacmlPolicy policy; + using (XmlReader reader = XmlReader.Create(new StringReader(policyDocument.OuterXml))) + { + policy = XacmlParser.ParseXacmlPolicy(reader); } + + return policy; + } + + private void PutInCache(string cachekey, int cacheTimeout, object cacheObject) + { + var cacheEntryOptions = new MemoryCacheEntryOptions() + .SetPriority(CacheItemPriority.High) + .SetAbsoluteExpiration(new TimeSpan(0, cacheTimeout, 0)); + + _memoryCache.Set(cachekey, cacheObject, cacheEntryOptions); } } diff --git a/test/IntegrationTests/PartiesControllerTest.cs b/test/IntegrationTests/PartiesControllerTest.cs index 93691325..cac8eebc 100644 --- a/test/IntegrationTests/PartiesControllerTest.cs +++ b/test/IntegrationTests/PartiesControllerTest.cs @@ -2,6 +2,7 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; +using Altinn.Common.AccessToken.Services; using Altinn.Platform.Authorization.Controllers; using Altinn.Platform.Authorization.IntegrationTests.MockServices; using Altinn.Platform.Authorization.IntegrationTests.Util; @@ -61,6 +62,7 @@ private HttpClient GetTestClient() services.AddSingleton(); services.AddSingleton(); services.AddSingleton, JwtCookiePostConfigureOptionsStub>(); + services.AddSingleton(); }); }).CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); diff --git a/test/IntegrationTests/PolicyControllerTest.cs b/test/IntegrationTests/PolicyControllerTest.cs index 1c8bcaf6..663d6df3 100644 --- a/test/IntegrationTests/PolicyControllerTest.cs +++ b/test/IntegrationTests/PolicyControllerTest.cs @@ -6,6 +6,7 @@ using System.Net.Http.Headers; using System.Threading.Tasks; using Altinn.Authorization.ABAC.Constants; +using Altinn.Common.AccessToken.Services; using Altinn.Platform.Authorization.Constants; using Altinn.Platform.Authorization.Controllers; using Altinn.Platform.Authorization.IntegrationTests.Data; @@ -492,6 +493,7 @@ private HttpClient GetTestClient() services.AddSingleton(); services.AddSingleton(); services.AddSingleton, JwtCookiePostConfigureOptionsStub>(); + services.AddSingleton(); }); }).CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); diff --git a/test/IntegrationTests/ResourceRegistry_DecisionTests.cs b/test/IntegrationTests/ResourceRegistry_DecisionTests.cs index fe3757d7..f5a0c9ce 100644 --- a/test/IntegrationTests/ResourceRegistry_DecisionTests.cs +++ b/test/IntegrationTests/ResourceRegistry_DecisionTests.cs @@ -99,6 +99,96 @@ public async Task PDP_Decision_ResourceRegistry_OedFormuesfullmakt_Json_Indeterm AssertionUtil.AssertEqual(expected, contextResponse); } + /// + /// Tests the scenario where the reportee organization has access to 'ttd-accesslist-resource' through access list membership without any action filter. + /// + [Fact] + public async Task PDP_Decision_ResourceRegistry_AccessListAuthorization_Json_Permit() + { + string testCase = "ResourceRegistry_AccessListAuthorization_Json_Permit"; + HttpClient client = GetTestClient(); + HttpRequestMessage httpRequestMessage = TestSetupUtil.CreateJsonProfileXacmlRequest(testCase); + XacmlJsonResponse expected = TestSetupUtil.ReadExpectedJsonProfileResponse(testCase); + + // Act + XacmlJsonResponse contextResponse = await TestSetupUtil.GetXacmlJsonProfileContextResponseAsync(client, httpRequestMessage); + + // Assert + AssertionUtil.AssertEqual(expected, contextResponse); + } + + /// + /// Tests the scenario where the reportee organization does NOT have access to 'ttd-accesslist-resource' through any access list membership. + /// + [Fact] + public async Task PDP_Decision_ResourceRegistry_AccessListAuthorization_Json_Deny() + { + string testCase = "ResourceRegistry_AccessListAuthorization_Json_Deny"; + HttpClient client = GetTestClient(); + HttpRequestMessage httpRequestMessage = TestSetupUtil.CreateJsonProfileXacmlRequest(testCase); + XacmlJsonResponse expected = TestSetupUtil.ReadExpectedJsonProfileResponse(testCase); + + // Act + XacmlJsonResponse contextResponse = await TestSetupUtil.GetXacmlJsonProfileContextResponseAsync(client, httpRequestMessage); + + // Assert + AssertionUtil.AssertEqual(expected, contextResponse); + } + + /// + /// Tests the scenario where the reportee organization has access to 'ttd-accesslist-resource' through access list membership with matching action filter. + /// + [Fact] + public async Task PDP_Decision_ResourceRegistry_AccessListAuthorization_Json_PermitWithActionFilterMatch() + { + string testCase = "ResourceRegistry_AccessListAuthorization_Json_PermitWithActionFilterMatch"; + HttpClient client = GetTestClient(); + HttpRequestMessage httpRequestMessage = TestSetupUtil.CreateJsonProfileXacmlRequest(testCase); + XacmlJsonResponse expected = TestSetupUtil.ReadExpectedJsonProfileResponse(testCase); + + // Act + XacmlJsonResponse contextResponse = await TestSetupUtil.GetXacmlJsonProfileContextResponseAsync(client, httpRequestMessage); + + // Assert + AssertionUtil.AssertEqual(expected, contextResponse); + } + + /// + /// Tests the scenario where the reportee organization has access to 'ttd-accesslist-resource' through access list membership but with action filter not matching the request action. + /// + [Fact] + public async Task PDP_Decision_ResourceRegistry_AccessListAuthorization_Json_DenyActionFilterNotMatching() + { + string testCase = "ResourceRegistry_AccessListAuthorization_Json_DenyActionFilterNotMatching"; + HttpClient client = GetTestClient(); + HttpRequestMessage httpRequestMessage = TestSetupUtil.CreateJsonProfileXacmlRequest(testCase); + XacmlJsonResponse expected = TestSetupUtil.ReadExpectedJsonProfileResponse(testCase); + + // Act + XacmlJsonResponse contextResponse = await TestSetupUtil.GetXacmlJsonProfileContextResponseAsync(client, httpRequestMessage); + + // Assert + AssertionUtil.AssertEqual(expected, contextResponse); + } + + /// + /// Tests the scenario where the reportee is a person. Currently the access list authorization service only supports organizations. + /// + [Fact] + public async Task PDP_Decision_ResourceRegistry_AccessListAuthorization_Json_DenyAccessListDontSupportPerson() + { + string testCase = "ResourceRegistry_AccessListAuthorization_Json_DenyAccessListDontSupportPerson"; + HttpClient client = GetTestClient(); + HttpRequestMessage httpRequestMessage = TestSetupUtil.CreateJsonProfileXacmlRequest(testCase); + XacmlJsonResponse expected = TestSetupUtil.ReadExpectedJsonProfileResponse(testCase); + + // Act + XacmlJsonResponse contextResponse = await TestSetupUtil.GetXacmlJsonProfileContextResponseAsync(client, httpRequestMessage); + + // Assert + AssertionUtil.AssertEqual(expected, contextResponse); + } + [Fact] public async Task PDP_Decision_ResourceRegistry0001() { diff --git a/test/IntegrationTests/Util/AssertionUtil.cs b/test/IntegrationTests/Util/AssertionUtil.cs index 993d6fcb..1f7803cf 100644 --- a/test/IntegrationTests/Util/AssertionUtil.cs +++ b/test/IntegrationTests/Util/AssertionUtil.cs @@ -286,6 +286,22 @@ public static void AssertAuthorizationEvent(Mock eventQueue, numberOfTimes); } + /// + /// Assert that two have the same property values. + /// + /// An instance with the expected values. + /// The instance to verify. + public static void AssertEqual(AccessListAuthorizationResponse expected, AccessListAuthorizationResponse actual) + { + Assert.NotNull(actual); + Assert.NotNull(expected); + + Assert.Equal(expected.Result, actual.Result); + Assert.Equal(expected.Subject.ToString(), actual.Subject.ToString()); + Assert.Equal(expected.Resource.ToString(), actual.Resource.ToString()); + Assert.Equal(expected.Action.ToString(), actual.Action.ToString()); + } + private static void AssertEqual(List expected, List actual) { if (expected == null) diff --git a/test/IntegrationTests/Util/JwtTokenMock.cs b/test/IntegrationTests/Util/JwtTokenMock.cs index 3652b2c0..8edb1e8a 100644 --- a/test/IntegrationTests/Util/JwtTokenMock.cs +++ b/test/IntegrationTests/Util/JwtTokenMock.cs @@ -1,5 +1,6 @@ using System; using System.IdentityModel.Tokens.Jwt; +using System.IO; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; @@ -26,7 +27,7 @@ public static string GenerateToken(ClaimsPrincipal principal, TimeSpan tokenExpi { Subject = new ClaimsIdentity(principal.Identity), Expires = DateTime.UtcNow.AddSeconds(tokenExpiry.TotalSeconds), - SigningCredentials = GetSigningCredentials(), + SigningCredentials = GetSigningCredentials(issuer), Audience = "altinn.no", Issuer = issuer }; @@ -37,8 +38,17 @@ public static string GenerateToken(ClaimsPrincipal principal, TimeSpan tokenExpi return serializedToken; } - private static SigningCredentials GetSigningCredentials() + private static SigningCredentials GetSigningCredentials(string issuer) { + string certPath = "selfSignedTestCertificate.pfx"; + if (!issuer.Equals("UnitTest") && File.Exists($"{issuer}-org.pfx")) + { + certPath = $"{issuer}-org.pfx"; + + X509Certificate2 certIssuer = new X509Certificate2(certPath); + return new X509SigningCredentials(certIssuer, SecurityAlgorithms.RsaSha256); + } + X509Certificate2 cert = new X509Certificate2("selfSignedTestCertificate.pfx", "qwer1234"); return new X509SigningCredentials(cert, SecurityAlgorithms.RsaSha256); } diff --git a/test/IntegrationTests/Util/PrincipalUtil.cs b/test/IntegrationTests/Util/PrincipalUtil.cs index c76f4dd1..f07321b4 100644 --- a/test/IntegrationTests/Util/PrincipalUtil.cs +++ b/test/IntegrationTests/Util/PrincipalUtil.cs @@ -90,10 +90,9 @@ public static ClaimsPrincipal GetClaimsPrincipal(string org, string orgNumber, s return new ClaimsPrincipal(identity); } - public static string GetAccessToken(string appId) + public static string GetAccessToken(string appId, string issuer = "www.altinn.no") { List claims = new List(); - string issuer = "www.altinn.no"; if (!string.IsNullOrEmpty(appId)) { claims.Add(new Claim("urn:altinn:app", appId, ClaimValueTypes.String, issuer)); diff --git a/test/IntegrationTests/platform-org.pem b/test/IntegrationTests/platform-org.pem new file mode 100644 index 00000000..f2a42d40 --- /dev/null +++ b/test/IntegrationTests/platform-org.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDAzCCAeugAwIBAgIJANTdO8o3I8x5MA0GCSqGSIb3DQEBCwUAMA4xDDAKBgNV +BAMTA3R0ZDAeFw0yMDA1MjUxMjIxMzdaFw0zMDA1MjQxMjIxMzdaMA4xDDAKBgNV +BAMTA3R0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMcfTsXwwLyC +UkIz06eadWJvG3yrzT+ZB2Oy/WPaZosDnPcnZvCDueN+oy0zTx5TyH5gCi1FvzX2 +7G2eZEKwQaRPv0yuM+McHy1rXxMSOlH/ebP9KJj3FDMUgZl1DCAjJxSAANdTwdrq +ydVg1Crp37AQx8IIEjnBhXsfQh1uPGt1XwgeNyjl00IejxvQOPzd1CofYWwODVtQ +l3PKn1SEgOGcB6wuHNRlnZPCIelQmqxWkcEZiu/NU+kst3NspVUQG2Jf2AF8UWgC +rnrhMQR0Ra1Vi7bWpu6QIKYkN9q0NRHeRSsELOvTh1FgDySYJtNd2xDRSf6IvOiu +tSipl1NZlV0CAwEAAaNkMGIwIAYDVR0OAQH/BBYEFIwq/KbSMzLETdo9NNxj0rz4 +qMqVMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgWgMCAGA1UdJQEB/wQWMBQG +CCsGAQUFBwMBBggrBgEFBQcDAjANBgkqhkiG9w0BAQsFAAOCAQEAE56UmH5gEYbe +1kVw7nrfH0R9FyVZGeQQWBn4/6Ifn+eMS9mxqe0Lq74Ue1zEzvRhRRqWYi9JlKNf +7QQNrc+DzCceIa1U6cMXgXKuXquVHLmRfqvKHbWHJfIkaY8Mlfy++77UmbkvIzly +T1HVhKKp6Xx0r5koa6frBh4Xo/vKBlEyQxWLWF0RPGpGErnYIosJ41M3Po3nw3lY +f7lmH47cdXatcntj2Ho/b2wGi9+W29teVCDfHn2/0oqc7K0EOY9c2ODLjUvQyPZR +OD2yykpyh9x/YeYHFDYdLDJ76/kIdxN43kLU4/hTrh9tMb1PZF+/4DshpAlRoQuL +o8I8avQm/A== +-----END CERTIFICATE----- diff --git a/test/IntegrationTests/platform-org.pfx b/test/IntegrationTests/platform-org.pfx new file mode 100644 index 00000000..6da835fb Binary files /dev/null and b/test/IntegrationTests/platform-org.pfx differ