Skip to content

Commit

Permalink
V8 feature/304 support custom button (#346)
Browse files Browse the repository at this point in the history
* Make multi decision request for actions

* Prepare code for useraction in task

* add controller endpoint

* trigger userdefined task actions

* Add authorization check

* Add some tests

* controller tests for actions

* added more tests for actionscontroller

* Change how errors are returned

* Add tests for MultiDecisionHelper and add xml comments

* Add tests for AuthorizationClient

* Fix some codesmells and add some more tests

* Fix style error

* Action Controller return 404 if no implementations found
Fix comments from review

* Apply suggestions from code review

Co-authored-by: Ivar Nesje <[email protected]>

* Apply suggestion from code review

Remove IUserActionService and rename UserActionFactory to UserActionService
Add JsonPropertyName to classes returned through the ActionsController

* Remove ValidationGroup from IUserAction

* Apply suggestions from code review

Co-authored-by: Ivar Nesje <[email protected]>

* Remove the need for NullUserAction

---------

Co-authored-by: Ivar Nesje <[email protected]>
  • Loading branch information
tjololo and ivarne authored Nov 27, 2023
1 parent 7bc0886 commit 81296a3
Show file tree
Hide file tree
Showing 53 changed files with 3,220 additions and 236 deletions.
129 changes: 129 additions & 0 deletions src/Altinn.App.Api/Controllers/ActionsController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#nullable enable
using Altinn.App.Api.Infrastructure.Filters;
using Altinn.App.Api.Models;
using Altinn.App.Core.Extensions;
using Altinn.App.Core.Features.Action;
using Altinn.App.Core.Internal.Exceptions;
using Altinn.App.Core.Internal.Instances;
using Altinn.App.Core.Models;
using Altinn.App.Core.Models.UserAction;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using IAuthorizationService = Altinn.App.Core.Internal.Auth.IAuthorizationService;

namespace Altinn.App.Api.Controllers;

/// <summary>
/// Controller that handles actions performed by users
/// </summary>
[AutoValidateAntiforgeryTokenIfAuthCookie]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[Route("{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/actions")]
public class ActionsController : ControllerBase
{
private readonly IAuthorizationService _authorization;
private readonly IInstanceClient _instanceClient;
private readonly UserActionService _userActionService;

/// <summary>
/// Create new instance of the <see cref="ActionsController"/> class
/// </summary>
/// <param name="authorization">The authorization service</param>
/// <param name="instanceClient">The instance client</param>
/// <param name="userActionService">The user action service</param>
public ActionsController(IAuthorizationService authorization, IInstanceClient instanceClient, UserActionService userActionService)
{
_authorization = authorization;
_instanceClient = instanceClient;
_userActionService = userActionService;
}

/// <summary>
/// Perform a task action on an instance
/// </summary>
/// <param name="org">unique identfier of the organisation responsible for the app</param>
/// <param name="app">application identifier which is unique within an organisation</param>
/// <param name="instanceOwnerPartyId">unique id of the party that this the owner of the instance</param>
/// <param name="instanceGuid">unique id to identify the instance</param>
/// <param name="actionRequest">user action request</param>
/// <returns><see cref="UserActionResponse"/></returns>
[HttpPost]
[Authorize]
[ProducesResponseType(typeof(UserActionResponse), 200)]
[ProducesResponseType(typeof(ProblemDetails), 400)]
[ProducesResponseType(401)]
public async Task<ActionResult<UserActionResponse>> Perform(
[FromRoute] string org,
[FromRoute] string app,
[FromRoute] int instanceOwnerPartyId,
[FromRoute] Guid instanceGuid,
[FromBody] UserActionRequest actionRequest)
{
var action = actionRequest.Action;
if (action == null)
{
return new BadRequestObjectResult(new ProblemDetails()
{
Instance = instanceGuid.ToString(),
Status = 400,
Title = "Action is missing",
Detail = "Action is missing in the request"
});
}

var instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid);

if (instance?.Process == null)
{
return Conflict($"Process is not started.");
}

if (instance.Process.Ended.HasValue)
{
return Conflict($"Process is ended.");
}

var userId = HttpContext.User.GetUserIdAsInt();
if (userId == null)
{
return Unauthorized();
}

var authorized = await _authorization.AuthorizeAction(new AppIdentifier(org, app), new InstanceIdentifier(instanceOwnerPartyId, instanceGuid), HttpContext.User, action, instance.Process?.CurrentTask?.ElementId);
if (!authorized)
{
return Forbid();
}

UserActionContext userActionContext = new UserActionContext(instance, userId.Value, actionRequest.ButtonId, actionRequest.Metadata);
var actionHandler = _userActionService.GetActionHandler(action);
if (actionHandler == null)
{
return new NotFoundObjectResult(new UserActionResponse()
{
Error = new ActionError()
{
Code = "ActionNotFound",
Message = $"Action handler with id {action} not found",
}
});
}

var result = await actionHandler.HandleAction(userActionContext);

if (!result.Success)
{
return new BadRequestObjectResult(new UserActionResponse()
{
FrontendActions = result.FrontendActions,
Error = result.Error
});
}

return new OkObjectResult(new UserActionResponse()
{
FrontendActions = result.FrontendActions,
UpdatedDataModels = result.UpdatedDataModels
});
}
}
29 changes: 17 additions & 12 deletions src/Altinn.App.Api/Controllers/ProcessController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public async Task<ActionResult<AppProcessState>> GetProcessState(
try
{
Instance instance = await _instanceClient.GetInstance(app, org, instanceOwnerPartyId, instanceGuid);
AppProcessState appProcessState = await ConvertAndAuthorizeActions(org, app, instanceOwnerPartyId, instanceGuid, instance.Process);
AppProcessState appProcessState = await ConvertAndAuthorizeActions(instance, instance.Process);

return Ok(appProcessState);
}
Expand Down Expand Up @@ -136,7 +136,7 @@ public async Task<ActionResult<AppProcessState>> StartProcess(
return Conflict(result.ErrorMessage);
}

AppProcessState appProcessState = await ConvertAndAuthorizeActions(org, app, instanceOwnerPartyId, instanceGuid, result.ProcessStateChange?.NewProcessState);
AppProcessState appProcessState = await ConvertAndAuthorizeActions(instance, result.ProcessStateChange?.NewProcessState);
return Ok(appProcessState);
}
catch (PlatformHttpException e)
Expand Down Expand Up @@ -302,7 +302,7 @@ public async Task<ActionResult<AppProcessState>> NextElement(
}
}

AppProcessState appProcessState = await ConvertAndAuthorizeActions(org, app, instanceOwnerPartyId, instanceGuid, result.ProcessStateChange?.NewProcessState);
AppProcessState appProcessState = await ConvertAndAuthorizeActions(instance, result.ProcessStateChange?.NewProcessState);

return Ok(appProcessState);
}
Expand Down Expand Up @@ -426,7 +426,7 @@ public async Task<ActionResult<AppProcessState>> CompleteProcess(
return StatusCode(500, $"More than {counter} iterations detected in process. Possible loop. Fix app process definition!");
}

AppProcessState appProcessState = await ConvertAndAuthorizeActions(org, app, instanceOwnerPartyId, instanceGuid, instance.Process);
AppProcessState appProcessState = await ConvertAndAuthorizeActions(instance, instance.Process);
return Ok(appProcessState);
}

Expand Down Expand Up @@ -456,7 +456,7 @@ public async Task<ActionResult> GetProcessHistory(
}
}

private async Task<AppProcessState> ConvertAndAuthorizeActions(string org, string app, int instanceOwnerPartyId, Guid instanceGuid, ProcessState? processState)
private async Task<AppProcessState> ConvertAndAuthorizeActions(Instance instance, ProcessState? processState)
{
AppProcessState appProcessState = new AppProcessState(processState);
if (appProcessState.CurrentTask?.ElementId != null)
Expand All @@ -465,13 +465,13 @@ private async Task<AppProcessState> ConvertAndAuthorizeActions(string org, strin
if (flowElement is ProcessTask processTask)
{
appProcessState.CurrentTask.Actions = new Dictionary<string, bool>();
foreach (AltinnAction action in processTask.ExtensionElements?.TaskExtension?.AltinnActions ?? new List<AltinnAction>())
{
appProcessState.CurrentTask.Actions.Add(action.Value, await AuthorizeAction(action.Value, org, app, instanceOwnerPartyId, instanceGuid, flowElement.Id));
}

appProcessState.CurrentTask.HasWriteAccess = await AuthorizeAction("write", org, app, instanceOwnerPartyId, instanceGuid, flowElement.Id);
appProcessState.CurrentTask.HasReadAccess = await AuthorizeAction("read", org, app, instanceOwnerPartyId, instanceGuid, flowElement.Id);
List<AltinnAction> actions = new List<AltinnAction>() { new("read"), new("write") };
actions.AddRange(processTask.ExtensionElements?.TaskExtension?.AltinnActions ?? new List<AltinnAction>());
var authDecisions = await AuthorizeActions(actions, instance);
appProcessState.CurrentTask.Actions = authDecisions.Where(a => a.ActionType == ActionType.ProcessAction).ToDictionary(a => a.Id, a => a.Authorized);
appProcessState.CurrentTask.HasReadAccess = authDecisions.Single(a => a.Id == "read").Authorized;
appProcessState.CurrentTask.HasWriteAccess = authDecisions.Single(a => a.Id == "write").Authorized;
appProcessState.CurrentTask.UserActions = authDecisions;
}
}

Expand Down Expand Up @@ -499,6 +499,11 @@ private async Task<bool> AuthorizeAction(string action, string org, string app,
return await _authorization.AuthorizeAction(new AppIdentifier(org, app), new InstanceIdentifier(instanceOwnerPartyId, instanceGuid), HttpContext.User, action, taskId);
}

private async Task<List<UserAction>> AuthorizeActions(List<AltinnAction> actions, Instance instance)
{
return await _authorization.AuthorizeActions(instance, HttpContext.User, actions);
}

private static string EnsureActionNotTaskType(string actionOrTaskType)
{
switch (actionOrTaskType)
Expand Down
28 changes: 28 additions & 0 deletions src/Altinn.App.Api/Models/UserActionRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#nullable enable
using System.Text.Json.Serialization;

namespace Altinn.App.Api.Models;

/// <summary>
/// Request model for user action
/// </summary>
public class UserActionRequest
{
/// <summary>
/// Action performed
/// </summary>
[JsonPropertyName("action")]
public string? Action { get; set; }

/// <summary>
/// The id of the button that was clicked
/// </summary>
[JsonPropertyName("buttonId")]
public string? ButtonId { get; set; }

/// <summary>
/// Additional metadata for the action
/// </summary>
[JsonPropertyName("metadata")]
public Dictionary<string, string>? Metadata { get; set; }
}
29 changes: 29 additions & 0 deletions src/Altinn.App.Api/Models/UserActionResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#nullable enable
using System.Text.Json.Serialization;
using Altinn.App.Core.Models.UserAction;

namespace Altinn.App.Api.Models;

/// <summary>
/// Response object from action endpoint
/// </summary>
public class UserActionResponse
{
/// <summary>
/// Data models that have been updated
/// </summary>
[JsonPropertyName("updatedDataModels")]
public Dictionary<string, object?>? UpdatedDataModels { get; set; }

/// <summary>
/// Actions frontend should perform after action has been performed backend
/// </summary>
[JsonPropertyName("frontendActions")]
public List<FrontendAction>? FrontendActions { get; set; }

/// <summary>
/// Validation issues that occured when processing action
/// </summary>
[JsonPropertyName("error")]
public ActionError? Error { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,7 @@ private static void AddProcessServices(IServiceCollection services)

private static void AddActionServices(IServiceCollection services)
{
services.TryAddTransient<UserActionFactory>();
services.AddTransient<IUserAction, NullUserAction>();
services.TryAddTransient<UserActionService>();
services.AddTransient<IUserAction, SigningUserAction>();
services.AddHttpClient<ISignClient, SignClient>();
services.AddTransientUserActionAuthorizerForActionInAllTasks<UniqueSignatureAuthorizer>("sign");
Expand Down
18 changes: 0 additions & 18 deletions src/Altinn.App.Core/Features/Action/NullUserAction.cs

This file was deleted.

10 changes: 7 additions & 3 deletions src/Altinn.App.Core/Features/Action/SigningUserAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public SigningUserAction(IProcessReader processReader, ILogger<SigningUserAction
/// <inheritdoc />
/// <exception cref="Altinn.App.Core.Helpers.PlatformHttpException"></exception>
/// <exception cref="Altinn.App.Core.Internal.App.ApplicationConfigException"></exception>
public async Task<bool> HandleAction(UserActionContext context)
public async Task<UserActionResult> HandleAction(UserActionContext context)
{
if (_processReader.GetFlowElement(context.Instance.Process.CurrentTask.ElementId) is ProcessTask currentTask)
{
Expand All @@ -53,13 +53,17 @@ public async Task<bool> HandleAction(UserActionContext context)
{
SignatureContext signatureContext = new SignatureContext(new InstanceIdentifier(context.Instance), currentTask.ExtensionElements?.TaskExtension?.SignatureConfiguration?.SignatureDataType!, await GetSignee(context.UserId), connectedDataElements);
await _signClient.SignDataElements(signatureContext);
return true;
return UserActionResult.SuccessResult();
}

throw new ApplicationConfigException("Missing configuration for signing. Check that the task has a signature configuration and that the data types to sign are defined.");
}

return false;
return UserActionResult.FailureResult(new ActionError()
{
Code = "NoProcessTask",
Message = "Current task is not a process task."
});
}

private static List<DataElementSignature> GetDataElementSignatures(List<DataElement> dataElements, List<string> dataTypesToSign)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
using Altinn.App.Core.Internal;

namespace Altinn.App.Core.Features.Action;

/// <summary>
/// Factory class for resolving <see cref="IUserAction"/> implementations
/// based on the id of the action.
/// </summary>
public class UserActionFactory
public class UserActionService
{
private readonly IEnumerable<IUserAction> _actionHandlers;

/// <summary>
/// Initializes a new instance of the <see cref="UserActionFactory"/> class.
/// Initializes a new instance of the <see cref="UserActionService"/> class.
/// </summary>
/// <param name="actionHandlers">The list of action handlers to choose from.</param>
public UserActionFactory(IEnumerable<IUserAction> actionHandlers)
public UserActionService(IEnumerable<IUserAction> actionHandlers)
{
_actionHandlers = actionHandlers;
}
Expand All @@ -21,14 +23,14 @@ public UserActionFactory(IEnumerable<IUserAction> actionHandlers)
/// Find the implementation of <see cref="IUserAction"/> based on the actionId
/// </summary>
/// <param name="actionId">The id of the action to handle.</param>
/// <returns>The first implementation of <see cref="IUserAction"/> that matches the actionId. If no match <see cref="NullUserAction"/> is returned</returns>
public IUserAction GetActionHandler(string? actionId)
/// <returns>The first implementation of <see cref="IUserAction"/> that matches the actionId. If no match null is returned</returns>
public IUserAction? GetActionHandler(string? actionId)
{
if (actionId != null)
{
return _actionHandlers.Where(ah => ah.Id.Equals(actionId, StringComparison.OrdinalIgnoreCase)).FirstOrDefault(new NullUserAction());
return _actionHandlers.FirstOrDefault(ah => ah.Id.Equals(actionId, StringComparison.OrdinalIgnoreCase));
}

return new NullUserAction();
return null;
}
}
2 changes: 1 addition & 1 deletion src/Altinn.App.Core/Features/IUserAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ public interface IUserAction
/// </summary>
/// <param name="context">The user action context</param>
/// <returns>If the handling of the action was a success</returns>
Task<bool> HandleAction(UserActionContext context);
Task<UserActionResult> HandleAction(UserActionContext context);
}
Loading

0 comments on commit 81296a3

Please sign in to comment.