Skip to content

Commit

Permalink
Latest features in storage (#46)
Browse files Browse the repository at this point in the history
* code builds

* fixed data repository update logic

* removed obsolete using

* Removed try/catch from controller

* added missing service

* bug fix

* downgraded package

* ignore props causing trouble

* Switch for properties in Datarepository

* fixed repository logic

* merged main

* back to dummy implementation in repository

---------

Co-authored-by: tba76 <[email protected]>
Co-authored-by: Bakken <[email protected]>
  • Loading branch information
3 people authored Jun 9, 2023
1 parent 4a86bba commit 4df2e32
Show file tree
Hide file tree
Showing 28 changed files with 1,773 additions and 148 deletions.
51 changes: 51 additions & 0 deletions src/Configuration/Storage/GeneralSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;

namespace Altinn.Platform.Storage.Configuration
{
/// <summary>
/// Configuration object used to hold general settings for the storage application.
/// </summary>
public class GeneralSettings
{
/// <summary>
/// Open Id Connect Well known endpoint. Related to JSON WEB token validation.
/// </summary>
public string OpenIdWellKnownEndpoint { get; set; }

/// <summary>
/// Hostname
/// </summary>
public string Hostname { get; set; }

/// <summary>
/// Name of the cookie for runtime
/// </summary>
public string RuntimeCookieName { get; set; }

/// <summary>
/// Gets or sets the URI for the SBL Bridge Authorization API.
/// </summary>
public Uri BridgeApiAuthorizationEndpoint { get; set; }

/// <summary>
/// Gets or sets the scopes for Instance Read.
/// </summary>
public List<string> InstanceReadScope { get; set; }

/// <summary>
/// Gets or sets the cache lifetime for text resources.
/// </summary>
public int TextResourceCacheLifeTimeInSeconds { get; set; }

/// <summary>
/// Gets or sets the cache lifetime for application title dictionary.
/// </summary>
public int AppTitleCacheLifeTimeInSeconds { get; set; }

/// <summary>
/// Gets or sets the cache lifetime for application metadata document.
/// </summary>
public int AppMetadataCacheLifeTimeInSeconds { get; set; }
}
}
5 changes: 5 additions & 0 deletions src/Constants/Authorization/AuthzConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ public static class AuthzConstants
/// </summary>
public const string POLICY_INSTANCE_COMPLETE = "InstanceComplete";

/// <summary>
/// Policy tag for authorizing client scope.
/// </summary>
public const string POLICY_INSTANCE_SIGN = "InstanceSign";

/// <summary>
/// Policy tag for authorizing client scope.
/// </summary>
Expand Down
201 changes: 121 additions & 80 deletions src/Controllers/Storage/DataController.cs

Large diffs are not rendered by default.

85 changes: 48 additions & 37 deletions src/Controllers/Storage/DataLockController.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using Altinn.Common.PEP.Interfaces;
#nullable enable
using Altinn.Platform.Storage.Authorization;
using Altinn.Platform.Storage.Helpers;
using Altinn.Platform.Storage.Interface.Models;
using Altinn.Platform.Storage.Repository;

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

Expand All @@ -16,24 +18,22 @@ public class DataLockController : ControllerBase
{
private readonly IInstanceRepository _instanceRepository;
private readonly IDataRepository _dataRepository;
private readonly AuthorizationHelper _authorizationHelper;
private readonly IAuthorization _authorizationService;

/// <summary>
/// Initializes a new instance of the <see cref="DataLockController"/> class
/// </summary>
/// <param name="instanceRepository">the instance repository</param>
/// <param name="dataRepository">the data repository handler</param>
/// <param name="pdp"></param>
/// <param name="authzLogger"></param>
/// <param name="authorizationService">the authorization service.</param>
public DataLockController(
IInstanceRepository instanceRepository,
IDataRepository dataRepository,
IPDP pdp,
ILogger<AuthorizationHelper> authzLogger)
IAuthorization authorizationService)
{
_instanceRepository = instanceRepository;
_dataRepository = dataRepository;
_authorizationHelper = new AuthorizationHelper(pdp, authzLogger);
_authorizationService = authorizationService;
}

/// <summary>
Expand All @@ -42,24 +42,43 @@ public DataLockController(
/// <param name="instanceOwnerPartyId">The party id of the instance owner.</param>
/// <param name="instanceGuid">The id of the instance that the data element is associated with.</param>
/// <param name="dataGuid">The id of the data element to delete.</param>
/// <returns></returns>
/// <returns>DataElement that was locked</returns>
[Authorize(Policy = AuthzConstants.POLICY_INSTANCE_WRITE)]
[HttpPut]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Produces("application/json")]
public async Task<ActionResult<DataElement>> Lock(int instanceOwnerPartyId, Guid instanceGuid, Guid dataGuid)
{
(DataElement dataElement, ActionResult errorMessage) = await GetDataElementAsync(instanceGuid, dataGuid);
if (dataElement == null)
(Instance? instance, ActionResult? instanceError) = await GetInstanceAsync(instanceGuid, instanceOwnerPartyId);
if (instance == null)
{
return instanceError!;
}

DataElement? dataElement = instance.Data.Find(d => d.Id == dataGuid.ToString());

if (dataElement?.Locked is true)
{
return errorMessage;
return Ok(dataElement);
}
dataElement.Locked = true;

DataElement updatedDataElement = await _dataRepository.Update(dataElement);
return Ok(updatedDataElement);
Dictionary<string, object> propertyList = new()
{
{ "/locked", true }
};

try
{
DataElement updatedDataElement = await _dataRepository.Update(instanceGuid, dataGuid, propertyList);
return Created(updatedDataElement.Id, updatedDataElement);
}
catch (RepositoryException e)
{
return e.StatusCodeSuggestion != null ? StatusCode((int)e.StatusCodeSuggestion) : StatusCode(500);
}
}

/// <summary>
Expand All @@ -68,7 +87,7 @@ public async Task<ActionResult<DataElement>> Lock(int instanceOwnerPartyId, Guid
/// <param name="instanceOwnerPartyId">The party id of the instance owner.</param>
/// <param name="instanceGuid">The id of the instance that the data element is associated with.</param>
/// <param name="dataGuid">The id of the data element to delete.</param>
/// <returns></returns>
/// <returns>DataElement that was unlocked</returns>
[Authorize]
[HttpDelete]
[ProducesResponseType(StatusCodes.Status200OK)]
Expand All @@ -78,30 +97,34 @@ public async Task<ActionResult<DataElement>> Lock(int instanceOwnerPartyId, Guid
[Produces("application/json")]
public async Task<ActionResult<DataElement>> Unlock(int instanceOwnerPartyId, Guid instanceGuid, Guid dataGuid)
{
(Instance instance, _) = await GetInstanceAsync(instanceGuid, instanceOwnerPartyId);
(Instance? instance, _) = await GetInstanceAsync(instanceGuid, instanceOwnerPartyId);
if (instance == null)
{
return Forbid();
}

bool authorized = await _authorizationHelper.AuthorizeAnyOfInstanceActions(HttpContext.User, instance, new List<string>() { "write", "unlock", "reject" });
bool authorized = await _authorizationService.AuthorizeAnyOfInstanceActions(instance, new List<string>() { "write", "unlock", "reject" });
if (!authorized)
{
return Forbid();
}

(DataElement dataElement, ActionResult errorMessage) = await GetDataElementAsync(instanceGuid, dataGuid);
if (dataElement == null)
Dictionary<string, object> propertyList = new()
{
return errorMessage;
{ "/locked", false }
};
try
{
DataElement updatedDataElement = await _dataRepository.Update(instanceGuid, dataGuid, propertyList);
return Ok(updatedDataElement);
}
catch (RepositoryException e)
{
return e.StatusCodeSuggestion != null ? StatusCode((int)e.StatusCodeSuggestion) : StatusCode(500);
}
dataElement.Locked = false;

DataElement updatedDataElement = await _dataRepository.Update(dataElement);
return Ok(updatedDataElement);
}

private async Task<(Instance Instance, ActionResult ErrorMessage)> GetInstanceAsync(Guid instanceGuid, int instanceOwnerPartyId)
private async Task<(Instance? Instance, ActionResult? ErrorMessage)> GetInstanceAsync(Guid instanceGuid, int instanceOwnerPartyId)
{
Instance instance = await _instanceRepository.GetOne(instanceOwnerPartyId, instanceGuid);

Expand All @@ -112,16 +135,4 @@ public async Task<ActionResult<DataElement>> Unlock(int instanceOwnerPartyId, Gu

return (instance, null);
}

private async Task<(DataElement DataElement, ActionResult ErrorMessage)> GetDataElementAsync(Guid instanceGuid, Guid dataGuid)
{
DataElement dataElement = await _dataRepository.Read(instanceGuid, dataGuid);

if (dataElement == null)
{
return (null, NotFound($"Unable to find any data element with id: {dataGuid}."));
}

return (dataElement, null);
}
}
52 changes: 52 additions & 0 deletions src/Controllers/Storage/SignController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;
using System.Threading.Tasks;
using Altinn.Platform.Storage.Helpers;
using Altinn.Platform.Storage.Interface.Models;
using Altinn.Platform.Storage.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace Altinn.Platform.Storage.Controllers
{
/// <summary>
/// Handles operations for signing all or a subset of dataelements for an instance
/// </summary>
[Route("storage/api/v1/instances")]
[ApiController]
public class SignController : ControllerBase
{
private readonly IInstanceService _instanceService;

/// <summary>
/// Initializes a new instance of the <see cref="SignController"/> class
/// </summary>
/// <param name="instanceService">A instance service with instance related business logic.</param>
public SignController(IInstanceService instanceService)
{
_instanceService = instanceService;
}

/// <summary>
/// Create signature document from listed data elements
/// </summary>
/// <param name="instanceOwnerPartyId">The party id of the instance owner.</param>
/// <param name="instanceGuid">The guid of the instance</param>
/// <param name="signRequest">Signrequest containing data element ids and sign status</param>
[Authorize(Policy = AuthzConstants.POLICY_INSTANCE_SIGN)]
[HttpPost("{instanceOwnerPartyId:int}/{instanceGuid:guid}/sign")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[Produces("application/json")]
public async Task<ActionResult> Sign([FromRoute] int instanceOwnerPartyId, [FromRoute] Guid instanceGuid, [FromBody] SignRequest signRequest)
{
if (string.IsNullOrEmpty(signRequest.Signee.UserId))
{
return Problem("The 'UserId' parameter must be defined for signee.", null, 400);
}

await _instanceService.CreateSignDocument(instanceOwnerPartyId, instanceGuid, signRequest, User.GetUserIdAsInt().Value);
return StatusCode(201, "SignDocument is created");
}
}
}
26 changes: 26 additions & 0 deletions src/Extensions/Storage/ContentDispositionHeaderValueExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;

namespace Altinn.Platform.Storage.Extensions
{
/// <summary>
/// Extensions to simplify the use of <see cref="ContentDispositionHeaderValue"/>.
/// </summary>
public static class ContentDispositionHeaderValueExtensions
{
/// <summary>
/// Obtain the filename value from FileNameStar or FileName if the FileNameStar property is empty.
/// Then remove any quotes and clean the filename with the AsFileName method.
/// </summary>
/// <param name="contentDisposition">The ContentDispositionHeaderValue object to get a filename from.</param>
/// <returns>A filename cleaned of any impurities.</returns>
public static string GetFilename(this ContentDispositionHeaderValue contentDisposition)
{
StringSegment filename = contentDisposition.FileNameStar.HasValue
? contentDisposition.FileNameStar
: contentDisposition.FileName;

return HeaderUtilities.RemoveQuotes(filename).Value.AsFileName(false);
}
}
}
39 changes: 39 additions & 0 deletions src/Extensions/Storage/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using System.IO;
using System.Linq;

namespace Altinn.Platform.Storage.Extensions
{
/// <summary>
/// Extensions to facilitate sanitization of string values
/// </summary>
public static class StringExtensions
{
/// <summary>
/// Sanitize the input as a file name.
/// </summary>
/// <param name="input">The input variable to be sanitized.</param>
/// <param name="throwExceptionOnInvalidCharacters">Throw exception instead of replacing invalid characters with '_'.</param>
/// <returns>A filename cleaned of any impurities.</returns>
public static string AsFileName(this string input, bool throwExceptionOnInvalidCharacters = true)
{
if (string.IsNullOrWhiteSpace(input))
{
return input;
}

char[] illegalFileNameCharacters = Path.GetInvalidFileNameChars();
if (throwExceptionOnInvalidCharacters)
{
if (illegalFileNameCharacters.Any(ic => input.Any(i => ic == i)))
{
throw new ArgumentOutOfRangeException(nameof(input));
}

return input;
}

return illegalFileNameCharacters.Aggregate(input, (current, c) => current.Replace(c, '_'));
}
}
}
Loading

0 comments on commit 4df2e32

Please sign in to comment.